• 01 - Critter Models 02
  • 02 - VantaJS Shaders
  • 03 - App Bundle 02
  • 04 - Cathedrals
  • 05 - App Bundle 01
  • 06 - Retroscopic
  • 07 - Games
  • 08 - Voronoi
  • 09 - Terrain
  • 10 - Critter Models 01
  • 11 - Chromatophores
  • 12 - Conformal Maps
  • 13 - Echo Logix
  • 14 - Music
  • 15 - Colour Theory
MIKE'S NOTEBOOK
Notes
Number:
Project:
01Critter Models 02
└ Locust└ Butterfly└ Cockroach└ Spider└ Scorpion└ Dragon└ Brazilian Treehopper└ Tardigrade└ Diatoms└ Bumblebee└ Wasp└ Hammerhead Worm└ Giraffe Weevil└ Trilobite Beetle
02VantaJS Shaders
└ Clouds└ Waves└ Cells└ Rings└ Halo└ Globe└ Net└ Dots└ Birds└ Trunk└ Topology└ About VantaJS
03App Bundle 02
└ Gutindex└ Insect Builder└ OpenLayers
04Cathedrals
└ Lattice└ Lost└ Lucent└ Lumen└ Glass└ Disco└ Hyperion└ Latent
05App Bundle 01
└ Robohash└ PrismJS└ Horoscope└ Binary Jazz└ Morse Code└ URL Extractor└ Entity Escaper└ Phonetic Alphabet└ Toki Pona└ Semaphore└ Cut Up Machine└ Text Mixer└ Currency Freaks└ Host IO└ CrUX API└ Logo Dev└ Web Speech API└ GEO DB
06Retroscopic
└ Galaxy└ Ocean└ Slime└ 24-Cell└ Unicorn└ Polyp└ Oneiric 1└ Oneiric 2└ Node└ Torus Knot└ GLSL Grid└ Liquid Crystal└ Silky└ Butterfly└ Ocean 2└ Icosahedron└ Luma└ Swarm└ Broken Solid└ Hypersphere└ Constraints└ Psycho Killer
01 / 03

LOCUST - THREEJS PROCEDURAL ANIMATION SOURCE CODE

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Locust</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; }
  html, body { background: #000; overflow: hidden; height: 100%; }
  #bgCanvas { position: fixed; inset: 0; width: 100%; height: 100%; z-index: 1; }
  #animationCanvas { position: fixed; inset: 0; width: 100%; height: 100%; z-index: 2; cursor: grab; touch-action: none; }
  #animationCanvas:active { cursor: grabbing; }
</style>
</head>
<body>
<canvas id="bgCanvas"></canvas>
<canvas id="animationCanvas"></canvas>

<script>
const DARK = { eye: 0x0a0604, gloss: 0xffffff, spine: 0x140b09, claw: 0x0e0805 };

const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;

const bgCanvas = document.getElementById('bgCanvas');
const bgCtx = bgCanvas.getContext('2d');
function initBackground(){
  bgCanvas.width = innerWidth; bgCanvas.height = innerHeight;
  const g = bgCtx.createRadialGradient(
    bgCanvas.width*0.5, bgCanvas.height*0.55, 0,
    bgCanvas.width*0.5, bgCanvas.height*0.55, Math.max(bgCanvas.width, bgCanvas.height)*0.75
  );
  g.addColorStop(0, '#0d0716');
  g.addColorStop(1, '#000000');
  bgCtx.fillStyle = g;
  bgCtx.fillRect(0, 0, bgCanvas.width, bgCanvas.height);
}

const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('animationCanvas'), antialias:true, alpha:true });
let scene, camera, locust;
const walkLegs = [];
const hindLegs = [];
const antennaJoints = [];
const palps = [];
const tegmina = [];
const hindWings = [];

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

