Docs

Flipbook Foundry is an animation library where every frame is a glyph. Actions are baked into fonts at build time; at runtime a figure is one character that gets swapped for the next drawing. The API borrows framer-motion's vocabulary. If you know animate, variants and stagger, you know this.

npm i flipbook-foundry

// Svelte
import { Stage, Figure, TextFlow, FlipbookConfig } from 'flipbook-foundry/svelte';
// React
import { Stage, Figure, TextFlow, FlipbookConfig, useAnimate } from 'flipbook-foundry/react';
// Anything else
import { createStage, animate, stagger } from 'flipbook-foundry';

// Fonts load from jsDelivr, pinned to your installed version, and only the
// chunks your figures use. To self-host, copy node_modules/flipbook-foundry/fonts/
// to your public folder and call setFontBase('/fonts/') once at startup.
// Preload with getLibrary().load(['regular', 'bold']).

Page

The simplest way in. Write a sentence and drop actions into it as {walk}. pretext lays the paragraph out once, with words in your font and figures in the Flipbook Stick font, all at text size. Every drawing of an action has the same width, so animating a figure only swaps its character and the layout never moves.

<Page
  text={'Bob {walk} left the house. Sue {wave} waved. The dog {run} gave chase.'}
  font="400 20px Georgia"   // any CSS font for the words
  figureScale={1.3}         // figure size relative to the text (default 1.3)
  align="center"            // or left
  phase={({ x, width }) => x / width}   // loops start by position: a wave
  bind:page                 // page.figures[i] are Characters you can direct
/>

{trip>get-up>dust-off}   a sequence, looped
{idle|sneak|run}         a figure that may switch between these
                         (room is reserved for the widest, so the line never moves)
a newline                starts a new paragraph

Figure

The motion component. On its own it renders inline: a single character in a line of text that you can style with CSS like any other. Inside a Stage it draws on the canvas instead, so thousands can share one frame.

<Figure
  animate="walk"            // action or variant to play; change it to play another
  initial="sit-idle"        // shown first; routes from here into animate
  exit="bow"                // played before removal from a Stage
  transition={{ onTwos: true }}
  whileHover="wave"         // inline only (Stage figures: use stage.hitTest)
  whileTap="jump"
  x={120} y={300}           // Stage px; y is the ground line under the feet
  size={64}                 // font size in px; the figure is ~0.7 of it
  facing={-1}               // 1 faces right, -1 mirrors
  layer="bold"              // regular | bold | limbs | limbs-bold
  body={{ char: 'A', font: '700 10px Georgia' }}   // a letter for a body
  onAnimationComplete={(action) => …}
  onEvent={(event, action) => …}                    // 'hit', 'land', 'release'…
/>

Transition

Passed as transition, inside a variant, or as animate()'s options.

fpsnumberFrames per second. Defaults to the action's authored rate (usually 24).
speednumberRate multiplier on top of fps.
onTwosbooleanHold each drawing for two frames, the hand-drawn flipbook cadence.
delaysecondsWait before starting. In animate(), may be a stagger() function.
repeatnumberExtra plays. Loops default to Infinity, one-shots to 0.
repeatType'loop' | 'reverse' | 'mirror'Restart, or ping-pong the drawings.
repeatDelaysecondsHold between repeats.
phase0–1Start a loop partway through, to desynchronise a crowd.
inPlacebooleanIgnore baked root motion: treadmill instead of travel.
interrupt'settle' | 'cut'How to leave what's playing. 'settle' (default) finishes a one-shot or plays a loop to its next exit, then settles to rest. 'cut' switches at once.
routebooleanRoute through hub transitions first (default true).

Variants

Name your states, then animate between names. A variant is an action name or a target with its own transition, a destination x, a facing, and what to do then.

const moods = {
  calm: 'idle',
  happy: { action: 'cheer', transition: { repeat: 2 }, then: 'calm' },
  leave: { action: 'walk', x: 900, then: 'wave' },   // travel to x, then wave
};

