# wity-scene — Full Documentation
Version: scene-core v1.0.0 · scene-headless v1.0.0 · scene-player v1.0.0 · scene-to-video v1.0.0 · scene-compose v1.0.0 · scene-to-pdf v1.0.0 · scene-to-pptx v1.0.0
---
## QUICK REFERENCE — read this first
### Core model
A wity-scene document is a **pure function of time**:
```
f(scene: WityScene, t: number) → ComputedFrame
```
- `scene` is parsed from XML (immutable)
- `t` is a time in seconds, clamped to `[0, scene.dur]`
- `ComputedFrame` is the complete render state — pixel positions, resolved props, effective opacity
No interpolation state. No playback timers. Call `evaluate(scene, t)` at any time, in any order.
### Install
```bash
npm install @wity/scene-core
# Node.js only (optional XML peer):
npm install @xmldom/xmldom
```
### Import
```js
import { parse, evaluate, serialize, validate, resolveUnit } from '@wity/scene-core';
// or:
import { parse, evaluate } from '@wity/scene';
```
### Minimal example
```js
const scene = parse(`
Opening Night
`);
const frame = evaluate(scene, 1.0);
// frame.elements → [{id, tag, x, y, opacity, visible, props, content}, ...]
```
---
## SCHEMA v1.0
### Document structure
```
WityScene
├── WsCharacter[] (ws-cast — semantic metadata entities, non-rendered)
└── WsLayer[] (z-ordered planes)
└── WsElement[] (ws-text | ws-rect | ws-image | ws-video | ws-audio)
```
### Root element
```xml
```
| Attribute | Type | Required |
|-----------|--------|----------|
| version | string | yes — must be "1.0" |
| width | number | yes — canvas width px |
| height | number | yes — canvas height px |
| dur | number | yes — total duration seconds |
### Layers
```xml
```
| Attribute | Default | Description |
|-----------|---------|-------------|
| id | — | unique layer id |
| z | 0 | layer stacking order |
| opacity | 1 | layer-level opacity, multiplied with element opacity |
### Common element attributes (visual elements)
| XML attribute | Object field | Default | Description |
|------------------|----------------|------------|-------------|
| id | id | auto | unique element id |
| x | x | 0 | horizontal position (unit value) |
| y | y | 0 | vertical position (unit value) |
| anchor | anchor | top-left | origin point for x/y |
| begin | begin | 0 | start time seconds |
| dur | dur | Infinity | duration seconds |
| z | z | 0 | z-index within layer |
| opacity | opacity | 1 | element opacity (0–1) |
| animate-in | animateIn | none | entrance animation |
| animate-out | animateOut | none | exit animation |
| animate-dur | animateDur | 0.4 | anim duration seconds |
| animate-easing | animateEasing | (none) | custom cubic bezier for animate-in/out: `"x1,y1,x2,y2"` |
| name | name | (none) | optional human-readable display name |
#### anchor values
`top-left` · `top` · `top-right` · `left` · `center` · `right` · `bottom-left` · `bottom` · `bottom-right`
#### animation values
`none` · `fade` · `fade-up` · `fade-down` · `slide-left` · `slide-right`
Slide animations travel 40px. Default easing: easeOutCubic (entrance) / easeInCubic (exit).
`animate-easing="x1,y1,x2,y2"`: override with a custom cubic bezier — same format as CSS `cubic-bezier()`. Applied to both in and out when set.
#### unit values
| Input | Meaning |
|----------|---------|
| `"50%"` | 50% of canvas width (for x/width) or height (for y/height) |
| `"120px"`| 120px |
| `120` | 120px |
### `` (inside ``, ``, ``)
Optional child elements that animate position and/or opacity over element-relative time.
```xml
Hello
```
| XML attribute | Object field | Required | Description |
|---------------|-------------|----------|-------------|
| t | t | yes | Element-relative time in seconds (from element's own `begin`) |
| x | x | no | Horizontal position at this keyframe (unit value) |
| y | y | no | Vertical position at this keyframe (unit value) |
| opacity | opacity | no | Opacity 0–1 at this keyframe |
| easing | easing | no | Cubic bezier for segment FROM this keyframe to the next: `"x1,y1,x2,y2"` |
- Only properties specified in at least one keyframe are animated; unspecified use the element's static value.
- `t` is element-relative — moving an element's `begin` does not require rewriting keyframe timestamps.
- Easing is defined on the departing keyframe (CSS convention).
- Composes with `animate-in`/`animate-out` — presets still apply at element boundaries.
- Not supported on `` or ``.
### ``
```xml
Opening Night
```
| XML attribute | Default |
|----------------|--------------|
| font-size | 3% (of min(width, height)) |
| font-family | sans-serif |
| font-weight | normal |
| color | #ffffff |
| text-align | center |
| line-height | 1.4 |
| max-width | (none) |
| letter-spacing | 0 |
Text content is the element's text node.
### ``
```xml
```
| XML attribute | Default |
|---------------|-------------|
| width | 100% |
| height | 100% |
| fill | transparent |
| stroke | (none) |
| stroke-width | 1 |
| rx | 0 |
### ``
```xml
```
| XML attribute | Default |
|---------------|----------|
| src | required |
| width | 100% |
| height | 100% |
| fit | cover — values: cover · contain · fill · none |
### ``
A video clip element — extends all common element attributes. Positioned and temporally placed within a layer.
```xml
Welcome back everyone
```
| XML attribute | Object field | Default | Description |
|---------------|-------------|---------|-------------|
| src | src | required | Video file URL |
| width | width | 100% | Display width |
| height | height | 100% | Display height |
| fit | fit | cover | Object-fit: cover · contain · fill · none |
| volume | volume | 1 | Playback volume 0–1 |
| trim-in | trimIn | 0 | Start offset within source file (seconds) |
| trim-out | trimOut | (none) | End offset within source file; omit = play to end |
| muted | muted | false | Mute audio track |
Both `` and `` may contain optional `` children (see below).
### ``
A temporal audio element — lives inside a ws-layer but has no visual output and no spatial attributes.
```xml
Intro melody
```
| XML attribute | Object field | Default | Description |
|---------------|-------------|----------|-------------|
| id | id | auto | Unique identifier |
| begin | begin | 0 | Start time seconds |
| dur | dur | Infinity | Duration seconds |
| src | src | required | Audio file URL |
| volume | volume | 1 | Volume 0–1 |
| loop | loop | false | Loop the audio |
| trim-in | trimIn | 0 | Start offset within source file (seconds) |
| trim-out | trimOut | (none) | End offset within source file |
| name | name | (none) | Optional human-readable display name |
### `` (inside `` or ``)
Optional timed speech/subtitle cue. Non-rendered metadata — consumed by analysis services, AI agents, and accessibility tools. Timestamps are relative to the **source media file** (aligned with trim-in/trim-out), not the scene timeline.
```xml
Welcome to the show
Thanks for having me
```
| XML attribute | Object field | Required | Description |
|---------------|-------------|----------|-------------|
| begin | begin | yes | Start time within source media (seconds) |
| end | end | yes | End time within source media (seconds) |
| speaker | speaker | no | ws-character id (links to scene.cast) |
Text content is the cue's text node.
In the parsed object, cues are accessed as `element.cues: WsCue[]` (optional — absent when no cues are present).
### `` and ``
The optional `` section is a direct child of `` (before layers). It contains `` metadata entities — **not rendered**, consumed by authoring tools, AI agents, players, and compilers.
```xml
```
| XML attribute | Object field | Required | Description |
|---------------|-------------|----------|-------------|
| id | id | yes | Unique character id |
| name | name | yes | Display name |
| role | role | no | Role in scene (e.g. "Host", "Narrator") |
| description | description | no | Free-form notes or personality |
| avatar-url | avatarUrl | no | Reference image URL |
Accessed as `scene.cast: WsCharacter[]` after parsing.
### Full example — product video scene
```xml
New Collection
Spring 2026
```
---
## API REFERENCE
### parse(xml)
```js
parse(xml: string): WityScene
```
Parse a wity-scene XML string into a WityScene object.
Throws if XML is malformed or root element is not ``.
Browser: native DOMParser. Node.js: @xmldom/xmldom (optional peer dep).
ID counter resets per call — auto-assigned ids are stable within a single parse.
### evaluate(scene, t)
```js
evaluate(scene: WityScene, t: number): ComputedFrame
```
Evaluate a scene at time t (clamped to [0, scene.dur]).
Pure function — no mutation, no side effects.
Elements in ComputedFrame are sorted by effective z ascending (painters algorithm).
Effective z = layer.z × 1000 + element.z.
Animation offsets are baked into x and y — renderer receives final pixel position.
`ws-audio` elements appear in `frame.elements` with correct `visible` flag but no pixel output — renderers should check `el.tag === 'ws-audio'` and handle accordingly.
`ws-character` entities are NOT in `frame.elements` — access via `scene.cast`.
### serialize(scene)
```js
serialize(scene: WityScene): string
```
Serialize a WityScene to canonical XML. Round-trips cleanly with parse().
Omits attributes equal to their defaults.
`` section appears before layers when `scene.cast` is non-empty.
### validate(scene)
```js
validate(scene: WityScene): { valid: boolean, errors: string[], warnings: string[] }
```
Structural validation without throwing. Checks root types, cast array, layer types, element types,
unit value forms, opacity/volume range [0,1], enum membership, id uniqueness across the scene.
### resolveUnit(value, containerSize)
```js
resolveUnit(value: string | number, containerSize: number): number
```
Resolve a unit value to pixels.
```js
resolveUnit('50%', 1920) // 960
resolveUnit('120px', 0) // 120
resolveUnit(80, 0) // 80
```
---
## TYPES
### WityScene
```js
{ version: string, width: number, height: number, dur: number, layers: WsLayer[], cast: WsCharacter[] }
```
### WsLayer
```js
{ id: string, z: number, opacity: number, elements: WsElement[] }
```
### WsElement = WsText | WsRect | WsImage | WsVideo | WsAudio
Visual elements share WsElementBase: `{ id, x, y, anchor, begin, dur, z, opacity, animateIn, animateOut, animateDur, animateEasing?, name? }`
```js
WsKeyframe: { t, x?, y?, opacity?, easing? }
WsText: WsElementBase & { tag: 'ws-text', content, fontSize, fontFamily, fontWeight, color, textAlign, lineHeight, maxWidth, letterSpacing, keyframes? }
WsRect: WsElementBase & { tag: 'ws-rect', width, height, fill, stroke, strokeWidth, rx, keyframes? }
WsImage: WsElementBase & { tag: 'ws-image', src, width, height, fit, keyframes? }
WsVideo: WsElementBase & { tag: 'ws-video', src, width, height, fit, volume, trimIn, trimOut, muted, cues? }
WsAudio: { tag: 'ws-audio', id, begin, dur, src, volume, loop, trimIn, trimOut, name?, cues? }
```
### WsCue (inside WsVideo or WsAudio)
Non-rendered metadata — timed speech/subtitle cues. Timestamps relative to source media file.
```js
{ begin: number, end: number, text: string, speaker?: string }
```
### WsCharacter
```js
{ id: string, name: string, role?: string, description?: string, avatarUrl?: string }
```
### ComputedFrame
```js
{ t: number, width: number, height: number, elements: ComputedElement[] }
```
### ComputedElement
```js
{
id: string,
tag: 'ws-text' | 'ws-rect' | 'ws-image' | 'ws-video' | 'ws-audio',
x: number, // px, anchor-adjusted + animation offset
y: number,
opacity: number, // element × layer × animation
z: number, // layer.z * 1000 + element.z
visible: boolean, // false outside [begin, begin+dur]
props: { // tag-specific, all units resolved to px
// ws-text: fontSize, fontFamily, fontWeight, color, textAlign, lineHeight, maxWidth, letterSpacing
// ws-rect: width, height, fill, stroke, strokeWidth, rx
// ws-image: src, width, height, fit
// ws-video: src, width, height, fit, volume, trimIn, trimOut, muted
// ws-audio: src, volume, loop, trimIn, trimOut
},
content: string | null, // text content for ws-text; null otherwise
}
```
---
## RENDERING
### HTML/CSS
```js
function render(container, frame) {
container.innerHTML = '';
for (const el of frame.elements) {
if (!el.visible) continue;
if (el.tag === 'ws-audio') continue; // handled by audio engine, not DOM
const div = document.createElement('div');
div.style.position = 'absolute';
div.style.left = `${el.x}px`;
div.style.top = `${el.y}px`;
div.style.opacity = String(el.opacity);
div.style.zIndex = String(el.z);
if (el.tag === 'ws-text') {
div.style.fontSize = `${el.props.fontSize}px`;
div.style.color = el.props.color;
div.style.fontFamily = el.props.fontFamily;
div.style.fontWeight = el.props.fontWeight;
div.style.textAlign = el.props.textAlign;
div.textContent = el.content ?? '';
}
if (el.tag === 'ws-rect') {
div.style.width = `${el.props.width}px`;
div.style.height = `${el.props.height}px`;
div.style.background = el.props.fill;
if (el.props.rx) div.style.borderRadius = `${el.props.rx}px`;
}
if (el.tag === 'ws-image' || el.tag === 'ws-video') {
const media = document.createElement(el.tag === 'ws-video' ? 'video' : 'img');
media.src = el.props.src;
media.style.width = `${el.props.width}px`;
media.style.height = `${el.props.height}px`;
media.style.objectFit = el.props.fit;
div.appendChild(media);
}
container.appendChild(div);
}
}
```
### Canvas 2D
```js
function renderCanvas(ctx, frame) {
ctx.clearRect(0, 0, frame.width, frame.height);
for (const el of frame.elements) {
if (!el.visible) continue;
if (el.tag === 'ws-audio') continue;
ctx.globalAlpha = el.opacity;
if (el.tag === 'ws-rect') {
ctx.fillStyle = el.props.fill;
ctx.fillRect(el.x, el.y, el.props.width, el.props.height);
}
if (el.tag === 'ws-text') {
ctx.fillStyle = el.props.color;
ctx.font = `${el.props.fontWeight} ${el.props.fontSize}px ${el.props.fontFamily}`;
ctx.textAlign = el.props.textAlign;
ctx.fillText(el.content ?? '', el.x, el.y);
}
}
ctx.globalAlpha = 1;
}
```
### requestAnimationFrame loop (manual)
```js
let start = null;
function tick(ts) {
if (!start) start = ts;
const t = (ts - start) / 1000;
render(container, evaluate(scene, t));
if (t < scene.dur) requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
```
### Node.js / FFmpeg (frame-by-frame compilation)
```js
import { parse, evaluate } from '@wity/scene-core';
import { createCanvas } from 'canvas'; // node-canvas
const scene = parse(xmlString);
const fps = 30;
const frames = Math.ceil(scene.dur * fps);
for (let i = 0; i < frames; i++) {
const t = i / fps;
const frame = evaluate(scene, t);
const canvas = createCanvas(frame.width, frame.height);
renderCanvas(canvas.getContext('2d'), frame);
// → write canvas to PNG → pipe to FFmpeg
}
```
---
## RENDERING A SCENE
`witySceneRender` is the single entry point for all scene rendering. Send your scene XML — routing to the correct internal pipeline is automatic. There are no other services to call. Never invoke downstream Lambdas directly.
Supported `outputFormat` values: `"mp4"` (default), `"pdf"`, `"pptx"`.
### Input
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `sceneXml` | string | required | The `` XML document |
| `outputFormat` | string | `"mp4"` | `"mp4"` · `"pdf"` · `"pptx"` |
| `options.fps` | number | `30` | Frame rate (mp4 only) |
| `options.sceneWidth` | number | `1280` | Canvas width px (mp4 only) |
| `options.sceneHeight` | number | `720` | Canvas height px (mp4 only) |
| `options.fontManifest` | object | `{}` | Custom fonts: `{ "FontName": "https://url/to/font.ttf" }` |
| `options.ts` | number | `0` | Snapshot timestamp seconds (pdf/pptx) |
| `options.timestamps` | number[] | — | Multi-page/slide timestamps (pdf/pptx); overrides `ts` |
| `options.variant` | string | `"standard"` | `"standard"` (96 DPI) or `"print"` (300 DPI) — pdf only |
| `options.dpi` | number | 96/300 | Override render DPI — pdf only |
```json
{ "sceneXml": "...", "outputFormat": "mp4", "options": { "fps": 30, "sceneWidth": 1920, "sceneHeight": 1080 } }
{ "sceneXml": "...", "outputFormat": "pdf", "options": { "ts": 2.0, "variant": "print" } }
{ "sceneXml": "...", "outputFormat": "pptx", "options": { "timestamps": [0, 5.0, 10.0] } }
```
### Output
| Field | Type | Description |
|-------|------|-------------|
| `url` | string | Public S3 URL of the rendered output |
| `fileSize` | number | Output file size in bytes |
| `pipeline` | string[] | Which internal Lambdas ran (for observability) |
| `pages` | number | Number of pages — pdf only |
| `slides` | number | Number of slides — pptx only |
### Routing (automatic)
**mp4:** gateway inspects element tags:
- Graphics only → `witySceneToVideo` only
- Graphics + media → `witySceneToVideo` → `witySceneCompose`
- Media only → `witySceneCompose` only (graphicsMp4Url: null)
**pdf:** → `witySceneToPdf` (single call).
**pptx:** → `witySceneToPptx` (single call).
Extensible: adding a new `outputFormat` requires one new Lambda + one entry in `PIPELINE_REGISTRY` — no changes to existing infrastructure.
---
## SERVER-SIDE PIPELINE INTERNALS
All pipelines are managed by `witySceneRender` — clients never invoke downstream Lambdas directly.
### mp4 pipeline (two-step)
```
sceneXml
├──▶ Step 1: witySceneToVideo (compile graphics)
│ ws-rect + ws-text + ws-image → PNG frames → silent MP4
│ ws-video and ws-audio are NOT rendered in this step
│ → returns: graphicsMp4Url (S3 URL)
│
└──▶ Step 2: witySceneCompose (full composite)
ws-video clips + ws-audio tracks + graphicsMp4Url
→ FFmpeg filter_complex → composited MP4 → S3 upload
→ returns: { url, fileSize }
```
Both steps receive the **same sceneXml**. Step 2 re-parses independently to extract ws-video/ws-audio. `graphicsMp4Url` is optional — null if the scene has no graphic elements.
---
### @wity/scene-to-video
Graphics-only compiler. Node.js / Lambda only.
```js
import { compile } from '@wity/scene-to-video';
// fontManifest: { 'FontFamily': 'https://cdn.../font.ttf' } — pass {} if no custom fonts
const { videoPath, cleanup } = await compile(sceneXml, fontManifest, { fps: 30 });
// videoPath = local /tmp path of the rendered silent MP4
// Upload to S3, then:
await cleanup();
```
**What is rendered:** ws-rect (filled rect + optional stroke + rx), ws-text (font/color/animate), ws-image (src + fit mode).
**What is skipped:** ws-video, ws-audio (they fall through to a warning log — no crash, no output).
Lambda `witySceneToVideo` input: `{ sceneXml, fontManifest?, fps? }`
Lambda response: `{ url: string, fileSize: number }`
Lambda config: 3008 MB / 300 s / 4096 MB ephemeral / Node.js 20
### @wity/scene-compose
Full compositor. Node.js / Lambda only.
```js
import { compose } from '@wity/scene-compose';
const { url, fileSize } = await compose(sceneXml, graphicsMp4Url, {
outputBucket: 'my-bucket', // or set OUTPUT_BUCKET env
fps: 30,
sceneWidth: 1920,
sceneHeight: 1080,
});
// url = public S3 URL of the final composited MP4
```
**Video compositing:** Each ws-video clip is downloaded, scaled/fit to its x/y/width/height, trimmed via -ss trimIn, overlaid with `enable='between(t,begin,end)'`. Fit modes: cover / contain / fill.
**Audio compositing:** All ws-audio tracks + non-muted embedded video audio → aresample=48000 → adelay:all=1 (begin offset, mono+stereo safe) → volume → amix normalize=0 dropout_transition=0.
**Graphics overlay:** graphicsMp4Url overlaid at 0:0 on top of all video compositing.
**S3 upload:** Streamed (no full-file read into memory) — safe for large outputs.
Lambda `witySceneCompose` input: `{ sceneXml, graphicsMp4Url?, fps?, sceneWidth?, sceneHeight? }`
Lambda response: `{ url: string, fileSize: number }`
Lambda config: 2048 MB / 300 s / 4096 MB ephemeral / Node.js 20
### @wity/scene-to-pdf
Document renderer. Node.js / Lambda only.
```js
import { render } from '@wity/scene-to-pdf';
const { pdfPath, cleanup } = await render(sceneXml, fontManifest, {
ts: 2.0, // snapshot time
variant: 'print', // 'standard' | 'print'
dpi: 300, // override render DPI
});
await cleanup();
// Multi-page:
const { pdfPath, cleanup } = await render(sceneXml, {}, { timestamps: [0, 3.0, 6.0] });
```
Lambda `witySceneToPdf` input: `{ sceneXml, fontManifest?, ts?, timestamps?, variant?, dpi? }`
Lambda response: `{ url: string, fileSize: number, pages: number }`
Lambda config: 2048 MB / 120 s / 1024 MB ephemeral / Node.js 20 / no FFmpeg layer
### @wity/scene-to-pptx
Presentation renderer. Node.js / Lambda only. Output importable in PowerPoint, Keynote, Google Slides.
```js
import { render } from '@wity/scene-to-pptx';
const { pptxPath, cleanup } = await render(sceneXml, fontManifest, {
timestamps: [0, 5.0, 10.0], // one slide per timestamp
});
await cleanup();
```
Lambda `witySceneToPptx` input: `{ sceneXml, fontManifest?, ts?, timestamps? }`
Lambda response: `{ url: string, fileSize: number, slides: number }`
Lambda config: 1024 MB / 120 s / 1024 MB ephemeral / Node.js 20 / no FFmpeg layer
### wityAudioProfile — audio loudness analysis
Standalone analysis Lambda. **Not routed through `witySceneRender`** — call directly. Downloads all `ws-video` and `ws-audio` media, decodes audio via FFmpeg, measures windowed RMS loudness, applies volume multipliers, returns per-element and power-mixed dBFS profiles.
```js
// Lambda input:
{ "sceneXml": "...", "options": { "windowMs": 100, "sampleRate": 16000 } }
// Lambda response — AudioProfileResult:
{
"sceneDuration": 15.0,
"windowMs": 100,
"sampleRate": 16000,
"elements": [
{
"id": "v1", "tag": "ws-video", "src": "https://...",
"begin": 0, "dur": 15, "volume": 0.8, "hasAudio": true,
"profile": {
"startTime": 0, "windowCount": 150, "windowMs": 100,
"rmsLinear": [0.12, 0.15, ...], // volume-adjusted
"rmsDbfs": [-18.4, -16.5, ...], // 20*log10(rmsLinear)
"peakDbfs": -6.2, "avgDbfs": -18.1
}
}
],
"mixed": {
"startTime": 0, "windowCount": 150, "windowMs": 100,
"rmsLinear": [...], "rmsDbfs": [...],
"peakDbfs": -5.8, "avgDbfs": -16.3
}
}
```
**Processing:** ws-video and ws-audio only. ws-rect/ws-text/ws-image ignored. `muted="true"` → skipped. Infinite `dur` → probed via ffprobe, clamped to `sceneDur - begin`. Decode errors are logged; element gets `profile: null`, processing continues.
**Mixed profile:** Power-additive (`Σ rms²` → `√`) across all elements, mapped to scene timeline with `begin` offsets. Useful for detecting quiet moments, loud peaks, or regions suitable for voice-over insertion.
Lambda config: 1024 MB / 120 s / 2048 MB ephemeral / Node.js 20 / FFmpeg layer required
---
## HEADLESS PLAYER — @wity/scene-player
`HeadlessPlayer` coordinates `SceneStore` + `TimelineState` + `evaluate()` into a single playback object. Emits `'frame'` on every RAF tick. Any renderer subscribes to `'frame'` and draws.
```js
import { HeadlessPlayer } from '@wity/scene-player';
const player = new HeadlessPlayer();
player.loadXml(xml);
// Wire renderer — only needs 'frame'
player.on('frame', ({ frame, t }) => renderCanvas(ctx, frame));
player.on('playback:ended', () => showReplayButton());
player.play();
player.seek(2.5); // emits 'frame' immediately — renderer stays in sync
player.pause();
```
### Mutation at runtime
```js
// URL arrives from backend — next frame picks it up, no reload
player.updateElement(videoElementId, { src: 'https://...' });
// Reposition a layer in time
player.updateLayer(layerId, { begin: 1.5, dur: 4.0 });
// Add an overlay layer
const lid = player.addLayer({ label: 'Lower Third', z: 20 });
player.addElement(lid, {
tag: 'ws-text', content: 'Now Live',
x: '50%', y: '85%', anchor: 'center',
animateIn: 'fade', animateDur: 0.3,
});
// Hot-swap entire scene (e.g. live edit from authoring layer)
player.replaceXml(updatedXml); // retains play/pause state
// Persist mutations
const xml = player.getXml();
```
### HeadlessPlayer API
```js
// Loading
player.loadXml(xml) // parse + load from XML string
player.loadScene(scene) // load pre-parsed WityScene
player.replaceXml(xml) // hot-swap; retains play state
// Playback
player.play()
player.pause()
player.stop() // pause + seek(0)
player.seek(t) // seek; emits 'frame' immediately
// State
player.currentTime // seconds
player.duration // seconds
player.isPlaying // boolean
player.isLoaded // boolean
player.progress // 0–1
// Mutation
player.addLayer(data) → layerId
player.updateLayer(id, patch)
player.removeLayer(id)
player.addElement(layerId, data) → elementId
player.updateElement(id, patch)
player.removeElement(id)
// Serialization / advanced
player.getXml() // serialize current scene → XML
player.getStore() // raw SceneStore for history/snap/clipboard
player.destroy()
```
### Events
| Event | Payload |
|---|---|
| `'frame'` | `{ frame: ComputedFrame, t }` |
| `'playback:started'` | `{ t }` |
| `'playback:paused'` | `{ t }` |
| `'playback:stopped'` | `{}` |
| `'playback:ended'` | `{ t }` |
| `'time:changed'` | `{ t }` |
| `'scene:loaded'` | `{ scene }` |
| `'scene:mutated'` | `{ scene }` |