const cam = { dist: 9.5, yaw: 0.55, pitch: 0.26, targetYaw: 0.55, targetPitch: 0.26, targetDist: 9.5, autoYaw: 0 };
function updateCamera(){
  const w = innerWidth;
  if (w < 480) cam.targetDist = Math.min(Math.max(cam.targetDist, 10), 15);
  camera.fov = w < 480 ? 58 : w < 768 ? 52 : 46;
  camera.updateProjectionMatrix();
}
function applyCamera(){
  cam.yaw   += (cam.targetYaw   - cam.yaw)   * 0.08;
  cam.pitch += (cam.targetPitch - cam.pitch) * 0.08;
  cam.dist  += (cam.targetDist  - cam.dist)  * 0.08;
  const total = cam.yaw + cam.autoYaw;
  const p = Math.max(-1.35, Math.min(1.35, cam.pitch));
  camera.position.set(
    Math.sin(total)*Math.cos(p)*cam.dist,
    Math.sin(p)*cam.dist + 0.5,
    Math.cos(total)*Math.cos(p)*cam.dist
  );
  camera.lookAt(0, 0.05, -0.25);
}

function chitin(color, opts={}){
  return new THREE.MeshStandardMaterial(Object.assign({
    color, roughness:0.42, metalness:0.28, side:THREE.DoubleSide
  }, opts));
}

const iriMats = [];
let hueShift = 0;
function iri(offset, opts={}){
  const { sat=0.85, light=0.52, roughness=0.30, metalness=0.55, opacity=1, transparent=false } = opts;
  const m = new THREE.MeshStandardMaterial({
    color:0xffffff, roughness, metalness, side:THREE.DoubleSide, transparent, opacity
  });
  m.userData = { offset, sat, light };
  m.color.setHSL(((offset%1)+1)%1, sat, light);
  iriMats.push(m);
  return m;
}

function segmentBetween(p0, p1, r0, r1, material, radialSeg=10){
  const dir = new THREE.Vector3().subVectors(p1, p0);
  const len = dir.length();
  const geo = new THREE.CylinderGeometry(r1, r0, len, radialSeg, 1, false);
  const mesh = new THREE.Mesh(geo, material);
  mesh.position.copy(p0).add(p1).multiplyScalar(0.5);
  mesh.quaternion.setFromUnitVectors(new THREE.Vector3(0,1,0), dir.clone().normalize());
  return mesh;
}
const V = (x,y,z)=>new THREE.Vector3(x,y,z);

function addSpines(parent, a, b, count, rad, s, mat, dirY=-1){
  const along = new THREE.Vector3().subVectors(b, a).normalize();
  for (let i=1;i<=count;i++){
    const t = i/(count+1);
    const base = new THREE.Vector3().lerpVectors(a, b, t);
    const tip = base.clone()
      .add(V(0, dirY*rad, 0))
      .add(along.clone().multiplyScalar(-rad*0.5*s));
    parent.add(segmentBetween(base, tip, rad*0.45, 0.001, mat, 4));
  }
}

function makeWalkLeg(side, cfg){
  const s = side;
  const legMat  = iri(cfg.hue,      { roughness:0.34, metalness:0.5 });
  const legMat2 = iri(cfg.hue+0.05, { roughness:0.30, metalness:0.55, light:0.56 });
  const darkMat = iri(cfg.hue-0.05, { roughness:0.42, metalness:0.45, light:0.38 });
  const spineMat = chitin(DARK.spine, { roughness:0.6, metalness:0.1 });

  const femDroop = 0.72, kneeBend = 1.30;

  const root = new THREE.Group();
  root.position.set(s*cfg.hipX, cfg.hipY, cfg.hipZ);
  root.rotation.y = cfg.yaw;

  const coxaEnd = V(s*0.15, -0.05, 0);
  root.add(segmentBetween(V(0,0,0), coxaEnd, 0.070, 0.052, darkMat, 8));

  const femur = new THREE.Group();
  femur.position.copy(coxaEnd);
  femur.rotation.z = -s*femDroop;
  root.add(femur);
  const femEnd = V(s*cfg.fem, 0, 0);
  femur.add(segmentBetween(V(0,0,0), femEnd, 0.055, 0.036, legMat, 10));

  const knee = new THREE.Group();
  knee.position.copy(femEnd);
  knee.rotation.z = -s*kneeBend;
  femur.add(knee);
  const tibEnd = V(s*cfg.tib, 0, 0);
  knee.add(segmentBetween(V(0,0,0), tibEnd, 0.033, 0.020, legMat2, 9));
  addSpines(knee, V(0,0,0), tibEnd, 5, 0.05, s, spineMat);

  let prev = tibEnd.clone();
  for (let k=0;k<3;k++){
    const nt = prev.clone().add(V(s*0.055, -0.055 - k*0.008, 0));
    knee.add(segmentBetween(prev, nt, 0.026 - k*0.004, 0.022 - k*0.004, darkMat, 6));
    prev = nt;
  }
  [-1,1].forEach(cs=>{
    knee.add(segmentBetween(prev, prev.clone().add(V(s*0.035, -0.03, cs*0.03)), 0.013, 0.001, chitin(DARK.claw), 4));
  });

  walkLegs.push({
    root, femur, knee, side:s,
    baseYaw:cfg.yaw, baseFemur:-s*femDroop, baseKnee:-s*kneeBend,
    phase:cfg.phase, swing:cfg.swing
  });
  return root;
}

