Time
Duration and delay
Section titled “Duration and delay”An Element’s duration is how long it takes from its first frame to its last, 1 second if left
out. Its delay is how long it waits before its first frame, holding that frame until then.
A Burst has no duration of its own: it lasts until its latest Child ends, counting each Child’s
stagger offset, delay and duration, all the way down through nested Bursts. So
changing one Child’s duration changes the Burst’s, and under @motly/gsap the tween’s.
import { each, Motly } from '@motly/gsap';import { gsap } from 'gsap';
gsap.registerPlugin(Motly);
// Every other Child waits 0.3 s. The Burst lasts until its latest Child ends: 0.3 + 0.8 s.const spec = { kind: 'burst', count: 12, radius: [0, 100], restAt: 0.5, children: { kind: 'circle', radius: [8, 0], fill: each(['deeppink', 'gold']), delay: each([0, 0.3]), duration: 0.8, },};
gsap.effects.burst('.origin', { spec, repeat: -1, repeatDelay: 0.5 });<div class="origin"></div>A Burst’s own delay holds back the whole Burst: nothing is thrown until it has passed. Offset
Children with their own delay, as above, or all of them in turn with stagger.
Stagger
Section titled “Stagger”stagger on a Burst starts each Child a fixed time after the one before it, in ray order: from
the first ray, clockwise. Each Child is thrown from the Origin when it starts, so the burst ripples out instead of
popping.
import { Motly } from '@motly/gsap';import { gsap } from 'gsap';
gsap.registerPlugin(Motly);
// Each Child starts 0.05 s after the one before it, clockwise from 12 o'clock.const spec = { kind: 'burst', count: 16, stagger: 0.05, radius: [0, 100], restAt: 0.6, children: { kind: 'circle', radius: [7, 0], fill: '#06d6a0', duration: 0.7 },};
gsap.effects.burst('.origin', { spec, repeat: -1, repeatDelay: 0.5 });<div class="origin"></div>{ each, easing } keeps the same span of starts, each × (count − 1), but spreads them along a
Curve, so a burst can land unevenly on purpose. A start the Curve puts before 0
is held at 0.
import { expoIn, Motly } from '@motly/gsap';import { gsap } from 'gsap';
gsap.registerPlugin(Motly);
// The same span of starts, spread along a curve: slow to begin, then all at once.const spec = { kind: 'burst', count: 16, stagger: { each: 0.05, easing: expoIn }, radius: [0, 100], restAt: 0.6, children: { kind: 'circle', radius: [7, 0], fill: '#118ab2', duration: 0.7 },};
gsap.effects.burst('.origin', { spec, repeat: -1, repeatDelay: 0.5 });<div class="origin"></div>Under @motly/gsap, stagger in vars is not this: GSAP’s stagger across targets is ignored
with a warning. spec.stagger staggers the Children of each burst.
Playback
Section titled “Playback”In @motly/core, an Instance plays itself: play() starts from the beginning and returns a
promise that settles the first time the Playhead reaches the end moving forward. It never rejects:
destroy() before the end settles it too.
| Method | What it does |
|---|---|
play() |
Move the Playhead to 0 and play forward. |
pause() |
Stop the Playhead where it is. |
resume() |
Carry on from where pause() left it, in the direction it was moving. |
reverse() |
Play backward from where it is, stopping at 0. Reaching 0 settles nothing. |
seek(t) |
Move the Playhead to t seconds and draw there now, playing or paused as before. |
setProgress(p) |
seek() by progress, 0 to 1: for a slider or a scroll position. |
destroy() |
Stop, and remove everything drawn. |
import { Burst, rand } from '@motly/core';import { SVGRenderer } from '@motly/core/svg';
const renderer = new SVGRenderer(document.querySelector('svg'));
const burst = new Burst( { kind: 'burst', count: 14, radius: [0, 90], restAt: 0.4, children: { kind: 'star', points: 5, radius: [rand(5, 11), 0], angle: [0, rand(-180, 180)], fill: 'gold', duration: 2, }, }, { renderer, origin: { x: 150, y: 100 }, seed: 5 },);
for (const button of document.querySelectorAll('button')) { button.addEventListener('click', () => burst[button.dataset.action]());}
const slider = document.querySelector('input');slider.addEventListener('input', () => burst.setProgress(slider.valueAsNumber));burst.setProgress(slider.valueAsNumber);<main> <svg aria-hidden="true" width="300" height="200"></svg> <div class="controls"> <button type="button" data-action="play">play()</button> <button type="button" data-action="pause">pause()</button> <button type="button" data-action="resume">resume()</button> <button type="button" data-action="reverse">reverse()</button> </div> <label>setProgress() <input type="range" min="0" max="1" step="0.01" value="0.4" /></label></main>
<style> main { display: grid; justify-items: center; gap: 0.75rem; } .controls { display: flex; gap: 0.5rem; } button { padding: 0.35rem 0.75rem; border: 1px solid #ffffff44; border-radius: 999px; background: none; color: inherit; font: 0.85rem ui-monospace, monospace; cursor: pointer; } label { display: flex; gap: 0.75rem; align-items: center; font: 0.85rem ui-monospace, monospace; }</style>An Instance is drawn only from its Playhead, never from a clock, so the same Playhead always draws the same frame: scrubbing back and forth never changes what you see.
Under @motly/gsap there are no such methods to call: the effect returns a GSAP tween, and the
tween’s own play(), reverse(), progress() and the rest move the burst.
Timelines
Section titled “Timelines”A Timeline sequences several Instances on one Playhead, so one play(), seek() or reverse()
moves them all. Create it from a Scope, and create its Instances with its own shape() and
burst(). Leave out the last argument, at, to place an Instance after everything on the Timeline
so far; pass it, in seconds, to overlap.
import { createScope, each, rand } from '@motly/core';import { SVGRenderer } from '@motly/core/svg';
const renderer = new SVGRenderer(document.querySelector('svg'));const scope = createScope();const timeline = scope.timeline();
const ring = { kind: 'circle', radius: [0, 50], fill: 'none', stroke: '#ffd166', strokeWidth: [10, 0], duration: 0.6,};const sparks = { kind: 'burst', count: 10, radius: [0, 60], children: { kind: 'circle', radius: [rand(3, 7), 0], fill: each(['#ef476f', '#06d6a0']), duration: 0.7, },};
// Left out, `at` places an Instance after everything so far; given, it overlaps at that second.timeline.shape(ring, { renderer, origin: { x: 80, y: 110 } });timeline.burst(sparks, { renderer, origin: { x: 210, y: 110 } });timeline.burst(sparks, { renderer, origin: { x: 340, y: 110 } }, 0.8);
timeline.play();<svg aria-hidden="true" width="420" height="220"></svg>An Instance on a Timeline follows the Timeline only: its own playback methods do nothing, though
awaiting its play() still settles when the Timeline carries it past its end. Before its start it
holds its first frame, and after its end its last.
A Scope owns what it creates: scope.destroy() removes every Instance and Timeline in it at once.
Under @motly/gsap, use a GSAP timeline and tl.burst() instead.
Drivers
Section titled “Drivers”What moves the Playhead is a Driver. Standalone, core’s own requestAnimationFrame loop does.
When something in your app already decides time, pass your own Driver to a Scope, and every
Instance in it follows that instead:
import { createScope } from '@motly/core';
const scope = createScope({ driver: myDriver });A Driver has one method, attach(target). It takes charge of the target’s Playhead and returns
the Playback controls above; the target gives its duration, draws with render(t), and settles
its play() with finish(). @motly/gsap does the same job without one: GSAP’s ticker decides
time, and the tween seeks each burst’s Playhead as it renders.
The Resting frame
Section titled “The Resting frame”A viewer who prefers reduced motion sees one still frame instead of the motion: the Resting frame,
at restAt, a progress from 0 to 1. Left out, it is the last frame, which for a Burst is usually
empty, so give every Burst a restAt where it still shows something. restAt is read from the Spec
you create the Instance from, not from a Child’s.
import { Burst, rand } from '@motly/core';import { SVGRenderer } from '@motly/core/svg';
const spec = { kind: 'burst', count: 12, radius: [0, 80], // Where the Resting frame is, as a progress from 0 to 1. Its last frame would be empty. restAt: 0.4, children: { kind: 'circle', radius: [rand(4, 9), 0], fill: 'gold', duration: 1.2 },};
for (const [id, reducedMotion] of [ ['#moving', 'never'], ['#resting', 'always'],]) { const renderer = new SVGRenderer(document.querySelector(id)); const burst = new Burst(spec, { renderer, origin: { x: 110, y: 110 }, reducedMotion, seed: 3 }); burst.play();}<main> <figure> <svg id="moving" aria-hidden="true" width="220" height="220"></svg> <figcaption>reducedMotion: 'never'</figcaption> </figure> <figure> <svg id="resting" aria-hidden="true" width="220" height="220"></svg> <figcaption>reducedMotion: 'always'</figcaption> </figure></main>
<style> main { display: flex; gap: 2rem; } figure { margin: 0; text-align: center; } figcaption { opacity: 0.6; font: 0.9rem ui-monospace, monospace; }</style>In @motly/core, reducedMotion in the second argument is 'user' by default, which follows the
viewer’s setting, read at each play(). 'always' and 'never' force it, for a demo or a test.
Under reduced motion, play() draws the Resting frame and settles at once, resume() does nothing,
and reverse() jumps to the first frame. A Timeline takes reducedMotion too, in
scope.timeline({ reducedMotion }), and decides for every Instance on it.
Under @motly/gsap, the Resting frame is shown for the tween’s full length, so everything after it
in a timeline keeps its timing. See reduced motion in the GSAP
guide.