• 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

TRILOBITE BEETLE - 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>Trilobite Beetle</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 TAU = Math.PI*2;
const GOLD = Math.PI*(3-Math.sqrt(5));

const bgCanvas = document.getElementById('bgCanvas');
const bgCtx = bgCanvas.getContext('2d');
function initBackground(){
  bgCanvas.width = innerWidth; bgCanvas.height = innerHeight;
  bgCtx.fillStyle = '#000'; bgCtx.fillRect(0,0,bgCanvas.width,bgCanvas.height);
}

const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('animationCanvas'), antialias:true, alpha:true });
let scene, camera, beetle;
const iriMats = [];
const segs = [], antennae = [], legParts = [], spineTips = [];
let pronotum, headGroup, headNeck;
let hueShift = 0;
let peekLevel = 0, peekUntil = 0, nextPeek = 2.0;

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

const cam = { dist: 4.4, yaw: 0.9, pitch: 0.68, targetYaw: 0.9, targetPitch: 0.68, targetDist: 4.4, autoYaw: 0 };
function updateCamera(){
  const w = innerWidth;
  if (w < 480){ cam.targetDist = Math.min(Math.max(cam.targetDist, 9.5), 16); camera.fov = 58; }
  else if (w < 768){ camera.fov = 52; }
  else { camera.fov = 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.3,
    Math.cos(total)*Math.cos(p)*cam.dist
  );
  camera.lookAt(0, 0.12, -0.2);
}