function makeHindLeg(side){
  const s = side;
  const femMat  = iri(0.62, { roughness:0.28, metalness:0.62, light:0.50 });
  const ridgeMat= iri(0.56, { roughness:0.38, metalness:0.5,  light:0.34 });
  const kneeMat = iri(0.50, { roughness:0.34, metalness:0.6,  light:0.40 });
  const tibMat  = iri(0.70, { roughness:0.30, metalness:0.55, light:0.56 });
  const darkMat = iri(0.66, { roughness:0.44, metalness:0.45, light:0.36 });
  const spineMat= chitin(DARK.spine, { roughness:0.6, metalness:0.1 });

  const Lf = 1.34, Lt = 1.46;

  const root = new THREE.Group();
  root.position.set(s*0.26, -0.02, 0.10);
  const baseYaw = s*1.30;
  root.rotation.y = baseYaw;

  const femur = new THREE.Group();
  const baseFem = s*0.62;
  femur.rotation.z = baseFem;
  root.add(femur);

  const bulge = new THREE.Mesh(new THREE.SphereGeometry(1, 22, 16), femMat);
  bulge.scale.set(Lf*0.46, 0.30, 0.21);
  bulge.position.set(s*Lf*0.40, 0.01, 0);
  femur.add(bulge);
  femur.add(segmentBetween(V(s*Lf*0.62,0.01,0), V(s*Lf,0,0), 0.16, 0.075, femMat, 12));
  for (let i=0;i<7;i++){
    const x = s*(0.16 + i*0.145);
    [-1,1].forEach(f=>{
      const z = f*(0.175 - i*0.012);
      const top = V(x, 0.17 - i*0.012, z);
      const bot = V(x - s*0.115, -0.17 + i*0.010, z*0.85);
      femur.add(segmentBetween(top, bot, 0.021, 0.021, ridgeMat, 4));
    });
  }
  const kneeKnob = new THREE.Mesh(new THREE.SphereGeometry(0.115, 14, 12), kneeMat);
  kneeKnob.position.set(s*Lf, 0, 0);
  femur.add(kneeKnob);

  const knee = new THREE.Group();
  knee.position.set(s*Lf, 0, 0);
  const baseKnee = -s*2.45;
  knee.rotation.z = baseKnee;
  femur.add(knee);

  const tibEnd = V(s*Lt, 0, 0);
  knee.add(segmentBetween(V(0,0,0), tibEnd, 0.072, 0.046, tibMat, 10));
  addSpines(knee, V(0.0,0,0), tibEnd, 9, 0.075, s, spineMat,  1);
  addSpines(knee, V(0.0,0,0), tibEnd, 9, 0.062, s, spineMat, -1);
  [-1,1].forEach(cs=>{
    knee.add(segmentBetween(tibEnd, tibEnd.clone().add(V(s*0.14, cs*0.09, cs*0.05)), 0.028, 0.001, spineMat, 5));
  });

  let prev = tibEnd.clone();
  for (let k=0;k<3;k++){
    const nt = prev.clone().add(V(s*0.10, -0.045, 0));
    knee.add(segmentBetween(prev, nt, 0.038 - k*0.006, 0.032 - k*0.006, darkMat, 6));
    prev = nt;
  }
  [-1,1].forEach(cs=>{
    knee.add(segmentBetween(prev, prev.clone().add(V(s*0.05, -0.04, cs*0.04)), 0.016, 0.001, chitin(DARK.claw), 4));
  });

  hindLegs.push({ root, femur, knee, side:s, baseYaw, baseFem, baseKnee });
  return root;
}

