Playback settings & triggers
Change how an animation plays — its length, loops, direction, what starts and stops it, what the
end leaves behind — without going back to the editor. Everything about when and how it plays
lives in one place, the document’s animator.timeline block, and every player lets you
override it at runtime from component props or the player options. This page is the
reference for those fields and the overrides.
The editor writes the same block from its playback panels; if you only want to set the defaults there, see Set default playback settings & triggers.
A document with its animator block. (The comments are explanatory; JSON does not allow
comments, so a real file has none.)
{ "type": "svg", "viewBox": "0 0 400 400",
// Everything about WHEN and HOW the animation plays lives here "animator": { "timeline": { "outTime": 2000, "iterations": "infinite", "iterationDirection": "alternate", // Plays as soon as half of it is on screen; pauses while it is scrolled away "actions": { "outOfView": "pause" }, "inViewThreshold": 0.5, "actionTriggers": { "start": "load" } } }, "children": [ { "type": "circle", "id": "ball", "cx": 0, "cy": 0, "r": 40, "fill": "#0087ff", "animate": { "transform": { "keyframes": [ { "time": 0, "value": { "translate": [200, 60] }, "easing": [0.33, 0, 0.67, 0.33] }, { "time": 2000, "value": { "translate": [200, 340] } } ] } } } ]}The same bouncing ball as in the web player, now two seconds per bounce and waiting until half of it has scrolled into view.
Two blocks — requests and moments
Section titled “Two blocks — requests and moments”Three things make an animation go: the triggers (the user hovers or clicks), a component’s
play / pause props (your app’s state), and the player API (play(), pause(),
stop() from your code). All three call the same built-in actions, so the format writes down
how each action looks exactly once, in the timeline’s actions block, and all three honour
it. The two blocks are not mirror images of each other:
actionTriggerskeys are the requests a page event can make —start,stop,pause(resumeis derived unless the file names it).actionskeys are the moments whose outcome is configurable —start,stop,finish,outOfView.startandstopare in both because each is both a request and a moment;pausehas one behaviour (freeze), so there is noactions.pause;finishandoutOfVieware not page events, so they have no list.
| action | does | how it looks (actions) | requested by (actionTriggers) |
|---|---|---|---|
| start | stopped or finished → playing | start.behavior: continue · restart · jumpToFinished | start |
| stop | playing or paused → stopped | stop.behavior: reset · rewind, stop.completeIteration | stop |
| pause / resume | freeze, and carry on | nothing to configure | pause; resume — derived unless the file names it |
| finish | the run ends by itself | finish.behavior: hold · reset · rewind | — (the player) |
| outOfView / wake | nobody can see it / seen again | outOfView.behavior: pause · continue · reset | — (the view sensor: a condition, not an event) |
So player.stop() on a looping heartbeat with actions: { stop: { completeIteration: true } }
finishes the beat and then rests — and so does a hoverOut listed in actionTriggers.stop, and so does
play={false} on the React component. One vocabulary, learnt once.
The timeline — what advances the playhead
Section titled “The timeline — what advances the playhead”animator.timeline says what drives the animation’s progress, exactly like a Web Animations
API timeline. Its type picks the kind, mirroring WAAPI’s DocumentTimeline /
ScrollTimeline / ViewTimeline:
timeline.type | The playhead follows… |
|---|---|
time (default — may be omitted) | wall time — something starts it (the actionTriggers), and it has the playback dynamics below |
scroll | a scroll container’s offset — Scroll-driven playback |
view | the SVG’s journey through the viewport — Scroll-driven playback |
Each type carries only the fields that mean something for it — a scrubbed timeline has no
actionTriggers, no actions and no delay, and the format gives them no slot there. Omitting
timeline entirely means a time-driven timeline with every default.
Timing
Section titled “Timing”Timing, the playback dynamics, how the animated attributes get updated and at what rate ALL live
in the timeline. animator itself keeps only what is not playback: the lookup tables
(definitions, bindings).
| Field | Values | Default | Meaning |
|---|---|---|---|
timeline.inTime | ms | 0 | where the content WINDOW begins. Keyframe times are absolute content ms |
timeline.outTime | ms | max keyframe time | …and where it ends — one pass plays inTime → outTime. The editor always writes it |
timeline.playDuration | ms | outTime − inTime | the window remapped to WALL time: one pass takes this long. Set it to play the same content faster or slower |
timeline.frameRate | fps | uncapped | target rate for the player’s frame loop only — a parameter of the engine timeline.engine selects, so it sits beside it |
timeline.engine | auto · native · js | auto | how the animated attributes get updated — Engine |
timeline.delay | ms | 0 | waited whenever a run starts from the beginning — the first play, a restart, a play after a reset or an end — never on a resume from a paused position. A negative value skips ahead instead: -500 starts right away from the frame at 0.5 s |
timeline.iterations | number · "infinite" | 1 | how many times the whole document timeline repeats |
timeline.iterationDirection | normal · reverse · alternate · alternateReverse | normal | each iteration’s direction; alternate ping-pongs on every other iteration. reverse applies to a single pass too — still one iteration |
Per-property loops vs iterations. There are two kinds of repetition, and they work at
different levels. iterations repeats the whole document — every element, from the first
keyframe to the last. A single property can also loop on its own: a segment of its own
keyframes repeats until it fills the document’s duration, while everything else plays through
once (see JSON format → Per-property loops). The property loop is
applied first, when the document is prepared; iterations then repeats the result. So both
can be used at once, and one runs inside the other: a wheel whose rotation loops, inside a
document set to infinite iterations, keeps spinning during every iteration.
Time axes
Section titled “Time axes”Three time axes appear in these names — all in ms, as everywhere on this API:
| Axis | Names |
|---|---|
| content ms — one pass of the content window | inTime, outTime, the handle’s getDuration() (× iterations for the whole run) |
| wall ms of one pass | playDuration |
| ms into the whole run, every iteration included | delay, the components’ showAtTime, the handle’s getCurrentTime() / setCurrentTime() |
Triggers — WHEN it plays
Section titled “Triggers — WHEN it plays”The actionTriggers block — inside the time timeline — names the user interactions that call the
built-in actions. One list per request — start, stop, pause, and resume when the file
names it — over one open vocabulary of events:
"timeline": { "actions": { "stop": { "completeIteration": true } }, "actionTriggers": { "start": ["hoverIn", "click"], "stop": "hoverOut", "pause": "click" }}Read cold: start on hover-in or click, stop (after the beat) on hover-out, a click while playing
pauses and the next click carries on. Each list is a single event or an array of them — the house
style of animate’s references — and the editor writes the shortest exact spelling.
| List | Calls | Acts while the player is… | Editor label |
|---|---|---|---|
start | start, per actions.start | stopped (or finished) | Play on |
stop | stop, per actions.stop | playing or paused | Stop on |
pause | pause — freezes the current frame | playing | Pause on |
resume | resume — carries on from there. Derived unless the file names it (below) | paused | Resume on (shown, not edited) |
Two clicks make two toggles — spelt with the lists themselves, no extra key:
| Click toggles | actionTriggers | A click while… |
|---|---|---|
| start / stop | { "start": "click", "stop": "click" } | stopped starts it; playing or paused stops it |
| play / pause | { "start": "click", "pause": "click" } | stopped starts it; playing pauses it; paused resumes it |
| Event | Fires when… |
|---|---|
load | the document is ready — the only event the visibility gate may hold back: nobody interacted, so it plays once enough of it is on screen |
hoverIn | the pointer enters the element |
hoverOut | the pointer leaves it — only after it entered (a pointer already over the element at load does not count) |
click | the element is clicked (a tap on React Native) |
The vocabulary is open: a later interaction (keyboard, focus, an app event) is a new word in the same lists, never a new key.
Which events each list may name
Section titled “Which events each list may name”Each list names only the events that make sense for it; any other event is ignored, with a warning (PX1110).
| List | load | hoverIn | hoverOut | click | Why |
|---|---|---|---|---|---|
start | ✓ | ✓ | — | ✓ | starting when the pointer leaves makes no sense |
stop | — | — | ✓ | ✓ | stopping on load, or when the pointer arrives, makes no sense |
pause | — | ✓ | ✓ | ✓ | a marquee pauses on hover-in, a hover animation on hover-out |
resume | — | ✓ | ✓ | ✓ | derived unless the file names it — see below |
What resumes a pause — derived unless named
Section titled “What resumes a pause — derived unless named”resume need not be written: when the file does not name it, the player derives it from start,
stop and pause. A paused animation then resumes on
- the counterpart of each
pauseevent —hoverIn↔hoverOut,click→click(loadhas none), and - the
startevents that do not also stop it — so a click that starts and stops ("start": "click", "stop": "click") stops a paused animation, as the lists say.
actionTriggers | Resume on | Plays as… |
|---|---|---|
{ "start": "load", "pause": "hoverIn" } | hoverOut (and load, which has fired by then) | a marquee: runs on load, holds while the pointer is over it, carries on when it leaves |
{ "start": "hoverIn", "pause": "hoverOut" } | hoverIn | a hover animation that holds where it is when the pointer leaves |
{ "start": "click", "pause": "click" } | click | the play / pause click toggle: start → pause → resume → pause … |
{ "start": "click", "stop": "click" } | — | the start / stop click toggle: one click starts it, the next stops it — also while paused |
A resume list the file does name is honoured as written. Where it differs from the
derivation — it names an event the derivation would not, or leaves out one it would (that pause
then waits for start or stop) — the player notes it
(PX1111), so a surprising resume is no mystery.
One event, several lists
Section titled “One event, several lists”An event may be listed under several lists. The player’s state picks the list it is read
from — start when stopped, pause when playing, the derived resume when paused, stop when
playing or paused — and every action checks the state as it was when the event arrived.
{ "start": "click", "pause": "click" }: a click while stopped starts, while playing pauses, while paused resumes.- Add
actions: { start: 'restart' }and a click while stopped restarts, while a click while paused still resumes.
Two lists that react to the same event in the same state are a collision: the players report it (PX1109) and one of them wins.
actionTriggers | Collision? | What happens |
|---|---|---|
{ "stop": "click", "pause": "click" } | yes — stop with pause | while playing, pause wins |
{ "start": "load", "pause": "hoverIn", "stop": "hoverOut" } | yes — stop with the counterpart of a pause event | while paused, hoverOut resumes rather than stops |
{ "start": "click", "pause": "click" } | no | the normal play / pause toggle — each list acts in another state |
{ "start": "click", "stop": "click" } | no | the start / stop click toggle — each list acts in another state |
{ "start": "hoverIn", "stop": "hoverOut" } | no | start on hover-in, stop on hover-out |
Every warning the players give about a triggers block, by kind:
| Kind | Example | The player… | Code |
|---|---|---|---|
eventNotAllowed | "stop": "load" | ignores the event (in a file, the document repair drops it from the list first — PX1108) | PX1110 |
collision | "stop": "click", "pause": "click" | lets pause win while playing, the resume while paused | PX1109 |
resumeExtra | "start": "load", "pause": "hoverIn", "resume": ["hoverOut", "click"] | honours the list as written; click is the event the derivation would not name | PX1111 |
resumeMissing | "start": "load", "pause": ["hoverIn", "click"], "resume": "hoverOut" | honours the list as written; a pause by click then waits for start or stop | PX1111 |
Absent, or empty
Section titled “Absent, or empty”A document with no actionTriggers block starts on load — a document is designed to play. An
empty block, "actionTriggers": {}, lists nothing: nothing starts it until your code calls
play(). A list that is absent is simply not listed.
Pause and carry on need no setting. “Pause when the pointer leaves” is hoverOut listed under
pause (hover-in resumes it — derived); “keep playing” is hoverOut not listed at all. That is
what the editor does for you: ticking Hover in under Play on adds Hover out under Pause
on, editable.
Actions — how start, stop and finish look
Section titled “Actions — how start, stop and finish look”timeline.actions gives the built-in actions their settings — one slot per moment. behavior names a
preset: one word for where the move ends and the path it takes there (an instant jump, playing
on, playing back) — and a slot that says only behavior is written as the word itself: "stop": "rewind"
is exactly "stop": { "behavior": "rewind" }. Every setting has a stated default, so a document that says
nothing plays the way it always did: a start continues, a stop resets, the end holds. A key here names
a moment; start and stop are requests too, so they appear in actionTriggers as well
(Two blocks).
| Field | Values | Default |
|---|---|---|
start.behavior | continue · restart · jumpToFinished | continue |
stop.behavior | reset · rewind | reset |
stop.completeIteration | true · false | false |
finish.behavior | hold · reset · rewind | hold |
outOfView.behavior | pause · continue · reset | pause — the visibility gate |
pause, resume and wake have nothing to configure.
actions.start.behavior | A start (a start event, play={true}, play())… |
|---|---|
continue (default) | continues from the current position. At the start or the end, that is a fresh run from the beginning |
restart | starts from the beginning every time — a splash that must replay on every click |
jumpToFinished | lands in finished: the end pose with no motion — overlay art that appears instantly and retreats animated |
actions.stop.behavior | A stop (a stop event, play={false}, stop())… |
|---|---|
reset (default) | jumps back to the state as before it started |
rewind | plays BACK to it — the VCR word, never confused with iterationDirection: reverse. The trash-can lid: opens on hover-in, closes animated on hover-out |
actions.stop.completeIteration | WHEN the stop takes effect |
|---|---|
false (default) | now |
true | after the current iteration — a heartbeat should not die mid-beat; a one-shot runs to its end first. Orthogonal to behavior: rewind after completion plays back from there — finish the motion, then undo it smoothly |
actions.finish.behavior | When it ends by itself (every iteration played)… |
|---|---|
hold (default) | keeps the last frame showing |
reset | jumps back to the start state |
rewind | plays back to the start state |
reset means ONE thing wherever it appears (the stop, finish and out-of-view presets): the state
as before it started — the SVG as authored. A hand-authored file whose static attributes differ
from its first keyframe can ask for the first frame instead with timeline.showWhenStopped,
which governs the whole stopped state — before the first start, after reset(), after a stop or
finish whose behavior is reset, and where a rewind lands (a paused animation shows its paused
frame, a finished one its held frame):
timeline.showWhenStopped | What is shown while stopped |
|---|---|
static (default) | the SVG as authored |
firstFrame | the first frame’s values. The editor never writes it: in its files the two are the same picture |
A one-shot document driven by a component’s play prop reads these settings too:
play={true} is a start (the default continue plays to the end and holds there, per finish
hold), play={false} a stop (reset snaps back, rewind plays back). The trash-can lid that
closes animated is actions={{ stop: 'rewind' }}; overlay art that appears at once
and retreats animated is actions={{ start: 'jumpToFinished', stop: 'rewind' }}.
Whether it may run — the visibility gate
Section titled “Whether it may run — the visibility gate”An animation nobody can see does not play, whatever started it. The gate is a condition on
every driver — the triggers, a component’s play prop and the API alike — not an event: once
none of it is on screen the player performs the built-in outOfView moment, and once it is seen
again wake, which carries on whatever was meant to run. outOfView has one setting; the two
numbers that say when the sensor counts it as seen are plain timeline keys:
actions.outOfView.behavior | Effect while none of it is on screen |
|---|---|
pause (default) | pause where it is; it resumes when the element comes back |
continue | keep playing — and start without waiting to be seen at all |
reset | jump back to the start, so the next entry replays it from the beginning |
continue reads differently in the two slots that take it: under actions.start it means “from
where it rests”, here “keep running”.
| Field | Default | Meaning |
|---|---|---|
timeline.inViewThreshold | 0.5 | how much of the element must be on screen before it may run: 0 any part, 0.5 half of it, 1 all of it. Playback stops only at zero visibility, so an element resting on the boundary cannot flicker |
timeline.inViewDebounce | 150 | and for how long, in ms. Scrolling straight past an animation therefore starts nothing; 0 starts the moment the threshold is met |
wake needs no setting: if it was meant to run, it runs again — from the paused position after
pause, from the start after reset; continue never stopped. A hidden browser tab counts as
out of view. An element taller than the screen counts as fully visible once it fills the screen,
so a threshold of 0.5 or 1 can always be met. Where nothing can measure visibility — a server
render, a test environment without IntersectionObserver — the animation plays as if fully
visible, rather than silently never playing.
To play an animation when it scrolls into view, nothing has to be set: start: load behind
the default gate IS that behaviour — it waits on its first frame until half of it is on screen
for 150 ms, plays, pauses when scrolled away, and carries on when it comes back.
Where triggers work
Section titled “Where triggers work”- Every player — web, React, Vue and React Native — honours the lists, the actions and the
gate, with one exception: React Native has no
hoverIn/hoverOut, because there is no hover on a touch screen. - Pre-rendered SVG + CSS animation + JS triggers honours all of them too. The editor writes a few lines of script into the file for this; no library is involved.
- Pre-rendered SVG + CSS animation (no script at all) listens for
loadand, through CSS:hover, forhoverInandhoverOut—hoverOutmay pause or stop, not both.clickand the visibility gate cannot be done in pure CSS and are dropped from the file; the actions keep their defaults (a class cannot restart keyframes or play them backwards). The editor marks every choice (CSS) or (JS) so you see what the script-free flavour keeps. See Pre-rendered SVG. - The CSS components (
SvgCssAnimationPlayerin React and Vue) take the sameactionTriggersobject,actionslimited tooutOfView, and the two gate numbers as props, and read them with the same rules;rewind,restart,jumpToFinishedand the iteration completion act as the defaults there, for the same reason.
Overriding from a player
Section titled “Overriding from a player”Example:
playback/override-web—pnpm example:docs, then open#playback/override-web. Example:playback/override-react—pnpm example:docs, then open#playback/override-react.
One document can play differently in each place you use it — the same file shown twice at two speeds, or a file that starts by itself everywhere except inside your own play/pause UI. Every player takes the same three overrides, and the file on disk is never modified:
| prop / option | what it carries | |
|---|---|---|
| the timing | timelineOptions | corrections to the file’s timeline block, minus its actionTriggers and actions — iterations, delay, playDuration, the gate numbers — as an object shaped like the file; whoever needs more edits the document |
| the actions | actions | corrections to the file’s timeline.actions, setting by setting — { stop: 'rewind' } changes how a stop looks and leaves the rest as the file has it |
| the triggers | playUsingActionTriggers | true plays on the file’s own triggers; an object is the whole actionTriggers block to use instead. Lists left out are simply not listed, never the file’s values |
Merge or replace — the one rule for the three:
timelineOptions, actions | patches — objects merge key by key, values replace, null deletes |
a playUsingActionTriggers object | replaces the file’s actionTriggers as a whole — its lists belong together, so switching hover → click would otherwise carry the hover-era lists along |
The prop is timelineOptions, not timeline: it is not the file’s timeline object — two of its
keys live on other props — and a prop that shares a file key’s name would promise the same shape.
actions keeps the file’s name because it is a partial of that block.
Web player
<div id="box" style="width: 300px; height: 300px"></div>import { createSvgAnimationPlayer } from '@pixodesk/svg-animator-web';
const a = createSvgAnimationPlayer({ src: '/bouncing-ball.json', container: '#box', timelineOptions: { iterations: 'infinite' }, actions: { stop: { completeIteration: true } }, // no `playUsingActionTriggers`: nothing starts until a.play()});a.play();a.stop(); // finishes the current iteration, then rests — the actions said soReact / Vue / React Native — the same objects, as props:
// the file's triggers, looping forever<SvgAnimationPlayer doc={doc} playUsingActionTriggers timelineOptions={{ iterations: 'infinite' }} />
// these triggers instead of the file's: starts on hover-in, plays back on hover-out<SvgAnimationPlayer doc={doc} playUsingActionTriggers={{ start: 'hoverIn', stop: 'hoverOut' }} actions={{ stop: 'rewind' }} />
// one click starts it, the next stops it<SvgAnimationPlayer doc={doc} playUsingActionTriggers={{ start: 'click', stop: 'click' }} />
// driven by your state — the same actions, the same settings<SvgAnimationPlayer doc={spinner} play={busy} actions={{ stop: { completeIteration: true } }} />How the merge works
Section titled “How the merge works”| Objects merge, key by key | timelineOptions: { outTime: 2000 } changes the window end and leaves iterations and everything else as the file has them; actions: { stop: 'rewind' } leaves stop.completeIteration and the other actions alone |
| Values replace | numbers, strings and arrays are taken as given, never combined |
null deletes | { timelineOptions: { delay: null } } removes the file’s delay, restoring what its absence means; { actions: { stop: null } } drops the file’s stop settings. This is the only way to get a default back, because there is no value that spells “unset” |
actionTriggers and actions are not timeline keys here | the file nests them, the props lift them: only playUsingActionTriggers carries triggers and only actions the action settings, so every key is reachable exactly one way — timelineOptions={{ actionTriggers: … }} is a type error |
Changing timeline.type starts over | switching between a clock and a scroll timeline keeps only inTime, outTime, iterations, engine and frameRate — the keys both kinds share. Clock-only keys (actionTriggers, actions, showWhenStopped, delay, playDuration, iterationDirection, the gate numbers) have no meaning on a scroll timeline and are dropped, with a console warning |
Every player uses the same code from core. applyPlaybackOverride(doc, { timelineOptions, actions, playUsingActionTriggers })
is the one implementation the web player and the three components call: the timing and the
actions merged (mergeAnimatorConfig’s rules, above), then — when playUsingActionTriggers is an object —
the actionTriggers block set to it as a whole. Its input is a PxPlaybackOverrideInput (the timelineOptions
and actions of PxPlaybackOverride plus playUsingActionTriggers), its answer a PxPlaybackOverrideResult: the new
document, sharing every untouched subtree, and the merge’s warnings. applyAnimatorConfig(doc, patch)
applies a patch to a whole document; mergeAnimatorConfig does the same one level down, on the
animator block alone, and reports what it could and could not do in a PxAnimatorConfigMergeResult.
A patch is a PxPatch of the wire type — every key optional at every depth, null deletes,
arrays and primitives replace — so PxAnimatorConfigPatch patches the animator block,
PxTimelinePatch the timing, typed from PxTimelineTiming (the timeline minus actionTriggers and
actions), and PxActionsPatch the actions block (PxActions). withTriggers(doc, triggers)
is the SET a PxActionTriggers block needs (an empty block and a replacement that drops keys are things
a merge cannot say).
A note on the components’ control props
Section titled “A note on the components’ control props”When you control a component with play, pause, showAtProgress or showAtTime,
the component ignores the triggers saved in the file: core’s withoutTriggerDriver replaces them
with an empty actionTriggers block, so nothing starts the animation by itself — the same patch the
web player applies when playUsingActionTriggers is absent. Only playUsingActionTriggers mode follows the file’s
triggers. The play / pause props call the built-in actions, so the file’s actions (and the
actions prop) still say how each move looks. playerRef is not a control prop: the handle is
filled in every mode and never changes which one you are in.
React, Vue and React Native decide this with the same core function, so they always agree.
resolveControlMode looks at the control props you passed (PxControlProps) and returns the
mode that wins (PxControlMode), plus a ready-made warning if you passed props from more than
one mode. controlModeTakesOverTrigger then says whether that mode ignores the file’s triggers —
true for every mode except playUsingActionTriggers.
For player authors — the same rules, exported
Section titled “For player authors — the same rules, exported”Everything above is decided by functions in @pixodesk/svg-animator-core, so a player you
write yourself cannot drift from the four shipped ones:
- Defaults.
PX_PLAYBACK_DEFAULTSis the one table of what an absent key means (start: 'load', the behaviorscontinue·reset·hold·pause, no iteration completion,showWhenStopped: 'static', 0.5, 150 ms).resolveTriggers(triggers)returns aPxResolvedTriggers— every list an array of the events it may name (triggerEventListnormalises one),resumederived unless the file names it, an absent block{ start: ['load'] }, an empty block nothing.resolveActions(actions)returns aPxResolvedActions— every setting defaulted;actionSettingsOf(slot)reads one slot however it is written (the word'rewind'is exactly{ behavior: 'rewind' }).resolveGate(view)returns aPxResolvedGate: theoutOfViewbehavior and the two sensor numbers, defaulted. - The lists.
PxTriggerListnames them (start·stop·pause·resume);allowedTriggerEvents(list)is the table of events each may name (the schema is built from it);counterpartTriggerEvent(event)is the event that undoes another (hoverIn↔hoverOut,click→click, none forload);derivedResume(start, stop, pause)is the derived resume. - Schemas.
PxActionTriggersSchema(fixed lists, each limited to its events) andPxActionsSchema, one fixed sub-schema per action —PxStartActionSchema,PxStopActionSchema,PxFinishActionSchema,PxOutOfViewActionSchema— each stating its defaults. - The event rule.
triggerActionFor(triggers, state, event)— the player’s state (PxPlayState: stopped · playing · paused · finished) picks the list, and the answer is aPxTriggerAction:play,pauseorstop.triggerListsEventsays which listeners to attach at all. - The warnings.
triggerIssues(triggers)returns every problem with a block as aPxTriggerIssue—{ kind, list, event, otherList? }, itskindaPxTriggerIssueKind(eventNotAllowed·collision·resumeExtra·resumeMissing); an empty array is a clean block. The web player reports them as PX1110, PX1109 and PX1111 (both resume kinds). - Where they live. The trigger functions (
resolveTriggers…triggerIssues) and their types (PxResolvedTriggers,PxTriggerIssue) come from@pixodesk/svg-animator-core/engine; the constsPxTriggerList,PxTriggerIssueKind,PxPlayStateandPxTriggerActionfrom the main entry. - The executor. One
PxPlayExecutorper player performs every built-in action —start()peractions.start,stop()peractions.stop(preset and iteration completion),pause()/resume(), the state-ruleplay(),reset()(the unconditional reset-now),rewind(), andonNaturalFinish(), which performsactions.finishwhen the engine reports the end. It owns the one state engines cannot report: paused versus stopped.createPlayExecutor(api, config, raf)builds one over anyPxPlayTarget(the slice of the API it drives) from aPxPlayExecutorConfig(the run’s geometry and theactions—executorConfigOf(view)reads them from the runtime view, aPxApiExtrasConfig);ensurePlayExecutorreuses the one an engine attached.composeApiExtras(api, cfg, raf)then completes an engine’s api (aPxApiExtrasTarget) with the composed members —play(),stop(),reset(),playOnce(),getDuration()— asPxApiExtras.applyDeclarativeActiondrives aPxDeclarativeTarget(the public api, or a component’s proxy to it) from the declarative resolver’s actions. - CSS.
fillModeOf(showWhenStopped, finishBehavior)is theanimation-fill-modethe two settings spell — (static,hold)forwards, (firstFrame,hold)both, (static,reset)none, (firstFrame,reset)backwards— what the CSS export and the WAAPI engine hand to the browser.
Scroll-driven playback (in development)
Section titled “Scroll-driven playback (in development)”In development. Scroll-driven playback is not finished yet: the fields below may change, and not every combination works in every player. Time-driven playback — the default — is not affected.
Instead of playing on a clock, the animation can follow the scroll position — the playhead moves as the user scrolls: scroll down and the animation goes forward, scroll back up and it goes backward, stop and it stays on that frame. This is the model of CSS scroll-driven animations. Choose Timeline → scroll in the editor’s playback panel, or set it in the document:
"animator": { // The playhead follows the SVG's journey through the viewport instead of the clock; // `outTime` is the keyframe span the scroll range maps onto. "timeline": { "type": "view", "outTime": 3000, "range": { "start": { "phase": "entry", "fraction": 0 }, "end": { "phase": "exit", "fraction": 1 } } }}timeline: { "type": "view" } alone means “show the whole animation, first frame to last, as
the SVG travels across the viewport — the scroll position, not the clock, decides which frame
is on screen”; type: "scroll" follows the scroll container’s offset instead. The clock
fields (actionTriggers, actions, delay, "infinite") have no slot in these timelines. The
rest of the object tunes it:
timeline. | Values | Meaning |
|---|---|---|
type | view · scroll | progress = the SVG’s journey across the scrollport, or the scroll container’s offset ratio |
axis | block (default) · inline · x · y | which axis; block = vertical in normal writing mode |
source | nearest (default) · root | for type: scroll — the nearest scrollable ancestor, or the document |
subject | parent · scroller · a CSS selector | for type: view — whose journey is measured (default: the <svg> itself). parent is what makes a pinned section work |
range.start / range.end | { phase, fraction } | the slice of the journey mapped to 0–100 %; phase ∈ cover (default) · contain · entry · exit · entryCrossing · exitCrossing; fraction is a position within that phase, 0 = its start, 1 = its end |
iterations | number | the animation repeats this many times across the range (finite only — "infinite" cannot map onto a range) |
smoothing | ms | catch-up lag — the playhead eases toward the scroll position instead of snapping (smoother under momentum scrolling) |
pin | true · { align, offset, distance } | hold the canvas still on screen while scrolling moves the animation forward and back (position: sticky); align ∈ top/center/bottom, offset in px from the aligned position, distance in viewport heights creates the scroll travel |
engine | auto (default) · native · js | who computes progress and applies values — see Engine: auto/native use the browser’s ScrollTimeline where supported, js measures itself |
Support: the web player (both engines, and therefore React and Vue), and the SVG + JS
animation export. Not yet: the CSS export or React Native. The complete “scrollytelling”
pattern is subject: "parent" + pin: true inside a tall section.
Engine
Section titled “Engine”timeline.engine says how the animated attributes get updated — the same values on every timeline type:
| Value | Time-driven timeline | Scroll / view timeline |
|---|---|---|
auto (default) | the Web Animations API — played by the browser itself, so it stays smooth even while the page is busy — with an automatic fallback to the player’s frame loop when the document animates something WAAPI cannot express (path morphing, gradient geometry, filters, text on a path, …) | the browser’s own ScrollTimeline / ViewTimeline where supported; otherwise the player measures scroll progress itself and drives WAAPI (or the frame loop, if WAAPI declines the document) |
native | Web Animations API only | the browser’s ScrollTimeline / ViewTimeline driving WAAPI (where unsupported, the player measures progress instead — WAAPI stays) |
js | a requestAnimationFrame loop that writes attributes every frame; honors frameRate; universal browser support | the player measures scroll progress and applies values through its frame loop — identical everywhere |
Leave it on auto unless you need a guarantee — for instance js for path morphing in
Safari < 18.5. React Native ignores engine (playback is always native-driven).
Core exports the helpers the players decide this with, so a player you write yourself gets the
same answer: resolveTimelineEngine turns the document’s setting into
the engine that will actually run, isNativeForced says whether native was asked for outright,
and mayUseNativeScrollTimeline whether a scroll timeline may be handed to the browser’s own
ScrollTimeline.