@elucim/dsl
v0.22.0
Published
JSON/YAML DSL for declarative Elucim animations — define visualizations without writing React code
Maintainers
Readme
@elucim/dsl
Normalized JSON/YAML documents for animated Elucim scenes — designed for agents, editors, and content pipelines.
@elucim/dsl lets you describe animated diagrams as data. A public ElucimDocument is the normalized single-scene shape: version: '2.0', scene, an ID-keyed elements registry, optional timelines, optional stateMachines, and optional metadata. The <DslRenderer> component renders these documents as interactive Elucim visuals — no React authoring required.
Install
npm install @elucim/dsl @elucim/core react react-dom
# or
pnpm add @elucim/dsl @elucim/core react react-domQuick Start
From JSON
import { DslRenderer } from '@elucim/dsl';
import type { ElucimDocument } from '@elucim/dsl';
const myDiagram: ElucimDocument = {
version: '2.0',
scene: {
type: 'player',
width: 800,
height: 600,
fps: 30,
background: '#0d0d1a',
children: ['orbit'],
},
elements: {
orbit: {
id: 'orbit',
type: 'circle',
props: {
type: 'circle',
cx: 400,
cy: 300,
r: 100,
stroke: '#3b82f6',
strokeWidth: 3,
fill: 'none',
opacity: 0,
},
},
},
timelines: {
intro: {
id: 'intro',
duration: 60,
tracks: [
{
target: 'orbit',
property: 'opacity',
keyframes: [
{ frame: 0, value: 0 },
{ frame: 60, value: 1, easing: 'easeOutCubic' },
],
},
],
},
},
defaultStateMachine: 'main',
stateMachines: {
main: {
id: 'main',
entry: 'intro',
states: { intro: { timeline: 'intro' } },
transitions: [{ id: 'entry-start', from: 'entry', to: 'intro', trigger: 'onStart' }],
},
},
};
function App() {
return <DslRenderer dsl={myDiagram} />;
}From YAML
import { DslRenderer, fromYaml } from '@elucim/dsl';
const yaml = `
version: "2.0"
scene:
type: player
width: 800
height: 600
fps: 30
background: "#0d0d1a"
children: [orbit]
elements:
orbit:
id: orbit
type: circle
props:
type: circle
cx: 400
cy: 300
r: 100
stroke: "#3b82f6"
strokeWidth: 3
fill: none
opacity: 0
timelines:
intro:
id: intro
duration: 60
tracks:
- target: orbit
property: opacity
keyframes:
- { frame: 0, value: 0 }
- { frame: 60, value: 1, easing: easeOutCubic }
defaultStateMachine: main
stateMachines:
main:
id: main
entry: intro
states:
intro: { timeline: intro }
transitions:
- { id: entry-start, from: entry, to: intro, trigger: onStart }
`;
const myDiagram = fromYaml(yaml);
function App() {
return <DslRenderer dsl={myDiagram} />;
}API
<DslRenderer dsl={doc} />
Validates a normalized document and renders it as React components. If validation fails, it displays error messages instead of crashing.
Props:
dsl: ElucimDocument— The normalized document to renderclassName?: string— CSS class for the wrapper divstyle?: CSSProperties— Inline styles for the wrapper divtheme?: ElucimTheme— Custom color tokens as CSS custom propertiescolorScheme?: 'light' | 'dark' | 'auto'— Inject light/dark theme variables automaticallyposter?: 'first' | 'last' | number— Render a static frame instead of interactive playbackonError?: (errors: Array<{ path: string; message: string }>) => void— Callback for validation errorsref?: React.Ref<DslRendererRef>— Imperative handle for programmatic control
validate(doc: unknown): ValidationResult
Validates a document without rendering it.
import { validate } from '@elucim/dsl';
const result = validate(myDoc);
if (!result.valid) {
console.log(result.errors);
// [{ path: 'elements.orbit.props.cx', message: 'Required numeric field "cx"...', severity: 'error' }]
}fromYaml(input: string): ElucimDocument
Parses a YAML string into a validated ElucimDocument. It uses a JSON-compatible schema so YAML values such as on, yes, and NO stay as strings instead of being coerced to booleans.
import { fromYaml, ElucimYamlError } from '@elucim/dsl';
try {
const doc = fromYaml(yamlString);
// doc is a validated ElucimDocument, ready for <DslRenderer>
} catch (e) {
if (e instanceof ElucimYamlError) {
console.error(e.message);
console.error(e.validationErrors);
}
}renderToSvgString(doc, frame, options?)
Renders a document to an SVG string without a browser DOM — useful for server-side rendering, thumbnails, and static export.
import { renderToSvgString } from '@elucim/dsl';
const svg = renderToSvgString(myDoc, 0);Agent authoring helpers
@elucim/dsl/agent provides a small, deterministic toolkit for LLM and host workflows. It creates normalized documents, applies higher-level commands, generates explicit timeline/state-machine structures, and returns agent-readable quality reports.
import {
applyAgentCommands,
createDocument,
evaluateSceneForAgent,
inspectSceneForAgent,
repairDocumentForAgent,
sampleAnimationForAgent,
} from '@elucim/dsl/agent';
const doc = applyAgentCommands(createDocument({
preset: 'slide',
metadata: { title: 'Slope intuition' },
}), [
{
op: 'addElement',
element: {
id: 'title',
type: 'text',
role: 'title',
intent: { purpose: 'Introduce the core concept' },
layout: { x: 96, y: 96 },
props: { content: 'Slope as local change', fill: '$title' },
},
},
{ op: 'addRevealTimeline', timeline: { id: 'intro', targets: ['title'], preset: 'fadeIn' } },
{ op: 'createStateMachine', stateMachine: { id: 'main', timelineId: 'intro', start: 'onStart' } },
]).document;
const report = evaluateSceneForAgent(doc);
const repaired = repairDocumentForAgent(doc);
const animation = sampleAnimationForAgent(repaired.document, 'intro');
const inspection = inspectSceneForAgent(repaired.document, { timelineId: 'intro' });The agent helpers intentionally produce timelines and state machines rather than wrapper animation props. Use them when you want an LLM to make targeted scene edits without memorizing the full document schema. Diagnostic helpers such as getTimelineBounds(), repairDocumentForAgent(), sampleAnimationForAgent(), inspectSceneForAgent(), and createLoopingStateMachine() help agents detect timeline mistakes, auto-extend too-short timeline durations, prove that properties change over sampled frames, catch tiny/off-canvas/low-contrast scenes, and wire a generated timeline into live playback.
Diagram polish for agents
Generated diagrams should be checked with the deterministic polish APIs before handing them to a user. evaluateSceneForAgent() includes report.polish, and the same analysis is available directly as analyzePolish(doc). The report returns category scores plus diagnostics for layout, hierarchy, readability, contrast, graph readability, explanatory structure, and motion.
import {
analyzePolish,
applyNudge,
createBadgePreset,
createBoundaryPreset,
createCalloutCardPreset,
createComparisonTablePreset,
createConnectorPreset,
createDecisionNodePreset,
createQueueStackPreset,
createStepCardPreset,
createTextBlockPreset,
createTimelineRoadmapPreset,
inspectPolishHeuristics,
suggestDocumentNudges,
} from '@elucim/dsl';
const polish = analyzePolish(doc);
const heuristics = inspectPolishHeuristics(doc);
const nudges = suggestDocumentNudges(doc);
const safe = nudges.filter(nudge => nudge.confidence === 'safe');
const polished = safe.reduce((current, nudge) => applyNudge(current, nudge).document, doc);
const calloutElements = createCalloutCardPreset({
id: 'key-insight',
x: 80,
y: 420,
title: 'Key insight',
body: 'A semantic, token-based callout gives agents a polished starting point.',
});
const stepElements = createStepCardPreset({
id: 'draft',
x: 80,
y: 120,
title: 'Draft',
body: 'Cards, text blocks, and connectors are normal grouped elements.',
index: 1,
});
const connectorElements = createConnectorPreset({
id: 'draft-to-review',
from: 'draft',
to: 'review',
fromBounds: { id: 'draft', x: 80, y: 120, width: 300, height: 132 },
toBounds: { id: 'review', x: 440, y: 120, width: 300, height: 132 },
label: 'then',
});
const textBlockElements = createTextBlockPreset({
id: 'takeaway',
x: 80,
y: 320,
width: 360,
text: 'Wrapped text emits editable text lines inside a group.',
});
const supportingElements = [
...createBadgePreset({ id: 'status', x: 80, y: 80, label: 'review' }),
...createBoundaryPreset({ id: 'system', x: 60, y: 110, width: 420, height: 260, label: 'System boundary' }),
...createDecisionNodePreset({ id: 'cache-hit', x: 560, y: 120, text: 'Cache hit?' }),
...createQueueStackPreset({ id: 'queue', x: 80, y: 420, items: [{ label: 'Request' }, { label: 'Render' }] }),
...createTimelineRoadmapPreset({ id: 'roadmap', x: 420, y: 420, milestones: [{ label: 'Draft' }, { label: 'Polish' }] }),
...createComparisonTablePreset({
id: 'tradeoffs',
x: 80,
y: 560,
columns: ['Agent', 'Human'],
rows: [{ label: 'Best at', cells: ['Fast structure', 'Final taste'] }],
}),
];Semantic motion for agents
Use semantic motion helpers when an agent knows the intent of the animation but should not hand-author every keyframe. These helpers compile to ordinary Elucim timelines/state machines, so the result remains editable and renderer-independent.
import {
createAutoStaggerTimeline,
createReducedMotionDocument,
createSemanticMotionTimeline,
createStateSnapshotMotion,
holdFinalFrame,
lintMotion,
planMotionBeats,
previewBeatDiffs,
} from '@elucim/dsl';
const beats = planMotionBeats({ seconds: 12, fps: 30, beatCount: 4 });
const intro = createSemanticMotionTimeline(doc, {
id: 'intro-flow',
preset: 'revealFlow',
group: 'steps',
duration: beats[0].duration,
});
const handoff = createSemanticMotionTimeline(doc, {
id: 'draft-to-review-motion',
preset: 'handoff',
from: 'draft',
to: 'review',
connectorId: 'draft-to-review',
});
const rankedReveal = createAutoStaggerTimeline(doc, {
id: 'ranked-reveal',
group: 'steps',
orderBy: 'rank',
});
const lint = lintMotion({ ...doc, timelines: { intro, handoff, rankedReveal } });
const preview = previewBeatDiffs({ ...doc, timelines: { intro } }, { timelineId: 'intro-flow', beats });
const reduced = createReducedMotionDocument({ ...doc, timelines: { intro } }, { mode: 'minimal' });
const poster = holdFinalFrame({ ...doc, timelines: { intro } }, { timelineId: 'intro-flow' });Supported semantic presets are revealFlow, emphasizeDecision, tracePath, loopOnce, handoff, drainQueue, and compareBeforeAfter. createStateSnapshotMotion() turns named visual states into timelines plus a state machine, while holdFinalFrame() creates static poster/final-state documents. lintMotion() checks blank first frames, too-fast transitions, simultaneous overload, hidden labels, flashing, excessive motion, and reduced-motion readiness. previewBeatDiffs() returns agent-readable beat summaries describing what appears, disappears, moves, or changes.
Agent guidance:
- Prefer semantic roles and intent (
role: 'title',role: 'callout',intent.importance: 'primary' | 'secondary' | 'supporting') so polish can preserve explanatory meaning. - Add explicit relationship intent when the layout matters:
intent.target,intent.flowFrom,intent.flowTo,intent.relationship,intent.group, pluslayout.rankandlayout.locked. - Use composite helpers such as
createStepCardPreset(),createTextBlockPreset(),createCardGridPreset(),createConnectorPreset(),createDecisionNodePreset(),createBoundaryPreset(),createBadgePreset(),createQueueStackPreset(),createTimelineRoadmapPreset(),createComparisonTablePreset(),createAutoLayoutGroupPreset(), andcreateProgressiveRevealGroupPreset()for designed-slide structure. They emit ordinary editable groups, primitives, and timelines, not a separate persisted layout format. - Prefer
createConnectorPreset()for reading order and layout relationships. Its generated connector intent is consumed as a virtual ELK edge during semantic layout. - Use theme tokens such as
$title,$surface,$primary, and$mutedinstead of one-off literal colors unless a specific color is necessary. - Run
suggestDocumentNudges()after drafting. Applysafenudges automatically; presentreviewnudges, especially graph layout changes, for review. - Use
inspectPolishHeuristics()when an agent needs the raw evidence behind the aggregate score: element bounds, intersections, off-canvas overflow, text sizing, literal colors, graph crossings/overlaps, connector continuations, and semantic relationships. - Present
smooth-connector-continuationsas a review nudge when straight line/arrow connectors should become editable Bezier curves with rounded caps and smoother directionality. - Prefer semantic motion helpers over raw keyframes for common beats: reveal a flow, emphasize a decision, trace a connector, hand off between steps, drain a queue, compare before/after, and create reduced-motion fallbacks.
- For graph elements, provide stable node IDs and
edges; the layered graph nudge rewrites node coordinates while keeping the graph editable. - Use
getAgentOperationCatalog()when a host or CLI needs to pass the available authoring, validation, inspection, polish, and layout operations to an agent.
For broader explanatory scenes, agents can request an ELK-backed semantic layout review nudge. ELK is used as a layout solver only; the result is written back as ordinary editable element coordinates.
import { applyNudge, suggestSemanticLayoutNudges } from '@elucim/dsl';
const [layoutNudge] = await suggestSemanticLayoutNudges(doc);
const next = layoutNudge ? applyNudge(doc, layoutNudge).document : doc;Semantic layout nudges are always review confidence because they may move multiple elements.
DslRendererRef
Imperative handle exposed via ref on <DslRenderer>:
getSvgElement()— Returns the underlyingSVGSVGElementseekToFrame(frame)— Jump to a specific framegetTotalFrames()— Total frame count for the current playback surfaceplay()/pause()— Control playbackisPlaying()— Whether playback is active
Math expressions
compileExpression(expr: string) and compileVectorExpression(expr: string) compile safe math strings for function plots and vector fields. The evaluator supports arithmetic, common trig/log functions, constants (PI, E, TAU), and variables (x, or x/y for vector fields). It does not use arbitrary JavaScript evaluation.
Document Schema
Every public document uses this normalized structure:
interface ElucimDocument {
$schema?: string;
version: '2.0';
scene: ElucimScene;
elements: Record<string, ElucimElement>;
timelines?: Record<string, ElucimTimeline>;
stateMachines?: Record<string, ElucimStateMachine>;
defaultStateMachine?: string;
metadata?: ElucimMetadata;
}scene
The scene defines the render surface and top-level element order.
| Field | Description |
|-------|-------------|
| type | Usually player for interactive playback or scene for host-controlled rendering |
| width / height | Scene dimensions in pixels |
| preset | Optional shorthand: card (640×360), slide (1280×720), or square (600×600) |
| fps | Frames per second for timelines |
| background | Background color or semantic token |
| children | Ordered array of top-level element IDs |
Presentation and slide-deck composition is host-level React composition with @elucim/core components such as <Presentation> and <Slide>, not a DSL root shape.
elements
Elements are keyed by stable ID. Each element includes its id, type, optional layout/metadata, and a props object for render properties.
Supported primitives include circle, line, arrow, rect, polygon, bezierCurve, text, image, group, and barChart.
Supported math/data elements include axes, functionPlot, vector, vectorField, matrix, graph, and latex.
timelines
Timelines contain explicit property tracks and keyframes. Use timelines instead of wrapper animation nodes.
{
"intro": {
"id": "intro",
"duration": 45,
"tracks": [
{
"target": "title",
"property": "opacity",
"keyframes": [
{ "frame": 0, "value": 0 },
{ "frame": 45, "value": 1 }
]
}
]
}
}Common animated properties include opacity, translate, scale, rotate, fill, and stroke.
stateMachines
State machines connect timelines into interactive flows. Use them for entry animation, click/key-driven transitions, reset behavior, or auto-advancing states.
{
"main": {
"id": "main",
"entry": "intro",
"states": {
"intro": { "timeline": "intro" },
"focus": { "timeline": "focus" }
},
"transitions": [
{ "id": "entry-start", "from": "entry", "to": "intro", "trigger": "onStart" },
{ "id": "intro-next", "from": "intro", "to": "focus", "exitTime": 1 }
]
}
}Semantic Color Tokens
Use $token syntax in color fields to create theme-adaptive visualizations. Tokens resolve to CSS custom properties at render time.
{
"version": "2.0",
"scene": { "type": "player", "preset": "card", "background": "$background", "children": ["label"] },
"elements": {
"label": {
"id": "label",
"type": "text",
"props": { "type": "text", "x": 320, "y": 180, "content": "Hello", "fill": "$foreground", "textAnchor": "middle" }
}
}
}Available tokens include $foreground, $background, $title, $subtitle, $muted, $primary, $secondary, $tertiary, $success, $warning, $error, $surface, $border, and $accent.
Pair tokens with colorScheme or theme:
<DslRenderer dsl={doc} colorScheme="auto" theme={{ accent: '#ff6600' }} />For AI Agents
When instructing an LLM to create Elucim diagrams:
- Ask it to produce JSON matching the normalized
ElucimDocumentschema. - Set
version: "2.0"and include exactly onesceneplus an ID-keyedelementsobject. - Use stable, semantic element IDs (
title,axis-x,curve-sine) so later edits can target specific objects. - Put motion in
timelineswith explicit tracks/keyframes; do not generate wrapper animation nodes. - Use
stateMachinesfor entry, click/key, reset, and auto-advance behavior. - Use math expression strings for function plots and vector fields.
- Use semantic color tokens (
$accent,$foreground,$background) when the host theme should control colors.
Useful APIs for agent and host workflows include createDocument, applyAgentCommands, addRevealTimeline, createStateMachine, evaluateSceneForAgent, validateForAgent, summarizeDocument, diffDocuments, and suggestDocumentNudges from @elucim/dsl/agent, plus low-level preview helpers such as applyTimelineFrame and transitionStateMachine from @elucim/dsl.
Tips:
- Use
posteron<DslRenderer>to show a static preview frame before playback starts. - Use
renderToSvgString(doc, frame)to generate SVG previews server-side for thumbnails and social cards.
Example prompt:
"Create an Elucim DSL JSON document with
version: \"2.0\", a single player scene, stable element IDs for axes and two function plots, an intro timeline that draws the sine curve before the cosine curve, and a state machine that starts the intro on load."
Related
- @elucim/core — React rendering engine and component APIs
- @elucim/editor — Visual canvas editor for normalized Elucim documents
- Elucim Docs — Full docs with live interactive examples
