Files
Halcyon/web/static/starfield.js
T
2026-09-19 20:21:47 +02:00

91 lines
2.8 KiB
JavaScript

// Slowly drifting, twinkling starfield with the odd shooting star.
export function startStarfield(canvas) {
const ctx = canvas.getContext('2d');
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)');
const tints = ['#ffffff', '#ffffff', '#ffffff', '#9ff8ff', '#ffb3f0'];
let w = 0;
let h = 0;
let stars = [];
let shooters = [];
let nextShot = 4000;
let last = performance.now();
let raf = 0;
function resize() {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
w = window.innerWidth;
h = window.innerHeight;
canvas.width = Math.round(w * dpr);
canvas.height = Math.round(h * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
const n = Math.min(320, Math.round((w * h) / 5500));
stars = Array.from({ length: n }, () => ({
x: Math.random() * w,
y: Math.random() * h,
z: 0.25 + Math.random() * 0.75,
phase: Math.random() * Math.PI * 2,
speed: 0.6 + Math.random() * 1.8,
tint: tints[Math.floor(Math.random() * tints.length)],
}));
draw(performance.now());
}
function draw(now) {
ctx.clearRect(0, 0, w, h);
for (const s of stars) {
const tw = 0.5 + 0.5 * Math.sin(now / 1000 * s.speed + s.phase);
ctx.globalAlpha = reduce.matches ? 0.5 * s.z : (0.25 + 0.75 * tw) * s.z;
ctx.fillStyle = s.tint;
const r = 0.4 + s.z * 1.3;
ctx.fillRect(s.x, s.y, r, r);
}
for (const s of shooters) {
const g = ctx.createLinearGradient(s.x, s.y, s.x - s.vx * 9, s.y - s.vy * 9);
g.addColorStop(0, 'rgba(255,255,255,0.95)');
g.addColorStop(1, 'rgba(255,58,216,0)');
ctx.globalAlpha = Math.min(1, s.life);
ctx.strokeStyle = g;
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(s.x, s.y);
ctx.lineTo(s.x - s.vx * 9, s.y - s.vy * 9);
ctx.stroke();
}
ctx.globalAlpha = 1;
}
function frame(now) {
const dt = Math.min(64, now - last);
last = now;
for (const s of stars) {
s.x -= s.z * 0.012 * dt;
if (s.x < -2) { s.x = w + 2; s.y = Math.random() * h; }
}
nextShot -= dt;
if (nextShot <= 0) {
nextShot = 7000 + Math.random() * 9000;
shooters.push({ x: Math.random() * w * 0.8 + w * 0.2, y: Math.random() * h * 0.4, vx: -(6 + Math.random() * 4), vy: 2 + Math.random() * 2, life: 1.4 });
}
for (const s of shooters) {
s.x += s.vx * dt / 16;
s.y += s.vy * dt / 16;
s.life -= dt / 900;
}
shooters = shooters.filter((s) => s.life > 0);
draw(now);
raf = requestAnimationFrame(frame);
}
function sync() {
cancelAnimationFrame(raf);
if (!reduce.matches) raf = requestAnimationFrame((t) => { last = t; frame(t); });
else draw(performance.now());
}
window.addEventListener('resize', resize);
reduce.addEventListener('change', sync);
resize();
sync();
}