/* global React, ReactDOM, TweaksPanel, useTweaks, TweakSection, TweakSlider, TweakRadio, TweakSelect, TweakToggle, TweakColor, TweakButton */
const { useState, useEffect, useRef, useMemo, useCallback } = React;
const LOGO = "logo.png";
// ============================================================
// SCENES
// ============================================================
function ScenePulse({ glitch }) {
return (
);
}
const GLITCH_WORDS = ["DECAY", "VOID", "MACHINE", "FLESH", "STATIC", "NEON", "BLOOD", "CRTRL", "DRONE", "PULSE", "FAITH", "CIRCUIT", "CORPSE", "BINARY", "EUTHANOIZE"];
function SceneGlitchText({ glitch }) {
const [w, setW] = useState(GLITCH_WORDS[0]);
useEffect(() => {
let i = 0;
const id = setInterval(() => {
i = (i + 1) % GLITCH_WORDS.length;
setW(GLITCH_WORDS[i]);
}, 1100);
return () => clearInterval(id);
}, []);
return (
{w}
SIG ▼ CORRUPT
BUF ▼ 0xFA9E
RX ▼ {Math.floor(Math.random()*9999)}
);
}
function SceneTunnel({ tunnelOpts }) {
const {
rings = 18,
cycle = 5,
rotSpeed = 4, // seconds per full 360° rotation
shape = "square",
color = "white",
yaw = true,
} = tunnelOpts || {};
const items = [];
for (let i = 0; i < rings; i++) {
const flyDelay = -(cycle / rings) * i;
const colorClass =
color === "accent" ? "tr-accent" :
color === "mix" ? (i % 2 === 0 ? "tr-accent" : "") :
"";
items.push(
);
}
return (
);
}
function SceneStrobe({ glitch }) {
return (
▰▰▰ 124 BPM ▰▰▰ KICK ▰▰▰ KICK ▰▰▰
);
}
function SceneOscilloscope() {
const canvasRef = useRef(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
let raf;
const resize = () => {
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
};
resize();
window.addEventListener("resize", resize);
let t = 0;
const draw = () => {
const w = canvas.width, h = canvas.height;
ctx.clearRect(0, 0, w, h);
// Glow trails
ctx.lineCap = "round";
ctx.shadowBlur = 18;
ctx.shadowColor = "oklch(0.78 0.13 220)";
ctx.strokeStyle = "rgba(190,230,255,0.95)";
ctx.lineWidth = 2.5;
ctx.beginPath();
const cy = h / 2;
for (let x = 0; x < w; x += 2) {
const px = x / w;
const y =
Math.sin(px * 18 + t * 1.3) * 60 +
Math.sin(px * 5 + t * 0.6) * 100 * Math.sin(t * 0.4) +
Math.sin(px * 60 + t * 4) * 12 +
Math.cos(px * 3 + t * 0.8) * 80;
if (x === 0) ctx.moveTo(x, cy + y);
else ctx.lineTo(x, cy + y);
}
ctx.stroke();
// Second wave (inverse, red)
ctx.shadowColor = "oklch(0.55 0.22 25)";
ctx.strokeStyle = "rgba(220,90,90,0.6)";
ctx.lineWidth = 1.5;
ctx.beginPath();
for (let x = 0; x < w; x += 2) {
const px = x / w;
const y =
Math.sin(px * 9 - t * 0.9) * 80 +
Math.sin(px * 25 + t * 2) * 40 +
Math.cos(px * 4 - t * 0.5) * 60;
if (x === 0) ctx.moveTo(x, cy + y);
else ctx.lineTo(x, cy + y);
}
ctx.stroke();
t += 0.04;
raf = requestAnimationFrame(draw);
};
draw();
return () => {
cancelAnimationFrame(raf);
window.removeEventListener("resize", resize);
};
}, []);
return (
);
}
function SceneOccult() {
// distribute tick marks around an outer ring
const ticks = 24;
const tickEls = [];
for (let i = 0; i < ticks; i++) {
const ang = (i / ticks) * 360;
const label = ["I","II","III","IV","V","VI","VII","VIII","IX","X","XI","XII"][i % 12];
tickEls.push(
{label}
);
}
return (
— I N — N O M I N E — M A C H I N A —
);
}
function SceneRorschach() {
const [phrase, setPhrase] = useState("INDUSTRIAL");
const phrases = ["INDUSTRIAL", "ELECTRO·GOTH", "EBM", "CLUB·HELL", "DEUS·EX", "NOIR"];
useEffect(() => {
let i = 0;
const id = setInterval(() => {
i = (i + 1) % phrases.length;
setPhrase(phrases[i]);
}, 2400);
return () => clearInterval(id);
}, []);
return (
{phrase}
// RORSCHACH ▸ LIVE
);
}
function SceneData() {
// Random bar widths memoized so the stripe is stable
const barcode = useMemo(() => {
const arr = [];
for (let i = 0; i < 80; i++) {
arr.push(2 + Math.floor(Math.random() * 9));
}
return arr;
}, []);
return (
CLUBHELL
▶ INDUSTRIAL
EBM
CLUBHELL
▶ ELECTRO
GOTH
CLUBHELL
▶ INDUSTRIAL
EBM
CLUBHELL
▶ ELECTRO
GOTH
{barcode.map((w, i) => (
))}
EUTHANOIZE
▮ STATIC
FLESH
VOID
▮ NEON
DRONE
EUTHANOIZE
▮ STATIC
FLESH
VOID
▮ NEON
DRONE
);
}
// ============================================================
// SCENE LIST + PLAYER
// ============================================================
const SCENES = [
{ id: "pulse", name: "PULSE", Comp: ScenePulse, dur: 16 },
{ id: "glitch", name: "GLITCH TEXT", Comp: SceneGlitchText, dur: 18 },
{ id: "tunnel", name: "TUNNEL", Comp: SceneTunnel, dur: 20 },
{ id: "strobe", name: "STROBE", Comp: SceneStrobe, dur: 14 },
{ id: "osc", name: "OSCILLOSCOPE", Comp: SceneOscilloscope, dur: 18 },
{ id: "occult", name: "OCCULT GRID", Comp: SceneOccult, dur: 22 },
{ id: "ror", name: "RORSCHACH", Comp: SceneRorschach, dur: 16 },
{ id: "data", name: "DATA STREAM", Comp: SceneData, dur: 16 },
];
function App() {
const [t, setTweak] = useTweaks(window.TWEAK_DEFAULTS);
const [idx, setIdx] = useState(0);
const wipeRef = useRef(null);
const wsRef = useRef(null);
const pausedRef = useRef(t.paused); // updated every render, guards the timer callback
// Refs so WS closure always has latest setters without reconnecting
const setTweakRef = useRef(setTweak);
const setIdxRef = useRef(setIdx);
setTweakRef.current = setTweak;
setIdxRef.current = setIdx;
pausedRef.current = t.paused;
// Filter scenes based on which are enabled
const activeScenes = useMemo(
() => SCENES.filter(s => t.enabled[s.id] !== false),
[t.enabled]
);
// safety
const safeIdx = activeScenes.length === 0 ? 0 : idx % activeScenes.length;
const current = activeScenes[safeIdx] || SCENES[0];
// auto-advance
useEffect(() => {
if (activeScenes.length === 0) return;
if (t.paused) return;
const dur = (current.dur * 1000) / t.speed;
const id = setTimeout(() => {
if (pausedRef.current) return; // guard: pause pressed while timer was running
if (wipeRef.current) {
wipeRef.current.classList.remove("flash");
void wipeRef.current.offsetWidth;
wipeRef.current.classList.add("flash");
}
setIdx(i => (i + 1) % activeScenes.length);
}, dur);
return () => clearTimeout(id);
}, [safeIdx, activeScenes.length, t.speed, t.paused, current.dur]);
// HUD update
useEffect(() => {
const el = document.getElementById("hud-scn");
if (el) el.textContent = String(safeIdx + 1).padStart(2, "0");
}, [safeIdx]);
// Apply palette to CSS variables
useEffect(() => {
const r = document.documentElement;
const palettes = {
blood: { accent: "oklch(0.55 0.22 25)", cool: "oklch(0.78 0.13 220)" },
cyan: { accent: "oklch(0.78 0.13 220)", cool: "oklch(0.55 0.22 25)" },
mono: { accent: "#f4ebe4", cool: "#a8a39e" },
acid: { accent: "oklch(0.78 0.20 130)", cool: "oklch(0.65 0.20 320)" },
};
const p = palettes[t.palette] || palettes.blood;
r.style.setProperty("--accent", p.accent);
r.style.setProperty("--cool", p.cool);
}, [t.palette]);
// Random vignette flicker
useEffect(() => {
const fl = document.getElementById("flicker");
if (!fl) return;
let raf, last = 0;
const tick = (now) => {
if (now - last > 500 + Math.random() * 4000) {
last = now;
if (Math.random() < 0.5 * t.flicker) {
fl.style.opacity = String(0.7 + Math.random() * 0.3);
fl.style.background = Math.random() < 0.7 ? "#000" : "#fff";
setTimeout(() => { fl.style.opacity = "0"; }, 60 + Math.random() * 80);
}
}
raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, [t.flicker]);
// Attach wipeRef to the DOM element (it lives outside the React root)
useEffect(() => {
wipeRef.current = document.getElementById('wipe');
}, []);
// WebSocket — connect once, reconnect on close
useEffect(() => {
function triggerFlash() {
const wipe = document.getElementById('wipe');
if (wipe) {
wipe.classList.remove('flash');
void wipe.offsetWidth;
wipe.classList.add('flash');
}
const overlay = document.getElementById('euthanoize-overlay');
if (overlay) {
overlay.classList.remove('show');
void overlay.offsetWidth;
overlay.classList.add('show');
}
}
function connect() {
let ws;
const url = window.WS_URL || `ws://${location.host}`;
try { ws = new WebSocket(url); }
catch(e) { setTimeout(connect, 3000); return; }
wsRef.current = ws;
ws.onmessage = (evt) => {
try {
const msg = JSON.parse(evt.data);
if (msg.type === 'SET_TWEAK') setTweakRef.current(msg.key, msg.value);
if (msg.type === 'JUMP_SCENE') setIdxRef.current(msg.index);
if (msg.type === 'FLASH') triggerFlash();
} catch(e) {}
};
ws.onclose = () => setTimeout(connect, 2000);
}
connect();
return () => { if (wsRef.current) wsRef.current.close(); };
}, []);
// Broadcast state whenever scene or tweaks change
useEffect(() => {
const ws = wsRef.current;
if (!ws || ws.readyState !== WebSocket.OPEN) return;
ws.send(JSON.stringify({
type: 'STATE',
state: {
currentScene: safeIdx,
sceneId: current.id,
sceneName: current.name,
totalScenes: activeScenes.length,
tweaks: t,
scenes: activeScenes.map(s => ({ id: s.id, name: s.name })),
}
}));
}, [safeIdx, t, activeScenes.length]);
// Manual scene jump
useEffect(() => {
const handler = (e) => {
if (e.key === "ArrowRight" || e.key === " ") setIdx(i => (i + 1) % Math.max(activeScenes.length, 1));
if (e.key === "ArrowLeft") setIdx(i => (i - 1 + Math.max(activeScenes.length,1)) % Math.max(activeScenes.length, 1));
if (e.key === "f" || e.key === "F") {
if (!document.fullscreenElement) document.documentElement.requestFullscreen?.();
else document.exitFullscreen?.();
}
if (e.key === "h" || e.key === "H") {
document.body.classList.toggle("show-cursor");
const hud = document.getElementById("hud");
if (hud) hud.style.display = hud.style.display === "none" ? "" : "none";
}
if (e.key === "t" || e.key === "T") {
// Toggle Tweaks panel via host protocol
const open = document.querySelector(".twk-panel");
if (open) {
window.postMessage({ type: "__deactivate_edit_mode" }, "*");
window.parent.postMessage({ type: "__edit_mode_dismissed" }, "*");
} else {
window.postMessage({ type: "__activate_edit_mode" }, "*");
}
}
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [activeScenes.length]);
const Comp = current.Comp;
return (
<>
setTweak("speed", v)} />
setTweak("paused", v)} />
setTweak("glitch", v)} />
setTweak("flicker", v)} />
setTweak("palette", v)}
/>
setTweak("tunnel", { ...t.tunnel, rings: v })} />
setTweak("tunnel", { ...t.tunnel, cycle: v })} />
setTweak("tunnel", { ...t.tunnel, rotSpeed: v })} />
setTweak("tunnel", { ...t.tunnel, shape: v })}
/>
setTweak("tunnel", { ...t.tunnel, color: v })}
/>
setTweak("tunnel", { ...t.tunnel, yaw: v })} />
{SCENES.map((s, i) => (
setTweak("enabled", { ...t.enabled, [s.id]: v })}
/>
))}
{activeScenes.map((s, i) => (
setIdx(i)}
/>
))}
T · toggle this panel
← / → · jump scene
SPACE · next
F · fullscreen
H · hide HUD
>
);
}
// Tweak defaults — written to disk via __edit_mode_set_keys
window.TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"speed": 1,
"glitch": 1,
"flicker": 1,
"paused": false,
"palette": "blood",
"enabled": {
"pulse": true,
"glitch": true,
"tunnel": true,
"strobe": true,
"osc": true,
"occult": true,
"ror": true,
"data": true
},
"tunnel": {
"rings": 18,
"cycle": 5,
"rotSpeed": 4,
"shape": "square",
"color": "white",
"yaw": true
}
}/*EDITMODE-END*/;
ReactDOM.createRoot(document.getElementById("root")).render();