Skip to content

Instantly share code, notes, and snippets.

@miantiao-me
Created September 7, 2026 12:05
Show Gist options
  • Select an option

  • Save miantiao-me/b6477d762d5cf4cd1da4fadc776343b7 to your computer and use it in GitHub Desktop.

Select an option

Save miantiao-me/b6477d762d5cf4cd1da4fadc776343b7 to your computer and use it in GitHub Desktop.
Play Für Elise on Moonshot AI's interactive About-page piano — https://www.moonshot.ai/about
(() => {
"use strict";
const previousFurElise = window.furElise;
if (previousFurElise && typeof previousFurElise.stop === "function") previousFurElise.stop();
if (
window.pagePianoPiece &&
window.pagePianoPiece !== previousFurElise &&
typeof window.pagePianoPiece.stop === "function"
) {
window.pagePianoPiece.stop();
}
const KEY_BY_NOTE = {
C4: "a", "C#4": "w", D4: "s", "D#4": "e", E4: "d", F4: "f",
"F#4": "t", G4: "g", "G#4": "y", A4: "h", "A#4": "u", B4: "j",
C5: "k", "C#5": "o", D5: "l", "D#5": "p", E5: ";"
};
const SIXTEENTH = 0.25;
const EIGHTH = 0.5;
const QUARTER = 1;
const REPEAT_GAP = 0.5;
const phraseEvents = [];
let cursor = 0;
const append = (entries) => {
for (const [note, spacing] of entries) {
const hold = spacing === SIXTEENTH ? 0.14 : spacing === EIGHTH ? 0.32 : 0.62;
phraseEvents.push({ beat: cursor, note, duration: hold });
cursor += spacing;
}
};
// The opening turn acts as a pickup into each longer A-minor arrival.
append([
["E5", SIXTEENTH], ["D#5", SIXTEENTH], ["E5", SIXTEENTH],
["D#5", SIXTEENTH], ["E5", SIXTEENTH], ["B4", SIXTEENTH],
["D5", SIXTEENTH], ["C5", SIXTEENTH], ["A4", EIGHTH]
]);
// Low arpeggio notes are kept sequential so the page instrument remains a single melodic line.
append([
["C4", SIXTEENTH], ["E4", SIXTEENTH], ["A4", SIXTEENTH], ["B4", EIGHTH],
["E4", SIXTEENTH], ["G#4", SIXTEENTH], ["B4", SIXTEENTH], ["C5", EIGHTH]
]);
// The second statement includes the familiar E4 breath before the turn returns.
append([
["E4", EIGHTH], ["E5", SIXTEENTH], ["D#5", SIXTEENTH],
["E5", SIXTEENTH], ["D#5", SIXTEENTH], ["E5", SIXTEENTH],
["B4", SIXTEENTH], ["D5", SIXTEENTH], ["C5", SIXTEENTH], ["A4", EIGHTH]
]);
append([
["C4", SIXTEENTH], ["E4", SIXTEENTH], ["A4", SIXTEENTH], ["B4", EIGHTH],
["E4", SIXTEENTH], ["C5", SIXTEENTH], ["B4", EIGHTH], ["A4", QUARTER]
]);
const PHRASE_BEATS = cursor;
const REQUIRED_NOTES = [...new Set(phraseEvents.map((event) => event.note))];
let playing = false;
let scheduler = null;
let timers = new Set();
let active = new Map();
let generations = Object.create(null);
let runId = 0;
let startedAt = 0;
let currentBpm = 72;
let currentRepeats = 2;
let currentDurationMs = 0;
const send = (type, key) => {
window.dispatchEvent(new KeyboardEvent(type, { key, bubbles: true }));
};
const releaseAll = () => {
for (const { key } of active.values()) send("keyup", key);
active.clear();
};
const stop = (silent = false) => {
runId++;
playing = false;
if (scheduler !== null) clearInterval(scheduler);
scheduler = null;
for (const timer of timers) clearTimeout(timer);
timers.clear();
releaseAll();
if (!silent) console.info("Für Elise stopped; all held keys released.");
};
const validatePiano = () => {
const piano = document.querySelector('[aria-label="Interactive piano"]');
if (!piano) {
console.error('Für Elise: missing [aria-label="Interactive piano"].');
return null;
}
const labels = new Set(Array.from(
piano.querySelectorAll("[aria-label]"),
(element) => element.getAttribute("aria-label")
));
const missing = REQUIRED_NOTES.filter((note) =>
!Array.from(labels).some((label) => label && label.startsWith(`${note} piano key (`))
);
if (missing.length) {
console.error(`Für Elise: missing required keys: ${missing.join(", ")}.`);
return null;
}
return piano;
};
const start = (options = {}) => {
stop(true);
const piano = validatePiano();
if (!piano) return false;
const bpm = Number(options.bpm ?? 72);
const repeats = Number(options.repeats ?? 2);
if (!Number.isFinite(bpm) || bpm <= 0 || !Number.isFinite(repeats) || repeats <= 0) {
console.error("Für Elise: bpm and repeats must be positive numbers.");
return false;
}
currentBpm = bpm;
currentRepeats = Math.max(1, Math.floor(repeats));
const beatMs = 60000 / currentBpm;
const cycleBeats = PHRASE_BEATS + REPEAT_GAP;
currentDurationMs = (PHRASE_BEATS * currentRepeats + REPEAT_GAP * (currentRepeats - 1)) * beatMs;
const events = [];
for (let repeat = 0; repeat < currentRepeats; repeat++) {
const offset = repeat * cycleBeats;
phraseEvents.forEach((event) => events.push({ ...event, beat: event.beat + offset }));
}
const thisRun = ++runId;
let eventCursor = 0;
playing = true;
startedAt = performance.now() + 80;
piano.scrollIntoView({ behavior: "smooth", block: "center" });
const later = (callback, delay) => {
const timer = setTimeout(() => {
timers.delete(timer);
callback();
}, Math.max(0, delay));
timers.add(timer);
};
const press = (event) => {
if (!playing || runId !== thisRun) return;
const key = KEY_BY_NOTE[event.note];
const previous = active.get(event.note);
if (previous) send("keyup", previous.key);
const token = (generations[event.note] || 0) + 1;
generations[event.note] = token;
send("keydown", key);
active.set(event.note, { key, token });
const endAt = startedAt + (event.beat + event.duration) * beatMs;
later(() => {
const held = active.get(event.note);
if (!playing || runId !== thisRun || !held || held.token !== token) return;
send("keyup", key);
active.delete(event.note);
}, endAt - performance.now());
};
const scheduleAhead = () => {
if (!playing || runId !== thisRun) return;
const now = performance.now();
const horizon = now + 120;
while (eventCursor < events.length) {
const event = events[eventCursor];
const due = startedAt + event.beat * beatMs;
if (due > horizon) break;
eventCursor++;
if (now - due > 200) continue;
later(() => {
if (performance.now() - due <= 200) press(event);
}, due - performance.now());
}
if (now >= startedAt + currentDurationMs + 100) {
playing = false;
clearInterval(scheduler);
scheduler = null;
for (const timer of timers) clearTimeout(timer);
timers.clear();
releaseAll();
console.info("Für Elise finished.");
}
};
scheduler = setInterval(scheduleAhead, 25);
scheduleAhead();
console.info(
`Für Elise / 致爱丽丝 | ${currentBpm} BPM | ${currentRepeats} repeats | ` +
`${(currentDurationMs / 1000).toFixed(1)}s. Stop with furElise.stop().`
);
return true;
};
const status = () => ({
playing,
bpm: currentBpm,
repeats: currentRepeats,
elapsedSeconds: playing ? Math.max(0, (performance.now() - startedAt) / 1000) : 0,
durationSeconds: currentDurationMs / 1000,
heldNotes: Array.from(active.keys())
});
window.furElise = { start, stop, status };
window.pagePianoPiece = window.furElise;
start();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment