Add a clickable cuckoo clock and the 90-second talk to the landing page

The clock strikes the current hour. Two things in it are mechanism rather than
decoration:

- The pendulum TICKS. steps(2) over 2s = two discrete positions, one per second.
  That is what an escapement does — it chops continuous energy into countable
  ticks. A swept pendulum would contradict the entire site.

- The call is SYNTHESIZED, not sampled. A real cuckoo clock is two wooden pipes
  a minor third apart (6:5, so 700 Hz -> 583 Hz), pushed by a bellows. That's
  twenty lines of Web Audio: triangle pipes with a slight pitch sag as the
  bellows empties, plus a puff of bandpassed noise for the breath. The sound is
  the mechanism rebuilt, and the hero ships zero bytes of audio.

The talk is regenerated for a public audience — the finding, not the
procurement case. Transcript ships with it: audio is the alternative to reading,
never a replacement for the text. preload=none so the 800 KB only downloads for
readers who actually press play.
This commit is contained in:
Ryan Malloy 2026-07-14 09:29:51 -06:00
parent 6881489bf6
commit 63902f095a
4 changed files with 469 additions and 0 deletions

Binary file not shown.

View File

@ -0,0 +1,298 @@
---
// A cuckoo clock you can click. It strikes the current hour.
//
// Two things here are deliberate rather than decorative:
//
// 1. THE PENDULUM TICKS, it does not sweep. `steps(2)` over 2s means it lands
// in exactly two discrete positions per period — one per second. That IS an
// escapement: the mechanism that takes continuous energy (a falling weight,
// a crystal oscillator, a satellite) and chops it into countable ticks. The
// whole site is an argument about what happens when you put a scheduler in
// front of that chop, so the hero ought to do it correctly.
//
// 2. THE CALL IS SYNTHESIZED, not a sample. A real cuckoo clock is two wooden
// pipes pushed by a bellows, tuned a MINOR THIRD apart, high note first.
// That's ~700 Hz down to ~590 Hz — about twenty lines of Web Audio. So the
// sound isn't an asset we shipped; it's the mechanism, rebuilt. Also it
// means the hero costs zero bytes of audio and works offline.
//
// Audio only ever starts from a click, which is both good manners and the only
// thing browsers permit — an AudioContext can't be resumed without a gesture.
---
<div class="cuckoo">
<button
class="cuckoo__btn"
id="cuckoo-clock"
type="button"
aria-label="Cuckoo clock — click to strike the hour"
title="Strike the hour"
>
<svg viewBox="0 0 200 272" class="cuckoo__svg" aria-hidden="true">
<defs>
<linearGradient id="ck-brass" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#f2d79b" />
<stop offset="55%" stop-color="#d9a441" />
<stop offset="100%" stop-color="#8a6417" />
</linearGradient>
<linearGradient id="ck-wood" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#241f19" />
<stop offset="100%" stop-color="#14110e" />
</linearGradient>
<!-- The doorway. Anything drawn inside this clip can only be seen
through the opening — which is how the bird stays hidden until it
rides up into it. -->
<clipPath id="ck-door-clip">
<path d="M78 96V74a22 22 0 0 1 44 0v22Z" />
</clipPath>
</defs>
<!-- Case -->
<path
d="M100 20 172 58 172 202 28 202 28 58Z"
fill="url(#ck-wood)" stroke="url(#ck-brass)" stroke-width="2.5" stroke-linejoin="round"
/>
<!-- Roof, with the deep Black Forest overhang -->
<path
d="M100 8 192 58 176 66 100 24 24 66 8 58Z"
fill="#2b241c" stroke="url(#ck-brass)" stroke-width="2" stroke-linejoin="round"
/>
<!-- Bird, behind the doorway. Starts pushed down out of the opening. -->
<g clip-path="url(#ck-door-clip)">
<g class="ck-bird" transform="translate(0, 30)">
<circle cx="96" cy="84" r="11" fill="#e8e4dc" />
<path d="M105 80 124 85 105 90Z" fill="#d9a441" />
<circle cx="99" cy="80" r="2" fill="#161310" />
<path d="M90 74c-3-5 0-9 4-9-2 3-1 6 1 7Z" fill="#e8e4dc" />
</g>
</g>
<!-- Door frame + the two leaves, hinged at the outer jambs -->
<path d="M78 96V74a22 22 0 0 1 44 0v22" fill="none"
stroke="url(#ck-brass)" stroke-width="2" />
<g class="ck-doors">
<path class="ck-door ck-door--l" d="M78 96V74a22 22 0 0 1 22-22v44Z"
fill="#1d1915" stroke="url(#ck-brass)" stroke-width="1.4" />
<path class="ck-door ck-door--r" d="M122 96V74a22 22 0 0 0-22-22v44Z"
fill="#1d1915" stroke="url(#ck-brass)" stroke-width="1.4" />
</g>
<!-- Dial -->
<circle cx="100" cy="146" r="36" fill="#0d0b09" stroke="url(#ck-brass)" stroke-width="2.5" />
<g stroke="#d9a441" stroke-linecap="round" opacity="0.8">
<!-- 12 hour ticks; the quarters are longer, as on a real dial -->
<line x1="100" y1="114" x2="100" y2="121" stroke-width="1.8" />
<line x1="132" y1="146" x2="125" y2="146" stroke-width="1.8" />
<line x1="100" y1="178" x2="100" y2="171" stroke-width="1.8" />
<line x1="68" y1="146" x2="75" y2="146" stroke-width="1.8" />
<line x1="116" y1="118" x2="114" y2="122" stroke-width="1.1" />
<line x1="128" y1="130" x2="124" y2="132" stroke-width="1.1" />
<line x1="128" y1="162" x2="124" y2="160" stroke-width="1.1" />
<line x1="116" y1="174" x2="114" y2="170" stroke-width="1.1" />
<line x1="84" y1="174" x2="86" y2="170" stroke-width="1.1" />
<line x1="72" y1="162" x2="76" y2="160" stroke-width="1.1" />
<line x1="72" y1="130" x2="76" y2="132" stroke-width="1.1" />
<line x1="84" y1="118" x2="86" y2="122" stroke-width="1.1" />
</g>
<!-- Hands. Set to the real local time by the script below. -->
<line class="ck-hand-h" x1="100" y1="146" x2="100" y2="126"
stroke="#e8e4dc" stroke-width="3.2" stroke-linecap="round" />
<line class="ck-hand-m" x1="100" y1="146" x2="100" y2="118"
stroke="#e8e4dc" stroke-width="2" stroke-linecap="round" />
<circle cx="100" cy="146" r="2.8" fill="#d9a441" />
<!-- Pendulum. It TICKS — see the header comment. -->
<g class="ck-pendulum">
<line x1="100" y1="202" x2="100" y2="242" stroke="#a97c1f" stroke-width="1.6" />
<!-- Leaf-shaped bob, as on a Black Forest movement -->
<ellipse cx="100" cy="250" rx="11" ry="9"
fill="url(#ck-brass)" stroke="#8a6417" stroke-width="0.8" />
<circle cx="100" cy="250" r="3" fill="#8a6417" opacity="0.6" />
</g>
<!-- Pine-cone weights, on their chains -->
<g>
<line x1="60" y1="202" x2="60" y2="222" stroke="#8a6417" stroke-width="1" />
<path d="M54 222h12l-2 7h-8Zm1 7h10l-2 7h-6Zm1 7h8l-3 8h-2Z" fill="url(#ck-brass)" />
<line x1="140" y1="202" x2="140" y2="234" stroke="#8a6417" stroke-width="1" />
<path d="M134 234h12l-2 7h-8Zm1 7h10l-2 7h-6Zm1 7h8l-3 8h-2Z" fill="url(#ck-brass)" />
</g>
</svg>
<span class="cuckoo__hint">Strike the hour</span>
</button>
<p class="cuckoo__live" role="status" aria-live="polite" id="cuckoo-live"></p>
</div>
<script>
const clock = document.getElementById("cuckoo-clock");
const live = document.getElementById("cuckoo-live");
if (clock) {
const svg = clock.querySelector("svg");
// --- Set the hands to the real local time. It's a clock; it should be right.
const setHands = () => {
const now = new Date();
const m = now.getMinutes();
const h = now.getHours() % 12 + m / 60;
const hand = (sel, deg) =>
svg?.querySelector(sel)?.setAttribute(
"transform",
`rotate(${deg} 100 146)`,
);
hand(".ck-hand-h", h * 30);
hand(".ck-hand-m", m * 6);
};
setHands();
setInterval(setHands, 30_000);
// --- The call. Two wooden pipes, a minor third apart, high note first.
// A minor third is a frequency ratio of 6:5, so 700 Hz → 583 Hz. Triangle
// waves for the woody pipe tone; a puff of filtered noise for the bellows.
let ctx = null;
let striking = false;
const pipe = (freq, at, dur) => {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = "triangle";
osc.frequency.setValueAtTime(freq, at);
// Real pipes sag slightly as the bellows empties.
osc.frequency.linearRampToValueAtTime(freq * 0.985, at + dur);
gain.gain.setValueAtTime(0, at);
gain.gain.linearRampToValueAtTime(0.18, at + 0.02); // bellows push
gain.gain.setValueAtTime(0.18, at + dur - 0.06);
gain.gain.linearRampToValueAtTime(0, at + dur); // and release
osc.connect(gain).connect(ctx.destination);
osc.start(at);
osc.stop(at + dur + 0.02);
// The breath: a short hiss under the note's attack.
const noise = ctx.createBufferSource();
const buf = ctx.createBuffer(1, ctx.sampleRate * 0.06, ctx.sampleRate);
const data = buf.getChannelData(0);
for (let i = 0; i < data.length; i++) data[i] = (Math.random() * 2 - 1) * 0.5;
noise.buffer = buf;
const bp = ctx.createBiquadFilter();
bp.type = "bandpass";
bp.frequency.value = freq * 1.5;
bp.Q.value = 0.7;
const ng = ctx.createGain();
ng.gain.setValueAtTime(0.05, at);
ng.gain.exponentialRampToValueAtTime(0.001, at + 0.06);
noise.connect(bp).connect(ng).connect(ctx.destination);
noise.start(at);
};
// One "cuck-oo": high note, then the minor third below it.
const call = (at) => {
pipe(700, at, 0.24);
pipe(700 * 5 / 6, at + 0.30, 0.30);
return 0.78; // seconds until the next call may begin
};
clock.addEventListener("click", async () => {
if (striking) return;
striking = true;
// An AudioContext can only start from a user gesture — which a click is.
ctx = ctx ?? new (window.AudioContext || window.webkitAudioContext)();
if (ctx.state === "suspended") await ctx.resume();
const hour = new Date().getHours() % 12 || 12;
let t = ctx.currentTime + 0.05;
for (let i = 0; i < hour; i++) t += call(t);
clock.classList.add("is-striking");
if (live) live.textContent = `Cuckoo ×${hour}`;
const total = (t - ctx.currentTime) * 1000;
setTimeout(() => {
clock.classList.remove("is-striking");
striking = false;
}, total);
});
}
</script>
<style>
.cuckoo {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.4rem;
margin: 0 0 2rem;
}
.cuckoo__btn {
background: none;
border: 0;
padding: 0.5rem;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
border-radius: 8px;
}
.cuckoo__btn:focus-visible {
outline: 2px solid var(--sl-color-accent);
outline-offset: 4px;
}
.cuckoo__svg {
width: 168px;
height: auto;
filter: drop-shadow(0 18px 30px rgb(0 0 0 / 0.5));
}
.cuckoo__hint {
font-family: var(--sl-font-system-mono);
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.14em;
color: var(--sl-color-gray-3);
transition: color 0.2s;
}
.cuckoo__btn:hover .cuckoo__hint { color: var(--sl-color-accent); }
.cuckoo__live {
min-height: 1.2em;
margin: 0;
font-family: var(--sl-font-system-mono);
font-size: 0.75rem;
color: var(--sl-color-accent);
}
/* The escapement. steps(2) over 2s = two discrete positions, one per second.
A swept pendulum would be a lie: an escapement's whole job is to make the
motion DISCRETE. */
.ck-pendulum {
transform-origin: 100px 202px;
animation: ck-tick 2s steps(2, jump-none) infinite;
}
@keyframes ck-tick {
from { transform: rotate(-9deg); }
to { transform: rotate(9deg); }
}
/* Doors swing outward on their hinges; the bird rides out behind them. */
.ck-door { transition: transform 0.35s cubic-bezier(0.2, 0.8, 0.3, 1); }
.ck-door--l { transform-origin: 78px 96px; }
.ck-door--r { transform-origin: 122px 96px; }
.is-striking .ck-door--l { transform: rotate(-62deg); }
.is-striking .ck-door--r { transform: rotate(62deg); }
.ck-bird { transition: transform 0.3s cubic-bezier(0.3, 1.4, 0.5, 1); }
.is-striking .ck-bird { transform: translate(0, 0); }
@media (prefers-reduced-motion: reduce) {
.ck-pendulum { animation: none; }
.ck-door, .ck-bird { transition: none; }
}
</style>

