[ Parable build guide ]
How Dovetail was built
A furniture studio, drawn the way a cabinetmaker draws. The signature is a canvas orthographic line-drawing of a lounge chair that rotates front → 3/4 → side as you scroll or drag — no textures, no 3D library, ~120 lines of vanilla JS.
The idea
Furniture photography flatters. A drawing tells the truth — it can't hide a proportion or a joint behind soft light. Dovetail leans all the way into that: the whole site is styled like a sheet of drawing paper, blueprint grid and all, and the hero is a dimensioned elevation rather than a hero shot. The promise is precision, so the site had to look measured.
The stack
Deliberately plain. One index.html, one styles.css, one
main.js. No framework, no bundler, no CDN — the rotating drawing is hand-rolled canvas
2D, so the page has zero runtime dependencies and ships straight to GitHub Pages. Type is Fraunces
(a warm, high-contrast serif with a craftsman's confidence) for display, Inter for body, and
Space Mono for the technical meta a furniture drawing lives in — millimetres, Janka numbers,
plate references.
Signature technique — the rotating elevation
The lounge chair is a tiny 3D wireframe — a seat slab, a reclined back and four
splayed legs, built from boxes and prisms as arrays of edges. To draw it as a draughtsman would, we
rotate every point about the vertical axis and project it flat with no perspective:
drop nothing, foreshorten nothing, just spin and read off x and y. At 0°
you see the front elevation; at 90°, the side.
// spin about the vertical (Y) axis, then project orthographically
const project = (p, s, c) => ({
x: p[0] * c + p[2] * s, // s = sin(angle), c = cos(angle)
y: p[1], // height is untouched -> H is constant
depth: -p[0] * s + p[2] * c // kept only to fade far lines
});
// scroll maps the section's progress to the target angle...
target = p * (Math.PI * 0.56); // 0deg -> ~101deg
// ...and every frame eases toward it (a soft-close drawer):
angle += (target - angle) * 0.12;
Because height is never rotated, the overall-height dimension on the left stays a constant
820 mm, while the bottom dimension recomputes the projected width live — so the
read-out ticks up from the chair's width toward its full depth as it turns. The four material chips
swap the stroke colour (theme-aware, so walnut never disappears on a dark ground) and rewrite the
spec caption underneath.
Details that matter
- Cost, not vanity. Device-pixel-ratio is capped at 1.5 and the loop is throttled — line-drawing is cheap, and when the angle has settled there's nothing to redraw.
- It pauses when unseen. An
IntersectionObserverstops the render loop the instant the drawing scrolls out of view, and restarts it on the way back. - Reduced motion shows one clean elevation. Under
prefers-reduced-motionthere is no loop and no scroll rotation: the chair is drawn once as a still side elevation, and the material swap still works — instantly, with no transition. - Content is never hidden without JS. The hero rise and scroll reveals are gated
behind a
.jsclass set synchronously in<head>, and the hero's own elevation is a plain inline SVG — kill JavaScript and the page is still fully legible. - Two drawings, one language. The hero is a hand-built SVG side elevation; the joinery section is the live canvas. They share the same grid, ticks and mono labels, so the site reads as one drawing set.
The commission form is a demo — it validates and confirms in-place but sends nothing. Wire it to your own endpoint (Formspree, a serverless function, an email service) before taking real orders.
Ship it on GitHub Pages
Nothing to build. Push the folder and point Pages at the root.
gh repo create bswxyz/dovetail --public --source . --push
gh api --method POST /repos/bswxyz/dovetail/pages -f 'source[branch]=main' -f 'source[path]=/'
Relative paths and a .nojekyll file mean it serves from the project subpath without a
single config change. That's the whole point of the static build — the drawing does the work.