<Figure variants={moods} animate={mood} />

Stage

A canvas world. Each figure is one fillText of its current glyph, depth-sorted by its ground line and skipped when offscreen; a stage that's scrolled out of view stops painting. A thousand figures draw in a few milliseconds.

<Stage
  background={null}         // or a colour; null leaves the canvas transparent
  layers={['regular']}      // fonts to load before first draw
  onUpdate={(frame, stage) => …}      // game logic, every frame
  onBeforeDraw={(frame, stage) => …}  // draw terrain under the figures
  onAfterDraw={(frame, stage) => …}
>
  …figures, text flows…
</Stage>

// The Stage instance (bind:stage in Svelte, onReady / useStage in React):
stage.add({ x, y, size, facing })         // → Character
stage.remove(character, 'bow')           // exit action, then gone
stage.addProp({ text: 'A', font: '700 40px Georgia', attach: { to: c, anchor: 'handR' } })
stage.hitTest(x, y)                      // topmost Character or null
stage.toStage(clientX, clientY)          // pointer → stage space
stage.camera = { x, y, zoom }            // scroll and zoom big maps
stage.stats                               // { fps, characters, drawn, updateMs, drawMs }

Things & props

Glyphs don't have to be people. The Things font holds props and set pieces, some with frames of their own, so a sack can fill and a hole can deepen. A held tool is drawn at the figure's hand and turned by the forearm angle baked into every frame, so one shovel works with every action. Events like scoop, toss, grab and release are the beats you sync to.

// Things are glyphs too: props and set pieces in the Flipbook Stick Things font.
// shovel · broom · sack (12 fullnesses) · hole (12 depths) · pile (12 sizes) · wall · clod
await getLibrary().load(['regular', 'things']);

const sack   = stage.addThing({ name: 'sack', x: 600, y: ground, size: 120 });
const wall   = stage.addThing({ name: 'wall', x: 900, y: ground, layer: 'front' }); // hides who's behind
const shovel = stage.addThing({ name: 'shovel', size: 120 });

stage.hold(shovel, digger, 'R');      // tools turn with the forearm, every frame
stage.hold(sack, thief, 'L', true);   // sacks hang; true draws it behind the figure
stage.drop(sack);                     // lands under the hand
sack.frame++;                         // a drawing fuller

// Throw anything along an arc: a stolen letter, a clod of earth.
const letter = stage.addProp({ text: 'A', font: '700 54px Georgia', x, y });
await stage.fly(letter, { to: [sack.x, sack.y - 40], height: 90, spin: 1.5 });

// Real letters as objects: where each one is, so you can take it.
flow.glyphs();   // [{ index, char, x, y, width }, …]  then flow.setText(without it)

TextFlow

Paragraphs in any font, laid out by pretext. Measurement happens once, so wrapping is arithmetic and can run every frame around moving figures.

<Stage style="height: 420px">
  <TextFlow
    text={copy}
    font="400 22px Georgia"   // any CSS font; measured once by pretext
    x={0} y={0} width={720} lineHeight={32}
    padding={10}               // breathing room around figures
    avoid                      // re-wrap around every figure, every frame
  />
  <Figure animate="run" x={0} y={160} size={64} />
</Stage>

// Without components:
const flow = createTextFlow({ text, font, x, y, width, lineHeight });
flow.layout(obstacles);  // Rect[] → fragments, pure arithmetic
flow.draw(ctx);
flow.ledges();           // word tops: places to stand, sit or hang

animate() & stagger()

import { animate, stagger } from 'flipbook-foundry';

animate(bob, 'jump');                                // → controls (thenable)
await animate(bob, 'walk', { x: 400, then: 'wave' }); // travel, then settle
animate(crowd, 'cheer', { delay: stagger(0.04, { from: 'center' }) });

// Sequences, with framer-motion's `at`:
animate([
  [bob, 'walk', { x: 260 }],
  [sue, 'walk', { x: 340, at: '<' }],     // same time as previous
  [[bob, sue], 'high-five'],              // after both arrive
  [bob, 'cheer', { at: '+0.3' }],         // 0.3s after
]);

