Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
c81ab50
js bitmap context
Vipitis Sep 16, 2025
393b590
might need a loop -.-
Vipitis Sep 16, 2025
f0f533d
working loop
Vipitis Sep 17, 2025
d62dc39
assing js_array directly
Vipitis Sep 17, 2025
93e3639
add context class
Vipitis Sep 20, 2025
bd9b3ab
fix channels
Vipitis Sep 20, 2025
9017288
register auto backend
Vipitis Sep 20, 2025
a10970d
working events!
Vipitis Sep 20, 2025
e62274a
fix pixel order
Vipitis Sep 20, 2025
d79656c
cleanup testing code
Vipitis Sep 20, 2025
8d51188
remove unused context class
Vipitis Sep 20, 2025
1044d0f
add all events
Vipitis Sep 21, 2025
d393c52
add basic documentation
Vipitis Sep 21, 2025
8b71ed7
typos pass
Vipitis Sep 21, 2025
f197d7d
embed examples into docs
Vipitis Sep 21, 2025
529c3ec
maybe fix wheel location
Vipitis Sep 21, 2025
7025f87
maybe fix files
Vipitis Sep 21, 2025
58bc775
add canvas selector
Vipitis Sep 23, 2025
51459d3
add multicanvas example
Vipitis Sep 23, 2025
6cf0ba5
use asyncio loop
Vipitis Sep 25, 2025
d9f8fc0
ruff format
Vipitis Sep 25, 2025
fcd2d1c
add canvas element arg
Vipitis Sep 25, 2025
3e13446
simplify selector argument
Vipitis Sep 26, 2025
75e3ddc
icorrect type hints
Vipitis Sep 26, 2025
879399e
enbled wgpu context
Vipitis Sep 27, 2025
7544b77
fix button ids in pointer events
Vipitis Oct 4, 2025
2dd4824
add resize event
Vipitis Oct 7, 2025
053f2db
make the example resize
Vipitis Oct 7, 2025
ef8d9d3
fix pointer_move just inside or down
Vipitis Oct 7, 2025
475943c
some comments and VSCode formatting html
almarklein Oct 28, 2025
c2b3aa7
Merge branch 'main' into browser
almarklein Oct 28, 2025
485088a
script to serve examples
almarklein Oct 28, 2025
2584ecb
Add simple multi-canvas pyodide example
almarklein Oct 28, 2025
dd8545f
Check incoming canvas element, and use getElementById
almarklein Oct 28, 2025
740993a
make variable private
almarklein Oct 28, 2025
05d5b6e
Add pyscript example
almarklein Oct 28, 2025
69d2381
Add pyscript example
almarklein Oct 28, 2025
c2a313f
Flesh out the drawing mechanism
almarklein Oct 29, 2025
a338395
Merge branch 'main' into browser
almarklein Oct 29, 2025
041082a
Properly implement resizing
almarklein Oct 29, 2025
21da505
prevent canvas becoming infinitely large
almarklein Oct 29, 2025
da6ea72
Even better presentation, avoiding async
almarklein Oct 29, 2025
d8140a5
Allow right-click (prevent context menu)
almarklein Oct 29, 2025
643263a
implement close set_cursor, set_title
almarklein Oct 29, 2025
6edf5ec
use wrappers for adding handlers
almarklein Oct 29, 2025
11c6e68
Tweak all events. Only char does not work on chrome
almarklein Oct 29, 2025
1687744
Fix char event on chrome
almarklein Oct 29, 2025
28bf576
The js context is an implementation detail
almarklein Oct 30, 2025
0c881a2
rename server script
almarklein Oct 30, 2025
aa0f4cf
Remove old examples; their code ended up in other examples
almarklein Oct 30, 2025
24b353b
doc-build also builds wheel
almarklein Oct 30, 2025
cba8479
Add pyodide examples to docs without changing py source
almarklein Oct 30, 2025
34e4a35
clean
almarklein Oct 30, 2025
0af1ad0
add build to doc and example deps
almarklein Oct 30, 2025
702a0a4
add tests for html backend
almarklein Oct 30, 2025
b4f6d00
Fixes for setting size
almarklein Oct 30, 2025
2f85d5c
Merge branch 'main' into browser
almarklein Oct 30, 2025
27f45aa
Use flit to build wheels
almarklein Oct 30, 2025
03ca0c0
fix sphinx, i think
almarklein Oct 31, 2025
fc5d5da
Rename html backend -> pyodide backend
almarklein Oct 31, 2025
9826904
Fix that pointer up event did not always receive the right button
almarklein Oct 31, 2025
677db3c
more tweaks and fix docs
almarklein Oct 31, 2025
bd01cd9
Various tweaks
almarklein Oct 31, 2025
f3bfaf0
enable future wgpu support
almarklein Oct 31, 2025
464d314
Apply suggestions from code review
almarklein Nov 1, 2025
551cc50
fix dragging
almarklein Nov 1, 2025
8d39bc0
More review suggestions
almarklein Nov 1, 2025
eddaf54
default canvas id = 'canvas'
almarklein Nov 1, 2025
baf518f
show py docstrings in pyscript pages
almarklein Nov 1, 2025
4504f7c
Add back-to-list link in html exmaples
almarklein Nov 1, 2025
b06a8a1
use nearest neighbour
almarklein Nov 1, 2025
3bcbdde
Make sure 480 height fits for examples in docs
almarklein Nov 1, 2025
8f5f72b
Hopefully prevent popping up keyboard on mobile
almarklein Nov 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions examples/html_canvas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import rendercanvas
print("rendercanvas version:", rendercanvas.__version__)
from rendercanvas.base import BaseRenderCanvas, BaseCanvasGroup, BaseLoop

