← Back to Conche

[ Parable build guide ]

How Conche was built

A chocolate maker's whole craft is invisible by the time you hold the bar. Conche makes it visible twice: an origin flavor-wheel whose arc geometry is computed at build time, and a tempering curve that draws itself out of the batch log as you scroll.

Astro 5 · zero client frameworkbuild-time SVGPlayfair / Karla / Space Monolight + darkreduced-motion honest

The idea

Bean-to-bar makers talk like winemakers — origin, harvest, percentages, conching hours, tempering to the tenth of a degree. The usual move is to say it in prose and hope. We decided to plot it instead. The flavor wheel borrows the format every specialty-coffee and wine taster already trusts, then makes it answer a question prose can't: which of these eighteen notes does this origin actually hit? Hover Madagascar and raspberry lights up; hover Vietnam and the wheel goes leather and smoke. The tempering section does the same for craft: the curve is the recipe, so the curve is the graphic.

The stack — why Astro

Signature technique — a flavor wheel from pure trigonometry

Every segment of the wheel is one annular-sector path: two arcs and two radial lines. The helper lives in the component frontmatter, so it executes once, at build:

// FlavorWheel.astro — runs at build time, ships as plain paths
const P = (r, a) => {
  const rad = ((a - 90) * Math.PI) / 180;   // 0° = top, clockwise
  return [CX + r * Math.cos(rad), CY + r * Math.sin(rad)];
};
function ringSeg(r0, r1, a0, a1) {
  const [x0, y0] = P(r1, a0), [x1, y1] = P(r1, a1);
  const [x2, y2] = P(r0, a1), [x3, y3] = P(r0, a0);
  const large = a1 - a0 > 180 ? 1 : 0;
  return `M${x0} ${y0} A${r1} ${r1} 0 ${large} 1 ${x1} ${y1} ` +
         `L${x2} ${y2} A${r0} ${r0} 0 ${large} 0 ${x3} ${y3} Z`;
}

Each note segment carries data-origins="madagascar tanzania" — computed from the origin data, so the wheel can never disagree with the copy. The client script is then almost embarrassingly small: on hover, focus or tap of an origin, toggle a .lit class on matching segments and .has-active on the wheel; CSS does the dimming, the gold stroke, and the hub swap. Buttons are real <button>s with aria-pressed, the caption is an aria-live region, and Escape clears the selection.

Signature technique #2 — a curve that draws itself

The tempering chart is one SVG path with pathLength="1", which lets CSS treat the whole curve as a unit dash. Off-screen it's fully offset; when an IntersectionObserver adds .is-in, the offset transitions to zero and the batch appears to plot itself — melt, cool-and-seed, rework — with the temperature callouts fading in phase by phase:

.js .tfig .tpath { stroke-dasharray: 1; stroke-dashoffset: 1; }
.js .tfig.is-in .tpath {
  stroke-dashoffset: 0;
  transition: stroke-dashoffset 2.8s var(--ease) .15s;
}

No canvas, no requestAnimationFrame, no library — the browser's compositor does the drawing, and there is nothing to pause when it scrolls away because nothing loops.

Details that matter

The batch-list form is a demo — it validates and confirms in place but sends and stores nothing. Wire it to your own endpoint (Buttondown, Formspree, a serverless function) before collecting real addresses.

Ship it on GitHub Pages

Astro builds into ./docs, and Pages serves main + /docs directly — no Actions workflow needed:

// astro.config.mjs
export default defineConfig({
  site: 'https://bswxyz.github.io',
  base: '/conche',
  outDir: './docs',
});
npm run build
git add -A && git commit -m "build"
gh repo create bswxyz/conche --public --source . --push
gh api --method POST /repos/bswxyz/conche/pages \
  -f 'source[branch]=main' -f 'source[path]=/docs'

A .nojekyll in public/ rides along into the build so Pages serves the output untouched.

← Back to Conche