I wrote this function that draw a grid of triangles:
def create_triangles(side_length):
result = []
half_width = int(side_length / 2)
# height = int(side_length * math.sqrt(3) / 2)
height = side_length
max_width = 15 * side_length
max_height = 10 * height
for i in range(0, max_height, height):
if (i / height) % 2 == 0:
for j in range(0, max_width-half_width, half_width):
if j % side_length == 0:
triangle = (i-height/2, j-half_width, i+height/2, j, i-height/2, j+half_width)
else:
triangle = (i-height/2, j, i+height/2, j+half_width, i+height/2, j-half_width)
result.append(triangle)
else:
for j in range(half_width, max_width, half_width):
if j % side_length == 0:
triangle = (i-height/2, j-2*half_width, i+height/2, j-half_width+2, i-height/2, j)
else:
triangle = (i-height/2, j-half_width, i+height/2, j, i+height/2, j-2*half_width)
result.append(triangle)
return result
The current output is this:
As you can see some triangles are misaligned but I don't understand why.
As mentioned in the comments, floating points give you incorrect results; You want to make sure that the shared points representing the vertices of two adjacent triangles are concurrent. A simple approach is to reduce the points coordinates to ints, and organize the calculations so errors do not add up.
In the following examples, the misalignment is corrected, every triangle on the canvas is represented by a polygon, and individually drawn; each triangle can therefore be referenced when moused over, or addressed via an index, or a mapping (not implemented).
import tkinter as tk
import math
WIDTH, HEIGHT = 500, 500
class Point:
"""convenience for point arithmetic
"""
def __init__(self, x, y):
self.x, self.y = x, y
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
def __iter__(self):
yield self.x
yield self.y
def tile_with_triangles(canvas, side_length=50):
"""tiles the entire surface of the canvas with triangular polygons
"""
triangle_height = int(side_length * math.sqrt(3) / 2)
half_side = side_length // 2
p0 = Point(0, 0)
p1 = Point(0, side_length)
p2 = Point(triangle_height, half_side)
for idx, x in enumerate(range(-triangle_height, WIDTH+1, triangle_height)):
for y in range(-side_length, HEIGHT+1, side_length):
y += half_side * (idx%2 + 1)
offset = Point(x, y)
pa, pb, pc = p0 + offset, p1 + offset,p2 + offset
canvas.create_polygon(*pa, *pb, *pc, outline='black', fill='', activefill='red')
p2 = Point(-triangle_height, half_side) # flip the model triangle
for idx, x in enumerate(range(-triangle_height, WIDTH+triangle_height+1, triangle_height)):
for y in range(-side_length, HEIGHT+1, side_length):
y += half_side * (idx%2 + 1)
offset = Point(x, y)
pa, pb, pc = p0 + offset, p1 + offset,p2 + offset
canvas.create_polygon(*pa, *pb, *pc, outline='black', fill='', activefill='blue')
root = tk.Tk()
canvas = tk.Canvas(root, width=WIDTH, height=HEIGHT, bg='cyan')
canvas.pack()
tile_with_triangles(canvas) #, side_length=10)
root.mainloop()
I added an active fill property that will change the colors of each triangle when you mouse over.
Related
I have made a sand simulation in tkinter. Only tkinter. No pygame, no matplotlib, none of that. It works great, it looks great, but nothing in life is perfect.
Minor Problem #1: When I hold down and let the sand fall, the column where the cursor is stays the same color. I haven't been able to track this down yet.
Minor Problem #2: When I create happy little piles of sand, the sides seem to build up from the bottom rather that fall from the top. I suspect this is a rendering issue, but I also haven't found the reason for this.
That's all the problems I've noticed, but if you see any others feel free to let me know.
Code:
from tkinter import *
from random import choice, random
from copy import deepcopy
from colorsys import hsv_to_rgb
CELLSIZE = 30
AIR = 0
WALL = 1
SAND = 2
BG = '#cef'
SANDCOLOR = (45, 45, 86)
WALLCOLOR = (224, 37, 34)
TARGETFPS = 100
def randomColor(h, s, v):
h, s, v = (h / 360), s / 100, v / 100
s += (random() - 0.5) * 0.1
v += (random() - 0.5) * 0.1
if s < 0: s = 0
if s > 1: s = 1
if v < 0: v = 0
if v > 1: v = 1
r, g, b = [round(i * 255) for i in hsv_to_rgb(h, s, v)]
return '#%02x%02x%02x' % (r, g, b)
class App:
def __init__(self):
global WIDTH, HEIGHT, SANDCOLOR, WALLCOLOR
self.master = Tk()
self.master.title('Sand Simulation')
self.master.resizable(0, 0)
self.master.attributes('-fullscreen', True)
WIDTH = self.master.winfo_screenwidth() // CELLSIZE
HEIGHT = self.master.winfo_screenheight() // CELLSIZE
Width, Height = WIDTH * CELLSIZE, HEIGHT * CELLSIZE
self.canvas = Canvas(self.master, width=Width, height=Height, bg=BG, highlightthickness=0)
self.canvas.pack()
self.map = [[AIR] * WIDTH for i in range(HEIGHT)]
self.colors = [[BG] * WIDTH for i in range(HEIGHT)]
self.positions = []
for x in range(WIDTH):
for y in range(HEIGHT):
self.positions.append([x, y])
self.positions.reverse()
self.dragging, self.dragX, self.dragY = False, 0, 0
self.canvas.bind('<Button-1>', self.mouseDown)
self.canvas.bind('<B1-Motion>', self.mouseDrag)
self.canvas.bind('<ButtonRelease-1>', self.mouseUp)
## self.images = [PhotoImage(file='images/sandButton.png'), PhotoImage(file='images/sandButtonActivated.png'),
## PhotoImage(file='images/wallButton.png'), PhotoImage(file='images/wallButtonActivated.png')]
self.images = [PhotoImage().blank(), PhotoImage().blank(), PhotoImage().blank(), PhotoImage().blank()]
self.sandButton = self.canvas.create_image(125, 125, anchor='center', image=self.images[1])
self.wallButton = self.canvas.create_image(125, 325, anchor='center', image=self.images[2])
self.drawingMode = 'SAND'
self.master.after(round(1 / TARGETFPS * 1000), self.frame)
self.master.mainloop()
def swapBlocks(self, x1, y1, x2, y2):
block1 = self.map[y1][x1]
color1 = self.colors[y1][x1]
self.map[y1][x1] = self.map[y2][x2]
self.colors[y1][x1] = self.colors[y2][x2]
self.map[y2][x2] = block1
self.colors[y2][x2] = color1
def mouseDown(self, event):
if 50 < event.x < 200 and 50 < event.y < 200:
self.drawingMode = 'SAND'
self.canvas.itemconfig(self.sandButton, image=self.images[1])
self.canvas.itemconfig(self.wallButton, image=self.images[2])
elif 50 < event.x < 200 and 250 < event.y < 400:
self.drawingMode = 'WALL'
self.canvas.itemconfig(self.sandButton, image=self.images[0])
self.canvas.itemconfig(self.wallButton, image=self.images[3])
else:
self.dragging = True
self.dragX = event.x // CELLSIZE
self.dragY = event.y // CELLSIZE
if self.dragX > WIDTH - 1: self.dragX = WIDTH - 1
if self.dragX < 0: self.dragX = 0
if self.dragY > HEIGHT - 1: self.dragY = HEIGHT - 1
if self.dragY < 0: self.dragY = 0
def mouseDrag(self, event):
self.dragX = event.x // CELLSIZE
self.dragY = event.y // CELLSIZE
if self.dragX > WIDTH - 1: self.dragX = WIDTH - 1
if self.dragX < 0: self.dragX = 0
if self.dragY > HEIGHT - 1: self.dragY = HEIGHT - 1
if self.dragY < 0: self.dragY = 0
def mouseUp(self, event):
self.dragging = False
def updateParticles(self):
if self.dragging:
color = choice(['red', 'white', 'blue'])
if self.drawingMode == 'SAND':
self.map[self.dragY][self.dragX] = SAND
self.colors[self.dragY][self.dragX] = randomColor(SANDCOLOR[0], SANDCOLOR[1], SANDCOLOR[2])
elif self.drawingMode == 'WALL':
self.map[self.dragY][self.dragX] = WALL
self.colors[self.dragY][self.dragX] = randomColor(WALLCOLOR[0], WALLCOLOR[1], WALLCOLOR[2])
for block in self.positions:
x, y = block
block = self.map[y][x]
if block == SAND:
if y == HEIGHT - 1:
below = WALL
else:
below = self.map[y + 1][x]
if below == AIR:
self.swapBlocks(x, y, x, y + 1)
else:
left, right, belowLeft, belowRight = AIR, AIR, AIR, AIR
if y == HEIGHT - 1:
belowLeft, belowRight = WALL, WALL
else:
if x == 0:
belowLeft = WALL
left = WALL
else:
belowLeft = self.map[y + 1][x - 1]
left = self.map[y][x - 1]
if x == WIDTH - 1:
belowRight = WALL
right = WALL
else:
belowRight = self.map[y + 1][x + 1]
right = self.map[y][x + 1]
if belowLeft == AIR and belowRight == AIR:
if choice([True, False]):
if left == AIR:
self.swapBlocks(x, y, x - 1, y + 1)
else:
if right == AIR:
self.swapBlocks(x, y, x + 1, y + 1)
else:
if belowLeft == AIR and left == AIR:
self.swapBlocks(x, y, x - 1, y + 1)
if belowRight == AIR and right == AIR:
self.swapBlocks(x, y, x + 1, y + 1)
def renderMap(self, previousMap):
for block in self.positions:
x, y = block
previousBlock = previousMap[y][x]
currentBlock = self.map[y][x]
x1, y1 = x * CELLSIZE, y * CELLSIZE
x2, y2 = x1 + CELLSIZE, y1 + CELLSIZE
if previousBlock == AIR and currentBlock != AIR:
if currentBlock == WALL: color = self.colors[y][x]
if currentBlock == SAND: color = self.colors[y][x]
rect = self.canvas.create_rectangle(x1, y1, x2, y2, outline='', fill=color)
self.canvas.tag_lower(rect)
if previousBlock != AIR and currentBlock == AIR:
blockAtPosition = self.canvas.find_enclosed(x1, y1, x2, y2)
self.canvas.delete(blockAtPosition)
if previousBlock != AIR and currentBlock != AIR and previousBlock != currentBlock:
blockAtPosition = self.canvas.find_enclosed(x1, y1, x2, y2)
self.canvas.delete(blockAtPosition)
if currentBlock == WALL: color = self.colors[y][x]
if currentBlock == SAND: color = self.colors[y][x]
rect = self.canvas.create_rectangle(x1, y1, x2, y2, outline='', fill=color)
self.canvas.tag_lower(rect)
self.canvas.update()
def frame(self):
previousMap = deepcopy(self.map)
self.updateParticles()
self.renderMap(previousMap)
self.master.after(round(1 / TARGETFPS * 1000), self.frame)
def main():
app = App()
if __name__ == '__main__':
main()
Please help me fix any bugs, glitches, etc...
Problem #1 is caused by the fact that your rendering function doesn't create a new rectangle if both the old and new block types are sand. As the sand falls in a column, the first bit of sand causes rectangles to be created with its color, and because another bit of sand always falls in its place on the next frame, it just stays that color. You could compare old and new colors to see which blocks need refreshing, or store which blocks changed.
Problem #2 is related to the order of your positions. You're inserting [x,y] positions into the positions array from the left to the right, one column at a time, each column from top to bottom, and then reversing the result, which gives you an ordering of bottom to top per column with the columns from right to left. So when you iterate through the positions, you're essentially sweeping from right to left as you process the blocks, so any block that falls to the left is going to be reprocessed in the same update, and as it keeps falling to the left it will keep being reprocessed, until it settles, and all of that happened in one frame. So particles will seem to fall to the left instantly.
I bet you don't have this problem with particles falling to the right, which is why on the right side of your image you have some diagonal bands of color: problem #1 is happening there too any time two sand blocks fall to the right in sequence.
You can fix problem #2 by reordering your width and height loops when you set up self.positions. Since blocks always move down one row when they are moved, iterating from bottom to top will always update each particle only once. If you ever introduce anything that makes particles move up, you'll need a better solution.
I am writing a script to store movements over a hexgrid using Tkinter. As part of this I want to use a mouse-click on a Tkinter canvas to first identify the click location, and then draw a line between this point and the location previously clicked.
Generally this works, except that after I've drawn a line, it become an object that qualifies for future calls off the find_closest method. This means I can still draw lines between points, but selecting the underlying Hex in the Hexgrid over times becomes nearly impossible. I was wondering if someone could help me find a solution to exclude particular objects (lines) from the find_closest method.
edit: I hope this code example is minimal enough.
import tkinter
from tkinter import *
from math import radians, cos, sin, sqrt
class App:
def __init__(self, parent):
self.parent = parent
self.c1 = Canvas(self.parent, width=int(1.5*340), height=int(1.5*270), bg='white')
self.c1.grid(column=0, row=0, sticky='nsew')
self.clickcount = 0
self.clicks = [(0,0)]
self.startx = int(20*1.5)
self.starty = int(20*1.5)
self.radius = int(20*1.5) # length of a side
self.hexagons = []
self.columns = 10
self.initGrid(self.startx, self.starty, self.radius, self.columns)
self.c1.bind("<Button-1>", self.click)
def initGrid(self, x, y, radius, cols):
"""
2d grid of hexagons
"""
radius = radius
column = 0
for j in range(cols):
startx = x
starty = y
for i in range(6):
breadth = column * (1.5 * radius)
if column % 2 == 0:
offset = 0
else:
offset = radius * sqrt(3) / 2
self.draw(startx + breadth, starty + offset, radius)
starty = starty + 2 * (radius * sqrt(3) / 2)
column = column + 1
def draw(self, x, y, radius):
start_x = x
start_y = y
angle = 60
coords = []
for i in range(6):
end_x = start_x + radius * cos(radians(angle * i))
end_y = start_y + radius * sin(radians(angle * i))
coords.append([start_x, start_y])
start_x = end_x
start_y = end_y
hex = self.c1.create_polygon(coords[0][0], coords[0][1], coords[1][0], coords[1][1], coords[2][0],
coords[2][1], coords[3][0], coords[3][1], coords[4][0], coords[4][1],
coords[5][0], coords[5][1], fill='black')
self.hexagons.append(hex)
def click(self, evt):
self.clickcount = self.clickcount + 1
x, y = evt.x, evt.y
tuple_alfa = (evt.x, evt.y)
self.clicks.append(tuple_alfa)
if self.clickcount >= 2:
start = self.clicks[self.clickcount - 1]
startx = start[0]
starty = start[1]
self.c1.create_line(evt.x, evt.y, startx, starty, fill='white')
clicked = self.c1.find_closest(x, y)[0]
print(clicked)
root = tkinter.Tk()
App(root)
root.mainloop()
I need to create a widget that is used to pick a time. QTimeEdit widget doesn't seem intuitive or a good design. So I decided to create a time picker similar to the time picker in smartphones.
I managed to create the clock and click that makes the pointer (something similar to the pointer in the image) move to the currently clicked position (note: it's not perfect, it still looks bad). I would like to have help with making the inner clock
Here is my code:
from PyQt5 import QtWidgets, QtGui, QtCore
import math, sys
class ClockWidget(QtWidgets.QWidget): # I want to be able to reuse this class for other programs also, so please don't hard code values of the list, start and end
def __init__(self, start, end, lst=[], *args, **kwargs):
super(ClockWidget, self).__init__(*args, **kwargs)
self.lst = lst
if not self.lst:
self.lst = [*range(start, end)]
self.index_start = 0 # tune this to move the letters in the circle
self.pointer_angles_multiplier = 9 # just setting the default values
self.current = None
self.rects = []
#property
def index_start(self):
return self._index_start
#index_start.setter
def index_start(self, index):
self._index_start = index
def paintEvent(self, event):
self.rects = []
painter = QtGui.QPainter(self)
pen = QtGui.QPen()
pen.setColor(QtCore.Qt.red)
pen.setWidth(2)
painter.setPen(pen)
x, y = self.rect().x(), self.rect().y()
width, height = self.rect().width(), self.rect().height()
painter.drawEllipse(x, y, x + width, x + height)
s, t, equal_angles, radius = self.angle_calc()
radius -= 30
pen.setColor(QtCore.Qt.green)
pen.setWidth(2)
painter.setPen(pen)
""" pointer angle helps in determining to which position the pointer should be drawn"""
self.pointer_x, self.pointer_y = s + ((radius-30) * math.cos(self.pointer_angles_multiplier * equal_angles)), t \
+ ((radius-30) * math.sin(self.pointer_angles_multiplier * equal_angles))
""" The pendulum like pointer """
painter.drawLine(QtCore.QPointF(s, t), QtCore.QPointF(self.pointer_x, self.pointer_y))
painter.drawEllipse(QtCore.QRectF(QtCore.QPointF(self.pointer_x - 20, self.pointer_y - 40),
QtCore.QPointF(self.pointer_x + 30, self.pointer_y + 10)))
pen.setColor(QtCore.Qt.blue)
pen.setWidth(3)
font = self.font()
font.setPointSize(14)
painter.setFont(font)
painter.setPen(pen)
""" Drawing the number around the circle formula y = t + radius * cos(a)
y = s + radius * sin(a) where angle is in radians (s, t) are the mid point of the circle """
for index, char in enumerate(self.lst, start=self.index_start):
angle = equal_angles * index
y = t + radius * math.sin(angle)
x = s + radius * math.cos(angle)
# print(f"Add: {add_x}, index: {index}; char: {char}")
rect = QtCore.QRectF(x - 30, y - 40, x + 60, y) # clickable point
self.rects.append([index, char, rect]) # appends index, letter, rect
painter.setPen(QtCore.Qt.blue)
painter.drawRect(rect) # helps in visualizing the points where the click can received
print(f"Rect: {rect}; char: {char}")
painter.setPen(QtCore.Qt.red)
points = QtCore.QPointF(x, y)
painter.drawText(points, str(char))
def mousePressEvent(self, event):
for x in self.rects:
index, char, rect = x
if event.button() & QtCore.Qt.LeftButton and rect.contains(event.pos()):
self.pointer_angles_multiplier = index
self.current = char
self.update()
break
def angle_calc(self):
"""
This will simply return (midpoints of circle, divides a circle into the len(list) and return the
angle in radians, radius)
"""
return ((self.rect().width() - self.rect().x()) / 2, (self.rect().height() - self.rect().y()) / 2,
(360 / len(self.lst)) * (math.pi / 180), (self.rect().width() / 2))
def resizeEvent(self, event: QtGui.QResizeEvent):
"""This is supposed to maintain a Square aspect ratio on widget resizing but doesn't work
correctly as you will see when executing"""
if event.size().width() > event.size().height():
self.resize(event.size().height(), event.size().width())
else:
self.resize(event.size().width(), event.size().width())
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
message = ClockWidget(1, 13)
message.index_start = 10
message.show()
sys.exit(app.exec())
The Output:
The blue rectangles represent the clickable region. I would be glad if you could also, make the pointer move to the closest number when clicked inside the clock (Not just move the pointer when the clicked inside the blue region)
There is one more problem in my code, that is the numbers are not evenly spaced from the outer circle. (like the number 12 is closer to the outer circle than the number 6)
Disclaimer: I will not explain the cause of the error but the code I provide I think should give a clear explanation of the errors.
The logic is to calculate the position of the centers of each small circle, and use the exinscribed rectangle to take it as a base to draw the text and check if the point where you click is close to the texts.
from functools import cached_property
import math
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class ClockWidget(QtWidgets.QWidget):
L = 12
r = 40.0
DELTA_ANGLE = 2 * math.pi / L
current_index = 9
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
R = min(self.rect().width(), self.rect().height()) / 2
margin = 4
Rect = QtCore.QRectF(0, 0, 2 * R - margin, 2 * R - margin)
Rect.moveCenter(self.rect().center())
painter.setBrush(QtGui.QColor("gray"))
painter.drawEllipse(Rect)
rect = QtCore.QRectF(0, 0, self.r, self.r)
if 0 <= self.current_index < 12:
c = self.center_by_index(self.current_index)
rect.moveCenter(c)
pen = QtGui.QPen(QtGui.QColor("red"))
pen.setWidth(5)
painter.setPen(pen)
painter.drawLine(c, self.rect().center())
painter.setBrush(QtGui.QColor("red"))
painter.drawEllipse(rect)
for i in range(self.L):
j = (i + 2) % self.L + 1
c = self.center_by_index(i)
rect.moveCenter(c)
painter.setPen(QtGui.QColor("white"))
painter.drawText(rect, QtCore.Qt.AlignCenter, str(j))
def center_by_index(self, index):
R = min(self.rect().width(), self.rect().height()) / 2
angle = self.DELTA_ANGLE * index
center = self.rect().center()
return center + (R - self.r) * QtCore.QPointF(math.cos(angle), math.sin(angle))
def index_by_click(self, pos):
for i in range(self.L):
c = self.center_by_index(i)
delta = QtGui.QVector2D(pos).distanceToPoint(QtGui.QVector2D(c))
if delta < self.r:
return i
return -1
def mousePressEvent(self, event):
i = self.index_by_click(event.pos())
if i >= 0:
self.current_index = i
self.update()
#property
def hour(self):
return (self.current_index + 2) % self.L + 1
def minumumSizeHint(self):
return QtCore.QSize(100, 100)
def main():
app = QtWidgets.QApplication(sys.argv)
view = ClockWidget()
view.resize(400, 400)
view.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
I'm attempting to create a triangle tessellation like the following in Python:
All I've gotten is Sierpensky's triangle. I assume it'd use some of the same code.
import turtle as t
import math
import colorsys
t.hideturtle()
t.speed(0)
t.tracer(0,0)
h = 0
def draw_tri(x,y,size):
global h
t.up()
t.goto(x,y)
t.seth(0)
t.down()
color = colorsys.hsv_to_rgb(h,1,1)
h += 0.1
t.color(color)
t.left(120)
t.fd(size)
t.left(120)
t.fd(size)
t.end_fill()
def draw_s(x,y,size,n):
if n == 0:
draw_tri(x,y,size)
return
draw_s(x,y,size/2,n-1)
draw_s(x+size/2,y,size/2,n-1)
draw_s(x+size/4,y+size*math.sqrt(3)/4,size/2,n-1)
draw_s(-300,-250,600,6)
t.update()
There are various approaches; the following example generates all line segments prior to directing the turtle to draw them on the canvas.
import turtle as t
import math
WIDTH, HEIGHT = 800, 800
OFFSET = -WIDTH // 2, -HEIGHT // 2
class Point:
"""convenience for point arithmetic
"""
def __init__(self, x=0, y=0):
self.x, self.y = x, y
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
def __iter__(self):
yield self.x
yield self.y
def get_line_segments(side_length=50):
"""calculates the coordinates of all vertices
organizes them by line segment
stores the segments in a container and returns it
"""
triangle_height = int(side_length * math.sqrt(3) / 2)
half_side = side_length // 2
p0 = Point(0, 0)
p1 = Point(0, side_length)
p2 = Point(triangle_height, half_side)
segments = []
for idx, x in enumerate(range(-triangle_height, WIDTH+1, triangle_height)):
for y in range(-side_length, HEIGHT+1, side_length):
y += half_side * (idx%2 + 1)
offset = Point(x, y)
pa, pb, pc = p0 + offset, p1 + offset,p2 + offset
segments += [[pa, pb], [pb, pc], [pc, pa]]
return segments
def draw_segment(segment):
p0, p1 = segment
p0, p1 = p0 + offset, p1 + offset
t.penup()
t.goto(p0)
t.pendown()
t.goto(p1)
def draw_tiling():
for segment in get_line_segments():
draw_segment(segment)
t.hideturtle()
t.speed(0)
t.tracer(0,0)
offset = Point(*OFFSET)
draw_tiling()
t.update()
t.exitonclick()
If you want to see how the tiling is traced, you can replace the following lines:
# t.hideturtle()
t.speed(1)
# t.tracer(0, 0)
and enlarge the canvas screen with your mouse to see the boundary of the tiling (I made it overlap the standard size of the window)
As #ReblochonMasque notes, there are multiple approaches to the problem. Here's one I worked out to use as little turtle code as possible to solve the problem:
from turtle import Screen, Turtle
TRIANGLE_SIDE = 60
TRIANGLE_HEIGHT = TRIANGLE_SIDE * 3 ** 0.5 / 2
CURSOR_SIZE = 20
screen = Screen()
width = TRIANGLE_SIDE * (screen.window_width() // TRIANGLE_SIDE)
height = TRIANGLE_HEIGHT * (screen.window_height() // TRIANGLE_HEIGHT)
diagonal = width + height
turtle = Turtle('square', visible=False)
turtle.shapesize(diagonal / CURSOR_SIZE, 1 / CURSOR_SIZE)
turtle.penup()
turtle.sety(height/2)
turtle.setheading(270)
turtle = turtle.clone()
turtle.setx(width/2)
turtle.setheading(210)
turtle = turtle.clone()
turtle.setx(-width/2)
turtle.setheading(330)
for _ in range(int(diagonal / TRIANGLE_HEIGHT)):
for turtle in screen.turtles():
turtle.forward(TRIANGLE_HEIGHT)
turtle.stamp()
screen.exitonclick()
It probably could use optimizing but it gets the job done. And it's fun to watch...
Does Spyder have a good way to detect what is causing a crash?
All the changes we made are in the ArrayPQ class and the node class, the other code works without our changes.
import math
import numpy as np
import numpy.random as rand
import numpy.linalg as linalg
from tkinter import Tk, Frame, Canvas
class ArrayPQ:
def __init__(self, num_balls):
self.collisionNodes = np.array([])
self.pastCollisions = np.zeros(num_balls)
def parent(self, i):
return (i-1) // 2
def right(self, i):
return (i*2) + 1
def left(self, i):
return (i*2) + 2
def insert(self, i, j, value, num_collisions_i, num_collisions_j):
self.pastCollisions[i] = num_collisions_i
self.pastCollisions[j] = num_collisions_j
self.collisionNodes = np.append(self.collisionNodes, node.nodeinit(self, i, j, value, num_collisions_i, num_collisions_j))
self.heapify1(i)
def heapify1(self, i):
l = self.left(i)
r = self.right(i)
end = len(self.collisionNodes)
top = i
if l < end and self.collisionNodes(i) < self.collisionNodes(l):
top = l
if r < end and self.collisionNodes(top) < self.collisionNodes(r):
top = r
if max != i:
self.swap(i, top)
self.heapify1(top)
def heapify2(self, i):
if self.right(i) + len(self.collisionNodes)
def delete(self, i):
self.swap(self, 0, (len(self.collisionNodes) -1))
del self.collisionNodes[len(self.collisionNodes)-1]
self.heapify1(i)
def swap(self, i, j):
self.collisionNodes[i], self.collisionNodes[j] = self.collisionNodes[j],
self.collisionNodes[i]
def get_next(self):
temp = self.collisionsNodes[0]
return temp.BBi, temp.BBj, temp.T, temp.CCi, temp.CCj
class node:
def nodeinit(self, Bi, Bj, T, Ci, Cj):
self.BBi = Bi
self.BBj = Bj
self.TT = T
self.CCi = Ci
self.CCj = Cj
class Painter:
#__init__ performs the construction of a Painter object
def __init__(self, root, scale=500, border=5, refresh_speed=5,
filename='balls.txt', min_radius=5, max_radius=10, num_balls=20):
#width and height are used to set the simulation borders
width = scale + border
height = scale + border
self.time = 0
#setup will set up the necessary graphics window and the ball list
self.setup(root, width, height, border, refresh_speed)
#Check the input parameter 'filename' to load predetermined simulation
#otherwise set up the default simulation
if filename is None:
self.init_balls(max_radius, min_radius, num_balls)
self.num_balls = num_balls
else:
self.num_balls = self.read_balls(scale, filename)
#Create the priority data structure
self.PQ = ArrayPQ(self.num_balls)
#Initialize all possible collision times
self.init_ball_collision_times()
self.init_wall_collision_times()
#draw will draw the graphics to the window
self.draw()
#refresh is a loop method intended to create animations
self.refresh()
#A blank return indicates the end of the function
return
#setup creates the window to display the graphics along with the red border
#of the simulation
def setup(self, root, width, height, border, refresh_speed):
# Draw frame etc
self.app_frame = Frame(root)
self.app_frame.pack()
self.canvas = Canvas(self.app_frame, width = width, height = height)
self.canvas_size = (int(self.canvas.cget('width')),
int(self.canvas.cget('height')))
self.canvas.pack()
self.refresh_speed = refresh_speed
# Work area
self.min_x = border
self.max_x = width - border
self.min_y = border
self.max_y = height - border
#create array to hold the n number of balls
self.balls = []
self.ball_handles = dict()
return
def read_balls(self, scale, filename):
f = open(filename)
num_balls = int(f.readline().strip())
for l in f:
ll = l.strip().split(" ")
x = scale*float(ll[0])
y = scale*float(ll[1])
vx = scale*float(ll[2])
vy = scale*float(ll[3])
radius = scale*float(ll[4])
mass = float(ll[5])
r = int(ll[6])
g = int(ll[7])
b = int(ll[8])
tk_rgb = "#%02x%02x%02x" % (r, g, b)
new_ball = Ball(radius, x, y, vx, vy, mass, tk_rgb)
self.balls.append(new_ball)
return num_balls
def init_balls(self, max_radius, min_radius, num_balls):
for i in np.arange(num_balls):
while(True):
radius = (max_radius - min_radius) * rand.random_sample() +
min_radius
ball_min_x = self.min_x + radius
ball_max_x = self.max_x - radius
x = (ball_max_x - ball_min_x)*rand.random_sample() + ball_min_x
ball_min_y = self.min_y + radius
ball_max_y = self.max_y - radius
y = (ball_max_y - ball_min_y)*rand.random_sample() + ball_min_y
vx = rand.random_sample()
vy = rand.random_sample()
mass = 1.0 # rand.random_sample()
new_ball = Ball(radius, x, y, vx, vy, mass)
if not new_ball.check_overlap(self.balls):
self.balls.append(new_ball)
break
#init_wall_collision_times will set all of the balls' minimum collision time
def init_wall_collision_times(self):
for i in np.arange(len(self.balls)):
bi = self.balls[i]
tix = bi.horizontal_wall_collision_time(self.min_x, self.max_x)
tiy = bi.vertical_wall_collision_time(self.min_y, self.max_y)
self.PQ.insert(i, -1, tix + self.time, self.balls[i].count, -1)
self.PQ.insert(-1, i, tiy + self.time, -1, self.balls[i].count)
return
#init_ball_collision_times will set all of the balls' minimum collision time
#with all other balls and store that time within the ith and jth index of
#PQ.ball_collision_time
def init_ball_collision_times(self):
for i in np.arange(self.num_balls):
bi = self.balls[i]
for j in np.arange(i+1, self.num_balls):
bj = self.balls[j]
tij = bi.ball_collision_time(bj)
self.PQ.insert(i, j, tij + self.time, self.balls[i].count,
self.balls[j].count)
# self.ball_collision_times[i][j] = tij
# self.ball_collision_times[j][i] = tij
return
#walls (horizontal and vertical) and all other balls within the PQ array
def update_collision_times(self, i):
bi = self.balls[i]
tix = bi.horizontal_wall_collision_time(self.min_x, self.max_x)
tiy = bi.vertical_wall_collision_time(self.min_y, self.max_y)
self.PQ.insert(i, -1, tix + self.time,self.balls[i].count, -1)
self.PQ.insert(-1, i, tiy + self.time, -1, self.balls[i].count)
for j in np.arange(self.num_balls):
bj = self.balls[j]
tij = bi.ball_collision_time(bj) + self.time
if i > j:
self.PQ.insert(j, i,
tij,self.balls[j].count,self.balls[i].count)
else:
self.PQ.insert(i, j, tij,self.balls[i].count,
self.balls[j].count)
return
#draw will draw the borders and all balls within self.balls
def draw(self):
#Draw walls
self.canvas.create_line((self.min_x, self.min_y), (self.min_x,
self.max_y), fill = "red")
self.canvas.create_line((self.min_x, self.min_y), (self.max_x,
self.min_y), fill = "red")
self.canvas.create_line((self.min_x, self.max_y), (self.max_x,
self.max_y), fill = "red")
self.canvas.create_line((self.max_x, self.min_y), (self.max_x,
self.max_y), fill = "red")
#Draw balls
for b in self.balls:
obj = self.canvas.create_oval(b.x - b.radius, b.y - b.radius, b.x +
b.radius, b.y + b.radius, outline=b.tk_rgb, fill=b.tk_rgb)
self.ball_handles[b] = obj
self.canvas.update()
#refresh is called to update the state of the simulation
#-each refresh call can be considered one iteration of the simulation
def refresh(self):
#get the next collision
i, j, t, num_collisions_i, num_collision_j = self.PQ.get_next()
#gather the current collisions of the ith and jth ball
current_collisions_i = self.balls[i].count
current_collisions_j = self.balls[j].count
#Check the difference in time between the predicted collision time and
#the current time stamp of the simulation
delta = t - self.time
#If the difference is greater than 1, then just move the balls
if delta > 1.0:
# cap delta to 1.0
for bi in self.balls:
bi.move()
self.canvas.move(self.ball_handles[bi], bi.vx, bi.vy)
self.time += 1.0
#Otherwise a collision has occurred
else:
#Move all balls
for bi in self.balls:
bi.move(delta)
self.canvas.move(self.ball_handles[bi], bi.vx*delta,
bi.vy*delta)
#increment the simulation time stamp
self.time += delta
#Delete the top element within the Priority Queue
self.PQ.delete()
#if i is -1 then this indicates a collision with a vertical wall
#also this if statement checks if the number of collisions recorded
#when the collision returned by PQ.get_next() is equal to the
#number of collisions within the jth ball
#this acts as a test to check if the collision is still valid
if i == -1 and num_collision_j == current_collisions_j:
#compute what happens from the vertical wall collision
self.balls[j].collide_with_vertical_wall()
#update collision times for the jth ball
self.update_collision_times(j)
#if j is -1 then this indicates a collision a horizontal wall
#while also checking if the number of collisions match
#to see if the collision is valid
elif j == -1 and num_collisions_i == current_collisions_i:
#compute what happens from the horizontal wall collision
self.balls[i].collide_with_horizontal_wall()
#update collision times for the ith ball
self.update_collision_times(i)
elif num_collision_j == current_collisions_j and num_collisions_i ==
current_collisions_i:
#Execute collision across the ith and jth ball
self.balls[i].collide_with_ball(self.balls[j])
#update collision times for both the ith and jth ball
self.update_collision_times(i)
self.update_collision_times(j)
#update the canvas to draw the new locations of each ball
self.canvas.update()
self.canvas.after(self.refresh_speed, self.refresh)
def __init__(self, radius, x, y, vx, vy, mass, tk_rgb="#000000"):
self.radius = radius
self.x = x
self.y = y
self.vx = vx
self.vy = vy
self.mass = mass
self.tk_rgb = tk_rgb
#since this ball was just initialized, it hasn't had any collisions yet
self.count = 0
return
#move changes the displacement of the ball by the velocity
def move(self, dt=1.0):
self.x += self.vx*dt
self.y += self.vy*dt
return
#check_overlap checks if this ball is overlapping with any other
#ball, it is used to see if a collision has occurred
def check_overlap(self, others):
for b in others:
min_dist = b.radius + self.radius
center_dist = math.sqrt((b.x - self.x)*(b.x - self.x) + \
(b.y - self.y)*(b.y - self.y))
if center_dist < min_dist:
return True
return False
#collide_with_ball computes collision, changing the Ball's velocity
#as well as the other ball's velocity
def collide_with_ball(self, other):
dv_x = other.vx - self.vx
dv_y = other.vy - self.vy
dr_x = other.x - self.x
dr_y = other.y - self.y
sigma = self.radius + other.radius
dv_dr = dv_x * dr_x + dv_y * dr_y
J = 2.0 * self.mass * other.mass * dv_dr/((self.mass +
other.mass)*sigma)
Jx = J * dr_x/sigma
Jy = J * dr_y/sigma
self.vx += Jx/self.mass
self.vy += Jy/self.mass
other.vx -= Jx/other.mass
other.vy -= Jy/other.mass
#Increment the collision count for both balls
self.count += 1
other.count += 1
return
# Compute when an instance of Ball collides with the given Ball other
# Return the timestamp that this will occur
def ball_collision_time(self, other):
dr_x = other.x - self.x
dr_y = other.y - self.y
if dr_x == 0 and dr_y == 0:
return float('inf')
dv_x = other.vx - self.vx
dv_y = other.vy - self.vy
dv_dr = dv_x * dr_x + dv_y * dr_y
if dv_dr > 0:
return float('inf')
dv_dv = dv_x * dv_x + dv_y * dv_y
dr_dr = dr_x * dr_x + dr_y * dr_y
sigma = self.radius + other.radius
d = dv_dr * dv_dr - dv_dv * (dr_dr - sigma * sigma)
# No solution
if d < 0 or dv_dv == 0:
return float('inf')
return - (dv_dr + np.sqrt(d))/dv_dv
#collide_with_horizontal_wall executes the change in the Ball's
#velocity when colliding with a horizontal wall
def collide_with_horizontal_wall(self):
self.vx = -self.vx
self.count += 1
return
#collide_with_vertical_wall executes the change in the Ball's
#velocity when colliding with a vertical wall
def collide_with_vertical_wall(self):
self.vy = -self.vy
self.count += 1
return
# Compute when the instance of Ball collides with a horizontal wall
# Return the time stamp that this will occur
def horizontal_wall_collision_time(self, min_x, max_x):
if self.vx < 0:
# x + delta_t * vx = min_x + radius
return (min_x + self.radius - self.x)/(1.0*self.vx)
if self.vx > 0:
# x + delta_t * vx = max_x - radius
return (max_x - self.radius - self.x)/(1.0*self.vx)
return float('inf')
# Compute when the instance of Ball collides with a vertical wall
# Return the time stamp that this will occur
# Inputs of min_y and max_y
def vertical_wall_collision_time(self, min_y, max_y):
if self.vy < 0:
# y + delta_t * vy = min_y + radius
return (min_y + self.radius - self.y)/(1.0*self.vy)
if self.vy > 0:
# y + delta_t * vy = max_y - radius
return (max_y - self.radius - self.y)/(1.0*self.vy)
return float('inf')
#show_stats will print out the Ball's data
#specifically the radius, position, velocity, mass, and color
def show_stats(self):
print("radius: %f"%self.radius)
print("position: %f, %f"%(self.x, self.y))
print("velocity: %f, %f"%(self.vx, self.vy))
print("mass: %f"%self.mass)
print("rgb: %s"%self.tk_rgb)
return
#Main script
if __name__ == "__main__":
#Set the parameters for the graphics window and simulation
scale = 800
border = 5
#Set radius range for all balls
max_radius = 20
min_radius = 5
#set number of balls
num_balls = 10
#set refresh rate
refresh_speed = 5
rand.seed(12394)
#create the graphics object
root = Tk()
#Create the painter object
p = Painter(root, scale, border, refresh_speed, None, min_radius,max_radius,
num_balls)
#If you have the commented out files in your directory then
#uncomment them to see what they do
#p = Painter(root, scale, border, refresh_speed, 'wallbouncing.txt')
#p = Painter(root, scale, border, refresh_speed, 'p10.txt')
#p = Painter(root, scale, border, refresh_speed, 'billiards4.txt')
#p = Painter(root, scale, border, refresh_speed, 'squeeze.txt')
#run the Painter main loop function (calls refresh many times)
root.mainloop()