Created
September 7, 2026 12:05
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| (() => { | |
| "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