animate
rpic extension. animate declares how objects enter the drawing —
as metadata. The static SVG is untouched except for its stable per-shape
ids (s0, s1, …); a thin web player drives a GSAP timeline against
those ids.
.PS
margin = 0.12
B1: box "load" fit
arrow
B2: box "run" fit
arrow
C: circle "done" rad 0.3
animate B1 with "pop" for 0.4
animate 1st arrow with "draw"
animate B2 with "pop" after 1st arrow
animate 2nd arrow with "draw"
animate C with "fade" for 0.8
.PEThis page is the host document — the timeline above is playing live
(hit ▶ replay). The draw effect traces paths; pop overshoots, which is
why the source reserves headroom with margin.
Beyond entrances, the palette tells a story — a label that types itself in, steps that pop in sequence, and a wiggle to point at the one that matters:
.PS
margin = 0.15
A: box "parse" fit
arrow
B: box "eval" fit
arrow
C: box "draw" fit
T: "rpic animates" at B.n + (0, 0.55)
animate T with "type" for 1.0
animate A with "pop" after T
animate B with "pop" after A
animate C with "pop" after B
animate B with "wiggle" wiggles 6 after C
.PEQuick start — plain HTML, no build step
The zero-effort path is the CLI writing the whole page for you:
rpic --html -o fig.html fig.pic
That’s a single self-contained file — SVG, manifest and the GSAP player
inlined, and GSAP itself (plus only the plugins your effects need) pulled
from a pinned, integrity-checked CDN tag. Open it and the timeline plays;
animate scroll sources get scroll-scrubbing wired, draggable objects
respond. A drawing with no animations becomes a plain static page.
Prefer to own the page? Everything here runs from two CDN files: GSAP and the rpic package (which ships the player). No bundler required.
Compile in the browser. The wasm build compiles pic source on the page and plays the manifest in one go:
<div id="stage"></div>
<script type="module">
import { ready, compile, animate } from
'https://cdn.jsdelivr.net/npm/@strategicprojects/rpic@0.11.0/index.js';
import { gsap } from 'https://cdn.jsdelivr.net/npm/gsap@3.13.0/+esm';
await ready(); // the .wasm is fetched from the CDN
const { svg, animations } = compile(`box "A"; arrow; box "B"
animate 1st box with "pop"
animate last arrow with "draw"
animate last box with "fade"`);
const stage = document.querySelector('#stage');
stage.innerHTML = svg;
animate(stage, animations, gsap);
</script>
Or pre-render with the CLI. rpic --json fig.pic gives you { svg, animations, … }; paste both into a static page and import only the player —
animate() never touches the wasm, so nothing is compiled at runtime. This is
the classic GSAP header pattern:
<head>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js"
integrity="sha384-HOvlOYPIs/zjoIkWUGXkVmXsjr8GuZLV+Q+rcPwmJOVZVpvTSXQChiN4t9Euv9Vc"
crossorigin="anonymous"></script>
<!-- only if the source uses move / morph / scramble / wiggle: -->
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/MotionPathPlugin.min.js"
integrity="sha384-ZdcNM4JVCcTz+LG7hy3tkzchm/ljdYJcwthjW8G4vqAXApMm4ve+3ByX+RfrAlFt"
crossorigin="anonymous"></script>
</head>
<body>
<div id="stage"><!-- paste the `svg` string here --></div>
<script type="module">
import { animate } from
'https://cdn.jsdelivr.net/npm/@strategicprojects/rpic@0.11.0/player.js';
gsap.registerPlugin(MotionPathPlugin); // only with the plugin above
const animations = [/* paste the `animations` array here */];
animate(document.querySelector('#stage'), animations, gsap);
</script>
</body>
player.js is the player as its own zero-import module, so the page fetches
nothing wasm-related at all — importing index.js also works but statically
pulls the wasm glue JS (harmless, just ~10 KB you don’t need with a
pre-rendered SVG).
The same pattern loads dist/MorphSVGPlugin.min.js, dist/ScrambleTextPlugin.min.js
and dist/CustomEase.min.js + dist/CustomWiggle.min.js when those effects are
in play — see the plugin note below. Pin the versions you tested
(jsdelivr also accepts ranges like @0.10). If you have a bundler, skip the
CDN and npm install @strategicprojects/rpic gsap — see
Consuming the manifest.
Syntax
animate <place> with "<effect>" [along <path>] [into <shape>] [to <colour>] [from <dir>]
[out] [stagger <d>] [for <dur>] [at <t> | after <place>] [delay <d>] [repeat <n>] [yoyo] [ease "<name>"]
Only the target and effect are required; the trailing clauses are optional and may appear in any order.
| Clause | Example | Meaning |
|---|---|---|
animate place with “effect” |
animate B1 with "pop" |
animate that object (required) |
along place |
along L |
path to follow — required by move, ignored elsewhere |
into place |
into B |
shape to morph into — required by morph, ignored elsewhere |
to colour |
to "crimson" |
target colour for highlight; any rpic colour form, ignored elsewhere |
from dir |
from left |
entry direction (up/down/left/right) — required by slide, ignored elsewhere |
by word|char|“chars” |
by word, by "01" |
type reveal granularity (default char), or a scramble charset; ignored on other effects |
wiggles n |
wiggles 6 |
oscillation count for wiggle (default 6), ignored elsewhere |
out |
out |
play the effect as an exit (reverse) instead of an entrance |
stagger expr |
stagger 0.12 |
on a block target, fan the effect across its children, ds apart |
for expr |
for 0.4 |
duration in seconds (default 0.6) |
at expr |
at 1.5 |
start at an absolute time |
after place |
after 1st arrow |
start when that object’s animation ends |
delay expr |
delay 0.3 |
offset added on top of the computed start |
repeat expr |
repeat 2 |
replay count; -1 loops forever, 0/absent plays once |
yoyo |
yoyo |
reverse direction on each repeat (needs repeat) |
ease “name” |
ease "elastic.out" |
override the effect’s default easing with any GSAP ease |
The place is any native pic reference — a label (B1), an ordinal
(1st arrow, 2nd last line, last box), or previous. The target must be a
drawn shape; animating a bare point (a name with no geometry) is an error.
Effects
| Effect | Motion | Easing |
|---|---|---|
fade |
opacity 0 → 1 | power1.out |
pop |
scale 0 → 1 | back.out(1.7) (overshoots) |
draw |
strokes trace themselves; arrowheads and text pop/fade in as the shaft reaches them. Narrow to a sub-segment with from <p>/to <p> |
none for the trace |
move |
travel along another object’s path (needs along) |
none |
highlight |
recolour + thicken the outline with a scale pulse (to <colour>), or a colour-free pulse |
power1.inOut |
slide |
translate in from a compass direction (needs from <dir>) |
power2.out |
morph |
tween the outline into another shape’s geometry (needs into <shape>) |
power1.inOut |
type |
reveal a label one character (or by word) at a time — a typewriter |
none |
scramble |
glyphs cycle through random characters and resolve to the text (custom set with by "…") |
none |
wiggle |
a quick oscillating shake that returns to rest — “look here” emphasis (wiggles <n>) |
CustomWiggle |
Any other effect string is accepted by the parser but flagged with an
unknown_animation_effect warning in the --json bundle — it renders nothing.
rpic only emits metadata — the browser player does the animating. Three
effects lean on optional GSAP plugins that you must
register before playing the timeline:
move(motion along a path) →MotionPathPluginmorph(shape-into-shape) →MorphSVGPluginscramble(decode text) →ScrambleTextPluginwiggle(shake emphasis) →CustomWiggle(which registersCustomEasetoo)
Use GSAP ≥ 3.13. Since 3.13 (May 2025,
after Webflow’s acquisition) the entire toolset — MorphSVGPlugin and
ScrambleTextPlugin included — is free, even commercially; on older
versions those were paid Club GreenSock plugins that silently loaded a
watermarked trial build. Register them once, up front:
import { gsap } from 'gsap';
import { MotionPathPlugin } from 'gsap/MotionPathPlugin';
import { MorphSVGPlugin } from 'gsap/MorphSVGPlugin';
import { ScrambleTextPlugin } from 'gsap/ScrambleTextPlugin';
import { CustomEase } from 'gsap/CustomEase';
import { CustomWiggle } from 'gsap/CustomWiggle';
gsap.registerPlugin(MotionPathPlugin, MorphSVGPlugin, ScrambleTextPlugin, CustomEase, CustomWiggle);The other effects (fade, pop, draw, slide, highlight, type) use
only the GSAP core and need no plugins. animate scroll additionally wants
ScrollTrigger — see
Scroll-scrubbing below.
Timing model
Every animation resolves to an absolute start and duration in seconds.
There are four ways to place one on the timeline:
- Sequential (default). With no
at/after, an animation begins when the previously declared one ends — animations cascade in source order. afterplace — begin when a specific named object’s animation ends. This lets you branch instead of running one straight chain.att — pin to an absolute second (run things in parallel, or line up with an external soundtrack).atandafterwrite the same slot, so if you give both the last one wins.delayd — shift whatever start the clauses above produced by d seconds. It stacks on sequential,at, orafter.
The example below exercises all four in one timeline:
.PS
margin = 0.12
B1: box "load" fit
arrow
B2: box "run" fit
arrow
C: circle "done" rad 0.3
animate B1 with "pop" for 0.4
animate 1st arrow with "draw"
animate B2 with "pop" after 1st arrow
animate 2nd arrow with "draw" at 1.2
animate C with "fade" for 0.8 delay 0.3
.PEIts --json manifest is a flat, absolute-time array — no player needed to read it:
[
{ "id": "s0", "effect": "pop", "start": 0, "duration": 0.4 },
{ "id": "s1", "effect": "draw", "start": 0.4, "duration": 0.6 },
{ "id": "s2", "effect": "pop", "start": 1, "duration": 0.6 },
{ "id": "s3", "effect": "draw", "start": 1.2, "duration": 0.6 },
{ "id": "s4", "effect": "fade", "start": 2.1, "duration": 0.8 }
]
Looping and easing
repeat, yoyo, and ease map straight onto the GSAP tween that plays each
object — they let an object pulse, breathe, or bounce instead of entering once.
repeatn replays the effect n more times;repeat -1loops forever.yoyoreverses every other pass, so arepeated effect eases back to its start instead of snapping. On its own (norepeat) it does nothing and rpic emits ayoyo_without_repeatwarning.ease“name” swaps the effect’s built-in easing for any GSAP ease —"none","power2.inOut","elastic.out(1, 0.3)","bounce.out","steps(6)", and so on. See the easing reference below for the full family/variant list.
An infinite repeat never blocks the sequence: sequential and after timing
track only the first pass, so a looping highlight can run under everything else.
.PS
margin = 0.2
P: circle "ping" rad 0.35 fill 0.9
box "settle" with .w at P.e + (0.6,0) fit
animate P with "pop" for 0.7 repeat -1 yoyo ease "sine.inOut"
animate last box with "fade" for 0.6 ease "elastic.out(1, 0.4)"
.PEOnly the keys you set reach the manifest, so this timeline is:
[
{ "id": "s0", "effect": "pop", "start": 0, "duration": 0.7,
"repeat": -1, "yoyo": true, "ease": "sine.inOut" },
{ "id": "s1", "effect": "fade", "start": 0.7, "duration": 0.6,
"ease": "elastic.out(1, 0.4)" }
]
Easing reference
rpic passes the ease string straight through — it never parses or
validates it, so any name GSAP understands
works, and that page is the authoritative catalogue (including plugin eases
like CustomEase once you register them). The names follow one pattern:
<family>.<variant> e.g. power2.out, sine.inOut, bounce.in
<family>.<variant>(<args>) e.g. back.out(1.7), elastic.out(1, 0.3), steps(6)
The variant is the curve’s shape — always spell it out (a bare power2 is
ambiguous):
| Variant | Motion |
|---|---|
.in |
starts slow, accelerates into the end |
.out |
starts fast, decelerates to a stop — the everyday choice (GSAP’s default is power1.out) |
.inOut |
eases at both ends |
The families (each with all three variants unless noted):
| Family | Feel |
|---|---|
none / power0 |
linear — no easing (no variants) |
power1 … power4 |
polynomial ramps, gentle (power1) to sharp (power4) |
sine |
soft, sinusoidal |
expo |
very sharp — near-flat, then snaps |
circ |
circular arc |
back |
overshoots and settles; back.out(1.7) sets the overshoot |
elastic |
springy wobble; elastic.out(amp, period) tunes it |
bounce |
bounces like a dropped ball |
steps(n) |
snaps through n discrete stops (no variants) |
Because it’s an opaque passthrough, a typo ("powr2.out") sails through rpic
and only surfaces in the browser, where GSAP falls back to its default ease —
so copy names from the GSAP page, or use its live ease
visualizer to preview a curve before pasting.
Explore every ease
Pick a family and in/out/inOut to see its curve and the motion side by
side — the marker climbs the axis with that ease. Every panel here is drawn by
rpic itself: each curve is plotted from the ease’s own formula (the in and
inOut shapes are derived from out), and the marker rides move with
ease "<family>.<variant>" — the exact string you’d pass in your own source.
The plugin eases (rough/slow/expoScale, and CustomEase/CustomBounce/
CustomWiggle) have no closed-form formula for rpic to plot, so they’re not in
this explorer — but their motion works the same once you registerPlugin and
pass the name to ease. The panel below the curve shows the exact pic source
for the selected combination — copy it and render it yourself.
The curves, drawn in rpic
Words only go so far — here is each family’s .out curve, plotting input time
(x, 0 → 1) against eased progress (y, 0 → 1). The dashed diagonal is the linear
(no-ease) reference; back/elastic/bounce overshoot past the top of the
cell on purpose. This whole figure is itself drawn by rpic — the source
samples every ease formula in a for loop, so it’s a working demo of the
engine’s own maths (sin/exp/sqrt/^, if/else for the piecewise
bounce). Copy it and plot the .in or .inOut variants to compare.
The static grid above becomes the graph view of GSAP’s visualiser once you
animate it: the curve draws itself (its segments are a block, revealed by
draw + stagger), while a value marker rides move up the axis on the right
with the same ease — so the marker’s height traces the curve’s value as it’s
drawn (hit ▶ replay). The only piece pic can’t do is GSAP’s live numeric
value: readout — there’s no animated counter.
.PS
margin = 0.25
u = 2.4
n = 28
box wid u ht u thick 0.7 outlined 0xcbd5e1 with .sw at (0,0)
line from (0,0) to (u,u) dashed thick 0.6 colored 0xcbd5e1
Curve: [
for i=0 to n-1 do {
t=i/n; s=(i+1)/n
a=1-(1-t)^3; b=1-(1-s)^3
line from (u*t,u*a) to (u*s,u*b) thick 2.4 colored 0x2f855a
}
] with .sw at (0,0)
VT: line from (u+0.45,0) to (u+0.45,u) thick 0.9 colored 0xcbd5e1
VD: dot at VT.start rad 0.09 colored 0x0ae448
"power2.out" at (u/2, -0.28)
animate Curve with "draw" for 0.05 stagger 0.06
animate VD with "move" along VT ease "power2.out" for 1.7 repeat -1 yoyo at 0
.PEAnd the same eases as motion — a live “ease visualiser” like
GSAP’s, read as movement instead of a graph.
Six dots cover an identical track in the same time, differing only in ease, so
you compare them side by side (hit ▶ replay): none marches at a constant rate,
power2.out/expo.out shoot ahead then coast in, back/elastic overshoot and
spring back, bounce.out overshoots the end and settles — exactly the curves
above, in motion. Each dot rides move along its track with repeat -1 yoyo,
so they loop forever.
.PS
margin = 0.2
dy = 0.5
L0: line from (0,0) to (3,0) thick 0.8 colored 0xcbd5e1
L1: line from (0,-dy) to (3,-dy) thick 0.8 colored 0xcbd5e1
L2: line from (0,-2*dy) to (3,-2*dy) thick 0.8 colored 0xcbd5e1
L3: line from (0,-3*dy) to (3,-3*dy) thick 0.8 colored 0xcbd5e1
L4: line from (0,-4*dy) to (3,-4*dy) thick 0.8 colored 0xcbd5e1
L5: line from (0,-5*dy) to (3,-5*dy) thick 0.8 colored 0xcbd5e1
D0: dot at L0.start rad 0.08 colored 0x2f855a
D1: dot at L1.start rad 0.08 colored 0x3182ce
D2: dot at L2.start rad 0.08 colored 0x805ad5
D3: dot at L3.start rad 0.08 colored 0xdd6b20
D4: dot at L4.start rad 0.08 colored 0xe53e3e
D5: dot at L5.start rad 0.08 colored 0x319795
"none" ljust above at L0.start + (0.12,0.04)
"power2.out" ljust above at L1.start + (0.12,0.04)
"expo.out" ljust above at L2.start + (0.12,0.04)
"back.out(1.7)" ljust above at L3.start + (0.12,0.04)
"elastic.out(1,0.3)" ljust above at L4.start + (0.12,0.04)
"bounce.out" ljust above at L5.start + (0.12,0.04)
animate D0 with "move" along L0 for 2 ease "none" repeat -1 yoyo at 0
animate D1 with "move" along L1 for 2 ease "power2.out" repeat -1 yoyo at 0
animate D2 with "move" along L2 for 2 ease "expo.out" repeat -1 yoyo at 0
animate D3 with "move" along L3 for 2 ease "back.out(1.7)" repeat -1 yoyo at 0
animate D4 with "move" along L4 for 2 ease "elastic.out(1,0.3)" repeat -1 yoyo at 0
animate D5 with "move" along L5 for 2 ease "bounce.out" repeat -1 yoyo at 0
.PEMotion along a path
The move effect sends its target travelling along another object’s
geometry — a token down a wire, a packet along an arrow, current around a
loop. Name the path with along <place>; it can be any drawn line, arrow,
or spline. The traveller is usually a small dot
placed at the path’s start.
.PS
margin = 0.2
A: box "in" fit
arrow right 1.6
B: box "out" fit
D: dot at 1st arrow.start rad 0.07 colored "red"
animate 1st arrow with "draw" for 0.8
animate D with "move" along 1st arrow for 1.2 after 1st arrow
.PEThe manifest entry gains a path key naming the followed object’s id
("path":"s1"), so the player can drive it with GSAP’s MotionPathPlugin:
[
{ "id": "s1", "effect": "draw", "start": 0, "duration": 0.8 },
{ "id": "s3", "effect": "move", "start": 0.8, "duration": 1.2, "path": "s1" }
]
The dot travels once and rests at the path’s end. move without an along
path is an error; an along on any other effect is ignored with an
along_without_move warning. Add repeat -1 for a continuous flow, or
repeat -1 yoyo for a there-and-back shuttle.
Partial draw — reveal a stroke segment
By default draw traces the whole stroke, 0 → 100%. Two optional clauses —
from <p> and to <p> — narrow it to any sub-segment, given as a fraction
of the stroke (60%, or the bare fraction 0.6):
animate P with "draw" # unchanged: 0% → 100%
animate P with "draw" to 60% # trace only the first 60%
animate P with "draw" from 40% to 60% # reveal just the 40%–60% window
Reach for it to sweep a highlight along one leg of a wire, reveal a mid-section,
or animate a moving window down a path. out retracts the window instead of
growing it. The example below reveals only the middle third of the arc — drawn a
second time in grey behind it as a track, a common idiom so the whole path
stays visible while the window highlights a slice (as on the
home page):
.PS
margin = 0.2
arc from (0,1.1) to (3.4,1.1) rad 2 thick 2.4 colored "0xcbd5e1"
A: arc from (0,1.1) to (3.4,1.1) rad 2 thick 2.4 colored "0x2f855a"
animate A with "draw" from 33% to 66% for 1.4
.PEEach end rides the manifest only when set ("drawFrom":0.4,"drawTo":0.6, as
fractions), so a plain draw stays byte-identical. Out-of-range values clamp to
[0,1]. The player realises the window with a pure stroke-dasharray sweep — no
extra GSAP plugin. from/to here are the reveal fraction only for the draw
effect; on slide/highlight those same words keep their direction/colour
meaning.
rpic’s window is a fraction of the stroke (40%), not GSAP
DrawSVGPlugin’s absolute pixel range ("20 350"). Fractions are resolution-
independent and need no plugin; if you need exact pixel offsets, drive
DrawSVGPlugin yourself against the emitted s<N> element.
Morphing shapes
morph tweens an object’s outline into another shape’s geometry — a box
becoming a circle, one symbol flowing into the next. Name the target with
into <shape>; both are ordinary drawn objects, so place the target where the
morph should land (a shared centre reads as a shape-shift in place). The player
uses GSAP’s MorphSVGPlugin.
.PS
margin = 0.25
B: circle rad 0.45 at (1.2,0)
A: box wid 0.8 ht 0.8 "shape" fit at (1.2,0)
animate A with "morph" into B for 1.2 repeat -1 yoyo ease "power1.inOut"
.PEThe manifest entry gains a morph key naming the target’s id ("morph":"s0").
The target must be a visible shape (an invis reference draws no geometry to
read). morph without into is an error; into on any other effect is ignored
with an into_without_morph warning.
Emphasis
highlight draws the eye to an already-drawn object — the “now look here” beat
of a step-by-step explanation. With to <colour> it recolours and thickens
the object’s outline (any rpic colour form — a name, rgb(), 0xRRGGBB) and
gives it a small scale pulse, so the emphasis reads at a glance; without a
colour it’s a colour-free scale pulse.
Like the other effects it’s a one-directional tween, so it composes with the
timing clauses: add repeat 1 yoyo for a single flash-and-return, or
repeat -1 yoyo for a continuous pulse.
.PS
margin = 0.15
A: box "start" fit
arrow
B: box "critical" fit
arrow
C: box "end" fit
animate B with "highlight" to "crimson" for 0.6 repeat -1 yoyo
.PEThe target colour rides the manifest entry as a color key
("color":"crimson" above; rgb()/0xRRGGBB resolve to #rrggbb). to on any
non-highlight effect is ignored with a to_without_highlight warning.
Attention wiggle
wiggle gives an object a quick oscillating shake that returns to rest — the
“look here” nudge, without moving the object from its place. Set the number of
oscillations with wiggles <n> (default 6). It’s built on GSAP’s
CustomWiggle ease, so register
that (and CustomEase) — see the callout above.
.PS
margin = 0.15
A: box "start" fit
arrow
B: box "look!" fit
arrow
C: box "end" fit
animate B with "wiggle" wiggles 8 for 0.8 repeat -1 delay 1
.PEThe count rides the manifest as "wiggles":8; absent, the player uses 6.
wiggles on any non-wiggle effect is ignored with a wiggles_without_wiggle
warning.
Any GSAP ease name passes straight through ease "<name>", so a springy
landing is already one word away — ease "bounce.out", ease "elastic.out",
ease "back.out(1.7)". For a tunable bounce (strength, squash-and-stretch),
register CustomBounce,
CustomBounce.create("myBounce", { strength: 0.6, squash: 3 }), and reference
it by name: ease "myBounce".
Typewriter text
type reveals a label one piece at a time — the way a caption “speaks” a step.
By default each character appears in turn; add by word to reveal whole
words instead. The whole reveal spans the effect’s for duration.
.PS
margin = 0.15
"Describe the picture." at (0,0)
animate last with "type" for 1.6
"rpic draws it." at (0,-0.4)
animate last with "type" by word for 0.9 after 2nd last
.PEUnlike GSAP’s SplitText (which does not support SVG <text>), the split is
native: the SVG backend wraps each unit of a type target in a
<tspan class="rpic-ch">, and the player staggers their opacity — no plugin,
nothing to install. The tspans carry no positioning, so the pre-animation
render is visually identical to a plain label, and a drawing with no type
animation emits byte-identical text. by on any non-type effect is ignored
with a by_without_type warning; out reverses it into a staggered erase.
Scramble reveal
scramble cycles a label’s glyphs through random characters and resolves them
into the real text — a “decoding” reveal. Unlike type it needs no split: GSAP’s
ScrambleTextPlugin drives the <text> element’s content directly, so the
effect has zero cost on the SVG side. Pass a custom character set with
by "…" (default is upper-case letters).
.PS
margin = 0.15
box "ACCESS GRANTED" fit
animate last with "scramble" for 1.4
.PEThe charset rides the manifest as a chars key ("chars":"01" for a binary
“Matrix” look); absent, the player uses upperCase. Needs
ScrambleTextPlugin registered (see the callout above). out scrambles the
text away to nothing; by "…" on a non-scramble effect warns
by_without_scramble.
Staggering a group
Point animate at a [ … ] block and add stagger <d> to fan the effect
across the block’s children — each successive child starts d seconds after
the previous, so a row or grid assembles itself one piece at a time. It expands
to one ordinary manifest entry per child, so the timeline stays a flat array.
.PS
margin = 0.15
Row: [ box "1"; move 0.2; box "2"; move 0.2; box "3"; move 0.2; box "4" ]
animate Row with "pop" for 0.4 stagger 0.12
.PEOnly the block’s visible children get a slot — invisible helpers (explicit
moves, invis spines) are skipped, so the four boxes above stagger evenly
even though moves separate them. Children fan in source order; the sequence
resumes after the last child, so a following animate picks up cleanly.
stagger on a non-block target is ignored with a stagger_without_block
warning (the single object still animates).
Direction and exit
slide enters an object by translating it in from a compass direction —
from up, down, left, or right — offset by its own size so it clears its
slot. The out modifier is orthogonal: it turns any effect into an exit,
reversing the tween (fade away, pop out, retract a draw, slide off). Together
they let a diagram tell a two-beat story — build up, then tear down.
.PS
margin = 0.3
A: box "L" fit
B: box "U" fit at A + (1.2,0)
C: box "R" fit at B + (1.2,0)
animate A with "slide" from left for 0.5
animate B with "slide" from up for 0.5 delay 0.15
animate C with "slide" from right for 0.5 delay 0.3
.PEout composes with the timing clauses, so an object can enter and later leave —
here a box pops in, then fades out a beat afterwards:
.PS
margin = 0.2
S: box "step" fit
animate S with "pop" for 0.4
animate S with "fade" out for 0.5 after S delay 0.6
.PEslide needs a from direction (omitting it is an error); from on any other
effect is ignored with a from_without_slide warning. out rides the manifest
as "out":true, and slide’s direction as "from":"left" — both only when set.
A control loop drawing itself, wires first:
.PS
texlabels = 1; margin = 0.12
arrow "$u$" above
S: circle rad 10/72.27
line right 0.35
G: box "$G(s)$" wid 1 ht 0.55
arrow "$y$" above
line -> down G.ht from last arrow then left last arrow.c.x - S.x then to S.s
animate 1st arrow with "draw" for 0.4
animate S with "pop"
animate 2nd last line with "draw" for 0.3
animate G with "pop" for 0.5
animate 2nd arrow with "draw" for 0.4
animate last line with "draw" for 0.9
.PEConsuming the manifest
rpic_core::compile_json (and the JS/Python bindings) return
{ svg, animations, diagnostics, warnings }. Each animation is
{ id, effect, start, duration } plus the optional repeat / yoyo / ease /
path / color / out / from / morph keys — present only when the source
set them, so plain manifests stay compact. The JS package ships the player,
which folds those keys straight into GSAP. move needs MotionPathPlugin and
morph needs MorphSVGPlugin registered on your GSAP instance (GSAP ≥
3.13, where both are free — see the note under Effects):
import { ready, compile, animate } from '@strategicprojects/rpic';
import { gsap } from 'gsap';
import { MotionPathPlugin } from 'gsap/MotionPathPlugin';
import { MorphSVGPlugin } from 'gsap/MorphSVGPlugin';
gsap.registerPlugin(MotionPathPlugin, MorphSVGPlugin); // for `move` / `morph`
await ready();
const { svg, animations } = compile(src);
stage.innerHTML = svg;
animate(stage, animations, gsap);
Scroll-scrubbing
Autoplay is the default. Add a bare animate scroll directive anywhere to hint
that the timeline should instead be scrubbed by scroll position — the
diagram builds as the reader scrolls through it. This is a whole-timeline
decision, so it isn’t attached to any one object; it surfaces as a top-level
scroll: true in the compile bundle (present only when the directive is used).
Scrubbing is the host’s call, so the flag is a neutral hint: the player still returns a GSAP timeline, and the consumer decides how to drive it. Wire it with ScrollTrigger:
import { ready, compile, animate } from '@strategicprojects/rpic';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
await ready();
const { svg, animations, scroll } = compile(src);
stage.innerHTML = svg;
const tl = animate(stage, animations, gsap);
if (scroll) {
tl.pause();
ScrollTrigger.create({ animation: tl, trigger: stage, start: 'top 80%', end: 'bottom 20%', scrub: true });
}
Making objects draggable
draggable <place> is a different axis from the timeline: it makes an object
grabbable in the browser (GSAP Draggable).
Because dragging is interaction, not an authored, play-on-load effect, it is its
own directive rather than an animate … with effect.
draggable N # N can be dragged
draggable N inertia # …and thrown, with momentum
draggable N bounds B # constrain to another object's box
draggable N x # lock to one axis (x or y)
.PS
margin = 0.2
F: box wid 4 ht 2.2 dashed
A: circle "A" rad 0.3 at F.c + (-1,0)
B: circle "B" rad 0.3 at F.c + (1,0)
line from A to B
draggable A inertia bounds F
draggable B inertia bounds F
.PEDrag either node — each throws with momentum (inertia) and stays inside the
frame (bounds F). The connecting line does not follow (rpic emits static
geometry); live graph editing is the host application’s job, for which the
interactions metadata is the starting point.
The compiled bundle gains a top-level interactions array
([{id, kind:"drag", inertia?, bounds?, axis?}, …]) alongside animations —
absent when nothing is draggable, so plain bundles are byte-identical. The
static SVG and geometry are untouched: dragging is a runtime affordance, so
PNG/PDF and non-interactive consumers see exactly the same picture.
Register Draggable (and
InertiaPlugin for
inertia, both free since GSAP 3.13), then wire the objects with the
interactive helper:
import { ready, compile, animate, interactive } from '@strategicprojects/rpic';
import { Draggable } from 'gsap/Draggable';
import { InertiaPlugin } from 'gsap/InertiaPlugin';
gsap.registerPlugin(Draggable, InertiaPlugin);
await ready();
const { svg, animations, interactions } = compile(src);
stage.innerHTML = svg;
animate(stage, animations, gsap);
interactive(stage, interactions, Draggable);