Add background narration track with sound toggle
Click-to-play M4A narration, looping with fade in and out, behind a bottom-left Sound on/off button. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d57194b8dd
commit
aebcf40fa9
3 changed files with 62 additions and 0 deletions
BIN
app/public/audio/we-build-tomorrow.m4a
Normal file
BIN
app/public/audio/we-build-tomorrow.m4a
Normal file
Binary file not shown.
|
|
@ -1,4 +1,5 @@
|
|||
import { Nav } from '@/components/Nav'
|
||||
import { SoundToggle } from '@/components/SoundToggle'
|
||||
import { ScrollScene } from '@/components/ScrollScene'
|
||||
import { Intro } from '@/components/Intro'
|
||||
import { Services, WhyUs } from '@/components/Services'
|
||||
|
|
@ -13,6 +14,7 @@ export default function App() {
|
|||
Skip the fly-through
|
||||
</a>
|
||||
<Nav />
|
||||
<SoundToggle />
|
||||
<main>
|
||||
<ScrollScene />
|
||||
<Intro />
|
||||
|
|
|
|||
60
app/src/components/SoundToggle.tsx
Normal file
60
app/src/components/SoundToggle.tsx
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Volume2, VolumeX } from 'lucide-react'
|
||||
|
||||
const SRC = '/audio/we-build-tomorrow.m4a'
|
||||
const VOLUME = 0.6
|
||||
|
||||
// Background narration. Browsers block sound until the visitor asks for it,
|
||||
// so it starts on click, loops, and fades in and out rather than cutting.
|
||||
export function SoundToggle() {
|
||||
const audio = useRef<HTMLAudioElement | null>(null)
|
||||
const fade = useRef<number | undefined>(undefined)
|
||||
const [on, setOn] = useState(false)
|
||||
|
||||
const ramp = (to: number, done?: () => void) => {
|
||||
const a = audio.current
|
||||
if (!a) return
|
||||
window.clearInterval(fade.current)
|
||||
fade.current = window.setInterval(() => {
|
||||
const step = to > a.volume ? 0.05 : -0.05
|
||||
a.volume = Math.min(1, Math.max(0, a.volume + step))
|
||||
if (Math.abs(a.volume - to) < 0.051) {
|
||||
a.volume = to
|
||||
window.clearInterval(fade.current)
|
||||
done?.()
|
||||
}
|
||||
}, 40)
|
||||
}
|
||||
|
||||
useEffect(() => () => window.clearInterval(fade.current), [])
|
||||
|
||||
const toggle = () => {
|
||||
if (!audio.current) {
|
||||
const a = new Audio(SRC)
|
||||
a.loop = true
|
||||
a.volume = 0
|
||||
audio.current = a
|
||||
}
|
||||
const a = audio.current
|
||||
if (on) {
|
||||
ramp(0, () => a.pause())
|
||||
setOn(false)
|
||||
} else {
|
||||
a.play().then(() => ramp(VOLUME)).catch(() => setOn(false))
|
||||
setOn(true)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggle}
|
||||
aria-pressed={on}
|
||||
aria-label={on ? 'Mute background narration' : 'Play background narration'}
|
||||
className="fixed bottom-5 left-5 z-[60] inline-flex h-11 items-center gap-2.5 rounded-full border border-bone/25 bg-ink/70 px-4 text-[13px] font-medium text-bone backdrop-blur-md transition-colors hover:border-gold hover:text-gold sm:bottom-7 sm:left-7"
|
||||
>
|
||||
{on ? <Volume2 size={16} strokeWidth={1.75} className="text-gold" /> : <VolumeX size={16} strokeWidth={1.75} />}
|
||||
<span className="hidden sm:inline">{on ? 'Sound on' : 'Sound off'}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in a new issue