@pixodesk/svg-animator-core
v1.0.43
Published
Pixodesk SVG animator core — schema, document types, materializers and sampling shared by all platform players
Downloads
1,210
Readme
animator-core
📖 Full user guide: docs/format/core-library.md · all docs
Platform-neutral core of the Pixodesk SVG animator: the document schema, the effect materializers, the interpolation engine and the path sampler — with no DOM dependency at all. It is what every player shares, so the web player and the React Native player produce identical values from the same document.
🚧 Status - This project is currently under development.
Do I need this package?
Usually no. If you just want to play an animation, install a player:
| You are building for | Install |
|---|---|
| Browser (vanilla JS) | @pixodesk/svg-animator-web |
| React | @pixodesk/svg-animator-react |
| Vue | @pixodesk/svg-animator-vue |
| React Native | @pixodesk/svg-animator-rn 🧪 |
Each of those depends on this package and re-exports what you need.
Install it directly when you want to work with documents rather than play them — validating them, transforming them, flattening them for a renderer of your own, or computing values at a given time without rendering anything.
npm install @pixodesk/svg-animator-coreWhy it exists
A player has to answer two very different questions:
- What should be on screen at time t? — schema, effects, easing, interpolation, path sampling. Pure computation, identical on every platform.
- How do I put it there? — DOM elements, WAAPI,
react-native-svg. Platform-specific.
Everything in category 1 lives here. The package compiles without the TypeScript
dom library, so a stray document reference is a build error rather than a
runtime crash on a non-browser platform.
What's inside
| Area | Exports |
|---|---|
| Schema & types | PxAnimatedSvgDocumentSchema, PxNodeSchema, PxEffectsSchema, … plus every Px* TypeScript type and the px schema builder |
| Validation | validateDocument (the whole document, strict), isPxDocument, isValidPxDocument, validateNodeEffects |
| Materializers | materializeAllInTree, materializeNodeEffects |
| Interpolation | calcAnimationValues, interpolateValue, normalizeBindings |
| Sampling / geometry | createPathSampler, bezier helpers, cubicBezier, splitEasing |
| Text | materializeGlyphText, layoutGlyphTextChars, extendedPathForBrowser |
| Node helpers | toDomProps, sanitizeAttributeValue, generateNewIds |
| Playback engine | createAdapterAnimator + the PxPlatformAdapter interface |
| Wire enums | PxTimelineEngine / PxTimelineEngineSetting, PxStartOn, PxOutAction, PxFinishAction, PxFillMode, PxPlaybackDirection, PxScrollKind, PxScrollAxis, PxScrollSource, PxScrollPhase, PxPinAlign, PxAlongPathMode, PxLoopRepeatAt, PxLoopDirection, PxStrokeTrimSubPaths, PxMaskType, PxCloneWithout, PxUnits, PxGradientType, PxGradientSpreadMethod, PxPathOverflow, PxLengthAdjust, PxTextPathMethod, PxTextPathSpacing — every two-or-more-way wire selector is a named enum, not a bare string. Each is a const namespace AND the string type derived from it under the same name, so PxStartOn.click and startOn?: PxStartOn come from one import |
Validating a document
isPxDocument(json) is the cheap shallow gate (is this a Px document at all?);
isValidPxDocument(json) runs the full schema. For per-field diagnostics, call a schema's
isValid with a context:
import { PxAnimatedSvgDocumentSchema, type PxValidationContext } from '@pixodesk/svg-animator-core';
const ctx: PxValidationContext = { errors: [], warnings: [], strict: true };
const ok = PxAnimatedSvgDocumentSchema.isValid(doc, ctx, []);
if (!ok) console.error(ctx.errors); // ["children[0].effects.strokeTrim.range: …", …]Two modes, two different questions:
| mode | question it answers | undeclared keys |
|---|---|---|
| default (strict absent/false) | is this document repairable? — what sanitize would accept | ignored, so unknown future fields stay forward-compatible |
| strict: true | is this document well-formed? — the wire shape locked to its schema | reported as errors on closed objects |
Use default in production readers and strict in tests and tooling. Two notes on strict, both
deliberate: it reaches inside unions (a union member is checked in the caller's mode, though its
per-branch errors are not reported unless every branch fails), and it ignores keys whose value is
undefined — those cannot survive JSON.stringify, so strict judges the document rather than the
in-memory object that produced it.
The schema the format is written in
Nothing above is validated by hand. Every block of the format has a runtime schema built with the
px schema builder, and validateDocument walks those. You can walk them too: describeSchema
turns any schema into a plain description — keys, types, and whether each is optional — which is
how this repo generates its published SCHEMA.json, and schemaKeys lists just the keys.
There is one schema value per block, named after it: PxTriggerSchema, PxElementAnimationSchema,
PxKeyframeValueSchema, PxAttrValueSchema, PxTransformValueSchema, PxBezierPathSchema,
PxScrollSchema, PxScrollRangeSchema, PxScrollRangePointSchema, PxRetimeEffectSchema and
PxGradientStopSchema among them. Two hold a node's shared halves rather than a block of their
own — PxNodeBaseSchema is what every node has, PxSvgNodeRootSchema what only the root <svg> adds.
To type a schema, or build one of your own: PxSchema is the schema type itself, PxSchemaDesc
what describeSchema hands back, PxInfer the document type a schema describes, and PxRemoveIndex
strips the index signature that SVG pass-through keys bring with them.
Reading and reshaping a document
diagnoseDocument is the load-time check every player runs before it builds anything: it hands
back the problems it found instead of throwing, and validateDocument above is the fuller form of
the same question.
A document's timeline has two shapes — the nested object the file stores, and the flat view the
engines read. flattenAnimatorTimeline and nestAnimatorTimeline convert between them, so an
editor can hold one and a player the other with neither having to guess.
PX_TRANSFORM_PART_KEYS lists the parts a transform is written in, in the order they compose.
The materialization pipeline
materializeAllInTree(doc, engine) is the single entry point that turns a
lightweight editor document into a flat tree any renderer can walk:
- Effects —
node.effects(transformBy, repeater, maskedBy, strokeTrim, clone/retime, gradients, textPath) become real nodes, wrappers and defs. - Loops — each property's
loopis expanded into explicit keyframes. - Motion paths — tangented
transformkeyframes plusautoOrientare sampled into plain{translate, rotate}keyframes. - Animated
<use>— replaced by<g>+ a deep clone with fresh ids.
Steps 3 and 4 run when engine is native. Pass native for any renderer
without live <use> propagation — that includes react-native-svg — and
js only for the DOM, which resolves <use> references natively.
import {
materializeAllInTree, generateNewIds, calcAnimationValues,
normalizeBindings, PxTimelineEngine,
} from '@pixodesk/svg-animator-core';
// Flatten once …
const flat = generateNewIds(materializeAllInTree(doc, PxTimelineEngine.native));
// … then ask for values at any time, with no renderer involved.
for (const binding of normalizeBindings(flat, PxTimelineEngine.js) ?? []) {
const values = calcAnimationValues(binding.animate, 500); // t = 500 ms
console.log(binding.id, values); // → { opacity: '0.5', transform: 'translate(…)' }
}This is exactly how the React Native player precomputes its animation tracks, and how the frame-loop engine renders each tick in the browser — same function, same numbers.
Writing your own player
Implement PxPlatformAdapter and hand it to createAdapterAnimator; the
engine handles timing, delay, direction, iterations, fill, playback rate and the
lifecycle callbacks, then calls you with plain attribute writes.
import { createAdapterAnimator, type PxPlatformAdapter } from '@pixodesk/svg-animator-core';
const adapter: PxPlatformAdapter = {
isConnected: () => true,
setAttribute: (id, attrName, value) => { /* apply to your element */ },
};
const api = createAdapterAnimator(flatDoc, adapter, {
onFinish: () => console.log('done'),
});
api.play();Frame scheduling resolves requestAnimationFrame from globalThis at call time
and falls back to setTimeout, so the engine works in browsers, React Native and
test environments with faked timers.
Every player agrees on one meaning of time, and the helpers that define it are exported so a player
of your own cannot drift from it. seekCeilingMs is the highest time you can seek to and
progressSpanMs the span a 0–1 progress maps onto; clampSeekMs holds a seek inside that span,
and timeToProgress / progressToTimeMs convert between the two. isValidPlaybackRate says
whether a rate can be used, and PX_RATE_REJECTED is what a setter reports when it cannot.
createRunClock is the clock the frame loop itself runs on. The callbacks that engine accepts are
PxEngineCallbacks — the playback lifecycle plus the diagnostics channel, and the shape each
player's own callback type is built on.
Versioning
Every package in this repo is released in lockstep. A player depends on the
matching core version (^x.y.z), so upgrading a player upgrades the core with it.
A document carries its own version, which moves independently of the package's: animator.version,
stored under the key PX_WIRE_VERSION_KEY and parsed into a PxWireVersion. PX_WIRE_VERSION is
the version this build writes and PX_WIRE_BASELINE_VERSION the oldest it still reads;
PX_WIRE_STEPS is the ordered list of conversions between them, each a PxWireVersionStep of
some PxWireStepKind. convertWireDocument brings a document up to this build and reports what it
did in a PxWireConversionResult. applyWireSteps runs a chosen subset of those steps instead,
taking a PxWireConversionOptions — which is what the release tooling uses.