Check failure on line 3 in examples/html_canvas.py

View workflow job for this annotation

GitHub Actions / Linting

Ruff (E402)

examples/html_canvas.py:3:1: E402 Module level import not at top of file

import numpy as np

Check failure on line 5 in examples/html_canvas.py

View workflow job for this annotation

GitHub Actions / Linting

Ruff (E402)

examples/html_canvas.py:5:1: E402 Module level import not at top of file

# packages available inside pyodide
from pyodide.ffi import run_sync

Check failure on line 8 in examples/html_canvas.py

View workflow job for this annotation

GitHub Actions / Linting

Ruff (E402)

examples/html_canvas.py:8:1: E402 Module level import not at top of file
from js import document, ImageData, Uint8ClampedArray, window

Check failure on line 9 in examples/html_canvas.py

View workflow job for this annotation

GitHub Actions / Linting

Ruff (E402)

examples/html_canvas.py:9:1: E402 Module level import not at top of file
# import sys
# assert sys.platform == "emscripten" # use in the future to direct the auto backend?

# TODO event loop for js? https://rendercanvas.readthedocs.io/stable/backendapi.html#rendercanvas.stub.StubLoop
# https://pyodide.org/en/stable/usage/sdl.html#working-with-infinite-loop
# https://pyodide.org/en/stable/usage/api/python-api/webloop.html
# https://github.com/pyodide/pyodide/blob/0.28.2/src/py/pyodide/webloop.py
# also the asyncio.py implementation
class HTMLLoop(BaseLoop):
def __init__(self):
super().__init__()
self._webloop = None
self.__pending_tasks = []
self._stop_event = None

def _rc_init(self):
from pyodide.webloop import WebLoop
import asyncio

Check failure on line 27 in examples/html_canvas.py

View workflow job for this annotation

GitHub Actions / Linting

Ruff (F401)

examples/html_canvas.py:27:16: F401 `asyncio` imported but unused
self._webloop = WebLoop()

# TODO later try this
# try:
# self._interactive_loop = self._webloop.get_running_loop()
# self._stop_event = PyodideFuture()
# self._mark_as_interactive()
# except Exception:
# self._interactive_loop = None
self._interactive_loop = None

def _rc_run(self):
import asyncio #so the .run method is now overwritten I guess
if self._interactive_loop is not None:
return
# self._webloop.run_forever() # or untill stop event?
asyncio.run(self._rc_run_async())

async def _rc_run_async(self):
import asyncio
self._run_loop = self._webloop

while self.__pending_tasks:
self._rc_add_task(*self.__pending_tasks.pop(-1))

if self._stop_event is None:
self._stop_event = asyncio.Event()
await self._stop_event.wait()

# untested maybe...
def _rc_stop_(self):
while self.__tasks:
task = self.__tasks.pop()
task.cancel()

self._stop_event.set()
self._stop_event = None
self._run_loop = None

def _rc_call_later(self, delay, callback, *args):
self._webloop.call_later(delay, callback, *args)

pyodide_loop = HTMLLoop()

# needed for completeness? somehow is required for other examples - hmm?
class HTMLCanvasGroup(BaseCanvasGroup):
pass

# TODO: make this a proper RenderCanvas, just a poc for now
# https://rendercanvas.readthedocs.io/stable/backendapi.html#rendercanvas.stub.StubRenderCanvas
class HTMLBitmapCanvas(BaseRenderCanvas):
_rc_canvas_group = HTMLCanvasGroup(pyodide_loop) # todo do we need the group?
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
canvas_element = document.getElementById("canvas")
self.canvas_element = canvas_element
self.context = canvas_element.getContext("bitmaprenderer")