const c = animate(bob, 'dance');
c.stop();  await c.finished;

Character

The state behind every figure. It never draws; renderers read glyph.

const c = createCharacter({ x: 0, y: 300, size: 64, animate: 'idle' });
c.animate('trip');                 // interrupts, routes hubs, resolves when done
c.pose('jump', 14);                // freeze on a drawing
c.on('hit', () => shake());        // baked events: hit, land, release, grab…
c.on('frame', ({ action, frame }) => …);
c.anchor('handR');                 // [x, y] stage px — where to put the letter
c.bounds();                        // ink box, for hit-tests and text flow
c.glyph;                           // the current character to draw
c.use(wander(), greet());          // behaviours

// Compose: actions play in order; rest-pose routes (get-up, stand-up…)
// are inserted for you. Loops play one cycle each.
c.sequence(['sneak', 'trip', 'dust-off', 'wave'], { loop: true });

Behaviors

Small brains for crowds. They run once per frame and only ever call animate().

import { wander, greet } from 'flipbook-foundry';

stage.add({ x, y }).use(
  wander({
    area: [0, 1200],
    gaits: ['walk', 'walk', 'jog', 'skip'],
    idles: ['idle', 'look-around', 'yawn', 'dance'],
    pause: [1, 4],
    clumsiness: 0.1,            // chance of a trip on the way
  }),
  greet({ radius: 70, actions: ['wave', 'high-five', 'bow'] }),
);

// A behaviour is just (character, stage, now) => void. Write your own:
const flee = (c, stage) => { if (near(c, cat)) c.animate({ action: 'sprint', x: 0 }); };

React hooks

const animate = useAnimate();       // animate() that stops on unmount
const stage = useStage();           // enclosing Stage (null until fonts load)
useStageFrame((frame, stage) => …); // per-frame logic
const reduced = useReducedMotion(); // honours prefers-reduced-motion

<Figure ref={setBob} animate="idle" />   // ref → Character

FlipbookConfig

Defaults for every Figure below it, like framer-motion's MotionConfig.

<FlipbookConfig
  family={stick}            // figure family (default: stick)
  transition={{ onTwos: true }}
  layer="bold"
  size={48}
  reducedMotion="user"      // 'user' | 'always' | 'never' — freezes figures on frame 0
>
  …
</FlipbookConfig>

Families

A family is a set of figure fonts and the manifest of actions drawn in them. Each one is its own package; flipbook-foundry ships with @flipbook-foundry/stick as the default, so you only name a family to switch or self-host. Families load their fonts from jsDelivr, pinned to the installed version.

import stick from '@flipbook-foundry/stick';
import { getLibrary, setDefaultFamily, setFontBase } from 'flipbook-foundry';

setDefaultFamily(stick);              // the family used when none is given
<Stage family={stick}>…</Stage>        // a Stage's Figures draw in its family
<Page family={stick} text={…} />
<Figure family={stick} animate="wave" />   // inline figures

getLibrary(stick).names();            // that family's actions
setFontBase('/fonts/', stick);        // self-host its fonts

Hubs & routing

Every one-shot starts and ends on a rest pose, called a hub: stand, crouch, sit, lie, perch (on a ledge) or hang (from one). Ask a lying figure to wave and it plays get-up first. Ask a standing one to sit-idle and it plays sit-down. Ledge actions move the ground line for you: sit-on puts a figure on a ledge, hop-off drops it back to the floor.

Loops pass through the same rest pose. Each one has exit frames, at most half a second apart, with a short settle into rest baked from each. A new action waits for the next exit, settles, and starts; a one-shot finishes first. Loops start on their calmest frame. Nothing cuts mid-pose unless you pass { interrupt: 'cut' }.

Action reference

locomotion

