anatomy-asset
Make crafted, highly detailed isometric SVG assets for landing pages, product sites, launch posts, social cards and decks — a hero object, a set of spot illustrations, a feature-tile scene — drawn as real, well-made physical things with many small precise solid parts, four-tone shading, correct painter's order and calm CSS-only ambient motion, mostly monochrome with brand accents. Use this when someone wants a nice-looking illustration, hero image, product visual, spot illustration, icon set, isometric artwork, "Linear-style" or "premium" drawing, or a visual for a section of a site, where the goal is a striking, on-brand picture rather than a diagram that explains how something works. Ships a standalone .svg that works as <img>, inline or in design tools, plus a preview page; can add WebGL shaders or the 3D mode when asked. For interactive figures that explain a technical concept with a live readout, use the anatomy skill instead.
Install
npx skills add https://github.com/Teddarific/skills --skill anatomy-assetVerify
Look at the figure the way a demanding reviewer would, before anyone else does. Rendering it is cheap, and guessing is expensive.
Capture
scripts/capture.mjs starts its own headless Chrome (set CHROME_PATH if it can't find one), loads a file or URL, and clips to .iso-plate by default. It also prints the readout, any console errors or exceptions, and whether the page scrolls sideways.
--select takes any CSS selector; by default it clips to the first .iso-plate. Each --times delay counts from the first shot, so 1200,2400,3600 shoots at about 1.2, 2.4 and 3.6 s after it (a capture takes 60–200 ms, so delays closer together than that run late), saved as name-<ms>.png. --key sends the same key codes as drive.mjs (7 as Digit7, . as Period; write a comma as Comma, since the list is comma separated). The other options are --height (1000), --scale (2), --wait (2500 ms before the first shot), --zoom-scale (4) and --theme dark|light. Read every PNG you make: open it and look.
Both scripts start Chrome with WebGL on (--enable-unsafe-swiftshader --ignore-gpu-blocklist), so shaders show in capture.mjs stills too. Use drive.mjs for anything that has to be timed or slowed down.
Drive
scripts/drive.mjs runs a scripted session in headless Chrome with WebGL on. Use it for sequences: hover, then click, then type, then wait for a phase. Use it also for slowed-down contact sheets that catch pops, and for the no-WebGL fallback. It prints the readout after each shot, then any console errors and exceptions.
Options are --width (1440), --height (1100), --scale (1), --reduced, --theme dark|light, --no-webgl (every getContext("webgl") or "webgl2" returns null, to check the SVG fallback), --preload file.js (a script that runs before the page's own scripts: counters, stubs, hooks, or window.__driveSpeed=0.04 to run slowly from the first frame) and --actions file.json. Selectors default to the first .iso-plate, and fractions run 0..1 across the element.
--theme (in both scripts) only emulates prefers-color-scheme. Kit pages choose their theme when they are built (data-theme on <body>) and ignore that media query, so to check the light theme, build with --light and drive the -light.html file.
Traps:
- Shots are correct after
scroll, so you can scroll to a figure lower on a page. A viewport tall enough for the whole card (--height 1300) is still the simplest: the figure is visible at scroll 0, and itsIntersectionObserverstarts it. - Frame spacing in a sheet is
everyplus the capture time (60–200 ms, more on heavy WebGL pages, where capture also stalls animation frames). For transitions faster than that, put["slow", 0.05]before the sheet and["slow", 1]after it.wait,everyand the page'ssetTimeouttimers stay in real time, so at 0.05 a sheet every 100 ms is 5 ms of page time apart, and an idle tour's resume timer still fires on the wall clock: a key press holds it off for only about 3.6 s of real time, so press a harmless key again before each slowed sheet. An event that starts on load needs the page slow from its first frame:--preloada file withwindow.__driveSpeed=0.04. - Slow mode doesn't slow timers.
setTimeoutruns on the wall clock, so during a long slowed capture an idle tour's resume timer (the Raptor's 3.6 squietTour) can restart the tour and close the engine mid-sheet. Before every slowed sheet stop the tour and wait for rest: preload a stub for that timer (const st = window.setTimeout; window.setTimeout = (f, d, ...a) => (d === 3600 ? 0 : st(f, d, ...a));), pressEscape,["until", "document.querySelector('[data-readout]').textContent.includes('cold')"], and only then the key that starts the transition. A sheet labelled "opening" that starts from a tour that has already opened films a close. - Wait for a phase, don't guess.
["until", "document.querySelector('[data-readout]').textContent.includes('main stage')", 8000]returns as soon as the readout reaches the phase. - A
nullselector is the default. In every action a missing ornullselector means the first.iso-plate(forkey, the focusable[tabindex="0"]), so["sheet", "s.png", 200, 16, null, 0.5]films the card. - Long eval strings break inline JSON quoting. Put anything with
evaloruntilin--actions steps.json. - Pages inline their live code as
<script type="module">, so top-level variables aren't visible toeval. Read state through the DOM instead: readout text,aria-valuetext,data-*attributes. To count frames or read a value per frame, install a hook with--preload(it runs before the page, so it can wraprequestAnimationFrameorHTMLCanvasElement.prototype.getContext).
Line ends
The audit sees recorded shapes, never line markup, so it can't see a seam, rule, rib or tick that stops in the middle of a face. scripts/lines.mjs checks the drawing itself, in headless Chrome:
Every open subpath of every .iso-line in the card (seams, grooves, ribs from fadedSvg, rule lines, ticks, needles, pointers, wires) must end within 1 viewBox px of another stroke, a dot or an outline, or under a fill painted after it. A short straight mark (up to 7 px: a tick) needs one end rooted within 2 px; both ends floating fails. The kit's own outlines, creases, bevels and tube edges aren't checked, and neither are ends that a fadeLineSvg ramp fades out. lineSvg(d, { free: true }) (or "start", "end") marks an end that is meant to stop: rule lines that stand for printed text, a needle's tail, a pointer's tip. Use it for nothing else. It prints each floating end in card px, ready for capture.mjs --zoom, with the start of its d so you can find it in the build; --out rings them on a 2× shot of the card. Run it on both themes and in every pose the figure rests in, and fix every line until it prints 0:
Audit
Screenshots catch what you think to look at. A figure with pipes, cables and round parts also needs the geometry checked, because a cable through a duct or a part drawn over the thing in front of it is invisible at 1× and obvious at 4×. Wire kit/audit.mjs into the build (kit.md, section 12): helpers put into one A.recorder(P, { order, spring }), the build calls A.settle(R, P) and ends with A.auditOrExit(R, P), then run:
It prints audit passed or audit failed: n problems and exits non-zero on any failure. Every line counts, including what happens in mid-flight and items with no shape, except two lists that are there to be read: in passing and free ends apart. Depth order and pass-through (tube against everything, and solid against solid) are both checked on the same replayed frames: every frame of the springs opening and closing until they settle, every frame of a reversal 8 ms either side of each stagger boundary (reversalsOf: 0.027, 0.043, 0.062, 0.078 … 0.393 s on the Raptor's 35 ms stagger), and the prefix states (prefixStates). Depth uses every fourth of those frames. How to read and fix each line:
A zero audit is necessary, not sufficient: it can't see a seam, a staircase or a texture that stops, and it only checks what you recorded. A stand-in shape that silences it is a bug, which is why it fails one. Run the line check, then look.
Orbit (3D figures)
A figure with turning groups (references/3d.md) adds the orbit audit and the browser checks. Run them on the light page and the dark page.
Time. A 28-part figure with one turn, one hinge and one clip took 85 s quick and 17 minutes full, past a 10-minute shell timeout: run the full audit in the background. Quick runs undersample (continuity checks 100 halving candidates instead of 800, and the order scan a tenth of the states), so they guide, and only the full run's turn audit passed counts. A run with --only ends turn audit partial, never passed.
States. The audit sweeps each turn group at 0.3° (order) and 0.05° (continuity), plus 2000 random states and every order-change event ± 0.001°. Motion clips (clips: [{ name, at(t), from, to, frame }]) are walked frame by frame: a gesture, a grasp, a break-apart. Free and hinge groups are proved only on the states you give, so give every clip, at several turn angles.
Lines. separation, unseparated, cycles, forced, swaps, layers, rigid, swept, posed, folds, self, crowding, endon, coverage, depth, continuity. 3d.md, section 13, has each line's failure and fix. A planted fault fails each line: a sunk pad (separation), a pinwheel of three tilted sticks (cycles), a fibre bent in a vertical plane (folds), a tube at 30° (endon), a rib row that stops (continuity).
Browser checks (each runs its control first; a control that doesn't fail means the check is broken):
--order: the recorded shapes in a WebGL z-buffer against the DOM's fills: 0 live pairs with 4+ interior pixels at every angle and clip state.--pops: first difference at every degree (θ → θ + 0.02°) and second difference at 0.1° over a full turn: 0 surviving regions, 0 cores of 6+ px. Read the 1–5 px cores it lists.--lines: 0 floating ends at 24 angles.--perf: the budgets in3d.md, section 15, at 1440 and at 390 with 4× CPU.
Orbit sheets. 0–345° at 15°, then 0.1° steps across the three largest order changes (the audit lists them under "order changes"), then every clip slowed (["slow", 0.05]). Read them for spider angles (a hand that reads as legs from some side), parts that vanish behind others, and lines that end on another part's outline.
Checklist for 3D figures
-
turn audit passedwith the full state set (not--quick, not--only), clips included. -
--orderon--statesfor every hinge and clip,--popson a 0.02° path for every hinge (3d.md, section 14). -
turn-checkorder, pops and lines at 0, fidelity passed, every control failing as designed. - Orbit sheets read at 15°; 8× crops of every knuckle, crossing and texture end at 8 angles, light and dark.
- Materials read as metal on the light page: highlight bands move with the turn, nothing pops at a tone crossfade.
- Every shader anchored to its group follows it at 0.1° steps; world effects don't turn; ink stops at occluding strokes (8× crop where a cover mask meets a part).
- Budgets measured at 1440 and 390 × 4, with the half-rate lever if a dense figure needs it.
Close-ups
The owner found the Raptor's faults on a close-up of the light theme: cables over the main duct, a stepped bell, bolt rows that stopped dead. Shoot that close-up yourself, everywhere, before anyone else does.
- Build both themes (
node build.mjsandnode build.mjs --light) and list the junctions: every flange, tee, elbow, ball joint, clamp, valve, bellows, band, saddle pad and crossing, every curved body (bells, throats, domes, cones, valve bulges, balls), every band joint of a body cut withbandsOf, and every place a texture ends. On flat parts too: every seam, weld, stiffener and trim line on a box face (does it reach both corners?), every row of ticks or marks on a plate (is it on a ruler strip?), every rib row's two ends along its axis, every textured hose at a chunk joint and at its ferrules, every rod that slides (at rest, mid-stroke and at full stroke), and every window or screen with text or shader content in it. - Shoot each area at 4× in both themes, assembled.
--zoom x,y,w,htakes a region in card px and scales it by--zoom-scale(4): - And apart, if the figure comes apart. Drive it there, wait for rest, then shoot a region at scale 4. The Raptor toggles apart on Space and its readout starts
apart ·only once every spring has arrived, soapart.jsonis run withnode scripts/drive.mjs page.html --actions apart.json, then again onpage-light.html. - Look at each PNG for these faults, in this order: a line that runs through or into something; a crossing whose nearer line is drawn behind; a bend, end or clamp sitting on another line's outline; a flange standing off its port or sunk into it; tone blocks stepping across a curved surface, scallops or a pale wedge; a band ending on a straight cut; a row of ribs, dots or rule lines stopping in the middle of a surface; a hairline across a tube's shine or shade at a chunk joint; a seam notching the outline; a seam, stiffener or rib that stops short of the edge it should reach; a short mark on a top that touches nothing; a hose texture with straight hard sides, or one that restarts or is cut at a chunk joint; a rod whose outline breaks while it moves; text that crosses its window; shader content that spills past an opening's rim. Compare with the previous round's shot of the same region.
- Shaders: the same regions during the effect, plus contact sheets slowed with
["slow", 0.05](webgl.md, section 9).
Detail pass. Last, sweep at 4× in a grid of crops (the subject and mechanism for a Figure, the whole drawing for a Hero or an Epic; SKILL.md, workflow step 1) and look for any face, corner or stretch of base with nothing true on it. Every figure is highly detailed (SKILL.md, "Every creation is highly detailed"): add the real parts that belong there (screws, seams, bevels, label plates, scales, the brackets that hold things), never decoration that belongs to no part.
Light-theme shaders
Light is the shipping theme, and on paper a glow is ink. Every light conversion so far failed review first on something a picture of one frame doesn't show: ink that rose while the light fell, a pale flash before the glow, a salmon stage, a fringe beside a stroke. So measure the ink as well as looking at it (webgl.md, section 8).
Ink per frame. Save this as ink.js and pass it with --preload. For every frame the page draws, it logs the mean ink one canvas region lays on white, 255 − lum(rgb + 255 − a), read back in the same task as the draw:
- Choose what to measure. Before the transition, set
window.__inkCanvas(a selector) andwindow.__inkBox([x, y, w, h]in device px of the canvas, measured from its bottom-left; scale CSS px bycanvas.width / canvas.clientWidth) with aneval. Read the rows afterwards with["eval", "window.__ink.join(' ')"]. - Do it once per region that matters: a cavity, the core, an area only the fallback covers. Run it slowed, across first light (
--preloada second file withwindow.__driveSpeed=0.04), every phase change and the fade-out. - What to look for. Ink follows the physical quantity monotonically: up as the light comes in, down as it goes. There is no hump at a hand-off and no dip at first light.
- What it caught. The afterburner's cavity peaked at 3600 on the cut before its fix; afterwards it fell monotonically from 1894 to 155. Those rows are in that figure's own units, a sum over its window region rather than this hook's mean. On the ripple tank's light page the hook reads 14.2 at rest and falls as the lamp comes in, because that page's light brightens the paper.
Colour along the ramp. For anything that heats or cools, eval a pixel read at the centre and the rim at several temperatures (or several temperatures' worth of a slowed run) and write them down. The order must be dull red → orange → yellow → near-white, with the rim cooler than the centre and no salmon or grey stage on the way.
Cross-sections. Read one row of ink across a flame, a jet or a stream. It peaks on the axis and falls to translucent edges. An edge-heavy profile is a hollow, outlined flame.
Strokes at 8–16×. Crop every place where ink meets an SVG stroke (cut faces, rims, mouths, creases, a canvas edge with a body entering) at 8–16× in light. Look for a pale gap between the ink and the line, ink over the line, or a stroke the ink cleared.
Both paths, and nested. The dark build should match its baseline, because every light change is gated on uLight. Render a dark plate on a light page and a light plate on its own, each with and without WebGL: a :not([data-theme="dark"]) rule can reach through the page into a dark plate.
The review checklist
It is an object
- At a glance it reads as one real, well-made thing, and you can name it.
- Its mechanism maps to the concept: what moves and what that means can be said in one sentence.
- It isn't a stack of abstract plates and isn't one plain slab.
Parts and volume
- 30–60 solids for an inline figure; a hero subject that really has more parts can go far past that (the Raptor has about 270). Every part rests on another and has a job.
- Sides shaded and tops lighter; bevels on the main slabs; a halo under the base and under lifted parts.
- Small details belong to parts: screws at the corners of plates, grooves, rulers, knurls, label plates.
Lines
- At 4×, every line ends on a face, another line or a terminal dot. Nothing stops in the air.
- No leader lines, balloons, dimension arrows or floating labels; at most one engraved label.
- Painter's order is right everywhere: nothing behind shows through, springs and frames are split.
- Audit zero. With pipes, cables or round parts in the figure,
node build.mjs --auditendsaudit passed: zero on every failing line, assembled, apart and in every frame of the replayed explode (opening, closing and reversing). No interlock was hidden with a key bias, and every excused pair (allow,crowd) has been looked at at 8×. - No pipe or cable passes through another pipe, cable or solid, and every crossing is drawn in depth order: the nearer line on top, crossing on straights, well apart in depth.
- No line runs across the subject's face where it could run beside the structure; no bend pinches to a V on screen; no bend, end or clamp lands on another line's outline, assembled or apart.
- Every flange sits on its port (on a saddle pad if the port is curved); every collar, clamp, band, bellows and valve gaps the pipe it wraps; every valve and bellows sits on a straight; no route doubles back on screen; every end, apart too, ends on a flange, coupling, connector or terminal dot.
- Junction close-ups. Every junction area shot at 4× in both themes, assembled and apart (see "Close-ups"), and each PNG read.
- No staircase. Every curved surface is smooth-shaded at 4×: no stepped tone blocks, no scalloped terraces, no pale wedge, no band that ends on a straight cut, no kink at a band joint; waisted bodies follow their true outline; seams meet the outline without a nick.
- Line ends zero.
node scripts/lines.mjsprintsline ends: 0on both themes and in every resting pose; everyfreeend is a rule line standing for text, a needle tail or a pointer tip. - Shapes are true. No stand-in shapes: what is recorded is what is drawn, including rods that slide.
- No abrupt texture ends. Every row of ribs, bolt dots, rule lines or grooves ends on a real edge or fades out; none stops at full strength in the middle of a surface, flat faces included. Seams on boxes wrap their corners, rib rows end on their seams, ticks sit on a ruler strip, and a texture on a hose follows its cross-section and runs through every chunk joint. Pipe stripes run onto their end caps and show no hairline at any chunk joint, also on a strongly curved tapered tube (a volute).
Tone and colour
- Base
lo, structuremid, subjecthi, exactly onelitaccent. - Monochrome unless colour is the subject.
Card and words
-
Fig n/ title / hint / live readout are in the corners, with legend keys and a caption under the card. - Every number is true and comes from the model. The readout changes as things move, and its words and numbers come from the same instant: a phase word from the model next to a number from a lagging gauge reads
relief 85 barbefore the valve has cracked. - Text the machine displays fits its window at its widest string and is clipped to it.
Motion and access
- Eased, calm, no snapping; an idle tour; pauses on interaction.
- Stops offscreen; still under
--reduced; no console errors (including React key warnings). - Keyboard works, the focus ring shows, and
aria-valuetextreads the state in words.
Shaders (if the figure has one; references/webgl.md)
- A contact sheet across every transition (start, each phase change, steady, stop), slowed with
["slow", 0.05](or--preloadwithwindow.__driveSpeed=0.04for an effect that starts on load) so every frame of each crossfade shows: no sudden colour switch, no one-frame flash, nothing cut out, and brightness never rises during a fade-out. - 2× and 4× crops where the effect meets the SVG: no seams, hard edges, banding, dark collars or double lines; things on a plane register with that plane's lines.
- The canvas's outer rows and columns read alpha 0 in every phase.
- Without WebGL (
--no-webgl) the SVG alone still reads; under reduced motion you get one still, representative frame; the loop stops when idle and offscreen. - Light theme (
webgl.md, section 8; "Light-theme shaders" above):- Glows read through saturation, with a saturated body, a pale core and a coloured rim, never as dark or brown ink.
- What the light falls on is tinted, and the cavity that holds the light is a darker physical ground.
- Hot bodies run in blackbody order with no salmon stage.
- Smoke and steam are light grey.
- There are no halos in empty air.
- Ink per frame follows the light monotonically through first light, every hand-off and the fade-out.
- Ink stops at each stroke's inner edge at 8–16×.
- The fallback shows the same inks and ground, and hands over to the shader without a dip.
- Dark is unchanged, and a dark plate on a light page still works.
Composition
- Centred, filling about 55–65% of the card; it doesn't touch the corner labels.
- Still readable at 390 px with no sideways scroll, with a part selected and during a burn, not only at rest: the longest readout must not run under the controls.