Skill explainer · 001

teardown: how a skill turns “how does this machine work?” into a 3D simulator

You ask Claude how something works. It builds the machine as one offline HTML page where the parts move, the gauges read real numbers, a button opens up the part doing the work, and the page tours itself when nobody is touching it. The same code then records a 60 fps film with a soundtrack timed to the same events. This page covers how the skill is organised so it can do that reliably, and the rendering, animation, UI and audio ideas underneath.

github.com/elvarofficial/teardown @ e7e668c · 2026-10-30 MIT · built on whaleyxbt/claude-motion three 0.186 · esbuild · Playwright · ffmpeg · Python stdlib
PART A

How the skill is organised

What Claude reads, what code it runs, and what it copies. The main lesson: a hard task becomes reliable when judgment lives in prose and mechanics live in code.

A1The repository: who reads each file

Click any file. Only one file in this repo, SKILL.md, gets into Claude's context automatically. Everything else is code Claude runs, code it copies, or code the browser runs.

Where the bytes are (approx., source only)
SKILL.md is about 12 KB of prose, against roughly 390 KB of JavaScript and Python. The prose holds the parts that need judgment, like what to build, the rules and taste, and how to review. The code holds everything that has to come out the same every time: bundling, frame capture, synthesis, mastering, contact sheets.

A2Anatomy of SKILL.md

The order of its sections, and why each one is there. Expand any of them. If you're writing your own complex skill, this is the main pattern to copy.

A3The agent's loop, step by step

What Claude actually does with the skill, from the trigger to the final MP4. agent steps need judgment (Claude thinking and writing code). tool steps are deterministic scripts the skill ships. Steps 9–11 repeat until a full review pass finds nothing.

A4Ten lessons for writing complex skills

Why a skill like this beats a long prompt. Each lesson is tied to the place in teardown where you can see it.

A critique worth learning from. SKILL.md says "Every beat goes into <name>-film/timeline.json. The score reads the same file." But the shipped V8 timeline.json is only {"duration":26}. Its beat times live in film.js's SCRIPT and are typed again by hand in score.py (CLICKS = (3.5, 6.9, 9.7, …), with the comment "Times … mirror v8-shared/film.js"). So --stills beats on this film grabs only frame 0. A rule that no code enforces drifts, even in the author's own examples. If a contract matters, write a script that checks it.
PART B

How the machine runtime works

The architecture every machine follows, with live demos built from the skill's own code and real numbers.

B1One input drives everything

Rules 2 and 3: "Every gauge, chart and label reads from the same function that poses the parts", and "One input drives everything." In the V8 that input is the crank angle. Below, one number (ψ, degrees after this cylinder fires, 0–720°) drives the piston, rod, valves, charge colour, P-V diagram and valve chart, all through the formulas in v8-shared/kin.js with its real dimensions (bore 101.6 mm, crank radius 44.5, rod 145, compression ratio 10.2).

ψ 0°
speed 90°/s
// kin.js: everything is a function of the phase ps. Nothing is keyframed.
export const pistonFrac = (ps) => {                 // 0 at TDC, 1 at BDC
  const a = (ps * Math.PI) / 180;
  const sp = E.r * Math.cos(a) + Math.sqrt(E.rod * E.rod - (E.r * Math.sin(a)) ** 2);
  return (S_TDC - sp) / STROKE;
};
export const volume = (ps) => 1 + (E.cr - 1) * pistonFrac(ps);   // in clearance volumes
export const pressure = (ps) => { /* polytropic compression, fast burn, expansion */ };

The panels in the real page (console.js) import pressure, volume, liftEx and liftIn from the same module that engine.js uses to pose the meshes. A chart can't disagree with the motion because both are computed by the same function.

B2The render(t) contract: why frames must be pure

The capture script doesn't play the page. It calls window.SIM.render(t) for t = 0, 1/60, 2/60…, waits for each screenshot, and the review step jumps straight to --still 12.5. So a frame can't depend on the frame before it. Both dots below should follow the same path. One is computed as x = f(t); the other accumulates x += v·dt like a typical game loop. Try seeking.