walk24f @24loopAn easy, bouncy stroll.
run16f @24loopLeaning, arm-pumping run with a flight phase.
jog20f @24loopEasy jog with a small float between steps.
sprint12f @24loopAll-out sprint, deep lean and high knees.
sneak32f @24loopHunched cartoon sneak, high careful steps.
tiptoe16f @24loopDainty, upright tiptoe on stretched legs.
march24f @24loopProud parade march, high knees, stiff arm swing.
skip24f @24loopHappy step-hop skip with big arm pumps.
backpedal24f @24loopNervous walk backward, arms up in front.
skid-stop26f @24stand → standcontactBrake out of a run: lean-back skid, dust, wobble, stand.
start-run16f @24stand → standRock back, coil, burst into a run (ends on run frame 0).
shuffle-step16f @24stand → standOne small step forward, as in a queue.
sneak-off32f @24loopBig cartoon tiptoe: knee to the chin, hold, toe down, paws up.

aerial

hop12f @24looplaunch landSpringy little hop in place, looping.
jump34f @24stand → standlaunch landWind up, spring high, tuck, land with a squash.
leap34f @24stand → standlaunch landRunning split leap that sails forward.
frontflip34f @24stand → standlaunch landTucked frontflip, sticks the landing.
backflip34f @24stand → standlaunch landTucked backflip, sticks the landing.
fall16f @24loopFlailing, tumbling fall (loops while airborne).
land26f @24stand → standlandHard landing: crunch, dazed wobble, stand.

pratfall

trip42f @24stand → liecontact landSnag, flailing dive, belly-skid, flip over onto the back.
tumble32f @24stand → standlandForward roll that travels, pops up to standing.
slip28f @24stand → liecontact landBanana-peel: feet fly up, thud onto the back.
stumble34f @24stand → standcontactCatch a toe, windmill, recover.
faceplant42f @24stand → crouchlandTopple forward like a plank, splat, push up to a crouch.
topple30f @24stand → lieimpactStiff as a board, falls backward like a domino.

transition

sit-down22f @24stand → sitcontactLower down, hands back, plop onto the floor.
stand-up22f @24sit → standPull the knees in, rock forward, rise.
lie-down28f @24stand → liecontactSit, then ease back flat.
get-up32f @24lie → standcontactKick, rock up, scramble to the feet.
crouch-down12f @24stand → crouchDrop into a crouch with a little overshoot.
rise14f @24crouch → standSpring up from a crouch, settle tall.
climb-up28f @24hang → standHang → pull up → knee over → stand on the ledge. Ledge is y=0; ends 190 forward.
hop-off20f @24perch → standlandScoot forward off a ledge onto the floor 180 below. Ledge is y=0.
sit-on20f @24stand → perchcontactBack onto a ledge behind and perch. Ledge is y=0; floor is 180 below.

idle

idle32f @12loopBreathing, settling weight, alive but still.
look-around48f @24stand → standShades the eyes, peers forward, then cranes up.
tap-foot24f @24loopHands on hips, impatient foot tapping.
yawn40f @24stand → standBig full-body stretch-yawn.
sleep32f @12loopAsleep on the back, slow breathing.
fidget40f @24stand → standScratches head, shifts weight, scuffs a foot.
sit-idle32f @12loopSitting on the floor, knee bobbing, glancing around.
perch-idle24f @24loopSitting on a ledge, legs dangling and swinging.
hang-idle32f @24loopHanging from a ledge by both hands, legs swinging.
type24f @24loopkeyHunched over an invisible keyboard, pecking away.
sweat32f @24loopNervous: wipes the brow, tugs the collar, glances about.
lean-back32f @12loopLeaning back on a wall, arms folded, ankles crossed.
hang-swing32f @24loopHanging from a ledge, swinging the legs like a pendulum.
search40f @24loopBent over, hand shading the eyes, scanning the ground.

gesture

