Skip to content

web

FreeBodyEngine.core.window.web #

The web window backend - runs under Pyodide (this engine compiled to WASM, executing inside an actual browser tab, not a native process at all - see utils.get_platform()'s own docstring for why sys.platform reports "emscripten" there). Unlike every other backend (GLFW/Wayland/ X11/Win32/headless), there's no OS-level window to create: "the window" is an HTML <canvas> element already sitting in the page (or created here if one isn't), and "the GPU context" is that canvas's webgl2 context, reached entirely through Pyodide's js bridge - none of PyOpenGL/GLFW/SDL2 is importable here at all, since none of them exist inside the WASM sandbox. See graphics/webgl/renderer.py for the WebGL2 renderer that actually draws into this context.

Input works the same shape as every other backend (poll-per-frame state that update() refreshes) but is fed the opposite way around: a native backend polls its OS/library for the current state every frame (e.g. glfw.get_key()); a browser only ever pushes input as async DOM events, so this backend's job is to catch those events the instant they fire (via addEventListener, using pyodide.ffi.create_proxy to hand a JS-callable wrapper for a Python callback) and buffer them into the same per-frame state shape update() already exposes everywhere else - real backends just get there via a different route.

CANVAS_ID = 'fb-canvas' module-attribute #

JS_KEY_MAP = {'KeyA': Key.A, 'KeyB': Key.B, 'KeyC': Key.C, 'KeyD': Key.D, 'KeyE': Key.E, 'KeyF': Key.F, 'KeyG': Key.G, 'KeyH': Key.H, 'KeyI': Key.I, 'KeyJ': Key.J, 'KeyK': Key.K, 'KeyL': Key.L, 'KeyM': Key.M, 'KeyN': Key.N, 'KeyO': Key.O, 'KeyP': Key.P, 'KeyQ': Key.Q, 'KeyR': Key.R, 'KeyS': Key.S, 'KeyT': Key.T, 'KeyU': Key.U, 'KeyV': Key.V, 'KeyW': Key.W, 'KeyX': Key.X, 'KeyY': Key.Y, 'KeyZ': Key.Z, 'Digit0': Key.ZERO, 'Digit1': Key.ONE, 'Digit2': Key.TWO, 'Digit3': Key.THREE, 'Digit4': Key.FOUR, 'Digit5': Key.FIVE, 'Digit6': Key.SIX, 'Digit7': Key.SEVEN, 'Digit8': Key.EIGHT, 'Digit9': Key.NINE, 'Minus': Key.MINUS, 'Equal': Key.EQUAL, 'BracketLeft': Key.LEFT_BRACKET, 'BracketRight': Key.RIGHT_BRACKET, 'Backslash': Key.BACKSLASH, 'Semicolon': Key.SEMICOLON, 'Quote': Key.APOSTROPHE, 'Backquote': Key.TILDE, 'Comma': Key.COMMA, 'Period': Key.PERIOD, 'Slash': Key.SLASH, 'Space': Key.SPACE, 'Enter': Key.RETURN, 'Backspace': Key.BACKSPACE, 'Tab': Key.TAB, 'Escape': Key.ESCAPE, 'CapsLock': Key.CAPS_LOCK, 'ControlLeft': Key.L_CTRL, 'ControlRight': Key.R_CTRL, 'ShiftLeft': Key.L_SHIFT, 'ShiftRight': Key.R_SHIFT, 'AltLeft': Key.L_ALT, 'AltRight': Key.R_ALT, 'MetaLeft': Key.L_SUPER, 'MetaRight': Key.R_SUPER, 'Insert': Key.INSERT, 'Delete': Key.DELETE, 'Home': Key.HOME, 'End': Key.END, 'PageUp': Key.PG_UP, 'PageDown': Key.PG_DOWN, 'ArrowUp': Key.UP, 'ArrowDown': Key.DOWN, 'ArrowLeft': Key.LEFT, 'ArrowRight': Key.RIGHT, 'F1': Key.F1, 'F2': Key.F2, 'F3': Key.F3, 'F4': Key.F4, 'F5': Key.F5, 'F6': Key.F6, 'F7': Key.F7, 'F8': Key.F8, 'F9': Key.F9, 'F10': Key.F10, 'F11': Key.F11} module-attribute #

JS_MOUSE_BUTTON_MAP = {0: 0, 1: 1, 2: 2} module-attribute #

WebMouse(window) #

Bases: Mouse

Mouse implementation for WebWindow - unlike a native backend's Mouse (which polls a live OS/library button state every frame - see GLFWMouse.update()), the source of truth here is already the async JS event listeners WebWindow's constructor wired up; update() just diffs against last frame's snapshot of that same state to derive pressed/released/dragging, the same as every other Mouse backend does from its own polled state.

double_click_threshold = 0.4 instance-attribute #

drag_threshold = 4.0 instance-attribute #

last_click_time = [-500.0, -500.0, -500.0] instance-attribute #

scroll_delta = Vector(0.0, 0.0) instance-attribute #

window = window instance-attribute #

get_double_click(button) #

get_down(button) #

get_drag_start(button, world=False) #

get_dragging(button) #

get_pressed(button) #

get_released(button) #

get_scroll_delta() #

How far the scroll wheel moved this frame - drained from _scroll_accum (the live, wheel-event-fed accumulator - see WebWindow._on_wheel()) into this stable snapshot once per update(), the same as GLFWMouse.get_scroll_delta()'s own docstring describes for its GLFW callback equivalent. This used to return _scroll_accum directly, which update() also resets to zero every frame - whichever ran first each frame, this read the reset value, not the accumulated one, so mouse-wheel scroll events were silently discarded 100% of the time on web.

hide_cursor() #

Hides the system cursor over the canvas via CSS.

lock_position() #

Requests Pointer Lock on the canvas - the browser equivalent of a native cursor-lock (mouse movement keeps reporting deltas with the system cursor hidden and pinned in place).

set_cursor(shape='default') #

Sets the canvas's CSS cursor style - a real implementation (unlike Mouse's own concrete no-op default), since every shape this engine's UI hover system asks for (see ui/manager.py) has a direct standard CSS cursor keyword equivalent.

unlock_position() #

Releases a Pointer Lock previously requested by lock_position().

update() #

Diffs this frame's JS-event-fed button state against last frame's to derive pressed/released, and drives drag/double-click tracking off real elapsed time (fb.get_time()) - mirroring GLFWMouse.update()'s own logic exactly, just against a JS-delivered _mouse_down instead of a polled one.

WebWindow(size, title='FreeBodyEngine') #

Bases: Window

Window backend for a browser tab running under Pyodide. Owns the page's <canvas> element and its webgl2 context; has no OS-level position/decorations/title bar at all (those concepts belong to the browser tab/window, not to anything this engine controls), so the handful of abstract methods about them are honest no-ops rather than faked native behavior.

Finds (or creates) the page's canvas, sizes it to size, and gets its webgl2 rendering context - graphics/webgl/renderer.py reads self.gl back out of this window the same way GL33Renderer reads self.window._window on GLFW.

canvas = canvas instance-attribute #

framebuffer_size property #

The canvas's actual drawing-buffer resolution (width/height attributes, not its CSS box size) - scaled by devicePixelRatio so this reads correctly on a HiDPI display, matching every other backend's framebuffer_size/size distinction.

gl = canvas.getContext('webgl2', context_options) instance-attribute #

position property writable #

Always (0, 0) - a canvas has no OS-level window position; where it sits on the page is a CSS/layout concern outside this engine.

size property writable #

The canvas's CSS pixel size.

window_type = 'web' instance-attribute #

close() #

Marks this window as closed and tears down its event listeners - there's no native resource to release (the canvas/GL context are just left as the browser tab exits, whether now or later).

create_mouse() #

Creates and returns WebMouse, wired up to this window's JS-event-fed button/position state.

draw() #

No-op - WebGL2 presents to the canvas implicitly the moment the current JS task (this whole animation-frame callback - see core/main.py's web run loop) returns control to the browser, the same way a native backend's real swap-buffers call presents its already-rendered backbuffer.

get_clipboard_text() #

Still a no-op (see Window.get_clipboard_text's own concrete no-op default) - the Clipboard API (navigator.clipboard. readText()) is async (returns a JS Promise) and this method's contract is synchronous, so it's not bridged here. This does NOT mean pasting is broken on web, though: ui/manager.py's Ctrl+V handling calls this and does nothing useful with the None it gets back, but the actual paste happens through a separate, genuinely synchronous path - see _on_paste() above, which the browser's own native paste event drives directly into UIManager.paste_text() without ever needing to come through here.

is_ready() #

True until close() is called - a browser tab has no equivalent of a native "window close" request this engine would need to poll for (closing the tab just kills the whole WASM runtime outright).

set_clipboard_text(text) #

Writes text to the system clipboard via the Clipboard API's writeText() - unlike reading (see get_clipboard_text()/ _on_paste()'s docstrings for why that needs the native paste event instead of this same API's readText()), writing has nothing a caller needs handed back, so there's no synchronous- contract mismatch to work around here: the write just happens in the background, the same "fire it and don't wait" shape as any other JS Promise this engine doesn't need the result of.

Two real constraints inherited straight from the browser, not something this method can smooth over: writeText() only exists in a secure context (HTTPS, or localhost - not a plain http:// page), and only succeeds when called from within a real user- gesture handler (a click callback, e.g. phonon's "copy share link" button) - calling it on a timer or at startup gets silently rejected. A rejection here is real feedback of that, not this binding failing, so it's surfaced as a warning rather than swallowed.

set_title(new_title) #

Sets the browser tab's title (document.title) - the closest web equivalent of a native window's title bar.

update() #

Applies any canvas resize requested by the page (see the resize() note below) and emits FRAMEBUFFER_RESIZE/WINDOW_RESIZE so the renderer's viewport and any letterboxing UI stay in sync - there's no OS event queue to pump here (JS event listeners already deliver input the instant it happens, not once per polled frame), so this is lighter than a native backend's update().