particle animation background for kivy - python

I want to put a particle animation in the background screen of my software, something like the link below, but for Python and kivymd
codepen.io/JulianLaval/pen/KpLXOO
I know this may be difficult or impossible for kivymd right now but if anyone has an idea please let me know

Yes! This is absolutely possible (everything is possible in Kivy). Check out the code below:
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.graphics import Line, Color
from random import randint
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.properties import ListProperty
from math import sin, cos
class ParticleMesh(Widget):
points = ListProperty()
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.direction = []
self.point_number = 50
Clock.schedule_once(lambda dt: self.plot_points(), 2)
def plot_points(self):
for _ in range(self.point_number):
x = randint(0, self.width)
y = randint(0, self.height)
self.points.extend([x, y])
self.direction.append(randint(0, 359))
Clock.schedule_interval(self.update_positions, 0)
def draw_lines(self):
self.canvas.after.clear()
with self.canvas.after:
for i in range(0, len(self.points), 2):
for j in range(i + 2, len(self.points), 2):
d = self.distance_between_points(self.points[i], self.points[i + 1], self.points[j],
self.points[j + 1])
if d > 120:
continue
color = d / 120
Color(rgba=[color, color, color, 1])
Line(points=[self.points[i], self.points[i + 1], self.points[j], self.points[j + 1]])
def update_positions(self, *args):
step = 1
for i, j in zip(range(0, len(self.points), 2), range(len(self.direction))):
theta = self.direction[j]
self.points[i] += step * cos(theta)
self.points[i + 1] += step * sin(theta)
if self.off_screen(self.points[i], self.points[i + 1]):
self.direction[j] = 90 + self.direction[j]
self.draw_lines()
#staticmethod
def distance_between_points(x1, y1, x2, y2):
return ((x1 - x2) ** 2 + (y1 - y2) ** 2) ** 0.5
def off_screen(self, x, y):
return x < -5 or x > self.width + 5 or y < -5 or y > self.height + 5
kv = """
FloatLayout:
canvas.before:
Color:
rgba: 1, 1, 1, 1
Rectangle:
size: self.size
pos: self.pos
ParticleMesh:
canvas:
Color:
rgba: 0, 0, 0, 1
Point:
points: self.points
pointsize: 2
"""
class MeshApp(App):
def build(self):
return Builder.load_string(kv)
if __name__ == '__main__':
MeshApp().run()
This code will create the following (this is just a screenshot - if you run the app, the points move about):
First plot_points is called, which creates an array of points randomly placed on the screen. A random direction for each point is also created. This direction is represented by an angle between 0 and 359. On the completion of this function, a Clock object is instantiated and calls update_positions every frame.
The update_positions moves the particles by one pixel in the angle specified by directions. If the position of the particle is greater than the screen the direction is reversed.
Finally draw_lines is called. This function first clears all existing lines then draws new ones. If the points are at a distance greater than 120 pixels no line is created. However, if they are closer than 120 pixels a line is drawn such that the closer the two points are, the darker the line will be.
You can always increase or decrease the number of points on the screen by changing the self.point_number property.
I will leave it up to you to change the colour of the points and the background - I don't think that should be too hard.

Related

Get location of pixel upon click in Kivy

