ipycanvas displaying final stroke_lines thoughout animation - jupyter-lab

So I was playing with animating some Bezier curves - just part of learning how to use ipycanvas (0,10,2) -- The animation I produced is really hurting my head. What I expected to see was a set of straight lines between 4 Bezier control points "bouncing" around the canvas with the Bezier curve moving along with them.
I did get the moving Bezier curve -- BUT the control points stayed static. Even stranger they were static in the final position and the curve came to meet them.
Now sometimes Python's structures and references can get a little tricky and so you can sometimes get confusing results if you are not really thinking it through -- and this totally could be what's going on - but I am at a loss.
So to make sure I was not confused I printed the control points (pts) at the beginning and then displayed them to the canvas. This confirmed my suspicion. Through quantum tunneling or some other magic time travel the line canvas.stroke_lines(pts) reaches into the future and grabs the pts array as it will exist in the future and keeps the control points in their final state.
Every other use of pts uses the current temporal state.
So what I need to know is A) The laws of physics are safe and I am just too dumb to understand my own code. B) There is some odd bug in ipycanvas that I should report. C) How to monetize this time-traveling code -- like, could we use it to somehow factor large numbers?
from ipycanvas import Canvas, hold_canvas
import numpy as np
def rgb_to_hex(rgb):
if len(rgb) == 3:
return '#%02x%02x%02x' % rgb
elif len(rgb) == 4:
return '#%02x%02x%02x%02x' % rgb
def Bezier4(t, pts):
p = t**np.arange(0, 4,1)
M=np.matrix([[0,0,0,1],[0,0,3,-3],[0,3,-6,3],[1,-3,3,-1]])
return np.asarray((p*M*pts))
canvas = Canvas(width=800, height=800)
display(canvas) # display the canvas in the output cell..
pts = np.random.randint(50, 750, size=[4, 2]) #choose random starting point
print(pts) #print so we can compare with ending state
d = np.random.uniform(-4,4,size=[4,2]) #some random velocity vectors
c = rgb_to_hex(tuple(np.random.randint(75, 255,size=3))) #some random color
canvas.font = '16px serif' #font for displaying the changing pts array
with hold_canvas(canvas):
for ani in range(300):
#logic to bounce the points about...
for n in range(0,len(pts)):
pts[n]=pts[n] + d[n]
if pts[n][0] >= 800 or pts[n][0] <= 0 :
d[n][0] = - d[n][0]
if pts[n][1] >= 800 or pts[n][1] <= 0 :
d[n][1] = - d[n][1]
#calculate the points needed to display a bezier curve
B = [(Bezier4(i, pts)).ravel() for i in np.linspace(0,1,15)]
#begin display output....
canvas.clear()
#first draw bezier curve...
canvas.stroke_style = c
canvas.stroke_lines(B)
#Now draw control points
canvas.stroke_style = rgb_to_hex((255,255,128, 50))
canvas.stroke_lines(pts)
#print the control points to the canvas so we can see them move
canvas.stroke_style = rgb_to_hex((255,255,128, 150))
canvas.stroke_text(str(pts), 10, 32)
canvas.sleep(20)
In all seriousness, I have tried to think through what can be happening and I am coming up blank. Since ipycanvas is talking to the browser/javascript maybe all of the data for the frames are rendered first and the array used to hold the pts data for the stroke_lines ends up with the final values... Whereas the B array is recreated in each loop... It's a guess.

