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" class="cuckoo__btn"
id="cuckoo-clock" id="cuckoo-clock"
type="button" type="button"
aria-label="Cuckoo clock — click to strike the hour" aria-label="Cuckoo clock. Click for one call; double-click to strike the hour."
title="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"> <svg viewBox="0 0 200 272" class="cuckoo__svg" aria-hidden="true">
<defs> <defs>
@ -120,7 +120,9 @@
</g> </g>
</svg> </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> </button>
<p class="cuckoo__live" role="status" aria-live="polite" id="cuckoo-live"></p> <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 return 0.78; // seconds until the next call may begin
}; };
clock.addEventListener("click", async () => { const strike = async (calls) => {
if (striking) return; if (striking) return;
striking = true; striking = true;
@ -204,18 +206,42 @@
ctx = ctx ?? new (window.AudioContext || window.webkitAudioContext)(); ctx = ctx ?? new (window.AudioContext || window.webkitAudioContext)();
if (ctx.state === "suspended") await ctx.resume(); if (ctx.state === "suspended") await ctx.resume();
const hour = new Date().getHours() % 12 || 12;
let t = ctx.currentTime + 0.05; 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"); clock.classList.add("is-striking");
if (live) live.textContent = `Cuckoo ×${hour}`; if (live) live.textContent = `Cuckoo ×${calls}`;
const total = (t - ctx.currentTime) * 1000; const total = (t - ctx.currentTime) * 1000;
setTimeout(() => { setTimeout(() => {
clock.classList.remove("is-striking"); clock.classList.remove("is-striking");
striking = false; striking = false;
}, total); }, 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> </script>
@ -260,6 +286,7 @@
transition: color 0.2s; transition: color 0.2s;
} }
.cuckoo__btn:hover .cuckoo__hint { color: var(--sl-color-accent); } .cuckoo__btn:hover .cuckoo__hint { color: var(--sl-color-accent); }
.cuckoo__hint-sep { opacity: 0.5; margin: 0 0.15em; }
.cuckoo__live { .cuckoo__live {
min-height: 1.2em; min-height: 1.2em;