Skip to content
Open
Show file tree
Hide file tree
Changes from 64 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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -164,13 +164,13 @@ jobs:
- name: Install dev dependencies
run: |
python -m pip install --upgrade pip
pip install -U flit build twine
pip install -U flit twine
- name: Create source distribution
run: |
python -m build -n -s
python -m flit build --no-use-vcs --format sdist
- name: Build wheel
run: |
python -m build -n -w
python -m flit build --no-use-vcs --format wheel
- name: Test sdist
shell: bash
run: |
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Special for this repo
nogit/
docs/gallery/
docs/static/*.whl
docs/sg_execution_times.rst
examples/screenshots/

Expand Down
80 changes: 79 additions & 1 deletion docs/backends.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ The table below gives an overview of the names in the different ``rendercanvas``
| ``loop``
- | Create a standalone canvas using wx, or
| integrate a render canvas in a wx application.
* - ``pyodide``
- | ``PyodideRenderCanvas`` (toplevel)
| ``RenderCanvas`` (alias)
| ``loop`` (an ``AsyncioLoop``)
- | Backend when Python is running in the browser,
| via Pyodide or PyScript.


There are also three loop-backends. These are mainly intended for use with the glfw backend:
Expand Down Expand Up @@ -168,7 +174,7 @@ Alternatively, you can select the specific qt library to use, making it easy to
loop.run() # calls app.exec_()


It is technically possible to e.g. use a ``glfw`` canvas with the Qt loop. However, this is not recommended because Qt gets confused in the precense of other windows and may hang or segfault.
It is technically possible to e.g. use a ``glfw`` canvas with the Qt loop. However, this is not recommended because Qt gets confused in the presence of other windows and may hang or segfault.
But the other way around, running a Qt canvas in e.g. the trio loop, works fine:

.. code-block:: py
Expand Down Expand Up @@ -265,6 +271,78 @@ subclass implementing a remote frame-buffer. There are also some `wgpu examples
canvas # Use as cell output


Support for Pyodide
-------------------

When Python is running in the browser using Pyodide, the auto backend selects
the ``rendercanvas.pyodide.PyodideRenderCanvas`` class. This backend requires no
additional dependencies. Currently only presenting a bitmap is supported, as
shown in the examples :doc:`noise.py <gallery/noise>` and :doc:`snake.py<gallery/snake>`.
Support for wgpu is underway.

An HTMLCanvasElement to be assumed to be present in the
DOM. By default it connects to the canvas with id "rendercanvas", but a
different id or element can also be provided using ``RenderCanvas(canvas_element)``.

An example using PyScript (which uses Pyodide):

.. code-block:: html

<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<script type="module" src="https://pyscript.net/releases/2025.10.3/core.js"></script>
</head>
<body>
<canvas id='rendercanvas' style="background:#aaa; width: 640px; height: 480px;"></canvas>
<br>
<script type="py" src="yourcode.py" config='{"packages": ["numpy", "sniffio", "rendercanvas"]}'>
</script>
</body>
</html>


An example using Pyodide directly:

.. code-block:: html

<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<script src="https://cdn.jsdelivr.net/pyodide/v0.29.0/full/pyodide.js"></script>
</head>
<body>
<canvas id="rendercanvas" width="640" height="480"></canvas>
<script type="text/javascript">
async function main(){
pythonCode = `
# Use python script as normally
import numpy as np
from rendercanvas.auto import RenderCanvas, loop

canvas = RenderCanvas()
context = canvas.get_context("bitmap")
data = np.random.uniform(127, 255, size=(24, 32, 4)).astype(np.uint8)

@canvas.request_draw
def animate():
context.set_bitmap(data)
`
// load Pyodide and install Rendercanvas
let pyodide = await loadPyodide();
await pyodide.loadPackage("micropip");
const micropip = pyodide.pyimport("micropip");
await micropip.install("rendercanvas");
// have to call as runPythonAsync
pyodide.runPythonAsync(pythonCode);
}
main();
</script>
</body>
</html>


.. _env_vars:

Expand Down
98 changes: 93 additions & 5 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,20 @@

import os
import sys
import shutil

import flit

ROOT_DIR = os.path.abspath(os.path.join(__file__, "..", ".."))
sys.path.insert(0, ROOT_DIR)

os.environ["RENDERCANVAS_FORCE_OFFSCREEN"] = "true"


# Load wglibu so autodoc can query docstrings
# Load wgpu so autodoc can query docstrings
import rendercanvas # noqa: E402
import rendercanvas.stub # noqa: E402 - we use the stub backend to generate doccs
import rendercanvas._context # noqa: E402 - we use the ContexInterface to generate doccs
import rendercanvas.stub # noqa: E402 - we use the stub backend to generate docs
import rendercanvas._context # noqa: E402 - we use the ContextInterface to generate docs
import rendercanvas.utils.bitmappresentadapter # noqa: E402

# -- Project information -----------------------------------------------------
Expand All @@ -40,10 +42,10 @@
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
"sphinx_rtd_theme",
"sphinx.ext.autodoc",
"sphinx.ext.napoleon",
"sphinx.ext.intersphinx",
"sphinx_rtd_theme",
"sphinx_gallery.gen_gallery",
]

Expand All @@ -64,8 +66,88 @@
master_doc = "index"


# -- Build wheel so Pyodide examples can use exactly this version of rendercanvas -----------------------------------------------------

short_version = ".".join(str(i) for i in rendercanvas.version_info[:3])
wheel_name = f"rendercanvas-{short_version}-py3-none-any.whl"

# Build the wheel
toml_filename = os.path.join(ROOT_DIR, "pyproject.toml")
flit.main(["-f", toml_filename, "build", "--no-use-vcs", "--format", "wheel"])
wheel_filename = os.path.join(ROOT_DIR, "dist", wheel_name)
assert os.path.isfile(wheel_filename), f"{wheel_name} does not exist"

# Copy into static
print("Copy wheel to static dir")
shutil.copy(
wheel_filename,
os.path.join(ROOT_DIR, "docs", "static", wheel_name),
)


# -- Sphinx Gallery -----------------------------------------------------

iframe_placeholde_rst = """
.. only:: html
Interactive example
===================
This uses Pyodide. If this does not work, your browser may not have sufficient support for wasm/pyodide/wgpu (check your browser dev console).
.. raw:: html
<iframe src="pyodide.html#example.py"></iframe>
"""

python_files = {}


def add_pyodide_to_examples(app):
if app.builder.name != "html":
return

gallery_dir = os.path.join(ROOT_DIR, "docs", "gallery")

for fname in os.listdir(gallery_dir):
filename = os.path.join(gallery_dir, fname)
if not fname.endswith(".py"):
continue
with open(filename, "rb") as f:
py = f.read().decode()
if fname in ["drag.py", "noise.py", "snake.py"]:
# todo: later we detect by using a special comment in the py file
print("Adding Pyodide example to", fname)
fname_rst = fname.replace(".py", ".rst")
# Update rst file
rst = iframe_placeholde_rst.replace("example.py", fname)
with open(os.path.join(gallery_dir, fname_rst), "ab") as f:
f.write(rst.encode())
python_files[fname] = py


def add_files_to_run_pyodide_examples(app, exception):
if app.builder.name != "html":
return

gallery_build_dir = os.path.join(app.outdir, "gallery")

# Write html file that can load pyodide examples
with open(
os.path.join(ROOT_DIR, "docs", "static", "_pyodide_iframe.html"), "rb"
) as f:
html = f.read().decode()
html = html.replace('"rendercanvas"', f'"../_static/{wheel_name}"')
with open(os.path.join(gallery_build_dir, "pyodide.html"), "wb") as f:
f.write(html.encode())

# Write the python files
for fname, py in python_files.items():
print("Writing", fname)
with open(os.path.join(gallery_build_dir, fname), "wb") as f:
f.write(py.encode())


# Suppress "cannot cache unpickable configuration value" for sphinx_gallery_conf
# See https://github.com/sphinx-doc/sphinx/issues/12300
suppress_warnings = ["config.cache"]
Expand All @@ -75,9 +157,10 @@
"gallery_dirs": "gallery",
"backreferences_dir": "gallery/backreferences",
"doc_module": ("rendercanvas",),
# "image_scrapers": (),,
# "image_scrapers": (),
"remove_config_comments": True,
"examples_dirs": "../examples/",
"ignore_pattern": r"serve_browser_examples\.py",
}

# -- Options for HTML output -------------------------------------------------
Expand All @@ -92,3 +175,8 @@
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["static"]
html_css_files = ["custom.css"]


def setup(app):
app.connect("builder-inited", add_pyodide_to_examples)
app.connect("build-finished", add_files_to_run_pyodide_examples)
6 changes: 3 additions & 3 deletions docs/contextapi.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ then present the result to the screen. For this, the canvas provides one or more
└─────────┘ └────────┘

This means that for the context to be able to present to any canvas, it must
support *both* the 'image' and 'screen' present-methods. If the context prefers
support *both* the 'bitmap' and 'screen' present-methods. If the context prefers
presenting to the screen, and the canvas supports that, all is well. Similarly,
if the context has a bitmap to present, and the canvas supports the
bitmap-method, there's no problem.
Expand All @@ -44,7 +44,7 @@ on the CPU. All GPU API's have ways to do this.
download from gpu to cpu

If the context has a bitmap to present, and the canvas only supports presenting
to screen, you can usse a small utility: the ``BitmapPresentAdapter`` takes a
to screen, you can use a small utility: the ``BitmapPresentAdapter`` takes a
bitmap and presents it to the screen.

.. code-block::
Expand All @@ -58,7 +58,7 @@ bitmap and presents it to the screen.

This way, contexts can be made to work with all canvas backens.

Canvases may also provide additionaly present-methods. If a context knows how to
Canvases may also provide additionally present-methods. If a context knows how to
use that present-method, it can make use of it. Examples could be presenting
diff images or video streams.

Expand Down
6 changes: 3 additions & 3 deletions docs/start.rst
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,12 @@ Async
A render canvas can be used in a fully async setting using e.g. Asyncio or Trio, or in an event-drived framework like Qt.
If you like callbacks, ``loop.call_later()`` always works. If you like async, use ``loop.add_task()``. Event handlers can always be async.

If you make use of async functions (co-routines), and want to keep your code portable accross
If you make use of async functions (co-routines), and want to keep your code portable across
different canvas backends, restrict your use of async features to ``sleep`` and ``Event``;
these are the only features currently implemened in our async adapter utility.
these are the only features currently implemented in our async adapter utility.
We recommend importing these from :doc:`rendercanvas.utils.asyncs <utils_asyncs>` or use ``sniffio`` to detect the library that they can be imported from.

On the other hand, if you know your code always runs on the asyncio loop, you can fully make use of ``asyncio``. Dito for Trio.
On the other hand, if you know your code always runs on the asyncio loop, you can fully make use of ``asyncio``. Ditto for Trio.

If you use Qt and get nervous from async code, no worries, when running on Qt, ``asyncio`` is not even imported. You can regard most async functions
as syntactic sugar for pieces of code chained with ``call_later``. That's more or less how our async adapter works :)
Expand Down
41 changes: 41 additions & 0 deletions docs/static/_pyodide_iframe.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<!doctype html>
<html>

<head>
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Rendercanvas example.py in Pyodide</title>
<script src="https://cdn.jsdelivr.net/pyodide/v0.29.0/full/pyodide.js"></script>
</head>

<body>
<dialog id="loading" style='outline: none; border: none; background: transparent;'>
<h1>Loading...</h1>
</dialog>
<canvas id='rendercanvas' style='width:calc(100% - 20px); height: 450px; background-color: #ddd;'></canvas>
<script type="text/javascript">
async function main() {
let loading = document.getElementById('loading');
loading.showModal();
try {
let example_name = document.location.hash.slice(1);
pythonCode = await (await fetch(example_name)).text();
let pyodide = await loadPyodide();
await pyodide.loadPackage("micropip");
const micropip = pyodide.pyimport("micropip");
await micropip.install('sniffio');
await micropip.install('numpy');
// The below loads rendercanvas from pypi. But we will replace it with the name of the wheel,
// so that it's loaded from the docs (in _static).
await micropip.install("rendercanvas");
// Run the Python code async because some calls are async it seems.
pyodide.runPythonAsync(pythonCode);
loading.close();
} catch (err) {
loading.innerHTML = "Failed to load: " + err;
}
}
main();
</script>
</body>

</html>
6 changes: 6 additions & 0 deletions docs/static/custom.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
div.sphx-glr-download,
div.sphx-glr-download-link-note {
display: none;
}

div.document iframe {
width: 100%;
height: 500px;
border: none;
}
2 changes: 2 additions & 0 deletions examples/drag.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ def on_pointer_down(event):
bx, by = block[:2]
if bx - hs < x < bx + hs and by - hs < y < by + hs:
dragging = i, (bx, by), (x, y)
canvas.set_cursor("pointer")
break


Expand Down Expand Up @@ -107,6 +108,7 @@ def on_pointer_up(event):
global dragging
if event["button"] == 1:
dragging = None
canvas.set_cursor("default")


@canvas.add_event_handler("key_down")
Expand Down
Loading