← Mike Higgins Home
Custom Code Experiment •

ThreeJS: Fly

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>Insect</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; }
    </style>
</head>
<body>
    <canvas id="bgCanvas"></canvas>
    <canvas id="animationCanvas"></canvas>

    <script>
        const colors = {
            primary: 0x4db8d4,
            dark1:   0x0d2a3a,
            dark2:   0x0a1e2a,
            dark3:   0x000000,
            light:   0xaee8f5
        };

        const bgCanvas = document.getElementById("bgCanvas");
        const bgCtx = bgCanvas.getContext("2d");
        const pixelSize = 8;
        let cols, rows, pixels = [];

        function initBackground() {
            bgCanvas.width = window.innerWidth;
            bgCanvas.height = window.innerHeight;
            cols = Math.ceil(bgCanvas.width / pixelSize);
            rows = Math.ceil(bgCanvas.height / pixelSize);
            pixels = [];
            for (let i = 0; i < cols * rows; i++) {
                pixels.push({ brightness: 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 pixel = pixels[y * cols + x];
                    pixel.brightness += pixel.speed;
                    if (pixel.brightness > 0.4) { pixel.brightness = 0; }
                    bgCtx.fillStyle = `rgb(${Math.floor(pixel.brightness*10)},${Math.floor(pixel.brightness*30)},${Math.floor(pixel.brightness*50)})`;
                    bgCtx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);
                }
            }
            requestAnimationFrame(animateBackground);
        }

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

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

        function updateCamera() {
            const w = window.innerWidth;
            if (w < 480) { camera.position.set(0, 0.5, 7); camera.fov = 60; }
            else if (w < 768) { camera.position.set(0, 0.3, 6.5); camera.fov = 55; }
            else { camera.position.set(0, 0.3, 5.5); camera.fov = 50; }
            camera.updateProjectionMatrix();
        }

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

        function createInsect() {
            const group = new THREE.Group();

            const headGeo = new THREE.SphereGeometry(0.22, 24, 24);
            const headMesh = new THREE.Mesh(headGeo, mat(colors.dark1));
            headMesh.position.set(0, 0.08, 1.35);
            headMesh.scale.set(1, 0.95, 1);
            group.add(headMesh);

            const eyeGeo = new THREE.SphereGeometry(0.18, 20, 20, 0, Math.PI);
            [-1, 1].forEach(side => {
                const eye = new THREE.Mesh(eyeGeo, mat(colors.light));
                eye.position.set(side * 0.2, 0.1, 1.38);
                eye.rotation.y = side * Math.PI * 0.35;
                eye.scale.set(1, 0.9, 0.7);
                group.add(eye);
            });

            [-1, 1].forEach(side => {
                const pts = [];
                const segs = 14;
                for (let i = 0; i < segs; i++) {
                    const t = i / (segs - 1);
                    pts.push(new THREE.Vector3(
                        side * (0.12 + t * 0.35),
                        0.18 + t * 0.55 - t * t * 0.15,
                        1.35 + t * 0.18 - t * t * 0.3
                    ));
                }
                const curve = new THREE.CatmullRomCurve3(pts);
                const antGeo = new THREE.TubeGeometry(curve, 12, 0.013 - 0, 6, false);
                const antTaper = new THREE.TubeGeometry(curve, 12, 0.008, 6, false);
                group.add(new THREE.Mesh(antTaper, mat(colors.dark1)));
            });

            const thoraxGeo = new THREE.SphereGeometry(0.3, 28, 28);
            const thorax = new THREE.Mesh(thoraxGeo, mat(colors.primary));
            thorax.position.set(0, 0.12, 0.72);
            thorax.scale.set(0.95, 1.05, 1.5);
            group.add(thorax);

            const scutGeo = new THREE.SphereGeometry(0.14, 16, 16);
            const scut = new THREE.Mesh(scutGeo, mat(colors.primary));
            scut.position.set(0, 0.33, 0.55);
            scut.scale.set(0.9, 0.7, 1.1);
            group.add(scut);

            const protoGeo = new THREE.CylinderGeometry(0.12, 0.18, 0.18, 16);
            const proto = new THREE.Mesh(protoGeo, mat(colors.dark1));
            proto.position.set(0, 0.1, 1.05);
            proto.rotation.x = Math.PI * 0.5;
            group.add(proto);

            const abdSegs = 8;
            for (let i = 0; i < abdSegs; i++) {
                const t = i / (abdSegs - 1);
                const r = 0.17 * (1 - t * 0.72);
                const segGeo = new THREE.CylinderGeometry(r * 0.88, r, 0.32, 14, 2);
                const isAlt = i % 2 === 0;
                const seg = new THREE.Mesh(segGeo, mat(isAlt ? colors.primary : colors.dark1));
                const z = -0.12 - i * 0.34;
                const y = 0.08 - i * i * 0.012;
                seg.position.set(0, y, z);
                seg.rotation.x = Math.PI * 0.5 + i * 0.018;
                group.add(seg);

                if (i > 1 && i < abdSegs - 1) {
                    [-1, 1].forEach(side => {
                        const nubGeo = new THREE.SphereGeometry(r * 0.4, 8, 8);
                        const nub = new THREE.Mesh(nubGeo, mat(colors.light));
                        nub.position.set(side * r * 1.1, y, z);
                        group.add(nub);
                    });
                }
            }

            [-1, 1].forEach(side => {
                const cerciPts = [
                    new THREE.Vector3(side * 0.04, 0.08 - 7*7*0.012, -0.12 - 7*0.34),
                    new THREE.Vector3(side * 0.1, 0.02 - 7*7*0.012 - 0.04, -0.12 - 7*0.34 - 0.2),
                    new THREE.Vector3(side * 0.13, -0.1, -2.75)
                ];
                const cerciCurve = new THREE.CatmullRomCurve3(cerciPts);
                const cerciGeo = new THREE.TubeGeometry(cerciCurve, 8, 0.018, 5, false);
                group.add(new THREE.Mesh(cerciGeo, mat(colors.light)));
            });

            const legDefs = [
                { side: -1, z: 0.88, az: 0.55, sp: 0.9, drop: -0.18 },
                { side:  1, z: 0.88, az:-0.55, sp: 0.9, drop: -0.18 },
                { side: -1, z: 0.68, az: 0.75, sp: 1.05, drop: -0.22 },
                { side:  1, z: 0.68, az:-0.75, sp: 1.05, drop: -0.22 },
                { side: -1, z: 0.48, az: 0.95, sp: 1.12, drop: -0.26 },
                { side:  1, z: 0.48, az:-0.95, sp: 1.12, drop: -0.26 },
            ];

            legDefs.forEach(def => {
                const baseX = def.side * 0.28;
                const baseY = 0.0;
                const baseZ = def.z;

                const coxGeo = new THREE.CylinderGeometry(0.028, 0.032, 0.1, 8);
                const cox = new THREE.Mesh(coxGeo, mat(colors.dark1));
                cox.position.set(baseX, baseY, baseZ);
                cox.rotation.z = def.az;
                group.add(cox);

                const femX = baseX + def.side * 0.22;
                const femY = baseY + def.drop * 0.4;
                const femGeo = new THREE.CylinderGeometry(0.028, 0.022, 0.42, 8);
                const fem = new THREE.Mesh(femGeo, mat(colors.primary));
                fem.position.set(femX, femY, baseZ);
                fem.rotation.z = def.az * 0.55;
                fem.rotation.y = def.side * 0.25;
                group.add(fem);

                const tibX = femX + def.side * 0.26 * def.sp;
                const tibY = femY + def.drop * 0.7;
                const tibGeo = new THREE.CylinderGeometry(0.018, 0.013, 0.46, 7);
                const tib = new THREE.Mesh(tibGeo, mat(colors.light));
                tib.position.set(tibX, tibY, baseZ + 0.02);
                tib.rotation.z = def.az * 0.25 + def.side * 0.6;
                tib.rotation.y = def.side * 0.2;
                group.add(tib);

                for (let t = 0; t < 3; t++) {
                    const tarX = tibX + def.side * (0.08 + t * 0.06);
                    const tarY = tibY + def.drop * 0.55 - t * 0.06;
                    const tarGeo = new THREE.CylinderGeometry(0.011, 0.008, 0.1, 5);
                    const tar = new THREE.Mesh(tarGeo, mat(colors.dark1));
                    tar.position.set(tarX, tarY, baseZ + t * 0.02);
                    tar.rotation.z = def.az * 0.1 + def.side * 0.9;
                    group.add(tar);
                }
            });

            const wingDefs = [
                { side:  1, fore: true,  x:  0.3,  y: 0.38, z: 0.75, rx: -0.08, ry: -0.18, rz:  0.05 },
                { side:  1, fore: false, x:  0.32, y: 0.3,  z: 0.42, rx: -0.12, ry: -0.28, rz:  0.08 },
                { side: -1, fore: true,  x: -0.3,  y: 0.38, z: 0.75, rx: -0.08, ry:  0.18, rz: -0.05 },
                { side: -1, fore: false, x: -0.32, y: 0.3,  z: 0.42, rx: -0.12, ry:  0.28, rz: -0.08 },
            ];

            wingDefs.forEach(def => {
                const len = def.fore ? 1.85 : 1.55;
                const wid = def.fore ? 0.42 : 0.38;

                const wShape = new THREE.Shape();
                wShape.moveTo(0, 0);
                wShape.bezierCurveTo(len * 0.25, wid * 0.6, len * 0.55, wid, len * 0.75, wid * 0.85);
                wShape.bezierCurveTo(len * 0.9, wid * 0.65, len, wid * 0.2, len, 0);
                wShape.bezierCurveTo(len * 0.9, -wid * 0.18, len * 0.5, -wid * 0.25, len * 0.2, -wid * 0.1);
                wShape.bezierCurveTo(len * 0.05, -wid * 0.05, 0, 0, 0, 0);

                const wGeo = new THREE.ShapeGeometry(wShape, 8);
                const wing = new THREE.Mesh(wGeo, new THREE.MeshBasicMaterial({ color: colors.light, wireframe: true, side: THREE.DoubleSide, transparent: true, opacity: 0.85 }));
                wing.position.set(def.x, def.y, def.z);
                wing.rotation.set(def.rx, def.ry, def.rz);
                if (def.side === -1) wing.scale.x = -1;
                group.add(wing);

                const veinData = [
                    { p0: [0,0,0],            p1: [len*0.5, 0.02, 0],      p2: [len, 0, 0] },
                    { p0: [0,0,0],            p1: [len*0.4, wid*0.4, 0],   p2: [len*0.8, wid*0.6, 0] },
                    { p0: [len*0.1,wid*0.08,0], p1: [len*0.5, wid*0.55, 0], p2: [len*0.72, wid*0.78, 0] },
                    { p0: [len*0.2, 0.01, 0], p1: [len*0.22, wid*0.25, 0], p2: [len*0.25, wid*0.48, 0] },
                    { p0: [len*0.42, 0.01, 0],p1: [len*0.44, wid*0.3, 0],  p2: [len*0.47, wid*0.58, 0] },
                    { p0: [len*0.62, 0.01, 0],p1: [len*0.63, wid*0.25, 0], p2: [len*0.65, wid*0.45, 0] },
                    { p0: [len*0.78,wid*0.05,0],p1:[len*0.88,wid*0.08,0],  p2: [len*0.96, wid*0.05, 0] },
                ];

                veinData.forEach(v => {
                    const vc = new THREE.QuadraticBezierCurve3(
                        new THREE.Vector3(...v.p0),
                        new THREE.Vector3(...v.p1),
                        new THREE.Vector3(...v.p2)
                    );
                    const vGeo = new THREE.TubeGeometry(vc, 8, 0.006, 4, false);
                    const vMesh = new THREE.Mesh(vGeo, mat(colors.primary));
                    vMesh.position.set(def.x, def.y, def.z);
                    vMesh.rotation.set(def.rx, def.ry, def.rz);
                    if (def.side === -1) vMesh.scale.x = -1;
                    group.add(vMesh);
                });
            });

            for (let i = 0; i < 5; i++) {
                [-1, 1].forEach(side => {
                    const spGeo = new THREE.TorusGeometry(0.025, 0.008, 6, 12);
                    const sp = new THREE.Mesh(spGeo, mat(colors.light));
                    sp.position.set(side * 0.19, 0.05 - i * i * 0.01, 0.35 - i * 0.34);
                    sp.rotation.y = side * Math.PI * 0.4;
                    group.add(sp);
                });
            }

            return group;
        }

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

            scene = new THREE.Scene();
            scene.background = new THREE.Color(colors.dark3);

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

            insect = createInsect();
            scene.add(insect);

            const amb = new THREE.AmbientLight(colors.light, 0.5);
            scene.add(amb);
            const pl1 = new THREE.PointLight(colors.primary, 1.5, 100);
            pl1.position.set(5, 5, 5); scene.add(pl1);
            const pl2 = new THREE.PointLight(colors.light, 0.8, 100);
            pl2.position.set(-5, -3, 3); scene.add(pl2);
            const pl3 = new THREE.PointLight(colors.primary, 0.6, 100);
            pl3.position.set(0, 2, -8); scene.add(pl3);

            animate();
        }

        function animate() {
            requestAnimationFrame(animate);
            const t = Date.now();
            if (insect) {
                insect.rotation.y += 0.004;
                insect.position.y = Math.sin(t * 0.0015) * 0.09;
                insect.rotation.x = Math.sin(t * 0.001) * 0.05;
                insect.rotation.z = Math.sin(t * 0.0018) * 0.018;
            }
            renderer.render(scene, camera);
        }

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

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