How teardown keeps it pure
  • No Math.random and no Date in render. Use the seeded rng(seed) (mulberry32) from kit/util.js at setup, or the stateless hash(n).
  • Anything integrated over time is tabulated at load. film.js integrates the crank angle under a changing RPM into an array TH[] at 240 Hz, and thetaAt(t) interpolates it. The Strandbeest's crank under changing wind is handled the same way.
  • Particles and agents are simulated once into typed arrays; render only reads them.
The same function, two clocks

kit/runtime.js wraps one setup() two ways. In live mode, requestAnimationFrame feeds it (now − start)/1000. In capture mode (window.__CAPTURE), it exposes window.SIM = {render(t), duration, fps, ready} and never loops. Playwright then drives time: for (i…) { await draw(from + i/fps); page.screenshot() → ffmpeg stdin }. Each frame takes as long as it needs to render, so the video plays smoothly at 60 fps even if the machine rendering it can't reach 60 fps live.

B3An exact solver instead of animation: the Strandbeest leg

From strandbeest-shared/jansen.js: Theo Jansen's 11 “holy numbers” plus a 15-unit crank. Each joint is where two circles intersect: it sits at a known distance from two joints already solved. Five intersections solve the whole leg for any crank angle. There's no inverse kinematics, physics or keyframing. Turn the crank and choose a joint to see its two circles.

crank 0°
// the two points at distance r0 from p0 and r1 from p1; pick chooses one
const cc = (p0, r0, p1, r1, pick) => { … };
export function solveLeg(th) {
  const A = [m * Math.cos(th), m * Math.sin(th)];   // crank pin
  const B = cc(A, j, PIVOT, b, hiY);
  const C = cc(A, k, PIVOT, c, loY);
  const D = cc(B, e, PIVOT, d, loX);
  const E = cc(D, f, C, g, loX);
  const F = cc(E, h, C, i, loY);                    // the foot
  return {O: [0, 0], P: PIVOT, A, B, C, D, E, F};
}

B4Auto-tour ↔ visitor handoff

From live.js. Left alone, the live page plays the film's own SCRIPT as a tour, with a fake cursor that presses the real buttons. Any click, drag, wheel or key calls takeOver(). After 25 s with no input, startTour() runs again (6 s in this demo). The camera never jumps: each frame it eases toward its target with k = 1 − e^(−dt/τ), using τ = 0.18 s on tour and 0.45 s when the visitor drives. Click the buttons, or drag the viewport.

AUTO TOUR · CLICK TO TAKE OVER

B5The film is a script of operator moves

v8-shared/film.js doesn't use keyframes. Its SCRIPT is nine moves an operator would make: switch the camera, track a cylinder, open the throttle. Each view is a function of the seconds since it started, and moving between views blends every camera parameter with a 1.6 s smootherstep. The RPM ramp changes how fast the crank turns, and the score places a thud on every firing (one every 90° of crank), so the beat speeds up when the throttle opens. Drag the playhead.

Rows, top to bottom: camera view · tracked cylinder · RPM · score thuds (firings) and clicks (button presses) · where --stills beats would grab review frames (beat + 0.3 s)
export const SCRIPT = [
  {t: 0, cam: 'open'},   {t: 3.5, cam: 'cyl'},  {t: 6.9, track: 6},
  {t: 9.7, cam: 'valley'}, {t: 12.1, rpm: [700, 2400, 1.4]},
  {t: 14.7, cam: 'side'}, {t: 17.3, cam: 'front'}, {t: 19.9, cam: 'hero'}, {t: 22.5, track: 1},
];
PART C

Domain knowledge: rendering, UI, animation, audio

The technical ideas the skill depends on, explained for when you build visual things yourself.

C1What happens in one frame: the post-processing pipeline

The V8 stage (v8-shared/stage.js) doesn't draw straight to the screen. An EffectComposer passes the image through several full-screen passes, each reading the previous pass's texture. Click a pass.

Tone mapping: why the output pass is needed

