donau2space-dev/js/starfield.js

221 lines
5.8 KiB
JavaScript

// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// DONAU2SPACE // DEV ENTITY — Canvas Starfield
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
import { random, lerp, clamp } from './utils.js';
const STAR_COUNT = 900;
const LAYER_COUNT = 3;
const SHOOT_CHANCE = 0.001;
let canvas, ctx, W, H;
let stars = [];
let shootingStars = [];
let mouseX = 0.5, mouseY = 0.5;
let targetParallax = { x: 0, y: 0 };
let parallax = { x: 0, y: 0 };
let warpFactor = 0;
let warpTarget = 0;
let drift = 0;
let running = true;
let nebulae = [];
function createStar() {
const layer = (Math.random() * LAYER_COUNT) | 0;
const depth = (layer + 1) / LAYER_COUNT;
return {
x: Math.random() * 2 - 0.5,
y: Math.random() * 2 - 0.5,
size: random(0.3, 1.8) * depth,
brightness: random(0.3, 1),
twinkleSpeed: random(0.5, 3),
twinklePhase: random(0, Math.PI * 2),
layer,
depth,
hue: Math.random() < 0.15 ? random(20, 50) : random(200, 240),
saturation: Math.random() < 0.8 ? random(0, 15) : random(40, 70),
};
}
function createNebula() {
return {
x: random(0.1, 0.9),
y: random(0.1, 0.9),
radius: random(150, 400),
hue: random(180, 300),
alpha: random(0.01, 0.03),
drift: random(-0.00002, 0.00002),
};
}
function createShootingStar() {
const angle = random(-0.5, -0.2);
const speed = random(6, 14);
return {
x: random(-0.1, 1.1),
y: random(-0.1, 0.3),
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * -speed,
life: 1,
decay: random(0.015, 0.035),
length: random(40, 100),
brightness: random(0.6, 1),
};
}
export function initStarfield(canvasEl) {
canvas = canvasEl;
ctx = canvas.getContext('2d');
resize();
window.addEventListener('resize', resize);
document.addEventListener('mousemove', e => {
mouseX = e.clientX / W;
mouseY = e.clientY / H;
});
for (let i = 0; i < STAR_COUNT; i++) stars.push(createStar());
for (let i = 0; i < 4; i++) nebulae.push(createNebula());
tick();
}
function resize() {
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
}
function tick() {
if (!running) return;
requestAnimationFrame(tick);
draw();
}
function draw() {
const time = performance.now() / 1000;
drift += 0.0001;
// Parallax smoothing
targetParallax.x = (mouseX - 0.5) * 30;
targetParallax.y = (mouseY - 0.5) * 20;
parallax.x = lerp(parallax.x, targetParallax.x, 0.03);
parallax.y = lerp(parallax.y, targetParallax.y, 0.03);
// Warp smoothing
warpFactor = lerp(warpFactor, warpTarget, 0.02);
ctx.clearRect(0, 0, W, H);
// Nebulae (subtle colored glow patches)
for (const neb of nebulae) {
neb.x += neb.drift;
const nx = neb.x * W + parallax.x * 0.2;
const ny = neb.y * H + parallax.y * 0.2;
const grad = ctx.createRadialGradient(nx, ny, 0, nx, ny, neb.radius);
grad.addColorStop(0, `hsla(${neb.hue}, 60%, 40%, ${neb.alpha})`);
grad.addColorStop(1, 'transparent');
ctx.fillStyle = grad;
ctx.fillRect(nx - neb.radius, ny - neb.radius, neb.radius * 2, neb.radius * 2);
}
// Stars
for (const star of stars) {
const twinkle = Math.sin(time * star.twinkleSpeed + star.twinklePhase) * 0.3 + 0.7;
const alpha = star.brightness * twinkle;
// Parallax offset based on depth
const px = parallax.x * star.depth;
const py = parallax.y * star.depth;
// Warp stretch
const warpStretch = warpFactor * star.depth * 40;
let sx = (star.x + drift * star.depth) * W + px;
let sy = star.y * H + py;
// Wrap around
sx = ((sx % W) + W) % W;
sy = ((sy % H) + H) % H;
const size = star.size * (1 + warpFactor * 2);
ctx.globalAlpha = clamp(alpha, 0, 1);
if (warpFactor > 0.1) {
// Warp mode: draw streaks
ctx.strokeStyle = `hsla(${star.hue}, ${star.saturation}%, 85%, ${alpha})`;
ctx.lineWidth = size * 0.6;
ctx.beginPath();
ctx.moveTo(sx, sy);
ctx.lineTo(sx - warpStretch, sy);
ctx.stroke();
} else {
// Normal: draw dots with subtle glow
ctx.fillStyle = `hsla(${star.hue}, ${star.saturation}%, 85%, ${alpha})`;
ctx.beginPath();
ctx.arc(sx, sy, size, 0, Math.PI * 2);
ctx.fill();
// Glow for brighter stars
if (star.brightness > 0.7 && star.size > 1) {
ctx.globalAlpha = alpha * 0.15;
ctx.beginPath();
ctx.arc(sx, sy, size * 3, 0, Math.PI * 2);
ctx.fill();
}
}
}
ctx.globalAlpha = 1;
// Shooting stars
if (Math.random() < SHOOT_CHANCE && shootingStars.length < 3) {
shootingStars.push(createShootingStar());
}
for (let i = shootingStars.length - 1; i >= 0; i--) {
const ss = shootingStars[i];
ss.x += ss.vx / W;
ss.y += ss.vy / H;
ss.life -= ss.decay;
if (ss.life <= 0) {
shootingStars.splice(i, 1);
continue;
}
const sx = ss.x * W;
const sy = ss.y * H;
const ex = sx - (ss.vx / W) * ss.length;
const ey = sy - (ss.vy / H) * ss.length;
const grad = ctx.createLinearGradient(sx, sy, ex, ey);
grad.addColorStop(0, `rgba(255, 255, 255, ${ss.life * ss.brightness})`);
grad.addColorStop(1, 'rgba(255, 255, 255, 0)');
ctx.strokeStyle = grad;
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(sx, sy);
ctx.lineTo(ex, ey);
ctx.stroke();
}
}
export function setWarp(factor) {
warpTarget = clamp(factor, 0, 1);
}
export function triggerShootingStar() {
shootingStars.push(createShootingStar());
}
export function pause() { running = false; }
export function resume() {
if (!running) { running = true; tick(); }
}
export function setOpacity(val) {
if (canvas) canvas.style.opacity = clamp(val, 0, 1);
}