View File

@ -0,0 +1,136 @@
---
// "Listen to the talk" — the 90-second version of this site.
//
// preload="none" matters: without it every reader's browser downloads the whole
// MP3 on page load whether or not they ever press play. On a docs page that's
// ~800 KB of bandwidth for a feature most people skip. The cost is a ~200 ms
// delay before audio starts, which nobody notices on a 90-second talk.
//
// The transcript below is not an accessibility afterthought — it's the reason
// the page is searchable and the reason someone in an open-plan office can
// still get the argument. Audio is the alternative to reading, never a
// replacement for the text.
interface Props {
src?: string;
seconds?: number;
}
const { src = "/audio/lightning-talk.mp3", seconds = 89 } = Astro.props;
---
<div class="talk">
<button class="talk__btn" id="talk-btn" type="button">
<svg class="talk__icon talk__icon--play" viewBox="0 0 24 24" aria-hidden="true">
<path d="M6 4l14 8-14 8Z" fill="currentColor" stroke="none" />
</svg>
<svg class="talk__icon talk__icon--pause is-hidden" viewBox="0 0 24 24" aria-hidden="true">
<path d="M7 4h4v16H7ZM13 4h4v16h-4Z" fill="currentColor" stroke="none" />
</svg>
<span class="talk__label">Listen to the talk</span>
<span class="talk__dur">{seconds}s</span>
<span class="talk__progress" id="talk-progress"></span>
</button>
<audio id="talk-audio" preload="none" src={src}></audio>
<details class="talk__transcript">
<summary>Transcript</summary>
<slot />
</details>
</div>
<script>
const btn = document.getElementById("talk-btn");
const audio = document.getElementById("talk-audio") as HTMLAudioElement | null;
const progress = document.getElementById("talk-progress");
if (btn && audio) {
const play = btn.querySelector(".talk__icon--play");
const pause = btn.querySelector(".talk__icon--pause");
// Toggle a class, not `.hidden`. These are SVGElements, which inherit from
// Element rather than HTMLElement — so the `hidden` reflected property
// doesn't exist on them and assigning it silently does nothing.
const showPlaying = (playing: boolean) => {
play?.classList.toggle("is-hidden", playing);
pause?.classList.toggle("is-hidden", !playing);
};
btn.addEventListener("click", () => {
audio.paused ? audio.play() : audio.pause();
});
audio.addEventListener("play", () => showPlaying(true));
audio.addEventListener("pause", () => showPlaying(false));
audio.addEventListener("ended", () => {
showPlaying(false);
if (progress) progress.style.width = "0";
});
audio.addEventListener("timeupdate", () => {
if (progress && audio.duration) {
progress.style.width = `${(audio.currentTime / audio.duration) * 100}%`;
}
});
}
</script>
<style>
.talk { margin: 0 0 2rem; }
.talk__btn {
position: relative;
overflow: hidden;
display: inline-flex;
align-items: center;
gap: 0.6rem;
padding: 0.65rem 1.1rem;
border: 1px solid var(--sl-color-hairline-light);
border-radius: 6px;
background: var(--sl-color-bg-nav);
color: var(--sl-color-gray-2);
font: inherit;
font-size: 0.9rem;
cursor: pointer;
transition: color 0.2s, border-color 0.2s;
}
.talk__btn:hover {
color: var(--sl-color-white);
border-color: var(--sl-color-accent-low);
}
.talk__icon { width: 16px; height: 16px; color: var(--sl-color-accent); }
.talk__icon.is-hidden { display: none; }
.talk__label { font-weight: 600; }
.talk__dur {
font-family: var(--sl-font-system-mono);
font-size: 0.75rem;
font-variant-numeric: tabular-nums;
color: var(--sl-color-gray-3);
}
/* Fills along the bottom edge as it plays. */
.talk__progress {
position: absolute;
bottom: 0;
left: 0;
height: 2px;
width: 0;
background: var(--sl-color-accent);
transition: width 0.15s linear;
}
.talk__transcript {
margin-top: 0.9rem;
font-size: 0.9rem;
}
.talk__transcript summary {
cursor: pointer;
font-family: var(--sl-font-system-mono);
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--sl-color-gray-3);
}
.talk__transcript summary:hover { color: var(--sl-color-accent); }
.talk__transcript[open] summary { margin-bottom: 0.6rem; }
</style>

View File

@ -4,6 +4,41 @@ description: A field report on GPS Stratum 1 timekeeping on a Raspberry Pi 4 —
---
import { Aside, CardGrid, Card } from '@astrojs/starlight/components';
import CuckooClock from '../../components/CuckooClock.astro';
import ListenToTalk from '../../components/ListenToTalk.astro';
<CuckooClock />
<ListenToTalk>
Here's a thing that cost us a week. It might be costing you right now, and you'd
have no way to know.
We built a GPS-disciplined time server on a Raspberry Pi 4. Stratum 1 — a
satellite pulse, once a second, straight into a GPIO pin. Then we did what every
guide says to do next. Install a realtime kernel. Isolate the interrupt. Pin it
to a quiet core.
**The realtime kernel made it three times worse.** Not slightly worse. Three
times. 2134 ns of jitter became 6947 ns.
Here is why. PREEMPT_RT buys its determinism by turning interrupt handlers into
schedulable threads. For almost every driver that's a good trade. But the PPS
driver takes its timestamp *inside* its handler. So threading it doesn't defer
some work — it defers **the act of looking at the clock**. We didn't make the
system more predictable. We put a scheduler between the pulse and the clock.
The fix is four lines. One flag: `IRQF_NO_THREAD`. Keep that one handler in
hard-IRQ context, and let everything else stay preemptible. Our error went from
2468 ns to **199 ns**.
As far as we can tell, nobody has applied this. Which means anyone running GPS
timing on a realtime kernel is quietly eating microseconds of error and has no
reason to suspect it — because nothing looks broken. chrony still says Stratum 1.
The dashboard still says locked. The number is just silently worse.
So that's the talk. Measure your clock. Don't trust the guide. And if you take
one thing from us, [take the patch](/reference/the-patch/).
</ListenToTalk>
**This is not a build guide.**