Pyodide Integration

Basic support for pyodide has been added as of version 3.0. This will allow the pyglet to be run inside a browser environment. Support for this is extremely limited, as it relies upon another project to be functional.

Currently only WebGL2 is supported. Tested via Chrome (138.0.7204.50) and Edge (138.0.3351.65).

Limitations

While users generally will not have to work with JavaScript in relation to your pyglet script, there are programming differences between the Python and JavaScript language itself that impacts users.

Below is a list of information to keep in mind when working with both JavaScript and Pyglet.

Asynchronous Behavior

JavaScript (and Pyodide) relies on asynchronous behavior, which is very different than how Python operates normally.

Although there is an asyncio standard module, it requires rethinking how you program. Many libraries are not created with asynchronous behavior in mind. Generally many functions will assume that a prior function or call was completed successfully before it runs. This assumption is where a lot of things break down.

While most of this behavior has been insulated from the user, there are still areas where this can affect your application which requires specific handling.

Custom fonts

In most operating systems, when a font is loaded, the operations are done synchronously. This means after we have successfully called and executed adding the data, it will be ready to use immediately after.

For example:

with open("action_man.ttf", 'rb') as f:
   pyglet.font.add_file(f)

font = pyglet.font.load("Action Man", 12)

label = pyglet.text.Label("Hello, world!", font_name="Action Man")

For a browser to see the custom font, it has to be loaded through JavaScript and into the browser. This loading process is done asynchronously.

In the example above, although the loading of the custom font has been started, there is no guarantee it is ready by the time the label is created. To resolve this, a dispatcher was created that will notify the user when a custom font is ready:

@pyglet.font.manager.event
def on_font_loaded(font_family_name: str, weight: str, italic: str, stretch: str) -> None:
    # If the font loaded matches what you want for your label, assign it.
    if font_family_name == "Action Man":
        label.font_name = "Action Man"

This can be useful if you want to either wait to create text until the font is ready, or update existing text with the proper font.

Note

pyglet.options.text_antialiasing is not supported. Browsers can not render text without anti-aliasing.

Audio

Audio is also loaded asynchronously with JavaScript through the Web Audio API.

Audio can be kept the same as a normal application (media.play()). Pyglet will create the resources necessary to output audio. However, AudioPlayer still relies on a callback from JavaScript Web Audio that the data is decoded and ready. When it’s ready, the player will immediately load the data into the buffer and output, so long as it is active. In testing, this has not been noticeable, but it is recommended to load the audio at the beginning of your application.

Warning

Web Audio does not allow a player to restart playback of a source once stopped, nor seek.

Note

Web Audio sources are not streaming, it should be considered a StaticSource at all times.

Images

Image decoding is also asynchronous in JavaScript API’s, but not used in Pyglet.

Normally this would require a similar callback system as the custom fonts where pyglet has to submit data to JavaScript APIs and wait for a callback to notify the user when it’s ready. However, Pyglet will instead use Python only decoders to improve speed and compatibility. Through testing, decoding images is actually much slower through the JavaScript API than what Python and Pyglet can provide.

Pyglet only supports PNG and BMP image formats without any dependencies.

For further image extension support, it is recommended to import PIL (pillow). Pyodide provides a supported version in their distribution. It can be loaded through JavaScript via await pyodide.loadPackage("pillow"). Pyglet will utilize Pillow as the default image decoder when found.

File System

Pyodide utilizes a “Virtual File System” or VFS. For more information visit: https://pyodide.org/en/stable/usage/file-system.html

In short, the browser or Pyodide does not access your local files the same way as a normal application does. It can only access what is in the VFS. The pyglet.resource module, pathlib, and os module typically only can see this structure.

Users will need to configure Pyodide to copy files into the VFS via pyodide.FS.writeFile. For a simple example of making a project available in a browser, see the following example script in our tools folder: https://github.com/pyglet/pyglet/tree/development/tools/pyodide

Note

The VFS data only exists per browser session, so refreshing the browser will clear the VFS and require it to be loaded again.

Platform

Pyglet differentiates the browser environment through the pyglet.compat_platform property. Through pyodide, this is reported as emscripten. If you want branching behavior depending on the operating system, it is recommended to use that property.

pyglet.app.run

In normal Pyglet operation, the way a typical platform event loop behaves is that operating system API calls are made to wait for an event to occur with a small timeout. This will prevent high CPU usage, and allow scheduled calls to be independent of the window refresh rate.

Pyglet will utilize an asyncio loop for scheduled events, and requestAnimationFrame from JavaScript for the canvas refresh rate. See:: https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame this refresh rate is determined by the browser and cannot be modified by pyglet.

If you want your event loop to rely on requestAnimationFrame for precision, you can start your pyglet application via pyglet.app.run(None). Otherwise, the existing usage can be left.

Warning

A memory leak exists in Pyodide version 0.27.7 with asyncio. It is recommended to use pyglet.app.run(None) until this is resolved.

Controllers

JavaScript utilizes the Gamepad API, which may differ slightly from the SDL Controller API that pyglet utilizes.

From testing, detection and mapping between controllers were accurate and consistent.

To see how the Javascript Gamepad API maps controls via the “Standard Gamepad” profile, you can visit: https://w3c.github.io/gamepad/#remapping

Currently re-mapping this “Standard Gamepad” layout is not supported.

Window

Pyglet uses the following canvas id by default: pygletCanvas for events and drawing. If this is not found, it will be created. Pyglet can treat an existing canvas as its own by changing the pyglet.options.pyodide.canvas_id name.