Build an interactive isometric line-art "figure" (Fig N style) of an object in a single HTML/SVG file: monochrome technical-illustration look, parts you can press that produce real output, live state readout. Use for hero illustrations, interactive diagrams, product figures, and playable desk objects (computer, synth, calculator).
Installs into .claude/skills of the current project.
Are you the author of Iso Figure?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/mrbongoc-iso-figure)
---
name: iso-figure
description: Build an interactive isometric line-art "figure" (Fig N style) of an object in a single HTML/SVG file: monochrome technical-illustration look, parts you can press that produce real output, live state readout. Use for hero illustrations, interactive diagrams, product figures, and playable desk objects (computer, synth, calculator).
---
# Interactive isometric figure
Produces one self-contained HTML file: a rounded panel captioned like a textbook plate, holding an isometric line drawing of an object whose parts actually work. A switch toggles, keys depress, a screen shows what was typed, a synth key makes sound. No libraries, no canvas, no 3D engine, just plain SVG generated by about 15 lines of projection code.
The object must do something real. A figure that only animates is decoration; a figure where pressing a part produces output (text, a number, a sound, a changed display) is the point. Decide what the output is before drawing anything.
## The four things that make it work
1. **Everything is a flat 2D shape placed on a 3D plane.** Never hand-compute skewed polygons. Draw ordinary `<rect>`, `<text>`, `<path>` in a face's local coordinates and let one SVG `matrix()` put it on the plane. Text, glyphs and rounded corners then sit in correct perspective for free.
2. **Objects are boxes; a box is its three visible faces.** Complex objects are boxes plus flat detail drawn on their faces (vents, slots, bezels, LEDs, labels).
3. **Interaction is class toggles on SVG groups.** One `state` object, one `render()`; CSS does the motion.
4. **The frame sells it.** Four monospace corner captions: `Fig N` (top-left), OBJECT NAME (top-right), the instruction (bottom-left), a live state readout (bottom-right).
## Kernel (copy verbatim, then build the scene)
```js
const C = Math.cos(Math.PI/6), S = Math.sin(Math.PI/6), OX = 465, OY = 300; // where the 3D origin lands in the viewBox
const P = (x,y,z) => [(x-y)*C + OX, (x+y)*S - z + OY]; // point: 3D -> screen
const D = (x,y,z) => [(x-y)*C, (x+y)*S - z]; // direction only
// plane = origin O + in-plane axes U,V. Anything inside a <g> with this transform lies flat on that plane.
const plane = (O,U,V) => { const o=P(...O), u=D(...U), v=D(...V);
return `matrix(${u[0]} ${u[1]} ${v[0]} ${v[1]} ${o[0]} ${o[1]})`; };
const TOP = (x,y,z) => plane([x,y,z],[1,0,0],[0,1,0]); // z = const
const FRONT = (x,y,z) => plane([x,y,z],[1,0,0],[0,0,-1]); // y = const (faces lower-left); pass its top-left corner
const SIDE = (x,y,z) => plane([x,y,z],[0,-1,0],[0,0,-1]); // x = const (faces lower-right); pass its top-left corner
const rect = (t,w,h,r=0,cls='face') =>
`<g transform="${t}"><rect class="${cls}" width="${w}" height="${h}" rx="${r}"/></g>`;
// box at (x,y,z), size w along x, d along y, h along z
const box = (x,y,z,w,d,h,r=0) =>
rect(SIDE(x+w,y+d,z+h), d,h,Math.min(r,h/4)) +
rect(FRONT(x,y+d,z+h), w,h,Math.min(r,h/4)) +
rect(TOP(x,y,z+h), w,d,r,'face top');
```
Axes: +x runs down-right, +y runs down-left, +z is up. The viewer sees the top, the `y = max` face (left) and the `x = max` face (right). Put the "front" of the object (screen, display) on a FRONT face and anything the user reaches for (keys, buttons, dials) at larger `y`, in front of it.
Detail on a face: open a group with that face's transform and draw in face-local units, origin at the face's top-left.
```js
svg += `<g transform="${FRONT(70,190,240)}">
<rect class="face glass" x="28" y="31" width="124" height="104" rx="12"/>
<text x="38" y="112" font-size="8.5">> hello</text>
</g>`;
```
## Required CSS
```css
.face { fill:var(--body); stroke:var(--line); stroke-width:1; vector-effect:non-scaling-stroke; stroke-linejoin:round; }
.face.top { fill:var(--deck); }
.detail { fill:none; stroke:var(--detail); stroke-width:1; vector-effect:non-scaling-stroke; }
.press { cursor:pointer; transition:transform 60ms ease-out; }
.press.down { transform:translateY(4px); }
.press.down .face { stroke:var(--live); }
```
`vector-effect:non-scaling-stroke` is not optional. Without it the matrix skews stroke widths and the hairline look is lost. Faces must have an opaque fill: painter's-order occlusion depends on it.
## Procedure
1. **Name the output.** What does pressing a part produce? Typed text, an arithmetic result, a note, a toggled light. Everything else serves that.
2. **Decompose the object** into 3-8 boxes. Write their `(x,y,z,w,d,h)` as a small table first. Pick the pressable parts and the one surface that displays state.
3. **Emit back to front.** Paint order is the only depth sorting: smaller `x+y` first, lower `z` first. For grids (keys, buttons) loop rows by increasing `y`, then columns by increasing `x`. Parts that sit on top of others (black piano keys over white) go after everything beneath them.
4. **Wrap each pressable part in its own `<g class="press" data-…>`** containing its box and its label. A label drawn on the part's TOP plane inside the same group moves with it when pressed.
5. **Frame it numerically.** Project the scene's extreme corners with `P`, then set `OX/OY` and `viewBox` for roughly 8-15% margin on every side. Eyeballing clips tall objects.
6. **Wire state.** One `state` object, one `render()` that toggles classes and rewrites the display and readout. Pointer and keyboard input call the same functions.
7. **Motion in CSS only.** Pressed part: `translateY(4px)` (straight down on screen is −z in iso) over about 60ms, stroke flashing to the live colour. Honour `prefers-reduced-motion`.
8. **Readout** in the bottom-right, terse and lowercase, dot-separated: `off` → `on · 9 chars · key t`. It changes on every interaction.
9. **Verify in a browser.** Load it headless, drive it with real key presses and mouse presses at projected coordinates, read back the readout, and screenshot one interacted state. Check nothing is clipped, nearer parts cover farther ones, and there are no console errors.
## Input patterns
Keyboard and pointer share one handler:
```js
const keys = new Map([...stage.querySelectorAll('.key')].map(k => [k.dataset.key, k]));
function press(k) {
const el = keys.get(k); if (!el) return false;
el.classList.add('down'); setTimeout(() => el.classList.remove('down'), 110);
/* …update state… */ render(); return true;
}
stage.addEventListener('click', e => { const k = e.target.closest('.key'); if (k) press(k.dataset.key); });
window.addEventListener('keydown', e => {
if (e.metaKey || e.ctrlKey || e.altKey) return;
if (press(e.key.length === 1 ? e.key.toLowerCase() : e.key)) e.preventDefault();
});
```
For parts that are held rather than tapped (instrument keys): use `pointerdown` / `pointermove` / `pointerup` on the svg with `setPointerCapture`, find the part under the pointer with `document.elementFromPoint(...).closest('.key')` so dragging across parts works, set `touch-action:none` on the svg, ignore `e.repeat` on keydown, and release everything on window `blur`.
For text typed onto a screen: show only the tail of the buffer, clip with a `<clipPath>` matching the glass rect, and escape `&` and `<` before writing into the SVG.
For sound: create the `AudioContext` inside the first press handler (browsers block it before a gesture), route through one master gain into an `AnalyserNode`, and draw the analyser's time-domain data as a `<polyline>` on the screen face each animation frame, starting from a rising zero crossing so the trace holds still.
## Look
- Define every colour as a CSS custom property and provide both a dark and a light set; the drawing works in either. Dark: panel `#141516` on page `#0c0d0e`. Light: panel `#f6f6f5` on page `#e9eaea`.
- Two greys only for linework (structure and detail). One near-white (near-black in light theme) `--live` colour reserved for whatever is live: lit display content, the pressed key, the selected button. That contrast is the whole effect; do not add hue, gradients or shadows.
- Two-tone parts (white and black keys, digit and operator keys) are allowed as light fill with dark stroke versus dark fill with light stroke.
- Captions: monospace, 13px, wide letter-spacing, uppercase except `Fig N` and the readout. Lay the two caption rows out with flex and wrapping so they survive phone width.
- Small radii on boxes (2-14 units). Side-face radius is clamped to `h/4` in the kernel so thin slabs don't turn into lenses.
- Texture from repetition, not shading: vent slits, key grids, ribbed lines.
## Don'ts
- No real company logos, product silhouettes or trademarked marks on the object or its screen; use the owner's own mark or an invented glyph.
- No hand-written skewed polygon coordinates. If you are typing point lists for a face, use `plane()` instead.
- No external scripts; the file must run opened from disk.
- Don't build something you only watch. If the user can't press it and get output, pick a different object.
- Give the svg `role="img"`, `tabindex="0"` and an `aria-label` stating what it is and how to operate it.