← Mike Higgins Home
Custom Code Experiment •

ThreeJS: Scorpion 1

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 Scorpion 1</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: 0xe8a020,
            dark1:   0x3a1a00,
            dark2:   0x1e0d00,
            dark3:   0x000000,
            light:   0xffd080
        };

        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*40)},${Math.floor(pixel.brightness*20)},${Math.floor(pixel.brightness*5)})`;
                    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, scorpion;
        let tailSegments = [];
        let stingerGroup;
        let leftClaw, rightClaw;
        let leftClawMovable, rightClawMovable;
        let legGroups = [];

        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, 9); camera.fov = 60; }
            else if (w < 768) { camera.position.set(0, 0, 8); camera.fov = 55; }
            else { camera.position.set(0, 0, 7); camera.fov = 50; }
            camera.lookAt(0, 0, 0);
            camera.updateProjectionMatrix();
        }

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

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

            const headGeo = new THREE.SphereGeometry(0.28, 20, 20);
            const head = new THREE.Mesh(headGeo, mat(colors.dark1));
            head.position.set(0, 0.05, 1.5);
            head.scale.set(1.2, 0.7, 1.1);
            group.add(head);

            [[-0.08,0],[0.08,0],[-0.22,0],[0.22,0]].forEach(([ex]) => {
                const eGeo = new THREE.SphereGeometry(0.045, 8, 8);
                const eye = new THREE.Mesh(eGeo, mat(colors.light));
                eye.position.set(ex, 0.12, 1.6);
                group.add(eye);
            });

            [-1,1].forEach(side => {
                const pts = [
                    new THREE.Vector3(side*0.12, 0.0, 1.65),
                    new THREE.Vector3(side*0.18, -0.05, 1.85),
                    new THREE.Vector3(side*0.22, -0.08, 2.0)
                ];
                const curve = new THREE.CatmullRomCurve3(pts);
                const cGeo = new THREE.TubeGeometry(curve, 6, 0.025, 5, false);
                group.add(new THREE.Mesh(cGeo, mat(colors.dark1)));
                const tipGeo = new THREE.SphereGeometry(0.04, 6, 6);
                const tip = new THREE.Mesh(tipGeo, mat(colors.light));
                tip.position.set(side*0.22, -0.08, 2.0);
                group.add(tip);
            });

            [-1,1].forEach(side => {
                const armPts = [
                    new THREE.Vector3(side*0.3, 0.0, 1.4),
                    new THREE.Vector3(side*0.7, 0.05, 1.55),
                    new THREE.Vector3(side*1.0, 0.0, 1.7)
                ];
                const armCurve = new THREE.CatmullRomCurve3(armPts);
                const armGeo = new THREE.TubeGeometry(armCurve, 8, 0.045, 6, false);
                group.add(new THREE.Mesh(armGeo, mat(colors.primary)));

                const manusGeo = new THREE.SphereGeometry(0.1, 12, 12);
                const manus = new THREE.Mesh(manusGeo, mat(colors.primary));
                manus.position.set(side*1.1, 0.0, 1.82);
                manus.scale.set(1.0, 0.75, 1.2);
                group.add(manus);

                const fxPivot = new THREE.Group();
                fxPivot.position.set(side*1.1, 0.0, 1.82);
                const fxPts = [
                    new THREE.Vector3(0, 0, 0),
                    new THREE.Vector3(side*0.05, 0.07, 0.22),
                    new THREE.Vector3(side*0.0, 0.05, 0.40)
                ];
                const fxCurve = new THREE.CatmullRomCurve3(fxPts);
                const fxGeo = new THREE.TubeGeometry(fxCurve, 6, 0.022, 5, false);
                fxPivot.add(new THREE.Mesh(fxGeo, mat(colors.dark1)));
                group.add(fxPivot);

                const mvPivot = new THREE.Group();
                mvPivot.position.set(side*1.1, 0.0, 1.82);
                const mvPts = [
                    new THREE.Vector3(0, 0, 0),
                    new THREE.Vector3(side*0.05, -0.07, 0.22),
                    new THREE.Vector3(side*0.0, -0.05, 0.40)
                ];
                const mvCurve = new THREE.CatmullRomCurve3(mvPts);
                const mvGeo = new THREE.TubeGeometry(mvCurve, 6, 0.020, 5, false);
                mvPivot.add(new THREE.Mesh(mvGeo, mat(colors.light)));
                group.add(mvPivot);

                if (side === -1) { leftClaw = fxPivot; leftClawMovable = mvPivot; }
                else { rightClaw = fxPivot; rightClawMovable = mvPivot; }
            });

            const carapaceGeo = new THREE.SphereGeometry(0.52, 24, 24);
            const carapace = new THREE.Mesh(carapaceGeo, mat(colors.primary));
            carapace.position.set(0, 0.08, 0.7);
            carapace.scale.set(1.1, 0.6, 1.6);
            group.add(carapace);

            const mesoSegs = 6;
            for (let i = 0; i < mesoSegs; i++) {
                const t = i / (mesoSegs - 1);
                const w = 0.44 - t * 0.12;
                const segGeo = new THREE.CylinderGeometry(w * 0.88, w, 0.28, 14, 2);
                const isAlt = i % 2 === 0;
                const seg = new THREE.Mesh(segGeo, mat(isAlt ? colors.primary : colors.dark1));
                const z = -0.1 - i * 0.3;
                const y = 0.06 - i * 0.01;
                seg.position.set(0, y, z);
                seg.rotation.x = Math.PI * 0.5;
                group.add(seg);
                [-1,1].forEach(side => {
                    const nubGeo = new THREE.SphereGeometry(w * 0.28, 7, 7);
                    const nub = new THREE.Mesh(nubGeo, mat(colors.light));
                    nub.position.set(side * w * 1.05, y, z);
                    group.add(nub);
                });
            }

            const tailSegsCount = 5;
            const tailBaseZ = -0.1 - mesoSegs * 0.3;
            const tailBaseY = 0.06 - mesoSegs * 0.01;
            let prevPivot = group;
            let prevPos = new THREE.Vector3(0, tailBaseY, tailBaseZ);

            for (let i = 0; i < tailSegsCount; i++) {
                const r = 0.14 - i * 0.018;
                const pivot = new THREE.Group();
                pivot.position.copy(prevPos);
                const isAlt = i % 2 === 0;
                const segGeo = new THREE.CylinderGeometry(r * 0.9, r, 0.30, 12, 2);
                const seg = new THREE.Mesh(segGeo, mat(isAlt ? colors.dark1 : colors.primary));
                seg.position.set(0, 0, -0.15);
                seg.rotation.x = Math.PI * 0.5;
                pivot.add(seg);
                prevPivot.add(pivot);
                tailSegments.push(pivot);
                prevPivot = pivot;
                prevPos = new THREE.Vector3(0, 0, -0.30);
            }

            stingerGroup = new THREE.Group();
            stingerGroup.position.set(0, 0, -0.30);

            const bulbGeo = new THREE.SphereGeometry(0.14, 12, 12);
            const bulb = new THREE.Mesh(bulbGeo, mat(colors.primary));
            bulb.scale.set(0.85, 1.1, 1.0);
            stingerGroup.add(bulb);

            const stingerPts = [
                new THREE.Vector3(0, 0.0, 0),
                new THREE.Vector3(0, 0.22, 0.12),
                new THREE.Vector3(0, 0.38, 0.28)
            ];
            const stingerCurve = new THREE.CatmullRomCurve3(stingerPts);
            const stingerGeo = new THREE.TubeGeometry(stingerCurve, 8, 0.022, 5, false);
            stingerGroup.add(new THREE.Mesh(stingerGeo, mat(colors.light)));

            const tipGeo2 = new THREE.ConeGeometry(0.022, 0.12, 6);
            const stingerTip = new THREE.Mesh(tipGeo2, mat(colors.light));
            stingerTip.position.set(0, 0.44, 0.34);
            stingerTip.rotation.x = -0.7;
            stingerGroup.add(stingerTip);

            prevPivot.add(stingerGroup);

            const legPairs = [
                { z: 1.15, spread: 1.0 },
                { z: 0.82, spread: 1.05 },
                { z: 0.50, spread: 1.08 },
                { z: 0.18, spread: 1.12 },
            ];

            legPairs.forEach((pair, pi) => {
                [-1,1].forEach(side => {
                    const legGroup = new THREE.Group();
                    legGroup.position.set(side * 0.48, -0.02, pair.z);
                    group.add(legGroup);

                    const coxGeo = new THREE.CylinderGeometry(0.03, 0.035, 0.12, 7);
                    const cox = new THREE.Mesh(coxGeo, mat(colors.dark1));
                    cox.rotation.z = side * 1.1;
                    legGroup.add(cox);

                    const femGroup = new THREE.Group();
                    femGroup.position.set(side * 0.18, -0.06, 0);
                    const femGeo = new THREE.CylinderGeometry(0.025, 0.020, 0.45, 7);
                    const fem = new THREE.Mesh(femGeo, mat(colors.primary));
                    fem.rotation.z = side * 0.6;
                    femGroup.add(fem);
                    legGroup.add(femGroup);

                    const tibGroup = new THREE.Group();
                    tibGroup.position.set(side * 0.32, -0.18, 0);
                    const tibGeo = new THREE.CylinderGeometry(0.018, 0.013, 0.42, 6);
                    const tib = new THREE.Mesh(tibGeo, mat(colors.light));
                    tib.rotation.z = side * 0.9;
                    tibGroup.add(tib);
                    legGroup.add(tibGroup);

                    for (let t = 0; t < 2; t++) {
                        const tarGeo = new THREE.CylinderGeometry(0.011, 0.008, 0.10, 5);
                        const tar = new THREE.Mesh(tarGeo, mat(colors.dark1));
                        tar.position.set(side * (0.42 + t * 0.07), -0.26 - t * 0.06, 0);
                        tar.rotation.z = side * 0.92;
                        legGroup.add(tar);
                    }

                    const clawGeo = new THREE.ConeGeometry(0.012, 0.07, 5);
                    const claw = new THREE.Mesh(clawGeo, mat(colors.light));
                    claw.position.set(side * 0.57, -0.35, 0);
                    claw.rotation.z = side * -0.4;
                    legGroup.add(claw);

                    legGroups.push({ group: legGroup, side, pi });
                });
            });

            [-1,1].forEach(side => {
                for (let t = 0; t < 5; t++) {
                    const torusGeo = new THREE.TorusGeometry(0.018, 0.006, 5, 8);
                    const pec = new THREE.Mesh(torusGeo, mat(colors.light));
                    pec.position.set(side * (0.06 + t * 0.07), -0.12, 0.0);
                    pec.rotation.x = Math.PI * 0.5;
                    group.add(pec);
                }
            });

            group.position.set(0, 0.6, -0.5);
            group.rotation.x = 0.08;
            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();

            scorpion = createScorpion();
            scene.add(scorpion);

            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() * 0.001;

            if (scorpion) {
                scorpion.rotation.y += 0.004;
                scorpion.position.y = 0.6 + Math.sin(t * 1.5) * 0.07;
                scorpion.rotation.z = Math.sin(t * 1.8) * 0.015;

                tailSegments.forEach((seg, i) => {
                    const phase = i * 0.4;
                    const base = 0.28 + i * 0.18;
                    seg.rotation.x = base + Math.sin(t * 1.2 + phase) * 0.06;
                });

                if (stingerGroup) {
                    stingerGroup.rotation.x = Math.sin(t * 2.0) * 0.08;
                }

                const clawAngle = (Math.sin(t * 1.5) * 0.5 + 0.5) * 0.35;
                if (leftClawMovable) leftClawMovable.rotation.x = -clawAngle;
                if (rightClawMovable) rightClawMovable.rotation.x = -clawAngle;
                if (leftClaw) leftClaw.rotation.x = clawAngle * 0.5;
                if (rightClaw) rightClaw.rotation.x = clawAngle * 0.5;

                legGroups.forEach(({ group: lg, side, pi }) => {
                    const phase = pi * 0.8 + (side > 0 ? Math.PI : 0);
                    const swing = Math.sin(t * 2.2 + phase) * 0.12;
                    lg.rotation.x = swing;
                    lg.position.y = -0.02 + Math.abs(Math.sin(t * 2.2 + phase)) * 0.04;
                });
            }

            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>