wave40f @24stand → standBig, friendly, whole-body wave.
point32f @24stand → standpointWind back and jab a finger forward, leaning in.
beckon24f @24loopCome-here curl of the arm, head cocked.
shrug30f @24stand → standWho knows? Palms up, head tipped.
clap16f @24loopclapEnthusiastic applause.
salute30f @24stand → standSnappy military salute.
bow40f @24stand → standDeep, theatrical bow.
think32f @12loopHand to chin, pondering.
facepalm40f @24stand → standhitHand to face. Sigh.
peek36f @24stand → standLeans out head-first as if round a corner, holds, snaps back.
dust-off32f @24stand → standBrushes off shoulder and knees, then a small proud nod.
peek-out48f @24stand → standout inHiding behind a wall: lean out, look about, duck back, peek again lower.
wave-hello64f @24stand → standTurn to face the viewer, wave, turn back.

emote

cheer20f @24looplandJumping for joy, arms in a V.
laugh16f @24loopDoubled over, belly-shaking laughter.
cry24f @12loopFace in hands, sobbing.
panic12f @24loopArms flailing, running on the spot.
angry16f @24loopstompFists clenched, stomping tantrum.
dance32f @24loopFour-beat groove: disco point, arm roll, shimmy.
flex36f @24stand → standDouble-biceps pose with pumps.
shiver8f @24loopHunched and hugging, shivering.
victory30f @24stand → standlandLeaping fist-pump, landing with a YES.
exhausted48f @24stand → standDoubles over, hands on knees, gasping — then recovers.
huddle16f @24loopFreezing: arms wrapped round, shivering, stamping from foot to foot.
cover16f @24loopArms over the head against the rain, hunched and hurrying in place.
wobble32f @24loopTeetering on an edge, arms windmilling, one foot up for balance.
rub-hands16f @24loopScheming villain hand-rub, hunched and bouncing.

interaction

push32f @24loopLeaning hard into something, shoving it forward.
pull24f @24loopLeaning back and hauling, stepping backwards.
carry28f @24loopWalking with something held overhead.
lift40f @24stand → standgrab liftedSquat, grab, and hoist something overhead.
throw32f @24stand → standreleaseOverhead wind-up and throw.
catch28f @24stand → standcatchReady, catch, absorb the impact.
kick28f @24stand → standhitBig wind-up and a long snapping kick.
punch22f @24stand → standhitChambered straight punch.
high-five24f @24stand → standhitUp high! Slap and recoil.
lean32f @12loopCasually propped against a wall.
knock30f @24stand → standhitKnock-knock-knock, then listen.
dig32f @24loopscoop tossShovel in hand: drive, stamp, lever, and pitch the dirt over the shoulder.
scoop28f @24stand → standgrabBend, snatch something off the floor, and peer at it.
toss-back22f @24stand → standreleaseFling something back over the shoulder without looking.
scoop-toss36f @24stand → standgrab releaseSnatch something off the floor and pitch it over the shoulder in one go.
drag28f @24loopHunched, heavy walk hauling a sack behind.
erase20f @24looprubScrub away at something in front with an eraser, overhead down to the knees and back.

Authoring actions

Actions are written as sparse pose keys on a skeleton with fixed bone lengths. Angles interpolate along splines, so hands sweep arcs instead of cutting corners. At bake time a solver plants the lowest foot, derives root motion from it so nothing skates, and adds speed lines and impact puffs. Then every frame is written into the fonts. The skeleton exists only at build time.

// packages/rig/actions/emote.ts
export const shrug: ActionDef = {
  name: 'shrug', category: 'gesture', description: '…',
  frames: 30, fps: 24, loop: false, from: 'stand', to: 'stand',
  keys: [
    { at: 0, ...STAND },
    { at: 8, armL: [-20, 120], armR: [20, 120], head: -10, ease: 'snap' },
    { at: 20, head: 4, event: 'shrugged' },
    { at: 29, ...STAND },
  ],
};

// npm run build:font   → bakes every action into the fonts + manifest
// npm run sheet shrug  → contact sheet in .sheets/shrug.png
// /forge               → pose it by hand, copy the ActionDef