API reference

Every public class, method, and event in @codexo/exojs. Generated from source.

N

namespaceFunctions & Constants

@codexo/exojs / core / stable

Free functions and constants exported from @codexo/exojs.

36
props
53
methods
0
events
Overview

Free functions and constants exported from @codexo/exojs. Import any of them directly, for example `import { bezierCurveTo } from '@codexo/exojs'`.

Functions53
bezierCurveTo(fromX: number, fromY: number, cpX1: number, cpY1: number, cpX2: number, cpY2: number, toX: number, toY: number, path: number[], len: number): number[]
Sample a cubic Bézier curve at len evenly-spaced t values and append the resulting (x, y) pairs to path. The starting point (fromX, fromY) is always pushed first. Returns path for chaining.
The browser's own network-reachability hint: navigator.onLine, kept current by the online and offline window events. What the browser reports is whether it has a usable network interface. A captive portal, a dead resolver and a firewalled origin all report as online, so this can say "probably reachable" and "definitely not", never "reachable". Shared by BrowserPlatform and by a standalone Connectivity, so the two window listeners and the navigator read exist once.
A ContainerBlockStore over the Cache API. Per-origin and evictable, which is what a block store wants: the browser may reclaim the space at any time and the only cost is a refetch. Storage is opened lazily on first use, so constructing one costs nothing and an environment without caches (a non-secure context, a worker without the API, Node) degrades to a store that holds nothing rather than throwing.
cacheNamespacePrefix(namespace: string): string
The key prefix every record of namespace shares.
clamp(value: number, min: number, max: number): number
Clamp value to the closed interval [min, max].
Exact byte length one mip level of width x height texels occupies in format. A level is padded out to whole blocks, so a 5x5 BC7 level costs four blocks, not one and a half. Container parsers compare this against the byte length the file declares: a mismatch means the file is truncated or its format was mis-identified, and uploading it would hand the driver a short buffer.
Generic arcade-stick controllers. Covers the standard 8-button plus shoulder/trigger layout common to most fightsticks. No analog sticks are present, so the axis list is empty and the D-pad is exposed as discrete buttons (indices 12-15).
Build the Shader behind a filter pass: fills in the default vertex stage per language, upgrades legacy GLSL when asked, and carries any uniform declaration through to the source. This is how a filter declares typed uniforms: ShaderFilterOptions.uniforms carries starting VALUES, so the declaration has to reach the source, and the source is what the filter is then built from with ShaderFilter.from. ts const shader = createFilterShader({ glsl: { fragment }, wgsl, uniforms: { uTime: UniformType.Float }, }); const filter = ShaderFilter.from(shader, { uniforms: { uTime: 0 } });
Nintendo Joy-Con (L) held horizontally as a solo controller. Declares only channels that physically exist on the device - one stick (surfaced through the LeftStick channels so gamepad-agnostic code that binds "the stick" works regardless of which half is held), four face buttons, the L and ZL outer shoulders, the SL/SR rail buttons, Minus, Capture, and the stick-click. Right-stick channels, Plus/Home, Touchpad and auxiliary axes are intentionally absent - use Gamepad.hasChannel before binding anything that may not exist on every family. The SL/SR rail buttons take paddle channels rather than the standard shoulder channels, which belong to the physical L and ZL. Naming the channel after the physical button is what keeps the prompt labels honest: LeftShoulder renders as "L" and LeftTrigger as "ZL", so a prompt only points at the right button when those channels carry the buttons actually labelled that way. Which of them is comfortable in the horizontal solo grip is a question for the game's binding layer. The specific slots follow SDL, which assigns the rail buttons by the hand they sit under rather than by grip: left SL is SDL_GAMEPAD_BUTTON_LEFT_PADDLE1 (this engine's Paddle1) and left SR is LEFT_PADDLE2 (Paddle3). The right half takes the two right-hand slots, so a pair of solo Joy-Cons never collides on a paddle channel. Button indices are verified against hardware (Chromium on Windows, Bluetooth, 057e:2006), which reports the pad as mapping: "standard" with 17 buttons and 2 axes.
Nintendo Joy-Con (R) held horizontally as a solo controller. Mirrors createJoyConLeftGamepadMapping - see it for why the lone stick takes the LeftStick channels and why the SL/SR rail buttons take paddle channels. Following SDL, this half takes the two right-hand slots: SR is SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1 (Paddle2) and SL is RIGHT_PADDLE2 (Paddle4). Button indices are verified against hardware (Chromium on Windows, Bluetooth, 057e:2007), which reports the pad as mapping: "standard" with 17 buttons and 2 axes.
Sony PlayStation controllers - DualShock 3, DualShock 4 and DualSense (PS3 / PS4 / PS5) over USB or Bluetooth. On PS4 and PS5 pads index 17 is the touchpad **click** (DUALSHOCK_BUTTON_TOUCHPAD == DUAL_SENSE_BUTTON_TOUCHPAD == 17); a PS3 pad has no touchpad and stops at the standard index 16. Select/Share/Create sits on the standard Select slot (index 8) on all three, so it needs no entry of its own - it is only printed differently, which is what the per-generation prompt labels carry. Touchpad *coordinates* are not available: browsers model the touchpad as a button, and the standard axis set stops after the two sticks. Reading finger positions requires WebHID and a custom GamepadDefinition.
Baseline mapping for dual-analog controllers that follow the standard W3C Gamepad API layout (axes 0-3 for both sticks, axes 4-7 auxiliary). Every standard-layout device is this layout plus a family tag and at most the one device-specific button at index 17.
Valve Steam Controller (28de:1102, 28de:1142). The device is recognised by family - a game that ships Steam artwork gets to pick it - but its LAYOUT is the generic standard one and is **not verified** for this device. Chromium lists both product IDs in gamepad_id_list.cc as kXInputTypeNone and normalises neither, so a raw Steam Controller may well report a different index order than the standard layout assumed here; no hardware confirmation exists either way. With Steam Input active the device instead appears as 28de:11ff "Steam Virtual Gamepad", which genuinely is standard-mapped. The right trackpad is surfaced as the right stick and the left trackpad as the left stick. Gyro and haptic-only inputs are not represented.
Valve Steam Deck (and the new Valve Controller via vendor fallback) when its raw HID gamepad is exposed directly to the browser - i.e. when Steam Input is *not* intercepting the device. With Steam Input intercepting, the device appears as 28de:11ff "Steam Virtual Gamepad" with a standard W3C layout instead. The raw layout is non-standard: face buttons live at indices 3-6 (not the W3C-standard 0-3), the D-pad at 16-19, paddles at 20-23, and triggers report as analog axes 8/9 rather than buttons 6/7. Indices are derived from the Linux SDL_GameControllerDB entry for Valve Steam Deck. Because those indices are raw rather than W3C-standard, this mapping declares GamepadMappingLayout.Raw: should a browser ever report the same device as mapping: "standard", resolveGamepadDefinition discards this layout in favour of the generic one rather than routing standard indices through raw slots.
Nintendo Switch Pro Controller over USB or Bluetooth. Index 17 is the Capture button (SWITCH_PRO_BUTTON_CAPTURE == BUTTON_INDEX_COUNT); Home maps to Guide, Minus/Plus to Select/Start. Some browsers only recognise the controller after it has been paired through Steam or a dedicated driver.
Normalise a texture, region or descriptor into a background descriptor. A texture becomes a nine-slice whose slices default to a third of the source per axis, so a frame is usable without measuring it first; a real skin states its slices. A source edge under three pixels degenerates to a slice of zero, which stretches that axis rather than failing. Passing fit paints the texture flat instead, and a descriptor is returned unchanged.
Resolve patch against base, role by role and state by state. Roles the patch does not name keep the base object itself, so an unchanged theme stays identical by reference - which is what makes a cascade refresh a pointer comparison rather than a deep diff.
Microsoft Xbox controllers (Xbox One, Xbox Series X/S, and compatible third-party XInput devices) over USB or Bluetooth. XInput follows the W3C standard layout natively. Index 17 is the Share button of the Xbox Series pads (XBOX_SERIES_X_BUTTON_SHARE == BUTTON_INDEX_COUNT). The Elite paddles are deliberately absent: the Windows path is XInput, whose XINPUT_GAMEPAD struct has no paddle bits, and no browser mapper emits them. They are reachable only through WebHID and therefore belong to a custom GamepadDefinition, not here.
Cross-fade from one playing Voice to another over duration. Ramps to up to toVolume and from down to silence. By default from is stopped when the fade finishes; pass stopAfter: false to keep it alive. Both voices must already be playing - start the incoming voice (typically at volume 0) before calling: ts const next = app.audio.play(track, { volume: 0 }); await crossFade(current, next, Time.seconds(1)); Returns a Promise that resolves once duration elapses.
decodeAudioData(arrayBuffer: ArrayBuffer): Promise<AudioBuffer>
Decode raw audio bytes into an AudioBuffer using the shared OfflineAudioContext. The context's sample rate matches the live AudioContext when one exists, else defaults to 44.1 kHz - decoding never forces a live context into existence. Note: on some older mobile WebKit versions decodeAudioData requires a running (live) context - decoding may fail with a browser-level error rather than an ExoJS-shaped error in those environments.
degreesToRadians(degree: number): number
Convert degree to radians.
formatShaderError(source: string, infoLog: string): string
Format a driver info log against its shader source: parses ERROR: 0:<line>: (ANGLE/Mesa GLSL) and WGSL :<line>:<col> positions, and returns the raw log plus a numbered source excerpt (±2 lines, > marker on each failing line). Falls back to the raw log verbatim when no line references parse (or none of them land inside source).
getAudioContext(): AudioContext
Return the global singleton AudioContext, creating it if it does not yet exist. Also starts interaction-unlock monitoring so the context will resume on the first user gesture. Throws AudioUnsupportedError when the environment provides no AudioContext.
getDistance(x1: number, y1: number, x2: number, y2: number): number
Euclidean distance between (x1, y1) and (x2, y2).
getOfflineAudioContext(): OfflineAudioContext
Return the shared singleton OfflineAudioContext used for audio decoding. Its sample rate matches the live AudioContext when one already exists, otherwise a 44.1 kHz default is used. Never creates a live AudioContext - decoding before the first user gesture must not spawn a suspended context (AU2); buffers resample transparently on playback if the rates differ.
inputChannelFromToken(token: string): InputChannelChannel a single InputBinding can subscribe to. | null
Channel token names, or null when no control carries that token. Returning null rather than falling back to a nearby control is the point: a profile saved by a newer build, a hand-edited file, or a typo must be reported to the player, never silently rebound to a different input. Callers that persist bindings (see BindingProfile.fromJSON) turn the null into a throw. Gamepad tokens resolve to slot-0-relative channels; the owning ActionMap rebases them onto its own pad.
Canonical InputToken for channel.
inRange(value: number, min: number, max: number): boolean
Return true when value lies within [min, max] (inclusive, regardless of whether min < max).
Returns true for blend modes that require the backdrop-aware compositor (shader-side blend + GPU texture copy). Modes 0-4 use fixed-function blending and return false. Modes 5-17 return true.
isAudioContextReady(): boolean
Return true if the global AudioContext has been created and is currently in the running state. Safe to call before getAudioContext; returns false if no context exists yet.
isCompressedTextureFormat(value: string): value
Whether value names a format this engine knows how to upload.
Whether surface is a canvas element in a document, and so carries the DOM affordances (style, layout box, event target, parent element) that an OffscreenCanvas does not. Realm-safe: HTMLCanvasElement is not defined in a worker, where the answer is always false.
isPowerOfTwo(value: number): boolean
Return true when value is a non-zero power of two.
Resolve a KeyboardEvent.code to its Keyboard channel, or undefined for a key ExoJS does not track (media and IME/language keys, and the empty code a soft keyboard reports). This is the single seam between the platform's physical-key identity and the engine's channel model - see Keyboard for what "physical" means here and why keyCode is never used. For a modifier this returns the SIDE-SPECIFIC channel ('ShiftLeft' → Keyboard.ShiftLeft), never the aggregate - InputSystem writes the aggregate channel alongside it on every keydown/keyup. Useful in a rebinding UI that has a raw DOM event rather than an engine channel; InputSystem.onKeyDown already reports the resolved channel.
lerp(startValue: number, endValue: number, ratio: number): number
Linear interpolation between startValue and endValue at normalized ratio ∈ [0, 1].
The index width a mesh drawn from indices over vertexCount vertices needs. An authored stream keeps the width it was authored with - narrowing a Uint32Array that happens to fit would make the format depend on the values rather than on the declaration, so the same geometry could change width when its content changes. A non-indexed mesh has its indices synthesized, so its width follows purely from how many vertices there are to address.
supported in the engine's preference order. Backends collect what their device reports in whatever order they probe it; this puts both of them on one ranking so variant selection cannot depend on which backend is live.
quadraticCurveTo(fromX: number, fromY: number, cpX: number, cpY: number, toX: number, toY: number, path: number[], len: number): number[]
Sample a quadratic Bézier curve at len + 1 evenly-spaced t values (including t = 0 and t = 1) and append the resulting (x, y) pairs to path. Returns path for chaining.
radiansToDegrees(radian: number): number
Convert radian to degrees.
registerAudioWorkletProcessor(audioContext: BaseAudioContext, processorName: string, source: string): Promise<void>
Registers an AudioWorkletProcessor (defined by source) with the given AudioContext under processorName. Idempotent: subsequent calls with the same context+name return immediately. Concurrent calls (race-condition protection) share a single in-flight registration promise. Source is bundled at build time as a JavaScript string and turned into a Blob URL at runtime - no separate worklet asset file is shipped.
Register a custom NodeSerializer. Defaults to the defaultSerializationRegistry; pass app.serializers to register the type only for that Application (it stays isolated from other apps and from the global registry, resolving through the fallback chain). Use this to make your own SceneNode subclasses serializable. Delegate to a base type's behaviour by composing with the framework helpers, or register a fully custom serializer. ts registerSerializer('PowerUp', PowerUp, { write: (node) => ({ kind: node.kind }), read: (data) => new PowerUp(typeof data.kind === 'string' ? data.kind : ''), });
Advance the frame counter and zero all per-frame accumulators in place. Call once at the start of each render tick before recording new data. Note: RenderStats.gpuMemoryBytes is intentionally **not** reset here. It is a running total of live GPU resources owned by the backend's resource accountant, which persists across frames; zeroing it each tick would make it read 0 after the first frame. RenderStats.gpuFrameTimeMs is likewise left alone: it describes an already-finished frame whose results resolved late, so zeroing it per tick would blank it on every frame it is read on.
The string a CacheStore persists a record under. Composed of stable values only - never object identity, a constructor name a minifier may rewrite, or an install ordinal - so a record written in one session is found again in the next. The source key comes last and is left unescaped: it is the only field that may contain a separator, and nothing follows it to be confused with. That also puts every record of one namespace in one contiguous key range, which is what lets a store clear a namespace without an index.
sign(value: number): number
Return the sign of value as -1, 0, or 1. Unlike Math.sign, returns 0 only for strict zero (not -0).
triangulate(vertices: ArrayLike<number>): Uint32Array
Triangulate a simple 2D polygon by ear-clipping. Input: vertices is a flat sequence of (x, y) pairs (length must be even, minimum 6 = 3 vertices). Polygon may be CW or CCW; the algorithm normalises to CCW internally. Output: a Uint32Array of indices in groups of 3 (per triangle), referencing vertex positions (the i-th vertex spans vertices[2*i], vertices[2*i + 1]). Polygons that are degenerate (all collinear), zero-area, or self-intersecting may produce incomplete output but must not throw or hang.
trimRotation(degrees: number): number
Normalise a rotation in degrees to the range [0, 360). Negative values are wrapped into the positive range.
Constants36
Order in which the engine prefers compressed formats when several are available, best first. Both backends filter this to the formats their device actually implements, so RenderBackend.supportedTextureFormats - and therefore variant selection - ranks identically on WebGL2 and WebGPU. The order runs from the highest-quality RGBA formats down to the cheapest, with the single- and two-channel formats last: they are not interchangeable with an RGBA format, so they only ever rank against each other.
The asset types every Application installs. The set is fixed at build time and cannot be added to or replaced at runtime: a type an application brings of its own is installed alongside these, on that application only. Their ids are short rather than reverse-DNS because the engine owns them, and because they double as the cache namespaces a persistent store writes under. A type is listed unconditionally even where the browser API behind it may be absent. Constructing the type touches nothing; only loading an asset of it does, and a missing API should fail that load rather than silently make the type unknown - which is what a conditional list did, leaving the type-level suffix table claiming a type the runtime had never installed.
DEFAULT_BLOCK_CACHE_NAME: "exojs-container-blocks"
The name cacheApiBlockStore opens when the caller names none.
The theme a UI layer uses until one is assigned. It reproduces the widgets' built-in look, so assigning a patch changes only what the patch names.
INSTANCE_TRANSFORM_GLSL: "\nin vec2 a_position;\nin vec2 a_texcoord;\nin vec4 a_color;\nin uint a_nodeIndex;\n\nuniform mat3 u_projection;\nuniform mat3 u_group;\nuniform vec4 u_viewport;\nuniform sampler2D u_transforms;\nuniform sampler2D u_tintTexture;\n\n// #exo-include transform-texture\n\nvec2 exoInstanceClipPosition(vec2 localPosition, uint nodeIndex) {\n int row = int(nodeIndex);\n vec4 m0 = texelFetch(u_transforms, exoTransformTexel(row, 0), 0); // a, b, c, d\n vec4 m1 = texelFetch(u_transforms, exoTransformTexel(row, 1), 0); // tx, ty, snapMode, 0\n // mat3() takes columns, the engine's Matrix is row-major — hence the\n // interleave. Yields | a b tx ; c d ty ; 0 0 1 |.\n mat3 transform = mat3(\n m0.x, m0.z, 0.0,\n m0.y, m0.w, 0.0,\n m1.x, m1.y, 1.0\n );\n\n vec2 clip = (u_projection * u_group * transform * vec3(localPosition, 1.0)).xy;\n\n // Render-only pixel snapping (m1.z: 0 = none, non-zero = snap origin). Snap\n // the instance ORIGIN's device-pixel position and rigid-shift the primitive\n // by the same delta. floor(x + 0.5) matches the CPU Math.round policy;\n // GLSL round() is undefined at .5.\n if (m1.z != 0.0) {\n vec2 originClip = (u_projection * u_group * vec3(m1.x, m1.y, 1.0)).xy;\n vec2 originDevice = u_viewport.xy + (originClip * 0.5 + 0.5) * u_viewport.zw;\n clip += (floor(originDevice + 0.5) - originDevice) * 2.0 / max(u_viewport.zw, vec2(1.0));\n }\n\n return clip;\n}\n\nvec4 exoInstanceTint(uint nodeIndex) {\n return texelFetch(u_tintTexture, exoTintTexel(int(nodeIndex)), 0);\n}\n"
GLSL ES 3.00 form of the instanced-batch contract. Insert it directly after the #version 300 es directive of a custom material's **vertex** shader; it declares no precision qualifier, relying on the vertex stage's default highp for floats. Declares the engine-supplied inputs (a_position, a_texcoord, a_color, a_nodeIndex) and uniforms, and exposes two helpers: - vec2 exoInstanceClipPosition(vec2 localPosition, uint nodeIndex) - maps a local-space vertex of the current instance to clip space, including pixel snapping. Returns clip space rather than world space because snapping is defined in device pixels and needs the projection, group matrix and viewport together. - vec4 exoInstanceTint(uint nodeIndex) - the instance's tint, to be passed through to the fragment stage. Unused declarations are stripped at link time; a batch shader that ignores texcoords or vertex colors needs no further ceremony. The #exo-include comment the constant carries is expanded by the engine when it compiles the shader - it is what keeps the buffers' texel addressing internal, so leave it in place.
INSTANCE_TRANSFORM_WGSL: "\nstruct ExoTransformSlot {\n m0: vec4<f32>,\n m1: vec4<f32>,\n};\n\nstruct ExoTransformUniforms {\n projection: mat3x3<f32>,\n group: mat3x3<f32>,\n flags: vec4<f32>,\n viewport: vec4<f32>, // device-pixel snap rect (x, y, width, height)\n};\n\n@group(0) @binding(0) var<uniform> exoUniforms: ExoTransformUniforms;\n@group(0) @binding(1) var<storage, read> exoTransforms: array<ExoTransformSlot>;\n@group(0) @binding(2) var<storage, read> exoTints: array<u32>;\n\nfn exoInstanceClipPosition(localPosition: vec2<f32>, nodeIndex: u32) -> vec2<f32> {\n // Shared slot convention: m0 = (a, b, c, d), m1 = (tx, ty, snapMode, 0).\n let slot = exoTransforms[nodeIndex];\n let world = vec3<f32>(\n slot.m0.x * localPosition.x + slot.m0.y * localPosition.y + slot.m1.x,\n slot.m0.z * localPosition.x + slot.m0.w * localPosition.y + slot.m1.y,\n 1.0\n );\n\n var clip = (exoUniforms.projection * exoUniforms.group * world).xy;\n\n // Render-only pixel snapping (slot.m1.z: 0 = none, non-zero = snap origin).\n // floor(x + 0.5) matches the CPU Math.round policy; WGSL round() is\n // half-to-even.\n if (slot.m1.z != 0.0) {\n let originClip = (exoUniforms.projection * exoUniforms.group * vec3<f32>(slot.m1.x, slot.m1.y, 1.0)).xy;\n let originDevice = exoUniforms.viewport.xy + (originClip * 0.5 + vec2<f32>(0.5)) * exoUniforms.viewport.zw;\n clip = clip + (floor(originDevice + vec2<f32>(0.5)) - originDevice) * 2.0 / max(exoUniforms.viewport.zw, vec2<f32>(1.0));\n }\n\n return clip;\n}\n\nfn exoInstanceTint(nodeIndex: u32) -> vec4<f32> {\n return unpack4x8unorm(exoTints[nodeIndex]);\n}\n"
WGSL form of the instanced-batch contract, for the WebGPU backend. Insert it at the top of a custom material's shader source. Declares the engine-owned @group(0) bindings and exposes the same two helpers as INSTANCE_TRANSFORM_GLSL. Because WGSL has no global vertex inputs, both take the node index explicitly - which is also why the GLSL helpers take it as a parameter, so a material's two shader bodies read alike: - exoInstanceClipPosition(localPosition: vec2<f32>, nodeIndex: u32) -> vec2<f32> - exoInstanceTint(nodeIndex: u32) -> vec4<f32> The vertex entry point must take @location(6) nodeIndex: u32 in its input struct; @location(0..2) carry position, texcoord and color as in the GLSL form.
JSON, stored as the text that arrived rather than as the parsed value. Keeping the text is what makes the stored form byte-exact: a parsed value round-trips through key order, number formatting and undefined handling that the response never had. Parsing is cheap enough to repeat on read.
maxPointers: 16
Maximum number of simultaneous tracked pointers (mouse / touch / pen).
maxUint16VertexCount: 65536
Largest vertex count a 16-bit index stream can address. A Uint16Array index holds 0..65535, so a mesh of exactly this many vertices is still fully addressable; one vertex more is not.
The built-in music asset type.
Fires once for every Application that finishes starting up, for tooling that does not own the application it wants to reach: a devtools panel, an inspector overlay, a probe script driving a page it did not write. Dispatched from Application.start once the render backend is initialized and Application.capabilities has settled, and before the initial scene navigation - so a listener is attached in time to observe the first scene load. An application that is constructed but never started never reaches this point and is never announced. Each application is announced at most once, however often it is stopped and started again. **Development builds only.** The dispatch is compiled out of production builds, where this signal therefore never fires. The signal object itself still exists, so importing it is safe in any build - it simply stays silent. This is the engine's one deliberate process-wide hook, and it exists for tooling rather than for application code: an application that wants to know about its own startup already has await app.start(). It is a notification, **not** a registry - nothing here holds a list of live applications, so a listener added after an application started will not hear about it. A handler must not retain the application beyond its Application.destroy: what is handed out is a live instance, and keeping it alive past its teardown pins its backend, loader and audio context with it. Hold what you need from it, or drop the reference when the application goes down. ts import { onAppInitialized } from '@codexo/exojs'; onAppInitialized.add(app => { console.log(ExoJS running on ${app.backend.backendType}); });
Signal that dispatches once the global AudioContext enters the running state. Handles browser autoplay-policy by listening for user-interaction events (mousedown, touchstart, touchend) and resuming a suspended context automatically. Dispatches once per run of the context: on the first unlock, and again on every later locked-to-running edge, so an object constructed while the context sits suspended (an iOS audio-session interruption, a bfcache restore, ...) still gets its setup callback. Handlers subscribed while the context is already running do not receive a dispatch for that run - check isAudioContextReady first and set up directly when it returns true, then subscribe only for the locked case and unsubscribe on the first dispatch.
Dispatches when any AudioSystem drops a play call because the browser's autoplay policy still blocks audio, carrying the system that was blocked. The process-wide counterpart to AudioSystem.onPlaybackBlocked, and process-wide for the same reason onAudioContextReady is: the AudioContext is a singleton, so the lock belongs to the page rather than to any one application, and a single gesture releases it for all of them. Use this where the blocked system is not reachable - a wrapper hosting someone else's code, a preview shell - and the instance signal everywhere else. The payload is what keeps two applications on one page distinguishable. Throttled per system exactly as the instance signal is.
pointerSlotSize: 16
Number of channel slots reserved per pointer. 16 pointers × 16 slots = 256 (fills the Pointers category exactly).
SERIALIZATION_VERSION: 1
Current scene-serialization format version. Written into every SerializedScene and checked on deserialize so saved data can outlive the code that produced it (see the migration chain in migrate).
TAU: number
τ = 2π, the full-circle radian constant.
textGradientMaxStops: 8
How many stops one text gradient may carry. The ramp is evaluated per fragment from the node's own packed style row, so the stop list has to be a fixed-size part of it rather than a texture of its own. Eight covers the gradients display type actually uses; more of them would cost every text node the space whether it gradients or not.
The built-in text asset type.