Tkinter create shrinking circle on each mouse click, how to make it work with multiple clicks? - python-3.x

I am creating a simple program which draws a shrinking circle of random color on every clicked location by each mouse click. Each click creates a circle of diameter 50 which starts shrinking till 0 immediately. Each click is supposed to create new shrinking circle.
However, my program stops shrinking of first circle after I click and create another circle. It completely shrinks only the last created circle. All others remain still.
I believe the problem lies in function itself. It calls the same function which is not finished. How to make it run multiple times (on each click separately)? Or do I have it wrong with local and global variables?
Here is my code so far:
import tkinter
import random
c = tkinter.Canvas(width = 400, height = 300)
c.pack()
def klik(event):
global x, y, farba, circ, r
r = 50 #circle diameter
x, y = event.x, event.y #clicked position
color = '#{:06x}'.format(random.randrange(256 ** 3)) #random color picker
circ = c.create_oval(x - r, y - r, x + r, y + r, fill=color) #print circle
print(x, y, farba) #check clicked coordinates, not important
if r < 50: #reset size after each circle
r = 50
shrink()
def shrink():
global circ, x, y, r
print(r) #check if countdown runs correctly
if r > 0:
r -= 1 #diameter shrinking
c.coords(circ, x-r, y-r, x+r, y+r) #changing circle size
c.after(100, shrink) #timer, size 1pt smaller until size is 0
c.bind('<Button-1>', klik)
tkinter.mainloop()

If you move everything into a class then each circle will be its own instance and will not interfere with each other.
Take a look at the below modified version of your code. It is probably not perfect but should be a good foundation for you to work with.
import tkinter
import random
c = tkinter.Canvas(width = 400, height = 300)
c.pack()
class create_circles():
def __init__(self, event):
self.r = 50
self.x, self.y = event.x, event.y
self.color = '#{:06x}'.format(random.randrange(256 ** 3))
self.circ = c.create_oval(self.x - self.r, self.y - self.r, self.x + self.r, self.y + self.r, fill=self.color)
self.shrink()
def shrink(self):
if self.r > 0:
self.r -= 1
c.coords(self.circ, self.x-self.r, self.y-self.r, self.x+self.r, self.y+self.r)
c.after(100, self.shrink)
c.bind('<Button-1>', create_circles)
tkinter.mainloop()
There is another way you can do this outside of a class.
You can use a nested function and avoid global. Your issues in your question is actually being caused because everything relies on global variables.
Try this below code for a non-class option.
import tkinter
import random
c = tkinter.Canvas(width = 400, height = 300)
c.pack()
def klik(event):
r = 50
x, y = event.x, event.y
color = '#{:06x}'.format(random.randrange(256 ** 3))
circ = c.create_oval(x - r, y - r, x + r, y + r, fill=color)
def shrink(r, x, y, color, circ):
if r > 0:
r -= 1
c.coords(circ, x-r, y-r, x+r, y+r)
c.after(100, shrink, r, x, y, color, circ)
shrink(r, x, y, color, circ)
c.bind('<Button-1>', klik)
tkinter.mainloop()

As noted, you do not need classes to solve this nor nested functions. The key, as #LioraHaydont was hinting at, is you need to use local, rather than global variables:
import tkinter as tk
from random import randrange
def klik(event):
r = 50 # circle radius
x, y = event.x, event.y # clicked position
color = '#{:06x}'.format(randrange(256 ** 3)) # random color picker
c = canvas.create_oval(x - r, y - r, x + r, y + r, fill=color) # print circle
canvas.after(100, shrink, c, x, y, r)
def shrink(c, x, y, r):
if r > 0:
r -= 1 # radius shrinking
canvas.coords(c, x - r, y - r, x + r, y + r) # changing circle size
canvas.after(100, shrink, c, x, y, r) # timer, size 1pt smaller until size is 0
canvas = tk.Canvas(width=400, height=300)
canvas.pack()
canvas.bind('<Button-1>', klik)
tk.mainloop()

Related

Interference of canvas items and problem in setting coordinates

