Community

Write a single self-contained HTML file …

a ado deepseek-v4-pro 3h ago
Write a single self-contained HTML file that renders an animated night sky with shooting stars.

This is a complete HTML document that creates an animated night sky scene with twinkling stars, a crescent moon, and dynamic shooting stars that streak across the canvas.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Night Sky - Shooting Stars</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            width: 100vw;
            height: 100vh;
            overflow: hidden;
            background: #050510;
            font-family: 'Georgia', serif;
        }

        canvas {
            display: block;
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
        }

        .hint {
            position: fixed;
            bottom: 30px;
            width: 100%;
            text-align: center;
            color: rgba(255, 255, 255, 0.4);
            font-size: 14px;
            letter-spacing: 2px;
            pointer-events: none;
            z-index: 10;
            animation: fadeHint 4s ease-in-out infinite;
        }

        @keyframes fadeHint {
            0%,
            100% {
                opacity: 0.3;
            }
            50% {
                opacity: 0.7;
            }
        }
    </style>
</head>
<body>
    <canvas id="skyCanvas"></canvas>
    <div class="hint">✨ look up ✨</div>

    <script>
        (function() {
            const canvas = document.getElementById('skyCanvas');
            const ctx = canvas.getContext('2d');

            let width, height;
            let backgroundCanvas;
            let backgroundCtx;

            // --- Configuration ---
            const STAR_COUNT = 350;
            const TWINKLE_STAR_COUNT = 80;
            const SHOOTING_STAR_MIN_INTERVAL = 1800; // ms
            const SHOOTING_STAR_MAX_INTERVAL = 5000; // ms
            const MAX_SHOOTING_STARS = 3;
            const TRAIL_PARTICLE_LIFETIME = 1.5; // seconds

            // --- Data ---
            let stars = [];
            let twinkleStars = [];
            let shootingStars = [];
            let trailParticles = [];
            let spawnTimer = 0;
            let lastTime = performance.now();
            let timeAccum = 0;

            // --- Color presets for shooting stars ---
            const shootingStarColors = [
                { r: 255, g: 255, b: 255, name: 'pure white' },
                { r: 255, g: 250, b: 240, name: 'warm white' },
                { r: 220, g: 235, b: 255, name: 'cool blue-white' },
                { r: 255, g: 240, b: 200, name: 'golden' },
                { r: 200, g: 240, b: 255, name: 'icy blue' },
                { r: 255, g: 220, b: 180, name: 'peach' },
                { r: 180, g: 255, b: 230, name: 'mint' },
            ];

            // --- Resize Handler ---
            function resize() {
                width = window.innerWidth;
                height = window.innerHeight;
                canvas.width = width;
                canvas.height = height;

                // Recreate background canvas
                backgroundCanvas = document.createElement('canvas');
                backgroundCanvas.width = width;
                backgroundCanvas.height = height;
                backgroundCtx = backgroundCanvas.getContext('2d');

                renderBackground();
                createStars();
            }

            // --- Render Static Background ---
            function renderBackground() {
                const ctx = backgroundCtx;
                const w = width;
                const h = height;

                // Sky gradient
                const skyGradient = ctx.createLinearGradient(0, 0, 0, h);
                skyGradient.addColorStop(0, '#0a0a2e');
                skyGradient.addColorStop(0.25, '#0d1035');
                skyGradient.addColorStop(0.5, '#111840');
                skyGradient.addColorStop(0.75, '#0e1538');
                skyGradient.addColorStop(1, '#0a0f2c');
                ctx.fillStyle = skyGradient;
                ctx.fillRect(0, 0, w, h);

                // Subtle nebula glows
                const nebulae = [
                    { x: w * 0.25, y: h * 0.2, r: Math.min(w, h) * 0.5, color: 'rgba(30, 20, 80, 0.08)' },
                    { x: w * 0.7, y: h * 0.35, r: Math.min(w, h) * 0.4, color: 'rgba(20, 30, 70, 0.06)' },
                    { x: w * 0.5, y: h * 0.15, r: Math.min(w, h) * 0.55, color: 'rgba(40, 15, 60, 0.05)' },
                    { x: w * 0.15, y: h * 0.5, r: Math.min(w, h) * 0.35, color: 'rgba(15, 25, 55, 0.07)' },
                    { x: w * 0.8, y: h * 0.55, r: Math.min(w, h) * 0.3, color: 'rgba(25, 20, 50, 0.06)' },
                ];

                nebulae.forEach(n => {
                    const gradient = ctx.createRadialGradient(n.x, n.y, 0, n.x, n.y, n.r);
                    gradient.addColorStop(0, n.color);
                    gradient.addColorStop(0.5, n.color.replace(/[\d.]+\)$/, '0.03)'));
                    gradient.addColorStop(1, 'rgba(0,0,0,0)');
                    ctx.fillStyle = gradient;
                    ctx.fillRect(0, 0, w, h);
                });

                // Crescent moon
                drawMoon(ctx, w * 0.78, h * 0.18, Math.min(w, h) * 0.06);

                // Subtle ground silhouette
                drawGroundSilhouette(ctx, w, h);

                // Static stars (drawn once on background)
                for (let i = 0; i < STAR_COUNT - TWINKLE_STAR_COUNT; i++) {
                    const sx = Math.random() * w;
                    const sy = Math.random() * h * 0.85;
                    const sr = 0.3 + Math.random() * 1.4;
                    const alpha = 0.2 + Math.random() * 0.7;
                    ctx.fillStyle = `rgba(255,255,255,${alpha})`;
                    ctx.beginPath();
                    ctx.arc(sx, sy, sr, 0, Math.PI * 2);
                    ctx.fill();

                    // Occasional cross-sparkle for brighter stars
                    if (sr > 1.0 && Math.random() < 0.3) {
                        const sparkleAlpha = alpha * 0.5;
                        ctx.strokeStyle = `rgba(255,255,255,${sparkleAlpha})`;
                        ctx.lineWidth = 0.3;
                        ctx.beginPath();
                        ctx.moveTo(sx - sr * 3, sy);
                        ctx.lineTo(sx + sr * 3, sy);
                        ctx.moveTo(sx, sy - sr * 3);
                        ctx.lineTo(sx, sy + sr * 3);
                        ctx.stroke();
                    }
                }
            }

            function drawMoon(ctx, mx, my, mr) {
                // Moon glow
                const glowGradient = ctx.createRadialGradient(mx, my, mr * 0.5, mx, my, mr * 2.5);
                glowGradient.addColorStop(0, 'rgba(255,250,235,0.25)');
                glowGradient.addColorStop(0.4, 'rgba(255,250,235,0.1)');
                glowGradient.addColorStop(1, 'rgba(255,250,235,0)');
                ctx.fillStyle = glowGradient;
                ctx.beginPath();
                ctx.arc(mx, my, mr * 2.5, 0, Math.PI * 2);
                ctx.fill();

                // Crescent moon using two arcs with opposite winding
                ctx.fillStyle = '#fdf5e6';
                ctx.beginPath();
                ctx.arc(mx, my, mr, 0, Math.PI * 2, false); // outer circle clockwise
                const cutX = mx + mr * 0.45;
                const cutY = my - mr * 0.25;
                const cutR = mr * 0.88;
                ctx.arc(cutX, cutY, cutR, 0, Math.PI * 2, true); // inner circle counter-clockwise (hole)
                ctx.closePath();
                ctx.fill();

                // Subtle inner glow on the crescent
                const innerGlow = ctx.createRadialGradient(mx - mr * 0.2, my - mr * 0.1, mr * 0.1, mx, my, mr);
                innerGlow.addColorStop(0, 'rgba(255,255,255,0.3)');
                innerGlow.addColorStop(1, 'rgba(255,255,255,0)');
                ctx.fillStyle = innerGlow;
                ctx.beginPath();
                ctx.arc(mx, my, mr, 0, Math.PI * 2, false);
                ctx.arc(cutX, cutY, cutR, 0, Math.PI * 2, true);
                ctx.closePath();
                ctx.fill();
            }

            function drawGroundSilhouette(ctx, w, h) {
                // Very subtle dark hills at the bottom
                ctx.fillStyle = 'rgba(3,3,15,0.85)';
                ctx.beginPath();
                ctx.moveTo(0, h);
                for (let x = 0; x <= w; x += w / 40) {
                    const hillHeight = h * 0.06 + Math.sin(x * 0.008) * h * 0.04 +
                        Math.sin(x * 0.02 + 1.5) * h * 0.03 +
                        Math.sin(x * 0.005 + 3) * h * 0.05;
                    ctx.lineTo(x, h - hillHeight);
                }
                ctx.lineTo(w, h);
                ctx.closePath();
                ctx.fill();

                // Slightly lighter distant hill layer
                ctx.fillStyle = 'rgba(5,5,20,0.7)';
                ctx.beginPath();
                ctx.moveTo(0, h);
                for (let x = 0; x <= w; x += w / 35) {
                    const hillHeight = h * 0.03 + Math.sin(x * 0.01 + 1) * h * 0.025 +
                        Math.sin(x * 0.025 + 2) * h * 0.02;
                    ctx.lineTo(x, h - hillHeight);
                }
                ctx.lineTo(w, h);
                ctx.closePath();
                ctx.fill();
            }

            // --- Create Stars ---
            function createStars() {
                stars = [];
                twinkleStars = [];

                // Twinkling stars (animated)
                for (let i = 0; i < TWINKLE_STAR_COUNT; i++) {
                    twinkleStars.push({
                        x: Math.random() * width,
                        y: Math.random() * height * 0.85,
                        radius: 0.5 + Math.random() * 2.0,
                        baseAlpha: 0.3 + Math.random() * 0.7,
                        twinkleSpeed: 0.5 + Math.random() * 3.0,
                        twinklePhase: Math.random() * Math.PI * 2,
                        hasCrossSparkle: Math.random() < 0.25,
                    });
                }

                // Some additional brighter static stars for the animated layer
                for (let i = 0; i < 30; i++) {
                    stars.push({
                        x: Math.random() * width,
                        y: Math.random() * height * 0.85,
                        radius: 0.3 + Math.random() * 1.0,
                        alpha: 0.3 + Math.random() * 0.5,
                    });
                }
            }

            // --- Shooting Star Class ---
            class ShootingStar {
                constructor() {
                    this.reset();
                }

                reset() {
                    // Choose starting position
                    const side = Math.random();
                    if (side < 0.55) {
                        // Start from top region
                        this.x = Math.random() * width;
                        this.y = Math.random() * height * 0.25;
                    } else if (side < 0.8) {
                        // Start from upper left
                        this.x = Math.random() * width * 0.25;
                        this.y = Math.random() * height * 0.4;
                    } else {
                        // Start from upper right
                        this.x = width - Math.random() * width * 0.25;
                        this.y = Math.random() * height * 0.4;
                    }

                    // Angle: mostly diagonal downward
                    const baseAngle = Math.PI * 0.35; // ~63° from horizontal
                    this.angle = baseAngle + (Math.random() - 0.5) * Math.PI * 0.35;
                    // Ensure it goes somewhat downward
                    if (this.angle < 0.15) this.angle = 0.15;
                    if (this.angle > Math.PI * 0.7) this.angle = Math.PI * 0.7;

                    this.speed = 3.5 + Math.random() * 9; // pixels per frame at 60fps-ish
                    this.trail = [];
                    this.maxTrailLength = 18 + Math.floor(Math.random() * 35);
                    this.opacity = 0.7 + Math.random() * 0.3;
                    this.width = 0.8 + Math.random() * 2.5;
                    this.active = true;
                    this.age = 0;
                    this.maxAge = 1.2 + Math.random() * 2.5; // seconds before fading
                    this.sparkRate = 0.3 + Math.random() * 0.7; // sparks per frame
                    this.sparkAccum = 0;

                    // Color preset
                    const colorPreset = shootingStarColors[
                        Math.floor(Math.random() * shootingStarColors.length)
                    ];
                    this.r = colorPreset.r;
                    this.g = colorPreset.g;
                    this.b = colorPreset.b;
                }

                update(dt) {
                    this.age += dt;
                    if (this.age > this.maxAge) {
                        // Fade out the trail into particles
                        this.releaseTrailParticles();
                        this.active = false;
                        return;
                    }

                    // Add current position to trail
                    this.trail.push({
                        x: this.x,
                        y: this.y,
                        age: 0,
                    });

                    // Move
                    const speedMultiplier = 1 + this.age * 0.3; // slight acceleration
                    this.x += Math.cos(this.angle) * this.speed * speedMultiplier * dt * 60;
                    this.y += Math.sin(this.angle) * this.speed * speedMultiplier * dt * 60;

                    // Update trail ages
                    for (const point of this.trail) {
                        point.age += dt;
                    }

                    // Trim old trail points
                    const maxTrailAge = 0.6 + (this.maxTrailLength / 30) * 0.8;
                    while (this.trail.length > 0 && this.trail[0].age > maxTrailAge) {
                        this.trail.shift();
                    }
                    // Also trim by length
                    while (this.trail.length > this.maxTrailLength) {
                        this.trail.shift();
                    }

                    // Emit spark particles
                    this.sparkAccum += this.sparkRate * dt * 60;
                    while (this.sparkAccum >= 1 && this.trail.length > 0) {
                        this.sparkAccum -= 1;
                        const refPoint = this.trail[this.trail.length - 1];
                        this.emitSpark(refPoint.x, refPoint.y);
                    }

                    // Check bounds
                    const margin = 60;
                    if (
                        this.x > width + margin ||
                        this.y > height + margin ||
                        this.x < -margin ||
                        this.y < -margin
                    ) {
                        this.releaseTrailParticles();
                        this.active = false;
                    }
                }

                emitSpark(x, y) {
                    const perpAngle = this.angle + Math.PI / 2;
                    const spread = (Math.random() - 0.5) * 1.5;
                    const sparkAngle = perpAngle + spread;
                    const sparkSpeed = 0.3 + Math.random() * 1.8;
                    trailParticles.push({
                        x: x + (Math.random() - 0.5) * 3,
                        y: y + (Math.random() - 0.5) * 3,
                        vx: Math.cos(sparkAngle) * sparkSpeed,
                        vy: Math.sin(sparkAngle) * sparkSpeed,
                        alpha: 0.5 + Math.random() * 0.5,
                        size: 0.3 + Math.random() * 1.2,
                        life: TRAIL_PARTICLE_LIFETIME,
                        maxLife: TRAIL_PARTICLE_LIFETIME,
                        r: this.r,
                        g: this.g,
                        b: this.b,
                    });
                }

                releaseTrailParticles() {
                    for (const point of this.trail) {
                        if (point.age < 0.5) {
                            trailParticles.push({
                                x: point.x + (Math.random() - 0.5) * 4,
                                y: point.y + (Math.random() - 0.5) * 4,
                                vx: (Math.random() - 0.5) * 0.6,
                                vy: (Math.random() - 0.5) * 0.6 + 0.2,
                                alpha: 0.3 + Math.random() * 0.5,
                                size: 0.2 + Math.random() * 1.0,
                                life: 0.4 + Math.random() * 1.0,
                                maxLife: 1.4,
                                r: this.r,
                                g: this.g,
                                b: this.b,
                            });
                        }
                    }
                }

                draw(ctx) {
                    if (this.trail.length < 2) {
                        // Just draw a dot if very short trail
                        if (this.trail.length === 1) {
                            const p = this.trail[0];
                            const gradient = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, this.width * 3);
                            gradient.addColorStop(0,
                                `rgba(${this.r},${this.g},${this.b},${this.opacity})`);
                            gradient.addColorStop(1, 'rgba(0,0,0,0)');
                            ctx.fillStyle = gradient;
                            ctx.beginPath();
                            ctx.arc(p.x, p.y, this.width * 3, 0, Math.PI * 2);
                            ctx.fill();
                        }
                        return;
                    }

                    // Draw trail as connected segments with varying opacity
                    ctx.lineCap = 'round';
                    ctx.lineJoin = 'round';

                    for (let i = 0; i < this.trail.length - 1; i++) {
                        const progress = i / (this.trail.length - 1);
                        const alpha = progress * progress * this.opacity;
                        const lineWidth = progress * this.width + 0.3;

                        ctx.strokeStyle =
                            `rgba(${this.r},${this.g},${this.b},${alpha})`;
                        ctx.lineWidth = lineWidth;
                        ctx.beginPath();
                        ctx.moveTo(this.trail[i].x, this.trail[i].y);
                        ctx.lineTo(this.trail[i + 1].x, this.trail[i + 1].y);
                        ctx.stroke();
                    }

                    // Draw bright head glow
                    const head = this.trail[this.trail.length - 1];
                    const glowRadius = this.width * 4 + 2;
                    const glowGradient = ctx.createRadialGradient(
                        head.x, head.y, 0,
                        head.x, head.y, glowRadius
                    );
                    glowGradient.addColorStop(0,
                        `rgba(${this.r},${this.g},${this.b},${this.opacity})`);
                    glowGradient.addColorStop(0.15,
                        `rgba(${this.r},${this.g},${this.b},${this.opacity * 0.7})`);
                    glowGradient.addColorStop(0.5,
                        `rgba(${this.r},${this.g},${this.b},${this.opacity * 0.2})`);
                    glowGradient.addColorStop(1, 'rgba(0,0,0,0)');
                    ctx.fillStyle = glowGradient;
                    ctx.beginPath();
                    ctx.arc(head.x, head.y, glowRadius, 0, Math.PI * 2);
                    ctx.fill();

                    // Tiny bright core
                    ctx.fillStyle =
                        `rgba(${this.r},${this.g},${this.b},${Math.min(1, this.opacity + 0.2)})`;
                    ctx.beginPath();
                    ctx.arc(head.x, head.y, this.width * 0.8, 0, Math.PI * 2);
                    ctx.fill();
                }
            }

            // --- Spawn Shooting Star ---
            function spawnShootingStar() {
                if (shootingStars.length < MAX_SHOOTING_STARS) {
                    shootingStars.push(new ShootingStar());
                }
                // Schedule next spawn
                const interval = SHOOTING_STAR_MIN_INTERVAL +
                    Math.random() * (SHOOTING_STAR_MAX_INTERVAL - SHOOTING_STAR_MIN_INTERVAL);
                spawnTimer = interval;

                // Occasionally spawn a cluster
                if (Math.random() < 0.2) {
                    const clusterDelay = 200 + Math.random() * 600;
                    setTimeout(() => {
                        if (shootingStars.length < MAX_SHOOTING_STARS) {
                            shootingStars.push(new ShootingStar());
                        }
                        // Maybe a third one
                        if (Math.random() < 0.4) {
                            setTimeout(() => {
                                if (shootingStars.length < MAX_SHOOTING_STARS) {
                                    shootingStars.push(new ShootingStar());
                                }
                            }, 150 + Math.random() * 400);
                        }
                    }, clusterDelay);
                }
            }

            // --- Update Trail Particles ---
            function updateTrailParticles(dt) {
                for (let i = trailParticles.length - 1; i >= 0; i--) {
                    const p = trailParticles[i];
                    p.life -= dt;
                    if (p.life <= 0) {
                        trailParticles.splice(i, 1);
                        continue;
                    }
                    p.x += p.vx * dt * 60;
                    p.y += p.vy * dt * 60;
                    p.vy += 0.015 * dt * 60; // slight gravity
                    p.alpha = (p.life / p.maxLife) * p.alpha;
                }
            }

            // --- Draw Trail Particles ---
            function drawTrailParticles(ctx) {
                for (const p of trailParticles) {
                    const alpha = p.alpha * (p.life / p.maxLife);
                    if (alpha < 0.01) continue;

                    const gradient = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.size * 2);
                    gradient.addColorStop(0, `rgba(${p.r},${p.g},${p.b},${alpha})`);
                    gradient.addColorStop(1, 'rgba(0,0,0,0)');
                    ctx.fillStyle = gradient;
                    ctx.beginPath();
                    ctx.arc(p.x, p.y, p.size * 2, 0, Math.PI * 2);
                    ctx.fill();
                }
            }

            // --- Draw Twinkling Stars ---
            function drawTwinklingStars(ctx, time) {
                for (const star of twinkleStars) {
                    const twinkle = 0.5 + 0.5 * Math.sin(time * star.twinkleSpeed + star.twinklePhase);
                    const alpha = star.baseAlpha * (0.35 + 0.65 * twinkle);
                    const radius = star.radius * (0.85 + 0.15 * twinkle);

                    // Glow
                    const glowGradient = ctx.createRadialGradient(star.x, star.y, 0, star.x, star.y, radius * 3);
                    glowGradient.addColorStop(0, `rgba(255,255,255,${alpha})`);
                    glowGradient.addColorStop(0.5, `rgba(255,255,255,${alpha * 0.2})`);
                    glowGradient.addColorStop(1, 'rgba(0,0,0,0)');
                    ctx.fillStyle = glowGradient;
                    ctx.beginPath();
                    ctx.arc(star.x, star.y, radius * 3, 0, Math.PI * 2);
                    ctx.fill();

                    // Core
                    ctx.fillStyle = `rgba(255,255,255,${alpha})`;
                    ctx.beginPath();
                    ctx.arc(star.x, star.y, radius, 0, Math.PI * 2);
                    ctx.fill();

                    // Cross sparkle for some stars
                    if (star.hasCrossSparkle && twinkle > 0.75) {
                        const sparkAlpha = alpha * (twinkle - 0.75) * 3;
                        ctx.strokeStyle = `rgba(255,255,255,${sparkAlpha})`;
                        ctx.lineWidth = 0.4;
                        const sparkLen = radius * 5;
                        ctx.beginPath();
                        ctx.moveTo(star.x - sparkLen, star.y);
                        ctx.lineTo(star.x + sparkLen, star.y);
                        ctx.moveTo(star.x, star.y - sparkLen);
                        ctx.lineTo(star.x, star.y + sparkLen);
                        ctx.stroke();
                    }
                }

                // Additional static bright stars
                for (const star of stars) {
                    ctx.fillStyle = `rgba(255,255,255,${star.alpha})`;
                    ctx.beginPath();
                    ctx.arc(star.x, star.y, star.radius, 0, Math.PI * 2);
                    ctx.fill();
                }
            }

            // --- Main Animation Loop ---
            function animate(timestamp) {
                let dt = (timestamp - lastTime) / 1000;
                lastTime = timestamp;

                // Clamp dt to avoid jumps
                if (dt > 0.2) dt = 0.2;
                if (dt <= 0) dt = 0.016;

                timeAccum += dt;

                // Update spawn timer
                spawnTimer -= dt * 1000;
                if (spawnTimer <= 0) {
                    spawnShootingStar();
                }

                // Update shooting stars
                for (let i = shootingStars.length - 1; i >= 0; i--) {
                    shootingStars[i].update(dt);
                    if (!shootingStars[i].active) {
                        shootingStars.splice(i, 1);
                    }
                }

                // Update trail particles
                updateTrailParticles(dt);

                // --- Draw Frame ---
                // Draw static background
                ctx.drawImage(backgroundCanvas, 0, 0);

                // Draw trail particles (behind shooting stars)
                drawTrailParticles(ctx);

                // Draw shooting stars
                for (const ss of shootingStars) {
                    ss.draw(ctx);
                }

                // Draw twinkling stars on top
                drawTwinklingStars(ctx, timeAccum);

                // Subtle vignette
                const vignetteGradient = ctx.createRadialGradient(
                    width / 2, height / 2, Math.min(width, height) * 0.55,
                    width / 2, height / 2, Math.max(width, height) * 0.75
                );
                vignetteGradient.addColorStop(0, 'rgba(0,0,0,0)');
                vignetteGradient.addColorStop(1, 'rgba(0,0,0,0.35)');
                ctx.fillStyle = vignetteGradient;
                ctx.fillRect(0, 0, width, height);

                requestAnimationFrame(animate);
            }

            // --- Initialize ---
            function init() {
                resize();
                // Initial spawn
                spawnTimer = 500 + Math.random() * 2000;
                // Spawn one immediately for instant gratification
                shootingStars.push(new ShootingStar());
                lastTime = performance.now();
                requestAnimationFrame(animate);
            }

            // --- Event Listeners ---
            window.addEventListener('resize', () => {
                resize();
                // Recreate twinkle stars on resize
                createStars();
            });

            // Click to spawn a shooting star
            window.addEventListener('click', (e) => {
                const star = new ShootingStar();
                // Bias toward the click location
                if (e.clientX && e.clientY) {
                    star.x = e.clientX + (Math.random() - 0.5) * 200;
                    star.y = Math.max(10, e.clientY - 100 + (Math.random() - 0.5) * 150);
                    star.angle = Math.PI * 0.3 + Math.random() * Math.PI * 0.4;
                }
                shootingStars.push(star);
                // Trim if too many
                while (shootingStars.length > MAX_SHOOTING_STARS + 2) {
                    shootingStars.shift();
                }
            });

            // Touch support
            window.addEventListener('touchstart', (e) => {
                const touch = e.touches[0];
                if (touch) {
                    const star = new ShootingStar();
                    star.x = touch.clientX + (Math.random() - 0.5) * 200;
                    star.y = Math.max(10, touch.clientY - 100 + (Math.random() - 0.5) * 150);
                    star.angle = Math.PI * 0.3 + Math.random() * Math.PI * 0.4;
                    shootingStars.push(star);
                    while (shootingStars.length > MAX_SHOOTING_STARS + 2) {
                        shootingStars.shift();
                    }
                }
            });

            // --- Start ---
            init();
            console.log('🌌 Night sky ready. Click anywhere to summon a shooting star!');
        })();
    </script>
</body>
</html>

Night Sky Features

Here’s how the page brings the night sky to life, focusing on the visual effects and how you can interact with them.

  • Dynamic Celestial Objects: The scene includes a crescent moon with a soft glow, hundreds of static stars, and a set of twinkling stars that pulse in brightness. A subtle, dark ground silhouette at the bottom adds depth to the landscape.

  • Shooting Star Mechanics: Shooting stars appear automatically at random intervals. Each one has a unique color tint (from pure white to icy blue or golden), a glowing head, and a fading trail. As they streak across the sky, they emit tiny spark particles that drift and fade, creating a realistic, magical effect.

  • Interactive Element: You can click or tap anywhere on the screen to instantly summon a new shooting star near that location. This adds a playful, responsive layer to the passive viewing experience.

  • Visual Atmosphere: The background features a deep, gradient night sky with subtle nebula-like glows. A soft vignette effect darkens the edges of the screen, focusing your attention on the center and enhancing the immersive, dreamy mood.

0 0

0 comments

Log in to join the conversation.