1

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.

nickdmax
  • 539
  • 2
  • 4
  • 11
  • So changing the range of the ani loop to say 30000 does take a while to generate (I would also change the `canvas.sleep` to something small like 2 so you don't have to wait forever for the animation to finish). This lends support to my theory that the javascript array for pts exists in its final state when the animation starts. – nickdmax Jan 09 '22 at 06:15
  • Ok -- my guess has to be somewhat correct -- you can get the expected display by using a copy of the numpy array: `pts2 = np.copy(pts)` – nickdmax Jan 09 '22 at 06:31

1 Answers1

0

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)

nickdmax
  • 539
  • 2
  • 4
  • 11
  • The bug in the bouncing logic is because the `d` vectors use floating point numbers but the points use integer values -- changing the `d` vectors like so fixes it: `d = np.random.randint(-8,8,size=[4,2])` – nickdmax Jan 09 '22 at 07:52