I am using Kivy to design an application to draw an n-sided polygon over a live video stream to demarcate regions of interest. The problem I have is that Kivy provides coordinates w.r.t to the whole window and not just the image. What I would like is to have the location of the pixel (in x and y cords) clicked. I have looked at to_local() method but it didn't make much sense, neither did it produce desired results. Any help would be appreciated, below is the MRE.
from kivy.app import App
from kivy.uix.image import Image
from kivy.uix.boxlayout import BoxLayout
from kivy.graphics import Color, Ellipse, Line
from random import random
class ImageView(Image):
def on_touch_down(self, touch):
##
# This provides touch cords for the entire app and not just the location of the pixel clicked#
print("Touch Cords", touch.x, touch.y)
##
color = (random(), 1, 1)
with self.canvas:
Color(*color, mode='hsv')
d = 30.
Ellipse(pos=(touch.x - d / 2, touch.y - d / 2), size=(d, d))
touch.ud['line'] = Line(points=(touch.x, touch.y))
def on_touch_move(self, touch):
touch.ud['line'].points += [touch.x, touch.y]
class DMSApp(App):
def build(self):
imagewidget = ImageView(source="/home/red/Downloads/600.png")
imagewidget.size_hint = (1, .5)
imagewidget.pos_hint = {"top": 1}
layout = BoxLayout(size_hint=(1, 1))
layout.add_widget(imagewidget)
return layout
if __name__ == '__main__':
DMSApp().run()
You can calculate the coordinates of the touch relative to the lower left corner of the actual image that appears in the GUI. Those coordinates can then be scaled to the actual size of the source image to get a reasonable estimate of the actual pixel coordinates within the source. Here is a modified version of your in_touch_down() method that does that (only minimal testing performed):
def on_touch_down(self, touch):
if not self.collide_point(*touch.pos):
return super(ImageView, self).on_touch_down(touch)
lr_space = (self.width - self.norm_image_size[0]) / 2 # empty space in Image widget left and right of actual image
tb_space = (self.height - self.norm_image_size[1]) / 2 # empty space in Image widget above and below actual image
print('lr_space =', lr_space, ', tb_space =', tb_space)
print("Touch Cords", touch.x, touch.y)
print('Size of image within ImageView widget:', self.norm_image_size)
print('ImageView widget:, pos:', self.pos, ', size:', self.size)
print('image extents in x:', self.x + lr_space, self.right - lr_space)
print('image extents in y:', self.y + tb_space, self.top - tb_space)
pixel_x = touch.x - lr_space - self.x # x coordinate of touch measured from lower left of actual image
pixel_y = touch.y - tb_space - self.y # y coordinate of touch measured from lower left of actual image
if pixel_x < 0 or pixel_y < 0:
print('clicked outside of image\n')
return True
elif pixel_x > self.norm_image_size[0] or \
pixel_y > self.norm_image_size[1]:
print('clicked outside of image\n')
return True
else:
print('clicked inside image, coords:', pixel_x, pixel_y)
# scale coordinates to actual pixels of the Image source
print('actual pixel coords:',
pixel_x * self.texture_size[0] / self.norm_image_size[0],
pixel_y * self.texture_size[1] / self.norm_image_size[1], '\n')
color = (random(), 1, 1)
with self.canvas:
Color(*color, mode='hsv')
d = 30.
Ellipse(pos=(touch.x - d / 2, touch.y - d / 2), size=(d, d))
touch.ud['line'] = Line(points=(touch.x, touch.y))
return True

Can't zoom in with gluLookAt(), only zoom out?

I'm trying to implement an orbital camera in PyOpenGL Legacy and am trying to make it zoom in and out (so, go forward and back).
This is the relevant bit of code:
def update(self):
self.pos = self.orbitalPos()
print(f"self.pos length {self.magnitude(self.pos)}")
self.look = self.pos * (-1.0)
gluLookAt(*(self.pos), *(self.look), *(self.up))
def moveForward(self):
print("in moveForward")
direction = glm.normalize(self.look)
speed = direction * 0.001
self.pos = self.pos + speed
self.radius = self.magnitude(self.pos)
print(self.radius)
def moveBack(self):
print("in moveBack")
direction = glm.normalize(self.look)
speed = direction * (-0.001)
self.pos = self.pos + speed
self.radius = self.magnitude(self.pos)
print(self.radius)
def orbitalPos(self):
return glm.vec3(
self.radius * math.sin(math.radians(self.phi)) * math.sin(math.radians(self.theta)),
self.radius * math.cos(math.radians(self.phi)),
self.radius * math.sin(math.radians(self.phi)) * math.cos(math.radians(self.theta))
)
def magnitude(self, vec):
return math.sqrt(vec.x ** 2 + vec.y ** 2 + vec.z ** 2)
Up is (0,1,0). In the main function, I call ether moveForward() or moveBack() and then call update().
This is just zooming in and out so phi and theta stay the same, and the only thing that changes is the radius, which is decreasing with moveForward() and increasing with moveBack(), as it should. But in the pygame window the object just keeps getting further away no matter what key I press.
I'm fairly sure I'm borking something in the algebra but I don't know what. Any ideas?
gluLookAt not only sets a matrix, but defines a matrix and multiplies the current matrix (which can be the matrix of the last frame) with the new look at matrix. Therefore you have to load the identity matrix with glLoadIdentity before calling gluLookAt:
def update(self):
self.pos = self.orbitalPos()
print(f"self.pos length {self.magnitude(self.pos)}")
self.look = self.pos * (-1.0)
glLoadIdentity()
gluLookAt(*(self.pos), *(self.look), *(self.up))