function iri(offset, opts={}){
  const { sat=0.85, light=0.5, roughness=0.32, 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=12){
  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;
}

function dimpledSphere(r, amp, nPhi, nTheta){
  const geo = new THREE.SphereGeometry(r, 60, 44);
  const pos = geo.attributes.position, v = new THREE.Vector3();
  for (let i=0;i<pos.count;i++){
    v.fromBufferAttribute(pos, i);
    const len = v.length() || 1;
    let c = v.y/len; c = c<-1?-1:c>1?1:c;
    const phi = Math.acos(c);
    const theta = Math.atan2(v.z, v.x);
    const dim = (0.5+0.5*Math.cos(phi*nPhi)) * (0.5+0.5*Math.cos(theta*nTheta));
    v.multiplyScalar(1 - amp*dim);
    pos.setXYZ(i, v.x, v.y, v.z);
  }
  pos.needsUpdate = true;
  geo.computeVertexNormals();
  return geo;
}

// ---- Armour plates -------------------------------------------------------
// Outline is the left half, front (negative y) to rear (positive y); mirrored for the right.
// After rotation.x = -PI/2 the shape's -y points toward +z (the head end).
function plateOutline(kind, hw, hd, spine){
  if (kind === 'pronotum') return [
    [0, -hd*1.25],
    [-hw*0.18, -hd*0.82],
    [-hw*0.46, -hd*1.10],
    [-hw*0.72, -hd*0.72],
    [-hw*0.94, -hd*0.30],
    [-hw - spine, hd*0.30],
    [-hw*0.92, hd*0.70],
    [-hw*0.55, hd*0.98],
    [0, hd],
  ];
  if (kind === 'tail') return [
    [0, -hd],
    [-hw*0.55, -hd*0.95],
    [-hw*0.88, -hd*0.50],
    [-hw - spine*0.7, -hd*0.05],
    [-hw*0.70, hd*0.55],
    [-hw*0.32, hd*1.15],
    [0, hd*1.55],
  ];
  return [
    [0, -hd],
    [-hw*0.55, -hd*0.96],
    [-hw*0.86, -hd*0.55],
    [-hw - spine*0.50, -hd*0.05],
    [-hw - spine, hd*0.55],
    [-hw*0.90, hd*0.58],
    [-hw*0.58, hd*0.96],
    [0, hd],
  ];
}

function plateShape(pts, sx=1, sy=1){
  const sh = new THREE.Shape();
  const left = pts.map(p=>[p[0]*sx, p[1]*sy]);
  const right = left.slice(1, -1).reverse().map(p=>[-p[0], p[1]]);
  const all = left.concat(right);
  sh.moveTo(all[0][0], all[0][1]);
  for (let i=1;i<all.length;i++) sh.lineTo(all[i][0], all[i][1]);
  sh.closePath();
  return sh;
}

function makePlate(cfg){
  const g = new THREE.Group();
  const pts = plateOutline(cfg.kind, cfg.hw, cfg.hd, cfg.spine);

  // main plate
  const body = new THREE.Mesh(
    new THREE.ExtrudeGeometry(plateShape(pts), { depth:0.06, bevelEnabled:true, bevelThickness:0.05, bevelSize:0.045, bevelSegments:4, curveSegments:4 }),
    iri(cfg.hue, { roughness:0.38, metalness:0.55, light:0.46 }));
  body.rotation.x = -Math.PI/2;
  g.add(body);

  // coloured margin plate sitting just beneath, slightly larger
  const rim = new THREE.Mesh(
    new THREE.ExtrudeGeometry(plateShape(pts, 1.09, 1.06), { depth:0.02, bevelEnabled:true, bevelThickness:0.025, bevelSize:0.03, bevelSegments:3, curveSegments:4 }),
    iri(cfg.hue + 0.42, { roughness:0.42, metalness:0.5, light:0.6 }));
  rim.rotation.x = -Math.PI/2;
  rim.position.y = -0.035;
  g.add(rim);

  // dorsal keel + the two paramedian ridges that give the "trilobite" three-lobed look
  const keelH = cfg.kind === 'pronotum' ? 0.13 : 0.095;
  const ridgeDefs = cfg.kind === 'pronotum'
    ? [[0, 1.0, 0.16], [-0.42, 0.8, 0.13], [0.42, 0.8, 0.13]]
    : [[0, 1.0, 0.11], [-0.48, 0.7, 0.085], [0.48, 0.7, 0.085]];
  ridgeDefs.forEach(([rx, rs, rr], k)=>{
    const ridge = new THREE.Mesh(dimpledSphere(rr, 0.10, 7, 9), iri(cfg.hue + 0.03 + k*0.02, { roughness:0.4, metalness:0.55, light:0.5 }));
    ridge.scale.set(1.0, keelH/rr*rs, (cfg.hd*0.95)/rr * (cfg.kind === 'tail' ? 1.2 : 1));
    ridge.position.set(rx*cfg.hw, 0.10, cfg.kind === 'tail' ? -cfg.hd*0.15 : 0);
    g.add(ridge);
  });

  // a few stiff bristles along each lateral spine
  const hm = iri(cfg.hue + 0.06, { roughness:0.5, metalness:0.35, light:0.62 });
  const tipIdx = cfg.kind === 'pronotum' ? 5 : 4;
  [-1,1].forEach(sx=>{
    const tip = pts[cfg.kind === 'tail' ? 3 : tipIdx];
    const prev = pts[(cfg.kind === 'tail' ? 3 : tipIdx) - 1];
    for (let i=0;i<3;i++){
      const t = 0.25 + i*0.3;
      const bx = sx*(prev[0] + (tip[0]-prev[0])*t), bz = -(prev[1] + (tip[1]-prev[1])*t);
      const p = new THREE.Vector3(bx, 0.10, bz);
      const q = p.clone().add(new THREE.Vector3(sx*0.16, 0.06 + (i%2)*0.05, 0.04 - i*0.05));
      g.add(segmentBetween(p, q, 0.009, 0.001, hm, 4));
    }
  });
  return g;
}

function makeLeg(cfg){
  const s = cfg.side, m = iri(cfg.hue, { roughness:0.45, metalness:0.4, light:0.6 });
  const hm = iri(cfg.hue+0.05, { roughness:0.55, metalness:0.3, light:0.66 });
  const root = new THREE.Group();
  root.position.set(s*cfg.x, cfg.y, cfg.z);
  root.rotation.y = cfg.yaw||0;

  const lift = new THREE.Group();
  lift.rotation.z = s*cfg.out;
  root.add(lift);

  const A = new THREE.Vector3(s*cfg.f, -cfg.fdrop, 0);
  lift.add(segmentBetween(new THREE.Vector3(0,0,0), A, 0.042, 0.028, m, 12));

  const knee = new THREE.Group(); knee.position.copy(A); lift.add(knee);
  const B = new THREE.Vector3(s*cfg.t, -cfg.tdrop, 0.02);
  knee.add(segmentBetween(new THREE.Vector3(0,0,0), B, 0.028, 0.016, m, 12));
  const C = B.clone().add(new THREE.Vector3(s*0.08, -0.09, 0.01));
  knee.add(segmentBetween(B, C, 0.015, 0.006, m, 10));
  // tarsal claw
  knee.add(segmentBetween(C, C.clone().add(new THREE.Vector3(s*0.03, -0.03, 0.03)), 0.007, 0.001, m, 6));
  for (let i=1;i<5;i++){
    const p = new THREE.Vector3().lerpVectors(new THREE.Vector3(0,0,0), B, i/5);
    [-1,1].forEach(d=>{
      knee.add(segmentBetween(p, p.clone().add(new THREE.Vector3(s*0.03*d, -0.06, 0.02*d)), 0.007, 0.001, hm, 4));
    });
  }
  for (let i=1;i<4;i++){
    const p = new THREE.Vector3().lerpVectors(new THREE.Vector3(0,0,0), A, i/4);
    lift.add(segmentBetween(p, p.clone().add(new THREE.Vector3(s*0.02, -0.06, 0.015)), 0.007, 0.001, hm, 4));
  }
  legParts.push({ root, lift, knee, side:s, baseYaw:cfg.yaw||0, baseOut:s*cfg.out, phase:cfg.phase||0, swing:cfg.swing??0.2 });
  return root;
}

function makeAntenna(sx, hue){
  const an = new THREE.Group();
  const m = iri(hue, { roughness:0.45, light:0.6 });
  const bm = iri(hue + 0.08, { roughness:0.4, light:0.62 });
  let p = new THREE.Vector3(0,0,0);
  const step = new THREE.Vector3(sx*0.05, 0.015, 0.075);
  for (let i=0;i<4;i++){
    const q = p.clone().add(step);
    an.add(segmentBetween(p, q, 0.016 - i*0.002, 0.012 - i*0.002, m, 8));
    const bead = new THREE.Mesh(new THREE.SphereGeometry(0.018 - i*0.002, 12, 10), bm);
    bead.position.copy(q); an.add(bead);
    p = q;
  }
  return an;
}

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

  // soft underbody linking the plates
  const under = new THREE.Mesh(dimpledSphere(0.5, 0.08, 6, 20), iri(0.62, { roughness:0.55, metalness:0.35, light:0.32 }));
  under.scale.set(0.95, 0.30, 3.1);
  under.position.set(0, 0.14, -0.25);
  g.add(under);

  // ---- head (retractable beneath the pronotum) ----
  headNeck = new THREE.Group();
  headNeck.position.set(0, 0.11, 1.05);
  g.add(headNeck);
  headGroup = new THREE.Group();
  headNeck.add(headGroup);
  const head = new THREE.Mesh(new THREE.SphereGeometry(0.15, 28, 22), iri(0.02, { roughness:0.4, light:0.44 }));
  head.scale.set(1.15, 0.7, 1.0); headGroup.add(head);
  [-1,1].forEach(sx=>{
    const eye = new THREE.Mesh(new THREE.SphereGeometry(0.05, 18, 14), iri(0.10, { roughness:0.2, metalness:0.65, light:0.55 }));
    eye.position.set(sx*0.13, 0.03, 0.06); headGroup.add(eye);
    const mand = segmentBetween(new THREE.Vector3(sx*0.06, -0.03, 0.12), new THREE.Vector3(sx*0.03, -0.04, 0.22), 0.018, 0.004, iri(0.0, { light:0.42 }), 8);
    headGroup.add(mand);
    const an = makeAntenna(sx, 0.9);
    an.position.set(sx*0.10, 0.02, 0.12);
    headGroup.add(an); antennae.push({ an, sx });
  });

  // ---- pronotum ----
  pronotum = makePlate({ kind:'pronotum', hw:0.64, hd:0.46, spine:0.20, hue:0.04 });
  pronotum.position.set(0, 0.28, 0.92);
  g.add(pronotum);
  segs.push({ group:pronotum, baseY:0.28, idx:0 });

  // ---- abdominal tergites ----
  const N = 8;
  for (let i=0;i<N;i++){
    const t = i/(N-1);
    const hw = 0.56 + 0.10*Math.sin(Math.min(1, i/2.2)*Math.PI*0.5) - 0.075*Math.max(0, i-2);
    const spine = 0.26 + 0.12*Math.sin(t*Math.PI) - 0.06*t;
    const kind = i === N-1 ? 'tail' : 'tergite';
    const plate = makePlate({ kind, hw, hd:0.19, spine, hue:0.10 + t*0.26 });
    plate.position.set(0, 0.26 - i*0.017, 0.30 - i*0.32);
    g.add(plate);
    segs.push({ group:plate, baseY:plate.position.y, idx:i+1 });
  }

  // ---- legs: three pairs tucked under the front ----
  const legPairs = [
    { x:0.30, y:0.10, z:1.02, out:0.55, yaw:-0.70, f:0.24, fdrop:0.12, t:0.20, tdrop:0.20, hue:0.16, swing:0.22, phase:0 },
    { x:0.36, y:0.10, z:0.62, out:0.60, yaw: 0.05, f:0.28, fdrop:0.13, t:0.24, tdrop:0.22, hue:0.14, swing:0.20, phase:Math.PI },
    { x:0.36, y:0.10, z:0.28, out:0.62, yaw: 0.75, f:0.32, fdrop:0.14, t:0.30, tdrop:0.24, hue:0.12, swing:0.22, phase:0 },
  ];
  legPairs.forEach(c=>{
    g.add(makeLeg(Object.assign({}, c, { side: 1 })));
    g.add(makeLeg(Object.assign({}, c, { side:-1, yaw:-c.yaw, phase:c.phase + Math.PI })));
  });

  g.position.y = 0.0;
  g.scale.setScalar(1.425);
  return g;
}

