all movies have animations
This commit is contained in:
@@ -0,0 +1,727 @@
|
||||
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;
|
||||
|
||||
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() * 10), // staggered start above top
|
||||
speed: 0.1 + Math.random() * 0.5,
|
||||
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
|
||||
if (drop.y * FONT_SIZE > H + TRAIL_LEN * FONT_SIZE) {
|
||||
drop.y = -(Math.random() * 30);
|
||||
drop.speed = 0.3 + Math.random() * 0.3;
|
||||
drop.glyphs = [];
|
||||
}
|
||||
});
|
||||
|
||||
if (matrixCanvas) matrixAnimFrame = requestAnimationFrame(drawFrame);
|
||||
}
|
||||
|
||||
matrixAnimFrame = requestAnimationFrame(drawFrame);
|
||||
}
|
||||
|
||||
function stopMatrix() {
|
||||
if (!matrixCanvas) return;
|
||||
matrixCanvas.style.opacity = '0';
|
||||
cancelAnimationFrame(matrixAnimFrame);
|
||||
setTimeout(() => {
|
||||
matrixCanvas.remove();
|
||||
matrixCanvas = null;
|
||||
}, 1200);
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user