Using motly with GSAP
The vocabulary rule
Section titled “The vocabulary rule”Beside spec, vars takes four keys of motly’s: seed, container, renderer and
reducedMotion, described below. Every other key means what it means in any GSAP tween, and
motly’s own words for the burst live only in spec:
delayholds back the whole tween.spec.delayis a wait at the start of the burst, inside the tween’s length, as it is when the Spec plays without GSAP.easewarps the burst’s time;spec.easinggives each property its curve.easeis'none'unless you give one, so a Spec runs at its own pace rather than under GSAP’s default ease.durationstretches or squeezes the whole burst. Without it, the tween lasts as long as the Spec.repeat,yoyo,paused,onStart,onUpdate,onCompleteandscrollTriggerwork as on any tween.
import { Motly } from '@motly/gsap';import { gsap } from 'gsap';
gsap.registerPlugin(Motly);
// A ring that lasts 1 second on its own.const ring = { kind: 'circle', radius: [8, 90], fill: 'none', stroke: 'deeppink', strokeWidth: [10, 0], duration: 1, restAt: 0.3,};
// Every key beside `spec` is GSAP's: squeeze it to 0.6 s, ease its time, repeat it forever.gsap.effects.shape('.dot', { spec: ring, duration: 0.6, ease: 'power2.out', repeat: -1, repeatDelay: 0.4,});<div class="dot"></div>
<style> .dot { width: 16px; height: 16px; border-radius: 50%; background: deeppink; }</style>keyframes, startAt, runBackwards and stagger do not apply to a burst: the types reject
them, and if you pass them anyway they are ignored with a warning. To stagger bursts, use a loop or
timeline positions.
Where a burst comes from
Section titled “Where a burst comes from”An element, a selector or a list gives one burst from each element’s centre. The centre is read when the tween starts from 0 moving forward, not on every frame, so a burst stays where it began if the page scrolls.
A point, { x, y } in viewport pixels as clientX and clientY give, bursts from that point:
import { each, Motly, rand } from '@motly/gsap';import { gsap } from 'gsap';
gsap.registerPlugin(Motly);
const spec = { kind: 'burst', count: 10, radius: [0, 60], restAt: 0.4, children: { kind: 'circle', radius: [rand(3, 7), 0], fill: each(['#ffd166', '#ef476f', '#06d6a0', '#118ab2']), duration: rand(0.5, 0.8), },};
// A point in viewport pixels, as clientX and clientY give, instead of an element.addEventListener('pointerdown', (event) => { gsap.effects.burst({ x: event.clientX, y: event.clientY }, { spec });});<p>Click anywhere</p>
<style> p { opacity: 0.6; user-select: none; }</style>Each element or point gets its own Seed, so a row of icons does not show the same burst copied.
seed: 7 draws the same bursts on every page load: the element or point at index i draws from
seed + i. Without seed, each call draws a random Seed, which a repeat, a restart and a scrub all
keep.
import { each, Motly, rand } from '@motly/gsap';import { gsap } from 'gsap';
gsap.registerPlugin(Motly);
const spec = { kind: 'burst', count: 9, radius: [0, rand(40, 70)], restAt: 0.4, children: { kind: 'star', points: 5, radius: [rand(4, 10), 0], angle: [0, rand(-180, 180)], fill: each(['gold', 'deeppink', 'white']), duration: rand(0.5, 0.9), },};
// One burst per matching element; the ith draws from seed + i, so the three differ from each// other but are the same on every click and every page load.document.querySelector('#seeded').addEventListener('click', () => { gsap.effects.burst('.icon', { spec, seed: 7 });});
// Without a seed, every call draws a fresh random burst.document.querySelector('#random').addEventListener('click', () => { gsap.effects.burst('.icon', { spec });});<main> <div class="icons"> <span class="icon">★</span> <span class="icon">★</span> <span class="icon">★</span> </div> <button type="button" id="seeded">seed: 7</button> <button type="button" id="random">No seed</button></main>
<style> main { display: grid; grid-template-columns: auto auto; gap: 2rem 1rem; justify-items: center; } .icons { grid-column: 1 / -1; display: flex; gap: 5rem; font-size: 2rem; color: gold; } button { padding: 0.5rem 1rem; border: 1px solid #ffffff44; border-radius: 999px; background: none; color: inherit; font: 1rem ui-monospace, monospace; cursor: pointer; }</style>A selector or list that matches nothing warns, and gives a tween as long as the Spec that draws nothing, so a timeline keeps its timing.
Where a burst is painted
Section titled “Where a burst is painted”By default, a burst is painted in an overlay the tween owns: fixed over the viewport, above the page, catching no clicks, and in the document only while the tween is between its ends.
container, an element or a selector, paints it inside that element instead, so it scrolls with it
and is clipped by it. A burst scrubbed by ScrollTrigger in a pinned section needs this.
import { Motly, rand } from '@motly/gsap';import { gsap } from 'gsap';
gsap.registerPlugin(Motly);
const spec = { kind: 'burst', count: 16, radius: [0, 200], restAt: 0.3, children: { kind: 'circle', radius: [rand(5, 10), 0], fill: 'deeppink', duration: 1 },};
const button = document.querySelector('button');button.addEventListener('click', () => { // The burst still comes from the button, but it is painted in the card. gsap.effects.burst(button, { spec, container: '.card' });});<div class="card"> <p>Painted inside the card, and clipped by it</p> <button type="button">Burst</button></div>
<style> .card { display: grid; place-items: center; width: 320px; height: 180px; overflow: hidden; border-radius: 1rem; background: #2d1b3d; } p { margin: 0; opacity: 0.6; } button { padding: 0.75rem 1.5rem; border: 0; border-radius: 999px; background: deeppink; color: white; font: 600 1rem system-ui, sans-serif; cursor: pointer; }</style>The container must have a size. A static container is made relative while a burst is drawn in it, and set back once the last one is cleared. A selector that matches nothing warns and gives a tween as long as the Spec that draws nothing.
renderer is 'auto' by default, which keeps bursts under 50 Elements in SVG and paints larger
ones on a canvas. 'svg' or 'canvas' forces one.
Cleanup
Section titled “Cleanup”A burst never outlives its tween, wherever GSAP reaches the tween:
- At either end, nothing is left drawn.
tween.kill(),tween.revert(),tl.revert()andgsap.context().revert()clear a burst mid-flight. So doesuseGSAPwhen its component unmounts:
useGSAP( () => { gsap.effects.burst(buttonRef.current, { spec }); }, { scope: containerRef },);Give onInterrupt in vars. motly chains its own clear-up before yours there. Replacing it later
with tween.eventCallback('onInterrupt', fn) drops the clear-up, so a later kill() leaves the
burst drawn. For the same reason, reading it back, through eventCallback() or tween.vars, gives
motly’s wrapper rather than your function.
Reduced motion
Section titled “Reduced motion”A viewer who prefers reduced motion sees the Spec’s still Resting frame, at spec.restAt, for the
tween’s full length, so everything after it in a timeline keeps its timing. The preference is read
each time the tween starts from 0, so changing it needs no reload; a repeat keeps the one it
started with. reducedMotion: 'always' or 'never' in vars forces either, for a demo or a test:
import { Motly, rand } from '@motly/gsap';import { gsap } from 'gsap';
gsap.registerPlugin(Motly);
const spec = { kind: 'burst', count: 12, radius: [0, 80], // The Resting frame: what a viewer who prefers reduced motion sees, for the whole tween. restAt: 0.4, children: { kind: 'circle', radius: [rand(4, 9), 0], fill: 'gold', duration: 1.2 },};
// Left out, reducedMotion follows the viewer's setting. These force it, as a demo or test would.for (const button of document.querySelectorAll('button')) { button.addEventListener('click', () => { gsap.effects.burst(button, { spec, reducedMotion: button.dataset.reducedMotion }); });}<main> <button type="button" data-reduced-motion="never">reducedMotion: 'never'</button> <button type="button" data-reduced-motion="always">reducedMotion: 'always'</button></main>
<style> main { display: flex; gap: 3rem; } button { padding: 0.75rem 1.25rem; border: 0; border-radius: 999px; background: deeppink; color: white; font: 0.9rem ui-monospace, monospace; cursor: pointer; }</style>This covers the bursts only. Your own tweens around them keep moving; wrap those in
gsap.matchMedia() with (prefers-reduced-motion: reduce).
Importing the package types gsap.effects.burst, gsap.effects.shape, tl.burst and tl.shape:
spec must be a Spec of the right kind. BurstVars, ShapeVars, MotlyVars and RendererName
are exported for vars built apart from the call:
import type { BurstVars } from '@motly/gsap';
const vars: BurstVars = { spec: { kind: 'burst', children: { kind: 'circle' } }, seed: 7 };gsap.effects.burst('.icon', vars);