Lighting is computed in linear HDR, where a softbox can be 3.4× brighter than white. A screen only shows 0–1. Tone mapping is the curve that squeezes the range. Clamping clips highlights to flat white. ACES Filmic (the kit's stage3d.js) rolls them off but shifts colours. Khronos PBR Neutral (the V8 stage) keeps base colours true up to about 0.8 and then compresses. After that comes the sRGB transfer curve, because displays expect gamma-encoded values.

exposure 1.00
Strips: a scene ramp of linear radiance 0 → 4, run through each curve and then sRGB-encoded, in a warm material colour.

C2Cutaways: clipping planes and stencil caps (live three.js)

Rule 4: "The part doing the work opens up." The V8 has three cutaway modes: glass (see-through liners), section (everything past a plane through the tracked cylinder is removed) and solid. A clipping plane makes the GPU throw away every fragment on one side, which leaves the cut parts hollow because there's no geometry on the cut face. The fix is a stencil cap. Toggle caps off in section mode to see the hollow shells. Drag to orbit.

Loading three.js…
cut z 0.00
How the stencil cap works

Picture a ray from your eye through a pixel on the cut plane. If that pixel lies inside a solid, the ray leaves the clipped solid one more time than it enters, so it crosses one more back face than front face. Each solid is drawn a second time with colour writes off, adding +1 to the stencil buffer for every back face and −1 for every front face. Then a flat plane is drawn on the cut with stencilFunc: NotEqual 0, so it only shows where the count is non-zero. That's the solid interior. In engine.js these are the "two stencil twins" on every solid part.

Gotchas from the skill
  • renderer.localClippingEnabled = true lets clipping be set per material.
  • The render target needs a stencil buffer: new WebGLRenderTarget(…, {stencilBuffer: true}), because the composer draws off-screen.
  • Screen-space AO (GTAO) draws the scene again using its own normal material. That material needs the same clippingPlanes, or the cut-away half comes back as a "pale ghost". stage.setCut() handles that.

C3Lighting a studio: softboxes, reflections, shadows

Glossy parts get their look from what they reflect. stage.js builds a tiny scene of glowing rectangles (a 12×12 top box, two tall side strips and a low front strip), and PMREMGenerator bakes it into a pre-filtered environment map: one mip level per roughness. Each pixel below is computed from the same layout. Compare full with plain, and try the skill's rule "roughness ≥ 0.45 and envMapIntensity ≈ 0.5 on big surfaces".

roughness 0.12
envMapIntensity 1.00
Shadow maps: Basic vs PCF vs VSM

A shadow map is a depth picture taken from the light. Each pixel on the floor asks "was something closer to the light than me?" Asking once per pixel (Basic) gives stair-steps at the map's resolution. PCF asks several neighbours and averages, which keeps the edge crisp. VSM stores depth and depth², blurs them, and uses a statistical bound to get wide soft penumbrae. The V8 uses VSM with radius 14, blurSamples 24 and a 4096² map. Its lowest tier falls back to BasicShadowMap.

shadow mapSize 28²
(scaled down so the texels are visible)

C4Easing, with numbers

SKILL.md's "Taste, with numbers" gives the agent exact curves instead of words like "smooth": entrances cubic-bezier(0.16, 1, 0.3, 1), exits cubic-bezier(0.6, 0, 0.9, 0.35) at half the entrance's length, state to state cubic-bezier(0.65, 0, 0.35, 1), and linear only for continuous motion. The film code itself uses smootherstep (6u⁵ − 15u⁴ + 10u³), which has zero velocity and zero acceleration at both ends, so camera blends never jerk.

entrance length 0.90 s
Right: a panel enters, holds for 1 s + 0.25 s/word (the skill's reading-time rule), and exits in half the time.

C5Performance: frame budgets, quality tiers, shader warm-up

At 60 fps a frame has 16.7 ms. live.js watches its own frame time. After a 0.6 s settle, it averages over windows of at least 0.5 s and 6 frames. If the average is over 22 ms (below about 45 fps), the window counts as slow, and one slow window (two after the first 6 s) drops a quality tier. The tier never goes back up. Drag the GPU slider to simulate a laptop's integrated GPU.

GPU desktop
Shader warm-up (kit/warm.js)

three.js compiles a shader program the first time something draws with it, and the first frame waits for each one in turn, which can freeze the page for 1–2 s on a phone. warmComposer() calls renderer.compileAsync() for the scene and every post-pass material (which compileAsync never sees on its own). With KHR_parallel_shader_compile they all compile at once, off the main thread, before the first frame.

C6The overlay: a fixed canvas, scaled, with leaders into 3D

The UI around the machine (console.js) is plain DOM and SVG laid out at a fixed 1600 × 900 CSS px. live.js scales the whole #app with one CSS transform, scale = min(innerWidth/1600, innerHeight/900), so the layout never reflows and the film and the page look identical. Callout leaders connect a fixed label to a moving 3D point. Every frame, the point is projected to screen pixels (stage.project()), and the SVG line is redrawn to that spot.

// stage.js: world point → CSS px. Returns null when the point is behind the camera.
const project = (x, y, z) => {
  pv.set(x, y, z).project(camera);          // → normalised device coords, −1..1
  if (pv.z > 1) return null;
  return [(pv.x * 0.5 + 0.5) * size.w, (-pv.y * 0.5 + 0.5) * size.h];   // y flips: NDC up, CSS down
};

C7Sound from the same model

v8-shared/score.py runs the crank model again in Python. It integrates the same RPM curve at the same 1/20 playback and places a soft 62 Hz thud on every power stroke, plus a click on every button press and a rush of air under the throttle drag. The sfx/ engine is pure Python with no dependencies. It synthesises at 48 kHz, measures loudness with ITU-R BS.1770 (K-weighting, gating) and masters to −14 LUFS, the level streaming platforms normalise to, with a true peak ≤ −2 dBFS so AAC encoding won't clip. Press play to hear the thud-and-click layer (no autoplay). The thud is pitched up here so laptop speakers can play it.

Sound rules from SKILL.md: every time comes from the timeline, never a typed number · accent, never narrate: continuous motion stays silent · one key for every tonal sound.

C8The three.js traps the examples already hit

SKILL.md ends its technical section with bugs the author ran into. It costs only a few lines and saves the agent hours. Here's why each one happens.

→Steal this structure for your own complex skill

teardown's shape, generalised. It fits any skill where the output is an artifact whose quality the agent has to check: reports, slide decks, data pipelines, UI, video.

my-skill/
├─ SKILL.md            # trigger description + playbook
│                      #   first run · examples table · file roles
│                      #   numbered rules · contracts · commands
│                      #   review loop + checklist + stop rule
│                      #   known traps · taste as numbers
├─ paths.mjs           # every binary / dir, env-overridable
├─ tools/
│  ├─ build.*          # deterministic: source → artifact
│  ├─ render.*         # artifact → something the model can SEE
│  └─ check.*          # contracts enforced by code, not prose
├─ kit/                # shared runtime the examples import
└─ examples/
   ├─ a-simple/        # 2–3 complete, *diverse* worked examples
   ├─ b-medium/        #   the agent copies the closest one
   └─ c-hard/
Checklist when you write one
  1. Write the description as the phrases users will actually say.
  2. Can every mechanical step be a script? Make it one.
  3. What's the one contract that keeps everything in sync? Name it, and check it in code.
  4. How will the model see its output? Ship a tool that turns output into images or text.
  5. Write the review checklist and the stop condition.
  6. Turn every taste word ("smooth", "readable") into a number.
  7. Ship examples that cover different shapes of the problem, not three of the same.
  8. Keep a "traps" list and add to it after every painful bug.
Explainer by Claude for skill-explainers · source analysed: elvarofficial/teardown @ e7e668c (MIT). Formulas in the demos are ported from v8-shared/kin.js, strandbeest-shared/jansen.js, v8-shared/film.js, v8-shared/live.js and v8-shared/score.py. The lighting, shadow and tone-mapping canvases are simplified illustrations.