There are two ways to get the code to behave as expected and avoid the unsightly time-traveling code. The first way is to switch the location of the line with hold_canvas(canvas): to inside the loop. This however renders the canvas.sleep(20) line rather useless.
canvas = Canvas(width=800, height=800)
display(canvas)
pts = np.random.randint(50, 750, size=[4, 2])
print(pts)
d = np.random.uniform(-8,8,size=[4,2])
c = rgb_to_hex(tuple(np.random.randint(75, 255,size=3)))
canvas.font = '16px serif'
#with hold_canvas(canvas):
for ani in range(300):
with hold_canvas(canvas):
for n in range(0,len(pts)):
if pts[n][0] > 800 or pts[n][0] < 0 :
d[n][0] = -d[n][0]
if pts[n][1] > 800 or pts[n][1] < 50 :
d[n][1] = -d[n][1]
pts[n]=pts[n] + d[n]
B = [(Bezier4(i, pts)).ravel() for i in np.linspace(0,1,25)]
canvas.clear()
canvas.stroke_style = c
canvas.stroke_lines(B)
canvas.stroke_style = rgb_to_hex((255,255,128, 50))
#pts2 = np.copy(pts)
canvas.stroke_lines(pts)
canvas.fill_style = rgb_to_hex((255,255,255, 150))
canvas.fill_circles(pts.T[0], pts.T[1],np.array([4]*4))
canvas.stroke_style = rgb_to_hex((255,255,128, 150))
canvas.fill_text(str(pts), 10, 32)
sleep(20/1000)
#canvas.sleep(20)
In this version, the control lines are updated as expected. This version is a little more "real time" and thus the sleep(20/1000) is needed to
The other way to do it would be just to ensure that a copy of pts is made and passed to canvas.stroke_lines:
canvas = Canvas(width=800, height=800)
display(canvas)
pts = np.random.randint(50, 750, size=[4, 2])
print(pts)
d = np.random.uniform(-8,8,size=[4,2])
c = rgb_to_hex(tuple(np.random.randint(75, 255,size=3)))
canvas.font = '16px serif'
with hold_canvas(canvas):
for ani in range(300):
#with hold_canvas(canvas):
for n in range(0,len(pts)):
if pts[n][0] > 800 or pts[n][0] < 0:
d[n][0] = -d[n][0]
if pts[n][1] > 800 or pts[n][1] < 50:
d[n][1] = -d[n][1]
pts[n]=pts[n] + d[n]
B = [(Bezier4(i, pts)).ravel() for i in np.linspace(0,1,35)]
canvas.clear()
canvas.stroke_style = c
canvas.stroke_lines(B)
canvas.stroke_style = rgb_to_hex((255,255,128, 50))
pts2 = np.copy(pts)
canvas.stroke_lines(pts2)
canvas.fill_style = rgb_to_hex((255,255,255, 150))
canvas.fill_circles(pts.T[0], pts.T[1],np.array([4]*4))
canvas.stroke_style = rgb_to_hex((255,255,128, 150))
canvas.fill_text(str(pts), 10, 32)
#sleep(20/1000)
canvas.sleep(20)
I could not actually find the data passed between the python and the browser but it seems pretty logical that what is happening is that python is finishing its work (and ani loop) before sending the widget instructions on what to draw, and the pts values sent are the final ones.
(yes I know there is a bug in the bouncing logic)

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

mplcursors: show and highlight coordinates of nearby local extreme

I have code that shows the label for each point in a matplotlib scatterplot using mplcursors, similar to this example. I want to know how to, form a list of values, make a certain point stand out, as in if I have a graph of points y=-x^2. When I go near the peak, it shouldn't show 0.001, but 0 instead, without the trouble needing to find the exact mouse placement of the top. I can't solve for each point in the graph, as I don't have a specific function.
Supposing the points in the scatter plot are ordered, we can investigate whether an extreme in a nearby window is also an extreme in a somewhat larger window. If, so we can report that extreme with its x and y coordinates.
The code below only shows the annotation when we're close to a local maximum or minimum. It also temporarily shows a horizontal and vertical line to indicate the exact spot. The code can be a starting point for many variations.
import matplotlib.pyplot as plt
import mplcursors
import numpy as np
near_window = 10 # the width of the nearby window
far_window = 20 # the width of the far window
def show_annotation(sel):
ind = sel.target.index
near_start_index = max(0, ind - near_window)
y_near = y[near_start_index: min(N, ind + near_window)]
y_far = y[max(0, ind - far_window): min(N, ind + far_window)]
near_max = y_near.max()
far_max = y_far.max()
annotation_str = ''
if near_max == far_max:
near_argmax = y_near.argmax()
annotation_str = f'local max:\nx:{x[near_start_index + near_argmax]:.3f}\ny:{near_max:.3f}'
maxline = plt.axhline(near_max, color='crimson', ls=':')
maxline_x = plt.axvline(x[near_start_index+near_argmax], color='grey', ls=':')
sel.extras.append(maxline)
sel.extras.append(maxline_x)
else:
near_min = y_near.min()
far_min = y_far.min()
if near_min == far_min:
near_argmin = y_near.argmin()
annotation_str = f'local min:\nx:{x[near_start_index+near_argmin]:.3f}\ny:{near_min:.3f}'
minline = plt.axhline(near_min, color='limegreen', ls=':')
minline_x = plt.axvline(x[near_start_index + near_argmin], color='grey', ls=':')
sel.extras.append(minline)
sel.extras.append(minline_x)
if len(annotation_str) > 0:
sel.annotation.set_text(annotation_str)
else:
sel.annotation.set_visible(False) # hide the annotation
# sel.annotation.set_text(f'x:{sel.target[0]:.3f}\n y:{sel.target[1]:.3f}')
N = 500
x = np.linspace(0, 100, 500)
y = np.cumsum(np.random.normal(0, 0.1, N))
box = np.ones(20) / 20
y = np.convolve(y, box, mode='same')
scat = plt.scatter(x, y, s=1)
cursor = mplcursors.cursor(scat, hover=True)
cursor.connect('add', show_annotation)
plt.show()

