742 lines
24 KiB
JavaScript
742 lines
24 KiB
JavaScript
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;
|
||
makenight();
|
||
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;
|
||
makenight();
|
||
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;
|
||
makenight();
|
||
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;
|
||
makenight();
|
||
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
|
||
makenight();
|
||
|
||
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);
|
||
} |