fetch('./ddate-now') .then(r => r.text()) .then(d => { document.getElementById('ddate').textContent = d.trim(); }) .catch(() => { document.getElementById('ddate').textContent = ''; }); const canvas = document.getElementById('warp'); const ctx = canvas.getContext('2d'); let w, h, cx, cy, stars = [], animId = null, active = false; const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; const isMobile = window.innerWidth < 768 || window.innerHeight < 600 || /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); const starCount = isMobile ? 200 : 400; const speed = prefersReduced ? 0.01 : 0.02; function resize() { w = canvas.width = window.innerWidth; h = canvas.height = window.innerHeight; cx = w / 2; cy = h / 2; } class Star { constructor() { this.reset(); } reset() { const angle = Math.random() * Math.PI * 2; const radius = Math.random() * Math.max(w, h); this.x = Math.cos(angle) * radius; this.y = Math.sin(angle) * radius; this.z = Math.random() * w; this.pz = this.z; } update() { this.pz = this.z; this.z -= speed * this.z; if (this.z < 1) this.reset(); } draw() { const sz = 1 / this.z, spz = 1 / this.pz; const sx = this.x * sz * w + cx, sy = this.y * sz * h + cy; const px = this.x * spz * w + cx, py = this.y * spz * h + cy; const r = Math.max(0, (1 - this.z / w) * 2); ctx.beginPath(); ctx.strokeStyle = `rgba(255, 255, 255, ${r})`; ctx.lineWidth = r * 2; ctx.moveTo(px, py); ctx.lineTo(sx, sy); ctx.stroke(); } } function init() { resize(); stars = []; for (let i = 0; i < starCount; i++) stars.push(new Star()); } function animate() { ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'; ctx.fillRect(0, 0, w, h); for (let i = 0; i < stars.length; i++) { stars[i].update(); stars[i].draw(); } animId = requestAnimationFrame(animate); } function startWarp() { if (active) return; active = true; canvas.classList.add('active'); init(); animate(); } function stopWarp() { if (!active) return; active = false; canvas.classList.remove('active'); if (animId) cancelAnimationFrame(animId); } document.addEventListener('click', (e) => { if (e.target.classList.contains('warp-trigger')) { e.preventDefault(); active ? stopWarp() : startWarp(); } }); window.addEventListener('resize', () => { if (active) resize(); });