How to get cursor coordinates relative to matrix scale in pyglet/opengl?

I am making a 2D game in pyglet and use both glTranslatef and glScalef:
def background_motion(dt):
if stars.left:
pyglet.gl.glTranslatef(stars.speed, 0, 0)
stars.translation[0] += stars.speed
if stars.right:
pyglet.gl.glTranslatef(-stars.speed, 0, 0)
stars.translation[0] -= stars.speed
if stars.up:
pyglet.gl.glTranslatef(0, -stars.speed, 0)
stars.translation[1] -= stars.speed
if stars.down:
pyglet.gl.glTranslatef(0, stars.speed, 0)
stars.translation[1] += stars.speed
pyglet.clock.schedule_interval(background_motion, 0.05)
#window.event
def on_mouse_scroll(x, y, scroll_x, scroll_y):
if scroll_y > 0:
stars.scale += 0.01
elif scroll_y < 0:
stars.scale -= 0.01
#window.event
def on_draw():
window.clear()
pyglet.gl.glScalef(stars.scale,stars.scale, 1, 1)
stars.image.draw()
for s in game.ships:
s.draw()
pyglet.gl.glPushMatrix()
pyglet.gl.glLoadIdentity()
#HUD Start
overlay.draw(stars.image.x,stars.image.y,game.ships,stars.scale,stars.image.width)
if game.pause:
pause_text.draw()
#HUD End
pyglet.gl.glPopMatrix()
stars.scale = 1
However I also need the cursor coordinates relative to the background. For the movement I simply added the translation onto the x y coordinates which works however only when I don't scale the matrix:
#window.event
def on_mouse_motion(x, y, dx, dy):
if player.course_setting:
player.projected_heading = (x - stars.translation[0],y -stars.translation[1])
How can I get the cursor coordinates accounting for scale?
You'll have to unproject the pointer position. Projection happens as following:
p_eye = M · p
p_clip = P · p_eye
at this point the primitive is clipped, but we can ignore this for the moment. After clipping comes the homogenous divide, which brings the coordinates into NDC space, i.e. the viewport is treated as a cuboid of dimensions [-1,1]×[-1,1]×[0,1]
p_NDC = p_clip / p_clip.w
From there it's mapped into pixel dimensions. I'm going to omit this step here.
Unprojecting is doing these operations in reverse. There's a small trick in there, regarding the homogenous divide, though; this is kind of an "antisymmetric" (not the proper term for this, but it gets across the point) operation, and happens at the end, for each projection and unprojection. Unprojection hence is
p_NDC.w = 1
p_eye' = inv(P)·p_NDC
p' = inv(M)·p_eye'
p = p' / p'.w
All of this has been wrapped into unproject functions for your convenience by GLU (if you insist on using the fixed function matrix stack) or GLM – but not my linmath.h, though.

Creating a symmetrical grid of random size squares in Python3/Tkinter

