init
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
// 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();
|
||||
}
|
||||
Reference in New Issue
Block a user