Move multiple tkinter.canvas figures simultaneously - python

I want to make many circles to move on a given trajectory simultaneously. Everything works fine only with a single figure moving. Whenever I add another figure it starts speeding up until it starts freezing apparently. This happens no matter what I use threads or canvas.move() and canvas.after() methods.
Actually this is also quite weird, because with my full version of code it started to slow down after adding more figures. The one that I sent is simplified in order to show the issue with moving. Maybe that happens, because I used my own method to draw trajectory lines with many little squares, but that is not the point.
What can be done to move figures at the same time with the same speed without that much of a lag? I wanted to try to use procceses instead of threads, but did not really understand how they work and I doubt that would change anything significantly.
Maybe you could also give an advice on how to work with individual pixels in canvas without drawing rectangle with width of one pixel?
EDIT:
Forgot to mention. For some reason even if there is a single figure moving, if I move my mouse figure's moving will slow down until I stop touching the mouse. I suppose the reason is that tkinter starts registering events, but I need it to draw the trajectory, so removing is not really an option. What can be done to get rid of this issue?
import math
import tkinter as tk
from enum import Enum
import threading
class Trajectory:
# This trajectory should do the same trick both for circles, squares and maybe images
# x0, y0 - start point, x1, y1 - end point
def __init__(self, x0, y0, x1, y1, id, diameter):
print('aaaa', id)
self.x0 = x0
self.y0 = y0
self.x1 = x1
self.y1 = y1
self.id = id
self.sign = 1
self.diameter = diameter
self.dir = self.get_normalized_dir()
def has_arrived(self, x, y):
return math.sqrt((self.x1 - (x + self.diameter // 2)) * (self.x1 - (x + self.diameter // 2)) +
(self.y1 - (y + self.diameter // 2)) * (self.y1 - (y + self.diameter // 2))) < 1
def get_normalized_dir(self):
L = math.sqrt((self.x1 - self.x0) * (self.x1 - self.x0) + (self.y1 - self.y0) * (self.y1 - self.y0))
return (self.x1 - self.x0) / L, (self.y1 - self.y0) / L
def swap_points(self):
self.x0, self.y0, self.x1, self.y1 = self.x1, self.y1, self.x0, self.y0
def __str__(self):
return f'x0 {self.x0} y0 {self.y0} x1 {self.x1} y1 {self.y1} id {self.id} sign {self.sign}'
class App(tk.Tk):
def __init__(self):
# action_33 was intented as an Easter egg to smth (at least I think so). However,
# I forgot what it meant :(
super().__init__()
self.bind('<Motion>', self.on_mouse)
self.geometry('400x400')
self.resizable(False, False)
self.canvas = tk.Canvas(self, bg='white', width=400, height=400)
self.canvas.pack(fill="both", expand=True)
self.start_point = []
self.end_point = []
self.is_drawing = False
self.OUTLINE = 'black'
self.canvas.bind("<Button-1>", self.callback)
self.title('Object trajetory')
self.bg_line = None
self.figure_color = 'green'
self.figures = [] # will store only trajectory class
self.diameter = 40
def move_figures(self):
# if not self.is_drawing:
for figure in self.figures:
self.canvas.move(figure.id, figure.dir[0] * 0.1 * figure.sign, figure.dir[1] * 0.1 * figure.sign)
if figure.has_arrived(self.canvas.coords(figure.id)[0], self.canvas.coords(figure.id)[1]):
figure.sign = -figure.sign
figure.swap_points()
self.canvas.after(1, self.move_figures)
def move_for_thread(self, figure):
while True:
self.canvas.move(figure.id, figure.dir[0] * 0.1 * figure.sign, figure.dir[1] * 0.1 * figure.sign)
if figure.has_arrived(self.canvas.coords(figure.id)[0], self.canvas.coords(figure.id)[1]):
figure.sign = -figure.sign
figure.swap_points()
def delete_shadow_line(self):
if self.bg_line is not None:
self.canvas.delete(self.bg_line)
def on_mouse(self, event):
if self.is_drawing:
self.delete_shadow_line()
self.bg_line = self.canvas.create_line(self.start_point[0], self.start_point[1], event.x, event.y)
def callback(self, event):
if not self.is_drawing:
self.start_point = [event.x, event.y]
self.is_drawing = True
else:
self.is_drawing = False
self.bg_line = None
self.end_point = [event.x, event.y]
fig = self.canvas.create_oval(self.start_point[0] - self.diameter // 2,
self.start_point[1] - self.diameter // 2,
self.start_point[0] + self.diameter // 2,
self.start_point[1] + self.diameter // 2,
fill=self.figure_color, outline='')
# self.figures.append(
# Trajectory(self.start_point[0], self.start_point[1], self.end_point[0], self.end_point[1], fig,
# self.diameter))
# self.move_figures()
traj = Trajectory(self.start_point[0], self.start_point[1], self.end_point[0], self.end_point[1], fig, self.diameter)
t = threading.Thread(target=self.move_for_thread, args=(traj,))
t.start()
if __name__ == '__main__':
print('a')
app = App()
app.mainloop()

Using threads is adding more complexity than necessary. The code that uses after is easily capable of doing the animation.
However, you have a critical flaw. You're calling move_figures() every time you draw a line, and each time you call it you are starting another animation loop. Since the animation loop moves everything, the first object gets moved 1000 times per second. When you add two elements, each element is moving 1000 times per second twice. Three elements, it's moving 1000 times per second three times and so on.
So, start by removing the threading code. Then, call move_figures() exactly once, and within it, you should not be calling it with after(1, ...) since that's attempting to animate it 1000 times per second. Instead, you can reduce the load by 90% by using after(10, ...) or some other number bigger than 1.
You can call the function exactly once by calling it from App.__init__ rather than in App.callback.

Related

QtWidgets.QGraphicsLineItem issue when setting viewport to QOpenGLWidget()

Situation
I have a PyQt5 app that shows lines, text and circles, it shows them correctly but the text rendering is a bit slow. I have a custom class for QGrapichsView that implement all this.
problem
When I set in the properties of the gv the following I start getting errors such as the example. The text and circles render correctly at a much faster render time(much better) but the lines get the error in rendering.
self.gl_widget = QOpenGLWidget()
format = QSurfaceFormat()
# format.setVersion(3, 0)
format.setProfile(QSurfaceFormat.CoreProfile)
self.gl_widget.setFormat(format)
self.setViewport(self.gl_widget)
the render of text get much much better and it shows them as it should. but a problem comes with the lines that start having strange behavior.
example with issue
example without issue
note how the width of the lines is variable even tough is set to a unique value, also, when I do a zoom out or zoom in, some of this lines appear and disappear randomly.
As soon as I use path item the problems begin, just a line item does not create this problem.
Does anybody have any idea what could this mean?
what to look for?
The issue is that the width of the lines are random, and not the set value I put in the code. Also when you zoom in or out, it disappears.
It seems to have something to do with the set width, as a bigger width helps, but does not remove it.
minimal reproducible example
import sys
from PyQt5.QtWidgets import QApplication, QGraphicsScene, QGraphicsTextItem
from PyQt5 import QtWidgets, QtCore, QtGui
from PyQt5.QtWidgets import QOpenGLWidget
import numpy as np
from PyQt5.QtGui import QPainterPath, QPen
from PyQt5.QtWidgets import QGraphicsPathItem, QGraphicsLineItem, QGraphicsPolygonItem
from PyQt5.QtGui import QPolygonF
from PyQt5.QtCore import QLineF, QPointF
from PyQt5.QtGui import QSurfaceFormat
class GraphicsView(QtWidgets.QGraphicsView):
def __init__(self):
super(GraphicsView, self).__init__()
self.pos_init_class = None
# "VARIABLES INICIALES"
self.scale_factor = 1.5
# "ASIGNAR LINEAS DE MARCO"
self.setFrameShape(QtWidgets.QFrame.VLine)
# "ACTIVAR TRACKING DE POSICION DE MOUSE"
self.setMouseTracking(True)
# "REMOVER BARRAS DE SCROLL"
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
# "ASIGNAR ANCLA PARA HACER ZOOM SOBRE EL MISMO PUNTO"
self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
# "MEJORAR EL RENDER DE VECTORES"
self.setRenderHint(QtGui.QPainter.Antialiasing, False)
self.setRenderHint(QtGui.QPainter.SmoothPixmapTransform, False)
self.setRenderHint(QtGui.QPainter.TextAntialiasing, False)
self.setRenderHint(QtGui.QPainter.HighQualityAntialiasing, False)
self.setRenderHint(QtGui.QPainter.NonCosmeticDefaultPen, True)
self.setOptimizationFlag(QtWidgets.QGraphicsView.DontAdjustForAntialiasing, True)
self.setOptimizationFlag(QtWidgets.QGraphicsView.DontSavePainterState, True)
self.setCacheMode(QtWidgets.QGraphicsView.CacheBackground)
self.setViewportUpdateMode(QtWidgets.QGraphicsView.BoundingRectViewportUpdate)
#Try OpenGL stuff
# self.gl_widget = QOpenGLWidget()
# self.setViewport(self.gl_widget)
self.gl_widget = QOpenGLWidget()
format = QSurfaceFormat()
format.setVersion(2, 8)
format.setProfile(QSurfaceFormat.CoreProfile)
self.gl_widget.setFormat(format)
self.setViewport(self.gl_widget)
def mousePressEvent(self, event):
pos = self.mapToScene(event.pos())
# "PAN MOUSE"
if event.button() == QtCore.Qt.MiddleButton:
self.pos_init_class = pos
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.ClosedHandCursor)
super(GraphicsView, self).mousePressEvent(event)
def mouseReleaseEvent(self, event):
# PAN Y RENDER TEXT
if self.pos_init_class and event.button() == QtCore.Qt.MiddleButton:
# PAN
self.pos_init_class = None
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.ArrowCursor)
super(GraphicsView, self).mouseReleaseEvent(event)
def mouseMoveEvent(self, event):
if self.pos_init_class:
# "PAN"
delta = self.pos_init_class - self.mapToScene(event.pos())
r = self.mapToScene(self.viewport().rect()).boundingRect()
self.setSceneRect(r.translated(delta))
super(GraphicsView, self).mouseMoveEvent(event)
def wheelEvent(self, event):
old_pos = self.mapToScene(event.pos())
# Determine the zoom factor
if event.angleDelta().y() > 0:
zoom_factor = self.scale_factor
else:
zoom_factor = 1 / self.scale_factor
# Apply the transformation to the view
transform = QtGui.QTransform()
transform.translate(old_pos.x(), old_pos.y())
transform.scale(zoom_factor, zoom_factor)
transform.translate(-old_pos.x(), -old_pos.y())
# Get the current transformation matrix and apply the new transformation to it
current_transform = self.transform()
self.setTransform(transform * current_transform)
def zoom_extent(self):
x_range, y_range, h_range, w_range = self.scene().itemsBoundingRect().getRect()
rect = QtCore.QRectF(x_range, y_range, h_range, w_range)
self.setSceneRect(rect)
unity = self.transform().mapRect(QtCore.QRectF(0, 0, 1, 1))
self.scale(1 / unity.width(), 1 / unity.height())
viewrect = self.viewport().rect()
scenerect = self.transform().mapRect(rect)
factor = min(viewrect.width() / scenerect.width(), viewrect.height() / scenerect.height())
self.scale(factor, factor)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.view = GraphicsView()
self.scene = QGraphicsScene()
self.view.setScene(self.scene)
self.generate_random_lines()
self.setCentralWidget(self.view)
self.showMaximized()
self.view.zoom_extent()
def rotate_vector(self, origin, point, angle):
"""
ROTATE A POINT COUNTERCLOCKWISE BY A GIVEN ANGLE AROUND A GIVEN ORIGIN. THE ANGLE SHOULD BE GIVEN IN RADIANS.
:param origin: SOURCE POINT ARRAYS, [X_SOURCE, Y_SOURCE], LEN N
:param point: DESTINATION POINT, [X_DEST, Y_DEST], LEN N
:param angle: ARRAY OF ANGLE TO ROTATE VECTOR (ORIGIN --> POINT), [ANG], LEN N
:return:
"""
ox, oy = origin
px, py = point
qx = ox + np.cos(angle) * (px - ox) - np.sin(angle) * (py - oy)
qy = oy + np.sin(angle) * (px - ox) + np.cos(angle) * (py - oy)
return qx, qy
def create_line_with_arrow_path(self, x1, y1, x2, y2, arr_width, arr_len):
"""
This function creates a line with an arrowhead at the end.
The line is created between two points (x1, y1) and (x2, y2).
The arrowhead is defined by its width (arr_width) and length (arr_len).
Returns a QGraphicsPathItem with the line and arrowhead.
"""
# Initialize the path for the line and arrowhead
path = QPainterPath()
path.moveTo(x1, y1)
path.lineTo(x2, y2)
# Calculate the midpoint of the line
mid_x = (x1 + x2) / 2
mid_y = (y1 + y2) / 2
# Define the points of the arrowhead
arrow_x = np.array([arr_width, -arr_len, -arr_width, -arr_len, arr_width]) * 5
arrow_y = np.array([0, arr_width, 0, -arr_width, 0]) * 5
arrow_x += mid_x
arrow_y += mid_y
# Calculate the angle of the line
angle = np.rad2deg(np.arctan2(y2 - y1, x2 - x1))
# Rotate the arrowhead points to align with the line
origin = (np.array([mid_x, mid_x, mid_x, mid_x, mid_x]), np.array([mid_y, mid_y, mid_y, mid_y, mid_y]))
point = (arrow_x, arrow_y)
self.x_init, self.y_init = self.rotate_vector(origin, point, np.deg2rad(angle))
# Add the arrowhead to the path
arrow_path = QtGui.QPainterPath()
arrow_path.moveTo(self.x_init[0], self.y_init[0])
for i in range(1, len(arrow_x)):
arrow_path.lineTo(self.x_init[i], self.y_init[i])
path.addPath(arrow_path)
# Create a QGraphicsPathItem with the line and arrowhead
item = QGraphicsPathItem(path)
pen = QPen()
pen.setWidthF(0.1)
item.setPen(pen)
return item, angle
def create_line_with_arrow_item(self, x1, y1, x2, y2, arr_width, arr_len):
# Calculate the midpoint of the line
mid_x = (x1 + x2) / 2
mid_y = (y1 + y2) / 2
# Define the coordinates for the arrow
arrow_x = np.array([arr_width, -arr_len, -arr_width, -arr_len, arr_width]) * 10
arrow_y = np.array([0, arr_width, 0, -arr_width, 0]) * 10
arrow_x += mid_x
arrow_y += mid_y
# Calculate the angle of the line
angle = np.rad2deg(np.arctan2(y2 - y1, x2 - x1))
# Rotate the arrow to align with the line
origin = (np.array([mid_x, mid_x, mid_x, mid_x, mid_x]), np.array([mid_y, mid_y, mid_y, mid_y, mid_y]))
point = (arrow_x, arrow_y)
x_init, y_init = self.rotate_vector(origin, point, np.deg2rad(angle))
# Create the line and arrow
line = QLineF(x1, y1, x2, y2)
arrow = QPolygonF([QPointF(x_init[0], y_init[0]),
QPointF(x_init[1], y_init[1]),
QPointF(x_init[2], y_init[2]),
QPointF(x_init[3], y_init[3]),
QPointF(x_init[4], y_init[4])])
item = QGraphicsLineItem(line)
item_arrow = QGraphicsPolygonItem(arrow)
# Set the pen for both line and arrow
pen = QPen()
pen.setWidthF(1)
item.setPen(pen)
item_arrow.setPen(pen)
# Return the line and arrow items
return item, item_arrow, angle
def generate_random_lines(self):
case = 'issue'
x = np.array([0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]) * 10
y = np.array([0, 20, 10, 0, 35, 90, 10, 60, 60, 90, 100]) * 10
for pos, i in enumerate(range(len(x) - 1)):
x1 = x[i]
y1 = y[i]
x2 = x[i + 1]
y2 = y[i + 1]
if case in ['issue']:
#add lines
path, angle = self.create_line_with_arrow_path(x1, y1, x2, y2, 0.5, 1.5)
self.scene.addItem(path)
# add text
text1 = QGraphicsTextItem()
text1.setPlainText(str(pos))
text1.setPos(x1, y1)
text1.setRotation(angle)
self.scene.addItem(text1)
else:
#add lines
line, arrow, angle = self.create_line_with_arrow_item(x1, y1, x2, y2, 0.5, 1.5)
self.scene.addItem(line)
self.scene.addItem(arrow)
# add text
text1 = QGraphicsTextItem()
text1.setPlainText(str(pos))
text1.setPos(x1, y1)
text1.setRotation(angle)
self.scene.addItem(text1)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
figures for minimal example
this is an example whit the issue
this is an example without the issue
The issue is caused by two reasons.
Sampling of the OpenGL surface
When an analog object is shown in a digital context, aliasing always happens. That's the basic difference between real and integer numbers.
Now, the "issue" with OpenGL (and, from my understanding, any 3D basic visualization) is that if an object doesn't fill a whole pixel, by default that pixel won't be shown.
Anti-aliasing allows to tell if a pixel should be shown even if only a part of that pixel may be covered by the object. OpenGL is capable of multisample anti-aliasing, which provides an optimized spacial anti-aliasing: the amount of samples tells the engine if a pixel should be shown (and how) or not.
I'm not completely sure on how Qt decides the default sampling (my assumption is that it's based on the detected screen capabilities), but, in my tests and according to the documentation, the default is -1, meaning that no multisampling is enabled.
The convention I found normally uses 16 samples as default, so you need to do the following:
self.gl_widget = QOpenGLWidget()
format = QSurfaceFormat()
format.setSamples(16)
# ...
The pen width is too small
You're using a default pen width of 0.1, which is extremely small. Even with the standard raster rendering (without QOpenGlWidget as viewport), the pen would be almost invisible.
Imagine having a thread (I mean a physical yarn) that is very small in size: you can see it if you're close to it, but as you get far from it you'll eventually stop seeing it at some point.
Set a cosmetic pen whenever the scale is too big
The solution is quite simple, conceptually speaking: use the actual size as long as the scaling (the "zoom factor") allows to properly see the object.
Making it in practice requires some ingenuity.
Take these considerations for the following example:
I'm assuming a very basic scenario based on your example; for obvious reasons, you may need some further implementation, also considering that you're using different pen widths depending on the objects;
I'm using a mix-in class that takes advantage of the cooperative multi-inheritance followed by PyQt;
the implementation is quite basic and assumes that you always call setPen() to set a "default pen width"; you can obviously make your own classes using the pen width as argument in the constructor;
the "cosmetic pen" choice is based on arbitrary values; the assumption is that a pen becomes "non cosmetic" whenever a pixel ratio of penWidth * 4 would theoretically cover a whole pixel;
class VisibleGlShapeItem(object):
'''
A pseudo class that potentially overrides setPen() and provides a
custom method to override the default setPen() implementation.
'''
penWidth = None
def setPen(self, pen):
super().setPen(pen)
if self.penWidth is None:
self.penWidth = pen.widthF()
def setCosmeticPen(self, cosmeticScale):
if self.penWidth is None:
self.penWidth = .1
cosmetic = .5 / cosmeticScale > self.penWidth
pen = self.pen()
if pen.isCosmetic() != cosmetic:
pen.setCosmetic(cosmetic)
if cosmetic:
pen.setWidthF(.5)
else:
pen.setWidthF(self.penWidth)
super().setPen(pen)
# mixin classes creation
class VisibleGlPathItem(VisibleGlShapeItem, QGraphicsPathItem): pass
class VisibleGlLineItem(VisibleGlShapeItem, QGraphicsLineItem): pass
class GraphicsView(QtWidgets.QGraphicsView):
def __init__(self):
# ...
self.setRenderHint(QtGui.QPainter.Antialiasing) # mandatory
# ...
self.gl_widget = QOpenGLWidget()
format.setSamples(16)
# ...
def wheelEvent(self, event):
# ...
self.updatePens()
def showEvent(self, event):
# ensure that updatePens is called at least on first start
super().showEvent(event)
if not event.spontaneous():
self.updatePens()
def updatePens(self):
# get the minimum transformation scale; while you seem to be using
# a fixed ratio, a reference should always be considered
scale = min(self.transform().m11(), self.transform().m22())
for item in self.items():
if isinstance(item, QGraphicsPathItem):
item.setCosmeticPen(scale)
class MainWindow(QtWidgets.QMainWindow):
# ...
def create_line_with_arrow_path(self, x1, y1, x2, y2, arr_width, arr_len):
# ...
item = VisibleGlPathItem(path)
pen = QPen()
pen.setWidthF(0.1)
item.setPen(pen)
return item, angle
Final notes
avoid unnecessary renderHint flags; by default, QGraphicsView only uses TextAntialiasing, so, setting any other flag to False is completely pointless; also, both HighQualityAntialiasing and NonCosmeticDefaultPen are obsolete;
unless you actually need numpy for other reasons, avoid its requirement for mathematical purposes: either use the math module, or Qt capabilities; for instance, if you need to "rotate" a point around another, you can use QLineF, as it's normally quite fast and has the major benefit of better readability:
vector = QLineF(p1, p2)
vector.setAngle(angle)
newP2 = vector.p2()
merge/group items; lines shouldn't be separated by their arrows; if they share the same pen, just use a single QPainterPath (and QGraphicsPathItem), otherwise make the arrows as child items of the line;
use caching: there's no point in creating new paths for each arrow every time; just create one (using a 0-reference point) and add a translated copy to the new path whenever you need it; the result is conceptually the same, with the benefit of working on the C++ side of things;
setFrameShape(QtWidgets.QFrame.VLine) only makes sense for separators; don't use it for the wrong reason or widget;
setOverrideCursor() is intended for the whole application, and you shouldn't use it to temporarily change the cursor of a single widget; use setCursor() and unsetCursor() instead;

how to make two particles collide in tkinter python 3?

I'm currently trying to make two particles in a gaseous state collide, but I'm not too sure how to write a code that will make this happen, I have a rough draft of what I could do but I don't really know how I could properly implent it (lines 43-54 i.e the code after the comment #collision of i with another particle j) after the collision occurs I wanted them to go in the opposite direction with different speedssince I will be computing the kinetic energy depending on the mass of the particles. The goal of the project is to basically show the conservation of kinetic energy of multiple particles of differnet parameters(velocity,mass,direction) moving in gaz. Here's my current code, any help would be greatly appreciated!!:
from tkinter import *
from random import *
myHeight=400
myWidth=600
mySpeed=10
x1=5
y=5
radius1=20
x2=7
y2=4
radius1=20
x=60
width=40
length=10
global particules
particules = []
def initialiseBall(dx,dy,radius,color):
b = [myWidth/2,myHeight/2, dx, dy, radius]
particules.append(b)
k = myCanvas.create_oval(myWidth/2-radius, myHeight/2,\
myWidth/2+radius,myHeight/2+radius,\
width=2,fill=color)
print(k)
def updateBalls():
N = len(particules)
for i in range(N):
particules[i][0] += particules [i][2]
particules[i][1] += particules [i][3]
# collision of i with the walls
if particules[i][0]<0 or particules[i][0]>=myWidth:
particules[i][2] *= -1
if particules[i][1]<0 or particules[i][1]>=myHeight:
particules[i][3] *= -1
#collision of i with another particle j
# for j in range(N):
# if j != i:
# compare the position of i and j
# dij = ...
# if dij ... :
#if collision, compute the normal vector
#change velocities
# if particules[i][1]<=particules[i][1]:
# particules[i][2] *= -1
# r = particules[i][4]
myCanvas.coords(i+1, particules[i][0]-particules[i][4],
particules[i][1]-particules[i][4],
particules[i][0]+particules[i][4],
particules[i][1]+particules[i][4])
def animation ():
updateBalls()
myCanvas.after(mySpeed, animation)
def kineticenergy(mass, velocity):
Ec = 1/2 * mass * velocity ** 2
return Ec
# def averagetrip(number, radius):
# #
# #
# #
# #
mainWindow=Tk()
mainWindow.title('particles reservoir')
myCanvas = Canvas(mainWindow, bg = 'grey', height = myHeight, width = myWidth)
myCanvas.pack(side=TOP)
# create 2 particules
initialiseBall(-1,0, 50, 'red')
initialiseBall(1,0, 50, 'blue')
print(particules)
'''
N = 100
for n in range(N):
initialiseBalle(-1 ,0, randint(5,10), 'red')
'''
animation()
mainWindow.mainloop()
Try this:
from math import sqrt, sin, cos
import tkinter as tk
import time
# For more info about this read: https://stackoverflow.com/a/17985217/11106801
def _create_circle(self, x, y, r, **kwargs):
return self.create_oval(x-r, y-r, x+r, y+r, **kwargs)
tk.Canvas.create_circle = _create_circle
WINDOW_WIDTH = 500
WINDOW_HEIGHT = 500
# The coefficient of restitution
# Set to 1 for perfectly elastic collitions
e = 1
class Ball:
def __init__(self, mass:float, r:float, x:float, y:float,
vx:float=0, vy:float=0, **kwargs):
"""
This is a class that defines what a ball is and how it interacts
with other balls.
Arguments:
mass:float # The mass of the ball
------------------------------------------------------
r:float # The radius of the ball, must be >0
------------------------------------------------------
x:float # The x position of the ball
# must be >0 and <WINDOW_WIDTH
y:float # The y position of the ball
# must be >0 and <WINDOW_HEIGHT
------------------------------------------------------
vx:float # The x velocity of the ball
vy:float # The y velocity of the ball
------------------------------------------------------
**kwargs # All of the args to be passed in to `create_circle`
"""
self.m = mass
self.r = r
self.x = x
self.y = y
self.vx = vx
self.vy = vy
self.kwargs = kwargs
def display(self, canvas:tk.Canvas) -> int:
"""
Displays the ball on the screen and returns the canvas_id (which
is a normal python int).
"""
canvas_id = canvas.create_circle(self.x, self.y, self.r, **self.kwargs)
return canvas_id
def move(self, all_balls:list, dt:float) -> (float, float):
"""
This moves the ball according to `self.vx` and `self.vy`.
It also checks for collisions with other balls.
Arguments:
all_balls:list # A list of all balls that are in the
# simulation. It can include this ball.
---------------------------------------------------
dt:float # delta time - used to move the balls
Returns:
dx:float # How much the ball has moved in the x direction
dy:float # How much the ball has moved in the y direction
Note: This function isn't optimised in any way. If you optimise
it, it should run faster.
"""
# Check if there are any collitions:
for other, _ in all_balls:
# Skip is `ball` is the same as this ball
if id(other) == id(self):
continue
# Check if there is a collision:
distance_squared = (other.x - self.x)**2 + (other.y - self.y)**2
if distance_squared <= (other.r + self.r)**2:
# Now the fun part - calulating the resultant velocity.
# I am assuming you already know all of the reasoning
# behind the math (if you don't ask physics.stackexchange.com)
# First I will find the normal vector of the balls' radii.
# That is just the unit vector in the direction of the
# balls' radii
ball_radii_vector_x = other.x - self.x
ball_radii_vector_y = other.y - self.y
abs_ball_radii_vector = sqrt(ball_radii_vector_x**2 +\
ball_radii_vector_y**2)
nx = ball_radii_vector_x / abs_ball_radii_vector **2*2
ny = ball_radii_vector_y / abs_ball_radii_vector **2*2
# Now I will calculate the tangent
tx = -ny
ty = nx
""" Only for debug
print("n =", (nx, ny), "\t t =", (tx, ty))
print("u1 =", (self.vx, self.vy),
"\t u2 =", (other.vx, other.vy))
#"""
# Now I will split the balls' velocity vectors to the sum
# of 2 vectors parallel to n and t
# self_velocity = λ*n + μ*t
# other_velocity = a*n + b*t
λ = (self.vx*ty - self.vy*tx) / (nx*ty - ny*tx)
# Sometimes `tx` can be 0 if so we are going to use `ty` instead
try:
μ = (self.vx - λ*nx) / tx
except ZeroDivisionError:
μ = (self.vy - λ*ny) / ty
""" Only for debug
print("λ =", λ, "\t μ =", μ)
#"""
a = (other.vx*ty - other.vy*tx) / (nx*ty - ny*tx)
# Sometimes `tx` can be 0 if so we are going to use `ty` instead
try:
b = (other.vx - a*nx) / tx
except ZeroDivisionError:
b = (other.vy - a*ny) / ty
""" Only for debug
print("a =", a, "\t b =", b)
#"""
self_u = λ
other_u = a
sum_mass_u = self.m*self_u + other.m*other_u
sum_masses = self.m + other.m
# Taken from en.wikipedia.org/wiki/Inelastic_collision
self_v = (e*other.m*(other_u-self_u) + sum_mass_u)/sum_masses
other_v = (e*self.m*(self_u-other_u) + sum_mass_u)/sum_masses
self.vx = self_v*nx + μ*tx
self.vy = self_v*ny + μ*ty
other.vx = other_v*nx + b*tx
other.vy = other_v*ny + b*ty
print("v1 =", (self.vx, self.vy),
"\t v2 =", (other.vx, other.vy))
# Move the ball
dx = self.vx * dt
dy = self.vy * dt
self.x += dx
self.y += dy
return dx, dy
class Simulator:
def __init__(self):
self.balls = [] # Contains tuples of (<Ball>, <canvas_id>)
self.root = tk.Tk()
self.root.resizable(False, False)
self.canvas = tk.Canvas(self.root, width=WINDOW_WIDTH,
height=WINDOW_HEIGHT)
self.canvas.pack()
def step(self, dt:float) -> None:
"""
Steps the simulation as id `dt` seconds have passed
"""
for ball, canvas_id in self.balls:
dx, dy = ball.move(self.balls, dt)
self.canvas.move(canvas_id, dx, dy)
def run(self, dt:float, total_time:float) -> None:
"""
This function keeps steping `dt` seconds until `total_time` has
elapsed.
Arguments:
dt:float # The time resolution in seconds
total_time:float # The number of seconds to simulate
Note: This function is just for proof of concept. It is badly written.
"""
for i in range(int(total_time//dt)):
self.step(dt)
self.root.update()
time.sleep(dt)
def add_ball(self, *args, **kwargs) -> None:
"""
Adds a ball by passing all of the args and keyword args to `Ball`.
It also displays the ball and appends it to the list of balls.
"""
ball = Ball(*args, **kwargs)
canvas_id = ball.display(self.canvas)
self.balls.append((ball, canvas_id))
app = Simulator()
app.add_ball(mass=1, r=10, x=20, y=250, vx=100, vy=0, fill="red")
app.add_ball(mass=1, r=10, x=240, y=250, vx=0, vy=0, fill="blue")
app.add_ball(mass=1, r=10, x=480, y=250, vx=0, vy=0, fill="yellow")
app.run(0.01, 10)
That code has a lot of comments describing how it works. If you still have any questions, ask me. Also the move method isn't optimised. The run method isn't great and can throw an error if it's still running and the user closes the window. I will try to find a better approch for that method. If you find any bugs, please let me know. I will try to fix them.

How to use time.sleep() to draw circles Tkinter?

I wrote a program to explore Tkinter & try out object-oriented programming. My goal is to draw concentric circles, starting with the outside and moving in.
The drawing works fine, but my time-delay between circles isn't working. I can see the count-down (with print) but it doesn't draw anything until after the count-down ends.
Possibly this is related to the creation of the object? Nothing happens until the object is finished being created? IDK.
Here's my code:
import tkinter as tk
import time
root = tk.Tk()
size = 1000
myCanvas = tk.Canvas(root, bg="white", height=size, width=size)
# draw circle
class Circle:
def __init__(self, rt, dia, color, x=0, y=0):
self.rt = rt
self.dia = dia
self.color = color
self.x = x # center cord x
self.y = y # center cord y
def draw_circle(self):
r = self.dia / 2
up_left = (self.x - r, self.y + r)
low_right = (self.x + r, self.y - r)
cord = up_left + low_right
self.rt.create_oval(cord, fill=self.color, outline="")
coord2 = 0, 300, 300, 0
#arc = myCanvas.create_oval(coord2, fill="blue")
def PickColor(r, g, b):
r = r % 250
g = g % 250
b = b % 250
return('#%02x%02x%02x' % (r, g, b))
class ConcentricCircles:
def __init__(self, rt, quantity):
self.rt = rt
self.quantity = quantity
def draw_circles(self):
q = self.quantity
circles = []
i = 0
for c in range(q, 1, -1):
time.sleep(0.005)
incr = size/(1.5*q-0.001*c*c*c)
print(c)
circles += [Circle(self.rt, incr*c, PickColor(110, 15*c^3-c^2, 300*c^5-c^4), size/2, size/2)]
circles[i].draw_circle()
i += 1
self.rt.pack()
a = ConcentricCircles(myCanvas, 30).draw_circles()
root.mainloop()
Here's what it draws:
When you use the sleep() function, the application suspends updates to the GUI. This means that the drawing of circles is also suspended. But you can force the application to update the GUI before it continues with update_idletasks(), see example below. I chose to make the update in the Circle.draw_circle() function:
def draw_circle(self):
r = self.dia / 2
up_left = (self.x - r, self.y + r)
low_right = (self.x + r, self.y - r)
cord = up_left + low_right
self.rt.create_oval(cord, fill=self.color, outline="")
self.rt.update_idletasks() # Updates the canvas
When you use sleep() the application is busy all the time it sleeps. You might want to research the after() function which schedules a function call but does not lock the app.

Pyglet App Running Slowly

I have this application that is creating an alternate version of cookie clicker named pizza clicker. It's very basic but it's running really slowly and I can't get why.
import pyglet
window = pyglet.window.Window(fullscreen=True, caption="Click For Pizzas", resizable=True)
win_width = window.width
win_height = window.height
window.set_fullscreen(False)
window.set_size(win_width, win_height)
image_height = round(int(win_width/5)/1.4, 1)
class Main(object):
def __init__(self):
self.label = pyglet.text.Label('Pizzas: 0', font_size=100, color=(0, 0, 0, 255),
x=win_width//2, y=win_height - 100,
anchor_x='left', anchor_y='top')
self.points = 0
self.number = 1
def background(self):
background_img = pyglet.resource.image('pizza_clicker.png')
background_img.width = (win_width/5)*4
background_img.height = win_height
background_img.blit(int(win_width/5), 0, 0.5)
def drawSidebar(self):
width = int(win_width/5)
height = int(win_height)
sidebar_pattern = pyglet.image.SolidColorImagePattern(color=(100, 100, 100, 100))
sidebar = sidebar_pattern.create_image(width, height)
sidebar.blit(0, 0)
pizza = []
images = ('pizza_1.png', 'pizza_5.png', 'pizza_5.png', 'pizza_5.png')
for i in range (0, len(images)):
divideby = 1.4 / (i + 1)
pizza.append(pyglet.resource.image(images[i]))
pizza[i].width = width
pizza[i].height = round(width/1.4, 1)
pizza[i].blit(0, window.height - (round(width/divideby, 1)))
def getNumber(self, y):
if y > window.height - int(image_height):
self.number = 1
elif y > window.height - (int(image_height)*2):
self.number = 5
elif y > window.height - (int(image_height)*3):
self.number = 10
elif y > window.height - (int(image_height)*4):
self.number = 20
def addPoint(self):
self.points += self.number
self.label.text = 'Pizzas: %s' %self.points
#window.event
def on_mouse_press(x, y, buttons, modifiers):
if buttons & pyglet.window.mouse.LEFT and x > win_width/5:
main.addPoint()
elif buttons & pyglet.window.mouse.LEFT and x < win_width/5:
main.getNumber(y)
#window.event
def on_draw():
window.clear()
main.background()
main.label.draw()
main.drawSidebar()
main = Main()
pyglet.app.run()
So the problem is that when I click on the right side of the window, it should add a point (or many) instantly but it lags for a few seconds. Also, just so nobody get confused, the code does work, but just slowly. What should I do to solve it?
On every draw() iteration, you're doing:
background_img = pyglet.resource.image('pizza_clicker.png')
This means you're loading in the same picture, from hard-drive, every render sequence. You're also doing a for loop over different pizza images where you also fetch them from hard drive:
for i in range (0, len(images)):
divideby = 1.4 / (i + 1)
pizza.append(pyglet.resource.image(images[i]))
I strongly suggest you read up on how resources are loaded in, and use a cProfiler analyzer.
A good example of how you could profile your code, is here.
Since this is a external source, I'll include two SO's links as well that's about equally good (but not as potent or self explaining):
https://stackoverflow.com/a/23164271/929999
Using cProfile results with KCacheGrind
Here's a tl-dr version:
python -m cProfile -o profile_data.pyprof your_script.py
pyprof2calltree -i profile_data.pyprof -k
This should render a so called, "call tree", of all the executions your code did, how long they took and how much memory they used up. All the way from start to bottom of your application.
However, I strongly suggest you do 1 rendering sequence and add a exit(1) after the first render. Just so you profile 1 run, not 60 per second.
Keywords to search for to get a hang of why your code is slow: Python, profiling, kcachegrind, cprofile, cprofiling, callstack.
Spoiler alert
To solve the majority of your problems, move all I/O intensive operations (loading images, creating shapes etc) into the __init__ call of your main class.
The end product would look something like this instead:
class Main(object):
def __init__(self):
self.label = pyglet.text.Label('Pizzas: 0', font_size=100, color=(0, 0, 0, 255),
x=win_width//2, y=win_height - 100,
anchor_x='left', anchor_y='top')
self.points = 0
self.number = 1
self.background_img = pyglet.resource.image('pizza_clicker.png')
self.background_img.width = (win_width/5)*4
self.background_img.height = win_height
sidebar_pattern = pyglet.image.SolidColorImagePattern(color=(100, 100, 100, 100))
self.sidebar = sidebar_pattern.create_image(width, height)
self.pizzas = []
width = int(win_width/5)
height = int(win_height)
self.pizza_images = ('pizza_1.png', 'pizza_5.png', 'pizza_5.png', 'pizza_5.png')
for i in range (0, len(pizza_images)):
resource = pyglet.resource.image(pizza_images[i])
resource.width = width
resource.height = round(width/1.4, 1) # Not sure why you're using width here.. meh.. keeping it -yolo-
self.pizzas.append(resource)
def background(self):
self.background_img.blit(int(win_width/5), 0, 0.5)
def drawSidebar(self):
width = int(win_width/5)
height = int(win_height) # You're using win_height here, but window.height later. It's strange.
self.sidebar.blit(0, 0)
for i in range (0, len(self.pizza_images)):
divideby = 1.4 / (i + 1)
self.pizzas[i].blit(0, window.height - (round(width/divideby, 1)))
def getNumber(self, y):
if y > window.height - int(image_height):
self.number = 1
elif y > window.height - (int(image_height)*2):
self.number = 5
elif y > window.height - (int(image_height)*3):
self.number = 10
elif y > window.height - (int(image_height)*4):
self.number = 20
def addPoint(self):
self.points += self.number
self.label.text = 'Pizzas: %s' %self.points
But why stop here, there's a lot of heavy use of blit here. Blit is fine for like one or two objects. But it quickly gets hard to track what and where you're "blitting" everything to. You're also doing a whole lof of division, addition and other sorts of calculations in loops and stuff.
Remember, loops are the devil in when it comes to rendering.
If you got a loop some where, you can almost certainly start looking there for performance issues (anyone looking at this comment and going "pff he has no clue what he's saying".. Yea I know, but it's a good beginners tip).
I strongly suggest you put your images into pyglet.sprite.Sprite() objects instead. They keep track of positions, rendering and most importantly, they support batched rendering. That is your holy grail of the mother land! If anything's going to save you performance wise in pyglet.. well.. 3D rendering in general, it's batched rendering.
See, the graphic card was designed with one thing in mind.. Taking a HUGE mathematical equation and just swallow it whole. It's particularly good at taking a big fat stack of information and just shooting it to your screen. It's not as good at multiple commands. Meaning if you're sending many smaller packets back and fourth to the graphics card, it's going to perform no wear near optimum because of overhead and other things.
So, putting your images into sprites, and putting those sprites into batches, and not using any for loops and on-rendering resources loads..
This is what your code would look like:
class Main(object):
def __init__(self):
self.label = pyglet.text.Label('Pizzas: 0', font_size=100, color=(0, 0, 0, 255),
x=win_width//2, y=win_height - 100,
anchor_x='left', anchor_y='top')
self.points = 0
self.number = 1
self.background_layer = pyglet.graphics.OrderedGroup(0)
self.foreground_layer = pyglet.graphics.OrderedGroup(1)
self.batch = pyglet.graphics.Batch()
self.background_img = pyglet.sprite.Sprite(pyglet.resource.image('pizza_clicker.png'), batch=self.batch, group=self.background_layer)
self.background_img.width = (win_width/5)*4
self.background_img.height = win_height
self.background.x = int(win_width/5)
self.background.y = 0
sidebar_pattern = pyglet.image.SolidColorImagePattern(color=(100, 100, 100, 100))
self.sidebar = pyglet.sprite.Sprite(sidebar_pattern.create_image(width, height), batch=self.batch, group=self.background_layer)
self.sidebar.x = 0
self.sidebar.y = 0
self.pizzas = []
width = int(win_width/5)
height = int(win_height)
self.pizza_images = ('pizza_1.png', 'pizza_5.png', 'pizza_5.png', 'pizza_5.png')
for i in range (0, len(pizza_images)):
divideby = 1.4 / (i + 1)
resource = pyglet.sprite.Sprite(pyglet.resource.image(pizza_images[i]), batch=self.batch, group=self.foreground_layer)
resource.width = width
resource.height = round(width/1.4, 1) # Not sure why you're using width here.. meh.. keeping it -yolo-
resource.x = 0
resource.y = window.height - (round(width/divideby, 1))
self.pizzas.append(resource)
def draw(self):
# This is instead of doing:
# - self.background.draw()
# - self.sidebar.draw()
# - self.pizzas[i].draw()
self.batch.draw()
self.label.draw() # You could put this in a batch as well :)
def getNumber(self, y):
if y > window.height - int(image_height):
self.number = 1
elif y > window.height - (int(image_height)*2):
self.number = 5
elif y > window.height - (int(image_height)*3):
self.number = 10
elif y > window.height - (int(image_height)*4):
self.number = 20
def addPoint(self):
self.points += self.number
self.label.text = 'Pizzas: %s' %self.points
#window.event
def on_draw():
window.clear()
main.draw()
Now, the code ain't perfect. But it will hopefully give you a sense of the heading you should be going towards. I haven't executed this code either, mainly because I don't have all the pizza images or the time. I might come back here and do so, and tidy up the (most likely) spelling errors I have.

Hide Turtle Window?

I am generating diagrams in Turtle, and as part of my program, I identify certain coordinates from my diagrams. I would like to be able to hide the complete turtle window, since I only care about the coordinates, is that possible?
Edit:
QUESTION 2:
This isn't really an answer, but a few other questions.
I got my program working to some extent, if you run it in IDLE and type "l" it will give you the list with the coordinates.
import Tkinter
import turtle
from turtle import rt, lt, fd # Right, Left, Forward
size = 10
root = Tkinter.Tk()
root.withdraw()
c = Tkinter.Canvas(master = root)
t = turtle.RawTurtle(c)
t.speed("Fastest")
# List entire coordinates
l = []
def findAndStoreCoords():
x = t.xcor()
y = t.ycor()
x = round(x, 0) # Round x to the nearest integer
y = round(y, 0) # Round y to the nearest integer
# Integrate coordinates into sub-list
l.append([x, y])
def hilbert(level, angle):
if level == 0:
return
t.rt(angle)
hilbert(level - 1, -angle)
t.fd(size)
findAndStoreCoords()
t.lt(angle)
hilbert(level - 1, angle)
t.fd(size)
findAndStoreCoords()
hilbert(level - 1, angle)
t.lt(angle)
t.fd(size)
findAndStoreCoords()
hilbert(level - 1, -angle)
t.rt(angle)
The problem is that Turtle is so SLOW! Is there any package that is just like Turtle but can do commands much faster?
I reimplemented the turtle class as suggested by thirtyseven. It is consistent with the api. (i.e. when you turn right in this class, it is the same as turning right in turtle.
This does not implement all the methods in the api, only common ones. (And the ones you used).
However, it's short and fairly straightforward to extend. Also, it keeps track of all of the points it has been to. It does this by adding an entry to pointsVisited every time you call forward, backward, or setpos (or any of the aliases for those functions).
import math
class UndrawnTurtle():
def __init__(self):
self.x, self.y, self.angle = 0.0, 0.0, 0.0
self.pointsVisited = []
self._visit()
def position(self):
return self.x, self.y
def xcor(self):
return self.x
def ycor(self):
return self.y
def forward(self, distance):
angle_radians = math.radians(self.angle)
self.x += math.cos(angle_radians) * distance
self.y += math.sin(angle_radians) * distance
self._visit()
def backward(self, distance):
self.forward(-distance)
def right(self, angle):
self.angle -= angle
def left(self, angle):
self.angle += angle
def setpos(self, x, y = None):
"""Can be passed either a tuple or two numbers."""
if y == None:
self.x = x[0]
self.y = y[1]
else:
self.x = x
self.y = y
self._visit()
def _visit(self):
"""Add point to the list of points gone to by the turtle."""
self.pointsVisited.append(self.position())
# Now for some aliases. Everything that's implemented in this class
# should be aliased the same way as the actual api.
fd = forward
bk = backward
back = backward
rt = right
lt = left
setposition = setpos
goto = setpos
pos = position
ut = UndrawnTurtle()
Yes, this is possible. The simplest way is to instantiate a root Tkinter window, withdraw it, and then use it as the master window for a RawTurtle's Canvas instance.
Example:
import Tkinter
import turtle
root=Tkinter.Tk()
root.withdraw()
c=Tkinter.Canvas(master=root)
t=turtle.RawTurtle(c)
t.fd(5)
print t.xcor() # outputs 5.0
Unfortunately, this still initiates the graphics system, but no window will appear.
The problem is that Turtle is so SLOW! Is there any package that is
just like Turtle but can do commands much faster?
Yes, turtle can. If we add a TurtleScreen to the tkinter implementation, and use it's tracer() functionality, we can speed things up more than turtle's speed() method. And we can simplify the code greatly by tossing the customizations and simply use turtle's own begin_poly(), end_poly() and get_poly() methods:
from tkinter import Tk, Canvas
from turtle import TurtleScreen, RawTurtle
SIZE = 10
def hilbert(level, angle):
if level == 0:
return
turtle.right(angle)
hilbert(level - 1, -angle)
turtle.forward(SIZE)
turtle.left(angle)
hilbert(level - 1, angle)
turtle.forward(SIZE)
hilbert(level - 1, angle)
turtle.left(angle)
turtle.forward(SIZE)
hilbert(level - 1, -angle)
turtle.right(angle)
root = Tk()
root.withdraw()
canvas = Canvas(master=root)
screen = TurtleScreen(canvas)
screen.tracer(False) # turn off turtle animation
turtle = RawTurtle(screen)
turtle.begin_poly() # start tracking movements
hilbert(5, 90)
turtle.end_poly() # end tracking movements
print(turtle.get_poly())
This prints all the points in a level 5 Hilbert curve in about 1/3 of a second on my system. Your posted code toke nearly 9 seconds to output a level 4.

Categories