← Mike Higgins Home
Custom Code Experiment •

Particle Landscape

Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>3D Particle Landscape</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            overflow: hidden;
            background: #000;
        }
        canvas {
            display: block;
        }
        #info {
            position: absolute;
            top: 10px;
            left: 10px;
            color: white;
            font-family: monospace;
            font-size: 12px;
            z-index: 100;
            background: rgba(0, 0, 0, 0.5);
            padding: 10px;
            border-radius: 5px;
        }
        #controls {
            position: absolute;
            bottom: 10px;
            left: 10px;
            color: white;
            font-family: monospace;
            font-size: 12px;
            z-index: 100;
            background: rgba(0, 0, 0, 0.5);
            padding: 10px;
            border-radius: 5px;
        }
        button {
            background: rgba(255, 255, 255, 0.1);
            border: 1px solid rgba(255, 255, 255, 0.3);
            color: white;
            padding: 5px 10px;
            margin: 5px;
            border-radius: 3px;
            cursor: pointer;
        }
        button:hover {
            background: rgba(255, 255, 255, 0.2);
        }
    </style>
</head>
<body>
    <div id="info">Loading 3D Particle Landscape...</div>
    <div id="controls">
        <button id="reset">Reset</button>
    </div>
    <canvas id="canvas"></canvas>

    <script>
        // Simple 3D Particle Landscape
        const canvas = document.getElementById('canvas');
        const ctx = canvas.getContext('2d');
        const info = document.getElementById('info');

        let width = window.innerWidth;
        let height = window.innerHeight;
        canvas.width = width;
        canvas.height = height;

        // Mouse position
        let mouseX = width / 2;
        let mouseY = height / 2;
        let mouseDown = false;

        // Camera settings
        let camera = {
            x: 0,
            y: 0,
            z: 500,
            rotationX: 0,
            rotationY: 0,
            fov: 600
        };

        // 3D Particle class
        class Particle3D {
            constructor(x, y, z, type = 'ground') {
                this.original = { x, y, z };
                this.x = x;
                this.y = y;
                this.z = z;
                this.type = type;
                this.size = 2;
                this.speed = Math.random() * 0.02 + 0.01;
                this.waveOffset = Math.random() * Math.PI * 2;
                this.color = this.getColor();
            }

            getColor() {
                switch(this.type) {
                    case 'mountain':
                        return this.z > 100 ? '#ffffff' : // Snow
                               this.z > 50 ? '#888888' : // Rock
                               '#4a6fa5'; // Mountain
                    case 'tree':
                        return '#2d5a27'; // Dark green
                    case 'water':
                        return '#4287f5'; // Blue water
                    case 'star':
                        return '#ffffff'; // White stars
                    case 'foliage':
                        return '#3e8c3e'; // Green foliage
                    default: // ground
                        const hue = 120 + Math.random() * 40; // Green range
                        return `hsl(${hue}, 70%, ${30 + Math.random() * 20}%)`;
                }
            }

            update(time) {
                // Return to original position with wave motion
                const wave = Math.sin(time + this.waveOffset) * 5;

                this.x += (this.original.x - this.x) * this.speed;
                this.y += (this.original.y - this.y) * this.speed + wave;
                this.z += (this.original.z - this.z) * this.speed;

                // Special behaviors
                if (this.type === 'star') {
                    this.twinkle = Math.sin(time * 2 + this.waveOffset) * 0.5 + 0.5;
                    this.size = 1 + this.twinkle * 3;
                }

                if (this.type === 'tree') {
                    this.y += Math.sin(time * 3 + this.waveOffset) * 2;
                }

                if (this.type === 'water') {
                    this.y += Math.sin(time * 5 + this.waveOffset) * 1;
                }
            }

            project() {
                // Simple 3D projection
                const scale = camera.fov / (camera.fov + this.z + camera.z);
                const x2d = (this.x + camera.x) * scale + width / 2;
                const y2d = (this.y + camera.y) * scale + height / 2;

                return {
                    x: x2d,
                    y: y2d,
                    scale: scale,
                    visible: scale > 0
                };
            }
        }

        // Create landscape
        let particles = [];
        let time = 0;

        function createLandscape() {
            particles = [];

            // Create ground
            for (let x = -400; x <= 400; x += 15) {
                for (let z = -200; z <= 200; z += 15) {
                    const height = Math.sin(x * 0.01) * 20 + Math.cos(z * 0.015) * 15;
                    const y = -height - 50;

                    particles.push(new Particle3D(x, y, z, 'ground'));
                }
            }

            // Create mountains
            for (let i = 0; i < 3; i++) {
                const centerX = (Math.random() - 0.5) * 600;
                const centerZ = (Math.random() - 0.5) * 300;
                const radius = 80 + Math.random() * 60;
                const peakHeight = 80 + Math.random() * 70;

                for (let a = 0; a < Math.PI * 2; a += 0.2) {
                    for (let r = 0; r < radius; r += 10) {
                        const x = centerX + Math.cos(a) * r;
                        const z = centerZ + Math.sin(a) * r;
                        const height = peakHeight * (1 - r / radius) * (Math.sin(a * 3) * 0.3 + 0.7);

                        particles.push(new Particle3D(x, -height, z, 'mountain'));
                    }
                }
            }

            // Create water (lake/river)
            for (let x = -100; x <= 100; x += 10) {
                for (let z = -50; z <= 50; z += 10) {
                    const y = -30;
                    if (Math.sqrt(x*x + z*z) < 80) {
                        particles.push(new Particle3D(x, y, z, 'water'));
                    }
                }
            }

            // Create trees
            for (let i = 0; i < 20; i++) {
                const x = (Math.random() - 0.5) * 500;
                const z = (Math.random() - 0.5) * 250;
                const y = -60;

                // Tree trunk
                particles.push(new Particle3D(x, y, z, 'tree'));

                // Tree foliage
                for (let j = 0; j < 3; j++) {
                    particles.push(new Particle3D(
                        x + (Math.random() - 0.5) * 10,
                        y - 5 - j * 8,
                        z + (Math.random() - 0.5) * 10,
                        'foliage'
                    ));
                }
            }

            // Create stars
            for (let i = 0; i < 100; i++) {
                particles.push(new Particle3D(
                    (Math.random() - 0.5) * 2000,
                    (Math.random() - 0.5) * 1000 - 300,
                    (Math.random() - 0.5) * 1000,
                    'star'
                ));
            }

            updateInfo();
        }

        // 3D projection helper
        function projectPoint(x, y, z) {
            const scale = camera.fov / (camera.fov + z + camera.z);
            return {
                x: (x + camera.x) * scale + width / 2,
                y: (y + camera.y) * scale + height / 2,
                scale: scale
            };
        }

        // Draw functions
        function drawParticle(particle, proj) {
            if (!proj.visible) return;

            ctx.beginPath();
            ctx.fillStyle = particle.color;
            const size = particle.size * proj.scale;

            if (particle.type === 'star') {
                // Draw star as twinkling circle
                ctx.arc(proj.x, proj.y, size, 0, Math.PI * 2);
                ctx.fill();
            } else {
                // Draw landscape particles as squares (faster)
                ctx.fillRect(proj.x - size/2, proj.y - size/2, size, size);
            }
        }

        function drawConnection(p1, p2, proj1, proj2) {
            if (!proj1.visible || !proj2.visible) return;

            const distance = Math.sqrt(
                Math.pow(p2.x - p1.x, 2) +
                Math.pow(p2.y - p1.y, 2) +
                Math.pow(p2.z - p1.z, 2)
            );

            if (distance < 30) {
                ctx.strokeStyle = `rgba(100, 150, 255, ${0.1 * (1 - distance/30)})`;
                ctx.lineWidth = 1;
                ctx.beginPath();
                ctx.moveTo(proj1.x, proj1.y);
                ctx.lineTo(proj2.x, proj2.y);
                ctx.stroke();
            }
        }

        // Animation loop
        function animate() {
            // Clear with gradient sky
            const gradient = ctx.createLinearGradient(0, 0, 0, height);
            gradient.addColorStop(0, '#0a192f'); // Dark blue top
            gradient.addColorStop(0.5, '#1a3a5f'); // Medium blue
            gradient.addColorStop(0.7, '#2a5a8f'); // Light blue
            gradient.addColorStop(1, '#0a192f'); // Dark blue bottom

            ctx.fillStyle = gradient;
            ctx.fillRect(0, 0, width, height);

            time += 0.01;

            // Update camera with mouse interaction
            if (mouseDown) {
                camera.rotationY += (mouseX - width/2) * 0.0001;
                camera.rotationX += (mouseY - height/2) * 0.0001;
            }

            // Camera movement
            camera.x = Math.sin(time * 0.1) * 50;
            camera.z = 400 + Math.sin(time * 0.05) * 50;

            // Update and sort particles by depth
            particles.forEach(p => p.update(time));

            // Sort by Z for proper depth rendering
            particles.sort((a, b) => (b.z + camera.z) - (a.z + camera.z));

            // Draw particles
            particles.forEach(particle => {
                const proj = particle.project();
                drawParticle(particle, proj);
            });

            // Draw connections between nearby particles (creates mesh effect)
            ctx.globalAlpha = 0.1;
            for (let i = 0; i < particles.length; i += 5) {
                for (let j = i + 1; j < Math.min(i + 10, particles.length); j++) {
                    if (particles[i].type === particles[j].type) {
                        const proj1 = particles[i].project();
                        const proj2 = particles[j].project();
                        drawConnection(particles[i], particles[j], proj1, proj2);
                    }
                }
            }
            ctx.globalAlpha = 1;

            // Update info
            updateInfo();

            requestAnimationFrame(animate);
        }

        function updateInfo() {
            info.innerHTML = `
                Particles: ${particles.length}<br>
                Camera: ${camera.x.toFixed(1)}, ${camera.y.toFixed(1)}, ${camera.z.toFixed(1)}
            `;
        }

        // Event listeners
        document.addEventListener('mousemove', (e) => {
            mouseX = e.clientX;
            mouseY = e.clientY;
        });

        document.addEventListener('mousedown', () => {
            mouseDown = true;
        });

        document.addEventListener('mouseup', () => {
            mouseDown = false;
        });

        document.addEventListener('wheel', (e) => {
            camera.z += e.deltaY * 0.5;
            camera.z = Math.max(100, Math.min(1000, camera.z));
        });

        // Control buttons (only reset remains)
        document.getElementById('reset').addEventListener('click', () => {
            camera = {
                x: 0,
                y: 0,
                z: 500,
                rotationX: 0,
                rotationY: 0,
                fov: 600
            };
            createLandscape();
        });

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

        // Initialize and start
        createLandscape();
        animate();
    </script>
</body>
</html>