function init(){
  initBackground(); updateSize();
  scene = new THREE.Scene(); scene.background = null;
  camera = new THREE.PerspectiveCamera(46, innerWidth/innerHeight, 0.1, 1000);
  updateCamera();
  beetle = createBeetle();
  scene.add(beetle);

  scene.add(new THREE.HemisphereLight(0xffffff, 0x101018, 0.55));
  const key  = new THREE.DirectionalLight(0xffffff, 1.35); key.position.set(4,8,6); scene.add(key);
  const rim  = new THREE.PointLight(0x33ccff, 1.5, 70); rim.position.set(-7,4,-4); scene.add(rim);
  const rim2 = new THREE.PointLight(0xff33aa, 1.3, 70); rim2.position.set(7,-1,-3); scene.add(rim2);
  const under= new THREE.PointLight(0x8855ff, 0.6, 50); under.position.set(0,-5,3); scene.add(under);
  const glint= new THREE.PointLight(0xffffff, 1.0, 55); glint.position.set(2,6,9); scene.add(glint);

  addPointerControls(); animate();
}

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); }

  if (beetle){
    beetle.position.y = Math.sin(t*1.1)*0.015;
    beetle.rotation.z = Math.sin(t*0.8)*0.018;
    beetle.rotation.x = Math.sin(t*0.6)*0.012;
  }

  // a slow ripple travelling back along the armour, plus the plates flexing on their overlaps
  for (let i=0;i<segs.length;i++){
    const s = segs[i];
    const ph = t*1.4 - s.idx*0.55;
    s.group.rotation.x = Math.sin(ph)*0.028;
    s.group.rotation.z = Math.sin(t*0.9 - s.idx*0.3)*0.012;
    s.group.position.y = s.baseY + Math.sin(ph)*0.012;
  }

  // head peeks out from under the pronotum now and then, otherwise stays tucked
  if (t > nextPeek){
    peekUntil = t + 1.5 + Math.random()*2.5;
    nextPeek  = peekUntil + 2.0 + Math.random()*4.0;
  }
  const peeking = t < peekUntil;
  peekLevel += ((peeking ? 1 : 0) - peekLevel) * 0.06;
  if (headNeck){
    headNeck.position.z = 1.05 + 0.34*peekLevel;
    headNeck.position.y = 0.11 + 0.04*peekLevel;
    headGroup.rotation.y = Math.sin(t*0.7)*0.18*peekLevel;
    headGroup.rotation.x = -0.15 + 0.12*peekLevel + Math.sin(t*1.3)*0.04*peekLevel;
  }
  for (let i=0;i<antennae.length;i++){
    const a = antennae[i];
    a.an.rotation.y = a.sx*(0.15 + Math.sin(t*3.2+i)*0.25);
    a.an.rotation.x = Math.cos(t*2.6+i*1.7)*0.14;
  }

  const walk = t * 1.3;
  for (let i=0;i<legParts.length;i++){
    const L = legParts[i];
    const ph = walk + L.phase;
    const recover = Math.max(0, Math.cos(ph));
    const ease = recover*recover*(3 - 2*recover);
    L.root.rotation.y = L.baseYaw + L.side * L.swing * Math.sin(ph);
    L.lift.rotation.z = L.baseOut + L.side * 0.22 * ease;
    L.knee.rotation.z = L.side * 0.18 * ease;
  }

  if (!dragging) cam.autoYaw += 0.0012;
  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(3.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(3.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.