function makeAntenna(side){
  const root = new THREE.Group();
  root.position.set(side*0.11, 0.30, 1.62);
  root.rotation.set(-0.55, side*0.30, 0);
  const joints = [];
  const N = 12;
  let parent = root;
  for (let i=0;i<N;i++){
    const t = i/N;
    const antMat = iri(0.04 + t*0.5, { roughness:0.4, metalness:0.42, light: i%2 ? 0.58 : 0.46 });
    const j = new THREE.Group();
    j.userData.bx = -0.035;
    j.userData.by = side*0.015;
    j.rotation.set(j.userData.bx, j.userData.by, 0);
    const segLen = 0.10*(1 - t*0.30);
    const r0 = 0.048*(1-t*0.55);
    const r1 = 0.048*(1-(t+1/N)*0.55);
    j.add(segmentBetween(V(0,0,0), V(0,0,segLen), r0, r1, antMat, 7));
    parent.add(j);
    const holder = new THREE.Group();
    holder.position.set(0,0,segLen);
    j.add(holder);
    parent = holder;
    joints.push(j);
  }
  antennaJoints.push({ joints, side });
  return root;
}

function makeTegmen(side){
  const s = side;
  const len = 3.05, wid = 0.80;
  const shape = new THREE.Shape();
  shape.moveTo(0, 0.02);
  shape.bezierCurveTo(len*0.10, wid*0.70, len*0.30, wid*0.96, len*0.55, wid*0.95);
  shape.bezierCurveTo(len*0.78, wid*0.94, len*0.94, wid*0.68, len, wid*0.28);
  shape.bezierCurveTo(len*0.99, wid*0.10, len*0.90, wid*0.01, len*0.70, 0.0);
  shape.bezierCurveTo(len*0.45,-0.02, len*0.20,-0.01, 0, 0.02);
  const geo = new THREE.ExtrudeGeometry(shape, {
    depth:0.02, bevelEnabled:true, bevelThickness:0.015, bevelSize:0.015, bevelSegments:2, steps:1, curveSegments:26
  });
  const mat = iri(s>0 ? 0.14 : 0.19, { roughness:0.22, metalness:0.7, light:0.52, opacity:0.88, transparent:true });

  const flap = new THREE.Group();
  flap.position.set(s*0.10, 0.34, 0.80);
  const yawG = new THREE.Group();
  yawG.rotation.y = Math.PI/2 - s*0.10;
  flap.add(yawG);
  const rollG = new THREE.Group();
  rollG.rotation.x = -s*0.58;
  yawG.add(rollG);
  rollG.add(new THREE.Mesh(geo, mat));

  for (let vi=0; vi<7; vi++){
    const off = vi/6;
    const vm = iri(0.20 + off*0.30, { roughness:0.3, metalness:0.6, light:0.62 });
    const curve = new THREE.QuadraticBezierCurve3(
      V(len*0.03, 0.05 + off*wid*0.30, 0.032),
      V(len*0.45, 0.10 + off*wid*0.78, 0.032),
      V(len*0.96, 0.08 + off*wid*0.24, 0.032)
    );
    rollG.add(new THREE.Mesh(new THREE.TubeGeometry(curve, 12, 0.011, 4, false), vm));
  }
  const cvMat = iri(0.34, { roughness:0.35, metalness:0.5, light:0.55 });
  for (let i=0;i<11;i++){
    const x = len*(0.10 + i*0.075);
    const y0 = 0.10 + (i%2 ? 0.06 : 0.24);
    rollG.add(segmentBetween(V(x, y0, 0.030), V(x + len*0.015, y0 + wid*0.36, 0.030), 0.008, 0.008, cvMat, 3));
  }

  tegmina.push({ flap, side:s });
  return flap;
}

