5.3 KiB
scene ($scene)
Ambient-scene runtime — mounts self-contained animated scenes (WebGL shaders / canvas 2D) on a host element and owns, once, the citizenship every ad-hoc background used to reinvent or skip: frame loop, off-view pause, resize + DPR cap, mandatory reduced-motion policy, pointer smoothing, WebGL context loss/restore, a concurrent-scene budget and full teardown.
An Engine* art (public methods over private state, no $state), the exact
sibling of arts/motion: the DOM arrives injected through the structural
SceneDom port (requestFrame / cancelFrame / observeResize /
observeIntersection / listen / prefersReducedMotion) — ActiveDom
satisfies it; this art imports no other art.
import { createEngineScene } from '$scene';
const scene = createEngineScene({ dom: uix.dom, logger: uix.logger });
const handle = scene.mount(node, meshEffect, { speed: 0.4 });
handle.setParams({ speed: 1 });
handle.dispose();
The effect contract
An effect is a RESOURCE (data + draw code, typed params) — the pattern of
sema's sounds.ts. Two shapes, one base:
| Field | Rule |
|---|---|
reduce |
Mandatory ('static-frame' renders one frame at t=0 · 'hide' mounts nothing). The engine throws SceneConfigError without it. |
defaults |
Authoring defaults; mount(_, _, params) shallow-merges over a copy. |
driver: 'webgl' | 'webgl2' |
fragmentShader() + setup(gl, program) + update(gl, state, api). Standard uniforms provided: uResolution (vec3 w,h,aspect — device px), uTime, uMouse (normalized, Y up). GLSL1 fragments: no # directives, premultiplied output, vUv varying provided. GLSL3 fragments ship their own #version 300 es. |
driver: 'canvas2d' |
setup(ctx, api) (deferred until the host is sized) + optional resize + draw(ctx, state, api). The ctx is pre-scaled to CSS px; the driver clears each frame. |
| custom pipeline (webgl) | Opt-in for effects that are real GEOMETRY (displaced meshes, particle clouds, multi-pass post-processing) rather than a fullscreen fragment. Add vertexShader() (the driver compiles it instead of its fullscreen triangle) + draw(gl, state, api) (issues the draw call after update), and optionally glContext: { depth?, dprCap? } (a depth buffer for 3-D; a tighter DPR cap for chunky-pixel looks — the engine takes min(engine cap, effect cap)). The effect owns its buffers, created lazily in update keyed by params so a context restore rebuilds them (setup has no params). Context, loop, resize, standard uniforms, blending and teardown stay with the driver. |
onPointerMove / onPointerDown / onPointerUp / onPointerLeave |
Opt-in — presence attaches HOST listeners through the port (the canvas never takes pointer events). On leave the engine also recenters the smoothed pointer in both spaces. |
Per-frame the effect reads SceneApi: time (seconds, accumulated while
running — off-view pauses never jump), dt (capped 0.1 s), live params,
size (buffer px + dpr + CSS px) and the smoothed pointer.
Why an extension, not a new driver. The custom pipeline was introduced by the
beameffect (3-D specular ribbons) as a justified extension of theSceneEffectcontract rather than a third driver — the driver keeps owning all the citizenship; the effect only takes over geometry + the draw call. Six Tier B effects reuse it (particles, dither, grid, eter, pixel-blast, hyperspeed). The contract lives intypes.ts; the design rationale indocs/process/PLAN-scene-ambient-pack.md.
Authoring a new effect
An effect is a shared resource; adding one to the Ambient pack is two files
(the contract half lives here, the registration half in the pack):
- Write the effect at
src/arts/scene/effects/{name}.tsagainst theSceneEffectcontract above — areducepolicy,defaults, a driver, and colors as concrete hex (token resolution is the CONSUMER's job, per pack P-4 — this art imports no uix). - Register it at
src/packs/ambient/effects/{name}.ts(import = opt-in, tree-shaken): callregisterAmbientEffect(...)anddeclare moduleto merge the param type intoAmbientEffects. Then it is<Ambient effect="{name}">.
The pack tier + the P contract those pieces obey are in
docs/architecture/packs.md.
Consumers + provenance
- The
Ambientpack (src/packs/ambient) mounts effects decoratively — tier + P contract indocs/architecture/packs.md. - The agentic phase will mount the same effects semantically through the
canonical
Auracomponent (delegate+sustain); at that point this engine is promoted to auix.sceneservice (defineEngineScenefactory) — deliberately NOT before (D4,docs/process/PLAN-scene-ambient-pack.md). - The seed collection lives untouched in
web/routes/demos/animationsas comparative reference; this runtime consolidates its 8 verbatimWebGLBackgroundcopies + the aurora WebGL2 pattern, and fixes what none of the 45 handled (context loss, per-frame layout reads, mandatory reduce).
Tests
npx vitest run src/arts/scene
Lifecycle, reduce policies, unsupported-context degradation, budget warning
and dispose idempotence run against a fake SceneDom (no browser needed).