Files
Space-Theme/web/public/assets/starfield.js
2026-02-02 23:57:09 +01:00

114 lines
3.4 KiB
JavaScript

(() => {
const canvas = document.getElementById("starfield");
if (!canvas) return;
const reduceMotion = window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches;
if (reduceMotion) return;
const ctx = canvas.getContext("2d", { alpha: true });
const cfg = {
get perf(){ return (document.documentElement.dataset.perf || "auto").toLowerCase(); },
starCount(){ return this.perf === "high" ? 1100 : this.perf === "medium" ? 650 : this.perf === "low" ? 0 : 500; },
fps(){ return this.perf === "high" ? 60 : this.perf === "medium" ? 30 : this.perf === "low" ? 0 : 25; },
dpr(){ return this.perf === "high" ? Math.min(devicePixelRatio||1, 2) : 1; },
twinkle(){ return this.perf === "high" ? 0.45 : 0.35; },
};
let w=0,h=0,dpr=1,stars=[],mouseX=0,mouseY=0,last=performance.now(),acc=0;
let lastPerf = "";
function rand(min,max){ return min + Math.random()*(max-min); }
function newStar(randomZ=false){
return { x: rand(-1,1), y: rand(-1,1), z: randomZ ? Math.random() : 1, r: rand(0.6,1.7), t: Math.random()*Math.PI*2 };
}
function initStars(){
const n = cfg.starCount();
stars = Array.from({length:n}, ()=>newStar(true));
}
function resize(){
dpr = cfg.dpr();
w = Math.floor(window.innerWidth);
h = Math.floor(window.innerHeight);
canvas.width = Math.floor(w*dpr);
canvas.height = Math.floor(h*dpr);
canvas.style.width = w+"px";
canvas.style.height = h+"px";
ctx.setTransform(dpr,0,0,dpr,0,0);
initStars();
}
function project(s){
const cx=w*0.5, cy=h*0.5;
const p = 1/(s.z*1.25);
return { x: cx + s.x*cx*p, y: cy + s.y*cy*p, size: s.r*p };
}
function tick(dt){
ctx.clearRect(0,0,w,h);
const g = ctx.createRadialGradient(w*0.5,h*0.45,0,w*0.5,h*0.45,Math.min(w,h)*0.85);
g.addColorStop(0,"rgba(66,245,255,0.04)");
g.addColorStop(0.5,"rgba(255,61,242,0.02)");
g.addColorStop(1,"rgba(0,0,0,0)");
ctx.fillStyle=g;
ctx.fillRect(0,0,w,h);
const mx=(mouseX-w*0.5)/(w*0.5), my=(mouseY-h*0.5)/(h*0.5);
const speed = (cfg.perf==="high" ? 0.03 : 0.022);
for(let i=0;i<stars.length;i++){
const s=stars[i];
s.z -= speed*dt;
if(s.z<=0.02) stars[i]=newStar(false);
s.t += dt*rand(2.0,5.0);
const p=project(s);
if(p.x<-60||p.x>w+60||p.y<-60||p.y>h+60) continue;
const tw = 0.7 + Math.sin(s.t)*cfg.twinkle()*0.25;
const alpha = Math.min(0.95, (0.18 + (1-s.z)*0.85)*tw);
const par = 0.22*(1-s.z);
const x2 = p.x + mx*18*par;
const y2 = p.y + my*12*par;
ctx.fillStyle = `rgba(240,247,255,${alpha})`;
ctx.beginPath();
ctx.arc(x2,y2,Math.max(0.7,p.size*0.55),0,Math.PI*2);
ctx.fill();
}
}
function frame(now){
const perf = cfg.perf;
if(perf !== lastPerf){
lastPerf = perf;
resize();
}
const fps = cfg.fps();
if(fps <= 0){ requestAnimationFrame(frame); return; }
const dt = Math.min((now-last)/1000, 0.05);
last = now;
acc += dt;
const step = 1/fps;
while(acc >= step){
tick(step);
acc -= step;
}
requestAnimationFrame(frame);
}
window.addEventListener("mousemove",(e)=>{ mouseX=e.clientX; mouseY=e.clientY; }, {passive:true});
window.addEventListener("resize", resize, {passive:true});
resize();
mouseX = window.innerWidth*0.5; mouseY = window.innerHeight*0.5;
requestAnimationFrame(frame);
})();