Skip to content

Instantly share code, notes, and snippets.

@mode-mercury
Created December 12, 2025 02:46
Show Gist options
  • Select an option

  • Save mode-mercury/5a1d8395332a3d2a9f86b610554e62e4 to your computer and use it in GitHub Desktop.

Select an option

Save mode-mercury/5a1d8395332a3d2a9f86b610554e62e4 to your computer and use it in GitHub Desktop.
Untitled
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>PROJECT ORACLE-93 · Spectral Collage Engine</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://fonts.googleapis.com/css2?family=Special+Elite&display=swap" rel="stylesheet">
<style>
:root {
--bg: #050403;
--paper: #f6f0df;
--ink: #1c130c;
--accent: #c8933b;
--accent2: #7e5530;
--danger: #c74b3b;
--mono: "Special Elite", "Courier New", monospace;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
background: radial-gradient(circle at top, #272018 0, #050403 55%, #000 100%);
color: var(--ink);
font-family: var(--mono);
-webkit-font-smoothing: antialiased;
}
body::before {
content: "";
position: fixed;
inset: 0;
background-image:
radial-gradient(circle at 50% 0, rgba(255,255,255,0.15) 0, transparent 50%),
linear-gradient(0deg, rgba(0,0,0,0.7), transparent 30%, transparent 70%, rgba(0,0,0,0.7));
mix-blend-mode: soft-light;
pointer-events: none;
opacity: 0.7;
}
.scanlines {
position: fixed;
inset: 0;
pointer-events: none;
background-image: repeating-linear-gradient(
to bottom,
rgba(0,0,0,0.25) 0,
rgba(0,0,0,0.25) 1px,
transparent 2px,
transparent 3px
);
opacity: 0.6;
mix-blend-mode: multiply;
z-index: 10;
}
.app {
max-width: 1200px;
height: 100vh;
margin: 0 auto;
padding: 6px 10px 10px;
display: flex;
flex-direction: column;
position: relative;
}
header {
flex-shrink: 0;
padding-bottom: 4px;
border-bottom: 1px dashed rgba(0,0,0,0.25);
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 8px;
}
header .title-block {
display: flex;
flex-direction: column;
gap: 2px;
}
header h1 {
margin: 0;
font-size: 1.1rem;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--accent);
}
header h1 span {
border: 1px solid rgba(0,0,0,0.7);
padding: 2px 6px;
background: rgba(0,0,0,0.08);
}
header .tagline {
font-size: 0.65rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: rgba(0,0,0,0.7);
}
header .stamp {
font-size: 0.7rem;
padding: 2px 6px;
border-radius: 3px;
border: 1px solid var(--danger);
color: var(--danger);
text-transform: uppercase;
letter-spacing: 0.2em;
transform: rotate(-2deg);
box-shadow: 0 0 0 1px rgba(0,0,0,0.6);
}
.main {
flex: 1;
display: flex;
gap: 10px;
padding-top: 6px;
min-height: 0;
}
.panel {
background: var(--paper);
border-radius: 6px;
border: 1px solid rgba(0,0,0,0.55);
box-shadow:
0 14px 30px rgba(0,0,0,0.65),
inset 0 0 0 1px rgba(255,255,255,0.35);
position: relative;
overflow: hidden;
}
.panel::before {
content: "";
position: absolute;
inset: 0;
background-image:
linear-gradient(135deg, rgba(255,255,255,0.15), transparent 45%, rgba(0,0,0,0.18)),
radial-gradient(circle at 10% 10%, rgba(255,255,255,0.3) 0, transparent 40%);
mix-blend-mode: soft-light;
opacity: 0.65;
pointer-events: none;
}
.panel-main {
flex: 2.1;
display: flex;
flex-direction: column;
min-width: 0;
}
.panel-side {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
.panel-header {
padding: 4px 8px;
border-bottom: 1px dashed rgba(0,0,0,0.35);
position: relative;
z-index: 2;
}
.panel-header-title {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.16em;
color: rgba(0,0,0,0.8);
display: flex;
justify-content: space-between;
align-items: center;
gap: 6px;
}
.panel-header-sub {
font-size: 0.6rem;
color: rgba(0,0,0,0.6);
margin-top: 2px;
display: flex;
justify-content: space-between;
gap: 8px;
flex-wrap: wrap;
}
.btn-row {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 2px;
}
.btn {
border-radius: 3px;
border: 1px solid rgba(0,0,0,0.7);
background: linear-gradient(180deg,#f1e6d0,#d7c39e);
padding: 2px 8px;
font-size: 0.65rem;
text-transform: uppercase;
letter-spacing: 0.12em;
cursor: pointer;
position: relative;
}
.btn::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(180deg,rgba(255,255,255,0.7),transparent 40%);
mix-blend-mode: screen;
opacity: 0.7;
pointer-events: none;
}
.btn:hover {
background: linear-gradient(180deg,#fff6e3,#dfcda8);
}
.btn.active {
box-shadow:
0 0 0 1px rgba(0,0,0,0.8),
inset 0 0 0 1px rgba(0,0,0,0.8);
background: linear-gradient(180deg,#fbe9c9,#d3b891);
}
.mini-btn {
border-radius: 3px;
border: 1px solid rgba(0,0,0,0.6);
background: linear-gradient(180deg,#f7ebd6,#e0c8a4);
padding: 1px 5px;
font-size: 0.6rem;
text-transform: uppercase;
letter-spacing: 0.12em;
cursor: pointer;
margin-left: 3px;
}
.mini-btn.active {
background: linear-gradient(180deg,#fbe9c9,#d3b891);
box-shadow: 0 0 0 1px rgba(0,0,0,0.8);
}
.badge {
border-radius: 999px;
border: 1px solid rgba(0,0,0,0.6);
padding: 1px 6px;
font-size: 0.55rem;
text-transform: uppercase;
letter-spacing: 0.12em;
background: rgba(0,0,0,0.05);
}
.badge-evp {
border-color: var(--danger);
color: var(--danger);
}
.canvas-wrap {
position: relative;
flex: 1;
min-height: 0;
margin: 6px 6px 0 6px;
border-radius: 4px;
overflow: hidden;
border: 1px solid rgba(0,0,0,0.7);
background: radial-gradient(circle at center,#3b2c23 0,#201610 50%,#050403 100%);
}
canvas {
width: 100%;
height: 100%;
display: block;
}
.hud-overlay {
position: absolute;
inset: 0;
pointer-events: none;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 4px 6px;
font-size: 0.6rem;
color: rgba(255,245,220,0.95);
text-shadow: 0 1px 0 rgba(0,0,0,0.8);
mix-blend-mode: screen;
}
.hud-top {
display: flex;
justify-content: space-between;
gap: 8px;
}
.hud-bottom {
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 8px;
}
.hud-box {
padding: 2px 4px;
border-radius: 2px;
border: 1px solid rgba(255,255,255,0.4);
background: rgba(0,0,0,0.45);
max-width: 60%;
}
.meter-bar {
width: 90px;
height: 6px;
border-radius: 3px;
border: 1px solid rgba(255,255,255,0.65);
background: rgba(0,0,0,0.7);
overflow: hidden;
position: relative;
}
.meter-fill {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 0%;
background: linear-gradient(90deg,#6bff6b,#ffe06b,#ff6b6b);
}
.tape-deck {
margin: 4px 6px 6px;
padding: 4px 6px;
border-radius: 4px;
border: 1px solid rgba(0,0,0,0.7);
background: linear-gradient(180deg,#f2e7d4,#d5c1a1);
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.6);
display: flex;
flex-direction: column;
gap: 4px;
position: relative;
z-index: 2;
}
.tape-deck-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
}
.deck-reels {
display: flex;
align-items: center;
gap: 14px;
}
.reel {
width: 32px;
height: 32px;
border-radius: 50%;
border: 2px solid rgba(0,0,0,0.8);
background: radial-gradient(circle at 30% 30%, #fff 0, #e1d4bd 35%, #b79c74 60%, #4b3a2a 100%);
position: relative;
box-shadow:
inset 0 0 0 2px rgba(255,255,255,0.7),
0 2px 4px rgba(0,0,0,0.5);
}
.reel::before,
.reel::after {
content: "";
position: absolute;
border-radius: 50%;
border: 1px solid rgba(0,0,0,0.8);
}
.reel::before {
width: 16px;
height: 16px;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
background: radial-gradient(circle, #fdf6e6 0, #ccb28c 70%, #3c2a1b 100%);
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.6);
}
.reel::after {
width: 4px;
height: 4px;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
background: #000;
}
.reel.spinning {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.tape-window {
flex: 1;
height: 18px;
border-radius: 10px;
border: 1px solid rgba(0,0,0,0.8);
background: linear-gradient(180deg,#2b2522,#101010);
box-shadow:
inset 0 0 0 1px rgba(255,255,255,0.3),
0 1px 0 rgba(255,255,255,0.25);
padding: 2px 4px;
display: flex;
align-items: center;
}
.tape-progress {
position: relative;
flex: 1;
height: 6px;
border-radius: 3px;
background: rgba(255,255,255,0.05);
overflow: hidden;
}
.tape-progress-fill {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 0%;
background: linear-gradient(90deg,#4bff8a,#ffe66b,#ff7b7b);
}
.tape-indicators {
font-size: 0.55rem;
margin-left: 6px;
color: rgba(0,0,0,0.8);
}
.tape-deck-bottom {
display: flex;
justify-content: space-between;
align-items: center;
gap: 6px;
font-size: 0.6rem;
}
.deck-buttons {
display: flex;
gap: 4px;
align-items: center;
}
.deck-label {
text-transform: uppercase;
letter-spacing: 0.12em;
font-size: 0.6rem;
color: rgba(0,0,0,0.7);
}
.deck-btn {
min-width: 40px;
}
.ticker-panel {
margin: 4px 6px 0;
padding: 4px 6px;
border-radius: 4px;
border: 1px solid rgba(0,0,0,0.7);
background: linear-gradient(180deg,#1a1512,#0a0604);
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.2);
position: relative;
z-index: 2;
height: 60px;
overflow: hidden;
}
.ticker-label {
font-size: 0.55rem;
color: #c8933b;
text-transform: uppercase;
letter-spacing: 0.12em;
margin-bottom: 2px;
}
.ticker-content {
font-size: 0.7rem;
color: #4bff8a;
text-shadow: 0 0 4px rgba(75,255,138,0.6);
line-height: 1.3;
height: 40px;
overflow-y: auto;
}
.ticker-word {
display: inline-block;
margin-right: 8px;
animation: tickerFlash 0.3s ease-in;
}
@keyframes tickerFlash {
0% { opacity: 0; transform: translateY(-5px); }
100% { opacity: 1; transform: translateY(0); }
}
.ticker-word.alert {
color: #ff6b6b;
text-shadow: 0 0 6px rgba(255,107,107,0.8);
font-weight: bold;
}
.simple-only { display: none; }
.advanced-only { display: block; }
.simple-view .panel-side {
display: none;
}
.simple-view .advanced-only {
display: none;
}
.simple-view .simple-only {
display: flex;
}
.simple-overlay {
margin: 4px 6px 6px;
padding: 6px;
border-radius: 4px;
border: 1px dashed rgba(0,0,0,0.7);
background: rgba(250,240,220,0.9);
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
text-align: center;
position: relative;
z-index: 2;
}
.simple-overlay-title {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.16em;
color: rgba(0,0,0,0.8);
}
.simple-overlay-sub {
font-size: 0.6rem;
color: rgba(0,0,0,0.65);
}
.conjure-btn {
margin-top: 4px;
padding: 6px 14px;
font-size: 0.8rem;
letter-spacing: 0.18em;
text-transform: uppercase;
border-radius: 4px;
border: 2px solid rgba(0,0,0,0.85);
background: radial-gradient(circle at top,#fff8e5,#e0c395);
cursor: pointer;
position: relative;
overflow: hidden;
}
.conjure-btn::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(120deg,rgba(255,255,255,0.8),transparent 40%);
mix-blend-mode: screen;
transform: translateX(-100%);
animation: sweep 2.5s linear infinite;
}
@keyframes sweep {
0% { transform: translateX(-120%); }
40% { transform: translateX(120%); }
100% { transform: translateX(120%); }
}
.conjure-btn.active {
box-shadow:
0 0 0 2px rgba(0,0,0,0.9),
0 0 14px rgba(200,60,60,0.7);
background: radial-gradient(circle at center,#fff,#f0c8a0);
}
.panel-side .body {
padding: 4px 6px 6px;
font-size: 0.65rem;
line-height: 1.35;
position: relative;
z-index: 2;
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.log-window {
flex: 1;
border-radius: 3px;
border: 1px solid rgba(0,0,0,0.7);
background: rgba(255,255,255,0.55);
padding: 4px;
overflow-y: auto;
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.6);
}
.log-line {
margin-bottom: 2px;
white-space: pre-wrap;
}
.log-time {
color: rgba(0,0,0,0.5);
}
.log-text {
color: rgba(0,0,0,0.85);
}
.field-row {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.field-tag {
border-radius: 3px;
border: 1px dashed rgba(0,0,0,0.6);
padding: 2px 4px;
font-size: 0.6rem;
background: rgba(255,255,255,0.65);
}
footer {
flex-shrink: 0;
padding-top: 3px;
font-size: 0.55rem;
color: rgba(0,0,0,0.65);
display: flex;
justify-content: space-between;
align-items: center;
}
footer code {
font-size: 0.56rem;
background: rgba(0,0,0,0.06);
padding: 1px 3px;
border-radius: 2px;
}
@media (max-width: 900px) {
.main {
flex-direction: column;
}
.panel-main, .panel-side {
flex: 1;
}
}
@media (max-width: 600px) {
header h1 {
font-size: 0.9rem;
}
header .tagline {
font-size: 0.6rem;
}
.panel-header-title {
flex-direction: column;
align-items: flex-start;
gap: 2px;
}
}
</style>
</head>
<body>
<div class="scanlines"></div>
<div class="app">
<header>
<div class="title-block">
<h1><span>PROJECT ORACLE-93</span> · FIELD TRANSDUCTION UNIT</h1>
<div class="tagline">Office of Unconventional Phenomena · Div. XIII · Circa 1939 (reconstructed)</div>
</div>
<div class="stamp">CLASSIFIED</div>
</header>
<div class="main">
<section class="panel panel-main">
<div class="panel-header">
<div class="panel-header-title">
<div>VISUAL COLLATION CHAMBER · MK-EVP / INFRARED (SIM)</div>
<div class="badge">CAM / MIC / HAPTIC DRIVEN</div>
</div>
<div class="panel-header-sub">
<div class="advanced-only">
<div class="btn-row">
<button class="btn" id="startBtn">INIT CAPTURE</button>
<button class="btn" id="flashBtn">FLASH</button>
<button class="btn" id="stopBtn">HALT</button>
<button class="btn" id="snapshotBtn">SNAPSHOT</button>
</div>
</div>
<div>
<div class="btn-row advanced-only">
<button class="btn active" id="toggleCamBtn">CAMERA</button>
<button class="btn active" id="toggleMicBtn">EVP</button>
<button class="btn active" id="toggleWarpBtn">WARP</button>
<button class="btn active" id="toggleSoundBtn">SOUND FX</button>
</div>
<div class="btn-row">
<span style="font-size:0.6rem;">Filters:</span>
<select id="filter1" class="mini-btn" style="padding:1px 3px;max-width:80px;">
<option value="">NONE</option>
</select>
<select id="filter2" class="mini-btn" style="padding:1px 3px;max-width:80px;">
<option value="">NONE</option>
</select>
<select id="filter3" class="mini-btn" style="padding:1px 3px;max-width:80px;">
<option value="">NONE</option>
</select>
<button class="mini-btn" id="randomFiltersBtn">RND</button>
</div>
<div class="btn-row">
<span style="font-size:0.6rem;">View:</span>
<button class="mini-btn" id="viewSimpleBtn">SIMPLE</button>
<button class="mini-btn active" id="viewAdvBtn">ADV</button>
</div>
</div>
</div>
</div>
<div class="canvas-wrap">
<canvas id="collageCanvas"></canvas>
<div class="hud-overlay">
<div class="hud-top">
<div class="hud-box">
CASE: <span id="caseLabel">RSW-047</span> · SITE: UNKNOWN DESERT<br>
MODE: REALTIME FIELD-GLUE COLLAGE
</div>
<div class="hud-box">
EVP LEVEL
<div class="meter-bar">
<div class="meter-fill" id="evpMeter"></div>
</div>
<div id="evpLabel">-</div>
</div>
</div>
<div class="hud-bottom">
<div class="hud-box">
INFRARED/RADAR/AURA FEEDS: <span style="color:#ffd27c;">ALGORITHMIC FROM SENSOR NOISE</span><br>
ORIENTATION / MOTION = FIELD DISTORTION · WHISPERS = COLLAGE INTENSITY
</div>
<div class="hud-box">
<span class="badge badge-evp">EVP</span>
DICTATION ENGINE ACTIVE · HIGH-GAIN MICROPHONE CHANNEL<br>
SPECTRAL VOCABULARY TRIGGER: <span id="vocabStatus">ARMED</span>
</div>
</div>
</div>
</div>
<div class="ticker-panel">
<div class="ticker-label">⚠ EVP DICTATION STREAM · REAL-TIME SPECTRAL VOCABULARY DETECTION</div>
<div class="ticker-content" id="tickerContent">
<span class="ticker-word">SYSTEM</span>
<span class="ticker-word">ONLINE</span>
<span class="ticker-word">LISTENING...</span>
</div>
</div>
<div class="tape-deck">
<div class="tape-deck-top">
<div class="deck-reels">
<div class="reel" id="reelLeft"></div>
<div class="tape-window">
<div class="tape-progress">
<div class="tape-progress-fill" id="tapeProgressFill"></div>
</div>
</div>
<div class="reel" id="reelRight"></div>
</div>
<div class="tape-indicators">
TAPE POS: <span id="tapeTime">00:00</span> | FREQ: <span id="freqDisplay">---Hz</span>
</div>
</div>
<div class="tape-deck-bottom">
<div class="deck-buttons">
<span class="deck-label">TAPE DECK:</span>
<button class="mini-btn deck-btn" id="deckRewBtn">⏪ REW</button>
<button class="mini-btn deck-btn" id="deckPlayBtn">▶︎ PLAY</button>
<button class="mini-btn deck-btn" id="deckStopBtn">■ STOP</button>
</div>
<div style="font-size:0.6rem;color:rgba(0,0,0,0.7);">
Static · Scanline · Radio Sweep · Sonar FX
</div>
</div>
</div>
<div class="simple-overlay simple-only">
<div class="simple-overlay-title">ONE-BUTTON ENTITY COLLAGE</div>
<div class="simple-overlay-sub">
Press to cut, splice, and manifest a transient composite from live feed + archive ghosts.
</div>
<button class="conjure-btn" id="conjureBtn">CONJURE ENTITY</button>
</div>
</section>
<aside class="panel panel-side">
<div class="panel-header">
<div class="panel-header-title">
<div>INTEL DOCKET · ROSWELL / MISC. DECLASS (SIM)</div>
<div class="badge">OSS-CIA STYLE INDEX</div>
</div>
</div>
<div class="body">
<div class="field-row">
<div class="field-tag">EVP: <span id="fieldEVP">IDLE</span></div>
<div class="field-tag">AURA: <span id="fieldAura">00.00</span></div>
<div class="field-tag">ORIENT-β: <span id="fieldBeta">0°</span></div>
<div class="field-tag">ORIENT-γ: <span id="fieldGamma">0°</span></div>
</div>
<div class="field-row">
<div class="field-tag">ACCEL-X: <span id="fieldAccelX">0.00</span></div>
<div class="field-tag">ACCEL-Y: <span id="fieldAccelY">0.00</span></div>
<div class="field-tag">ACCEL-Z: <span id="fieldAccelZ">0.00</span></div>
</div>
<div class="field-row">
<div class="field-tag">100Hz: <span id="freq100">0</span>dB</div>
<div class="field-tag">300Hz: <span id="freq300">0</span>dB</div>
<div class="field-tag">1kHz: <span id="freq1k">0</span>dB</div>
<div class="field-tag">2kHz: <span id="freq2k">0</span>dB</div>
<div class="field-tag">5kHz: <span id="freq5k">0</span>dB</div>
</div>
<div class="field-row advanced-only">
<div class="field-tag">
Mode:
<button class="mini-btn active" data-mode="mono">MONO</button>
<button class="mini-btn" data-mode="infra">INFRA</button>
<button class="mini-btn" data-mode="xray">X-RAY</button>
<button class="mini-btn" data-mode="ecto">ECTO</button>
</div>
<div class="field-tag">
Density:
<button class="mini-btn" data-density="low">LOW</button>
<button class="mini-btn active" data-density="med">MED</button>
<button class="mini-btn" data-density="high">HIGH</button>
</div>
</div>
<div class="field-row advanced-only">
<div class="field-tag">
Case File:
<select id="caseSelect" style="font-family:inherit;font-size:0.6rem;background:transparent;border:1px solid rgba(0,0,0,0.5);">
<option value="RSW-047">RSW-047 · Roswell Spillover</option>
<option value="EVP-131">EVP-131 · Basement Tape</option>
<option value="ATLAS-9">ATLAS-9 · Simulation Bleed</option>
</select>
</div>
</div>
<div class="log-window" id="logWindow"></div>
<div class="field-row">
<div class="field-tag">LAYERS: <span id="layerCount">0</span></div>
<div class="field-tag">PERSIST: <span id="persistCount">0</span></div>
</div>
<div style="font-size:0.55rem;color:rgba(0,0,0,0.7);margin-top:4px;">
NOTE: Geometric entities settle into permanent layers via RNG timers. Chromatic aberration & edge distortion active. Old layers fade to background.
</div>
</div>
</aside>
</div>
<footer>
<div>SIMULATION ONLY · ENHANCED VISUAL PROCESSING WITH PLATONIC GEOMETRIES.</div>
<div>FILE: <code>ORACLE-93_COLLAGE.html</code></div>
</footer>
</div>
<script>
const SPOOKY_VOCAB = [
"SHADOW", "WHISPER", "COLD", "PRESENCE", "WATCHING", "BENEATH", "FORGOTTEN",
"ECHO", "VOID", "STATIC", "BREACH", "SIGNAL", "ENTITY", "LIMINAL", "THRESHOLD",
"DECAY", "ANOMALY", "MANIFEST", "RESIDUE", "FREQUENCY", "INTERFERENCE", "CONTACT",
"UNKNOWN", "FEAR", "DARKNESS", "BEYOND", "GATEWAY", "RITUAL", "HAUNTED", "SPECTER",
"PHANTASM", "CURSED", "DREAD", "PARANORMAL", "OCCULT", "CLASSIFIED", "REDACTED",
"CONSPIRACY", "COVER-UP", "INCIDENT", "PHENOMENON", "UNEXPLAINED", "ROSWELL",
"BASEMENT", "TAPE", "RECORDING", "TRANSMISSION", "BROADCAST", "NUMBERS", "STATION",
"THEY", "WATCH", "FOLLOW", "KNOWS", "SEES", "HEARS", "WAITING", "COMING", "HERE",
"HELP", "RUN", "HIDE", "DONT", "STOP", "CANT", "WONT", "NEVER", "ALWAYS", "FOREVER",
"BLOOD", "BONE", "FLESH", "DEATH", "GRAVE", "TOMB", "CRYPT", "CEMETERY", "MORGUE"
];
// Expanded brighter spectrum colors for better visibility
const DARK_COLORS = [
[50, 100, 200], // bright blue
[50, 180, 100], // bright green
[100, 50, 150], // purple
[255, 255, 255], // white
[220, 50, 80], // bright red
[180, 80, 200], // bright purple
[80, 200, 220], // cyan
[255, 100, 200], // hot pink
[50, 150, 200], // sky blue
[200, 80, 100], // rose
[100, 255, 100], // lime green
[200, 220, 255], // ice blue
[180, 100, 255], // violet
[150, 80, 200], // deep purple
[255, 100, 100], // coral
[150, 150, 200], // lavender
[100, 255, 255], // cyan bright
[255, 150, 100], // orange
[255, 255, 200], // pale yellow
[150, 255, 150] // mint green
];
const canvas = document.getElementById("collageCanvas");
const ctx = canvas.getContext("2d", { willReadFrequently: true });
// Multiple canvases for layering
const persistCanvas = document.createElement("canvas");
const persistCtx = persistCanvas.getContext("2d", { willReadFrequently: true });
const tempCanvas = document.createElement("canvas");
const tempCtx = tempCanvas.getContext("2d", { willReadFrequently: true });
const swirlCanvas = document.createElement("canvas");
const swirlCtx = swirlCanvas.getContext("2d", { willReadFrequently: true });
const lavaCanvas = document.createElement("canvas");
const lavaCtx = lavaCanvas.getContext("2d", { willReadFrequently: true });
const gridCamCanvas = document.createElement("canvas");
const gridCamCtx = gridCamCanvas.getContext("2d", { willReadFrequently: true });
let width = 0, height = 0;
function resizeCanvas() {
const rect = canvas.getBoundingClientRect();
width = rect.width;
height = rect.height;
const ratio = window.devicePixelRatio || 1;
[canvas, persistCanvas, tempCanvas, swirlCanvas, lavaCanvas, gridCamCanvas].forEach(c => {
c.width = width * ratio;
c.height = height * ratio;
const context = c.getContext("2d");
context.setTransform(ratio, 0, 0, ratio, 0, 0);
});
// Initialize backgrounds
ctx.fillStyle = "#1b1410";
ctx.fillRect(0, 0, width, height);
persistCtx.fillStyle = "#1b1410";
persistCtx.fillRect(0, 0, width, height);
tempCtx.fillStyle = "#000";
tempCtx.fillRect(0, 0, width, height);
swirlCtx.fillStyle = "#000";
swirlCtx.fillRect(0, 0, width, height);
lavaCtx.fillStyle = "#000";
lavaCtx.fillRect(0, 0, width, height);
gridCamCtx.fillStyle = "#000";
gridCamCtx.fillRect(0, 0, width, height);
}
window.addEventListener("resize", resizeCanvas);
resizeCanvas();
// Controls
const startBtn = document.getElementById("startBtn");
const flashBtn = document.getElementById("flashBtn");
const stopBtn = document.getElementById("stopBtn");
const snapshotBtn = document.getElementById("snapshotBtn");
const toggleCamBtn = document.getElementById("toggleCamBtn");
const toggleMicBtn = document.getElementById("toggleMicBtn");
const toggleWarpBtn = document.getElementById("toggleWarpBtn");
const toggleSoundBtn = document.getElementById("toggleSoundBtn");
const filter1 = document.getElementById("filter1");
const filter2 = document.getElementById("filter2");
const filter3 = document.getElementById("filter3");
const randomFiltersBtn = document.getElementById("randomFiltersBtn");
const caseSelect = document.getElementById("caseSelect");
const caseLabel = document.getElementById("caseLabel");
const viewSimpleBtn = document.getElementById("viewSimpleBtn");
const viewAdvBtn = document.getElementById("viewAdvBtn");
const conjureBtn = document.getElementById("conjureBtn");
const reelLeft = document.getElementById("reelLeft");
const reelRight = document.getElementById("reelRight");
const tapeProgressFill = document.getElementById("tapeProgressFill");
const tapeTimeEl = document.getElementById("tapeTime");
const freqDisplay = document.getElementById("freqDisplay");
const deckRewBtn = document.getElementById("deckRewBtn");
const deckPlayBtn = document.getElementById("deckPlayBtn");
const deckStopBtn = document.getElementById("deckStopBtn");
const evpMeter = document.getElementById("evpMeter");
const evpLabel = document.getElementById("evpLabel");
const fieldEVP = document.getElementById("fieldEVP");
const fieldAura = document.getElementById("fieldAura");
const fieldBeta = document.getElementById("fieldBeta");
const fieldGamma = document.getElementById("fieldGamma");
const fieldAccelX = document.getElementById("fieldAccelX");
const fieldAccelY = document.getElementById("fieldAccelY");
const fieldAccelZ = document.getElementById("fieldAccelZ");
const freq100 = document.getElementById("freq100");
const freq300 = document.getElementById("freq300");
const freq1k = document.getElementById("freq1k");
const freq2k = document.getElementById("freq2k");
const freq5k = document.getElementById("freq5k");
const logWindow = document.getElementById("logWindow");
const tickerContent = document.getElementById("tickerContent");
const vocabStatus = document.getElementById("vocabStatus");
const layerCount = document.getElementById("layerCount");
const persistCount = document.getElementById("persistCount");
let running = false;
let useCam = true;
let useMic = true;
let useWarp = true;
let useSoundFX = true;
let mode = "mono";
let density = "med";
let viewMode = "advanced";
let flashActive = false;
let flashTimer = 0;
let flashFrozenImage = null;
let invertActive = false;
let mirrorActive = false;
let mirrorMode = "horizontal";
let kaleidoActive = false;
let kaleidoSegments = 6;
let downsampleActive = false;
let warpPhase = { x: 0, y: 0, intensity: 0 };
let activeFilters = ["", "", ""];
let blendMode = "normal";
let effectParams = {
blur: 0,
turbulence1: 0,
turbulence2: 0,
turbulence3: 0,
chromatic: 0,
zoom: 1,
orbitalAngle: 0,
brightness: 0,
contrast: 1,
saturation: 1,
gelColor: [255, 255, 255]
};
// NEW: Image transition state
let imageTransitionState = {
mode: 'overlay', // 'overlay', 'screen', 'darken', 'fade', 'displacement', 'melt'
alpha: 0,
targetAlpha: 0,
transitionSpeed: 0.02,
displacementMap: null,
meltPhase: 0
};
// Lava lamp state
let lavaBlobs = [];
let lavaDirection = { x: Math.random() - 0.5, y: Math.random() - 0.5 };
// Grid camera state
let gridSize = 24;
let gridScatter = { x: 0, y: 0 };
let videoStream = null;
let videoEl = null;
let audioStream = null;
let audioCtx = null;
let analyser = null;
let audioData = null;
let staticNode = null;
let scanlineOsc = null;
let radioSweepOsc = null;
let sonarOsc = null;
let soundFXGain = null;
let lastTime = performance.now();
let motion = { alpha:0, beta:0, gamma:0 };
let accel = { x:0, y:0, z:0 };
let tapePos = 0;
const TAPE_LENGTH_SEC = 360;
let conjureActive = false;
let conjureTimer = 0;
let nextVocabTrigger = 10 + Math.random() * 29;
let vocabTimer = 0;
let freqBands = { 100: 0, 300: 0, 1000: 0, 2000: 0, 5000: 0 };
// Persistent entities system
let entities = [];
let persistentEntities = [];
let swirls = [];
let noiseMasks = [];
// Perlin noise implementation
class PerlinNoise {
constructor() {
this.perm = [];
for (let i = 0; i < 256; i++) this.perm[i] = i;
for (let i = 255; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[this.perm[i], this.perm[j]] = [this.perm[j], this.perm[i]];
}
this.perm = [...this.perm, ...this.perm];
}
fade(t) { return t * t * t * (t * (t * 6 - 15) + 10); }
lerp(t, a, b) { return a + t * (b - a); }
grad(hash, x, y) {
const h = hash & 15;
const u = h < 8 ? x : y;
const v = h < 4 ? y : h === 12 || h === 14 ? x : 0;
return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v);
}
noise(x, y) {
const X = Math.floor(x) & 255;
const Y = Math.floor(y) & 255;
x -= Math.floor(x);
y -= Math.floor(y);
const u = this.fade(x);
const v = this.fade(y);
const a = this.perm[X] + Y;
const b = this.perm[X + 1] + Y;
return this.lerp(v,
this.lerp(u, this.grad(this.perm[a], x, y), this.grad(this.perm[b], x - 1, y)),
this.lerp(u, this.grad(this.perm[a + 1], x, y - 1), this.grad(this.perm[b + 1], x - 1, y - 1))
);
}
}
const perlin = new PerlinNoise();
const perlin2 = new PerlinNoise();
const perlin3 = new PerlinNoise();
// Available filters
const FILTERS = [
"infrared", "heat", "nightvision", "invert", "pixelated", "oversaturated",
"chromatic", "rgbsplit", "findedges", "contrast", "brightness", "colorgel",
"orbitalwarp", "zoom", "blur", "turbulence", "kaleidoscope", "triplemirror",
"warppinch", "fluorescent"
];
const imageUrls = [
"mnt/86AC971E-A1E8-484E-A03F-C67F067FE81C.jpg",
"mnt/7183C01A-2EDD-4DCA-96A3-D1B77E004019.jpg",
"mnt/A77755A4-9C75-4A26-B662-9B9F990B72E0.jpg",
"mnt/40C7B800-DFA6-40F6-960E-19E0029AEA69.jpg",
"mnt/A5D87E85-ABFC-4474-A8F9-2F40E34F2F2C.jpg",
"mnt/0365C615-D7DD-49CE-B69F-3EFCF8EC7E28.jpg",
"mnt/24704E24-370D-4B84-B5B3-E59CF5FCF2AC.jpg",
"mnt/797BEC40-B436-40F9-9265-FC93FB70A5FC.jpg",
"mnt/CC072660-BFD2-4448-B039-A3DBCA3D2071.jpg",
"mnt/A482182D-089F-4131-8B70-8C090FFD65F8.jpg"
];
const imagePool = [];
let imagesLoaded = false;
function loadImages() {
const promises = imageUrls.map(url => {
return new Promise(resolve => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => resolve(null);
img.src = url;
});
});
Promise.all(promises).then(list => {
list.forEach(img => { if (img) imagePool.push(img); });
imagesLoaded = true;
log("IMG_POOL", `Loaded ${imagePool.length} archival images from local cache.`);
});
}
loadImages();
function initAudioContext() {
if (audioCtx) return;
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
log("AUDIO_CTX", "Audio context initialized for sound effects.");
}
function initSoundFX() {
if (!audioCtx || !useSoundFX || soundFXGain) return;
soundFXGain = audioCtx.createGain();
soundFXGain.gain.value = 0.15;
soundFXGain.connect(audioCtx.destination);
const bufferSize = 2 * audioCtx.sampleRate;
const staticBuffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const staticData = staticBuffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
staticData[i] = (Math.random() * 2 - 1) * 0.3;
}
staticNode = audioCtx.createBufferSource();
staticNode.buffer = staticBuffer;
staticNode.loop = true;
const staticGain = audioCtx.createGain();
staticGain.gain.value = 0.08;
staticNode.connect(staticGain);
staticGain.connect(soundFXGain);
staticNode.start();
scanlineOsc = audioCtx.createOscillator();
scanlineOsc.type = "sine";
scanlineOsc.frequency.value = 60;
const scanlineGain = audioCtx.createGain();
scanlineGain.gain.value = 0.04;
scanlineOsc.connect(scanlineGain);
scanlineGain.connect(soundFXGain);
scanlineOsc.start();
radioSweepOsc = audioCtx.createOscillator();
radioSweepOsc.type = "triangle";
radioSweepOsc.frequency.value = 800;
const radioGain = audioCtx.createGain();
radioGain.gain.value = 0.06;
radioSweepOsc.connect(radioGain);
radioGain.connect(soundFXGain);
radioSweepOsc.start();
sonarOsc = audioCtx.createOscillator();
sonarOsc.type = "sine";
sonarOsc.frequency.value = 1200;
const sonarGain = audioCtx.createGain();
sonarGain.gain.value = 0;
sonarOsc.connect(sonarGain);
sonarGain.connect(soundFXGain);
sonarOsc.start();
log("SOUND_FX", "Atmospheric sound effects engaged.");
}
function updateSoundFX(evpLevel, time) {
if (!audioCtx || !useSoundFX || !radioSweepOsc) return;
const sweepFreq = 400 + Math.sin(time * 0.001) * 600 + evpLevel * 300;
radioSweepOsc.frequency.setValueAtTime(sweepFreq, audioCtx.currentTime);
if (Math.sin(time * 0.003) > 0.98) {
const sonarGain = sonarOsc.context.createGain();
sonarGain.gain.setValueAtTime(0.1, audioCtx.currentTime);
sonarGain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.3);
sonarOsc.disconnect();
sonarOsc.connect(sonarGain);
sonarGain.connect(soundFXGain);
}
}
function stopSoundFX() {
if (staticNode) { staticNode.stop(); staticNode = null; }
if (scanlineOsc) { scanlineOsc.stop(); scanlineOsc = null; }
if (radioSweepOsc) { radioSweepOsc.stop(); radioSweepOsc = null; }
if (sonarOsc) { sonarOsc.stop(); sonarOsc = null; }
soundFXGain = null;
}
// FIXED: Enhanced camera initialization
async function initCamera() {
if (videoStream || !useCam) return;
try {
log("CAMERA_REQ", "Requesting camera access...");
// Try different camera constraints for better compatibility
const constraints = {
video: {
facingMode: { ideal: "environment" },
width: { ideal: 1280, min: 640 },
height: { ideal: 720, min: 480 },
frameRate: { ideal: 30 }
},
audio: false
};
videoStream = await navigator.mediaDevices.getUserMedia(constraints);
videoEl = document.createElement("video");
videoEl.srcObject = videoStream;
videoEl.playsInline = true;
videoEl.muted = true;
videoEl.autoplay = true;
// Wait for video to be ready
await new Promise((resolve) => {
videoEl.onloadedmetadata = () => {
videoEl.play().then(resolve);
};
});
log("CAMERA_OK", "Camera feed acquired and active.");
return true;
} catch (e) {
log("CAMERA_DENIED", `Camera access denied: ${e.message}`);
useCam = false;
toggleCamBtn.classList.remove("active");
updateButtonStates();
// Create a fallback test pattern
videoEl = document.createElement("canvas");
videoEl.width = 640;
videoEl.height = 480;
const vctx = videoEl.getContext('2d');
// Draw test pattern
setInterval(() => {
vctx.fillStyle = '#1a1a2e';
vctx.fillRect(0, 0, 640, 480);
vctx.fillStyle = '#16213e';
for (let i = 0; i < 640; i += 40) {
vctx.fillRect(i, 0, 20, 480);
}
for (let j = 0; j < 480; j += 40) {
vctx.fillRect(0, j, 640, 20);
}
vctx.fillStyle = '#0f3460';
vctx.font = 'bold 48px monospace';
vctx.fillText('CAMERA SIM', 160, 240);
}, 100);
return false;
}
}
async function initAudio() {
if (audioStream || !useMic) return;
try {
initAudioContext();
log("AUDIO_REQ", "Requesting microphone access...");
audioStream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: false,
noiseSuppression: false,
autoGainControl: false
},
video: false
});
analyser = audioCtx.createAnalyser();
analyser.fftSize = 2048;
analyser.smoothingTimeConstant = 0.3;
const micSource = audioCtx.createMediaStreamSource(audioStream);
const highGain = audioCtx.createGain();
highGain.gain.value = 8.0;
micSource.connect(highGain);
highGain.connect(analyser);
audioData = new Uint8Array(analyser.frequencyBinCount);
log("AUDIO_OK", "EVP microphone channel armed and monitoring.");
} catch (e) {
log("AUDIO_DENIED", `Microphone access denied: ${e.message}`);
useMic = false;
toggleMicBtn.classList.remove("active");
updateButtonStates();
}
}
function stopMedia() {
if (videoStream) {
videoStream.getTracks().forEach(t => t.stop());
videoStream = null;
videoEl = null;
}
if (audioStream) {
audioStream.getTracks().forEach(t => t.stop());
audioStream = null;
}
if (analyser) {
analyser = null;
audioData = null;
}
stopSoundFX();
if (audioCtx && audioCtx.state !== 'closed') {
audioCtx.close();
audioCtx = null;
}
}
if (window.DeviceOrientationEvent) {
window.addEventListener("deviceorientation", (e) => {
motion.alpha = e.alpha || 0;
motion.beta = e.beta || 0;
motion.gamma = e.gamma || 0;
});
}
if (window.DeviceMotionEvent) {
window.addEventListener("devicemotion", (e) => {
if (e.accelerationIncludingGravity) {
accel.x = e.accelerationIncludingGravity.x || 0;
accel.y = e.accelerationIncludingGravity.y || 0;
accel.z = e.accelerationIncludingGravity.z || 0;
}
});
}
function pad(num) {
return num.toString().padStart(2,"0");
}
function timestamp() {
const d = new Date();
return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}
const phrasesBase = [
"Spectral blotch emerges near upper-right quadrant.",
"Platonic solid manifests in persistent layer.",
"Morphogenetic noise exceeds archival baseline.",
"Geometric entity settles into background matrix.",
"Chromatic aberration correlates with EVP spike.",
"Swirl rotation inverts at threshold boundary.",
"Polygon cluster entrains to audio frequency.",
"Edge distortion suggests dimensional bleed.",
"Trigon formation matches prior incident logs.",
"Noise mask reveals underlying pattern structure."
];
const phrasesEVP = [
"Tape hiss forms near-human cadence in mid-range band.",
"Loop artifacts imply erased-over confession.",
"Basement acoustics resonate with unknown subharmonic.",
"Detected phrase approximates 'DON'T STOP LISTENING'.",
"Transient peaks align with subject breathing rhythm.",
"Compression bloom hides low-volume muttering.",
"Analog dropout suggests tampered reel.",
"Rewind noise spikes when name is spoken.",
"Tape warble syncs with geomagnetic drift.",
"Noise floor refuses to stay still."
];
const phrasesATLAS = [
"Pixel bloom mirrors neural-spike pattern from Atlas logs.",
"Simulation bleed-over suspected in upper left quadrant.",
"Ghost frame appears between live feed and cached still.",
"Render aliasing mimics lattice patterns of Atlas tier-3.",
"UI jitter suggests non-local interference.",
"Data ghosting resembles corrupted avatar trace.",
"Crossfade event matches Atlas shutdown signature.",
"Motion parallax refuses to obey camera transform.",
"Grid warping correlated with prior migration tests.",
"Observer effect logged: viewer alters the collage."
];
let phrases = phrasesBase;
function updatePhrasesForCase(caseId) {
if (caseId === "RSW-047") phrases = phrasesBase;
else if (caseId === "EVP-131") phrases = phrasesEVP;
else if (caseId === "ATLAS-9") phrases = phrasesATLAS;
else phrases = phrasesBase;
}
function log(code, text) {
const line = document.createElement("div");
line.className = "log-line";
const tSpan = document.createElement("span");
tSpan.className = "log-time";
tSpan.textContent = `[${timestamp()}] ${code}: `;
const xSpan = document.createElement("span");
xSpan.className = "log-text";
xSpan.textContent = text;
line.appendChild(tSpan);
line.appendChild(xSpan);
logWindow.appendChild(line);
logWindow.scrollTop = logWindow.scrollHeight;
}
function randomLog() {
const idx = Math.floor(Math.random() * phrases.length);
log("FIELD_NOTE", phrases[idx]);
}
function addToTicker(word, isAlert = false) {
const span = document.createElement("span");
span.className = isAlert ? "ticker-word alert" : "ticker-word";
span.textContent = word;
tickerContent.appendChild(span);
tickerContent.scrollTop = tickerContent.scrollHeight;
while (tickerContent.children.length > 50) {
tickerContent.removeChild(tickerContent.firstChild);
}
}
setInterval(() => {
if (running) randomLog();
}, 8000 + Math.random()*4000);
function updateButtonStates() {
if (toggleCamBtn) toggleCamBtn.classList.toggle("active", useCam);
if (toggleMicBtn) toggleMicBtn.classList.toggle("active", useMic);
if (toggleWarpBtn) toggleWarpBtn.classList.toggle("active", useWarp);
if (toggleSoundBtn) toggleSoundBtn.classList.toggle("active", useSoundFX);
const spinning = running;
[reelLeft, reelRight].forEach(r => {
if (!r) return;
r.classList.toggle("spinning", spinning);
});
}
// FIXED: Enhanced session start with auto camera init
async function startSession() {
running = true;
if (startBtn) startBtn.classList.add("active");
if (stopBtn) stopBtn.classList.remove("active");
tapePos = 0;
initAudioContext();
await initCamera();
await initAudio();
if (useSoundFX) initSoundFX();
log("SESSION_START", "Collage engine engaged. Geometric synthesis active.");
}
function stopSession() {
running = false;
if (startBtn) startBtn.classList.remove("active");
if (stopBtn) stopBtn.classList.add("active");
stopMedia();
log("SESSION_STOP", "Collage halted. Persistent layers preserved.");
}
// Auto-initialize camera on page load
document.addEventListener('DOMContentLoaded', async () => {
// Initialize camera immediately
await initCamera();
// Start with camera active but not necessarily recording
log("SYSTEM_INIT", "PROJECT ORACLE-93 initialized. Camera feed pre-warmed.");
});
if (startBtn) startBtn.addEventListener("click", startSession);
if (stopBtn) stopBtn.addEventListener("click", stopSession);
if (flashBtn) {
flashBtn.addEventListener("click", () => {
if (!flashActive) {
// Freeze current frame
flashFrozenImage = document.createElement('canvas');
flashFrozenImage.width = canvas.width;
flashFrozenImage.height = canvas.height;
const frozenCtx = flashFrozenImage.getContext('2d');
frozenCtx.drawImage(canvas, 0, 0);
flashActive = true;
flashTimer = 4; // Keep flash for a bit
flashBtn.classList.add("active");
log("FLASH_ON", "Frame frozen - imagery intensified.");
} else {
flashActive = false;
flashTimer = 0;
flashFrozenImage = null;
flashBtn.classList.remove("active");
log("FLASH_OFF", "Flash released - back to subtle mode.");
}
});
}
// Initialize filter dropdowns
function initFilterDropdowns() {
[filter1, filter2, filter3].forEach(select => {
// Clear existing options
select.innerHTML = '<option value="">NONE</option>';
FILTERS.forEach(f => {
const option = document.createElement('option');
option.value = f;
option.textContent = f.toUpperCase();
select.appendChild(option);
});
});
}
initFilterDropdowns();
// Filter change listeners
if (filter1) filter1.addEventListener('change', () => {
activeFilters[0] = filter1.value;
log("FILTER_1", `Filter slot 1: ${filter1.value || 'NONE'}`);
});
if (filter2) filter2.addEventListener('change', () => {
activeFilters[1] = filter2.value;
log("FILTER_2", `Filter slot 2: ${filter2.value || 'NONE'}`);
});
if (filter3) filter3.addEventListener('change', () => {
activeFilters[2] = filter3.value;
log("FILTER_3", `Filter slot 3: ${filter3.value || 'NONE'}`);
});
// Random filters button
if (randomFiltersBtn) {
randomFiltersBtn.addEventListener('click', () => {
const numFilters = 1 + Math.floor(Math.random() * 3); // 1-3 filters
activeFilters = ["", "", ""];
for (let i = 0; i < numFilters; i++) {
const randomFilter = FILTERS[Math.floor(Math.random() * FILTERS.length)];
activeFilters[i] = randomFilter;
}
filter1.value = activeFilters[0];
filter2.value = activeFilters[1];
filter3.value = activeFilters[2];
log("FILTER_RND", `Random filters: ${activeFilters.filter(f => f).join(', ') || 'NONE'}`);
});
}
if (toggleCamBtn) {
toggleCamBtn.addEventListener("click", () => {
useCam = !useCam;
updateButtonStates();
if (useCam && running) initCamera();
});
}
if (toggleMicBtn) {
toggleMicBtn.addEventListener("click", () => {
useMic = !useMic;
updateButtonStates();
if (useMic && running) initAudio();
});
}
if (toggleWarpBtn) {
toggleWarpBtn.addEventListener("click", () => {
useWarp = !useWarp;
updateButtonStates();
});
}
if (toggleSoundBtn) {
toggleSoundBtn.addEventListener("click", () => {
useSoundFX = !useSoundFX;
updateButtonStates();
if (useSoundFX && running && audioCtx) {
initSoundFX();
} else if (!useSoundFX) {
stopSoundFX();
}
});
}
if (snapshotBtn) {
snapshotBtn.addEventListener("click", () => {
try {
const dataUrl = canvas.toDataURL("image/png");
const a = document.createElement("a");
a.href = dataUrl;
a.download = `oracle93_${Date.now()}.png`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
log("SNAPSHOT", "Still frame exported as PNG.");
} catch (e) {
log("SNAPSHOT_FAIL", "Export blocked by browser.");
}
});
}
if (caseSelect) {
caseSelect.addEventListener("change", () => {
const val = caseSelect.value;
caseLabel.textContent = val;
updatePhrasesForCase(val);
log("CASE_SWITCH", `Operator selected case file ${val}.`);
});
updatePhrasesForCase(caseSelect.value);
}
const modeButtons = document.querySelectorAll("[data-mode]");
modeButtons.forEach(btn => {
btn.addEventListener("click", () => {
const m = btn.getAttribute("data-mode");
mode = m;
modeButtons.forEach(b => b.classList.toggle("active", b === btn));
log("MODE_SET", `Visual mode set to ${m.toUpperCase()}.`);
});
});
modeButtons.forEach(b => {
if (b.getAttribute("data-mode") === mode) b.classList.add("active");
});
const densityButtons = document.querySelectorAll("[data-density]");
densityButtons.forEach(btn => {
btn.addEventListener("click", () => {
const d = btn.getAttribute("data-density");
density = d;
densityButtons.forEach(b => b.classList.toggle("active", b === btn));
log("DENSITY_SET", `Collage density set to ${d.toUpperCase()}.`);
});
});
densityButtons.forEach(b => {
if (b.getAttribute("data-density") === density) b.classList.add("active");
});
function setViewMode(modeView) {
viewMode = modeView;
document.body.classList.toggle("simple-view", viewMode === "simple");
if (viewSimpleBtn) viewSimpleBtn.classList.toggle("active", viewMode === "simple");
if (viewAdvBtn) viewAdvBtn.classList.toggle("active", viewMode === "advanced");
log("VIEW_MODE", `View switched to ${viewMode.toUpperCase()}.`);
}
if (viewSimpleBtn) viewSimpleBtn.addEventListener("click", () => setViewMode("simple"));
if (viewAdvBtn) viewAdvBtn.addEventListener("click", () => setViewMode("advanced"));
if (conjureBtn) {
conjureBtn.addEventListener("click", async () => {
if (viewMode === "simple") {
if (!running) {
await startSession();
conjureActive = true;
conjureTimer = 3.5;
conjureBtn.classList.add("active");
log("CONJURE_LATCH_ON", "Simple-mode entity collage running.");
} else {
stopSession();
conjureActive = false;
conjureTimer = 0;
conjureBtn.classList.remove("active");
log("CONJURE_LATCH_OFF", "Simple-mode entity collage halted.");
}
} else {
conjureActive = true;
conjureTimer = 3.5;
conjureBtn.classList.add("active");
log("CONJURE", "Entity collage pulse initiated.");
}
});
}
updateButtonStates();
function formatTapeTime(pos) {
const totalSec = TAPE_LENGTH_SEC * pos;
const m = Math.floor(totalSec / 60);
const s = Math.floor(totalSec % 60);
return `${pad(m)}:${pad(s)}`;
}
if (deckRewBtn) {
deckRewBtn.addEventListener("click", () => {
tapePos = 0;
if (tapeProgressFill) tapeProgressFill.style.width = "0%";
if (tapeTimeEl) tapeTimeEl.textContent = formatTapeTime(tapePos);
log("TAPE_REWIND", "Tape position reset to zero.");
});
}
if (deckPlayBtn) deckPlayBtn.addEventListener("click", () => { if (!running) startSession(); });
if (deckStopBtn) deckStopBtn.addEventListener("click", () => { if (running) stopSession(); });
function bumpHaptics(intensity) {
if (!("vibrate" in navigator)) return;
const amt = Math.min(200, 50 + intensity * 150);
navigator.vibrate([amt, 50, amt/2]);
}
function analyzeFrequencies() {
if (!analyser || !audioData) return;
analyser.getByteFrequencyData(audioData);
const sampleRate = audioCtx.sampleRate;
const binWidth = sampleRate / analyser.fftSize;
function getBinForFreq(freq) {
return Math.floor(freq / binWidth);
}
function getAvgAmplitude(freq, range = 20) {
const bin = getBinForFreq(freq);
const binRange = Math.floor(range / binWidth);
let sum = 0, count = 0;
for (let i = Math.max(0, bin - binRange); i <= Math.min(audioData.length - 1, bin + binRange); i++) {
sum += audioData[i];
count++;
}
return count > 0 ? sum / count : 0;
}
freqBands[100] = getAvgAmplitude(100);
freqBands[300] = getAvgAmplitude(300);
freqBands[1000] = getAvgAmplitude(1000);
freqBands[2000] = getAvgAmplitude(2000);
freqBands[5000] = getAvgAmplitude(5000);
if (freq100) freq100.textContent = Math.round(freqBands[100]);
if (freq300) freq300.textContent = Math.round(freqBands[300]);
if (freq1k) freq1k.textContent = Math.round(freqBands[1000]);
if (freq2k) freq2k.textContent = Math.round(freqBands[2000]);
if (freq5k) freq5k.textContent = Math.round(freqBands[5000]);
}
// Geometric shape generators
function drawPolygon(context, x, y, sides, radius, rotation, color, alpha) {
context.save();
context.translate(x, y);
context.rotate(rotation);
context.beginPath();
for (let i = 0; i <= sides; i++) {
const angle = (i / sides) * Math.PI * 2;
const px = Math.cos(angle) * radius;
const py = Math.sin(angle) * radius;
if (i === 0) context.moveTo(px, py);
else context.lineTo(px, py);
}
context.closePath();
context.strokeStyle = `rgba(${color[0]},${color[1]},${color[2]},${alpha})`;
context.fillStyle = `rgba(${color[0]},${color[1]},${color[2]},${alpha * 0.3})`;
context.lineWidth = 1 + alpha * 2;
context.fill();
context.stroke();
context.restore();
}
function drawPlatonicSolid(context, x, y, type, size, rotation, color, alpha) {
// 2D projections of platonic solids
context.save();
context.translate(x, y);
context.rotate(rotation);
if (type === "tetrahedron") {
// Triangle
drawPolygon(context, 0, 0, 3, size, 0, color, alpha);
} else if (type === "cube") {
// Isometric cube
const s = size;
context.beginPath();
context.moveTo(0, -s);
context.lineTo(s * 0.866, -s * 0.5);
context.lineTo(s * 0.866, s * 0.5);
context.lineTo(0, s);
context.lineTo(-s * 0.866, s * 0.5);
context.lineTo(-s * 0.866, -s * 0.5);
context.closePath();
context.strokeStyle = `rgba(${color[0]},${color[1]},${color[2]},${alpha})`;
context.fillStyle = `rgba(${color[0]},${color[1]},${color[2]},${alpha * 0.3})`;
context.lineWidth = 1 + alpha * 2;
context.fill();
context.stroke();
} else if (type === "octahedron") {
// Diamond shape
context.beginPath();
context.moveTo(0, -size);
context.lineTo(size * 0.7, 0);
context.lineTo(0, size);
context.lineTo(-size * 0.7, 0);
context.closePath();
context.strokeStyle = `rgba(${color[0]},${color[1]},${color[2]},${alpha})`;
context.fillStyle = `rgba(${color[0]},${color[1]},${color[2]},${alpha * 0.3})`;
context.lineWidth = 1 + alpha * 2;
context.fill();
context.stroke();
} else if (type === "icosahedron") {
// Pentagon
drawPolygon(context, 0, 0, 5, size, 0, color, alpha);
} else if (type === "dodecahedron") {
// Complex pentagon pattern
drawPolygon(context, 0, 0, 5, size, 0, color, alpha);
drawPolygon(context, 0, 0, 5, size * 0.6, Math.PI / 5, color, alpha * 0.7);
}
context.restore();
}
function createSwirl(time) {
return {
x: Math.random() * width,
y: Math.random() * height,
radius: 50 + Math.random() * 150,
rotation: Math.random() * Math.PI * 2,
rotSpeed: (Math.random() - 0.5) * 0.02,
curve: Math.random() * 2,
color: DARK_COLORS[Math.floor(Math.random() * DARK_COLORS.length)],
alpha: 0.2 + Math.random() * 0.4,
lifetime: 3 + Math.random() * 7,
age: 0
};
}
function createNoiseMask() {
return {
x: Math.random() * width,
y: Math.random() * height,
w: 20 + Math.random() * 100,
h: 20 + Math.random() * 100,
lifetime: 0.5 + Math.random() * 3,
permanent: Math.random() > 0.7,
age: 0,
alpha: 0.2 + Math.random() * 0.5
};
}
function createLavaBlob() {
const angle = Math.atan2(lavaDirection.y, lavaDirection.x);
const startX = lavaDirection.x > 0 ? -50 : width + 50;
const startY = lavaDirection.y > 0 ? -50 : height + 50;
return {
x: startX + (Math.random() - 0.5) * 100,
y: startY + (Math.random() - 0.5) * 100,
vx: lavaDirection.x * (0.5 + Math.random() * 1.5),
vy: lavaDirection.y * (0.5 + Math.random() * 1.5),
radius: 30 + Math.random() * 80,
color: {
r: Math.floor(Math.random() * 255),
g: Math.floor(Math.random() * 255),
b: Math.floor(Math.random() * 255)
},
colorChangeRate: 0.5 + Math.random() * 2,
alpha: 0.3 + Math.random() * 0.5
};
}
function updateLavaBlobs(dt) {
// Update existing blobs
for (let i = lavaBlobs.length - 1; i >= 0; i--) {
const blob = lavaBlobs[i];
blob.x += blob.vx;
blob.y += blob.vy;
// Change color randomly
blob.color.r += (Math.random() - 0.5) * blob.colorChangeRate * 10;
blob.color.g += (Math.random() - 0.5) * blob.colorChangeRate * 10;
blob.color.b += (Math.random() - 0.5) * blob.colorChangeRate * 10;
blob.color.r = Math.max(0, Math.min(255, blob.color.r));
blob.color.g = Math.max(0, Math.min(255, blob.color.g));
blob.color.b = Math.max(0, Math.min(255, blob.color.b));
// Remove if off screen
if (blob.x < -200 || blob.x > width + 200 || blob.y < -200 || blob.y > height + 200) {
lavaBlobs.splice(i, 1);
}
}
// Spawn new blobs
if (Math.random() < 0.1) {
lavaBlobs.push(createLavaBlob());
}
// Randomly change direction
if (Math.random() < 0.02) {
lavaDirection.x = (Math.random() - 0.5) * 2;
lavaDirection.y = (Math.random() - 0.5) * 2;
const mag = Math.sqrt(lavaDirection.x * lavaDirection.x + lavaDirection.y * lavaDirection.y);
lavaDirection.x /= mag;
lavaDirection.y /= mag;
}
}
function drawLavaBlobs() {
lavaCtx.fillStyle = "#000";
lavaCtx.fillRect(0, 0, width, height);
lavaBlobs.forEach(blob => {
const gradient = lavaCtx.createRadialGradient(blob.x, blob.y, 0, blob.x, blob.y, blob.radius);
gradient.addColorStop(0, `rgba(${Math.floor(blob.color.r)},${Math.floor(blob.color.g)},${Math.floor(blob.color.b)},${blob.alpha})`);
gradient.addColorStop(0.7, `rgba(${Math.floor(blob.color.r * 0.7)},${Math.floor(blob.color.g * 0.7)},${Math.floor(blob.color.b * 0.7)},${blob.alpha * 0.5})`);
gradient.addColorStop(1, `rgba(${Math.floor(blob.color.r * 0.3)},${Math.floor(blob.color.g * 0.3)},${Math.floor(blob.color.b * 0.3)},0)`);
lavaCtx.fillStyle = gradient;
lavaCtx.beginPath();
lavaCtx.arc(blob.x, blob.y, blob.radius, 0, Math.PI * 2);
lavaCtx.fill();
});
}
function createEntity(evpPercent) {
const types = ["polygon", "platonic", "pixel", "circle"];
const type = types[Math.floor(Math.random() * types.length)];
const entity = {
type: type,
x: Math.random() * width,
y: Math.random() * height,
vx: (Math.random() - 0.5) * 4 * (1 + evpPercent * 3),
vy: (Math.random() - 0.5) * 4 * (1 + evpPercent * 3),
rotation: Math.random() * Math.PI * 2,
rotSpeed: (Math.random() - 0.5) * 0.1,
color: DARK_COLORS[Math.floor(Math.random() * DARK_COLORS.length)],
alpha: 0.5 + Math.random() * 0.5,
settleTime: 1 + Math.random() * 10,
age: 0,
settled: false
};
if (type === "polygon") {
entity.sides = 3 + Math.floor(Math.random() * 10); // 3-12 sides
entity.size = 5 + Math.random() * 60; // More varied sizes
} else if (type === "platonic") {
const shapes = ["tetrahedron", "cube", "octahedron", "icosahedron", "dodecahedron"];
entity.shape = shapes[Math.floor(Math.random() * shapes.length)];
entity.size = 10 + Math.random() * 70;
} else if (type === "pixel") {
entity.size = 1 + Math.random() * 12;
} else if (type === "circle") {
entity.size = 5 + Math.random() * 50;
entity.filled = Math.random() > 0.5;
}
return entity;
}
function updateEntities(dt, evpPercent) {
// Update regular entities
for (let i = entities.length - 1; i >= 0; i--) {
const e = entities[i];
e.age += dt;
e.x += e.vx;
e.y += e.vy;
e.rotation += e.rotSpeed;
// Damping
e.vx *= 0.98;
e.vy *= 0.98;
// Check if settled
if (e.age >= e.settleTime && !e.settled) {
e.settled = true;
persistentEntities.push(e);
entities.splice(i, 1);
log("ENTITY_SETTLE", `${e.type} entity settled into persistent layer.`);
}
}
// Fade old persistent entities
for (let i = persistentEntities.length - 1; i >= 0; i--) {
const e = persistentEntities[i];
e.age += dt;
e.alpha *= 0.995; // Slow fade
if (e.alpha < 0.01) {
persistentEntities.splice(i, 1);
}
}
// Update swirls
for (let i = swirls.length - 1; i >= 0; i--) {
const s = swirls[i];
s.age += dt;
s.rotation += s.rotSpeed;
s.x += Math.sin(s.age * 0.5 + s.curve) * 0.5;
s.y += Math.cos(s.age * 0.5 + s.curve) * 0.5;
if (s.age >= s.lifetime) {
swirls.splice(i, 1);
}
}
// Update noise masks
for (let i = noiseMasks.length - 1; i >= 0; i--) {
const n = noiseMasks[i];
n.age += dt;
if (!n.permanent && n.age >= n.lifetime) {
noiseMasks.splice(i, 1);
}
}
// Spawn new entities based on audio/camera activity (spawn even when not running for visual feedback)
if (Math.random() < 0.15 * (1 + evpPercent)) {
entities.push(createEntity(evpPercent));
}
// Spawn swirls
if (Math.random() < 0.08) {
swirls.push(createSwirl(performance.now()));
}
// Spawn noise masks
if (Math.random() < 0.12) {
noiseMasks.push(createNoiseMask());
}
}
function drawEntity(context, e) {
if (e.type === "polygon") {
drawPolygon(context, e.x, e.y, e.sides, e.size, e.rotation, e.color, e.alpha);
} else if (e.type === "platonic") {
drawPlatonicSolid(context, e.x, e.y, e.shape, e.size, e.rotation, e.color, e.alpha);
} else if (e.type === "pixel") {
context.fillStyle = `rgba(${e.color[0]},${e.color[1]},${e.color[2]},${e.alpha})`;
context.fillRect(e.x - e.size/2, e.y - e.size/2, e.size, e.size);
} else if (e.type === "circle") {
context.save();
context.beginPath();
context.arc(e.x, e.y, e.size, 0, Math.PI * 2);
context.strokeStyle = `rgba(${e.color[0]},${e.color[1]},${e.color[2]},${e.alpha})`;
context.lineWidth = 1 + e.alpha * 2;
context.stroke();
if (e.filled) {
context.fillStyle = `rgba(${e.color[0]},${e.color[1]},${e.color[2]},${e.alpha * 0.3})`;
context.fill();
}
context.restore();
}
}
function drawSwirl(context, s) {
context.save();
context.translate(s.x, s.y);
context.rotate(s.rotation);
for (let i = 0; i < 3; i++) {
context.beginPath();
const r = s.radius * (0.3 + i * 0.35);
const points = 20;
for (let j = 0; j <= points; j++) {
const angle = (j / points) * Math.PI * 2;
const dist = r + Math.sin(angle * s.curve + s.age) * 10;
const px = Math.cos(angle) * dist;
const py = Math.sin(angle) * dist;
if (j === 0) context.moveTo(px, py);
else context.lineTo(px, py);
}
context.strokeStyle = `rgba(${s.color[0]},${s.color[1]},${s.color[2]},${s.alpha * 0.5})`;
context.lineWidth = 1;
context.stroke();
}
context.restore();
}
function drawNoiseMask(context, n) {
context.save();
context.globalAlpha = n.alpha;
for (let i = 0; i < 100; i++) {
const px = n.x + Math.random() * n.w;
const py = n.y + Math.random() * n.h;
const col = DARK_COLORS[Math.floor(Math.random() * DARK_COLORS.length)];
context.fillStyle = `rgb(${col[0]},${col[1]},${col[2]})`;
context.fillRect(px, py, 1, 1);
}
context.restore();
}
// NEW: Enhanced fluorescent filter
function applyFluorescent(context) {
const imageData = context.getImageData(0, 0, width, height);
const pixels = imageData.data;
for (let i = 0; i < pixels.length; i += 4) {
// Get original RGB values
const r = pixels[i];
const g = pixels[i + 1];
const b = pixels[i + 2];
// Calculate luminance
const luminance = 0.299 * r + 0.587 * g + 0.114 * b;
// Map to vibrant fluorescent colors
let fr, fg, fb;
if (luminance < 64) {
// Deep blues and purples
fr = Math.floor(luminance * 0.3);
fg = Math.floor(luminance * 0.1);
fb = Math.floor(luminance * 2.5);
} else if (luminance < 128) {
// Electric greens and cyans
fr = Math.floor((luminance - 64) * 0.5);
fg = Math.floor(64 + (luminance - 64) * 2);
fb = Math.floor(128 + (luminance - 64));
} else if (luminance < 192) {
// Hot pinks and magentas
fr = Math.floor(128 + (luminance - 128) * 1.5);
fg = Math.floor((luminance - 128) * 0.8);
fb = Math.floor(192 + (luminance - 128) * 0.5);
} else {
// Neon yellows and oranges
fr = 255;
fg = Math.floor(192 + (luminance - 192) * 0.8);
fb = Math.floor((luminance - 192) * 0.6);
}
// Apply with intensity boost
const boost = 1.5;
pixels[i] = Math.min(255, fr * boost);
pixels[i + 1] = Math.min(255, fg * boost);
pixels[i + 2] = Math.min(255, fb * boost);
// Add subtle glow effect by bleeding into adjacent pixels
if (Math.random() < 0.3 && i > width * 4) {
const bleed = 0.3;
pixels[i - 4] = Math.min(255, pixels[i - 4] + fr * bleed);
pixels[i - 3] = Math.min(255, pixels[i - 3] + fg * bleed);
pixels[i - 2] = Math.min(255, pixels[i - 2] + fb * bleed);
}
}
context.putImageData(imageData, 0, 0);
}
function applyChromaticAberration(sourceCanvas, destContext, offset) {
const tempCanvas = document.createElement('canvas');
tempCanvas.width = width;
tempCanvas.height = height;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.globalCompositeOperation = "lighter";
// Red channel
tempCtx.globalAlpha = 1;
tempCtx.drawImage(sourceCanvas, -offset, 0);
// Green channel
tempCtx.globalAlpha = 1;
tempCtx.globalCompositeOperation = "lighter";
tempCtx.drawImage(sourceCanvas, 0, 0);
// Blue channel
tempCtx.drawImage(sourceCanvas, offset, 0);
destContext.drawImage(tempCanvas, 0, 0);
}
function applyEdgeDistortion(context, time) {
const imageData = context.getImageData(0, 0, width, height);
const pixels = imageData.data;
const edgeWidth = 50;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const distFromEdge = Math.min(x, width - x, y, height - y);
if (distFromEdge < edgeWidth) {
const idx = (y * width + x) * 4;
const distort = (edgeWidth - distFromEdge) / edgeWidth;
const offset = Math.sin(time * 0.001 + y * 0.1) * distort * 5;
const sourceX = Math.min(width - 1, Math.max(0, x + offset));
const sourceIdx = (y * width + Math.floor(sourceX)) * 4;
pixels[idx] = pixels[sourceIdx];
pixels[idx + 1] = pixels[sourceIdx + 1];
pixels[idx + 2] = pixels[sourceIdx + 2];
}
}
}
context.putImageData(imageData, 0, 0);
}
function applyTurbulentDisplacement(context, time, intensity) {
const imageData = context.getImageData(0, 0, width, height);
const newImageData = context.createImageData(width, height);
const scale = 0.01;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const noiseX = perlin.noise(x * scale, y * scale + time * 0.001) * intensity;
const noiseY = perlin.noise(x * scale + 100, y * scale + time * 0.001 + 100) * intensity;
const sourceX = Math.min(width - 1, Math.max(0, Math.floor(x + noiseX)));
const sourceY = Math.min(height - 1, Math.max(0, Math.floor(y + noiseY)));
const destIdx = (y * width + x) * 4;
const sourceIdx = (sourceY * width + sourceX) * 4;
newImageData.data[destIdx] = imageData.data[sourceIdx];
newImageData.data[destIdx + 1] = imageData.data[sourceIdx + 1];
newImageData.data[destIdx + 2] = imageData.data[sourceIdx + 2];
newImageData.data[destIdx + 3] = imageData.data[sourceIdx + 3];
}
}
context.putImageData(newImageData, 0, 0);
}
function applyXYWarp(context, warpX, warpY) {
const imageData = context.getImageData(0, 0, width, height);
const newImageData = context.createImageData(width, height);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const offsetX = Math.sin(y * 0.02 + warpX) * 15;
const offsetY = Math.cos(x * 0.02 + warpY) * 15;
const sourceX = Math.min(width - 1, Math.max(0, Math.floor(x + offsetX)));
const sourceY = Math.min(height - 1, Math.max(0, Math.floor(y + offsetY)));
const destIdx = (y * width + x) * 4;
const sourceIdx = (sourceY * width + sourceX) * 4;
newImageData.data[destIdx] = imageData.data[sourceIdx];
newImageData.data[destIdx + 1] = imageData.data[sourceIdx + 1];
newImageData.data[destIdx + 2] = imageData.data[sourceIdx + 2];
newImageData.data[destIdx + 3] = imageData.data[sourceIdx + 3];
}
}
context.putImageData(newImageData, 0, 0);
}
function applyColorInvert(context) {
const imageData = context.getImageData(0, 0, width, height);
const pixels = imageData.data;
for (let i = 0; i < pixels.length; i += 4) {
pixels[i] = 255 - pixels[i];
pixels[i + 1] = 255 - pixels[i + 1];
pixels[i + 2] = 255 - pixels[i + 2];
}
context.putImageData(imageData, 0, 0);
}
function applyMirrorEffect(context, mode) {
const imageData = context.getImageData(0, 0, width, height);
const newImageData = context.createImageData(width, height);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let sourceX = x;
let sourceY = y;
if (mode === "horizontal") {
sourceX = x < width / 2 ? x : width - x - 1;
} else if (mode === "vertical") {
sourceY = y < height / 2 ? y : height - y - 1;
}
const destIdx = (y * width + x) * 4;
const sourceIdx = (sourceY * width + sourceX) * 4;
newImageData.data[destIdx] = imageData.data[sourceIdx];
newImageData.data[destIdx + 1] = imageData.data[sourceIdx + 1];
newImageData.data[destIdx + 2] = imageData.data[sourceIdx + 2];
newImageData.data[destIdx + 3] = imageData.data[sourceIdx + 3];
}
}
context.putImageData(newImageData, 0, 0);
}
function applyKaleidoscope(context, segments) {
const centerX = width / 2;
const centerY = height / 2;
const imageData = context.getImageData(0, 0, width, height);
const newImageData = context.createImageData(width, height);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const dx = x - centerX;
const dy = y - centerY;
const angle = Math.atan2(dy, dx);
const dist = Math.sqrt(dx * dx + dy * dy);
const segmentAngle = (Math.PI * 2) / segments;
const normalizedAngle = ((angle % segmentAngle) + segmentAngle) % segmentAngle;
const sourceX = Math.floor(centerX + Math.cos(normalizedAngle) * dist);
const sourceY = Math.floor(centerY + Math.sin(normalizedAngle) * dist);
if (sourceX >= 0 && sourceX < width && sourceY >= 0 && sourceY < height) {
const destIdx = (y * width + x) * 4;
const sourceIdx = (sourceY * width + sourceX) * 4;
newImageData.data[destIdx] = imageData.data[sourceIdx];
newImageData.data[destIdx + 1] = imageData.data[sourceIdx + 1];
newImageData.data[destIdx + 2] = imageData.data[sourceIdx + 2];
newImageData.data[destIdx + 3] = imageData.data[sourceIdx + 3];
}
}
}
context.putImageData(newImageData, 0, 0);
}
function applyDownsample(context, factor) {
const lowWidth = Math.floor(width / factor);
const lowHeight = Math.floor(height / factor);
const tempCanvas = document.createElement('canvas');
tempCanvas.width = lowWidth;
tempCanvas.height = lowHeight;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.drawImage(context.canvas, 0, 0, width, height, 0, 0, lowWidth, lowHeight);
context.clearRect(0, 0, width, height);
context.imageSmoothingEnabled = false;
context.drawImage(tempCanvas, 0, 0, lowWidth, lowHeight, 0, 0, width, height);
context.imageSmoothingEnabled = true;
}
// NEW: Enhanced blur function for image layers
function applyLayerBlur(context, intensity) {
if (intensity <= 0) return;
// Apply subtle Gaussian blur to all image layers
const imageData = context.getImageData(0, 0, width, height);
const pixels = imageData.data;
const tempData = new Uint8ClampedArray(pixels);
const radius = Math.ceil(intensity);
const sigma = radius / 3;
const kernelSize = Math.ceil(radius * 2);
const kernel = [];
let kernelSum = 0;
for (let i = -kernelSize; i <= kernelSize; i++) {
const val = Math.exp(-(i * i) / (2 * sigma * sigma));
kernel.push(val);
kernelSum += val;
}
for (let i = 0; i < kernel.length; i++) kernel[i] /= kernelSum;
// Horizontal pass
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let r = 0, g = 0, b = 0;
for (let k = 0; k < kernel.length; k++) {
const sx = Math.min(width - 1, Math.max(0, x + k - kernelSize));
const idx = (y * width + sx) * 4;
r += tempData[idx] * kernel[k];
g += tempData[idx + 1] * kernel[k];
b += tempData[idx + 2] * kernel[k];
}
const idx = (y * width + x) * 4;
pixels[idx] = r;
pixels[idx + 1] = g;
pixels[idx + 2] = b;
}
}
tempData.set(pixels);
// Vertical pass
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let r = 0, g = 0, b = 0;
for (let k = 0; k < kernel.length; k++) {
const sy = Math.min(height - 1, Math.max(0, y + k - kernelSize));
const idx = (sy * width + x) * 4;
r += tempData[idx] * kernel[k];
g += tempData[idx + 1] * kernel[k];
b += tempData[idx + 2] * kernel[k];
}
const idx = (y * width + x) * 4;
pixels[idx] = r;
pixels[idx + 1] = g;
pixels[idx + 2] = b;
}
}
context.putImageData(imageData, 0, 0);
}
function applyGaussianBlur(context, radius) {
if (radius <= 0) return;
const imageData = context.getImageData(0, 0, width, height);
const pixels = imageData.data;
const tempData = new Uint8ClampedArray(pixels);
const sigma = radius / 3;
const kernelSize = Math.ceil(radius * 2);
const kernel = [];
let kernelSum = 0;
for (let i = -kernelSize; i <= kernelSize; i++) {
const val = Math.exp(-(i * i) / (2 * sigma * sigma));
kernel.push(val);
kernelSum += val;
}
for (let i = 0; i < kernel.length; i++) kernel[i] /= kernelSum;
// Horizontal pass
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let r = 0, g = 0, b = 0;
for (let k = 0; k < kernel.length; k++) {
const sx = Math.min(width - 1, Math.max(0, x + k - kernelSize));
const idx = (y * width + sx) * 4;
r += tempData[idx] * kernel[k];
g += tempData[idx + 1] * kernel[k];
b += tempData[idx + 2] * kernel[k];
}
const idx = (y * width + x) * 4;
pixels[idx] = r;
pixels[idx + 1] = g;
pixels[idx + 2] = b;
}
}
tempData.set(pixels);
// Vertical pass
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let r = 0, g = 0, b = 0;
for (let k = 0; k < kernel.length; k++) {
const sy = Math.min(height - 1, Math.max(0, y + k - kernelSize));
const idx = (sy * width + x) * 4;
r += tempData[idx] * kernel[k];
g += tempData[idx + 1] * kernel[k];
b += tempData[idx + 2] * kernel[k];
}
const idx = (y * width + x) * 4;
pixels[idx] = r;
pixels[idx + 1] = g;
pixels[idx + 2] = b;
}
}
context.putImageData(imageData, 0, 0);
}
function applyInfraredFilter(context) {
const imageData = context.getImageData(0, 0, width, height);
const pixels = imageData.data;
for (let i = 0; i < pixels.length; i += 4) {
const brightness = (pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3;
pixels[i] = Math.min(255, brightness * 1.5 + 50);
pixels[i + 1] = Math.min(255, brightness * 0.3);
pixels[i + 2] = Math.min(255, brightness * 0.5);
}
context.putImageData(imageData, 0, 0);
}
function applyHeatSignature(context) {
const imageData = context.getImageData(0, 0, width, height);
const pixels = imageData.data;
for (let i = 0; i < pixels.length; i += 4) {
const brightness = (pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3;
if (brightness < 64) {
pixels[i] = 0;
pixels[i + 1] = 0;
pixels[i + 2] = brightness * 2;
} else if (brightness < 128) {
pixels[i] = (brightness - 64) * 2;
pixels[i + 1] = 0;
pixels[i + 2] = 128 - (brightness - 64);
} else if (brightness < 192) {
pixels[i] = 128 + (brightness - 128);
pixels[i + 1] = (brightness - 128) * 2;
pixels[i + 2] = 0;
} else {
pixels[i] = 255;
pixels[i + 1] = 128 + (brightness - 192);
pixels[i + 2] = (brightness - 192) * 2;
}
}
context.putImageData(imageData, 0, 0);
}
function applyNightVision(context) {
const imageData = context.getImageData(0, 0, width, height);
const pixels = imageData.data;
for (let i = 0; i < pixels.length; i += 4) {
const brightness = (pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3;
const enhanced = Math.min(255, brightness * 1.8 + 40);
pixels[i] = Math.min(255, enhanced * 0.2);
pixels[i + 1] = enhanced;
pixels[i + 2] = Math.min(255, enhanced * 0.4);
}
context.putImageData(imageData, 0, 0);
}
function applyRGBSplit(context, offset) {
const imageData = context.getImageData(0, 0, width, height);
const newImageData = context.createImageData(width, height);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const idx = (y * width + x) * 4;
const rIdx = (y * width + Math.min(width - 1, Math.max(0, x + offset))) * 4;
const gIdx = idx;
const bIdx = (y * width + Math.min(width - 1, Math.max(0, x - offset))) * 4;
newImageData.data[idx] = imageData.data[rIdx];
newImageData.data[idx + 1] = imageData.data[gIdx + 1];
newImageData.data[idx + 2] = imageData.data[bIdx + 2];
newImageData.data[idx + 3] = imageData.data[idx + 3];
}
}
context.putImageData(newImageData, 0, 0);
}
function applyFindEdges(context) {
const imageData = context.getImageData(0, 0, width, height);
const pixels = imageData.data;
const newImageData = context.createImageData(width, height);
const newPixels = newImageData.data;
const sobelX = [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]];
const sobelY = [[-1, -2, -1], [0, 0, 0], [1, 2, 1]];
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
let gx = 0, gy = 0;
for (let ky = -1; ky <= 1; ky++) {
for (let kx = -1; kx <= 1; kx++) {
const idx = ((y + ky) * width + (x + kx)) * 4;
const brightness = (pixels[idx] + pixels[idx + 1] + pixels[idx + 2]) / 3;
gx += brightness * sobelX[ky + 1][kx + 1];
gy += brightness * sobelY[ky + 1][kx + 1];
}
}
const magnitude = Math.min(255, Math.sqrt(gx * gx + gy * gy));
const idx = (y * width + x) * 4;
newPixels[idx] = magnitude;
newPixels[idx + 1] = magnitude;
newPixels[idx + 2] = magnitude;
newPixels[idx + 3] = 255;
}
}
context.putImageData(newImageData, 0, 0);
}
function applyContrast(context, amount) {
const imageData = context.getImageData(0, 0, width, height);
const pixels = imageData.data;
const factor = (259 * (amount + 255)) / (255 * (259 - amount));
for (let i = 0; i < pixels.length; i += 4) {
pixels[i] = Math.min(255, Math.max(0, factor * (pixels[i] - 128) + 128));
pixels[i + 1] = Math.min(255, Math.max(0, factor * (pixels[i + 1] - 128) + 128));
pixels[i + 2] = Math.min(255, Math.max(0, factor * (pixels[i + 2] - 128) + 128));
}
context.putImageData(imageData, 0, 0);
}
function applyBrightness(context, amount) {
const imageData = context.getImageData(0, 0, width, height);
const pixels = imageData.data;
for (let i = 0; i < pixels.length; i += 4) {
pixels[i] = Math.min(255, Math.max(0, pixels[i] + amount));
pixels[i + 1] = Math.min(255, Math.max(0, pixels[i + 1] + amount));
pixels[i + 2] = Math.min(255, Math.max(0, pixels[i + 2] + amount));
}
context.putImageData(imageData, 0, 0);
}
function applyColorGel(context, r, g, b, intensity) {
const imageData = context.getImageData(0, 0, width, height);
const pixels = imageData.data;
for (let i = 0; i < pixels.length; i += 4) {
pixels[i] = Math.min(255, pixels[i] + r * intensity);
pixels[i + 1] = Math.min(255, pixels[i + 1] + g * intensity);
pixels[i + 2] = Math.min(255, pixels[i + 2] + b * intensity);
}
context.putImageData(imageData, 0, 0);
}
function applyOversaturation(context, amount) {
const imageData = context.getImageData(0, 0, width, height);
const pixels = imageData.data;
for (let i = 0; i < pixels.length; i += 4) {
const r = pixels[i];
const g = pixels[i + 1];
const b = pixels[i + 2];
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
pixels[i] = Math.min(255, Math.max(0, gray + (r - gray) * amount));
pixels[i + 1] = Math.min(255, Math.max(0, gray + (g - gray) * amount));
pixels[i + 2] = Math.min(255, Math.max(0, gray + (b - gray) * amount));
}
context.putImageData(imageData, 0, 0);
}
function applyOrbitalWarp(context, angle, radius) {
const imageData = context.getImageData(0, 0, width, height);
const newImageData = context.createImageData(width, height);
const centerX = width / 2;
const centerY = height / 2;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const dx = x - centerX;
const dy = y - centerY;
const dist = Math.sqrt(dx * dx + dy * dy);
const originalAngle = Math.atan2(dy, dx);
const warpAmount = (1 - Math.min(1, dist / radius)) * angle;
const newAngle = originalAngle + warpAmount;
const sourceX = Math.floor(centerX + Math.cos(newAngle) * dist);
const sourceY = Math.floor(centerY + Math.sin(newAngle) * dist);
if (sourceX >= 0 && sourceX < width && sourceY >= 0 && sourceY < height) {
const destIdx = (y * width + x) * 4;
const sourceIdx = (sourceY * width + sourceX) * 4;
newImageData.data[destIdx] = imageData.data[sourceIdx];
newImageData.data[destIdx + 1] = imageData.data[sourceIdx + 1];
newImageData.data[destIdx + 2] = imageData.data[sourceIdx + 2];
newImageData.data[destIdx + 3] = imageData.data[sourceIdx + 3];
}
}
}
context.putImageData(newImageData, 0, 0);
}
function applyZoom(context, zoomLevel) {
const tempCanvas = document.createElement('canvas');
tempCanvas.width = width;
tempCanvas.height = height;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.drawImage(context.canvas, 0, 0);
context.clearRect(0, 0, width, height);
const scaledWidth = width * zoomLevel;
const scaledHeight = height * zoomLevel;
const offsetX = (width - scaledWidth) / 2;
const offsetY = (height - scaledHeight) / 2;
context.drawImage(tempCanvas, offsetX, offsetY, scaledWidth, scaledHeight);
}
function applyTripleMirror(context) {
const imageData = context.getImageData(0, 0, width, height);
const newImageData = context.createImageData(width, height);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let sourceX = x;
let sourceY = y;
// Divide into 3 vertical sections
const section = Math.floor((x / width) * 3);
if (section === 1) {
// Middle section - mirror horizontally
sourceX = width - x - 1;
} else if (section === 2) {
// Right section - mirror vertically
sourceY = height - y - 1;
}
const destIdx = (y * width + x) * 4;
const sourceIdx = (Math.min(height - 1, Math.max(0, sourceY)) * width + Math.min(width - 1, Math.max(0, sourceX))) * 4;
newImageData.data[destIdx] = imageData.data[sourceIdx];
newImageData.data[destIdx + 1] = imageData.data[sourceIdx + 1];
newImageData.data[destIdx + 2] = imageData.data[sourceIdx + 2];
newImageData.data[destIdx + 3] = imageData.data[sourceIdx + 3];
}
}
context.putImageData(newImageData, 0, 0);
}
function applyWarpPinch(context, centerX, centerY, radius, strength) {
const imageData = context.getImageData(0, 0, width, height);
const newImageData = context.createImageData(width, height);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const dx = x - centerX;
const dy = y - centerY;
const dist = Math.sqrt(dx * dx + dy * dy);
let sourceX = x;
let sourceY = y;
if (dist < radius) {
const amount = (radius - dist) / radius;
const pinch = Math.pow(amount, strength);
sourceX = centerX + dx * (1 - pinch);
sourceY = centerY + dy * (1 - pinch);
}
sourceX = Math.min(width - 1, Math.max(0, Math.floor(sourceX)));
sourceY = Math.min(height - 1, Math.max(0, Math.floor(sourceY)));
const destIdx = (y * width + x) * 4;
const sourceIdx = (sourceY * width + sourceX) * 4;
newImageData.data[destIdx] = imageData.data[sourceIdx];
newImageData.data[destIdx + 1] = imageData.data[sourceIdx + 1];
newImageData.data[destIdx + 2] = imageData.data[sourceIdx + 2];
newImageData.data[destIdx + 3] = imageData.data[sourceIdx + 3];
}
}
context.putImageData(newImageData, 0, 0);
}
// NEW: Enhanced image transition system
function applyImageTransition(context, time) {
if (!imagesLoaded || imagePool.length === 0) return;
const img = imagePool[Math.floor(time * 0.0001) % imagePool.length];
// Randomly change transition mode
if (Math.random() < 0.005) {
const modes = ['overlay', 'screen', 'darken', 'fade', 'displacement', 'melt'];
imageTransitionState.mode = modes[Math.floor(Math.random() * modes.length)];
log("TRANSITION", `Image blend mode changed to: ${imageTransitionState.mode}`);
}
// Update transition alpha
if (Math.random() < 0.01) {
imageTransitionState.targetAlpha = 0.1 + Math.random() * 0.4;
}
imageTransitionState.alpha += (imageTransitionState.targetAlpha - imageTransitionState.alpha) *
imageTransitionState.transitionSpeed;
const drawW = width * (0.3 + 0.2 * Math.sin(time * 0.0003));
const drawH = height * (0.3 + 0.2 * Math.cos(time * 0.0004));
const dx = width / 2 - drawW / 2 + Math.sin(time * 0.0005) * 100;
const dy = height / 2 - drawH / 2 + Math.cos(time * 0.0006) * 100;
// Save current canvas state
const tempCanvas = document.createElement('canvas');
tempCanvas.width = width;
tempCanvas.height = height;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.drawImage(context.canvas, 0, 0);
// Apply different blend modes
context.save();
switch(imageTransitionState.mode) {
case 'overlay':
context.globalCompositeOperation = 'overlay';
context.globalAlpha = imageTransitionState.alpha * 0.7;
break;
case 'screen':
context.globalCompositeOperation = 'screen';
context.globalAlpha = imageTransitionState.alpha * 0.5;
break;
case 'darken':
context.globalCompositeOperation = 'darken';
context.globalAlpha = imageTransitionState.alpha * 0.6;
break;
case 'fade':
// Fade in/out effect
const fade = 0.5 + 0.5 * Math.sin(time * 0.001);
context.globalCompositeOperation = 'source-over';
context.globalAlpha = imageTransitionState.alpha * fade;
break;
case 'displacement':
// Displacement based on lightness/saturation
context.globalCompositeOperation = 'source-over';
context.globalAlpha = imageTransitionState.alpha;
// Create displacement effect
const displacement = tempCtx.createImageData(width, height);
const dispData = displacement.data;
const srcData = tempCtx.getImageData(0, 0, width, height).data;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const idx = (y * width + x) * 4;
const lightness = (srcData[idx] + srcData[idx + 1] + srcData[idx + 2]) / (3 * 255);
const saturation = Math.sqrt(
Math.pow(srcData[idx] - srcData[idx + 1], 2) +
Math.pow(srcData[idx + 1] - srcData[idx + 2], 2) +
Math.pow(srcData[idx + 2] - srcData[idx], 2)
) / 255;
const dispX = Math.sin(lightness * Math.PI * 2 + time * 0.001) * 20 * saturation;
const dispY = Math.cos(saturation * Math.PI * 2 + time * 0.001) * 20 * lightness;
const sx = Math.min(width - 1, Math.max(0, x + dispX));
const sy = Math.min(height - 1, Math.max(0, y + dispY));
const sidx = (sy * width + sx) * 4;
dispData[idx] = srcData[sidx];
dispData[idx + 1] = srcData[sidx + 1];
dispData[idx + 2] = srcData[sidx + 2];
dispData[idx + 3] = srcData[sidx + 3];
}
}
tempCtx.putImageData(displacement, 0, 0);
break;
case 'melt':
// Melt effect
context.globalCompositeOperation = 'source-over';
imageTransitionState.meltPhase += 0.01;
const meltCanvas = document.createElement('canvas');
meltCanvas.width = width;
meltCanvas.height = height;
const meltCtx = meltCanvas.getContext('2d');
// Draw image with melt distortion
for (let y = 0; y < drawH; y++) {
const meltAmount = Math.sin(y / drawH * Math.PI + imageTransitionState.meltPhase) * 30;
meltCtx.drawImage(
img,
0, y, img.width, 1,
dx + meltAmount, dy + y, drawW, 1
);
}
context.globalAlpha = imageTransitionState.alpha;
context.drawImage(meltCanvas, 0, 0);
context.restore();
return;
}
context.drawImage(img, dx, dy, drawW, drawH);
context.restore();
}
function drawGridCamera() {
if (!videoEl || !useCam) return;
const vW = videoEl.videoWidth;
const vH = videoEl.videoHeight;
if (!vW || !vH) return;
gridCamCtx.fillStyle = "#000";
gridCamCtx.fillRect(0, 0, width, height);
// Randomly change grid size
if (Math.random() < 0.05) {
gridSize = 24 + Math.floor(Math.random() * 9); // 24-32
}
// Randomly change scatter
if (Math.random() < 0.08) {
gridScatter.x = (Math.random() - 0.5) * 40;
gridScatter.y = (Math.random() - 0.5) * 40;
}
const cellWidth = width / gridSize;
const cellHeight = height / gridSize;
const srcCellWidth = vW / gridSize;
const srcCellHeight = vH / gridSize;
// Create array of cell positions and shuffle
const cells = [];
for (let gy = 0; gy < gridSize; gy++) {
for (let gx = 0; gx < gridSize; gx++) {
cells.push({ gx, gy });
}
}
// Draw cells with scatter
cells.forEach(cell => {
const srcX = cell.gx * srcCellWidth;
const srcY = cell.gy * srcCellHeight;
const destX = cell.gx * cellWidth + (Math.random() - 0.5) * gridScatter.x;
const destY = cell.gy * cellHeight + (Math.random() - 0.5) * gridScatter.y;
gridCamCtx.drawImage(
videoEl,
srcX, srcY, srcCellWidth, srcCellHeight,
destX, destY, cellWidth, cellHeight
);
});
// Apply brightness and saturation
const imageData = gridCamCtx.getImageData(0, 0, width, height);
const pixels = imageData.data;
const brightnessBoost = 20 + Math.random() * 40;
const saturationBoost = 1.3 + Math.random() * 0.7;
for (let i = 0; i < pixels.length; i += 4) {
const r = pixels[i];
const g = pixels[i + 1];
const b = pixels[i + 2];
// Brightness
let nr = r + brightnessBoost;
let ng = g + brightnessBoost;
let nb = b + brightnessBoost;
// Saturation
const gray = 0.299 * nr + 0.587 * ng + 0.114 * nb;
nr = Math.min(255, Math.max(0, gray + (nr - gray) * saturationBoost));
ng = Math.min(255, Math.max(0, gray + (ng - gray) * saturationBoost));
nb = Math.min(255, Math.max(0, gray + (nb - gray) * saturationBoost));
pixels[i] = nr;
pixels[i + 1] = ng;
pixels[i + 2] = nb;
}
gridCamCtx.putImageData(imageData, 0, 0);
}
let lastConjureEndLogged = false;
let scanlineY = 0;
function loop(time) {
const dt = (time - lastTime) / 1000;
lastTime = time;
if (running) {
tapePos += dt / TAPE_LENGTH_SEC;
if (tapePos > 1) tapePos = 0;
vocabTimer += dt;
if (vocabTimer >= nextVocabTrigger) {
const word = SPOOKY_VOCAB[Math.floor(Math.random() * SPOOKY_VOCAB.length)];
const isAlert = Math.random() > 0.7;
addToTicker(word, isAlert);
log("EVP_DETECT", `Spectral vocabulary trigger: "${word}"`);
vocabTimer = 0;
nextVocabTrigger = 10 + Math.random() * 29;
if (isAlert) bumpHaptics(0.8);
}
}
// Flash timer (just for intensity calculation, doesn't auto-off)
if (flashActive && flashTimer > 0) {
flashTimer -= dt * 0.2; // Slow decay for smooth transition
if (flashTimer < 1) flashTimer = 1; // Keep minimum intensity
}
if (tapeProgressFill) tapeProgressFill.style.width = (tapePos*100).toFixed(1) + "%";
if (tapeTimeEl) tapeTimeEl.textContent = formatTapeTime(tapePos);
let evpLevel = 0;
if (analyser && audioData && useMic) {
analyser.getByteTimeDomainData(audioData);
let sum = 0;
for (let i=0;i<audioData.length;i++) {
const v = (audioData[i] - 128) / 128;
sum += v*v;
}
const rms = Math.sqrt(sum / audioData.length);
evpLevel = rms;
analyzeFrequencies();
} else {
evpLevel = 0.05 + 0.03 * Math.sin(time * 0.0007);
}
const evpPercentBase = Math.min(1, evpLevel * 8);
let evpPercent = evpPercentBase;
if (conjureActive && conjureTimer > 0) {
const phase = 1 - (conjureTimer / 3.5);
evpPercent = Math.min(1, evpPercentBase + 0.4 + 0.3*Math.sin(phase * Math.PI));
}
evpMeter.style.width = (evpPercent * 100).toFixed(1) + "%";
evpLabel.textContent = evpLevel.toFixed(3);
fieldEVP.textContent = evpLevel > 0.09 ? "ACTIVE" : "IDLE";
const auraVal = 0.5 + 0.5 * Math.sin(time*0.0005 + motion.beta*0.01);
fieldAura.textContent = auraVal.toFixed(2);
fieldBeta.textContent = Math.round(motion.beta) + "°";
fieldGamma.textContent = Math.round(motion.gamma) + "°";
fieldAccelX.textContent = accel.x.toFixed(2);
fieldAccelY.textContent = accel.y.toFixed(2);
fieldAccelZ.textContent = accel.z.toFixed(2);
updateSoundFX(evpLevel, time);
if (freqDisplay) {
const displayFreq = 200 + (motion.beta || 0) * 2 + evpLevel * 300;
freqDisplay.textContent = Math.round(displayFreq) + "Hz";
}
if (evpLevel > 0.14 && Math.random()<0.05) {
bumpHaptics(evpLevel);
}
// Update entity system
updateEntities(dt, evpPercent);
// Update lava blobs (always update for visual feedback)
updateLavaBlobs(dt);
drawLavaBlobs();
// Update grid camera (only when camera is active)
if (videoEl && useCam) {
drawGridCamera();
}
// Update counters
if (layerCount) layerCount.textContent = entities.length;
if (persistCount) persistCount.textContent = persistentEntities.length;
// Clear temp canvas
tempCtx.fillStyle = "#000";
tempCtx.fillRect(0, 0, width, height);
// Draw swirls on swirl canvas
swirlCtx.globalAlpha = 0.02;
swirlCtx.fillStyle = "#000";
swirlCtx.fillRect(0, 0, width, height);
swirlCtx.globalAlpha = 1;
swirls.forEach(s => drawSwirl(swirlCtx, s));
// Draw persistent entities to persist canvas (slow fade)
persistCtx.globalAlpha = 0.002;
persistCtx.fillStyle = "#000";
persistCtx.fillRect(0, 0, width, height);
persistCtx.globalAlpha = 1;
persistentEntities.forEach(e => drawEntity(persistCtx, e));
// Draw active entities to temp canvas with blend mode
tempCtx.globalCompositeOperation = "lighter";
entities.forEach(e => drawEntity(tempCtx, e));
tempCtx.globalCompositeOperation = "source-over";
// Draw noise masks
noiseMasks.forEach(n => drawNoiseMask(tempCtx, n));
// Composite to main canvas
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, width, height);
// If flash is active, draw frozen frame first
if (flashActive && flashFrozenImage) {
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
ctx.drawImage(flashFrozenImage, 0, 0);
}
// Layer 1: Swirls (background) - Screen blend
ctx.globalAlpha = 0.5;
ctx.globalCompositeOperation = "screen";
ctx.drawImage(swirlCanvas, 0, 0);
// Layer 2: Lava lamp - Lighten blend
ctx.globalAlpha = 0.6;
ctx.globalCompositeOperation = "lighten";
ctx.drawImage(lavaCanvas, 0, 0);
// Layer 3: Camera feed (always visible, brighter when flashed) - Screen blend
if (videoEl && useCam) {
const vW = videoEl.videoWidth;
const vH = videoEl.videoHeight;
if (vW && vH) {
const scale = Math.max(width / vW, height / vH);
const drawW = vW * scale;
const drawH = vH * scale;
const dx = (width - drawW) / 2;
const dy = (height - drawH) / 2;
ctx.save();
ctx.globalCompositeOperation = "screen";
if (flashActive) {
const flashIntensity = Math.min(1, flashTimer / 2);
ctx.globalAlpha = 0.5 + flashIntensity * 0.4;
} else {
ctx.globalAlpha = 0.3;
}
ctx.drawImage(videoEl, dx, dy, drawW, drawH);
ctx.restore();
}
}
// Layer 4: Grid camera - Multiply blend
ctx.globalAlpha = 0.7;
ctx.globalCompositeOperation = "multiply";
ctx.drawImage(gridCamCanvas, 0, 0);
// Layer 5: Persistent entities (fading background) - Lighten
ctx.globalAlpha = 0.6;
ctx.globalCompositeOperation = "lighten";
ctx.drawImage(persistCanvas, 0, 0);
// Layer 6: Archival images with enhanced transitions - Using new system
if (imagesLoaded && imagePool.length > 0) {
// Apply the enhanced image transition system
applyImageTransition(ctx, time);
// Apply natural blur to all image layers
applyLayerBlur(ctx, 0.8 + Math.sin(time * 0.0005) * 0.4);
}
// Layer 7: Active entities - Lighten
ctx.globalAlpha = 0.9;
ctx.globalCompositeOperation = "lighten";
ctx.drawImage(tempCanvas, 0, 0);
// Reset to normal blending for effects
ctx.globalCompositeOperation = "source-over";
// Random effect triggers based on RNG and sensor data
const accelMag = Math.sqrt(accel.x * accel.x + accel.y * accel.y + accel.z * accel.z);
const effectTrigger = evpLevel + accelMag * 0.01 + Math.abs(motion.beta) * 0.001;
// Randomize effect parameters continuously
if (Math.random() < 0.08) {
effectParams.turbulence1 = 10 + Math.random() * 30;
effectParams.turbulence2 = 5 + Math.random() * 20;
effectParams.turbulence3 = 3 + Math.random() * 12;
effectParams.blur = Math.random() * 4;
effectParams.chromatic = Math.floor(Math.random() * 10);
effectParams.zoom = 0.85 + Math.random() * 0.3;
effectParams.orbitalAngle = Math.random() * Math.PI * 0.6;
effectParams.brightness = -40 + Math.random() * 80;
effectParams.contrast = -40 + Math.random() * 80;
effectParams.saturation = 2 + Math.random() * 2.5;
effectParams.gelColor = [
Math.floor(Math.random() * 120) - 60,
Math.floor(Math.random() * 120) - 60,
Math.floor(Math.random() * 120) - 60
];
}
// Apply 3 layers of turbulent displacement with different Perlin noise
if (Math.random() < 0.5) {
const imageData = ctx.getImageData(0, 0, width, height);
const newImageData = ctx.createImageData(width, height);
const scale = 0.008 + Math.random() * 0.015;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const noise1 = perlin.noise(x * scale, y * scale + time * 0.0008) * effectParams.turbulence1;
const noise2 = perlin2.noise(x * scale * 1.5, y * scale * 1.5 + time * 0.0012) * effectParams.turbulence2;
const noise3 = perlin3.noise(x * scale * 2, y * scale * 2 + time * 0.0015) * effectParams.turbulence3;
const totalNoiseX = noise1 + noise2 + noise3;
const totalNoiseY = perlin.noise(x * scale + 100, y * scale + time * 0.0008 + 100) * effectParams.turbulence1 +
perlin2.noise(x * scale * 1.5 + 150, y * scale * 1.5 + time * 0.0012 + 150) * effectParams.turbulence2 +
perlin3.noise(x * scale * 2 + 200, y * scale * 2 + time * 0.0015 + 200) * effectParams.turbulence3;
const sourceX = Math.min(width - 1, Math.max(0, Math.floor(x + totalNoiseX)));
const sourceY = Math.min(height - 1, Math.max(0, Math.floor(y + totalNoiseY)));
const destIdx = (y * width + x) * 4;
const sourceIdx = (sourceY * width + sourceX) * 4;
newImageData.data[destIdx] = imageData.data[sourceIdx];
newImageData.data[destIdx + 1] = imageData.data[sourceIdx + 1];
newImageData.data[destIdx + 2] = imageData.data[sourceIdx + 2];
newImageData.data[destIdx + 3] = imageData.data[sourceIdx + 3];
}
}
ctx.putImageData(newImageData, 0, 0);
}
// Apply Gaussian blur at random amounts
if (effectParams.blur > 0.5 && Math.random() < 0.4) {
applyGaussianBlur(ctx, effectParams.blur);
}
// X/Y Warping (random, brief)
if (running && Math.random() < 0.02 + effectTrigger * 0.05) {
warpPhase.x = Math.random() * Math.PI * 2;
warpPhase.y = Math.random() * Math.PI * 2;
warpPhase.intensity = 2 + Math.random() * 3;
}
if (warpPhase.intensity > 0) {
applyXYWarp(ctx, warpPhase.x, warpPhase.y);
warpPhase.intensity -= dt;
}
// Downsample/Upsample (occasional, brief)
if (running && Math.random() < 0.01 + effectTrigger * 0.02) {
downsampleActive = true;
}
if (downsampleActive) {
applyDownsample(ctx, 3 + Math.floor(Math.random() * 3));
if (Math.random() < 0.3) downsampleActive = false;
}
// Color inversion (random flashes)
if (running && Math.random() < 0.008 + effectTrigger * 0.02) {
invertActive = !invertActive;
if (invertActive) {
log("INVERT_ON", "Color matrix inverted by field anomaly.");
}
}
if (invertActive) {
applyColorInvert(ctx);
if (Math.random() < 0.05) {
invertActive = false;
log("INVERT_OFF", "Color matrix restored.");
}
}
// Mirror effect (brief, random)
if (running && Math.random() < 0.005 + effectTrigger * 0.01) {
mirrorActive = true;
const modes = ["horizontal", "vertical"];
mirrorMode = modes[Math
mirrorMode = modes[Math.floor(Math.random() * modes.length)];
log("MIRROR_ON", `Mirror effect activated: ${mirrorMode}`);
}
if (mirrorActive) {
applyMirrorEffect(ctx, mirrorMode);
if (Math.random() < 0.08) {
mirrorActive = false;
log("MIRROR_OFF", "Mirror effect dissipated.");
}
}
// Kaleidoscope effect (brief, random)
if (running && Math.random() < 0.003 + effectTrigger * 0.008) {
kaleidoActive = true;
kaleidoSegments = 4 + Math.floor(Math.random() * 8);
log("KALEIDO_ON", `Kaleidoscope engaged: ${kaleidoSegments} segments`);
}
if (kaleidoActive) {
applyKaleidoscope(ctx, kaleidoSegments);
if (Math.random() < 0.06) {
kaleidoActive = false;
log("KALEIDO_OFF", "Kaleidoscope collapsed.");
}
}
// Apply chromatic aberration
if (running && effectParams.chromatic > 0 && Math.random() < 0.15) {
applyChromaticAberration(canvas, ctx, effectParams.chromatic);
}
// Apply edge distortion
if (running && Math.random() < 0.15) {
applyEdgeDistortion(ctx, time);
}
// Apply selected filters
activeFilters.forEach(filter => {
if (!filter) return;
switch(filter) {
case "infrared":
applyInfraredFilter(ctx);
break;
case "heat":
applyHeatSignature(ctx);
break;
case "nightvision":
applyNightVision(ctx);
break;
case "invert":
applyColorInvert(ctx);
break;
case "pixelated":
applyDownsample(ctx, 3 + Math.floor(Math.random() * 3));
break;
case "oversaturated":
applyOversaturation(ctx, effectParams.saturation);
break;
case "chromatic":
applyChromaticAberration(canvas, ctx, 2 + Math.floor(Math.random() * 10));
break;
case "rgbsplit":
applyRGBSplit(ctx, 2 + Math.floor(Math.random() * 12));
break;
case "findedges":
applyFindEdges(ctx);
break;
case "contrast":
applyContrast(ctx, effectParams.contrast);
break;
case "brightness":
applyBrightness(ctx, effectParams.brightness);
break;
case "colorgel":
applyColorGel(ctx, effectParams.gelColor[0], effectParams.gelColor[1], effectParams.gelColor[2], 0.5);
break;
case "orbitalwarp":
applyOrbitalWarp(ctx, effectParams.orbitalAngle, Math.min(width, height) * 0.8);
break;
case "zoom":
applyZoom(ctx, effectParams.zoom);
break;
case "blur":
applyGaussianBlur(ctx, 1 + Math.random() * 4);
break;
case "turbulence":
const turbScale = 0.01 + Math.random() * 0.02;
const turbIntensity = 10 + Math.random() * 30;
const turbImageData = ctx.getImageData(0, 0, width, height);
const turbNewImageData = ctx.createImageData(width, height);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const noiseX = perlin.noise(x * turbScale, y * turbScale + time * 0.001) * turbIntensity;
const noiseY = perlin.noise(x * turbScale + 100, y * turbScale + time * 0.001 + 100) * turbIntensity;
const sourceX = Math.min(width - 1, Math.max(0, Math.floor(x + noiseX)));
const sourceY = Math.min(height - 1, Math.max(0, Math.floor(y + noiseY)));
const destIdx = (y * width + x) * 4;
const sourceIdx = (sourceY * width + sourceX) * 4;
turbNewImageData.data[destIdx] = turbImageData.data[sourceIdx];
turbNewImageData.data[destIdx + 1] = turbImageData.data[sourceIdx + 1];
turbNewImageData.data[destIdx + 2] = turbImageData.data[sourceIdx + 2];
turbNewImageData.data[destIdx + 3] = turbImageData.data[sourceIdx + 3];
}
}
ctx.putImageData(turbNewImageData, 0, 0);
break;
case "kaleidoscope":
const kSegments = 4 + Math.floor(Math.random() * 8);
applyKaleidoscope(ctx, kSegments);
break;
case "triplemirror":
applyTripleMirror(ctx);
break;
case "warppinch":
const warpCenterX = Math.random() * width;
const warpCenterY = Math.random() * height;
const warpRadius = 50 + Math.random() * 200;
const warpStrength = 0.3 + Math.random() * 1.5;
applyWarpPinch(ctx, warpCenterX, warpCenterY, warpRadius, warpStrength);
break;
case "fluorescent":
applyFluorescent(ctx);
break;
}
});
// Scanline sweep
scanlineY = (scanlineY + 3) % height;
ctx.globalAlpha = 0.15;
const grad = ctx.createLinearGradient(0, scanlineY - 20, 0, scanlineY + 20);
grad.addColorStop(0, "rgba(100,150,255,0)");
grad.addColorStop(0.5, "rgba(100,150,255,0.4)");
grad.addColorStop(1, "rgba(100,150,255,0)");
ctx.fillStyle = grad;
ctx.fillRect(0, scanlineY - 20, width, 40);
// Scanline effect
ctx.globalAlpha = 0.05;
for (let y = 0; y < height; y += 2) {
ctx.fillStyle = "#000";
ctx.fillRect(0, y, width, 1);
}
if (conjureActive && conjureTimer > 0) {
const progress = 1 - (conjureTimer / 3.5);
const alpha = 0.15 + 0.5*Math.sin(progress*Math.PI);
const cx = width/2;
const cy = height/2;
const maxR = Math.min(width,height)*0.3;
const r = maxR*(0.6 + 0.4*Math.sin(progress*Math.PI*2));
const grad = ctx.createRadialGradient(cx,cy,0,cx,cy,r);
grad.addColorStop(0,`rgba(230,230,255,${alpha})`);
grad.addColorStop(0.4,`rgba(180,220,255,${alpha*0.7})`);
grad.addColorStop(1,"rgba(0,0,0,0)");
ctx.save();
ctx.globalCompositeOperation = "screen";
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(cx,cy,r,0,Math.PI*2);
ctx.fill();
ctx.restore();
conjureTimer -= dt;
lastConjureEndLogged = false;
if (conjureTimer <= 0) {
conjureTimer = 0;
if (viewMode !== "simple") {
conjureActive = false;
if (conjureBtn) conjureBtn.classList.remove("active");
if (!lastConjureEndLogged) {
log("CONJURE_END", "Entity collage pulse dissipated.");
lastConjureEndLogged = true;
}
} else {
conjureActive = false;
}
}
}
// Vignette
ctx.save();
const vignette = ctx.createRadialGradient(
width/2, height/2, Math.min(width,height)/4,
width/2, height/2, Math.max(width,height)/1.2
);
vignette.addColorStop(0,"rgba(0,0,0,0)");
vignette.addColorStop(1,"rgba(0,0,0,0.8)");
ctx.globalCompositeOperation = "multiply";
ctx.fillStyle = vignette;
ctx.fillRect(0,0,width,height);
ctx.restore();
updateButtonStates();
requestAnimationFrame(loop);
}
// Spawn initial entities and lava blobs so something shows immediately
for (let i = 0; i < 20; i++) {
entities.push(createEntity(0.5));
}
for (let i = 0; i < 5; i++) {
swirls.push(createSwirl(performance.now()));
}
for (let i = 0; i < 10; i++) {
lavaBlobs.push(createLavaBlob());
}
for (let i = 0; i < 5; i++) {
noiseMasks.push(createNoiseMask());
}
log("SYSTEM_INIT", "PROJECT ORACLE-93 initialized. Geometric synthesis engine ready.");
requestAnimationFrame(loop);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment