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().