From 74fd4fde32955f7eee78de6d93c89b9e10005d07 Mon Sep 17 00:00:00 2001 From: Johnny322 Date: Sun, 8 Feb 2026 15:41:36 +0100 Subject: [PATCH] More to pulsing music player --- src/App.vue | 69 ++++++++++++++++++++++--------- src/styles.css | 108 +++++++++++++++++++++++++++++++++++++------------ 2 files changed, 134 insertions(+), 43 deletions(-) diff --git a/src/App.vue b/src/App.vue index 300fe53..2956364 100644 --- a/src/App.vue +++ b/src/App.vue @@ -155,20 +155,24 @@
-
-
- -
- Status - {{ isPlaying ? 'Playing' : 'Paused' }} -
-
+
+
-
+
Pick a tile to start the round.
@@ -260,9 +264,13 @@ export default { audioContext: null as AudioContext | null, analyser: null as AnalyserNode | null, analyserData: null as Uint8Array | null, + frequencyData: null as Uint8Array | null, rafId: 0, mediaSource: null as MediaElementAudioSourceNode | null, - mediaElement: null as HTMLAudioElement | null + mediaElement: null as HTMLAudioElement | null, + rayLevel: 0, + rayHue: 200, + isAnswerClip: false } }, computed: { @@ -335,7 +343,9 @@ export default { this.analyser = analyser this.analyserData = new Uint8Array(analyser.fftSize) + this.frequencyData = new Uint8Array(analyser.frequencyBinCount) this.pulseLevel = 0 + this.rayLevel = 0 }, teardownAudio() { if (this.rafId) { @@ -352,28 +362,47 @@ export default { } this.mediaElement = null this.analyserData = null + this.frequencyData = null this.isPlaying = false this.pulseLevel = 0 + this.rayLevel = 0 }, startPulse() { - if (!this.analyser || !this.analyserData) return + if (!this.analyser || !this.analyserData || !this.frequencyData) return const analyser = this.analyser const data = this.analyserData + const freq = this.frequencyData const tick = () => { - if (!this.isPlaying || !this.analyser || !this.analyserData) { + if (!this.isPlaying || !this.analyser || !this.analyserData || !this.frequencyData) { this.rafId = 0 return } analyser.getByteTimeDomainData(data) + analyser.getByteFrequencyData(freq) let sumSquares = 0 for (let i = 0; i < data.length; i += 1) { const centered = (data[i] - 128) / 128 sumSquares += centered * centered } const rms = Math.sqrt(sumSquares / data.length) - const target = Math.min(1, rms * 2.8) - this.pulseLevel = this.pulseLevel * 0.7 + target * 0.3 + const target = Math.min(1, rms * 3.6) + this.pulseLevel = this.pulseLevel * 0.65 + target * 0.35 + + const lowBandEnd = Math.floor(freq.length * 0.2) + const highBandStart = Math.floor(freq.length * 0.55) + let lowSum = 0 + for (let i = 0; i < lowBandEnd; i += 1) lowSum += freq[i] + const lowAvg = lowSum / Math.max(1, lowBandEnd) / 255 + + let highSum = 0 + for (let i = highBandStart; i < freq.length; i += 1) highSum += freq[i] + const highAvg = highSum / Math.max(1, freq.length - highBandStart) / 255 + + const rayTarget = Math.min(1, (lowAvg * 0.6 + highAvg * 0.9) * 1.4) + this.rayLevel = this.rayLevel * 0.7 + rayTarget * 0.3 + const hueTarget = 180 + highAvg * 120 + this.rayHue = this.rayHue * 0.8 + hueTarget * 0.2 this.rafId = requestAnimationFrame(tick) } @@ -435,6 +464,7 @@ export default { this.currentTileKey = null this.currentClipUrl = '' this.lastAwardedTeamId = null + this.isAnswerClip = false this.teardownAudio() }, tileKey(cIndex: number, qIndex: number) { @@ -479,6 +509,7 @@ export default { this.tiles[key] = { status: 'playing', lastTeamId: null } this.currentTileKey = key this.currentClipUrl = encodeURI(clue.song) + this.isAnswerClip = false await nextTick() const player = this.getPlayer() if (player) { @@ -502,6 +533,7 @@ export default { this.lastAwardedTeamId = null if (clue.answer) { this.currentClipUrl = encodeURI(clue.answer) + this.isAnswerClip = true await nextTick() const player = this.getPlayer() if (player) { @@ -541,6 +573,7 @@ export default { this.tiles[key].status = 'void' this.currentTileKey = null this.currentClipUrl = '' + this.isAnswerClip = false const player = this.getPlayer() player?.pause() this.teardownAudio() diff --git a/src/styles.css b/src/styles.css index 8030a5a..040a9db 100644 --- a/src/styles.css +++ b/src/styles.css @@ -386,6 +386,10 @@ audio.hidden-audio { overflow: hidden; } +.custom-player.idle { + background: radial-gradient(circle at top, rgba(20, 20, 20, 0.5), rgba(5, 6, 12, 0.95) 70%); +} + .custom-player::after { content: ''; position: absolute; @@ -400,39 +404,72 @@ audio.hidden-audio { height: 96px; border-radius: 50%; background: radial-gradient(circle at 30% 30%, #fbd72b, #f58b2b 60%, #1d3b8b); - transform: scale(calc(1 + var(--pulse, 0) * 0.22)); - box-shadow: 0 0 calc(20px + var(--pulse, 0) * 60px) rgba(251, 215, 43, 0.35); - transition: transform 0.08s ease-out; + transform: scale(calc(1 + var(--pulse, 0) * 0.45)); + box-shadow: + 0 0 calc(18px + var(--pulse, 0) * 90px) rgba(251, 215, 43, 0.5), + 0 0 calc(40px + var(--pulse, 0) * 160px) rgba(61, 214, 255, 0.35); + transition: transform 0.06s ease-out; + position: relative; + z-index: 2; +} + +.pulse-orb.idle { + background: radial-gradient(circle at 40% 40%, #1a1a1a, #000 65%); + box-shadow: 0 0 12px rgba(0, 0, 0, 0.6); + transform: scale(1); +} + +.pulse-orb.playing { + background: linear-gradient( + 135deg, + #7df9ff, + #6c63ff, + #ff7ad9, + #ffd36a, + #7df9ff + ); + background-size: 240% 240%; + animation: gradientShift 6s ease-in-out infinite; +} + +.pulse-orb.answer { + background: radial-gradient(circle at 30% 30%, #6bff9f, #1ecb5a 65%, #0b5c2b); + box-shadow: + 0 0 calc(18px + var(--pulse, 0) * 90px) rgba(43, 220, 123, 0.6), + 0 0 calc(40px + var(--pulse, 0) * 160px) rgba(43, 220, 123, 0.3); } .pulse-orb.active { animation: pulseGlow 1.6s ease-in-out infinite; } -.player-controls { - display: grid; - gap: 12px; - place-items: center; +.pulse-rays { + position: absolute; + inset: -20%; + border-radius: 50%; + background: + conic-gradient( + from 0deg, + hsla(var(--ray-hue, 200), 90%, 70%, 0.3) 0deg, + transparent 30deg, + hsla(var(--ray-hue, 200), 90%, 70%, 0.45) 60deg, + transparent 95deg, + hsla(var(--ray-hue, 200), 90%, 70%, 0.25) 130deg, + transparent 170deg, + hsla(var(--ray-hue, 200), 90%, 70%, 0.35) 210deg, + transparent 250deg, + hsla(var(--ray-hue, 200), 90%, 70%, 0.4) 300deg, + transparent 340deg + ); + opacity: calc(0.1 + var(--ray, 0) * 0.9); + filter: blur(6px); + transform: scale(calc(0.9 + var(--ray, 0) * 0.6)); + transition: opacity 0.1s ease-out, transform 0.1s ease-out; + z-index: 1; } -.player-meta { - text-align: center; - font-size: 0.85rem; - color: rgba(255, 255, 255, 0.7); -} - -.player-meta .label { - text-transform: uppercase; - letter-spacing: 0.2em; - font-size: 0.65rem; - display: block; -} - -.player-meta .value { - display: block; - color: #f6f4ef; - font-weight: 600; - margin-top: 4px; +.pulse-rays.active { + animation: raySpin 6s linear infinite; } @keyframes pulseGlow { @@ -445,6 +482,27 @@ audio.hidden-audio { } } +@keyframes gradientShift { + 0% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + 100% { + background-position: 0% 50%; + } +} + +@keyframes raySpin { + from { + transform: scale(calc(0.9 + var(--ray, 0) * 0.6)) rotate(0deg); + } + to { + transform: scale(calc(0.9 + var(--ray, 0) * 0.6)) rotate(360deg); + } +} + .player-empty { color: rgba(255, 255, 255, 0.6); }