Creating Custom Time Picker Widget

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()

Python Turtle Write Value in Containing Box

I want to be able to create some turtles which display values by subclassing turtle.Turtle.
These turtles should display their value as text centered in their own shape. I also want to be able to position the turtles with accuracy, so setting/determining their width and height relative to a given font size is important.
This is my attempt so far:
I think this answer is relevant: How to know the pixel size of a specific text on turtle graphics in python? but it is quite old, and the bounding box it draws around the text is not in the correct position using python 3.8.
import turtle
FONT_SIZE = 32
class Tile(turtle.Turtle):
def __init__(self):
super().__init__(shape="square")
self.penup()
def show_value(self, val):
self.write(val, font=("Arial", FONT_SIZE, "bold"), align="center")
screen = turtle.Screen()
vals = [5, 7, 8, 2]
for i in range(len(vals)):
tile = Tile()
tile_size = (FONT_SIZE / 20)
tile.shapesize(tile_size)
tile.fillcolor("red" if i % 2 == 0 else "blue")
tile.setx(i * FONT_SIZE)
tile.show_value(vals[i])
turtle.done()
It would be very helpful to have Turtle Objects containing text such
as integer values, which can be used to display a variety of puzzles
and games, and can have their own click handlers attached.
Here's the rub, and the (two) reason(s) that approaches using stamp() as suggested in other answers won't work. First, you can't click on a hidden turtle:
from turtle import *
def doit(x, y):
print("Just do it!")
yertle = Turtle()
# comment out the following line if you want `onlick()` to work
yertle.hideturtle()
yertle.shape('square')
yertle.stamp()
yertle.onclick(doit)
done()
Stamps are not clickable entities. Second, you can't even click on a turtle that's behind ink left by this, or another, turtle:
from turtle import *
def doit(x, y):
print("Just do it!")
yertle = Turtle()
yertle.shape('square')
yertle.fillcolor('white')
yertle.onclick(doit)
myrtle = Turtle()
myrtle.shape('turtle')
myrtle.penup()
myrtle.sety(-16)
# comment out the following line if you want `onlick()` to work
myrtle.write('X', align='center', font=('Courier', 32, 'bold'))
myrtle.goto(100, 100) # move myrtle out of the way of clicking
done()
If you click on the letter 'X', nothing happens unless you manage to hit a portion of the square just beyond the letter. My belief is that although we think of the 'X' as dead ink over our live turtle, at the tkinter level they are both similar, possibly both capable of receiving events, so one obscures the click on the other.
So how can we do this? The approach I'm going to use is make a tile a turtle with an image where the images are generate by writing onto bitmaps:
tileset.py
from turtle import Screen, Turtle, Shape
from PIL import Image, ImageDraw, ImageFont, ImageTk
DEFAULT_FONT_FILE = "/Library/Fonts/Courier New Bold.ttf" # adjust for your system
DEFAULT_POINT_SIZE = 32
DEFAULT_OUTLINE_SIZE = 1
DEFAULT_OUTLINE_COLOR = 'black'
DEFAULT_BACKGROUND_COLOR = 'white'
class Tile(Turtle):
def __init__(self, shape, size):
super().__init__(shape)
self.penup()
self.size = size
def tile_size(self):
return self.size
class TileSet():
def __init__(self, font_file=DEFAULT_FONT_FILE, point_size=DEFAULT_POINT_SIZE, background_color=DEFAULT_BACKGROUND_COLOR, outline_size=DEFAULT_OUTLINE_SIZE, outline_color=DEFAULT_OUTLINE_COLOR):
self.font = ImageFont.truetype(font_file, point_size)
self.image = Image.new("RGB", (point_size, point_size))
self.draw = ImageDraw.Draw(self.image)
self.background_color = background_color
self.outline_size = outline_size
self.outline_color = outline_color
def register_image(self, string):
width, height = self.draw.textsize(string, font=self.font)
image = Image.new("RGB", (width + self.outline_size*2, height + self.outline_size*2), self.background_color)
draw = ImageDraw.Draw(image)
tile_size = (width + self.outline_size, height + self.outline_size)
draw.rectangle([(0, 0), tile_size], outline=self.outline_color)
draw.text((0, 0), string, font=self.font, fill="#000000")
photo_image = ImageTk.PhotoImage(image)
shape = Shape("image", photo_image)
Screen()._shapes[string] = shape # underpinning, not published API
return tile_size
def make_tile(self, string):
tile_size = self.register_image(string)
return Tile(string, tile_size)
Other than its image, the only differences a Tile instance has from a Turtle instance is an extra method tile_size() to return its width and height as generic turtles can't do this in the case of images. And a tile's pen is up at the start, instead of down.
I've drawn on a couple of SO questions and answers:
Dump characters (glyphs) from TrueType font (TTF) into bitmaps
How do you set a turtle's shape to a PIL image
And while I'm at it, this answer has been updated to be more system independent:
How to know the pixel size of a specific text on turtle graphics in python?
To demonstrate how my tile sets work, here's the well-know 15 puzzle implemented using them. It creates two tile sets, one with white backgrounds and one with red (pink) backgrounds:
from tileset import TileSet
from turtle import Screen
from functools import partial
from random import shuffle
SIZE = 4
OFFSETS = [(-1, 0), (0, -1), (1, 0), (0, 1)]
def slide(tile, row, col, x, y):
tile.onclick(None) # disable handler inside handler
for dy, dx in OFFSETS:
try:
if row + dy >= 0 <= col + dx and matrix[row + dy][col + dx] == None:
matrix[row][col] = None
row, col = row + dy, col + dx
matrix[row][col] = tile
width, height = tile.tile_size()
x, y = tile.position()
tile.setposition(x + dx * width, y - dy * height)
break
except IndexError:
pass
tile.onclick(partial(slide, tile, row, col))
screen = Screen()
matrix = [[None for _ in range(SIZE)] for _ in range(SIZE)]
white_tiles = TileSet(background_color='white')
red_tiles = TileSet(background_color='pink')
tiles = []
parity = True
for number in range(1, SIZE * SIZE):
string = str(number).rjust(2)
tiles.append(white_tiles.make_tile(string) if parity else red_tiles.make_tile(string))
parity = not parity
if number % SIZE == 0:
parity = not parity
shuffle(tiles)
width, height = tiles[0].tile_size()
offset_width, offset_height = width * 1.5, height * 1.5
for row in range(SIZE):
for col in range(SIZE):
if row == SIZE - 1 == col:
break
tile = tiles.pop(0)
width, height = tile.tile_size()
tile.goto(col * width - offset_width, offset_height - row * height)
tile.onclick(partial(slide, tile, row, col))
matrix[row][col] = tile
screen.mainloop()
If you click on a number tile that's next to the blank space, it will move into the blank space, otherwise nothing happens. This code doesn't guarantee a solvable puzzle -- half won't be solvable due to the random shuffle. It's just a demonstration, the fine details of it, and the tiles themselves, are left to you.