self._final_canvas_init()

def _rc_gui_poll(self):
# not sure if anything has to be done
pass

def _rc_get_present_methods(self):
# in the future maybe we can get the webgpu context (as JsProxy) or something... future stuff!
return {
"bitmap": {
"formats": ["rgba-u8"],
}
}

def _rc_request_draw(self):
# loop.call_soon?
loop = self._rc_canvas_group.get_loop()
loop.call_soon(self._draw_frame_and_present)
# window.requestAnimationFrame(self._rc_present_bitmap) #doesn't feel like this is the way... maybe more reading
# self._rc_force_draw()
# print("request draw called?")

def _rc_force_draw(self):
self._draw_frame_and_present() # returns without calling the present it seems

def _rc_present_bitmap(self):
# this actually "writes" the data to the canvas I guess.
self.context.transferFromImageBitmap(self._image_bitmap)

# reimplement so we might understand what's going on
def _draw_frame_and_present(self):
self._draw_frame()
self._rc_present_bitmap()

def _rc_get_physical_size(self):
return self.canvas_element.style.width, self.canvas_element.style.height

def _rc_get_logical_size(self):
return float(self.canvas_element.width), float(self.canvas_element.height)

def _rc_get_pixel_ratio(self) -> float:
ratio = window.devicePixelRatio
return ratio

def _rc_set_logical_size(self, width: float, height: float):
ratio = self._rc_get_pixel_ratio()
self.canvas_element.width = int(width * ratio) # only positive, int() -> floor()
self.canvas_element.height = int(height * ratio)
# also set the physical scale here?
# self.canvas_element.style.width = f"{width}px"
# self.canvas_element.style.height = f"{height}px"

def set_bitmap(self, bitmap):
# TODO: improve performance https://pyodide.org/en/stable/usage/type-conversions.html#buffers
# TODO: avoid memory leak!!!
# doesn't really exist? as it's part of the context? maybe we move it into the draw function...
self._last_bitmap = bitmap # keep track?
h, w, _ = bitmap.shape
flat_bitmap = bitmap.flatten()
js_array = Uint8ClampedArray.new(flat_bitmap.tolist())
image_data = ImageData.new(js_array, w, h)
# now this is the fake async call so it should be blocking
self._image_bitmap = run_sync(window.createImageBitmap(image_data))

def _rc_close(self):
# self.canvas_element.remove() # shouldn't really be needed?
pass

def _rc_get_closed(self):
# TODO: like check if the element still exists?
return False

def _rc_set_title(self, title: str):
# canvas element doens't have a title directly... but maybe the whole page?
document.title = title

# TODO: events


canvas = HTMLBitmapCanvas(title="RenderCanvas in Pyodide", max_fps=3.0)
def animate():
# based on the noise.py example
w, h = canvas._rc_get_logical_size()
shape = (int(h), int(w), 4) # third dimension sounds like it's needed
bitmap = np.random.uniform(0, 255, shape).astype(np.uint8)
canvas.set_bitmap(bitmap)

canvas.request_draw(animate)
pyodide_loop.run()
33 changes: 33 additions & 0 deletions examples/local_browser.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<!-- adapted from: https://traineq.org/imgui_bundle_online/projects/min_bundle_pyodide_app/demo_heart.source.txt -->
<!doctype html>
<html>
<head>
RenderCanvas HTML canvas via Pyodide <br>
<script src="https://cdn.jsdelivr.net/pyodide/v0.28.2/full/pyodide.js"></script>
</head>
<body>
<canvas id="canvas" width="640" height="480"></canvas>
some text below the canvas!
<script type="text/javascript">
async function main(){

// fetch the file locally for easier scripting
// --allow-file-access-from-files or local webserver
// TODO: replace the actual code here (unless you have the module)
pythonCode = await (await fetch("html_canvas.py")).text();

// Load Pyodide
let pyodide = await loadPyodide();

await pyodide.loadPackage("micropip");
const micropip = pyodide.pyimport("micropip");
await micropip.install('numpy');
await micropip.install('rendercanvas');

// Run the Python code async because some calls are async it seems.
pyodide.runPythonAsync(pythonCode);
}
main();
</script>
</body>
</html>
2 changes: 1 addition & 1 deletion rendercanvas/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,7 @@ def _rc_get_pixel_ratio(self) -> float:
raise NotImplementedError()

def _rc_set_logical_size(self, width: float, height: float):
"""Set the logical size. May be ignired when it makes no sense.
"""Set the logical size. May be ignored when it makes no sense.

The default implementation does nothing.
"""
Expand Down
Loading