← Mike Higgins Home
Custom Code Experiment •

ThreeJS: Mosquito

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: Mosquito</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(180,140,255,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 = {
            body:   0x8060aa,
            thorax: 0xa080cc,
            wing:   0xc8b0ff,
            vein:   0xf0e8ff,
            leg:    0x604888,
            eye:    0xff3060,
            probos: 0xd0a8ff,
            spot:   0x40ff90,
            dark:   0x1a0a2e,
        };

        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*20)},${Math.floor(p.b*10)},${Math.floor(p.b*45)})`;
                    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, mosquito;
        let wingL, wingR;
        let legGroups = [];

        const orbit = {
            theta: 0, phi: 1.1, 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 lineMat(color) {
            return new THREE.LineBasicMaterial({ color });
        }

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

            const shape = new THREE.Shape();
            shape.moveTo(0, 0);
            shape.bezierCurveTo(s*0.3, 0.18, s*1.0, 0.32, s*1.8, 0.18);
            shape.bezierCurveTo(s*2.3, 0.08, s*2.4, -0.04, s*2.2, -0.10);
            shape.bezierCurveTo(s*1.8, -0.22, s*0.9, -0.28, s*0.2, -0.12);
            shape.bezierCurveTo(s*0.05, -0.06, 0, 0, 0, 0);

            const geo = new THREE.ShapeGeometry(shape, 14);
            group.add(new THREE.Mesh(geo, mat(colors.wing)));

            const edgeGeo = new THREE.EdgesGeometry(geo);
            group.add(new THREE.LineSegments(edgeGeo, lineMat(colors.vein)));

            const veinDefs = [
                { pts: [[0,0,0.01],[s*0.8,0.18,0.01],[s*1.8,0.16,0.01],[s*2.2,0.05,0.01]], r: 0.009 },
                { pts: [[0,0,0.01],[s*0.7,0.10,0.01],[s*1.6,0.08,0.01],[s*2.1,-0.02,0.01]], r: 0.007 },
                { pts: [[s*0.1,-0.02,0.01],[s*0.8,0.02,0.01],[s*1.7,-0.01,0.01],[s*2.1,-0.08,0.01]], r: 0.006 },
                { pts: [[s*0.2,-0.06,0.01],[s*0.7,-0.06,0.01],[s*1.4,-0.09,0.01],[s*2.0,-0.14,0.01]], r: 0.005 },
            ];
            veinDefs.forEach(v => {
                const pts = v.pts.map(p => new THREE.Vector3(p[0], p[1], p[2]));
                const curve = new THREE.CatmullRomCurve3(pts);
                const vGeo = new THREE.TubeGeometry(curve, 8, v.r, 4, false);
                group.add(new THREE.Mesh(vGeo, mat(colors.vein)));
            });

            for (let i = 0; i < 3; i++) {
                const cx = s * (0.6 + i * 0.5);
                const cvPts = [
                    new THREE.Vector3(cx, 0.12 - i*0.03, 0.01),
                    new THREE.Vector3(cx + s*0.05, -0.04 - i*0.02, 0.01),
                ];
                const cvCurve = new THREE.CatmullRomCurve3(cvPts);
                const cvGeo = new THREE.TubeGeometry(cvCurve, 4, 0.004, 4, false);
                group.add(new THREE.Mesh(cvGeo, mat(colors.vein)));
            }

            return group;
        }

        function makeAntenna(side) {
            const group = new THREE.Group();
            const s = side;
            const segments = 14;

            for (let i = 0; i < segments; i++) {
                const t = i / segments;
                const y = 0.15 + t * 1.1;
                const x = s * (0.05 + t * 0.08);

                if (i < segments - 1) {
                    const sPts = [
                        new THREE.Vector3(x, y, 0),
                        new THREE.Vector3(x + s*0.006, y + 0.09, 0),
                    ];
                    const sCurve = new THREE.CatmullRomCurve3(sPts);
                    group.add(new THREE.Mesh(
                        new THREE.TubeGeometry(sCurve, 3, 0.008 - t*0.005, 4, false),
                        mat(colors.probos)
                    ));
                }

                if (i > 1 && i < segments - 2) {
                    const hairLen = 0.12 - t * 0.07;
                    for (let h = -1; h <= 1; h += 2) {
                        const hPts = [
                            new THREE.Vector3(x, y, 0),
                            new THREE.Vector3(x + s*hairLen*0.4, y + hairLen*0.3, h*hairLen*0.6),
                            new THREE.Vector3(x + s*hairLen*0.7, y + hairLen*0.1, h*hairLen),
                        ];
                        const hCurve = new THREE.CatmullRomCurve3(hPts);
                        group.add(new THREE.Mesh(
                            new THREE.TubeGeometry(hCurve, 3, 0.003, 3, false),
                            mat(colors.vein)
                        ));
                    }
                }
            }
            return group;
        }

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

            const zOff = (index - 1) * 0.28;

            const coxPts = [
                new THREE.Vector3(0, 0, 0),
                new THREE.Vector3(s*0.18, -0.08, 0.04),
            ];
            group.add(new THREE.Mesh(
                new THREE.TubeGeometry(new THREE.CatmullRomCurve3(coxPts), 4, 0.025, 5, false),
                mat(colors.leg)
            ));

            const femStart = new THREE.Vector3(s*0.18, -0.08, 0.04);
            const femEnd   = new THREE.Vector3(s*0.65, -0.55, 0.06);
            const femPts = [femStart, new THREE.Vector3(s*0.40, -0.30, 0.05), femEnd];
            group.add(new THREE.Mesh(
                new THREE.TubeGeometry(new THREE.CatmullRomCurve3(femPts), 6, 0.018, 5, false),
                mat(colors.body)
            ));

            const tibEnd = new THREE.Vector3(s*1.05, -1.05, -0.05);
            const tibPts = [femEnd, new THREE.Vector3(s*0.85, -0.80, 0.0), tibEnd];
            group.add(new THREE.Mesh(
                new THREE.TubeGeometry(new THREE.CatmullRomCurve3(tibPts), 6, 0.013, 5, false),
                mat(colors.leg)
            ));

            const tarsSegs = 3;
            let tStart = tibEnd.clone();
            for (let ti = 0; ti < tarsSegs; ti++) {
                const tEnd = new THREE.Vector3(
                    tStart.x + s*0.22,
                    tStart.y - 0.22 + ti*0.06,
                    tStart.z - 0.04
                );
                group.add(new THREE.Mesh(
                    new THREE.TubeGeometry(
                        new THREE.CatmullRomCurve3([tStart, tEnd]), 4,
                        0.009 - ti*0.002, 4, false
                    ),
                    mat(ti % 2 === 0 ? colors.dark : colors.vein)
                ));
                tStart = tEnd;
            }

            return group;
        }

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

            const abdSegs = 8;
            for (let i = 0; i < abdSegs; i++) {
                const t = i / (abdSegs - 1);
                const r = 0.13 - t * 0.07;
                const z = 0.1 + i * 0.30;
                const isAlt = i % 2 === 0;
                const geo = new THREE.CylinderGeometry(r * 0.85, r, 0.28, 12, 1);
                const seg = new THREE.Mesh(geo, mat(isAlt ? colors.body : colors.dark));
                seg.rotation.x = Math.PI * 0.5;
                seg.position.set(0, 0, z);
                group.add(seg);

                const ringGeo = new THREE.TorusGeometry(r, 0.008, 6, 14);
                const ring = new THREE.Mesh(ringGeo, mat(colors.vein));
                ring.position.set(0, 0, z + 0.13);
                group.add(ring);
            }

            const tailGeo = new THREE.ConeGeometry(0.04, 0.18, 8);
            const tail = new THREE.Mesh(tailGeo, mat(colors.dark));
            tail.rotation.x = Math.PI * 0.5;
            tail.position.set(0, 0, 0.1 + abdSegs * 0.30 + 0.09);
            group.add(tail);

            const thoraxGeo = new THREE.SphereGeometry(0.30, 16, 14);
            const thorax = new THREE.Mesh(thoraxGeo, mat(colors.thorax));
            thorax.scale.set(0.9, 1.15, 1.1);
            thorax.position.set(0, 0.12, -0.08);
            group.add(thorax);

            const scuGeo = new THREE.SphereGeometry(0.12, 10, 8);
            const scu = new THREE.Mesh(scuGeo, mat(colors.body));
            scu.scale.set(1, 0.6, 0.8);
            scu.position.set(0, 0.22, 0.18);
            group.add(scu);

            const headGeo = new THREE.SphereGeometry(0.18, 14, 12);
            const head = new THREE.Mesh(headGeo, mat(colors.thorax));
            head.scale.set(0.95, 0.9, 1.0);
            head.position.set(0, -0.04, -0.42);
            group.add(head);

            [-1, 1].forEach(side => {
                const eyeGeo = new THREE.SphereGeometry(0.11, 12, 10);
                const eye = new THREE.Mesh(eyeGeo, mat(colors.eye));
                eye.scale.set(0.75, 0.85, 0.9);
                eye.position.set(side * 0.15, 0.02, -0.44);
                group.add(eye);

                for (let fi = 0; fi < 4; fi++) {
                    const fGeo = new THREE.CircleGeometry(0.04, 6);
                    const facet = new THREE.Mesh(fGeo, mat(colors.vein));
                    const ang = (fi / 4) * Math.PI * 1.2 - 0.3;
                    facet.position.set(
                        side * (0.17 + Math.cos(ang)*0.05),
                        0.02 + Math.sin(ang)*0.05,
                        -0.42
                    );
                    group.add(facet);
                }
            });

            const probPts = [
                new THREE.Vector3(0, -0.08, -0.54),
                new THREE.Vector3(0, -0.20, -0.90),
                new THREE.Vector3(0, -0.28, -1.30),
                new THREE.Vector3(0, -0.32, -1.70),
            ];
            const probCurve = new THREE.CatmullRomCurve3(probPts);
            const probGeo = new THREE.TubeGeometry(probCurve, 12, 0.012, 6, false);
            group.add(new THREE.Mesh(probGeo, mat(colors.probos)));

            const tipGeo = new THREE.ConeGeometry(0.008, 0.06, 5);
            const tip = new THREE.Mesh(tipGeo, mat(colors.vein));
            tip.rotation.x = Math.PI * 0.5;
            tip.position.set(0, -0.34, -1.73);
            group.add(tip);

            [-1, 1].forEach(side => {
                const lpPts = [
                    new THREE.Vector3(side*0.03, -0.06, -0.52),
                    new THREE.Vector3(side*0.05, -0.14, -0.72),
                    new THREE.Vector3(side*0.04, -0.20, -0.90),
                ];
                group.add(new THREE.Mesh(
                    new THREE.TubeGeometry(new THREE.CatmullRomCurve3(lpPts), 6, 0.007, 4, false),
                    mat(colors.leg)
                ));
            });

            [-1, 1].forEach(side => {
                const antGroup = makeAntenna(side);
                antGroup.position.set(side * 0.06, 0.06, -0.52);
                group.add(antGroup);
            });

            wingL = new THREE.Group();
            wingL.position.set(0, 0.14, -0.05);
            const wL = makeWing(-1);
            wL.rotation.x = -0.18;
            wL.rotation.z = 0.10;
            wingL.add(wL);
            group.add(wingL);

            wingR = new THREE.Group();
            wingR.position.set(0, 0.14, -0.05);
            const wR = makeWing(1);
            wR.rotation.x = -0.18;
            wR.rotation.z = -0.10;
            wingR.add(wR);
            group.add(wingR);

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

            group.position.set(0, 0.3, 0.4);
            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;
                const 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();

            mosquito = createMosquito();
            scene.add(mosquito);

            scene.add(new THREE.AmbientLight(0xc0a0ff, 0.5));
            const pl1 = new THREE.PointLight(0xa070ff, 1.6, 100);
            pl1.position.set(4, 5, 3);
            scene.add(pl1);
            const pl2 = new THREE.PointLight(0xff3060, 0.7, 100);
            pl2.position.set(-4, -2, 2);
            scene.add(pl2);
            const pl3 = new THREE.PointLight(0x40d0ff, 0.5, 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 (mosquito) {
                mosquito.position.y = 0.3 + Math.sin(t * 1.8) * 0.06;
                mosquito.rotation.x = Math.sin(t * 2.3) * 0.03;
                mosquito.rotation.z = Math.sin(t * 1.5) * 0.02;

                if (wingL) {
                    wingL.rotation.z =  Math.sin(t * 18.0)               * 0.55;
                    wingL.rotation.x =  Math.sin(t * 17.0 + 0.4)         * 0.12;
                    wingL.rotation.y =  Math.sin(t * 19.0 + 0.8)         * 0.08;
                }
                if (wingR) {
                    wingR.rotation.z = -Math.sin(t * 18.5 + 0.2)         * 0.55;
                    wingR.rotation.x =  Math.sin(t * 17.5 + 1.1)         * 0.12;
                    wingR.rotation.y = -Math.sin(t * 19.5 + 0.5)         * 0.08;
                }

                legGroups.forEach(({ g, side, li }) => {
                    const phase = li * 0.7 + (side > 0 ? 1.2 : 0);
                    g.rotation.z = side * (Math.sin(t * 2.2 + phase) * 0.04);
                    g.rotation.x = Math.sin(t * 1.8 + phase * 0.5) * 0.03;
                });
            }

            renderer.render(scene, camera);
        }

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

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