← Mike Higgins Home
Custom Code Experiment •

ThreeJS: Bumble Bee

Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ThreeJS: Bumble Bee</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { background: #000; overflow: hidden; height: 100vh; }
        #bgCanvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; }
        #animationCanvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 2; cursor: grab; }
        #animationCanvas.dragging { cursor: grabbing; }
        #hint { position: absolute; bottom: 18px; left: 50%; transform: translateX(-50%); z-index: 10;
            color: rgba(255, 200, 40, 0.5); font-family: monospace; font-size: 12px; letter-spacing: 0.12em;
            pointer-events: none; transition: opacity 2s; user-select: none; }
    </style>
</head>
<body>
    <canvas id="bgCanvas"></canvas>
    <canvas id="animationCanvas"></canvas>
    <script>
        const colors = {
            yellow:  0xffc020,
            black:   0x1a1200,
            thorax:  0xe8a000,
            wing:    0xffe090,
            vein:    0xfff4c0,
            eye:     0x203000,
            leg:     0x2a1a00,
            pollen:  0xffdd00,
            stinger: 0x3a2800,
            fuzz:    0xffd060,
            stripe:  0xfff0a0,
        };

        const bgCanvas = document.getElementById('bgCanvas');
        const bgCtx = bgCanvas.getContext('2d');
        const px = 7;
        let cols, rows, pixels = [];

        function initBackground() {
            bgCanvas.width = window.innerWidth;
            bgCanvas.height = window.innerHeight;
            cols = Math.ceil(bgCanvas.width / px);
            rows = Math.ceil(bgCanvas.height / px);
            pixels = [];
            for (let i = 0; i < cols * rows; i++) {
                pixels.push({ b: Math.random() * 0.3, speed: 0.001 + Math.random() * 0.003 });
            }
        }

        function animateBackground() {
            for (let y = 0; y < rows; y++) {
                for (let x = 0; x < cols; x++) {
                    const p = pixels[y * cols + x];
                    p.b += p.speed;
                    if (p.b > 0.4) p.b = 0;
                    bgCtx.fillStyle = `rgb(${Math.floor(p.b*50)},${Math.floor(p.b*32)},${Math.floor(p.b*5)})`;
                    bgCtx.fillRect(x * px, y * px, px, px);
                }
            }
            requestAnimationFrame(animateBackground);
        }

        const renderer = new THREE.WebGLRenderer({
            canvas: document.getElementById('animationCanvas'),
            antialias: true, alpha: true
        });

        let scene, camera, bee;
        let wingFL, wingFR, wingHL, wingHR;
        let legGroups = [];

        const orbit = {
            theta: 0.4, phi: 1.0, radius: 8,
            isDragging: false, lastX: 0, lastY: 0,
            autoRotate: true, autoRotateSpeed: 0.003,
            velTheta: 0, velPhi: 0,
        };
        const PHI_MIN = 0.05, PHI_MAX = Math.PI * 0.95;
        const RADIUS_MIN = 3, RADIUS_MAX = 20;

        function setCameraFromOrbit() {
            const x = orbit.radius * Math.sin(orbit.phi) * Math.sin(orbit.theta);
            const y = orbit.radius * Math.cos(orbit.phi);
            const z = orbit.radius * Math.sin(orbit.phi) * Math.cos(orbit.theta);
            camera.position.set(x, y, z);
            camera.lookAt(0, 0, 0);
            camera.updateProjectionMatrix();
        }

        function updateSize() {
            renderer.setSize(window.innerWidth, window.innerHeight);
            renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
        }

        function updateCamera() {
            const w = window.innerWidth;
            orbit.radius = w < 480 ? 12 : w < 768 ? 10 : 8;
            camera.fov = 52;
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            setCameraFromOrbit();
        }

        function mat(color) {
            return new THREE.MeshBasicMaterial({ color, wireframe: true });
        }

        function addFuzz(group, cx, cy, cz, radius, count, color) {
            for (let i = 0; i < count; i++) {
                const phi   = Math.acos(2 * Math.random() - 1);
                const theta = Math.random() * Math.PI * 2;
                const r     = radius + 0.015 + Math.random() * 0.04;
                const fGeo  = new THREE.SphereGeometry(0.018 + Math.random()*0.014, 4, 4);
                const fuzz  = new THREE.Mesh(fGeo, mat(color));
                fuzz.position.set(
                    cx + r * Math.sin(phi) * Math.cos(theta),
                    cy + r * Math.sin(phi) * Math.sin(theta),
                    cz + r * Math.cos(phi)
                );
                group.add(fuzz);
            }
        }

        function makeWing(side, isFore) {
            const group = new THREE.Group();
            const s = side;

            if (isFore) {
                const shape = new THREE.Shape();
                shape.moveTo(0, 0);
                shape.bezierCurveTo(s*0.4, 0.30, s*1.1, 0.55, s*1.7, 0.40);
                shape.bezierCurveTo(s*2.1, 0.28, s*2.2, 0.05, s*2.0, -0.18);
                shape.bezierCurveTo(s*1.7, -0.38, s*1.0, -0.45, s*0.4, -0.28);
                shape.bezierCurveTo(s*0.12, -0.16, 0, -0.06, 0, 0);
                const geo = new THREE.ShapeGeometry(shape, 14);
                group.add(new THREE.Mesh(geo, mat(colors.wing)));
                group.add(new THREE.LineSegments(new THREE.EdgesGeometry(geo),
                    new THREE.LineBasicMaterial({ color: colors.vein })));

                [
                    { pts: [[0,0],[s*0.7,0.36],[s*1.5,0.36],[s*2.0,0.16]], r: 0.012 },
                    { pts: [[s*0.05,-0.05],[s*0.6,0.22],[s*1.3,0.22],[s*1.85,0.04]], r: 0.009 },
                    { pts: [[s*0.08,-0.08],[s*0.5,0.06],[s*1.1,0.02],[s*1.7,-0.12]], r: 0.008 },
                    { pts: [[s*0.1,-0.12],[s*0.5,-0.10],[s*1.0,-0.18],[s*1.6,-0.30]], r: 0.007 },
                ].forEach(v => {
                    const pts = v.pts.map(p => new THREE.Vector3(p[0], p[1], 0.01));
                    const geo2 = new THREE.TubeGeometry(new THREE.CatmullRomCurve3(pts), 8, v.r, 4, false);
                    group.add(new THREE.Mesh(geo2, mat(colors.vein)));
                });
                [[s*0.55,0.22,s*0.60,-0.04],[s*1.0,0.20,s*1.05,-0.10],[s*1.4,0.14,s*1.45,-0.20]].forEach(cv => {
                    const pts = [new THREE.Vector3(cv[0],cv[1],0.01), new THREE.Vector3(cv[2],cv[3],0.01)];
                    group.add(new THREE.Mesh(
                        new THREE.TubeGeometry(new THREE.CatmullRomCurve3(pts), 3, 0.005, 4, false),
                        mat(colors.vein)
                    ));
                });

            } else {
                const shape2 = new THREE.Shape();
                shape2.moveTo(0, 0);
                shape2.bezierCurveTo(s*0.3, 0.18, s*0.9, 0.32, s*1.3, 0.20);
                shape2.bezierCurveTo(s*1.6, 0.10, s*1.6, -0.10, s*1.4, -0.22);
                shape2.bezierCurveTo(s*1.1, -0.34, s*0.5, -0.30, s*0.15, -0.14);
                shape2.bezierCurveTo(s*0.04, -0.06, 0, 0, 0, 0);
                const geo3 = new THREE.ShapeGeometry(shape2, 10);
                group.add(new THREE.Mesh(geo3, mat(colors.wing)));
                group.add(new THREE.LineSegments(new THREE.EdgesGeometry(geo3),
                    new THREE.LineBasicMaterial({ color: colors.vein })));

                [[s*0.05,-0.03,s*0.8,0.18,s*1.25,0.06],[s*0.08,-0.08,s*0.6,0.00,s*1.1,-0.14]].forEach(v => {
                    const pts = [
                        new THREE.Vector3(v[0],v[1],0.01),
                        new THREE.Vector3(v[2],v[3],0.01),
                        new THREE.Vector3(v[4],v[5],0.01)
                    ];
                    group.add(new THREE.Mesh(
                        new THREE.TubeGeometry(new THREE.CatmullRomCurve3(pts), 6, 0.007, 4, false),
                        mat(colors.vein)
                    ));
                });
            }
            return group;
        }

        function makeLeg(side, index) {
            const group = new THREE.Group();
            const s = side;
            const isHind = index === 2;

            const cPts = [new THREE.Vector3(0,0,0), new THREE.Vector3(s*0.22,-0.10,0.02)];
            group.add(new THREE.Mesh(
                new THREE.TubeGeometry(new THREE.CatmullRomCurve3(cPts), 4, 0.030, 6, false),
                mat(colors.black)
            ));

            const fPts = [
                new THREE.Vector3(s*0.22,-0.10,0.02),
                new THREE.Vector3(s*0.50,-0.38,0.02),
                new THREE.Vector3(s*0.70,-0.55,0.00)
            ];
            group.add(new THREE.Mesh(
                new THREE.TubeGeometry(new THREE.CatmullRomCurve3(fPts), 6, 0.028, 6, false),
                mat(isHind ? colors.yellow : colors.black)
            ));

            const tPts = [
                new THREE.Vector3(s*0.70,-0.55,0.00),
                new THREE.Vector3(s*0.95,-0.82,-0.02),
                new THREE.Vector3(s*1.10,-1.10,-0.04)
            ];
            group.add(new THREE.Mesh(
                new THREE.TubeGeometry(new THREE.CatmullRomCurve3(tPts), 6, isHind ? 0.032 : 0.022, 6, false),
                mat(isHind ? colors.yellow : colors.black)
            ));

            if (isHind) {
                const pbGeo = new THREE.SphereGeometry(0.10, 10, 8);
                const pb = new THREE.Mesh(pbGeo, mat(colors.pollen));
                pb.scale.set(0.6, 1.1, 0.55);
                pb.position.set(s*0.92, -0.80, -0.02);
                group.add(pb);
                for (let i = 0; i < 5; i++) {
                    const ang = (i / 5) * Math.PI * 0.8 - 0.2;
                    const sPts = [
                        new THREE.Vector3(s*0.92, -0.80, -0.02),
                        new THREE.Vector3(s*(0.92 + Math.cos(ang)*0.08), -0.80 + Math.sin(ang)*0.08, -0.10)
                    ];
                    group.add(new THREE.Mesh(
                        new THREE.TubeGeometry(new THREE.CatmullRomCurve3(sPts), 3, 0.004, 3, false),
                        mat(colors.pollen)
                    ));
                }
            }

            let tStart = new THREE.Vector3(s*1.10,-1.10,-0.04);
            for (let ti = 0; ti < 3; ti++) {
                const tEnd = new THREE.Vector3(
                    tStart.x + s*0.18,
                    tStart.y - 0.18 + ti*0.04,
                    tStart.z - 0.02
                );
                group.add(new THREE.Mesh(
                    new THREE.TubeGeometry(new THREE.CatmullRomCurve3([tStart, tEnd]), 4,
                        0.016 - ti*0.003, 5, false),
                    mat(ti % 2 === 0 ? colors.black : colors.yellow)
                ));
                tStart = tEnd;
            }
            const clGeo = new THREE.ConeGeometry(0.010, 0.06, 5);
            const cl = new THREE.Mesh(clGeo, mat(colors.fuzz));
            cl.rotation.z = s * -0.5;
            cl.position.copy(tStart).addScaledVector(new THREE.Vector3(s,0,-0.5).normalize(), 0.04);
            group.add(cl);

            return group;
        }

        function createBee() {
            const group = new THREE.Group();
            legGroups = [];

            const abdData = [
                { r: 0.30, col: colors.yellow, z: 0.12 },
                { r: 0.36, col: colors.black,  z: 0.42 },
                { r: 0.38, col: colors.yellow, z: 0.72 },
                { r: 0.33, col: colors.black,  z: 1.00 },
                { r: 0.22, col: colors.yellow, z: 1.26 },
                { r: 0.12, col: colors.black,  z: 1.48 },
            ];
            abdData.forEach((s, i) => {
                const next = abdData[i + 1];
                const rTop = next ? next.r : s.r * 0.5;
                const geo = new THREE.CylinderGeometry(rTop * 0.92, s.r, 0.32, 16, 1);
                const seg = new THREE.Mesh(geo, mat(s.col));
                seg.rotation.x = Math.PI * 0.5;
                seg.position.set(0, 0, s.z);
                group.add(seg);

                if (s.col === colors.yellow) {
                    addFuzz(group, 0, 0, s.z, s.r, 18, colors.fuzz);
                }

                const ringGeo = new THREE.TorusGeometry(s.r + 0.01, 0.010, 6, 18);
                const ring = new THREE.Mesh(ringGeo, mat(colors.stripe));
                ring.position.set(0, 0, s.z);
                group.add(ring);
            });

            const stingPts = [
                new THREE.Vector3(0, 0, 1.62),
                new THREE.Vector3(0, -0.04, 1.76),
                new THREE.Vector3(0, -0.06, 1.88),
            ];
            group.add(new THREE.Mesh(
                new THREE.TubeGeometry(new THREE.CatmullRomCurve3(stingPts), 6, 0.016, 5, false),
                mat(colors.stinger)
            ));
            const stingTip = new THREE.Mesh(new THREE.ConeGeometry(0.010, 0.05, 5), mat(colors.stinger));
            stingTip.rotation.x = 0.4;
            stingTip.position.set(0, -0.07, 1.92);
            group.add(stingTip);

            const txGeo = new THREE.SphereGeometry(0.42, 18, 16);
            const thorax = new THREE.Mesh(txGeo, mat(colors.thorax));
            thorax.scale.set(1.0, 1.05, 1.15);
            thorax.position.set(0, 0.06, -0.28);
            group.add(thorax);
            addFuzz(group, 0, 0.06, -0.28, 0.44, 60, colors.fuzz);

            const proGeo = new THREE.TorusGeometry(0.30, 0.040, 8, 20);
            const pro = new THREE.Mesh(proGeo, mat(colors.yellow));
            pro.rotation.x = Math.PI * 0.5;
            pro.position.set(0, 0.08, -0.48);
            group.add(pro);

            const headGeo = new THREE.SphereGeometry(0.26, 16, 14);
            const head = new THREE.Mesh(headGeo, mat(colors.black));
            head.scale.set(1.0, 0.95, 1.0);
            head.position.set(0, 0.00, -0.78);
            group.add(head);
            addFuzz(group, 0, 0.00, -0.78, 0.27, 22, colors.fuzz);

            [-1, 1].forEach(side => {
                const eyeGeo = new THREE.SphereGeometry(0.14, 14, 12);
                const eye = new THREE.Mesh(eyeGeo, mat(colors.eye));
                eye.scale.set(0.70, 0.90, 0.80);
                eye.position.set(side * 0.21, 0.04, -0.80);
                group.add(eye);
                for (let fi = 0; fi < 6; fi++) {
                    const fGeo = new THREE.CircleGeometry(0.035, 6);
                    const facet = new THREE.Mesh(fGeo, mat(colors.fuzz));
                    const ang = (fi / 6) * Math.PI * 1.6 - 0.4;
                    facet.position.set(
                        side * (0.24 + Math.cos(ang)*0.04),
                        0.04 + Math.sin(ang)*0.06,
                        -0.78
                    );
                    group.add(facet);
                }
            });

            [-1, 1].forEach(side => {
                const scPts = [
                    new THREE.Vector3(side*0.08, 0.14, -0.90),
                    new THREE.Vector3(side*0.14, 0.28, -0.96),
                    new THREE.Vector3(side*0.18, 0.40, -0.98),
                ];
                group.add(new THREE.Mesh(
                    new THREE.TubeGeometry(new THREE.CatmullRomCurve3(scPts), 6, 0.016, 5, false),
                    mat(colors.black)
                ));
                const pelGeo = new THREE.SphereGeometry(0.028, 7, 6);
                const pel = new THREE.Mesh(pelGeo, mat(colors.yellow));
                pel.position.set(side*0.18, 0.42, -0.98);
                group.add(pel);
                const flPts = [
                    new THREE.Vector3(side*0.18, 0.42, -0.98),
                    new THREE.Vector3(side*0.32, 0.46, -0.96),
                    new THREE.Vector3(side*0.44, 0.40, -0.90),
                    new THREE.Vector3(side*0.52, 0.32, -0.84),
                ];
                group.add(new THREE.Mesh(
                    new THREE.TubeGeometry(new THREE.CatmullRomCurve3(flPts), 8, 0.011, 5, false),
                    mat(colors.black)
                ));
                const knob = new THREE.Mesh(new THREE.SphereGeometry(0.022, 6, 6), mat(colors.yellow));
                knob.position.set(side*0.52, 0.32, -0.84);
                group.add(knob);
            });

            const probPts = [
                new THREE.Vector3(0, -0.14, -0.90),
                new THREE.Vector3(0, -0.22, -1.00),
                new THREE.Vector3(0, -0.26, -1.12),
            ];
            group.add(new THREE.Mesh(
                new THREE.TubeGeometry(new THREE.CatmullRomCurve3(probPts), 6, 0.018, 5, false),
                mat(colors.yellow)
            ));
            const tongueGeo = new THREE.SphereGeometry(0.025, 7, 6);
            const tongue = new THREE.Mesh(tongueGeo, mat(colors.fuzz));
            tongue.position.set(0, -0.28, -1.14);
            group.add(tongue);

            [-1, 1].forEach(side => {
                const mPts = [
                    new THREE.Vector3(side*0.08, -0.10, -0.92),
                    new THREE.Vector3(side*0.16, -0.18, -1.02),
                ];
                group.add(new THREE.Mesh(
                    new THREE.TubeGeometry(new THREE.CatmullRomCurve3(mPts), 3, 0.014, 4, false),
                    mat(colors.black)
                ));
            });

            wingFL = new THREE.Group();
            wingFL.position.set(0, 0.16, -0.30);
            wingFL.rotation.z =  0.08;
            wingFL.rotation.x = -0.12;
            wingFL.add(makeWing(-1, true));
            group.add(wingFL);

            wingFR = new THREE.Group();
            wingFR.position.set(0, 0.16, -0.30);
            wingFR.rotation.z = -0.08;
            wingFR.rotation.x = -0.12;
            wingFR.add(makeWing(1, true));
            group.add(wingFR);

            wingHL = new THREE.Group();
            wingHL.position.set(0, 0.10, -0.10);
            wingHL.rotation.z =  0.12;
            wingHL.rotation.x = -0.10;
            wingHL.add(makeWing(-1, false));
            group.add(wingHL);

            wingHR = new THREE.Group();
            wingHR.position.set(0, 0.10, -0.10);
            wingHR.rotation.z = -0.12;
            wingHR.rotation.x = -0.10;
            wingHR.add(makeWing(1, false));
            group.add(wingHR);

            [-1, 1].forEach(side => {
                for (let li = 0; li < 3; li++) {
                    const legGroup = makeLeg(side, li);
                    legGroup.position.set(side * 0.38, -0.08, -0.45 + li * 0.30);
                    group.add(legGroup);
                    legGroups.push({ g: legGroup, side, li });
                }
            });

            group.position.set(0, 0.2, 0.2);
            return group;
        }

        function initOrbitControls() {
            const canvas = document.getElementById('animationCanvas');
            const hint   = document.getElementById('hint');

            function onDragStart(x, y) {
                orbit.isDragging = true; orbit.autoRotate = false;
                orbit.lastX = x; orbit.lastY = y;
                orbit.velTheta = 0; orbit.velPhi = 0;
                canvas.classList.add('dragging');
                hint.style.opacity = '0';
            }
            function onDragMove(x, y) {
                if (!orbit.isDragging) return;
                const dx = x - orbit.lastX, dy = y - orbit.lastY;
                const speed = 0.005;
                orbit.velTheta = -dx * speed;
                orbit.velPhi   = -dy * speed;
                orbit.theta += orbit.velTheta;
                orbit.phi = Math.max(PHI_MIN, Math.min(PHI_MAX, orbit.phi + orbit.velPhi));
                orbit.lastX = x; orbit.lastY = y;
                setCameraFromOrbit();
            }
            function onDragEnd() {
                orbit.isDragging = false;
                canvas.classList.remove('dragging');
                setTimeout(() => { orbit.autoRotate = true; }, 2000);
            }

            canvas.addEventListener('mousedown', e => onDragStart(e.clientX, e.clientY));
            window.addEventListener('mousemove', e => onDragMove(e.clientX, e.clientY));
            window.addEventListener('mouseup',   () => onDragEnd());
            canvas.addEventListener('touchstart', e => { e.preventDefault(); onDragStart(e.touches[0].clientX, e.touches[0].clientY); }, { passive: false });
            window.addEventListener('touchmove',  e => { e.preventDefault(); onDragMove(e.touches[0].clientX, e.touches[0].clientY); }, { passive: false });
            window.addEventListener('touchend',   () => onDragEnd());
            canvas.addEventListener('wheel', e => {
                e.preventDefault();
                orbit.radius = Math.max(RADIUS_MIN, Math.min(RADIUS_MAX, orbit.radius + e.deltaY * 0.01));
                setCameraFromOrbit();
            }, { passive: false });

            setTimeout(() => { hint.style.opacity = '0'; }, 3000);
        }

        function init() {
            initBackground();
            animateBackground();
            updateSize();

            scene = new THREE.Scene();
            scene.background = new THREE.Color(0x000000);

            camera = new THREE.PerspectiveCamera(52, window.innerWidth / window.innerHeight, 0.1, 1000);
            updateCamera();

            bee = createBee();
            scene.add(bee);

            scene.add(new THREE.AmbientLight(0xffcc60, 0.55));
            const pl1 = new THREE.PointLight(0xffa020, 1.8, 100);
            pl1.position.set(4, 5, 3);
            scene.add(pl1);
            const pl2 = new THREE.PointLight(0xffd060, 0.9, 100);
            pl2.position.set(-4, -2, 2);
            scene.add(pl2);
            const pl3 = new THREE.PointLight(0xffffff, 0.4, 100);
            pl3.position.set(0, 3, -8);
            scene.add(pl3);

            animate();
            initOrbitControls();
        }

        function animate() {
            requestAnimationFrame(animate);
            const t = Date.now() * 0.001;

            if (!orbit.isDragging) {
                if (orbit.autoRotate) {
                    orbit.theta += orbit.autoRotateSpeed;
                } else {
                    orbit.velTheta *= 0.92; orbit.velPhi *= 0.92;
                    orbit.theta += orbit.velTheta;
                    orbit.phi = Math.max(PHI_MIN, Math.min(PHI_MAX, orbit.phi + orbit.velPhi));
                }
                setCameraFromOrbit();
            }

            if (bee) {
                bee.position.y = 0.2 + Math.sin(t * 2.2) * 0.08;
                bee.rotation.x = Math.sin(t * 1.6) * 0.04;
                bee.rotation.z = Math.sin(t * 2.0 + 0.8) * 0.03;

                const buzz = t * 14.0;
                if (wingFL) {
                    wingFL.rotation.z =  0.08 + Math.sin(buzz)          * 0.60;
                    wingFL.rotation.x = -0.12 + Math.sin(buzz * 0.97)   * 0.10;
                }
                if (wingFR) {
                    wingFR.rotation.z = -0.08 - Math.sin(buzz + 0.05)   * 0.60;
                    wingFR.rotation.x = -0.12 + Math.sin(buzz * 0.97)   * 0.10;
                }
                if (wingHL) {
                    wingHL.rotation.z =  0.12 + Math.sin(buzz + 0.3)    * 0.45;
                    wingHL.rotation.x = -0.10 + Math.sin(buzz * 0.95)   * 0.08;
                }
                if (wingHR) {
                    wingHR.rotation.z = -0.12 - Math.sin(buzz + 0.35)   * 0.45;
                    wingHR.rotation.x = -0.10 + Math.sin(buzz * 0.95)   * 0.08;
                }

                legGroups.forEach(({ g, side, li }) => {
                    const phase = li * 0.9 + (side > 0 ? 1.4 : 0);
                    g.rotation.z = side * (0.05 + Math.sin(t * 2.0 + phase) * 0.05);
                    g.rotation.x = Math.sin(t * 1.7 + phase) * 0.04;
                });
            }

            renderer.render(scene, camera);
        }

        window.addEventListener('resize', () => {
            updateSize();
            initBackground();
            if (camera) updateCamera();
        });

        window.addEventListener('load', init);
    </script>
</body>
</html>