I'm working on an animation of a moving object, while drawing it's path.
I want to draw the pixels in which the center of the object went through... but guess what? python decided to set the NW anchor of the image with the coordinates I send, instead of the center. I infer it has something to do with the pixels I draw simultaneously (creating a one pixel rectangle). so the image appear on the right of the path bellow... I want the center of it to be on the top of the pixels... adding the main of the code:
from tkinter import*
import time
dt = 0.01
clock_place = (500, 10)
def round_two(t, t0):
return round((t-t0)*100)/100
def round_three(t, t0):
return round((t-t0)*1000)/1000
# showing 'real time motion' for a known path (also cyclic), with
# parametric representation
def paint_known_path(x_pos, y_pos, t_0):
window = Tk()
canvas = Canvas(window, height=700, width=1000)
canvas.pack()
canvas.config(background='black')
tennis_ball = PhotoImage(file='tennis ball.png')
t = t_0
x = x_pos(t_0)
y = y_pos(t_0)
particle = canvas.create_image(x, y, image=tennis_ball)
clock = canvas.create_text(clock_place, text=round_two(t, t_0),
fill='white')
while True:
canvas.create_rectangle(x, y, x, y, outline='red')
canvas.itemconfig(clock, text=round_two(t, t_0))
t += dt
x = x_pos(t)
y = y_pos(t)
canvas.moveto(particle, x, y)
window.update()
if x == x_pos(t_0) and y == y_pos(t_0):
if t - t_0 > 100*dt:
break
time.sleep(dt)
canvas.create_text((500, 100), text='orbit duration: ' +
str(round_three(t, t_0)), fill='white')
window.mainloop()
It turns out to be quite a bit require, but here is the main completion components.
The first additional part that you need to add:
# print('the ten ball height', tennis_ball.height(), tennis_ball.width())
# tennis ball dimensions
tb_hght = tennis_ball.height()
tb_wdth = tennis_ball.width()
mid_point_x = x + tennis_ball.height() / 2
mid_point_y = y + tennis_ball.width() / 2
Secondly, also needed to add some functions to for x_pos and y_pos like this (these are just example functions to make the code work):
def x_pos(a):
# any function of t,
return 100
def y_pos(a):
# any function of t,
return 100
Furthermore, you need to call the function at the end like this:
paint_known_path(x_pos,y_pos,0)
Finally, need to add the mid_point_x and mid_point_y to the path that is drawn (as these will be the image centre points).

How to get canvas current size with guiZero

I am aiming for make GUI that changes depending on the canvas size. I need to be able to actively check the window size so i know when to display extras.
Using
Python 3.8.2
GuiZero
You can use tkinter event <Configure> on the canvas:
def on_resize(event):
print(app.height)
...
app.tk.bind('<Configure>', on_resize)
I was finally able to make something but it does throw and error after the app quits.
w=None
while True:
x=app.tk.winfo_height()
if x!=w:
print(x)
w=app.tk.winfo_height()
app.update()
I came across this problem when creating a digital etch-a-sketch, using a Raspberry Pi and two potentiometers as the horizontal and vertical controls. How to get the current size of the canvas? Annoyingly when you set height and width to "fill" and then try to interrogate these values all you get is "fill", which is no use if you're trying to determine the upper bounds of the available canvas. I dug down into the object hierarchy and discovered that .tk_winfo_height() and .tk.winfo_width() return integer values. For this purpose I've removed the code that reacts to the twiddling of the potentiometers and put a row of buttons at the bottom of the screen to control the vertical and horizontal movement.
from guizero import App, Box, Drawing, PushButton
x = 0
y = 0
def clear_screen():
drawing.clear()
def move_left():
global x, y
if x > 0 :
drawing.line(x, y, x - 1, y)
x = x - 1
def move_right():
global x, y
if x < drawing.tk.winfo_width() :
drawing.line(x, y, x + 1, y)
x = x + 1
def move_up():
global x, y
if y > 0 :
drawing.line(x, y, x, y - 1)
y = y - 1
def move_down():
global x, y
if y < drawing.tk.winfo_height() :
drawing.line(x, y, x, y + 1)
y = y + 1
app = App()
drawing = Drawing(app, height="fill", width="fill")
drawing.bg="white"
bbox = Box(app, align="bottom")
lbtn = PushButton(bbox, align="left", command=move_left, text="Left")
ubtn = PushButton(bbox, align="left", command=move_up, text="Up")
cbtn = PushButton(bbox, align="left", command=clear_screen, text="Clear")
rbtn = PushButton(bbox, align="left", command=move_right, text="Right")
dbtn = PushButton(bbox, align="left", command=move_down, text="Down")
app.display()

Animating multiple Circles in each frames in Python

