How to display a moving ball trajectory - python

I have a solid-coloured ball moving in a tkinter canvas, using the move(), and update() method to refresh the canvas.
I want to keep track of the ball trajectory onscreen. i.e. - the ball moves to its new position but a line - series of points showing its trajectory - remains onscreen.
How do I do this? (two canvases, one of them transparent??)

Just as suggested, you can paint line that will start from the last position of the ball and end in the current new ball position.
I've made simple app doing so in tkinter, that you can modify to your needs.
It's using only one canvas and you can move ball just by clicking on the canvas. Right mouse click resets the canvas and ball position.
from tkinter import Tk, Canvas, Frame, BOTH
class MovingBall(Frame):
ball_r = 25
x, y = 0, 0 # ball last coords
def __init__(self):
super().__init__()
self.ball = None
self.initUI()
def initUI(self):
self.master.title("Moving Ball")
self.pack(fill=BOTH, expand=1)
self.canvas = Canvas(self)
self.ball = self.paint_ball(self.x, self.y)
self.canvas.pack(fill=BOTH, expand=1)
# Left click on canvas moves the ball
self.canvas.bind("<Button-1>", self.move)
# Right click on canvas reset ball position
self.canvas.bind("<Button-3>", self.reset)
def paint_ball(self, x, y):
return self.canvas.create_oval(x - self.ball_r, y - self.ball_r, x + self.ball_r, y + self.ball_r, fill="red",
outline="silver", width=1)
def paint_path(self, x, y):
return self.canvas.create_line(self.x, self.y, x, y, fill="silver", width=1)
def move(self, event):
# Remove last painted ball
self.canvas.delete(self.ball)
# Add new line to the path
self.paint_path(event.x, event.y)
# Paint new ball at new position
self.ball = self.paint_ball(event.x, event.y)
self.canvas.pack(fill=BOTH, expand=1)
# Store current ball coords
self.x, self.y = event.x, event.y
def reset(self, event):
"""Reset whole scene and put ball to 0,0"""
self.x, self.y = 0, 0
self.canvas.delete("all")
self.ball = self.paint_ball(self.x, self.y)
self.canvas.pack(fill=BOTH, expand=1)
def main():
root = Tk()
MovingBall()
root.geometry("1024x768")
root.mainloop()
if __name__ == '__main__':
main()

Related

Odd Python/Tkinter keybind class errors

Right now I'm attempting to bind the WASD keys to a player so that when pressed, a player will move in specific directions depending on what keys pressed. The problem is, whenever I press any keys, this error comes up and the "player" doesn't move.
File "main.py", line 143, in <module>
player.append(Player())
File "main.py", line 69, in __init__
root.bind('w', self.up)
AttributeError: 'Player' object has no attribute 'up'
the main code is below: (I dont know if some of the smaller bits are needed so apologies)
from tkinter import *
import random, math
root = Tk()
root.minsize(500,500)
class Player:
def __init__(self):
x = random.randint(100, 500)
y = random.randint(0, 500)
self.width = random.randint(75,100)
self.player = canvas.create_oval(x, y, x + self.width, y+self.width, fill="Blue")
self.speed =50/self.width
root.bind('w', self.up)
root.bind('d', self.right)
root.bind('s', self.down)
root.bind('a', self.left)
def getCoordsPlayer(self):
return canvas.coords(self.player)
def deleteplayer(self):
canvas.delete(self.player)
def moveplayer(self):
left, top, right, bottom = self.getCoords()
if left<0 or right > 500:
def left(self, event):
left, top, right, bottom = canvas.coords(self.player)
canvas.move(self.player, -10, 0)
canvas.update()
def right(self, event):
left, top, right, bottom = canvas.coords(self.player)
canvas.move(self.player, 10, 0)
canvas.update()
if top<0 or bottom > 500:
def up(self, event):
left, top, right, bottom = canvas.coords(self.player)
canvas.move(self.player, 0, -10)
canvas.update()
def down(self, event):
left, top, right, bottom = canvas.coords(self.player)
canvas.move(self.player, 0, 10)
canvas.update()
canvas = Canvas(root,width=600,height=600,bg="white")
canvas.place(x=0,y=0)
player = []
for i in range(1):
player.append(Player())

