MB-01 · MECHANICAL BALLET · SHEET 2 — NOTES

SHEET 2 OF 2 — DRAWING NOTES

The Guide

How an engineering drawing learned to dance: the full technical breakdown of Mechanical Ballet — gear geometry generated in code, mesh phasing, crank-slider and four-bar kinematics, Geneva equations, and the single GSAP master timeline that conducts the whole ensemble.

00

Concept

The seed was one sentence: a machine that dances. The interpretation: a live engineering drawing, drafted in ink on paper, where every part obeys real kinematics — and where the only thing being “animated” in the usual sense is the tempo. There are no keyframes on any gear, rod or pivot. A single master angle θ advances each frame, and every mechanism’s pose is computed from it, the way an engine follows its crankshaft.

The choreography, then, lives in one place: a GSAP master timeline that eases the machine’s angular velocity through six musical movements — overture, adagio, allegro vivace, a fermata (the machine holds its breath), a rubato passage danced in reverse, and da capo. Because the timeline shapes velocity rather than position, every transition inherits GSAP’s easing curves as physical acceleration — the machine genuinely lunges and settles.

01

Art direction

The aesthetic is a mid-century drafting sheet: cream paper, iron-ink linework, dash-dot centerlines, hatched sections, dimension arrows, a drawing frame with zone coordinates, and a proper title block. Red is rationed — it marks only what moves or what matters: index dots, traced curves, the movement name.

PAPER#F3EEE1
PAPER-2#EAE2CF
INK#232B33
INK-SOFT#66717D
CONSTRUCTION#98A1AB
VERMILLION#C2452D
BLUEPRINT#39597C
Fraunces Italic DISPLAY — OPTICAL SIZE 144, WEIGHTS 300/400
IBM PLEX MONO ANNOTATIONS · DIMENSIONS · SPECS — TRACKED CAPS

Atmosphere comes from three quiet layers: a 56-px graph grid at 5% ink, an SVG feTurbulence grain tile fixed over the viewport in multiply blend, and a soft radial highlight — paper that has seen a workshop.

02

The master timeline

The whole dance is conducted through one number. The ticker integrates angular velocity into the master angle; the timeline only ever tweens tempo:

// one clock — velocity in, pose out
const state = { theta: 0, tempo: 0, gain: 1, surge: 0 };

gsap.ticker.add(() => {
  const dt = Math.min(delta / 1000, 0.05);        // clamp tab-switch jumps
  const w  = state.tempo * state.gain + state.surge;
  state.theta += w * dt;                          // integrate
  updateEnsemble(state.theta);                    // pure function of θ
});

// the choreography — six movements, tempo only, all eased
master
  .call(setMov, [0])
  .to(state, { tempo: 0.95, duration: 2.6, ease: 'power2.inOut' })
  .to({},    { duration: 3.0 })                   // hold the movement
  .call(setMov, [1])
  .to(state, { tempo: 0.26, duration: 2.0, ease: 'power3.inOut' })
  ...
  .call(setMov, [3])                              // FERMATA — breath held
  .to(state, { tempo: 0,    duration: 1.3, ease: 'power4.out'  })
  .call(setMov, [4])                              // RUBATO — danced backwards
  .to(state, { tempo: -0.55, duration: 1.7, ease: 'power2.inOut' });
MOVEMENTTEMPO (RAD/S)EASEHOLD
I — Overture0 → 0.95power2.inOut3.0 s
II — Adagio→ 0.26power3.inOut4.0 s
III — Allegro vivace→ 1.90power2.inOut3.2 s
IV — Fermata→ 0.00power4.out1.5 s
V — Rubato→ −0.55power2.inOut2.6 s
VI — Da capo→ 0.95power3.inOutrepeat ∞

Pause/play never touches the timeline’s internal values — it tweens a separate gain multiplier to 0 or 1, so resuming never jumps. Clicking the machine adds a decaying surge term on top: a sforzando.

03

Drafting gears in code

Every gear is one generated <path>: for N teeth at module m, walk the root circle and lift a trapezoidal tooth to the addendum circle at every pitch — stylized flanks, honest proportions (addendum ≈ m, dedendum ≈ 1.15 m):

