library / particles & systems / starfield-warp
live - 60fps 1920 x 1080
starfield-warp.html - self-contained
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Starfield Warp</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            overflow: hidden;
            background: #000;
            height: 100vh;
        }

        canvas {
            display: block;
        }

        /* Click warp flash overlay */
        #warp-flash {
            position: fixed;
            inset: 0;
            pointer-events: none;
            opacity: 0;
            background: radial-gradient(ellipse at 50% 50%,
                    rgba(100, 180, 255, 0) 30%,
                    rgba(60, 120, 255, 0.4) 70%,
                    rgba(0, 60, 180, 0.7) 100%);
            transition: opacity 0.08s ease-in;
        }

        #warp-flash.on {
            opacity: 1;
            transition: opacity 0.08s ease-in;
        }

        #warp-flash.off {
            opacity: 0;
            transition: opacity 0.6s ease-out;
        }

        #hint {
            position: fixed;
            bottom: 24px;
            left: 50%;
            transform: translateX(-50%);
            color: rgba(120, 180, 255, 0.45);
            font-family: 'Courier New', monospace;
            font-size: 0.75rem;
            letter-spacing: 0.15em;
            text-transform: uppercase;
            pointer-events: none;
            animation: fadehint 1s ease-out 3s forwards;
        }

        @keyframes fadehint {
            to {
                opacity: 0;
            }
        }
    </style>
</head>

