@wity/scene-core
The full headless engine. Parse, evaluate, serialize, and validate wity-scene documents.
Install
npm install @wity/scene-coreFor Node.js environments, also install the optional XML peer:
npm install @xmldom/xmldomparse(xml)
Parse a wity-scene XML string into a WityScene object.
parse(xml: string): WitySceneThrows if the XML is malformed or the root element is not <wity-scene>.
import { parse } from '@wity/scene-core';
const scene = parse(xmlString);
// → { version: '1.0', width: 1920, height: 1080, dur: 8, layers: [...], cast: [...] }evaluate(scene, t)
Evaluate a scene at time t (seconds), returning the full render frame.
evaluate(scene: WityScene, t: number): ComputedFramet is clamped to [0, scene.dur]. Pure function — no mutation.
import { evaluate } from '@wity/scene-core';
const frame = evaluate(scene, 1.5);
// → { t: 1.5, width: 1920, height: 1080, elements: [...] }Note: ws-audio elements appear in frame.elements with visible set correctly for their temporal window but produce no pixel output. ws-character entities are not in elements — access them via scene.cast.
serialize(scene)
Serialize a WityScene back to a canonical XML string. Round-trips cleanly with parse().
serialize(scene: WityScene): stringimport { serialize } from '@wity/scene-core';
const xml = serialize(scene);
// → '<?xml version="1.0" encoding="UTF-8"?>\n<wity-scene ...'The <ws-cast> section is serialized before layers if scene.cast is non-empty.
validate(scene)
Validate a WityScene object without throwing.
validate(scene: WityScene): { valid: boolean, errors: string[], warnings: string[] }import { validate } from '@wity/scene-core';
const result = validate(scene);
if (!result.valid) {
console.error(result.errors);
}resolveUnit(value, containerSize)
Resolve a unit value string to pixels.
resolveUnit(value: string | number, containerSize: number): numberimport { resolveUnit } from '@wity/scene-core';
resolveUnit('50%', 1920) // → 960
resolveUnit('120px', 0) // → 120
resolveUnit(80, 0) // → 80Types
All JSDoc types are in schema/types.js and re-exported from the package root.
WityScene
{
version: string, // "1.0"
width: number, // canvas width px
height: number, // canvas height px
dur: number, // total duration seconds
layers: WsLayer[],
cast: WsCharacter[], // semantic entities (non-rendered)
}WsLayer
{
id: string,
z: number,
opacity: number, // 0–1
elements: WsElement[],
}WsElement
WsText | WsRect | WsImage | WsVideo | WsAudioAll visual elements (WsText, WsRect, WsImage, WsVideo) share WsElementBase:
{
id, x, y, anchor, begin, dur, z, opacity,
animateIn, animateOut, animateDur,
animateEasing?, // custom cubic bezier: "x1,y1,x2,y2" — overrides built-in preset easing
name?,
}WsAudio has a minimal base: { id, begin, dur } — no spatial attributes.
WsKeyframe
Keyframe children of WsText, WsRect, and WsImage. Animate position and/or opacity over element-relative time.
{
t: number, // element-relative time in seconds (from element's own begin)
x?: string | number, // horizontal position at this keyframe (unit value)
y?: string | number, // vertical position at this keyframe (unit value)
opacity?: number, // opacity 0–1 at this keyframe
easing?: string, // cubic bezier for segment FROM this keyframe: "x1,y1,x2,y2"
}Only specified properties are animated; unspecified fall back to the element's static attribute value. Keyframes are sorted by t and interpolated linearly by default.
WsText
WsElementBase & {
tag: 'ws-text',
content: string,
fontSize: string | number,
fontFamily: string,
fontWeight: string,
color: string,
textAlign: 'left' | 'center' | 'right',
lineHeight: number,
maxWidth: string | number | null,
letterSpacing: string | number,
keyframes?: WsKeyframe[],
}WsRect
WsElementBase & {
tag: 'ws-rect',
width: string | number,
height: string | number,
fill: string,
stroke: string | null,
strokeWidth: number,
rx: number,
keyframes?: WsKeyframe[],
}WsImage
WsElementBase & {
tag: 'ws-image',
src: string,
width: string | number,
height: string | number,
fit: 'cover' | 'contain' | 'fill' | 'none',
keyframes?: WsKeyframe[],
}WsVideo
WsElementBase & {
tag: 'ws-video',
src: string,
width: string | number,
height: string | number,
fit: 'cover' | 'contain' | 'fill' | 'none',
volume: number, // 0–1
trimIn: number, // seconds into source file
trimOut: number | null, // seconds into source file; null = no trim
muted: boolean,
cues?: WsCue[], // optional timed speech/subtitle cues
}WsAudio
{
tag: 'ws-audio',
id: string,
begin: number,
dur: number,
src: string,
volume: number, // 0–1
loop: boolean,
trimIn: number,
trimOut: number | null,
name?: string, // optional human-readable display name
cues?: WsCue[], // optional timed speech/subtitle cues
}WsCue
Timed speech/subtitle metadata nested inside WsVideo or WsAudio. Non-rendered — consumed by analysis services, AI agents, and accessibility tools. Timestamps are relative to the source media file.
{
begin: number, // start time within source media (seconds)
end: number, // end time within source media (seconds)
text: string, // speech/subtitle text content
speaker?: string, // optional ws-character id (links to scene.cast)
}WsCharacter
{
id: string,
name: string,
role?: string,
description?: string,
avatarUrl?: string,
}ComputedFrame
{
t: number, // time this frame was computed for
width: number,
height: number,
elements: ComputedElement[], // sorted by z ascending
}ComputedElement
{
id: string,
tag: 'ws-text' | 'ws-rect' | 'ws-image' | 'ws-video' | 'ws-audio',
x: number, // pixels, anchor-adjusted + animation offset (visual elements)
y: number,
opacity: number, // element × layer × animation opacity
z: number, // layer.z * 1000 + el.z
visible: boolean, // false outside [begin, begin+dur]
props: object, // tag-specific resolved props
content: string | null, // text content (ws-text only)
}