function makeHindWing(side){
  const s = side;
  const R = 2.5, a0 = -0.30, a1 = 0.42;
  const shape = new THREE.Shape();
  shape.moveTo(0,0);
  shape.absarc(0, 0, R, a0, a1, false);
  shape.lineTo(0,0);
  const geo = new THREE.ShapeGeometry(shape, 42);
  const mat = iri(s>0 ? 0.86 : 0.92, { roughness:0.16, metalness:0.4, light:0.62, opacity:0.42, transparent:true });

  const flap = new THREE.Group();
  flap.position.set(s*0.09, 0.24, 0.66);
  const openG = new THREE.Group();
  openG.scale.y = 0.04;
  flap.add(openG);
  const yawG = new THREE.Group();
  yawG.rotation.y = Math.PI/2 - s*0.14;
  openG.add(yawG);
  const rollG = new THREE.Group();
  rollG.rotation.x = s*1.30;
  yawG.add(rollG);
  rollG.add(new THREE.Mesh(geo, mat));

  for (let i=0;i<=9;i++){
    const ang = a0 + (a1-a0)*(i/9);
    const rm = iri(0.80 + i*0.02, { roughness:0.3, metalness:0.55, light:0.66 });
    rollG.add(segmentBetween(V(0,0,0.004), V(Math.cos(ang)*R, Math.sin(ang)*R, 0.004), 0.016, 0.004, rm, 4));
  }

  hindWings.push({ flap, openG, side:s });
  return flap;
}