<body>
    <canvas id="canvas"></canvas>
    <div id="warp-flash"></div>
    <div id="hint">Scroll to accelerate · Click to warp</div>

    <script>
        const canvas = document.getElementById('canvas');
        const ctx = canvas.getContext('2d');
        const flash = document.getElementById('warp-flash');

        let W = canvas.width = window.innerWidth;
        let H = canvas.height = window.innerHeight;
        const CX = () => W / 2;
        const CY = () => H / 2;

        window.addEventListener('resize', () => {
            W = canvas.width = window.innerWidth;
            H = canvas.height = window.innerHeight;
        });

        // ----- Speed state -----
        let targetSpeed = 5;
        let speed = 5;
        let warpBurst = 0;       // 0–1, decays after click
        const MIN_SPD = 2;
        const MAX_SPD = 45;

        window.addEventListener('wheel', e => {
            targetSpeed = Math.max(MIN_SPD, Math.min(MAX_SPD, targetSpeed + e.deltaY * 0.04));
        }, { passive: true });

        canvas.addEventListener('click', () => {
            warpBurst = 1.0;
            flash.className = 'on';
            setTimeout(() => { flash.className = 'off'; }, 100);
        });

        // ----- Milky Way band params -----
        const BAND_ANGLE = 0.35; // radians
        const BAND_WIDTH = H * 0.18;

        function inMilkyWay(x, y) {
            // Rotate point and check if near diagonal band
            const rx = (x - W / 2) * Math.cos(-BAND_ANGLE) - (y - H / 2) * Math.sin(-BAND_ANGLE);
            return Math.abs(rx) < BAND_WIDTH / 2;
        }

        // ----- Stars -----
        const STAR_COUNT = 900;

        class Star {
            constructor(reset = false) { this.init(reset); }

            init(reset = false) {
                // Milky way density: 30% of stars cluster in the band
                if (Math.random() < 0.3) {
                    // Place star near band
                    const along = (Math.random() - 0.5) * Math.max(W, H) * 2;
                    const perp = (Math.random() - 0.5) * BAND_WIDTH;
                    this.sx = along * Math.cos(BAND_ANGLE) - perp * Math.sin(BAND_ANGLE) + W / 2;
                    this.sy = along * Math.sin(BAND_ANGLE) + perp * Math.cos(BAND_ANGLE) + H / 2;
                } else {
                    this.sx = (Math.random() - 0.5) * W * 2;
                    this.sy = (Math.random() - 0.5) * H * 2;
                }
                this.z = reset ? Math.random() * W : W;
                this.pz = this.z;
                this.milky = inMilkyWay(this.sx + W / 2, this.sy + H / 2);
            }

            update(spd) {
                this.pz = this.z;
                this.z -= spd;
                if (this.z <= 1) { this.init(false); }
            }

            draw(spd) {
                const scale = W / 2;
                const sx = (this.sx / this.z) * scale + CX();
                const sy = (this.sy / this.z) * scale + CY();

                if (sx < -10 || sx > W + 10 || sy < -10 || sy > H + 10) return;

                const t = 1 - this.z / W;       // 0 at far, 1 at near
                const r = t * (this.milky ? 1.5 : 2.2);

                // Color: blue-white at center → warm gold near edge → pure white
                let hue, sat, lit;
                if (warpBurst > 0.3 || spd > 22) {
                    hue = 205; sat = 100; lit = 60 + t * 40;
                } else if (this.milky) {
                    // milky way: cool blue-white
                    hue = 210; sat = 30; lit = 70 + t * 30;
                } else {
                    hue = 45 + t * 15; sat = 10 + t * 30; lit = 60 + t * 40;
                }

                const alpha = Math.min(1, t * 1.4 + 0.1);

                // Draw streaks at speed
                if (spd > 10 || warpBurst > 0.1) {
                    const px = (this.sx / this.pz) * scale + CX();
                    const py = (this.sy / this.pz) * scale + CY();
                    const streakAlpha = alpha * Math.min(1, spd / 20) * (warpBurst > 0 ? 1.4 : 1);
                    const grad = ctx.createLinearGradient(px, py, sx, sy);
                    grad.addColorStop(0, `hsla(${hue},${sat}%,${lit}%,0)`);
                    grad.addColorStop(1, `hsla(${hue},${sat}%,${lit}%,${Math.min(1, streakAlpha)})`);
                    ctx.beginPath();
                    ctx.moveTo(px, py);
                    ctx.lineTo(sx, sy);
                    ctx.strokeStyle = grad;
                    ctx.lineWidth = r * 0.9;
                    ctx.stroke();
                }

                // Draw star point
                if (r > 0.3) {
                    ctx.beginPath();
                    ctx.arc(sx, sy, r, 0, Math.PI * 2);
                    ctx.fillStyle = `hsla(${hue},${sat}%,${lit}%,${alpha})`;
                    ctx.fill();

                    // Lens cross at high speed
                    if (spd > 28 || warpBurst > 0.5) {
                        const arm = r * 5;
                        ctx.strokeStyle = `hsla(${hue},${sat}%,${lit}%,${alpha * 0.3})`;
                        ctx.lineWidth = 0.5;
                        ctx.beginPath();
                        ctx.moveTo(sx - arm, sy); ctx.lineTo(sx + arm, sy);
                        ctx.moveTo(sx, sy - arm); ctx.lineTo(sx, sy + arm);
                        ctx.stroke();
                    }
                }
            }
        }

        const stars = Array.from({ length: STAR_COUNT }, () => new Star(true));

        function animate() {
            // Smooth speed with exponential easing
            const effectiveTarget = warpBurst > 0 ? MAX_SPD : targetSpeed;
            speed += (effectiveTarget - speed) * 0.05;
            warpBurst *= 0.94; // decay warp burst

            const trailAlpha = warpBurst > 0.2 ? 0.25 : Math.max(0.08, 1 - speed / 15);
            ctx.fillStyle = `rgba(0, 0, 0, ${trailAlpha})`;
            ctx.fillRect(0, 0, W, H);

            for (const s of stars) {
                s.update(speed);
                s.draw(speed);
            }

            requestAnimationFrame(animate);
        }

        animate();
    </script>
</body>

</html>

About this animation

High-speed starfield with scroll-controlled acceleration, Milky Way band density, and click warp burst.

Source Code
Source copied
Partner hosting options for the copied effect:
Vercel Netlify Hostinger