← Back to Patchbay

[ Parable build guide ]

How Patchbay was built

A synth maker whose homepage is a synthesizer. The signature is a draggable SVG patch-cable panel — sagging beziers, a plug-in bounce, LEDs that light per module, and an optional Web Audio voice when the classic patch completes.

Vite + vanilla TypeScriptSVG built from dataWeb Audio, opt-inSpace Grotesk / Inter / JetBrains Monolight + darkreduced-motion aware

The idea

Modular synthesis has one honest sales pitch: you wire the instrument, and no two patches are alike. A screenshot of a rack can't prove that. A rack you can actually patch can — so the centre of the page is a five-module eurorack row where dragging a cable from SAW OUT to AUDIO IN does exactly what it does on the hardware. Complete VCO → VCF → VCA → out and the rack sings. Patch the LFO somewhere weird and it sings weirder. The interaction is the brand argument.

The stack

Vite with vanilla TypeScript — no framework. The panel is a data model (MODULES, each with jacks, knobs and HP widths) rendered to SVG at runtime; the audio is one small Web Audio graph that mirrors the patch state. Space Grotesk carries the display voice (technical but warm), Inter does body text, and JetBrains Mono handles everything a faceplate would print: HP widths, current draw, jack labels. Both themes live as token sets on :root[data-theme] — rack-black by night, panel-silver by day — persisted under the patchbay-theme key.

Signature technique — cables that sag

A patch cable is not a line; it's a line that has given up in the middle. Every cable is a quadratic bezier whose control point hangs below the lower of its two endpoints — sag grows with distance, plus a slack term the animation loop plays with:

/* the cable curve: endpoints + a low midpoint. gravity, roughly. */
function cablePath(ax: number, ay: number, bx: number, by: number, slack = 0): string {
  const dist = Math.hypot(bx - ax, by - ay);
  const sag = Math.min(95, 24 + dist * 0.22) + slack;
  const mx = (ax + bx) / 2;
  const my = Math.max(ay, by) + sag;
  return `M ${ax} ${ay} Q ${mx} ${my} ${bx} ${by}`;
}

/* on plug-in: a damped bounce, then a slow idle sway */
let slack = Math.sin(t / 950 + c.phase) * 2.2;
if (age < 900) slack += 30 * Math.exp(-age / 200) * Math.cos(age / 65);

The bounce is a damped cosine — the cable overshoots and settles like it was actually let go — and the idle sway gives each cable its own phase so a full rack never moves in lockstep. Completion detection is a five-line BFS over the patch graph (with the filter and VCA acting as pass-throughs), so any route from oscillator to output counts, including the rude one that skips the filter entirely.

Details that matter

The build-slot form is a demo — it validates and confirms in place but sends nothing. Wire it to your own endpoint (Formspree, a serverless function, a mailing-list API) before taking real orders.

Ship it on GitHub Pages

Vite builds into docs/ with base: '/patchbay/', so Pages can serve the repo's docs folder with zero extra config. The guide you're reading lives in public/guide/ and is copied through untouched.

npm run build     # tsc --noEmit && vite build → docs/
gh repo create bswxyz/patchbay --public --source . --push
# repo → Settings → Pages → deploy from branch → main /docs

A .nojekyll file in public/ keeps Pages from post-processing the output, and every asset URL resolves under the /patchbay/ base.

← Back to Patchbay