// ============================================================
// 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 (
);
}
return (
SPEC.AGT — LIVE
ID / {shown.callsign ?? shown.id.toUpperCase()}
{shown.image && (

{ e.currentTarget.style.display = "none"; }}
/>
)}
{shown.initials}
ON AIR
{shown.name}
{shown.role}
- ORIGIN
- {shown.origin}
- VERSE
- {shown.verse}
- STATUS
- INFECTED
{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 (
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")}%
);
}
// ---------- 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,
});