← Mike Higgins Home
Custom Code Experiment •

ThreeJS: Butterfly 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, user-scalable=no">
    <title>ThreeJS: Butterfly 1</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body, html {
            width: 100%;
            height: 100%;
            overflow: hidden;
            background: #000000;
        }

        #bfly-wrap {
            position: relative;
            width: 100vw;
            height: 100vh;
            background: #000000;
            overflow: hidden;
        }

        #animC {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            display: block;
            z-index: 2;
        }
    </style>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
</head>
<body>
<div id="bfly-wrap">
    <canvas id="animC"></canvas>
</div>

<script>
    (function() {
        const colors = {
            wing1: 0x5bcaaa,
            wing2: 0x1d9e75,
            body: 0x9fe1cb,
            vein: 0xe1f5ee,
            dark: 0x04342c,
            spot: 0xfac775
        };

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

        let scene, camera, butterfly;
        let leftUpper, rightUpper;

        function updateSize() {
            const wrap = document.getElementById('bfly-wrap');
            const width = wrap.offsetWidth;
            const height = wrap.offsetHeight;
            renderer.setSize(width, height);
            renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
            if (camera) {
                camera.aspect = width / height;
                camera.updateProjectionMatrix();
            }
        }

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

        function makeWingShape(side, isUpper) {
            const group = new THREE.Group();
            const s = side;

            if (isUpper) {
                const shape = new THREE.Shape();
                shape.moveTo(0, 0);
                shape.bezierCurveTo(s * 0.8, 0.5, s * 1.6, 1.0, s * 2.0, 0.4);
                shape.bezierCurveTo(s * 2.2, 0.0, s * 1.9, -0.5, s * 1.0, -0.7);
                shape.bezierCurveTo(s * 0.5, -0.8, s * 0.1, -0.5, 0, 0);
                const geo = new THREE.ShapeGeometry(shape, 14);
                const mesh = new THREE.Mesh(geo, wireframeMaterial(colors.wing1));
                group.add(mesh);

                const edgeGeo = new THREE.EdgesGeometry(geo);
                const edgeMat = new THREE.LineBasicMaterial({ color: colors.vein });
                const wireEdges = new THREE.LineSegments(edgeGeo, edgeMat);
                group.add(wireEdges);

                for (let i = 0; i < 5; i++) {
                    const pt1 = new THREE.Vector3(0, 0, 0.01);
                    const pt2 = new THREE.Vector3(s * (0.5 + i * 0.25), (0.3 - i * 0.1), 0.01);
                    const pt3 = new THREE.Vector3(s * (0.8 + i * 0.3), (0.1 - i * 0.15), 0.01);
                    const curve = new THREE.CatmullRomCurve3([pt1, pt2, pt3]);
                    const tubeGeo = new THREE.TubeGeometry(curve, 5, 0.012, 4, false);
                    const tubeMat = wireframeMaterial(colors.vein);
                    group.add(new THREE.Mesh(tubeGeo, tubeMat));
                }

                for (let i = 0; i < 3; i++) {
                    const spotGeo = new THREE.CircleGeometry(0.08 + i * 0.03, 8);
                    const spotMat = wireframeMaterial(colors.spot);
                    const spot = new THREE.Mesh(spotGeo, spotMat);
                    spot.position.set(s * (0.9 + i * 0.5), 0.2 - i * 0.15, 0.02);
                    group.add(spot);
                }
            } else {
                const lowerShape = new THREE.Shape();
                lowerShape.moveTo(0, 0);
                lowerShape.bezierCurveTo(s * 0.5, -0.1, s * 1.3, 0.1, s * 1.6, -0.3);
                lowerShape.bezierCurveTo(s * 1.8, -0.6, s * 1.5, -1.1, s * 0.8, -1.2);
                lowerShape.bezierCurveTo(s * 0.3, -1.2, s * 0.1, -0.8, 0, 0);
                const geoLower = new THREE.ShapeGeometry(lowerShape, 12);
                const lowerMesh = new THREE.Mesh(geoLower, wireframeMaterial(colors.wing2));
                group.add(lowerMesh);

                const lowerEdges = new THREE.EdgesGeometry(geoLower);
                const edgeLowerMat = new THREE.LineBasicMaterial({ color: colors.vein });
                group.add(new THREE.LineSegments(lowerEdges, edgeLowerMat));

                for (let i = 0; i < 3; i++) {
                    const spotGeo = new THREE.CircleGeometry(0.07, 7);
                    const spotMat = wireframeMaterial(colors.spot);
                    const spot = new THREE.Mesh(spotGeo, spotMat);
                    spot.position.set(s * (0.5 + i * 0.4), -0.4 - i * 0.2, 0.02);
                    group.add(spot);
                }
            }

            return group;
        }

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

            const bodySegments = 7;
            for (let i = 0; i < bodySegments; i++) {
                const radius = 0.09 - i * 0.008;
                const yPos = 0.5 - i * 0.22;
                const useDark = (i % 2 === 0);
                const bodyMat = wireframeMaterial(useDark ? colors.dark : colors.body);
                const segmentGeo = new THREE.SphereGeometry(radius, 10, 10);
                const segment = new THREE.Mesh(segmentGeo, bodyMat);
                segment.position.set(0, yPos, 0);
                segment.scale.set(0.8, 1.0, 0.8);
                group.add(segment);
            }

            const headGeo = new THREE.SphereGeometry(0.13, 12, 12);
            const headMat = wireframeMaterial(colors.body);
            const head = new THREE.Mesh(headGeo, headMat);
            head.position.set(0, 0.72, 0);
            group.add(head);

            [-1, 1].forEach(side => {
                const eyeGeo = new THREE.SphereGeometry(0.04, 7, 7);
                const eyeMat = wireframeMaterial(colors.vein);
                const eye = new THREE.Mesh(eyeGeo, eyeMat);
                eye.position.set(side * 0.1, 0.76, 0.08);
                group.add(eye);

                const antPoints = [
                    new THREE.Vector3(side * 0.07, 0.82, 0),
                    new THREE.Vector3(side * 0.2, 1.1, 0.05),
                    new THREE.Vector3(side * 0.28, 1.35, 0.02)
                ];
                const antCurve = new THREE.CatmullRomCurve3(antPoints);
                const antTubeGeo = new THREE.TubeGeometry(antCurve, 8, 0.012, 4, false);
                const antMat = wireframeMaterial(colors.vein);
                group.add(new THREE.Mesh(antTubeGeo, antMat));

                const knobGeo = new THREE.SphereGeometry(0.035, 7, 7);
                const knobMat = wireframeMaterial(colors.spot);
                const knob = new THREE.Mesh(knobGeo, knobMat);
                knob.position.set(side * 0.28, 1.38, 0.02);
                group.add(knob);

                const legYPositions = [0.2, 0.0, -0.2];
                legYPositions.forEach(legY => {
                    const legPoints = [
                        new THREE.Vector3(side * 0.08, legY, 0),
                        new THREE.Vector3(side * 0.25, legY - 0.15, 0.05),
                        new THREE.Vector3(side * 0.35, legY - 0.35, 0.0)
                    ];
                    const legCurve = new THREE.CatmullRomCurve3(legPoints);
                    const legGeo = new THREE.TubeGeometry(legCurve, 5, 0.012, 4, false);
                    const legMat = wireframeMaterial(colors.dark);
                    group.add(new THREE.Mesh(legGeo, legMat));
                });
            });

            const leftWingPivot = new THREE.Group();
            const rightWingPivot = new THREE.Group();

            const leftUpperWing = makeWingShape(-1, true);
            const leftLowerWing = makeWingShape(-1, false);
            const rightUpperWing = makeWingShape(1, true);
            const rightLowerWing = makeWingShape(1, false);

            leftWingPivot.add(leftUpperWing);
            leftWingPivot.add(leftLowerWing);
            rightWingPivot.add(rightUpperWing);
            rightWingPivot.add(rightLowerWing);

            group.add(leftWingPivot);
            group.add(rightWingPivot);

            leftUpper = leftWingPivot;
            rightUpper = rightWingPivot;

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

        function init() {
            updateSize();

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

            camera = new THREE.PerspectiveCamera(50, 1, 0.1, 1000);
            const wrap = document.getElementById('bfly-wrap');
            camera.aspect = wrap.offsetWidth / wrap.offsetHeight;
            camera.position.set(0, 0.2, 6);
            camera.lookAt(0, 0.2, 0);
            camera.updateProjectionMatrix();

            butterfly = createButterfly();
            scene.add(butterfly);

            const ambientLight = new THREE.AmbientLight(0x404060, 0.5);
            scene.add(ambientLight);
            const pointLight1 = new THREE.PointLight(0x5bcaaa, 0.9);
            pointLight1.position.set(3, 4, 4);
            scene.add(pointLight1);
            const pointLight2 = new THREE.PointLight(0xfac775, 0.7);
            pointLight2.position.set(-3, 2, 5);
            scene.add(pointLight2);

            animate();
        }

        function animate() {
            requestAnimationFrame(animate);

            const time = Date.now() * 0.001;

            if (butterfly) {
                butterfly.position.y = Math.sin(time * 1.4) * 0.12;
                butterfly.rotation.z = Math.sin(time * 2.0) * 0.02;
                butterfly.rotation.y += 0.004;

                const flapAngle = Math.sin(time * 3.5) * 0.55;
                if (leftUpper) leftUpper.rotation.y = flapAngle;
                if (rightUpper) rightUpper.rotation.y = -flapAngle;
            }

            renderer.render(scene, camera);
        }

        window.addEventListener('resize', () => {
            updateSize();
            if (camera) {
                const wrap = document.getElementById('bfly-wrap');
                camera.aspect = wrap.offsetWidth / wrap.offsetHeight;
                camera.updateProjectionMatrix();
            }
        });
        init();
    })();
</script>
</body>
</html>