function createLocust(){
  const g = new THREE.Group();

  const nSeg = 10;
  for (let i=0;i<nSeg;i++){
    const t = i/(nSeg-1);
    const w = 0.42 * (1 - t*0.55);
    const z = 0.28 - i*0.245;
    const y = -0.02 + t*t*0.30;
    const hue = 0.30 + t*0.30;
    const geo = new THREE.SphereGeometry(1, 20, 14);
    const seg = new THREE.Mesh(geo, iri(hue, { light: i%2 ? 0.56 : 0.47, metalness:0.55 }));
    seg.scale.set(w, w*0.92, 0.20);
    seg.position.set(0, y, z);
    g.add(seg);
    const belly = new THREE.Mesh(geo, iri(hue+0.06, { light:0.64, metalness:0.42 }));
    belly.scale.set(w*0.82, w*0.42, 0.17);
    belly.position.set(0, y - w*0.55, z);
    g.add(belly);
    if (i<nSeg-1) [-1,1].forEach(sx=>{
      const b = new THREE.Mesh(new THREE.SphereGeometry(0.035,8,8), iri(hue-0.06,{light:0.34}));
      b.position.set(sx*w*0.96, y - w*0.25, z);
      g.add(b);
    });
  }
  [-1,1].forEach(sx=>{
    const ty = new THREE.Mesh(new THREE.CircleGeometry(0.13, 16), iri(0.24,{roughness:0.15,metalness:0.7,light:0.42}));
    ty.position.set(sx*0.40, 0.0, 0.20);
    ty.rotation.y = sx*Math.PI/2;
    g.add(ty);
  });
  const tailZ = 0.28 - (nSeg-1)*0.245, tailY = 0.28;
  [-1,1].forEach(sx=>{
    g.add(segmentBetween(V(sx*0.08, tailY, tailZ), V(sx*0.13, tailY+0.03, tailZ-0.20), 0.045, 0.012, iri(0.60,{metalness:0.5}), 6));
    [-1,1].forEach(vy=>{
      g.add(segmentBetween(V(sx*0.05, tailY+vy*0.07, tailZ-0.05), V(sx*0.09, tailY+vy*0.16, tailZ-0.34), 0.05, 0.015, iri(0.64,{light:0.44}), 6));
    });
  });

  const thorax = new THREE.Mesh(new THREE.SphereGeometry(0.44, 24, 18), iri(0.24,{roughness:0.3,metalness:0.55}));
  thorax.scale.set(0.98, 1.02, 1.55);
  thorax.position.set(0, 0.02, 0.72);
  g.add(thorax);

  const saddle = new THREE.Group();
  const back = new THREE.Mesh(new THREE.SphereGeometry(0.50, 28, 20), iri(0.10,{roughness:0.24,metalness:0.62,light:0.54}));
  back.scale.set(0.92, 0.86, 1.30);
  saddle.add(back);
  const keel = new THREE.Mesh(new THREE.SphereGeometry(0.42, 20, 14), iri(0.04,{roughness:0.2,metalness:0.72,light:0.46}));
  keel.scale.set(0.16, 0.52, 1.32);
  keel.position.set(0, 0.24, 0);
  saddle.add(keel);
  [-1,1].forEach(sx=>{
    const lobe = new THREE.Mesh(new THREE.SphereGeometry(0.42, 20, 16), iri(0.14,{roughness:0.26,metalness:0.6,light:0.50}));
    lobe.scale.set(0.30, 0.88, 1.24);
    lobe.position.set(sx*0.36, -0.10, -0.02);
    saddle.add(lobe);
  });
  [-0.28, 0.02, 0.30].forEach((zz,i)=>{
    const sul = new THREE.Mesh(new THREE.TorusGeometry(0.40, 0.022, 6, 20, Math.PI), iri(0.0 + i*0.04,{roughness:0.4,light:0.32}));
    sul.rotation.y = Math.PI/2; sul.rotation.x = Math.PI;
    sul.position.set(0, 0.06, zz);
    sul.scale.set(1, 1.05, 1);
    saddle.add(sul);
  });
  saddle.position.set(0, 0.16, 0.92);
  saddle.rotation.x = -0.05;
  g.add(saddle);

  const head = new THREE.Group();
  const skull = new THREE.Mesh(new THREE.SphereGeometry(0.30, 24, 20), iri(0.0,{roughness:0.28,metalness:0.55}));
  skull.scale.set(0.90, 1.30, 0.92);
  head.add(skull);
  const fast = new THREE.Mesh(new THREE.ConeGeometry(0.13, 0.22, 4), iri(0.06,{roughness:0.3,light:0.5}));
  fast.position.set(0, 0.30, 0.16); fast.rotation.x = 1.35;
  head.add(fast);
  [-1,1].forEach(sx=>{
    const eye = new THREE.Mesh(new THREE.SphereGeometry(0.15, 20, 16), chitin(DARK.eye,{roughness:0.13,metalness:0.6}));
    eye.scale.set(0.62, 1.35, 0.85);
    eye.position.set(sx*0.24, 0.14, 0.02);
    head.add(eye);
    const gloss = new THREE.Mesh(new THREE.SphereGeometry(0.045,10,10), chitin(DARK.gloss,{roughness:0.1,metalness:0.3}));
    gloss.position.set(sx*0.26, 0.28, 0.11);
    head.add(gloss);
  });
  const face = new THREE.Mesh(new THREE.SphereGeometry(0.22,18,14), iri(0.90,{roughness:0.32,light:0.50}));
  face.scale.set(0.86, 1.05, 0.42);
  face.position.set(0, -0.02, 0.20);
  head.add(face);
  [-1,1].forEach(sx=>{
    const mand = new THREE.Mesh(new THREE.SphereGeometry(0.10,12,10), iri(0.80,{roughness:0.25,metalness:0.66,light:0.36}));
    mand.scale.set(0.8, 1.15, 0.7);
    mand.position.set(sx*0.11, -0.30, 0.15);
    head.add(mand);
  });
  [-1,1].forEach(sx=>{
    const palp = new THREE.Group();
    palp.position.set(sx*0.16, -0.30, 0.16);
    let prev = V(0,0,0);
    for (let k=0;k<3;k++){
      const nt = prev.clone().add(V(sx*0.04, -0.075, 0.05));
      palp.add(segmentBetween(prev, nt, 0.030-k*0.006, 0.024-k*0.006, iri(0.94+k*0.03,{roughness:0.4}), 5));
      prev = nt;
    }
    head.add(palp);
    palps.push({ palp, side:sx });
  });
  head.position.set(0, 0.10, 1.42);
  head.rotation.x = 0.14;
  g.add(head);

  g.add(makeAntenna(-1));
  g.add(makeAntenna( 1));

  g.add(makeHindWing( 1));
  g.add(makeHindWing(-1));
  g.add(makeTegmen( 1));
  g.add(makeTegmen(-1));

  const legCfg = [
    { hipX:0.24, hipY:-0.14, hipZ:1.00, yaw:-0.60, fem:0.50, tib:0.60, swing:0.22, phase:0,       hue:0.02 },
    { hipX:0.27, hipY:-0.16, hipZ:0.52, yaw:-0.05, fem:0.54, tib:0.66, swing:0.22, phase:Math.PI, hue:0.16 },
  ];
  legCfg.forEach(c=>{
    g.add(makeWalkLeg( 1, c));
    g.add(makeWalkLeg(-1, Object.assign({}, c, { yaw:-c.yaw, phase:c.phase + Math.PI })));
  });

  g.add(makeHindLeg( 1));
  g.add(makeHindLeg(-1));

  g.scale.setScalar(0.98);
  return g;
}

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

  scene = new THREE.Scene();
  scene.background = null;
  scene.fog = new THREE.FogExp2(0x000000, 0.028);

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

  locust = createLocust();
  scene.add(locust);

  scene.add(new THREE.HemisphereLight(0xffffff, 0x141020, 0.6));
  const key = new THREE.DirectionalLight(0xffffff, 1.35); key.position.set(4,7,6); scene.add(key);
  const rim = new THREE.PointLight(0x33ccff, 1.5, 60); rim.position.set(-6,3,-4); scene.add(rim);
  const rim2 = new THREE.PointLight(0xff33aa, 1.3, 60); rim2.position.set(6,-2,-3); scene.add(rim2);
  const under = new THREE.PointLight(0x8855ff, 0.6, 40); under.position.set(0,-5,3); scene.add(under);
  const glint = new THREE.PointLight(0xffffff, 1.1, 40); glint.position.set(2,4,8); scene.add(glint);

  addPointerControls();
  animate();
}