function gearPath(N, m){
  const rp = N * m / 2;                // pitch radius
  const ra = rp + 0.95 * m;            // addendum (tip) circle
  const rr = rp - 1.15 * m;            // dedendum (root) circle
  const p  = TAU / N;                  // angular pitch
  const tipHalf = p * 0.16, rootHalf = p * 0.31;
  // per tooth: root arc → flank → tip arc → flank → next root
  for (let i = 0; i < N; i++){
    const a = i * p;
    d += `A ${rr} ${rr} 0 0 1 ${P(rr, a - rootHalf)} `
       + `L ${P(ra, a - tipHalf)} `
       + `A ${ra} ${ra} 0 0 1 ${P(ra, a + tipHalf)} `
       + `L ${P(rr, a + rootHalf)} `;
  }
}

Meshing is the part most fakes skip. Two constraints make it read as real: the speed ratio ω₂ = −(N₁/N₂)·ω₁, and the phase — a tooth of one wheel must meet a gap of the other exactly on the line of centres. With tooth centres at multiples of the pitch, solving that condition gives a closed form:

// driven angle so tooth meets gap on the line of centres
function meshAngle(gA, gB, thetaA){
  const phi = Math.atan2(gB.y - gA.y, gB.x - gA.x);  // centre-line angle
  const r   = gA.N / gB.N;                           // tooth ratio
  return -r * thetaA + phi * (1 + r) + Math.PI - Math.PI / gB.N;
}

The last term, π/N₂, is half a pitch — the “gap, not tooth” offset. Because the phase is derived, gears can be placed at any centre-line angle and they simply mesh. The hero train hangs four wheels off one flywheel this way.

04

Kinematics, three ways

Crank & slider (Act II). The piston’s distance from the crank centre along the bore axis is the classic finite-rod expression — visibly not a sine, which the act plots against a dashed sine for proof:

function sliderDist(θ, r, ℓ){
  return r·cosθ + √(ℓ² − r²·sin²θ);
}

Four-bar linkage (Act III). No angle formulas needed — the rocker pin B is just the intersection of two circles: radius b about the crank pin, radius c about the ground pivot. Pick the branch with the sign argument and the linkage can never dislocate:

function circleIntersect(p1, r1, p2, r2, sign){
  const dx = p2.x - p1.x, dy = p2.y - p1.y;
  const d  = Math.hypot(dx, dy);
  const a  = (r1² − r2² + d²) / (2d);       // distance to chord
  const h  = √(max(0, r1² − a²));           // half-chord height
  const xm = p1.x + a·dx/d, ym = p1.y + a·dy/d;
  return { x: xm − sign·dy·h/d, y: ym + sign·dx·h/d };
}

The coupler point sits off the AB bar (150 along, 118 perpendicular); its path is the red curve. The grey “motion study” ghosts are the same solver sampled at twelve crank angles and drawn once, statically — a drafting convention doing double duty as art. Link lengths satisfy Grashof (shortest bar is the crank), so the crank turns fully forever.

Geneva drive (Act IV). Intermittent motion from continuous input. For a four-slot wheel, the pin circle radius is R = d·sin(π/4); while the pin is engaged (driver within ±45° of the centre line) the star angle is:

function genevaBeta(θd, d, R){
  const cycle = Math.floor((θd + π/4) / TAU);
  const α = Math.min(θd - cycle·TAU, π/4);   // clamp past engagement → dwell
  return cycle·(π/2) + Math.atan2(R·sinα, d − R·cosα);
}

The clamp is the whole trick: past +45° the expression freezes at exactly a quarter turn, giving the 90° index / 270° dwell rhythm — the machine’s staccato.

05

The drawing draws itself

On load, every stroke is dashed to its own measured length and offset fully out, then eased back — so the sheet appears pen-stroke by pen-stroke before the machine wakes. Annotation layers get a second, separate pass: dimension lines, leaders and sense arrows draw themselves the same way, staggered group by group, while patterned strokes (dash-dot centerlines, pitch circles) and text fade — because dashing a dash-dot line would destroy its pattern:

function drawIn(svg, dur, each){
  const geo = [...svg.querySelectorAll('path,line,circle,rect,polyline')]
    .filter(e => !e.closest('[data-nodraw]'));
  geo.forEach(e => {
    const L = e.getTotalLength();
    e.style.strokeDasharray  = L;
    e.style.strokeDashoffset = L;
  });
  return gsap.to(geo, {
    strokeDashoffset: 0, duration: dur, ease: 'power2.inOut',
    stagger: { each }, onComplete: clearDashes   // dashes off → traces work
  });
}

The same routine fires per act on scroll (ScrollTrigger, once: true), and off-screen mechanisms stop integrating entirely — their ScrollTriggers toggle an active flag read by the single ticker.

06

The pinned mesh detail

