// ============================================================ // MISSION : INFECT — epideM:Ic | components // ============================================================ const { useState, useEffect, useRef, useMemo } = React; // ---------- utils ---------- const fmt = (s) => { if (!isFinite(s) || s < 0) s = 0; const m = Math.floor(s / 60); const r = Math.floor(s % 60); return `${m}:${r.toString().padStart(2, "0")}`; }; // ---------- ArtistCard: swaps when current verse changes ---------- function ArtistCard({ artist }) { const [shown, setShown] = useState(artist); const [glitch, setGlitch] = useState(false); useEffect(() => { if (!artist || shown?.id === artist?.id) return; setGlitch(true); const t1 = setTimeout(() => setShown(artist), 220); const t2 = setTimeout(() => setGlitch(false), 520); return () => { clearTimeout(t1); clearTimeout(t2); }; }, [artist?.id]); if (!shown) { return (
NO SIGNAL
); } return (
SPEC.AGT — LIVE ID / {shown.callsign ?? shown.id.toUpperCase()}
{shown.image && ( {`${shown.name} { e.currentTarget.style.display = "none"; }} /> )}
{shown.initials}
ON AIR
{shown.name}
{shown.role}
ORIGIN
{shown.origin}
VERSE
{shown.verse}
STATUS
INFECTED
{shown.links.map((l) => ( {l.icon} {l.label} ))}
{Array.from({ length: 28 }).map((_, i) => ( ))}
TRANSMISSION STABLE · PAYLOAD: VOCAL
); } // ---------- Player ---------- function Player({ track, isPlaying, progress, duration, onToggle, onSeek, onPrev, onNext, volume, onVolume, trackIndex, trackCount, }) { const pct = duration ? (progress / duration) * 100 : 0; return (
epideM:Ic cover
TRK {(trackIndex + 1).toString().padStart(2, "0")}/{trackCount.toString().padStart(2, "0")} {isPlaying ? "STREAMING" : "STANDBY"}
{track.title}
{track.artistIds.map((id, i) => ( {i > 0 && ×} {ARTISTS[id].name} ))}
{fmt(progress)}
{track.verses.map((v) => { const active = progress >= v.start && progress < v.end; return ( {v.label} ); })}
-{fmt(Math.max(0, duration - progress))}
{pct.toFixed(1).padStart(4, "0")}%
onVolume(parseFloat(e.target.value))} /> {Math.round(volume * 100).toString().padStart(2, "0")}
LOSSLESS · 24/96
); } // ---------- Waveform (procedural, seekable) ---------- function Waveform({ track, progress, duration, onSeek }) { const ref = useRef(null); const bars = useMemo(() => { // Deterministic per-track shape const seed = track.id.split("").reduce((a, c) => a + c.charCodeAt(0), 0); const rnd = (i) => { const x = Math.sin((seed + i) * 12.9898) * 43758.5453; return x - Math.floor(x); }; const N = 120; const arr = []; for (let i = 0; i < N; i++) { const t = i / N; // Envelope: builds, choruses, drops const env = 0.35 + 0.45 * Math.sin(t * Math.PI) + 0.18 * Math.sin(t * Math.PI * 6 + rnd(i) * 2) + 0.10 * rnd(i * 3); arr.push(Math.max(0.08, Math.min(1, env))); } return arr; }, [track.id]); const pct = duration ? progress / duration : 0; const handleClick = (e) => { if (!ref.current || !duration) return; const r = ref.current.getBoundingClientRect(); const x = (e.clientX - r.left) / r.width; onSeek(Math.max(0, Math.min(1, x)) * duration); }; return (
{track.verses.map((v) => { const left = (v.start / duration) * 100; const w = ((v.end - v.start) / duration) * 100; return (
{v.label}
); })}
{bars.map((h, i) => { const filled = i / bars.length <= pct; return ( ); })}
{fmt(progress)}
); } // ---------- VU meter ---------- function VU({ playing }) { const [vals, setVals] = useState(Array.from({ length: 12 }, () => 0.2)); useEffect(() => { if (!playing) { setVals((v) => v.map(() => 0.05)); return; } const id = setInterval(() => { setVals((v) => v.map(() => 0.2 + Math.random() * 0.8)); }, 90); return () => clearInterval(id); }, [playing]); return (
{vals.map((v, i) => (
))}
); } // ---------- Tracklist ---------- function Tracklist({ tracks, currentIdx, onPick, isPlaying, progress }) { return (
# TITLE / PERSONNEL PAYLOAD RUN
{tracks.map((t, i) => { const active = i === currentIdx; const trackPct = active && t.duration ? (progress / t.duration) * 100 : 0; return ( ); })}
); } // Export Object.assign(window, { ArtistCard, Player, Tracklist, Waveform, VU, fmt, });