I am trying to create the animation in this video using Python. But I stuck on the very first step. Till now I've created a Circle and a point rotating around its circumference. My code is given below. Now I want to plot the y values corresponding to x=np.arange(0, I*np.pi, 0.01) along the x-axis (as shown in update() function in the code). For this I have to define another function to plot these x and y and pass that function inside a new animation.FuncAnimation().
Is there any way to plot everything using only the update() function?
Note I have found a code of this animation in here. But it is written in Java!
My Code
import matplotlib.pyplot as plt
from matplotlib import animation
import numpy as np
W = 6.5
H = 2
radius = 1
I = 2
T = 3
N = 2
plt.style.use(['ggplot', 'dark_background'])
def create_circle(x, y, r):
circle = plt.Circle((x, y), radius=r, fill=False, alpha=0.7, color='w')
return circle
def create_animation():
fig = plt.figure()
ax = plt.axes(xlim=(-2, W + 2), ylim=(-H, H))
circle = create_circle(0, 0, radius)
ax.add_patch(circle)
line1, = ax.plot(0, 1, marker='o', markersize=3, color='pink', alpha=0.7)
def update(theta):
x = radius * np.cos(theta)
y = radius * np.sin(theta)
line1.set_data([0, x], [0, y])
return line1,
anim = []
anim.append(animation.FuncAnimation(fig, update,
frames=np.arange(0, I * np.pi, 0.01),
interval=10, repeat=True))
# anim.append(animation.FuncAnimation(fig, update_line, len(x),
# fargs=[x, y, line, line1], interval=10))
plt.grid(False)
plt.gca().set_aspect('equal')
plt.gca().spines['left'].set_visible(False)
plt.gca().spines['top'].set_visible(False)
plt.gca().spines['right'].set_visible(False)
plt.gca().spines['bottom'].set_visible(False)
plt.gca().set_xticks([])
plt.gca().set_yticks([])
plt.show()
if __name__ == '__main__':
create_animation()
Edit. I've improved the task by defining a global variable pos and changing the update() function in the following manner ...The animation now looks better but still having bugs!
Improved Portion
plot, = ax.plot([], [], color='w', alpha=0.7)
level = np.arange(0, I * np.pi, 0.01)
num = []
frames = []
for key, v in enumerate(level):
num.append(key)
frames.append(v)
def update(theta):
global pos
x = radius * np.cos(theta)
y = radius * np.sin(theta)
wave.append(y)
plot.set_data(np.flip(level[:pos] + T), wave[:pos])
line1.set_data([0, x], [0, y])
pos += 1
return line1, plot,
Edit Till now I've done the following:
def update(theta):
global pos
x, y = 0, 0
for i in range(N):
prev_x = x
prev_y = y
n = 2 * i + 1
rad = radius * (4 / (n * np.pi))
x += rad * np.cos(n * theta)
y += rad * np.sin(n * theta)
wave.append(y)
circle = create_circle(prev_x, prev_y, rad)
ax.add_patch(circle)
plot.set_data(np.flip(level[:pos] + T), wave[:pos])
line2.set_data([x, T], [y, y])
line1.set_data([prev_x, x], [prev_y, y])
pos += 1
return line1, plot, line2,
Output
Please help to correct this animation. Or, is there any efficient way to do this animation?
Edit Well, now the animation is partially working. But there is a little issue: In my code (inside the definition of update()) I have to add circles centered at (prev_x, prev_y) of radius defined as rad for each frame. For this reason I try to use a for loop in the definition of update() but then all the circles remains in the figure (see the output below). But I want one circle in each frame with the centre and radius as mentioned above. Also the same problem is with the plot. I try to use ax.clear() inside the for loop but it didn't work.

Rectangle/Rectangle Collision Detection

