// ============================================================ // Extras - marquee ticker + flicker-on intro overlay // ============================================================ // Retro scrolling marquee under the header -------------------------------- const MARQUEE_ITEMS = [ 'NOW BOOKING COMMISSIONS', 'GLOW DROPS', 'QUOTES WITHIN 72H', 'ANIME · VINTAGE · FIDGET TOYS · STATUES', 'PALM-SIZED TO 12 INCHES', 'DESIGN · PRINT · CREATE', ]; function Marquee() { const row = (keyPrefix) => MARQUEE_ITEMS.map((t, i) => { const accents = ['var(--cyan)', 'var(--magenta)', 'var(--sunset)', 'var(--vapor-purple)']; const c = `hsl(${accents[i % accents.length]})`; return ( {t} ); }); return (
); } // Tiny generated audio logo: beach wave -> tape warp -> synth pad ---------- function playFreshWaveIntroSound() { if (typeof window === 'undefined') return Promise.resolve(false); const AudioContextCtor = window.AudioContext || window.webkitAudioContext; if (!AudioContextCtor) return Promise.resolve(false); if (window.__fwIntroSoundActive) return Promise.resolve(true); const ctx = window.__fwAudioContext || new AudioContextCtor(); window.__fwAudioContext = ctx; const schedule = () => { if (ctx.state === 'suspended') return false; window.__fwIntroSoundActive = true; window.setTimeout(() => { window.__fwIntroSoundActive = false; }, 2600); const now = ctx.currentTime + 0.025; const master = ctx.createGain(); master.gain.setValueAtTime(0.0001, now); master.gain.linearRampToValueAtTime(0.24, now + 0.08); master.gain.exponentialRampToValueAtTime(0.0001, now + 2.35); master.connect(ctx.destination); const makeNoise = (duration, color = 0.985) => { const buffer = ctx.createBuffer(1, Math.ceil(ctx.sampleRate * duration), ctx.sampleRate); const data = buffer.getChannelData(0); let last = 0; for (let i = 0; i < data.length; i += 1) { const white = Math.random() * 2 - 1; last = (last * color) + (white * (1 - color)); data[i] = last * 3.2; } const source = ctx.createBufferSource(); source.buffer = buffer; return source; }; // Faint CRT hiss underneath the whole boot. const staticSource = makeNoise(1.9, 0.52); const staticFilter = ctx.createBiquadFilter(); const staticGain = ctx.createGain(); staticFilter.type = 'highpass'; staticFilter.frequency.setValueAtTime(1700, now); staticGain.gain.setValueAtTime(0.012, now); staticGain.gain.exponentialRampToValueAtTime(0.0001, now + 1.8); staticSource.connect(staticFilter); staticFilter.connect(staticGain); staticGain.connect(master); staticSource.start(now); staticSource.stop(now + 1.9); // Soft ocean wave that establishes "Fresh Wave" without becoming literal beach ambience. const wave = makeNoise(1.15, 0.992); const waveLow = ctx.createBiquadFilter(); const waveHigh = ctx.createBiquadFilter(); const waveGain = ctx.createGain(); waveLow.type = 'lowpass'; waveLow.frequency.setValueAtTime(360, now); waveLow.frequency.linearRampToValueAtTime(1380, now + 0.20); waveLow.frequency.exponentialRampToValueAtTime(460, now + 0.95); waveHigh.type = 'highpass'; waveHigh.frequency.setValueAtTime(55, now); waveGain.gain.setValueAtTime(0.0001, now); waveGain.gain.linearRampToValueAtTime(0.18, now + 0.16); waveGain.gain.exponentialRampToValueAtTime(0.012, now + 0.95); wave.connect(waveLow); waveLow.connect(waveHigh); waveHigh.connect(waveGain); waveGain.connect(master); wave.start(now); wave.stop(now + 1.15); // Tape-warp bridge: the beach wave stretches into signal wave. const warp = ctx.createOscillator(); const warpFilter = ctx.createBiquadFilter(); const warpGain = ctx.createGain(); warp.type = 'sawtooth'; warp.frequency.setValueAtTime(112, now + 0.42); warp.frequency.exponentialRampToValueAtTime(52, now + 0.78); warp.frequency.linearRampToValueAtTime(84, now + 1.05); warpFilter.type = 'bandpass'; warpFilter.frequency.setValueAtTime(720, now + 0.42); warpFilter.frequency.exponentialRampToValueAtTime(310, now + 1.05); warpFilter.Q.setValueAtTime(3.6, now); warpGain.gain.setValueAtTime(0.0001, now + 0.40); warpGain.gain.linearRampToValueAtTime(0.045, now + 0.62); warpGain.gain.exponentialRampToValueAtTime(0.0001, now + 1.16); warp.connect(warpFilter); warpFilter.connect(warpGain); warpGain.connect(master); warp.start(now + 0.40); warp.stop(now + 1.18); // VHS/PS1-ish synth pad: a place opening, not a song starting. const padGain = ctx.createGain(); const padFilter = ctx.createBiquadFilter(); padGain.gain.setValueAtTime(0.0001, now + 0.72); padGain.gain.linearRampToValueAtTime(0.075, now + 1.05); padGain.gain.exponentialRampToValueAtTime(0.0001, now + 2.30); padFilter.type = 'lowpass'; padFilter.frequency.setValueAtTime(620, now + 0.72); padFilter.frequency.linearRampToValueAtTime(1180, now + 1.36); padFilter.Q.setValueAtTime(0.7, now); [146.83, 220.0, 293.66].forEach((freq, index) => { const osc = ctx.createOscillator(); const voiceGain = ctx.createGain(); osc.type = index === 1 ? 'triangle' : 'sine'; osc.frequency.setValueAtTime(freq, now); osc.detune.setValueAtTime(index === 0 ? -7 : index === 1 ? 4 : 11, now); voiceGain.gain.setValueAtTime(index === 1 ? 0.55 : 0.38, now); osc.connect(voiceGain); voiceGain.connect(padFilter); osc.start(now + 0.72); osc.stop(now + 2.35); }); padFilter.connect(padGain); padGain.connect(master); return true; }; const resume = ctx.state === 'suspended' ? ctx.resume() : Promise.resolve(); return resume.then(schedule).catch(() => false); } // Flicker-on intro overlay (once per browser session) --------------------- function IntroOverlay() { const reduce = typeof window !== 'undefined' && window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; const seen = typeof sessionStorage !== 'undefined' && sessionStorage.getItem('fw-intro-seen') === '1'; const [show, setShow] = useState(!seen && !reduce); const [done, setDone] = useState(false); useEffect(() => { if (!show) return; playFreshWaveIntroSound(); const t1 = setTimeout(() => setDone(true), 2100); const t2 = setTimeout(() => { setShow(false); try { sessionStorage.setItem('fw-intro-seen', '1'); } catch (e) {} }, 2800); return () => { clearTimeout(t1); clearTimeout(t2); }; }, [show]); const dismiss = () => { setDone(true); setTimeout(() => setShow(false), 500); try { sessionStorage.setItem('fw-intro-seen', '1'); } catch (e) {} }; if (!show) return null; return (