Sheet 1’s interlude, Pas de deux, is a pinned ScrollTrigger scene: the section locks to the viewport for 2 600 px of scroll and a scrubbed timeline plots a 10:1 close-up of two teeth in mesh, in strict drafting order — blanks and centres, pitch circles tangent at P, the 20° line of action with both base circles, then dimensions and notes. Scroll is the plotter’s motor: reverse it and the construction un-draws.

const anTl = gsap.timeline({
  defaults: { ease: 'none' },          // scrub supplies the easing
  scrollTrigger: {
    trigger: '.pin-stage', start: 'top top',
    end: '+=2600', pin: true, scrub: 0.7,
    onUpdate: self => setPlotMeter(self.progress)  // 000–100% readout
  }
});
anTl.to(gearStrokes, { strokeDashoffset: 0, duration: 3, stagger: .05 }, 0)
    .to(pitchCircles, { opacity: 1 }, 3.4)         // patterned — fade, not dash
    .to(actionLine,   { strokeDashoffset: 0 }, 4.8)
    .to(dimensions,   { strokeDashoffset: 0 }, 6.6)
    .to(anState, { th: 1.15, duration: 6.6, onUpdate: pose }, 3.4);

Two honesty details: the wheels keep meshing while you scrub (same meshAngle phase law as the hero — one θ, both rotors), and the close-up gears are regenerated with more tip clearance than the hero’s (tipHalf 0.115p, addendum 0.78m), because at 10:1 the stylized flanks of the small wheels would visibly interpenetrate. The dashes are never cleared here — a scrubbed drawing must be able to un-draw.

07

The score is an interface

Because the choreography is data (six { ramp, hold, tempo, ease } rows), the site can plot it and play it. The timing diagram samples the exact curve the machine will dance — gsap.parseEase('power2.inOut') returns the easing as a callable function, so the plot is computed from the same numbers the timeline consumes, never traced by hand. A red playhead is driven by master.time() in the shared ticker, but only while the section is on screen.

// each movement gets a label when the timeline is built…
SEG.forEach((s, i) => {
  master.addLabel('m' + i)
    .call(setMov, [i])
    .to(state, { tempo: s.v, duration: s.ramp, ease: s.ease })
    .to({}, { duration: s.hold });
});
// …so a table row is a cue button
row.addEventListener('click', () => {
  if (!playing) setPlaying(true);   // gain eases in — no jump
  master.play('m' + i);
});

The active row is highlighted from inside setMov — the same callback that updates the transport — so the table, the transport and the machine can never disagree about which movement is playing.

08

Replicating it

  1. Static files only — index.html, one stylesheet, one script. GSAP 3.12.5 + ScrollTrigger from jsDelivr, Fraunces + IBM Plex Mono from Google Fonts, versions pinned.
  2. Build a tiny SVG helper (el(tag, attrs, parent)) and generate all geometry at runtime. No image assets exist on this site; the favicon is an inline data-URI gear.
  3. Write each mechanism as a pure pose function of θ. Keep a single integrator; never keyframe positions. Updates touch only transform and geometry attributes — no layout, comfortably 60 fps.
  4. Conduct with one timeline that tweens velocity through eased movements; route pause/play through a separate gain multiplier.
  5. Clamp dt (background tabs), phase gears with meshAngle, and cap traced polylines (420 points here) so the coupler curve stays cheap.
  6. Respect prefers-reduced-motion: skip the draw-in, hold tempo at zero, pose every mechanism at a considered angle, and pre-draw the full coupler curve so the page is still complete at rest. The play button remains — motion becomes opt-in.

09

Colophon

No AI-generated raster assets were used, deliberately — Sheet 1’s general note №1 (“all geometry is generated at runtime, no image assets”) is the site’s manifesto, and a photograph would break it. Every mark on both sheets is procedural SVG or type. Libraries: GSAP 3.12.5 (timeline, ticker, ScrollTrigger pin & scrub). Fonts: Fraunces (display, optical size 144) and IBM Plex Mono. Everything else is vanilla JavaScript, one sheet of CSS, and the kinematics on this page.

Sheet 1 at revision C also carries: a sheet-index nav with the active section underlined and a vermillion scroll-progress rule; a legend of line conventions and a revision-history block in the colophon; quiet parallax on each act’s figure (transforms only); and hover feedback on every spec row, legend entry and cue.

RETURN TO SHEET 1The Drawing
DWG №MB-01SHEET2 OF 2
DATE2026-07-20DRAWNFABLE-5