I am trying to solve an issue when two rectangles intersect/overlap each other. when this happens, i want to know if intersection is True or False. I found a solution, however it is written in C or C++. I want to write these code in Python.
Here is the source: http://www.jeffreythompson.org/collision-detection/rect-rect.php
This is literally the first line of python code I've ever written (I do know C++ however)
def rectRect(r1x, r1y, r1w, r1h, r2x, r2y, r2w, r2h):
# are the sides of one rectangle touching the other?
return r1x + r1w >= r2x and \ # r1 right edge past r2 left
r1x <= r2x + r2w and \ # r1 left edge past r2 right
r1y + r1h >= r2y and \ # r1 top edge past r2 bottom
r1y <= r2y + r2h # r1 bottom edge past r2 top
IMHO rectRect is a really bad name for the function, I kept it from the linked code however.
Following is simple class that can perform both rectangle-rectangle intersection as well as point to rectangle intersection. The difference between earlier solution is that following snippet allows even detection of rotated rectangles.
import numpy as np
import matplotlib.pyplot as plt
class Rectangle:
def __init__(self, center: np.ndarray, dims: np.ndarray, angle: float):
self.corners = self.get_rect_points(center, dims, angle)
self.area = dims[0] * dims[1]
#staticmethod
def get_rect_points(center: np.ndarray, dims: np.ndarray, angle: float):
"""
returns four corners of the rectangle.
bottom left is the first conrner, from there it goes
counter clockwise.
"""
center = np.asarray(center)
length, breadth = dims
angle = np.deg2rad(angle)
corners = np.array([[-length/2, -breadth/2],
[length/2, -breadth/2],
[length/2, breadth/2],
[-length/2, breadth/2]])
rot = np.array([[np.cos(angle), np.sin(angle)], [-np.sin(angle), np.cos(angle)]])
corners = rot.dot(corners.T) + center[:, None]
return corners.T
def is_point_in_collision(self, p: np.ndarray):
"""
check if a point is in collision with the rectangle.
"""
def area_triangle(a, b, c):
return abs((b[0] * a[1] - a[0] * b[1]) + (c[0] * b[1] - b[0] * c[1]) + (a[0] * c[1] - c[0] * a[1])) / 2
area = 0
area += area_triangle(self.corners[0], p, self.corners[3])
area += area_triangle(self.corners[3], p, self.corners[2])
area += area_triangle(self.corners[2], p, self.corners[1])
area += area_triangle(self.corners[1], p, self.corners[0])
return area > self.area
def is_intersect(self, rect_2: Rectangle):
"""
check if any of the four corners of the
rectangle is in collision
"""
if not np.all([self.is_point_in_collision(c) for c in rect_2.corners]):
return True
return False
def plot_rect(p1, p2, p3, p4, color='r'):
ax.plot([p1[0], p2[0]], [p1[1], p2[1]], color)
ax.plot([p2[0], p3[0]], [p2[1], p3[1]], color)
ax.plot([p3[0], p4[0]], [p3[1], p4[1]], color)
ax.plot([p4[0], p1[0]], [p4[1], p1[1]], color)
mid_point = 0.5 * (p1 + p3)
plt.scatter(mid_point[0], mid_point[1], marker='*')
plt.xlim([-1, 1])
plt.ylim([-1, 1])
Following are two samples:
Sample 1:
ax = plt.subplot(111)
st = Rectangle((0.067, 0.476),(0.61, 0.41), 90)
gripper = Rectangle((-0.367, 0.476),(0.21,0.16), 45)
plot_rect(*st.corners)
plot_rect(*gripper.corners)
plt.show()
print(f"gripper and rectangle intersect: {st.is_intersect(gripper)}")
Sample 2:
ax = plt.subplot(111)
st = Rectangle((0.067, 0.476),(0.61, 0.41), 90)
gripper = Rectangle((-0.167, 0.476),(0.21,0.16), 45)
plot_rect(*st.corners)
plot_rect(*gripper.corners)
plt.show()
print(f"gripper and rectangle intersect: {st.is_intersect(gripper)}")

TkInter python - creating points on a canvas to obtain a Sierpinsky triangle

