Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Tweening & Easing — Penner Equations, Tween Runner, Live Curve Visualizer

Live demo · Source

Working code for the article "Tweening and Easing from Scratch: Penner Equations, a Tween Runner and a Live Curve Visualizer". No dependencies; the library side is pure TypeScript math.

Contents:

  1. 31 easing functions (src/easing.ts) — linear + 10 families × (In, Out, InOut), derived from 10 bases via out(f) and inOut(f).
  2. Tween runner (src/tween.ts) — Tween, Sequence, Parallel. Never looks at the wall clock: update(dt) takes time from the outside and returns the UNCONSUMED leftover time.
  3. Live visualizer (src/demo.ts + src/plot.ts) — a clickable grid of the 31 curves, a ball running along the selected curve, a linear reference strip, a duration slider and a sequence(parallel(...), ...) choreography showcase.
  4. Bench (src/bench-cli.ts) — the contract table (f(0), f(1), min/max), call throughput and the measurement of "swallowed remainder" lag.

Install

npm install

Running

npm run dev      # Vite dev server → http://localhost:5173/
npm test         # vitest (headless, no DOM)
npm run build    # tsc --noEmit + vite build
npm run bench    # contract + throughput + lag tables
npx tsc --noEmit # type check only

The demo does not open over file:// — ES module imports do not work, you get a blank screen. npm run dev is required.

What is in the demo

  • Grid (960×520): 31 curves, dashed 0/1 reference lines in each cell. Click → select.
  • Stage (540×560): the selected curve is drawn large, with a red ball running on it. The strip below repeats the same motion with linear — the difference shows side by side.
  • Picking elasticOut makes the ball visibly overshoot the top dashed line (max 1.3731), picking backIn takes it below the bottom line (min −0.1000).
  • Showcase (400×560): a panel sliding with cubicOut + a card arriving with the sequence(parallel(backOut, sineOut), quadOut, elasticOut) choreography. It repeats itself every 2.6 seconds, and there is a "replay the choreography" button too.

Test

npm test

108 tests, 3 files:

  • test/easing.test.ts (89) — f(0)=0 / f(1)=1 for all 31 functions, no NaN, InOut symmetry; the out/inOut derivation identities (including out(out(f)) === f); overshoot tests (backIn < −0.05, backOut > 1.05, elasticOut > 1.3, elasticIn < −0.3, bounceOut ∈ [0,1]); family character (quad→quint stiffness order, expoIn(0.5) === quintIn(0.5), bounce's three trough points).
  • test/tween.test.ts (17) — the expected dt sequence, clamp + returning the leftover time, duration = 0, reset, sequence order/handoff, parallel completing according to the latest finisher, nested choreography.
  • test/demo.test.ts (2) — headless smoke test for src/demo.ts: 120 frames are run with a minimal Canvas2D fake, verifying that the ball crosses the 1 reference line.

There is no vi.useFakeTimers() in the tests: we already produce time ourselves.

Bench output (Node 22, Apple Silicon)

npm run bench prints three tables. Summary:

31 of 31 functions satisfy the contract.
Intentionally exceeding 0-1 range (6): backIn, backOut, backInOut, elasticIn, elasticOut, elasticInOut

function                ms         calls/sec     ns/call
--------------------------------------------------------
linear                 4.8     1,044,659,180        0.96
quadIn                36.7       136,313,447        7.34
quintInOut            71.5        69,948,820       14.30
sineIn                74.5        67,155,371       14.89
expoIn               240.7        20,774,152       48.14
circIn                39.8       125,672,609        7.96
backOut               75.2        66,503,290       15.04
elasticOut           325.9        15,342,149       65.18
bounceOut            104.1        48,033,604       20.82

Swallowed remainder — 10-tween chain, 30 FPS (dt = 1/30 s)
chain                    ideal s  correct frames  swallow frames       lag s
----------------------------------------------------------------------------
10 x 0.20 s                 2.00              61              70       0.300
10 x 0.25 s                 2.50              76              80       0.133
10 x 0.30 s                 3.00              90              90       0.000
10 x 0.35 s                 3.50             105             110       0.167

Timing numbers vary by machine; the table structure and the frame counts (deterministic) do not.

File layout

src/
  easing.ts      # 31 easings: 10 bases + out()/inOut()/family()
  tween.ts       # lerp, tweenValue, Tweenable, Tween, Sequence, Parallel
  plot.ts        # plotCurve (PAD = 0.45, dashed 0/1 lines), plotCell
  demo.ts        # grid + stage + showcase, rAF loop (the ONLY place the wall clock is read)
  benchmark.ts   # contract(), throughput(), swallowLag()
  bench-cli.ts   # CLI that prints the tables
test/
  easing.test.ts
  tween.test.ts
  demo.test.ts
index.html

Lessons learned (also told in the article)

  • The easing contract consists of nothing but f(0)=0 and f(1)=1. Monotonicity is not part of the contract; back and elastic deliberately leave the range. The test verifies this, it does not forbid it.
  • out(f) = t => 1 - f(1 - t) is its own inverse; that is why bounce, whose natural base is Out, also fits into the family() system without trouble.
  • If update(dt) does not return the leftover time, long chains lag. Measured: at 30 FPS a chain of 10 × 0.20 s takes 70 frames instead of 61 (0.300 s).
  • If you do not clamp elapsed to duration, t > 1 happens; values like expoIn(1.4) = 16 throw the object off-screen for a frame.

License

MIT

About

Lightweight animation tween runner and complete Robert Penner easing equation library built from scratch in TypeScript with interactive visual curve debugger.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages