114 lines
3.4 KiB
JavaScript
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);
|
|
})(); |