"tkinter.TclError: invalid command name" error after calling root.destroy()

I am in the process of learning tkinter on Python 3.X. I am writing a simple program which will get one or more balls (tkinter ovals) bouncing round a rectangular court (tkinter root window with a canvas and rectangle drawn on it).
I want to be able to terminate the program cleanly by pressing the q key, and have managed to bind the key to the root and fire the callback function when a key is pressed, which then calls root.destroy().
However, I'm still getting errors of the form _tkinter.TclError: invalid command name ".140625086752360" when I do so. This is driving me crazy. What am I doing wrong?
from tkinter import *
import time
import numpy
class Ball:
def bates():
"""
Generator for the sequential index number used in order to
identify the various balls.
"""
k = 0
while True:
yield k
k += 1
index = bates()
def __init__(self, parent, x, y, v=0.0, angle=0.0, accel=0.0, radius=10, border=2):
self.parent = parent # The parent Canvas widget
self.index = next(Ball.index) # Fortunately, I have all my feathers individually numbered, for just such an eventuality
self.x = x # X-coordinate (-1.0 .. 1.0)
self.y = y # Y-coordinate (-1.0 .. 1.0)
self.radius = radius # Radius (0.0 .. 1.0)
self.v = v # Velocity
self.theta = angle # Angle
self.accel = accel # Acceleration per tick
self.border = border # Border thickness (integer)
self.widget = self.parent.canvas.create_oval(
self.px() - self.pr(), self.py() - self.pr(),
self.px() + self.pr(), self.py() + self.pr(),
fill = "red", width=self.border, outline="black")
def __repr__(self):
return "[{}] x={:.4f} y={:.4f} v={:.4f} a={:.4f} r={:.4f} t={}, px={} py={} pr={}".format(
self.index, self.x, self.y, self.v, self.theta,
self.radius, self.border, self.px(), self.py(), self.pr())
def pr(self):
"""
Converts a radius from the range 0.0 .. 1.0 to window coordinates
based on the width and height of the window
"""
assert self.radius > 0.0 and self.radius <= 1.0
return int(min(self.parent.height, self.parent.width)*self.radius/2.0)
def px(self):
"""
Converts an X-coordinate in the range -1.0 .. +1.0 to a position
within the window based on its width
"""
assert self.x >= -1.0 and self.x <= 1.0
return int((1.0 + self.x) * self.parent.width / 2.0 + self.parent.border)
def py(self):
"""
Converts a Y-coordinate in the range -1.0 .. +1.0 to a position
within the window based on its height
"""
assert self.y >= -1.0 and self.y <= 1.0
return int((1.0 - self.y) * self.parent.height / 2.0 + self.parent.border)
def Move(self, x, y):
"""
Moves ball to absolute position (x, y) where x and y are both -1.0 .. 1.0
"""
oldx = self.px()
oldy = self.py()
self.x = x
self.y = y
deltax = self.px() - oldx
deltay = self.py() - oldy
if oldx != 0 or oldy != 0:
self.parent.canvas.move(self.widget, deltax, deltay)
def HandleWallCollision(self):
"""
Detects if a ball collides with the wall of the rectangular
Court.
"""
pass
class Court:
"""
A 2D rectangular enclosure containing a centred, rectagular
grid of balls (instances of the Ball class).
"""
def __init__(self,
width=1000, # Width of the canvas in pixels
height=750, # Height of the canvas in pixels
border=5, # Width of the border around the canvas in pixels
rows=1, # Number of rows of balls
cols=1, # Number of columns of balls
radius=0.05, # Ball radius
ballborder=1, # Width of the border around the balls in pixels
cycles=1000, # Number of animation cycles
tick=0.01): # Animation tick length (sec)
self.root = Tk()
self.height = height
self.width = width
self.border = border
self.cycles = cycles
self.tick = tick
self.canvas = Canvas(self.root, width=width+2*border, height=height+2*border)
self.rectangle = self.canvas.create_rectangle(border, border, width+border, height+border, outline="black", fill="white", width=border)
self.root.bind('<Key>', self.key)
self.CreateGrid(rows, cols, radius, ballborder)
self.canvas.pack()
self.afterid = self.root.after(0, self.Animate)
self.root.mainloop()
def __repr__(self):
s = "width={} height={} border={} balls={}\n".format(self.width,
self.height,
self.border,
len(self.balls))
for b in self.balls:
s += "> {}\n".format(b)
return s
def key(self, event):
print("Got key '{}'".format(event.char))
if event.char == 'q':
print("Bye!")
self.root.after_cancel(self.afterid)
self.root.destroy()
def CreateGrid(self, rows, cols, radius, border):
"""
Creates a rectangular rows x cols grid of balls of
the specified radius and border thickness
"""
self.balls = []
for r in range(1, rows+1):
y = 1.0-2.0*r/(rows+1)
for c in range(1, cols+1):
x = 2.0*c/(cols+1) - 1.0
self.balls.append(Ball(self, x, y, 0.001,
numpy.pi/6.0, 0.0, radius, border))
def Animate(self):
"""
Animates the movement of the various balls
"""
for c in range(self.cycles):
for b in self.balls:
b.v += b.accel
b.Move(b.x + b.v * numpy.cos(b.theta),
b.y + b.v * numpy.sin(b.theta))
self.canvas.update()
time.sleep(self.tick)
self.root.destroy()
I've included the full listing for completeness, but I'm fairly sure that the problem lies in the Court class. I presume it's some sort of callback or similar firing but I seem to be beating my head against a wall trying to fix it.
You have effectively got two mainloops. In your Court.__init__ method you use after to start the Animate method and then start the Tk mainloop which will process events until you destroy the main Tk window.
However the Animate method basically replicates this mainloop by calling update to process events then time.sleep to waste some time and repeating this. When you handle the keypress and terminate your window, the Animate method is still running and attempts to update the canvas which no longer exists.
The correct way to handle this is to rewrite the Animate method to perform a single round of moving the balls and then schedule another call of Animate using after and provide the necessary delay as the after parameter. This way the event system will call your animation function at the correct intervals while still processing all other window system events promptly.

Categories

Resources