Moving image in python tkinter

I am writing a simple program in which I want the (ball image png) to bounce off the walls. So far, I have writen this code:
import tkinter as tk
root = tk.Tk()
WIDTH = 500
HEIGHT = 500
canvas = tk.Canvas(root,bg="white",width=WIDTH,height=HEIGHT)
canvas.pack()
img = tk.PhotoImage(file="images/ball1.png")
ball = canvas.create_image(0,0,anchor="nw",image=img)
yspeed = 2
xspeed = 2
def move_ball():
global xspeed,yspeed,ball
canvas.move(ball,xspeed,yspeed)
canvas.after(10,move_ball)
move_ball()
root.mainloop()
You can get the current position with the coords method, then code in a check for each of the 4 walls. Here's the first one:
def move_ball():
global xspeed,yspeed,ball
xpos, ypos = canvas.coords(ball)
if xpos + width_of_ball > WIDTH:
# ball hit the right edge, reverse x direction
xspeed *= -1
canvas.move(ball,xspeed,yspeed)
canvas.after(10,move_ball)
This answer is the same thing as #Novel answer (even though I wrote it before I saw theirs). The only difference is in the fact that the logic of update doesn't expect you to make any edits for it to work, it considers both horizontal and vertical directions, and resizing the main window is compensated for.
import tkinter as tk
root = tk.Tk()
root.title('Infinite Bounce Simulator')
root.geometry('400x300+300+300')
xspeed = 4
yspeed = 3
canvas = tk.Canvas(root, highlightthickness=0, bg='#111')
canvas.pack(expand=True, fill='both')
ball = canvas.create_oval((0, 0, 20, 20), fill='red')
def update():
global xspeed, yspeed, ball
canvas.move(ball, xspeed, yspeed)
#Left, Top, Right, Bottom coordinates
l, t, r, b = canvas.coords(ball)
#flip speeds when edges are reached
if r > canvas.winfo_width() or l < 0:
xspeed = -xspeed
if b > canvas.winfo_height() or t < 0:
yspeed = -yspeed
#do it all again in 10 milliseconds
root.after(10, update)
root.after_idle(update)
root.mainloop()

Python Tkinter - Draw Shape to Screen Without Creating New Instance

