chore: improvements to /murmurations
d710f52f
1 file(s) · +35 −11
| 33 | 33 | > |
|
| 34 | 34 | Back |
|
| 35 | 35 | </a> |
|
| 36 | - | <audio id="murmuration-audio" src="/murmuration.mp3" loop preload="none"></audio> |
|
| 36 | + | <audio id="murmuration-audio" src="/murmuration.mp3" preload="none"></audio> |
|
| 37 | 37 | <div |
|
| 38 | 38 | id="poem-overlay" |
|
| 39 | 39 | class="pointer-events-none fixed inset-0 z-10 hidden items-center justify-center p-8 italic text-xl" |
|
| 48 | 48 | aria-label="Play audio" |
|
| 49 | 49 | aria-pressed="false" |
|
| 50 | 50 | > |
|
| 51 | - | <span id="audio-icon">▶</span> |
|
| 51 | + | <span id="audio-icon" class="flex h-8 w-8 items-center justify-center"></span> |
|
| 52 | 52 | </button> |
|
| 53 | 53 | <button |
|
| 54 | 54 | id="info-btn" |
|
| 115 | 115 | { t: 101, line: "to think again of dangerous and noble things.\nI want to be light and frolicsome.\nI want to be improbable beautiful and afraid of nothing,\nas though I had wings." }, |
|
| 116 | 116 | ]; |
|
| 117 | 117 | ||
| 118 | + | const POEM_END_T = 118; |
|
| 119 | + | const PLAY_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="h-4 w-4" viewBox="0 0 256 256"><path fill="currentColor" d="M231.36 116.19L87.28 28.06a14 14 0 0 0-14.18-.27A13.69 13.69 0 0 0 66 39.87v176.26a13.69 13.69 0 0 0 7.1 12.08a14 14 0 0 0 14.18-.27l144.08-88.13a13.82 13.82 0 0 0 0-23.62m-6.26 13.38L81 217.7a2 2 0 0 1-2.06 0a1.78 1.78 0 0 1-1-1.61V39.87a1.78 1.78 0 0 1 1-1.61A2.06 2.06 0 0 1 80 38a2 2 0 0 1 1 .31l144.1 88.12a1.82 1.82 0 0 1 0 3.14"/></svg>'; |
|
| 120 | + | const PAUSE_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="h-4 w-4" viewBox="0 0 256 256"><path fill="currentColor" d="M200 34h-40a14 14 0 0 0-14 14v160a14 14 0 0 0 14 14h40a14 14 0 0 0 14-14V48a14 14 0 0 0-14-14m2 174a2 2 0 0 1-2 2h-40a2 2 0 0 1-2-2V48a2 2 0 0 1 2-2h40a2 2 0 0 1 2 2ZM96 34H56a14 14 0 0 0-14 14v160a14 14 0 0 0 14 14h40a14 14 0 0 0 14-14V48a14 14 0 0 0-14-14m2 174a2 2 0 0 1-2 2H56a2 2 0 0 1-2-2V48a2 2 0 0 1 2-2h40a2 2 0 0 1 2 2Z"/></svg>'; |
|
| 121 | + | if (audioIcon) audioIcon.innerHTML = PLAY_SVG; |
|
| 122 | + | ||
| 118 | 123 | const poemText = poemOverlay?.firstElementChild as HTMLDivElement | null; |
|
| 119 | 124 | let lastCueIdx = -1; |
|
| 120 | 125 | let swapTimer: number | undefined; |
|
| 123 | 128 | if (!poemOverlay || !poemText) return; |
|
| 124 | 129 | if (idx === lastCueIdx) return; |
|
| 125 | 130 | window.clearTimeout(swapTimer); |
|
| 131 | + | const wasHidden = poemOverlay.classList.contains("hidden"); |
|
| 126 | 132 | poemText.classList.remove("opacity-100"); |
|
| 127 | 133 | poemText.classList.add("opacity-0"); |
|
| 128 | - | swapTimer = window.setTimeout(() => { |
|
| 134 | + | const swap = () => { |
|
| 129 | 135 | if (idx < 0) { |
|
| 130 | 136 | poemOverlay.classList.add("hidden"); |
|
| 131 | 137 | poemOverlay.classList.remove("flex"); |
|
| 135 | 141 | poemOverlay.classList.remove("hidden"); |
|
| 136 | 142 | poemOverlay.classList.add("flex"); |
|
| 137 | 143 | requestAnimationFrame(() => { |
|
| 138 | - | poemText.classList.remove("opacity-0"); |
|
| 139 | - | poemText.classList.add("opacity-100"); |
|
| 144 | + | requestAnimationFrame(() => { |
|
| 145 | + | poemText.classList.remove("opacity-0"); |
|
| 146 | + | poemText.classList.add("opacity-100"); |
|
| 147 | + | }); |
|
| 140 | 148 | }); |
|
| 141 | 149 | } |
|
| 142 | - | }, FADE_MS); |
|
| 150 | + | }; |
|
| 151 | + | if (wasHidden) swap(); |
|
| 152 | + | else swapTimer = window.setTimeout(swap, FADE_MS); |
|
| 143 | 153 | lastCueIdx = idx; |
|
| 144 | 154 | }; |
|
| 145 | 155 | ||
| 146 | 156 | const updateCue = () => { |
|
| 147 | 157 | if (!audioEl) return; |
|
| 148 | 158 | const t = audioEl.currentTime; |
|
| 159 | + | if (t >= POEM_END_T) { |
|
| 160 | + | renderCue(-1); |
|
| 161 | + | return; |
|
| 162 | + | } |
|
| 149 | 163 | let idx = -1; |
|
| 150 | 164 | for (let i = 0; i < cues.length; i++) { |
|
| 151 | 165 | if (cues[i].t <= t) idx = i; |
|
| 154 | 168 | renderCue(idx); |
|
| 155 | 169 | }; |
|
| 156 | 170 | ||
| 171 | + | const resetBtn = () => { |
|
| 172 | + | if (!audioBtn || !audioIcon) return; |
|
| 173 | + | audioIcon.innerHTML = PLAY_SVG; |
|
| 174 | + | audioBtn.setAttribute("aria-pressed", "false"); |
|
| 175 | + | audioBtn.setAttribute("aria-label", "Play audio"); |
|
| 176 | + | }; |
|
| 177 | + | ||
| 157 | 178 | if (audioBtn && audioEl && audioIcon) { |
|
| 158 | 179 | audioEl.addEventListener("timeupdate", updateCue); |
|
| 159 | 180 | audioEl.addEventListener("seeked", updateCue); |
|
| 160 | 181 | audioEl.addEventListener("pause", () => { |
|
| 161 | 182 | if (audioEl.currentTime === 0) renderCue(-1); |
|
| 162 | 183 | }); |
|
| 163 | - | audioEl.addEventListener("ended", () => renderCue(-1)); |
|
| 184 | + | audioEl.addEventListener("ended", () => { |
|
| 185 | + | renderCue(-1); |
|
| 186 | + | audioEl.currentTime = 0; |
|
| 187 | + | resetBtn(); |
|
| 188 | + | }); |
|
| 164 | 189 | ||
| 165 | 190 | audioBtn.addEventListener("click", async () => { |
|
| 166 | 191 | if (audioEl.paused) { |
|
| 167 | 192 | try { |
|
| 193 | + | if (audioEl.ended) audioEl.currentTime = 0; |
|
| 168 | 194 | await audioEl.play(); |
|
| 169 | - | audioIcon.textContent = "⏸"; |
|
| 195 | + | audioIcon.innerHTML = PAUSE_SVG; |
|
| 170 | 196 | audioBtn.setAttribute("aria-pressed", "true"); |
|
| 171 | 197 | audioBtn.setAttribute("aria-label", "Pause audio"); |
|
| 172 | 198 | updateCue(); |
|
| 175 | 201 | } |
|
| 176 | 202 | } else { |
|
| 177 | 203 | audioEl.pause(); |
|
| 178 | - | audioIcon.textContent = "▶"; |
|
| 179 | - | audioBtn.setAttribute("aria-pressed", "false"); |
|
| 180 | - | audioBtn.setAttribute("aria-label", "Play audio"); |
|
| 204 | + | resetBtn(); |
|
| 181 | 205 | } |
|
| 182 | 206 | }); |
|
| 183 | 207 | } |
|