function cureClick() { if (document.getElementById('cure-canvas')) return; if (document.getElementById('lightning-canvas')) stopLightning(); if (document.getElementById('typewriter-canvas')) stopTypewriter(); if (document.getElementById('matrix-canvas')) stopMatrix(); if (document.getElementById('rain-canvas')) stopRain(); const el = document.getElementById('interest'); el.textContent = "Actually... I'm feeling much better now!" startWater(); isDay = false; applyState(); setTimeout(() => { el.textContent = 'interest'; stopWater(); }, 10000); } let cureCanvas, cureCtx, cureAnimFrame; function startWater() { cureCanvas = document.createElement('canvas'); cureCanvas.id = 'cure-canvas'; cureCanvas.width = window.innerWidth; cureCanvas.height = window.innerHeight; Object.assign(cureCanvas.style, { position: 'fixed', top: 0, left: 0, pointerEvents: 'none', zIndex: -1, // behind everything opacity: '0', transition: 'opacity 1.4s ease' }); document.body.appendChild(cureCanvas); cureCtx = cureCanvas.getContext('2d'); requestAnimationFrame(() => { cureCanvas.style.opacity = '1'; }); const W = cureCanvas.width; const H = cureCanvas.height; // waterline sits at 38% from the top const waterlineY = H * 0.38; // floating particles below the surface const particles = Array.from({ length: 55 }, () => ({ x: Math.random() * W, y: waterlineY + 20 + Math.random() * (H - waterlineY - 20), r: 1 + Math.random() * 2.5, speedX: (Math.random() - 0.5) * 0.3, speedY: -0.08 - Math.random() * 0.18, // slow upward drift opacity: 0.08 + Math.random() * 0.18 })); function drawFrame(ts) { cureCtx.clearRect(0, 0, W, H); // ── sky above waterline (very subtle, mostly transparent) ────────── const sky = cureCtx.createLinearGradient(0, 0, 0, waterlineY); sky.addColorStop(0, 'rgba(190, 230, 255, 0.06)'); sky.addColorStop(1, 'rgba(190, 230, 255, 0.01)'); cureCtx.fillStyle = sky; cureCtx.fillRect(0, 0, W, waterlineY); // ── water body below waterline ───────────────────────────────────── const water = cureCtx.createLinearGradient(0, waterlineY, 0, H); water.addColorStop(0, 'rgba(80, 190, 220, 0.28)'); water.addColorStop(0.35, 'rgba(40, 140, 190, 0.22)'); water.addColorStop(0.7, 'rgba(20, 90, 150, 0.20)'); water.addColorStop(1, 'rgba(10, 50, 110, 0.22)'); cureCtx.fillStyle = water; cureCtx.fillRect(0, waterlineY, W, H - waterlineY); // ── caustic shimmer streaks ──────────────────────────────────────── const numStreaks = 10; for (let i = 0; i < numStreaks; i++) { const phase = (ts * 0.0004 + i * 0.63) % 1; const x = (i / numStreaks) * W + Math.sin(ts * 0.0008 + i) * 60; const yTop = waterlineY + 10 + Math.sin(ts * 0.001 + i * 1.3) * 18; const yBot = yTop + 40 + Math.sin(ts * 0.0007 + i * 0.9) * 30; const streak = cureCtx.createLinearGradient(x, yTop, x + 6, yBot); streak.addColorStop(0, 'rgba(200, 245, 255, 0)'); streak.addColorStop(0.4, `rgba(200, 245, 255, ${0.05 + 0.06 * Math.sin(ts * 0.002 + i)})`); streak.addColorStop(1, 'rgba(200, 245, 255, 0)'); cureCtx.fillStyle = streak; cureCtx.fillRect(x, yTop, 5, yBot - yTop); } // ── floating particles ───────────────────────────────────────────── particles.forEach(p => { p.x += p.speedX; p.y += p.speedY; // reset when reaching the surface if (p.y < waterlineY + 5) { p.y = H - 10; p.x = Math.random() * W; } if (p.x < 0) p.x = W; if (p.x > W) p.x = 0; const depth = (p.y - waterlineY) / (H - waterlineY); // 0 = surface, 1 = bottom cureCtx.beginPath(); cureCtx.arc(p.x, p.y, p.r, 0, Math.PI * 2); cureCtx.fillStyle = `rgba(180, 240, 255, ${p.opacity * (0.4 + 0.6 * (1 - depth))})`; cureCtx.fill(); }); // ── animated surface waves (2 layers, different speeds & amplitudes) ─ function drawWaveLine(amp, freq, speed, lineW, alpha) { cureCtx.beginPath(); for (let x = 0; x <= W; x += 3) { const y = waterlineY + Math.sin(x * freq + ts * speed) * amp + Math.sin(x * freq * 1.7 + ts * speed * 0.6) * (amp * 0.4); x === 0 ? cureCtx.moveTo(x, y) : cureCtx.lineTo(x, y); } cureCtx.strokeStyle = `rgba(210, 245, 255, ${alpha})`; cureCtx.lineWidth = lineW; cureCtx.stroke(); } // primary wave drawWaveLine(7, 0.012, 0.0018, 2.0, 0.70); // secondary wave (slightly offset phase) drawWaveLine(4, 0.018, 0.0024, 1.2, 0.40); // micro ripple drawWaveLine(2, 0.030, 0.0032, 0.7, 0.25); // ── foam/highlight on the crest ──────────────────────────────────── cureCtx.beginPath(); for (let x = 0; x <= W; x += 3) { const y = waterlineY + Math.sin(x * 0.012 + ts * 0.0018) * 7 + Math.sin(x * 0.020 + ts * 0.0024) * 3; x === 0 ? cureCtx.moveTo(x, y - 1) : cureCtx.lineTo(x, y - 1); } cureCtx.strokeStyle = 'rgba(255, 255, 255, 0.18)'; cureCtx.lineWidth = 1; cureCtx.stroke(); if (cureCanvas) cureAnimFrame = requestAnimationFrame(drawFrame); } cureAnimFrame = requestAnimationFrame(drawFrame); } function stopWater() { if (!cureCanvas) return; cureCanvas.style.opacity = '0'; cancelAnimationFrame(cureAnimFrame); setTimeout(() => { cureCanvas.remove(); cureCanvas = null; }, 1400); } function draculaClick() { if (document.getElementById('lightning-canvas')) return; if (document.getElementById('cure-canvas')) stopWater(); if (document.getElementById('typewriter-canvas')) stopTypewriter(); if (document.getElementById('matrix-canvas')) stopMatrix(); if (document.getElementById('rain-canvas')) stopRain(); const el = document.getElementById('interest'); el.textContent = 'You have your whole life ahead of you. And I only offer death.'; startLightning(); isDay = false; applyState(); setTimeout(() => { el.textContent = 'interest'; stopLightning(); }, 10000); } let lightningCanvas, lightningCtx, lightningInterval, lightningFlashTimeout; function startLightning() { lightningCanvas = document.createElement('canvas'); lightningCanvas.id = 'lightning-canvas'; lightningCanvas.width = window.innerWidth; lightningCanvas.height = window.innerHeight; Object.assign(lightningCanvas.style, { position: 'fixed', top: 0, left: 0, pointerEvents: 'none', zIndex: 9999, opacity: '0', transition: 'opacity 0.3s ease' }); document.body.appendChild(lightningCanvas); lightningCtx = lightningCanvas.getContext('2d'); requestAnimationFrame(() => { lightningCanvas.style.opacity = '1'; }); function drawBolt(x1, y1, x2, y2, roughness, depth) { if (depth === 0) { lightningCtx.beginPath(); lightningCtx.moveTo(x1, y1); lightningCtx.lineTo(x2, y2); lightningCtx.stroke(); return; } const mx = (x1 + x2) / 2 + (Math.random() - 0.5) * roughness; const my = (y1 + y2) / 2 + (Math.random() - 0.5) * roughness; drawBolt(x1, y1, mx, my, roughness / 2, depth - 1); drawBolt(mx, my, x2, y2, roughness / 2, depth - 1); // random fork branch if (depth > 2 && Math.random() < 0.4) { const fx = mx + (Math.random() - 0.3) * roughness * 2; const fy = my + Math.random() * roughness * 2; lightningCtx.save(); lightningCtx.globalAlpha *= 0.5; drawBolt(mx, my, fx, fy, roughness / 2, depth - 2); lightningCtx.restore(); } } function flashStrike() { lightningCtx.clearRect(0, 0, lightningCanvas.width, lightningCanvas.height); const numBolts = 1 + Math.floor(Math.random() * 3); for (let i = 0; i < numBolts; i++) { const startX = lightningCanvas.width * (0.1 + Math.random() * 0.8); const endX = startX + (Math.random() - 0.5) * 300; const color = Math.random() < 0.6 ? `rgba(220, 180, 255, ${0.6 + Math.random() * 0.4})` // purple : `rgba(255, 255, 255, ${0.7 + Math.random() * 0.3})`; // white lightningCtx.strokeStyle = color; lightningCtx.lineWidth = 0.5 + Math.random() * 2; lightningCtx.shadowColor = color; lightningCtx.shadowBlur = 18 + Math.random() * 30; lightningCtx.globalAlpha = 1; drawBolt(startX, 0, endX, lightningCanvas.height * (0.4 + Math.random() * 0.5), 120, 7); } // ambient flash on the body document.body.style.transition = 'background 0.05s'; document.body.style.background = `rgba(80, 0, 120, 0.08)`; setTimeout(() => { document.body.style.background = ''; }, 80); // fade the bolt out lightningFlashTimeout = setTimeout(() => { lightningCtx.clearRect(0, 0, lightningCanvas.width, lightningCanvas.height); }, 80 + Math.random() * 120); } // irregular strike cadence — like real lightning function scheduleStrike() { flashStrike(); const delay = 600 + Math.random() * 2200; lightningInterval = setTimeout(scheduleStrike, delay); } scheduleStrike(); } function stopLightning() { if (!lightningCanvas) return; lightningCanvas.style.opacity = '0'; clearTimeout(lightningInterval); clearTimeout(lightningFlashTimeout); setTimeout(() => { lightningCanvas.remove(); lightningCanvas = null; }, 800); } function dragonTypewriterClick() { if (document.getElementById('typewriter-canvas')) return; if (document.getElementById('lightning-canvas')) stopLightning(); if (document.getElementById('cure-canvas')) stopWater(); if (document.getElementById('matrix-canvas')) stopMatrix(); if (document.getElementById('rain-canvas')) stopRain(); const el = document.getElementById('interest'); el.textContent = 'case file 36609...'; startTypewriter(); isDay = false; applyState(); setTimeout(() => { el.textContent = 'interest'; stopTypewriter(); }, 10000); } let twCanvas, twCtx, twAnimFrame; function startTypewriter() { twCanvas = document.createElement('canvas'); twCanvas.id = 'typewriter-canvas'; twCanvas.width = window.innerWidth; twCanvas.height = window.innerHeight; Object.assign(twCanvas.style, { position: 'fixed', top: 0, left: 0, pointerEvents: 'none', zIndex: -1, opacity: '0', transition: 'opacity 1.2s ease' }); document.body.appendChild(twCanvas); twCtx = twCanvas.getContext('2d'); requestAnimationFrame(() => { twCanvas.style.opacity = '1'; }); const W = twCanvas.width; const H = twCanvas.height; // ── case file fragments ────────────────────────────────────────────── const fragments = [ 'SUBJECT: HARRIET VANGER', 'CASE NO. 36609-74', 'CLASSIFICATION: MISSING / PRESUMED', '████████████ DECEASED ████████████', 'LAST SEEN: HEDEBY ISLAND', 'NOVEMBER 1, 1966', 'INVESTIGATING OFFICER: ██████████', 'FINANCIAL IRREGULARITIES NOTED', 'SEE ATTACHMENT ██-C', 'WENNERSTROM GROUP — CLASSIFIED', 'DO NOT DISTRIBUTE', 'SOURCE: CONFIDENTIAL', 'SALANDER, L. — AUTHORIZED', 'BLOMKVIST, M. — AUTHORIZED', 'ALL OTHERS: ACCESS DENIED', '████████████████████████████', 'MOTIVE: UNKNOWN', 'SUSPECT LIST: REDACTED', 'EVIDENCE SEALED BY ORDER OF', '████████ COURT ████████', 'TRAUMA INDICATED / SEE REPORT 7', 'LISBETH — CLEARANCE LEVEL 3', 'THE DRAGON TATTOO', '——————————————————————', 'WHERE IS SHE.', 'WHO KNOWS.', 'FILE CLOSED: ██/██/████', ]; // glitch chars pool const glitchChars = '!@#$%&*?|\\/<>[]{}~^░▒▓█▄▀■□▪▫'; function randGlitch() { return glitchChars[Math.floor(Math.random() * glitchChars.length)]; } // ── active lines on screen ─────────────────────────────────────────── const lines = []; let fragmentPool = [...fragments]; function pickFragment() { if (!fragmentPool.length) fragmentPool = [...fragments]; const i = Math.floor(Math.random() * fragmentPool.length); return fragmentPool.splice(i, 1)[0]; } function spawnLine() { const text = pickFragment(); const fontSize = 11 + Math.floor(Math.random() * 9); const x = 40 + Math.random() * (W * 0.55); const y = 60 + Math.random() * (H - 120); const isRedact = text.includes('█') || Math.random() < 0.12; const speed = 28 + Math.random() * 22; // chars per second lines.push({ text, x, y, fontSize, typed: 0, // chars revealed so far (float) speed, glitching: false, glitchTimer: 0, glitchStr: '', opacity: 0.72 + Math.random() * 0.26, isRedact, done: false, holdTimer: 0, fadeOut: false, fadeAlpha: 1, stamp: Math.random() < 0.08, // rare CLASSIFIED/DENIED stamp }); } // spawn cadence spawnLine(); const spawnTimer = setInterval(() => { if (lines.length < 14) spawnLine(); }, 600); twCanvas._spawnTimer = spawnTimer; let lastTs = null; function drawFrame(ts) { if (!lastTs) lastTs = ts; const dt = (ts - lastTs) / 1000; lastTs = ts; twCtx.clearRect(0, 0, W, H); // faint scanline overlay — gives it that CRT/photocopy feel for (let sy = 0; sy < H; sy += 3) { twCtx.fillStyle = 'rgba(0,0,0,0.04)'; twCtx.fillRect(0, sy, W, 1); } lines.forEach((line, idx) => { twCtx.font = `${line.fontSize}px "Courier New", Courier, monospace`; if (line.fadeOut) { line.fadeAlpha -= dt * 0.6; if (line.fadeAlpha <= 0) { lines.splice(idx, 1); return; } } const alpha = line.opacity * (line.fadeOut ? line.fadeAlpha : 1); if (!line.done) { line.typed += line.speed * dt; if (line.typed >= line.text.length) { line.typed = line.text.length; line.done = true; } // random mid-type glitch burst if (!line.glitching && Math.random() < 0.004) { line.glitching = true; line.glitchTimer = 0.18 + Math.random() * 0.22; line.glitchStr = Array.from( { length: 3 + Math.floor(Math.random() * 5) }, randGlitch ).join(''); } } else { // after typing finishes, hold then fade line.holdTimer += dt; if (line.holdTimer > 3.5 + Math.random() * 3) { line.fadeOut = true; // spawn a replacement setTimeout(spawnLine, 200 + Math.random() * 600); } // occasional re-glitch on finished lines if (!line.glitching && Math.random() < 0.003) { line.glitching = true; line.glitchTimer = 0.1 + Math.random() * 0.15; line.glitchStr = Array.from( { length: 2 + Math.floor(Math.random() * 4) }, randGlitch ).join(''); } } // resolve glitch burst if (line.glitching) { line.glitchTimer -= dt; if (line.glitchTimer <= 0) { line.glitching = false; line.glitchStr = ''; } } // ── build visible string ─────────────────────────────────────── const visible = line.text.slice(0, Math.floor(line.typed)); let display = visible; // blinking cursor at the type head if (!line.done) { const blink = Math.floor(ts / 530) % 2 === 0; display += blink ? '▌' : ' '; } // append glitch burst after cursor if (line.glitching) display += line.glitchStr; // ── color choice ─────────────────────────────────────────────── // redacted blocks stay white-ish; normal text is the faded amber/grey const isRedactLine = line.text.includes('█'); if (isRedactLine) { twCtx.fillStyle = `rgba(220, 215, 200, ${alpha})`; } else if (line.stamp) { twCtx.fillStyle = `rgba(180, 30, 30, ${alpha * 0.85})`; } else { // slight warm/cool variation per line const warm = Math.random() < 0.5; twCtx.fillStyle = warm ? `rgba(210, 200, 180, ${alpha})` : `rgba(185, 200, 195, ${alpha})`; } twCtx.fillText(display, line.x, line.y); // ── underline for "stamp" lines ──────────────────────────────── if (line.stamp && line.done) { const mw = twCtx.measureText(line.text).width; twCtx.strokeStyle = `rgba(180, 30, 30, ${alpha * 0.6})`; twCtx.lineWidth = 1; twCtx.beginPath(); twCtx.moveTo(line.x, line.y + 3); twCtx.lineTo(line.x + mw, line.y + 3); twCtx.stroke(); } }); if (twCanvas) twAnimFrame = requestAnimationFrame(drawFrame); } twAnimFrame = requestAnimationFrame(drawFrame); } function stopTypewriter() { if (!twCanvas) return; twCanvas.style.opacity = '0'; clearInterval(twCanvas._spawnTimer); cancelAnimationFrame(twAnimFrame); setTimeout(() => { twCanvas.remove(); twCanvas = null; }, 1200); } function matrixClick() { if (document.getElementById('matrix-canvas')) return; if (document.getElementById('cure-canvas')) stopWater(); if (document.getElementById('lightning-canvas')) stopLightning(); if (document.getElementById('typewriter-canvas')) stopTypewriter(); if (document.getElementById('rain-canvas')) stopRain(); const el = document.getElementById('interest'); el.textContent = 'wake up, neo...'; startMatrix(); isDay = false; applyState(); setTimeout(() => { el.textContent = 'interest'; stopMatrix(); }, 10000); } let matrixCanvas, matrixCtx, matrixAnimFrame, matrixStopping = false; function startMatrix() { matrixCanvas = document.createElement('canvas'); matrixCanvas.id = 'matrix-canvas'; matrixCanvas.width = window.innerWidth; matrixCanvas.height = window.innerHeight; Object.assign(matrixCanvas.style, { position: 'fixed', top: 0, left: 0, pointerEvents: 'none', zIndex: -1, opacity: '0', transition: 'opacity 1.2s ease' }); document.body.appendChild(matrixCanvas); matrixCtx = matrixCanvas.getContext('2d'); requestAnimationFrame(() => { matrixCanvas.style.opacity = '1'; }); const W = matrixCanvas.width; const H = matrixCanvas.height; const FONT_SIZE = 16; const COLS = Math.floor(W / FONT_SIZE); // katakana unicode block ァ–ン (U+30A1–U+30F6) const KATA_START = 0x30A1; const KATA_END = 0x30F6; function randKata() { return String.fromCharCode( KATA_START + Math.floor(Math.random() * (KATA_END - KATA_START + 1)) ); } // each column: how far down it has dripped (in rows) // start negative so columns begin off-screen at staggered times const drops = Array.from({ length: COLS }, () => ({ y: -(Math.random() * 50), // staggered start above top speed: 0.1 + Math.random() * 0.2, glyphs: [] // trail of { char, age } })); const TRAIL_LEN = 14; // how many glyphs in the tail function drawFrame() { // dim the previous frame — this creates the fade trail matrixCtx.fillStyle = 'rgba(0, 0, 0, 0.10)'; matrixCtx.fillRect(0, 0, W, H); matrixCtx.font = `${FONT_SIZE}px monospace`; drops.forEach((drop, col) => { const x = col * FONT_SIZE; // advance drop drop.y += drop.speed; // push a new glyph at the head if (drop.y >= 0) { drop.glyphs.unshift({ char: randKata(), age: 0 }); if (drop.glyphs.length > TRAIL_LEN) drop.glyphs.pop(); } // age each glyph and draw drop.glyphs.forEach((g, i) => { g.age++; // randomly mutate the character occasionally if (Math.random() < 0.04) g.char = randKata(); const headY = Math.floor(drop.y) - i; if (headY < 0 || headY * FONT_SIZE > H) return; const t = i / TRAIL_LEN; // 0 = head, 1 = tail if (i === 0) { // head glyph — bright white flash matrixCtx.fillStyle = `rgba(200, 255, 220, 0.98)`; } else if (i < 3) { // near-head — bright green matrixCtx.fillStyle = `rgba(100, 255, 140, ${1 - t * 0.3})`; } else { // tail — fade to dark green const alpha = Math.max(0, 1 - t * 1.1); const green = Math.floor(180 - t * 120); matrixCtx.fillStyle = `rgba(0, ${green}, 60, ${alpha})`; } matrixCtx.fillText(g.char, x, headY * FONT_SIZE); }); // reset column when it scrolls fully off the bottom // reset column when it scrolls fully off the bottom if (drop.y * FONT_SIZE > H + TRAIL_LEN * FONT_SIZE) { if (matrixStopping) { // don't recycle — park it off-screen so the trail dies drop.y = -(TRAIL_LEN * 10); drop.glyphs = []; } else { drop.y = -(Math.random() * 30); drop.speed = 0.3 + Math.random() * 0.7; drop.glyphs = []; } } }); if (matrixCanvas) matrixAnimFrame = requestAnimationFrame(drawFrame); } matrixAnimFrame = requestAnimationFrame(drawFrame); } function stopMatrix() { if (!matrixCanvas) return; // signal: no new heads, let existing trails die out matrixStopping = true; // after trails have had time to fade, pull the canvas setTimeout(() => { cancelAnimationFrame(matrixAnimFrame); matrixCanvas.style.opacity = '0'; matrixCanvas.style.transition = 'opacity 1.2s ease'; setTimeout(() => { matrixCanvas.remove(); matrixCanvas = null; matrixStopping = false; }, 1200); }, 1800); } function bladerunnerClick() { if (document.getElementById('rain-canvas')) return; // already running, do nothing if (document.getElementById('cure-canvas')) stopWater(); if (document.getElementById('matrix-canvas')) stopMatrix(); if (document.getElementById('typewriter-canvas')) stopTypewriter(); if (document.getElementById('lightning-canvas')) stopLightning(); const el = document.getElementById('interest'); el.textContent = 'tears in rain'; startRain(); isDay = false applyState(); setTimeout(() => { el.textContent = "interest"; stopRain(); }, 10000); } let rainCanvas, rainCtx, rainInterval; function startRain() { rainCanvas = document.createElement('canvas'); rainCanvas.id = 'rain-canvas'; rainCanvas.width = window.innerWidth; rainCanvas.height = window.innerHeight; document.body.appendChild(rainCanvas); rainCtx = rainCanvas.getContext('2d'); requestAnimationFrame(() => { rainCanvas.style.opacity = '1'; }); const drops = Array.from({ length: 120 }, () => ({ x: Math.random() * rainCanvas.width, y: Math.random() * rainCanvas.height, speed: 8 + Math.random() * 7, length: 12 + Math.random() * 20, opacity: 0.2 + Math.random() * 0.4 })); rainInterval = setInterval(() => { rainCtx.clearRect(0, 0, rainCanvas.width, rainCanvas.height); drops.forEach(drop => { rainCtx.beginPath(); rainCtx.moveTo(drop.x, drop.y); rainCtx.lineTo(drop.x - 2, drop.y + drop.length); rainCtx.strokeStyle = `rgba(174, 214, 241, ${drop.opacity})`; rainCtx.lineWidth = 3; rainCtx.stroke(); drop.y += drop.speed; if (drop.y > rainCanvas.height) { drop.y = -drop.length; drop.x = Math.random() * rainCanvas.width; } }); }, 30); } function stopRain() { if (!rainCanvas) return; rainCanvas.style.opacity = '0'; setTimeout(() => { clearInterval(rainInterval); rainCanvas.remove(); rainCanvas = null; }, 800); }