Cuckoo: single click gives one call, double-click strikes the hour

Nine cuckoos on an idle click is a lot. There's no singleclick event and click
always fires before dblclick, so the single call is deferred by one double-click
interval (260ms) and cancelled if a second click lands.
This commit is contained in:
Ryan Malloy 2026-07-14 09:32:45 -06:00
parent 63902f095a
commit 6b6d80ded3

View File

@ -25,8 +25,8 @@
class="cuckoo__btn"
id="cuckoo-clock"
type="button"
aria-label="Cuckoo clock — click to strike the hour"
title="Strike the hour"
aria-label="Cuckoo clock. Click for one call; double-click to strike the hour."
title="Click for one call · double-click strikes the hour"
>
<svg viewBox="0 0 200 272" class="cuckoo__svg" aria-hidden="true">
<defs>
@ -120,7 +120,9 @@
</g>
</svg>
<span class="cuckoo__hint">Strike the hour</span>
<span class="cuckoo__hint">
Click <span class="cuckoo__hint-sep">·</span> double-click strikes the hour
</span>
</button>
<p class="cuckoo__live" role="status" aria-live="polite" id="cuckoo-live"></p>
@ -196,7 +198,7 @@
return 0.78; // seconds until the next call may begin
};
clock.addEventListener("click", async () => {
const strike = async (calls) => {
if (striking) return;
striking = true;
@ -204,18 +206,42 @@
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);
for (let i = 0; i < calls; i++) t += call(t);
clock.classList.add("is-striking");
if (live) live.textContent = `Cuckoo ×${hour}`;
if (live) live.textContent = `Cuckoo ×${calls}`;
const total = (t - ctx.currentTime) * 1000;
setTimeout(() => {
clock.classList.remove("is-striking");
striking = false;
}, total);
};
// A single click is a greeting: one call. Double-click and it strikes the
// hour properly — which at eleven o'clock is a commitment.
//
// There is no `singleclick` event, and `click` always fires BEFORE `dblclick`.
// So hold the single call for one double-click interval and cancel it if a
// second click lands. 260 ms is comfortably under the platform's threshold
// but longer than a deliberate double-tap.
let pending = null;
clock.addEventListener("click", () => {
if (pending) return; // this is the 2nd of a pair — let dblclick take it
pending = setTimeout(() => {
pending = null;
strike(1);
}, 260);
});
clock.addEventListener("dblclick", () => {
if (pending) {
clearTimeout(pending);
pending = null;
}
strike(new Date().getHours() % 12 || 12);
});
}
</script>
@ -260,6 +286,7 @@
transition: color 0.2s;
}
.cuckoo__btn:hover .cuckoo__hint { color: var(--sl-color-accent); }
.cuckoo__hint-sep { opacity: 0.5; margin: 0 0.15em; }
.cuckoo__live {
min-height: 1.2em;