From fae43ca185589b523b71548f3d17e056f2db8911 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 12 Oct 2026 21:22:05 +0200 Subject: [PATCH 1/6] bump versions --- docs/backends.rst | 2 +- docs/static/_pyodide_iframe.html | 2 +- examples/pyodide.html | 2 +- examples/serve_browser_examples.py | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/backends.rst b/docs/backends.rst index 7738e067..7c3d6a0d 100644 --- a/docs/backends.rst +++ b/docs/backends.rst @@ -440,7 +440,7 @@ Similar as with PyScript, you can chose between a ```` and a ``
- + diff --git a/docs/static/_pyodide_iframe.html b/docs/static/_pyodide_iframe.html index 602f5287..561b900d 100644 --- a/docs/static/_pyodide_iframe.html +++ b/docs/static/_pyodide_iframe.html @@ -4,7 +4,7 @@ Rendercanvas example.py in Pyodide - + diff --git a/examples/pyodide.html b/examples/pyodide.html index 77968376..dbb45b48 100644 --- a/examples/pyodide.html +++ b/examples/pyodide.html @@ -4,7 +4,7 @@ RenderCanvas Pyodide example - + diff --git a/examples/serve_browser_examples.py b/examples/serve_browser_examples.py index 6bebdf28..488ae5f9 100644 --- a/examples/serve_browser_examples.py +++ b/examples/serve_browser_examples.py @@ -57,7 +57,7 @@ def get_html_index(): RenderCanvas PyScript examples - + @@ -84,7 +84,7 @@ def get_html_index(): example.py via PyScript - + From f4b760731d64ee733529f48646f70ce2bb703ff1 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 12 Oct 2026 22:19:43 +0200 Subject: [PATCH 2/6] minimal reproducer --- examples/pyodide.html | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/examples/pyodide.html b/examples/pyodide.html index dbb45b48..39c0b697 100644 --- a/examples/pyodide.html +++ b/examples/pyodide.html @@ -4,7 +4,7 @@ RenderCanvas Pyodide example - + @@ -33,12 +33,14 @@ import numpy as np from js import document + from pyodide.ffi import can_run_sync + # Select by name - canvas_red = RenderCanvas(canvas_element="canvas1", size=(320, 240), update_mode="ondemand") + canvas_red = RenderCanvas(canvas_element="canvas1", size=(320, 240), update_mode="ondemand", present_method="screen") # Or by element canvas2_el = document.getElementById("canvas2") - canvas_green = RenderCanvas(canvas_element=canvas2_el, size=(320, 240), update_mode="continuous") + canvas_green = RenderCanvas(canvas_element=canvas2_el, size=(320, 240), update_mode="ondemand", present_method="bitmap") context_red = canvas_red.get_bitmap_context() context_green = canvas_green.get_bitmap_context() @@ -53,10 +55,12 @@ @canvas_red.request_draw def animate_red(): + print("can run_sync in red(screen):", can_run_sync()) context_red.set_bitmap(red_data) @canvas_green.request_draw def animate_green(): + print("can run_sync in green(bitmap):", can_run_sync()) context_green.set_bitmap(green_data) # The loop.run() is not required in Pyodide, but you can include it so that the code works on both desktop and Pyodide. @@ -69,6 +73,7 @@ await pyodide.loadPackage("micropip"); const micropip = pyodide.pyimport("micropip"); await micropip.install('numpy'); + await micropip.install('https://wgpu-py--753.org.readthedocs.build/en/753/_static/wgpu-0.32.0-py3-none-any.whl') // just to try screen method await micropip.install("rendercanvas"); // Run the Python code async From 26bea529939c9e553136b862815f1c5155e54621 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 12 Oct 2026 22:29:39 +0200 Subject: [PATCH 3/6] resolve promies then instead of callback --- rendercanvas/pyodide.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rendercanvas/pyodide.py b/rendercanvas/pyodide.py index 12f83ecc..1bbaacbe 100644 --- a/rendercanvas/pyodide.py +++ b/rendercanvas/pyodide.py @@ -20,7 +20,8 @@ raise ImportError("This module is only for use with Pyodide in the browser.") from pyodide.ffi import create_proxy, to_js -from js import window, document, ImageData, Uint8ClampedArray, OffscreenCanvas +from pyodide.code import run_js +from js import window, document, ImageData, Uint8ClampedArray, OffscreenCanvas, Promise def _inject_js_and_css(): @@ -133,7 +134,8 @@ def _rc_request_draw(self): self._time_to_draw() def _rc_request_paint(self): - window.requestAnimationFrame(create_proxy(lambda _: self._time_to_paint())) + js_promise = run_js("() => new Promise(resolve => requestAnimationFrame(resolve))") + js_promise().then(lambda _: self._time_to_paint()) def _rc_force_paint(self): # Not very clean to do this, and not sure if it works in a browser; From 2bfcb146805bee5107ab0681280d7fd245cf8ab7 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 12 Oct 2026 22:48:20 +0200 Subject: [PATCH 4/6] cleanup reproducer --- examples/pyodide.html | 9 ++------- rendercanvas/pyodide.py | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/examples/pyodide.html b/examples/pyodide.html index 39c0b697..6e984f2e 100644 --- a/examples/pyodide.html +++ b/examples/pyodide.html @@ -33,14 +33,12 @@ import numpy as np from js import document - from pyodide.ffi import can_run_sync - # Select by name - canvas_red = RenderCanvas(canvas_element="canvas1", size=(320, 240), update_mode="ondemand", present_method="screen") + canvas_red = RenderCanvas(canvas_element="canvas1", size=(320, 240), update_mode="ondemand") # Or by element canvas2_el = document.getElementById("canvas2") - canvas_green = RenderCanvas(canvas_element=canvas2_el, size=(320, 240), update_mode="ondemand", present_method="bitmap") + canvas_green = RenderCanvas(canvas_element=canvas2_el, size=(320, 240), update_mode="continuous") context_red = canvas_red.get_bitmap_context() context_green = canvas_green.get_bitmap_context() @@ -55,12 +53,10 @@ @canvas_red.request_draw def animate_red(): - print("can run_sync in red(screen):", can_run_sync()) context_red.set_bitmap(red_data) @canvas_green.request_draw def animate_green(): - print("can run_sync in green(bitmap):", can_run_sync()) context_green.set_bitmap(green_data) # The loop.run() is not required in Pyodide, but you can include it so that the code works on both desktop and Pyodide. @@ -73,7 +69,6 @@ await pyodide.loadPackage("micropip"); const micropip = pyodide.pyimport("micropip"); await micropip.install('numpy'); - await micropip.install('https://wgpu-py--753.org.readthedocs.build/en/753/_static/wgpu-0.32.0-py3-none-any.whl') // just to try screen method await micropip.install("rendercanvas"); // Run the Python code async diff --git a/rendercanvas/pyodide.py b/rendercanvas/pyodide.py index 1bbaacbe..0775ac6e 100644 --- a/rendercanvas/pyodide.py +++ b/rendercanvas/pyodide.py @@ -21,7 +21,7 @@ from pyodide.ffi import create_proxy, to_js from pyodide.code import run_js -from js import window, document, ImageData, Uint8ClampedArray, OffscreenCanvas, Promise +from js import window, document, ImageData, Uint8ClampedArray, OffscreenCanvas def _inject_js_and_css(): From bd0e4545f052d9a6c9057a0613ec5eb4bd551fa1 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 12 Oct 2026 22:51:40 +0200 Subject: [PATCH 5/6] ruff format --- rendercanvas/pyodide.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rendercanvas/pyodide.py b/rendercanvas/pyodide.py index 0775ac6e..7f46ad61 100644 --- a/rendercanvas/pyodide.py +++ b/rendercanvas/pyodide.py @@ -134,7 +134,9 @@ def _rc_request_draw(self): self._time_to_draw() def _rc_request_paint(self): - js_promise = run_js("() => new Promise(resolve => requestAnimationFrame(resolve))") + js_promise = run_js( + "() => new Promise(resolve => requestAnimationFrame(resolve))" + ) js_promise().then(lambda _: self._time_to_paint()) def _rc_force_paint(self): From 8804860fa02b8723b82be94c4a6198c2277bcc5e Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 14 Oct 2026 21:52:13 +0200 Subject: [PATCH 6/6] reuse next frame promise --- rendercanvas/pyodide.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/rendercanvas/pyodide.py b/rendercanvas/pyodide.py index 7f46ad61..f6dbe082 100644 --- a/rendercanvas/pyodide.py +++ b/rendercanvas/pyodide.py @@ -73,6 +73,11 @@ def __init__( # If size or title are not given, set them to None, so they are left as-is. This is usually preferred in html docs. kwargs["size"] = kwargs.get("size", None) + # instead of using the callback directly (which blocks stack switching), chain on the resolve. TODO: avoid the eval and implement it using pyodide ffi + self._next_frame_promise = run_js( + "() => new Promise(resolve => requestAnimationFrame(resolve))" + )() + # Finalize init super().__init__(*args, **kwargs) self._final_canvas_init() @@ -134,10 +139,7 @@ def _rc_request_draw(self): self._time_to_draw() def _rc_request_paint(self): - js_promise = run_js( - "() => new Promise(resolve => requestAnimationFrame(resolve))" - ) - js_promise().then(lambda _: self._time_to_paint()) + self._next_frame_promise.then(lambda _: self._time_to_paint()) def _rc_force_paint(self): # Not very clean to do this, and not sure if it works in a browser;