← Back to Halfstep

[ Parable build guide ]

How Halfstep was built

One e-bike, taken apart by the scrollbar. The signature is an exploded-assembly scroll: an inline-SVG service diagram whose six assemblies fly out into a labelled exploded view as you scroll, then bolt themselves back together — plus a configurator that repaints every diagram on the page live.

Vite + vanilla TypeScriptinline SVG onlyArchivo / Manrope / Space Monolight + darkreduced-motion honest

The idea

Every e-bike site shows you a glamour render and a spec table, far apart, hoping you won't compare them. Halfstep's voice is "specs stated plainly" — so the centrepiece states them on the parts themselves. A service diagram is the most honest drawing a bike company can publish: if it comes apart cleanly in the diagram, it comes apart cleanly in your kitchen. The scroll direction maps to disassembly, the labels are printed where the torque actually happens, and reassembly at the end is the quiet warranty pitch.

The stack

Vite with vanilla TypeScript — no framework. The page is static HTML with a handful of small modules: explode.ts (the flourish), configurator.ts and rangecalc.ts (which share one battery value through a ten-line state.ts), reveal.ts, theme.ts, form.ts. The bike itself is hand-plotted inline SVG — strokes with round caps for tubes, circles for wheels — so the paint colour is just a CSS variable (--paint) the configurator flips with data-paint on <html>. Type is Archivo at 900 for the engineered shouting, Manrope for body, Space Mono for everything a torque wrench would say.

Signature technique — the exploded-assembly scroll

The build section is a tall scroller (~3.4 viewports) with a position: sticky stage pinned inside it. Scroll progress p maps to three phases — fly out, hold the labelled diagram, fly home. Each part is an SVG <g> with an exploded offset and a stagger delay, so the wheels leave first and return last, like an actual teardown:

// global explode amount 0 → 1 → 0 across the three phases
let eRaw: number;
if (p < 0.4)      eRaw = easeInOutCubic(p / 0.4);        // coming apart
else if (p < 0.6) eRaw = 1;                              // exploded, labelled
else              eRaw = 1 - easeInOutCubic((p - 0.6) / 0.4); // reassembling

for (const w of wired) {
  const local = clamp(eRaw * (1 + STAGGER) - w.def.delay, 0, 1);
  const e = easeOutCubic(local);
  w.node.style.transform =
    `translate(${dx * e}px, ${dy * e}px) rotate(${rot * e}deg)`;
  // the callout's leader line chases its part
  w.line.setAttribute('x2', String(ax + dx * e));
  w.line.setAttribute('y2', String(ay + dy * e));
}

Because CSS pixel translations on SVG children resolve in user units, the same offsets work at every screen size. The callout leader lines are authored in the markup pointing at the assembled anchors, so with JavaScript disabled — or reduced motion on — the diagram is simply a labelled side view. The belt and ground shadows fade out with 1 − eRaw, because exploded parts don't cast shadows.

Details that matter

The reserve form is a demo — it validates and confirms in place but sends nothing. Wire it to your own endpoint (and a real deposit flow) before taking anyone's €100.

Ship it on GitHub Pages

Vite builds into docs/ with base: '/halfstep/', so Pages can serve the project subpath straight from the branch:

npm run build   # tsc --noEmit && vite build → docs/
gh repo create bswxyz/halfstep --public --source . --push
gh api --method POST /repos/bswxyz/halfstep/pages \
  -f 'source[branch]=main' -f 'source[path]=/docs'

The public/ folder carries .nojekyll and this guide verbatim into the build, so the whole site — diagram, configurator, guide — is one push.

← Back to Halfstep