const clamp = (v,a,b)=>Math.max(a, Math.min(b, v));
const sstep = (a,b,x)=>{ const t = clamp((x-a)/(b-a), 0, 1); return t*t*(3-2*t); };

const JUMP_END = 2.6;
let jumpStart = reduceMotion ? Infinity : 5;
let gaitSpeed = 0.5, gaitTarget = 0.5, nextPause = 3;

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

  hueShift = (t * 0.05) % 1;
  for (let i=0;i<iriMats.length;i++){
    const m = iriMats[i];
    m.color.setHSL((hueShift + m.userData.offset) % 1, m.userData.sat, m.userData.light);
  }

  const jt = t - jumpStart;
  let hindExt = 0, lift = 0, surge = 0, pitch = 0, wingOpen = 0, airborne = 0;
  if (jt >= 0 && jt < JUMP_END){
    hindExt  = -0.35*sstep(0, 0.45, jt) + 1.35*sstep(0.48, 0.68, jt) - 1.0*sstep(2.05, 2.5, jt);
    lift     = -0.20*(sstep(0,0.45,jt) - sstep(0.5,0.66,jt))
             +  1.30*(sstep(0.52,0.88,jt) - sstep(1.15,2.15,jt));
    surge    =  0.34*(sstep(0.52,0.90,jt) - sstep(1.30,2.20,jt));
    pitch    = -0.42*(sstep(0.5,0.82,jt) - sstep(1.05,1.95,jt))
             +  0.16*(sstep(1.25,1.85,jt) - sstep(2.05,2.45,jt));
    wingOpen =  sstep(0.62, 0.88, jt) - sstep(1.90, 2.30, jt);
    airborne =  sstep(0.48, 0.72, jt) - sstep(2.00, 2.45, jt);
  } else if (jt >= JUMP_END){
    jumpStart = t + 2.5 + Math.random()*4.5;
  }

  if (!reduceMotion && t > nextPause){
    gaitTarget = Math.random() < 0.45 ? 0.05 : 0.8 + Math.random()*0.7;
    nextPause = t + 1.5 + Math.random()*3;
  }
  gaitSpeed += (gaitTarget - gaitSpeed) * 0.03;
  const stride = gaitSpeed * (1 - airborne);
  const walk = (animate.wt = (animate.wt||0) + stride * 0.05);

  walkLegs.forEach(L=>{
    const ph = walk + L.phase;
    const swingLift = Math.max(0, Math.cos(ph));
    const ease = swingLift * swingLift * (3 - 2*swingLift);
    L.root.rotation.y  = L.baseYaw   + L.side * L.swing * Math.sin(ph) * (1-airborne) - L.side*0.35*airborne;
    L.femur.rotation.z = L.baseFemur + L.side * (0.30 * ease * (1-airborne) + 0.55*airborne);
    L.knee.rotation.z  = L.baseKnee  + L.side * (0.28 * ease * (1-airborne) + 0.45*airborne);
  });

  hindLegs.forEach(L=>{
    const s = L.side;
    const breathe = Math.sin(t*1.6 + s)*0.02;
    L.root.rotation.y  = L.baseYaw  + s*0.16*hindExt;
    L.femur.rotation.z = L.baseFem  - s*0.46*hindExt + s*breathe;
    L.knee.rotation.z  = L.baseKnee + s*(hindExt > 0 ? 2.28*hindExt : 0.85*hindExt);
  });

  antennaJoints.forEach(({joints, side})=>{
    for (let i=0;i<joints.length;i++){
      const tt = i/joints.length;
      const j = joints[i];
      const damp = 1 - airborne*0.7;
      j.rotation.x = j.userData.bx + Math.sin(t*2.4 - i*0.4) * 0.05 * tt * damp - airborne*0.05*tt;
      j.rotation.y = j.userData.by + Math.sin(t*1.9 - i*0.45 + side*1.3) * 0.07 * tt * damp;
    }
  });

  palps.forEach(({palp, side})=>{ palp.rotation.x = Math.sin(t*7 + side)*0.22; });

  const beat = Math.sin(t*42) * wingOpen;
  tegmina.forEach(({flap, side})=>{
    flap.rotation.z = side * (0.02 + wingOpen*0.55 + beat*0.30) + Math.sin(t*1.4 + side)*0.012*(1-wingOpen);
  });
  hindWings.forEach(({flap, openG, side})=>{
    openG.scale.y = 0.04 + 0.96*wingOpen;
    flap.rotation.z = side * (wingOpen*0.30 + beat*0.42);
    flap.visible = wingOpen > 0.01;
  });

  const busy = Math.min(1, gaitSpeed);
  locust.position.y = Math.sin(walk*2)*0.025*busy - 0.02 + lift;
  locust.position.x = Math.sin(walk)*0.015*busy;
  locust.position.z = surge;
  locust.rotation.z = Math.sin(walk*2)*0.015*busy + beat*0.03;
  locust.rotation.x = Math.sin(t*0.8)*0.015 + pitch;

  if (!dragging && !reduceMotion) cam.autoYaw += 0.0014 + gaitSpeed*0.0003;
  applyCamera();

  renderer.render(scene, camera);
}

