Skip to content

Core Concepts

Elucim animations are frame-based, not time-based. Every animation is defined in terms of frames:

frame 0frame 30frame 60frame 90circle fades intext appearsboth visible

Convert between frames and seconds:

  • frames = seconds × fps → 2 seconds at 30fps = 60 frames
  • seconds = frames / fps → 90 frames at 30fps = 3 seconds

Elucim uses SVG coordinates:

  • Origin (0, 0) is the top-left corner
  • X increases rightward
  • Y increases downward
(0, 0)(width, 0)(0, height)(width, height)XY(cx, cy) = center

For math primitives like <Axes>, you specify an origin point that becomes the mathematical (0,0), and a scale factor that maps math units to pixels.

Normalized Elucim documents have three layers of composition:

The visual elements: circle, line, bezierCurve, rect, text, polygon, axes, functionPlot, vector, vectorField, matrix, graph, latex, barChart.

Elements are stored by stable ID in elements. Layout metadata such as rotation, scale, and translate lives in each element’s layout object, while render-specific settings live in props. Sibling order in scene.children and group children arrays controls stacking.

Timelines animate element properties with explicit tracks and keyframes. Use timelines instead of React wrapper components for JSON/YAML documents.

State machines choose which timeline is active and how playback moves through Entry, states, transitions, and Exit. They can start automatically, wait for clicks/keys, reset, or auto-advance when a timeline completes.

import { DslRenderer, type ElucimDocument } from '@elucim/dsl';
const doc: ElucimDocument = {
version: '2.0',
scene: { type: 'player', width: 800, height: 600, children: ['axes', 'curve', 'label'] },
elements: {
axes: { id: 'axes', type: 'axes', props: { type: 'axes', origin: [400, 300], xRange: [-5, 5], yRange: [-3, 3], scale: 50 } },
curve: { id: 'curve', type: 'functionPlot', props: { type: 'functionPlot', expression: 'sin(x)', domain: [-5, 5], origin: [400, 300], scale: 50, stroke: '$accent', opacity: 0 } },
label: { id: 'label', type: 'latex', props: { type: 'latex', expression: 'f(x) = \\sin(x)', x: 600, y: 100, fontSize: 20, opacity: 0 } },
},
timelines: {
intro: {
id: 'intro',
duration: 65,
tracks: [
{ target: 'curve', property: 'opacity', keyframes: [{ frame: 20, value: 0 }, { frame: 40, value: 1 }] },
{ target: 'label', property: 'opacity', keyframes: [{ frame: 50, value: 0 }, { frame: 65, value: 1 }] },
],
},
},
defaultStateMachine: 'main',
stateMachines: {
main: {
id: 'main',
entry: 'intro',
states: { intro: { timeline: 'intro' } },
transitions: [{ id: 'entry-start', from: 'entry', to: 'intro', trigger: 'onStart' }],
},
},
};
export function SineScene() {
return <DslRenderer dsl={doc} />;
}

If you are hand-authoring React-only components, two core hooks let you create custom animated components:

  • useCurrentFrame() — Returns the current frame number for low-level React playback
  • interpolate(frame, inputRange, outputRange, options?) — Maps a frame to a value with easing
function PulsingDot() {
const frame = useCurrentFrame();
const scale = interpolate(frame, [0, 30, 60], [1, 1.5, 1]);
const opacity = interpolate(frame, [0, 15], [0, 1]);
return (
<circle cx={250} cy={250} r={20 * scale}
fill="#6c5ce7" opacity={opacity} />
);
}