I have a question revolving around what would be a viable approach to placing out random-sized squares on a symmetrical, non-visible grid on a tkinter-canvas. I'm going to explain it quite thoroughly as it's a somewhat proprietary problem.
This far I've tried to solve it mostly mathematically. But I've found it to be quite a complex problem, and it seems reasonable that there would be a better approach to take it on than what I've tried.
In its most basic form the code looks like this:
while x_len > canvas_width:
xpos = x_len + margin
squares[i].place(x=xpos, y=ypos)
x_len += square_size + space
i += 1
x_len is the total width of all the squares on a given row, and resets when exiting the while-loop (eg. when x_len > window width), among with xpos (the position on X), as well as altering Y-axis to create a new row.
When placing same-size squares it looks like this:
So far so good.
However when the squares are of random-size it looks like this (at best):
The core problem, beyond that the layout can be quite unpredictable, is that the squares aren't centered to the "invisible grid" - because there is none.
So to solve this I've tried an approach where I use a fixed distance and a relative distance based on every given square. This yields satisficing results for the Y-axis on the first row, but not on the X-axis, nor the following rows on Y.
See example (where first row is centered on Y, but following rows and X is not):
So with this method I'm using a per-square alteration in both Y- and X-axis, based on variables that I fetch from a list that contain widths for all of the generated squares.
In it's entirety it looks like this (though it's work in progress so it's not very well optimized):
square_widths = [60, 75, 75, 45...]
space = square_size*0.5
margin = (square_size+space)/2
xmax = frame_width - margin - square_size
xmin = -1 + margin
def iterate(ypos, xpos, x_len):
y = ypos
x = xpos
z = x_len
i=0
m_ypos = 0
extra_x = 0
while len(squares) <= 100:
n=-1
# row_ypos alters y for every new row
row_ypos += 200-square_widths[n]/2
# this if-statement is not relevant to the question
if x < 0:
n=0
xpos = x
extra_x = x
x_len = z
while x_len < xmax:
ypos = row_ypos
extra_x += 100
ypos = row_ypos + (200-square_widths[n])/2
xpos = extra_x + (200-square_widths[n])/2
squares[i].place(x=xpos, y=ypos)
x_len = extra_x + 200
i += 1
n += 1
What's most relevant here is row_ypos, that alters Y for each row, as well as ypos, that alters Y for each square (I don't have a working calculation for X yet). What I would want to achieve is a similar result that I get for Y-axis on the first row; on all rows and columns (eg. both in X and Y). To create a symmetrical grid with squares of different sizes.
So my questions are:
Is this really best practice to solve this?
If so - Do you have any tips on decent calculations that would do the trick?
If not - How would you approach this?
A sidenote is that it has to be done "manually" and I can not use built-in functions of tkinter to solve it.
Why don't you just use the grid geometry manager?
COLUMNS = 5
ROWS = 5
for i in range(COLUMNS*ROWS):
row, col = divmod(i, COLUMNS)
l = tk.Label(self, text=i, font=('', randint(10,50)))
l.grid(row=row, column=col)
This will line everything up, but the randomness may make the rows and columns different sizes. You can adjust that with the row- and columnconfigure functions:
import tkinter as tk
from random import randint
COLUMNS = 10
ROWS = 5
class GUI(tk.Frame):
def __init__(self, master=None, **kwargs):
tk.Frame.__init__(self, master, **kwargs)
labels = []
for i in range(COLUMNS*ROWS):
row, col = divmod(i, COLUMNS)
l = tk.Label(self, text=i, font=('', randint(10,50)))
l.grid(row=row, column=col)
labels.append(l)
self.update() # draw everything
max_width = max(w.winfo_width() for w in labels)
max_height = max(w.winfo_height() for w in labels)
for column in range(self.grid_size()[0]):
self.columnconfigure(col, minsize=max_width) # set all columns to the max width
for row in range(self.grid_size()[1]):
self.rowconfigure(row, minsize=max_height) # set all rows to the max height
def main():
root = tk.Tk()
win = GUI(root)
win.pack()
root.mainloop()
if __name__ == "__main__":
main()
I found the culprit that made the results not turn out the way expected, and it wasn't due to the calculations. Rather it turned out that the list I created didn't put the squares in correct order (which I should know since before).
And so I fetched the width from the raw data itself, which makes a lot more sense than creating a list.
The function now looks something like this (again, it's still under refinement, but I just wanted to post this, so that people don't waste their time in coming up with solutions to an already solved problem :)):
def iterate(ypos, xpos, x_len):
y = ypos
x = xpos
z = x_len
i=0
while len(squares) <= 100:
n=0
if y > 1:
ypos -= max1 + 10
if y < 0:
if ypos < 0:
ypos=10
else:
ypos += max1 + 10 #+ (max1-min1)/2
if x < 0:
n=0
xc=0
xpos = x
x_len = z
while x_len < xmax:
yc = ypos + (max1-squares[i].winfo_width())/2
if xpos <= 0:
xpos = 10
else:
xpos += max1 + 10
xc = xpos + (max1-squares[i].winfo_width())/2
squares[i].place(x=xc, y=yc)
x_len += max1 + 10
print (x_len)
i += 1
n += 1

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