jupyter-widgets-contrib / ipycanvas

Interactive Canvas in Jupyter
https://ipycanvas.readthedocs.io/en/latest/
BSD 3-Clause "New" or "Revised" License
690 stars 64 forks source link

Implement requestAnimationFrame for smooth animations #83

Closed ghost closed 3 years ago

ghost commented 4 years ago

Hi Martin, nice job your ipycanvas.

I quickly simplified and ported this codepen to a python notebook. The resulting animation flickers, because from the nb there is no way, afaik, to have the draw() function called by the browser's requestAnimationFrame

Any suggestion on how to implement it? In src/widget.ts and ipycanvas/canvas.py you have the infrastructure to handle callback from canvas events. It seems a good place to start.

Anyway I'm doing some test. I'd appreciate if you could describe how to build ipycanvas in local

This is the ported code:

# -- new cell --
from ipycanvas import Canvas, hold_canvas
import time

def sec_ms():
    tm = time.time()
    t = str(tm)
    ps = t.split('.')
    s = tm % 60
    ms = ps[1][:3]
    return (int(s), int(ms))

ctx = Canvas(width=300, height=300)

def draw():
    ctx.global_composite_operation = 'destination-over'
    ctx.clear_rect(0, 0, 300, 300)

    ctx.fill_style = 'rgba(0, 0, 0, 0.4)'
    ctx.stroke_style = 'rgba(0, 153, 255, 0.4)'

    ctx.begin_path()
    ctx.arc(150, 150, 105, 0, 3.14 * 2, False) # Earth orbit
    ctx.stroke()

    ctx.save()  # save Sun
    ctx.translate(150, 150)

    # Earth
    s, ms = sec_ms()
    ctx.rotate(((2 * 3.14) / 60) * s + ((2 * 3.14) / 60000) * ms)
    ctx.translate(105, 0)
    ctx.begin_path()
    ctx.arc(0,0,10,0,2*3.14)
    ctx.stroke()

    # Moon
    ctx.save()  # save Earth
    ctx.rotate(((2 * 3.14) / 6) * s + ((2 * 3.14) / 6000) * ms)
    ctx.translate(0, 28.5)
    ctx.begin_path()
    ctx.arc(0,0,3.5,0,2*3.14)
    ctx.stroke()
    ctx.restore()  # restore Earth

    ctx.restore()  # restore Sun

ctx

# -- new cell --
for i in range(200):
    draw()
    time.sleep(0.03)
martinRenou commented 4 years ago

Hello!

It would indeed be really nice. I made a small attempt for fast animations on the following PR: https://github.com/martinRenou/ipycanvas/pull/75.

It's basically transpiling the Python code into JavaScript code in order to execute it on the page in a very fast manner (no communication between back-end and fron-end involved during the animation). I am unsure if this implementation will integrate well with the current way of doing things in ipycanvas. There might be some difficult corner cases concerning canvas state... Anyway, it's just a POC and not usable right now (although feel free to give it a try locally).

you have the infrastructure to handle callback from canvas events

Using events might still be slow (you still rely on the communication between back-end and front-end).

martinRenou commented 4 years ago

Concerning your specific animation. You might be able to speed it up by using the MultiCanvas (see the documentation for its usage). Because you only have the earth and sun that move right? The background does not seem to change much. Also I see you import hold_canvas but don't use it, I highly recommend using it, it will speed up you animation even more ;)

for i in range(200):
    with hold_canvas(ctx):
        draw()
    time.sleep(0.03)
ghost commented 4 years ago

Yes, hold_canvas changed everything. Thanks

Transpiling python seems a project on it's own, perhaps not what an ipycanvas user expects from this library. I choose it today because of it's simplicity. Having the Canvas API + a lot of JS example is a big advantage.

Btw, I upvote the snake case to camel case conversion. All the examples on the net about Canvas are of course in camel case javascript

martinRenou commented 4 years ago

Transpiling python seems a project on it's own, perhaps not what an ipycanvas user expects from this library

The idea would be to make it transparent to users though. But agreed, simple is better. I am not 100% sure I want to finish this PR anyway.

Btw, I upvote the snake case to camel case conversion. All the examples on the net about Canvas are of course in camel case javascript

I disagree. I understand the point that examples out there are written in JavaScript (although I already wrote a few Notebook examples so it should be enough for starting playing with it), but this is a Python library and I want to respect the PEP as much as possible.

Note that the example in the fast animation PR is written in Camel case simply because I did not have time to write the translation between Python API to JavaScript API in the transpiler.

martinRenou commented 3 years ago

Closing for triage, please reopen it if needed

bliep commented 2 years ago

Hi Martin, could we reopen this issue?

If you execute the following with the jupyter-server and client on the same machine, it animates 'fairly smooth'. But it's not very smooth. I guess the main thing that prevents the smoothness is the different timings of the monitor refresh (when the browser updates the pixels) and the python sleep (or interpreter fwiw). A callback from requestAnimationFrame and drawing in python (just calling draw) could remove this timing jitter I think.

Any ideas?

from time import sleep
from ipycanvas import Canvas, hold_canvas

canvas = Canvas(width=512, height=512)
display(canvas)

def draw():
    with hold_canvas():
        canvas.fill_style = 'white'
        canvas.fill_rect(t,t,10,10)
        canvas.fill_style = 'black'
        canvas.fill_rect(t+1,t+1,10,10)

t=0
while True: 
    draw()
    t = (t+1)%512
    sleep(1/60.)

Oh, and, many thanks already :) This really is a very nice way for to draw onto a canvas from Python!

martinRenou commented 2 years ago

A callback from requestAnimationFrame and drawing in python (just calling draw) could remove this timing jitter I think.

I'm honestly not entirely sure that's a good idea. It might not change the performance. And this design would not work at all in the case of multiple clients.

You might want to try to reduce the number of draw calls you're making.