I'm tying to create a basic program for drawing to the screen by creating ovals every frame when the mouse is clicked. However, as the program runs for a bit, it starts becoming very choppy and the circles stop forming cohesive lines, due to the code not running fast enough to process precise mouse movement.
Here is my code -
import tkinter as tk
DRAW_HEIGHT = 560
DRAW_WIDTH = 560
PALETTE_HEIGHT = 40
def draw_palette(canvas):
canvas.create_rectangle(0, 0, DRAW_WIDTH, PALETTE_HEIGHT, fill = 'light grey', width= 0)
canvas.create_rectangle(DRAW_WIDTH/8, PALETTE_HEIGHT/5, 3*DRAW_WIDTH/8, 4*PALETTE_HEIGHT/5, fill = 'dark grey', width = 1)
canvas.create_rectangle(5*DRAW_WIDTH/8, PALETTE_HEIGHT/5, 7*DRAW_WIDTH/8, 4*PALETTE_HEIGHT/5, fill = 'dark grey',width = 1)
canvas.create_text(DRAW_WIDTH/4, PALETTE_HEIGHT/2, text = 'clear screen') #non-functional
class Brush():
def __init__(self,stroke_size,stroke_color):
self.size = stroke_size
self.color = stroke_color
self.mode = 'draw'
self.pos = (0,0)
self.clicked = False
def render(self,canvas):
if self.clicked:
canvas.create_oval( self.pos.x-self.size/2, self.pos.y-self.size/2,
self.pos.x+self.size/2, self.pos.y+self.size/2,
width = 0, fill = self.color )
def mouse_moved(self,event):
self.pos = event
def mouse_clicked(self,throwaway):
self.clicked = True
def mouse_released(self,throwaway):
self.clicked = False
#set up root window and canvas
root = tk.Tk()
root.geometry('{}x{}'.format(DRAW_WIDTH,DRAW_HEIGHT+PALETTE_HEIGHT))
c = tk.Canvas(root, width = DRAW_WIDTH, height = DRAW_HEIGHT + PALETTE_HEIGHT, bg = 'white')
c.pack()
b = Brush(40,'black')
#bind actions to functions
c.bind("<Button-1>",b.mouse_clicked)
c.bind("<ButtonRelease-1>",b.mouse_released)
c.bind("<Motion>",b.mouse_moved)
#main loop
while 1:
b.render(c)
draw_palette(c)
root.update()
I suppose I'm just asking if there's any way I can speed this up, but specifically I'm wondering if I can draw the shapes to the screen without using create_shape() every time.
For example,
oval = c.create_oval()
while 1:
canvas.draw(oval)
I know you can do something similar with canvas.move(), but I couldn't find anything that fit my situation.
I don't understand why you created loop while 1 and run render() and draw_palette() hundreds of times even if you don't need it.
I draw new circle in mouse_moved() and use root.mainloop() and it runs much better and create smoother line. Probably if I would draw line from previous place to current place or many ovals with some step then I would get even better line
EDIT: I changed little to draw first oval in mouse_click() - so I can see first oval even if I only click and don't move.
import tkinter as tk
# --- constanst ---
DRAW_HEIGHT = 560
DRAW_WIDTH = 560
PALETTE_HEIGHT = 40
# --- classes ---
class Brush():
def __init__(self,stroke_size,stroke_color):
self.size = stroke_size
self.color = stroke_color
self.mode = 'draw'
self.pos = (0,0)
self.clicked = False
def draw(self):
s = self.size/2
c.create_oval(
self.pos.x-s, self.pos.y-s,
self.pos.x+s, self.pos.y+s,
width=0, fill=self.color
)
def mouse_moved(self, event):
if self.clicked:
self.pos = event
self.draw()
def mouse_clicked(self, event):
self.clicked = True
self.pos = event
self.draw()
def mouse_released(self, event):
self.clicked = False
# --- functions ---
def draw_palette(canvas):
canvas.create_rectangle(0, 0, DRAW_WIDTH, PALETTE_HEIGHT, fill='light grey', width=0)
canvas.create_rectangle(DRAW_WIDTH/8, PALETTE_HEIGHT/5, 3*DRAW_WIDTH/8, 4*PALETTE_HEIGHT/5, fill='dark grey', width=1)
canvas.create_rectangle(5*DRAW_WIDTH/8, PALETTE_HEIGHT/5, 7*DRAW_WIDTH/8, 4*PALETTE_HEIGHT/5, fill='dark grey', width=1)
canvas.create_text(DRAW_WIDTH/4, PALETTE_HEIGHT/2, text='clear screen') #non-functional
# --- main ---
#set up root window and canvas
root = tk.Tk()
root.geometry('{}x{}'.format(DRAW_WIDTH, DRAW_HEIGHT+PALETTE_HEIGHT))
c = tk.Canvas(root, width=DRAW_WIDTH, height=DRAW_HEIGHT+PALETTE_HEIGHT, bg='white')
c.pack()
b = Brush(40, 'black')
#bind actions to functions
c.bind("<Button-1>", b.mouse_clicked)
c.bind("<ButtonRelease-1>", b.mouse_released)
c.bind("<Motion>", b.mouse_moved)
draw_palette(c)
root.mainloop()
EDIT:
I added function which adds ovals if distance between previous and current position is too big and there is gap. Now line is smooth even if mouse moves fast.
import tkinter as tk
# --- constanst ---
DRAW_HEIGHT = 560
DRAW_WIDTH = 560
PALETTE_HEIGHT = 40
# --- classes ---
class Brush():
def __init__(self,stroke_size,stroke_color):
self.size = stroke_size
self.color = stroke_color
self.mode = 'draw'
self.pos = None
self.prev = None
self.clicked = False
def draw_oval(self, x, y):
r = self.size/2 # radius
c.create_oval(x-r, y-r, x+r, y+r, width=0, fill=self.color)
def draw(self):
if self.pos:
self.draw_oval(self.pos.x, self.pos.y)
if self.prev:
# calculate distance between ovals
dx = self.pos.x - self.prev.x
dy = self.pos.y - self.prev.y
max_diff = max(abs(dx), abs(dy))
# add ovals if distance bigger then some size of oval (tested with //4, //8, //6, //5)
if max_diff > (self.size//6):
# how many ovals to add
parts = max_diff//(self.size//6)
# distance between ovals
step_x = dx/parts
step_y = dy/parts
# add ovals except first which is already on canvas
for i in range(1, parts):
x = self.pos.x - i*step_x
y = self.pos.y - i*step_y
self.draw_oval(x, y)
def mouse_moved(self, event):
if self.clicked:
self.prev = self.pos
self.pos = event
self.draw()
def mouse_clicked(self, event):
self.clicked = True
self.prev = None
self.pos = event
self.draw()
def mouse_released(self, event):
self.clicked = False
self.prev = None
self.pos = None
# --- functions ---
def draw_palette(canvas):
canvas.create_rectangle(0, 0, DRAW_WIDTH, PALETTE_HEIGHT, fill='light grey', width=0)
canvas.create_rectangle(DRAW_WIDTH/8, PALETTE_HEIGHT/5, 3*DRAW_WIDTH/8, 4*PALETTE_HEIGHT/5, fill='dark grey', width=1)
canvas.create_rectangle(5*DRAW_WIDTH/8, PALETTE_HEIGHT/5, 7*DRAW_WIDTH/8, 4*PALETTE_HEIGHT/5, fill='dark grey', width=1)
canvas.create_text(DRAW_WIDTH/4, PALETTE_HEIGHT/2, text='clear screen') #non-functional
# --- main ---
#set up root window and canvas
root = tk.Tk()
root.geometry('{}x{}'.format(DRAW_WIDTH, DRAW_HEIGHT+PALETTE_HEIGHT))
c = tk.Canvas(root, width=DRAW_WIDTH, height=DRAW_HEIGHT+PALETTE_HEIGHT, bg='white')
c.pack()
b = Brush(40, 'black')
#bind actions to functions
c.bind("<Button-1>", b.mouse_clicked)
c.bind("<ButtonRelease-1>", b.mouse_released)
c.bind("<Motion>", b.mouse_moved)
draw_palette(c)
root.mainloop()

how to draw a rectangle with python?

I want to draw a rectangle on the image I used this code :
import Tkinter as tk
class draw_rect(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.x = self.y = 0
self.canvas = tk.Canvas(self, width=400, height=400,cursor="cross")
self.canvas.pack(side="top", fill="both", expand=True)
self.canvas.bind("<ButtonPress-1>", self.on_button_press)
self.canvas.bind("<ButtonRelease-1>", self.on_button_release)
def on_button_press(self, event):
self.x = event.x
self.y = event.y
def on_button_release(self, event):
x0,y0 = (self.x, self.y)
x1,y1 = (event.x, event.y)
self.canvas.create_rectangle(x0,y0,x1,y1, fill="RED")
if __name__ == "__main__":
app = draw_rect()
app.mainloop()
the issue is it draws more than one rectangle, I want if I draw a new rectangle the previous one deleted.
one more thing I want to save the rectangle coordinate (x0,x1,y0,y1) that I can use it later.
Thank you.
You can use canvas.delete("all") to clear, and the coordinates are already saved in a variable.
def on_button_press(self, event):
self.x = event.x
self.y = event.y
self.canvas.delete("all")
def on_button_release(self, event):
x0,y0 = (self.x, self.y)
x1,y1 = (event.x, event.y)
self.canvas.create_rectangle(x0,y0,x1,y1, fill="RED")
print(x0,y0,x1,y1) # You can write to file, or store as lists or do w/e with these

"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