let dragging=false, lastX=0, lastY=0, pinchD=0;
function addPointerControls(){
  const c = renderer.domElement;
  c.addEventListener('pointerdown', e=>{ dragging=true; lastX=e.clientX; lastY=e.clientY; c.setPointerCapture(e.pointerId); });
  c.addEventListener('pointermove', e=>{
    if(!dragging) return;
    cam.targetYaw   += (e.clientX-lastX)*0.006;
    cam.targetPitch += (e.clientY-lastY)*0.006;
    lastX=e.clientX; lastY=e.clientY;
  });
  const stop=()=>dragging=false;
  c.addEventListener('pointerup', stop);
  c.addEventListener('pointercancel', stop);
  c.addEventListener('pointerleave', stop);
  c.addEventListener('wheel', e=>{ e.preventDefault(); cam.targetDist = Math.max(5, Math.min(18, cam.targetDist + e.deltaY*0.01)); }, {passive:false});
  c.addEventListener('touchmove', e=>{
    if(e.touches.length===2){
      const dx=e.touches[0].clientX-e.touches[1].clientX, dy=e.touches[0].clientY-e.touches[1].clientY;
      const d=Math.hypot(dx,dy);
      if(pinchD) cam.targetDist = Math.max(5, Math.min(18, cam.targetDist + (pinchD-d)*0.02));
      pinchD=d;
    }
  }, {passive:true});
  c.addEventListener('touchend', ()=>pinchD=0);
}

addEventListener('resize', ()=>{
  updateSize();
  initBackground();
  if(camera){ camera.aspect = innerWidth/innerHeight; updateCamera(); }
});
addEventListener('load', init);
</script>
</body>
</html>
MikeHiggins.me.uk LinkedIn GitHub Bluesky Contra Upwork
© 2026 Mike Higgins. All rights reserved.