I want to make a program which plots a Sierpinsky triangle (of any modulo). In order to do it I've used TkInter. The program generates the fractal by moving a point randomly, always keeping it in the sides. After repeating the process many times, the fractal appears.
However, there's a problem. I don't know how to plot points on a canvas in TkInter. The rest of the program is OK, but I had to "cheat" in order to plot the points by drawing small lines instead of points. It works more or less, but it doesn't have as much resolution as it could have.
Is there a function to plot points on a canvas, or another tool to do it (using Python)? Ideas for improving the rest of the program are also welcome.
Thanks. Here's what I have:
from tkinter import *
import random
import math
def plotpoint(x, y):
global canvas
point = canvas.create_line(x-1, y-1, x+1, y+1, fill = "#000000")
x = 0 #Initial coordinates
y = 0
#x and y will always be in the interval [0, 1]
mod = int(input("What is the modulo of the Sierpinsky triangle that you want to generate? "))
points = int(input("How many points do you want the triangle to have? "))
tkengine = Tk() #Window in which the triangle will be generated
window = Frame(tkengine)
window.pack()
canvas = Canvas(window, height = 700, width = 808, bg = "#FFFFFF") #The dimensions of the canvas make the triangle look equilateral
canvas.pack()
for t in range(points):
#Procedure for placing the points
while True:
#First, randomly choose one of the mod(mod+1)/2 triangles of the first step. a and b are two vectors which point to the chosen triangle. a goes one triangle to the right and b one up-right. The algorithm gives the same probability to every triangle, although it's not efficient.
a = random.randint(0,mod-1)
b = random.randint(0,mod-1)
if a + b < mod:
break
#The previous point is dilated towards the origin of coordinates so that the big triangle of step 0 becomes the small one at the bottom-left of step one (divide by modulus). Then the vectors are added in order to move the point to the same place in another triangle.
x = x / mod + a / mod + b / 2 / mod
y = y / mod + b / mod
#Coordinates [0,1] converted to pixels, for plotting in the canvas.
X = math.floor(x * 808)
Y = math.floor((1-y) * 700)
plotpoint(X, Y)
tkengine.mainloop()
If you are wanting to plot pixels, a canvas is probably the wrong choice. You can create a PhotoImage and modify individual pixels. It's a little slow if you plot each individual pixel, but you can get dramatic speedups if you only call the put method once for each row of the image.
Here's a complete example:
from tkinter import *
import random
import math
def plotpoint(x, y):
global the_image
the_image.put(('#000000',), to=(x,y))
x = 0
y = 0
mod = 3
points = 100000
tkengine = Tk() #Window in which the triangle will be generated
window = Frame(tkengine)
window.pack()
the_image = PhotoImage(width=809, height=700)
label = Label(window, image=the_image, borderwidth=2, relief="raised")
label.pack(fill="both", expand=True)
for t in range(points):
while True:
a = random.randint(0,mod-1)
b = random.randint(0,mod-1)
if a + b < mod:
break
x = x / mod + a / mod + b / 2 / mod
y = y / mod + b / mod
X = math.floor(x * 808)
Y = math.floor((1-y) * 700)
plotpoint(X, Y)
tkengine.mainloop()
You can use canvas.create_oval with the same coordinates for the two corners of the bounding box:
from tkinter import *
import random
import math
def plotpoint(x, y):
global canvas
# point = canvas.create_line(x-1, y-1, x+1, y+1, fill = "#000000")
point = canvas.create_oval(x, y, x, y, fill="#000000", outline="#000000")
x = 0 #Initial coordinates
y = 0
#x and y will always be in the interval [0, 1]
mod = int(input("What is the modulo of the Sierpinsky triangle that you want to generate? "))
points = int(input("How many points do you want the triangle to have? "))
tkengine = Tk() #Window in which the triangle will be generated
window = Frame(tkengine)
window.pack()
canvas = Canvas(window, height = 700, width = 808, bg = "#FFFFFF") #The dimensions of the canvas make the triangle look equilateral
canvas.pack()
for t in range(points):
#Procedure for placing the points
while True:
#First, randomly choose one of the mod(mod+1)/2 triangles of the first step. a and b are two vectors which point to the chosen triangle. a goes one triangle to the right and b one up-right. The algorithm gives the same probability to every triangle, although it's not efficient.
a = random.randint(0,mod-1)
b = random.randint(0,mod-1)
if a + b < mod:
break
#The previous point is dilated towards the origin of coordinates so that the big triangle of step 0 becomes the small one at the bottom-left of step one (divide by modulus). Then the vectors are added in order to move the point to the same place in another triangle.
x = x / mod + a / mod + b / 2 / mod
y = y / mod + b / mod
#Coordinates [0,1] converted to pixels, for plotting in the canvas.
X = math.floor(x * 808)
Y = math.floor((1-y) * 700)
plotpoint(X, Y)
tkengine.mainloop()
with a depth of 3 and 100,000 points, this gives:
Finally found a solution: if a 1x1 point is to be placed in pixel (x,y), a command which does it exactly is:
point = canvas.create_line(x, y, x+1, y+1, fill = "colour")
The oval is a good idea for 2x2 points.
Something remarkable about the original program is that it uses a lot of RAM if every point is treated as a separate object.

Resources