• 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

TARDIGRADE - 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>ThreeJS: Tardigrade</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 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, tardigrade, face = null;
const legParts = [];
const bodySegs = [];

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

const cam = { dist: 4.0, yaw: 0.5, pitch: 0.28, targetYaw: 0.5, targetPitch: 0.28, targetDist: 4.0, autoYaw: 0 };
function updateCamera(){
  const w = innerWidth;
  if (w < 480){ cam.targetDist = Math.min(Math.max(cam.targetDist, 8), 13); 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.4,
    Math.cos(total)*Math.cos(p)*cam.dist
  );
  camera.lookAt(0, -0.05, 0.05);
}

function matte(color, opts={}){
  return new THREE.MeshStandardMaterial(Object.assign({
    color, roughness:0.5, metalness:0.2, side:THREE.DoubleSide
  }, opts));
}

const iriMats = [];
let hueShift = 0;
function iri(offset, opts={}){
  const { sat=0.8, light=0.52, roughness=0.5, metalness:metalness=0.32, 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;
}

function wrinkle(geo, amp, ridges){
  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.z/len; c = c<-1?-1:c>1?1:c;
    const phi = Math.acos(c);
    const theta = Math.atan2(v.y, v.x);
    const d = 1 + amp*( Math.sin(phi*ridges)*0.6
                       + Math.sin(theta*7 + phi*4)*0.25
                       + Math.sin(theta*3 - phi*5)*0.15 );
    v.multiplyScalar(d);
    pos.setXYZ(i, v.x, v.y, v.z);
  }
  pos.needsUpdate = true;
  geo.computeVertexNormals();
}

function makeMouth(){
  const group = new THREE.Group();
  const rimMat  = iri(0.0, { light:0.6, sat:0.35, roughness:0.45, metalness:0.25 });
  const toothMat= matte(0xece6dd, { roughness:0.45, metalness:0.2 });
  const darkMat = matte(0x180a0d, { roughness:0.72, metalness:0.05 });

  const rim = new THREE.Mesh(new THREE.TorusGeometry(0.155, 0.05, 16, 32), rimMat);
  group.add(rim);
  const rim2 = new THREE.Mesh(new THREE.TorusGeometry(0.115, 0.022, 12, 28), rimMat);
  rim2.position.z = 0.02; group.add(rim2);

  const teeth = new THREE.Group();
  const nT = 16, ringR = 0.125;
  for (let i=0;i<nT;i++){
    const a = i/nT * Math.PI*2;
    const tooth = new THREE.Mesh(new THREE.ConeGeometry(0.023, 0.08, 6), toothMat);
    tooth.position.set(Math.cos(a)*ringR, Math.sin(a)*ringR, 0.02);
    tooth.rotation.z = a + Math.PI/2;
    teeth.add(tooth);
  }
  group.add(teeth);

  const inner = new THREE.Group();
  const nI = 11, innerR = 0.078;
  for (let i=0;i<nI;i++){
    const a = i/nI * Math.PI*2 + Math.PI/nI;
    const tooth = new THREE.Mesh(new THREE.ConeGeometry(0.016, 0.05, 5), toothMat);
    tooth.position.set(Math.cos(a)*innerR, Math.sin(a)*innerR, 0.035);
    tooth.rotation.z = a + Math.PI/2;
    inner.add(tooth);
  }
  group.add(inner);

  const hole = new THREE.Mesh(new THREE.CircleGeometry(0.05, 22), darkMat);
  hole.position.set(0,0,0.02);
  group.add(hole);
  const throat = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.03, 0.16, 22, 1, true), darkMat);
  throat.rotation.x = Math.PI/2; throat.position.set(0,0,-0.08);
  group.add(throat);

  return { group, teeth, inner, hole, throat };
}

function makeLobopodLeg(side, cfg){
  const s = side;
  const h = cfg.hue;
  const skin  = iri(h,      { roughness:0.55, metalness:0.28, light:0.55 });
  const skin2 = iri(h+0.04, { roughness:0.6,  metalness:0.24, light:0.48 });
  const clawMat = matte(0xd9d3cc, { roughness:0.35, metalness:0.25 });

  const root = new THREE.Group();
  root.position.set(s*cfg.x, cfg.y, cfg.z);
  root.rotation.z = s*cfg.out;
  root.rotation.x = cfg.back || 0;

  const swing = new THREE.Group();
  root.add(swing);

  const A = new THREE.Vector3(s*0.05, -cfg.len1, 0);
  swing.add(segmentBetween(new THREE.Vector3(0,0,0), A, 0.19, 0.14, skin, 14));
  for (let w=1; w<=3; w++){
    const p = new THREE.Vector3().lerpVectors(new THREE.Vector3(0,0,0), A, w/4);
    const ring = new THREE.Mesh(new THREE.TorusGeometry(0.165 - w*0.014, 0.03, 8, 16), skin2);
    ring.position.copy(p); ring.rotation.x = Math.PI/2;
    swing.add(ring);
  }

  const mid = new THREE.Group();
  mid.position.copy(A);
  mid.rotation.x = cfg.baseMid;
  swing.add(mid);

  const B = new THREE.Vector3(s*0.04, -cfg.len2, 0);
  mid.add(segmentBetween(new THREE.Vector3(0,0,0), B, 0.135, 0.095, skin, 14));
  const ring2 = new THREE.Mesh(new THREE.TorusGeometry(0.11, 0.025, 8, 16), skin2);
  ring2.position.copy(new THREE.Vector3().lerpVectors(new THREE.Vector3(0,0,0), B, 0.5));
  ring2.rotation.x = Math.PI/2; mid.add(ring2);

  const foot = new THREE.Mesh(new THREE.SphereGeometry(0.1, 14, 12), skin2);
  foot.position.copy(B); foot.scale.set(1,0.9,1); mid.add(foot);

  const nClaw = 3;
  for (let c=0;c<nClaw;c++){
    const fan = (c/(nClaw-1) - 0.5) * 1.1;
    const dir = new THREE.Vector3(Math.sin(fan)*0.5*s, -1, Math.cos(fan)*0.45 - 0.15).normalize();
    const mp  = B.clone().add(dir.clone().multiplyScalar(0.09));
    const tip = B.clone().add(dir.clone().multiplyScalar(0.17)).add(new THREE.Vector3(0,-0.06,0));
    mid.add(segmentBetween(B, mp, 0.032, 0.022, clawMat, 6));
    mid.add(segmentBetween(mp, tip, 0.022, 0.002, clawMat, 6));
  }

  legParts.push({ swing, mid, phase:cfg.phase, swingAmp:cfg.swingAmp, baseMid:cfg.baseMid });
  return root;
}

function createTardigrade(){
  const g = new THREE.Group();
  const body = new THREE.Group();
  g.add(body);

  const zHead = 1.15, zTail = -1.70, span = zHead - zTail;
  const radiusAt = u => 0.28 + 0.50*Math.exp(-Math.pow((u-0.34)/0.33, 2));

  const N = 13;
  for (let i=0;i<N;i++){
    const u = i/(N-1);
    const z = zHead - u*span;
    const rr = radiusAt(u);
    const hue = 0.03 + u*0.88;
    const mat = iri(hue, { light:(i%2?0.56:0.5), roughness:0.55, metalness:0.3 });
    const geo = new THREE.SphereGeometry(1, 30, 26);
    wrinkle(geo, 0.05, 3 + (i%2));
    const wide = rr*1.06, tall = rr*0.92, depth = Math.max(0.30, rr*0.76);
    const seg = new THREE.Mesh(geo, mat);
    seg.scale.set(wide, tall, depth);
    seg.position.set(0, -0.02*(1-u), z);
    body.add(seg);
    bodySegs.push({ mesh:seg, base:new THREE.Vector3(wide, tall, depth), u });
  }

  [[0.10,0.99],[0.20,1.03],[0.33,0.985],[0.46,1.02],[0.58,0.985],[0.70,1.0],[0.82,0.98]].forEach(([u,k])=>{
    const rr = radiusAt(u);
    const z = zHead - u*span;
    const ring = new THREE.Mesh(new THREE.TorusGeometry(rr*k, 0.06, 12, 34),
                                iri(0.03 + u*0.88, { light:(k<1?0.42:0.58), metalness:0.3 }));
    ring.position.set(0, -0.02*(1-u), z);
    ring.scale.set(1.05, 0.92, 1);
    body.add(ring);
  });

  for (let i=0;i<60;i++){
    const u = 0.06 + Math.random()*0.86;
    const rr = radiusAt(u);
    const th = (Math.random()*1.7 - 0.35) * Math.PI;
    const bump = new THREE.Mesh(new THREE.SphereGeometry(0.02 + Math.random()*0.012, 8, 8),
                                iri(0.03 + u*0.88, { light:0.62, metalness:0.25 }));
    bump.position.set(Math.cos(th)*rr*1.02, Math.sin(th)*rr*0.9 - 0.02, zHead - u*span);
    body.add(bump);
  }

  let zf = -Infinity;
  bodySegs.forEach(sd => { zf = Math.max(zf, sd.mesh.position.z + sd.base.z); });

  const faceMat  = iri(0.02, { light:0.54, roughness:0.55, metalness:0.3 });
  const faceMat2 = iri(0.05, { light:0.6,  roughness:0.5,  metalness:0.3 });
  const faceGroup = new THREE.Group();
  body.add(faceGroup);

  const lips = [];
  const nLobe = 6;
  for (let i=0;i<nLobe;i++){
    const a = i/nLobe * Math.PI*2 + Math.PI/2;
    const rr = 0.27;
    const lobe = new THREE.Mesh(new THREE.SphereGeometry(1, 16, 14), faceMat);
    const bx = 0.12 + Math.abs(Math.sin(a))*0.05;
    const by = 0.15, bz = 0.17;
    lobe.scale.set(bx, by, bz);
    lobe.position.set(Math.cos(a)*rr, 0.03 + Math.sin(a)*rr*0.9, zf - 0.04);
    lobe.userData = { bx, by, bz };
    faceGroup.add(lobe);
    lips.push(lobe);
  }

  const snoutBaseZ = zf + 0.06;
  const snoutGroup = new THREE.Group();
  snoutGroup.position.set(0, 0.03, snoutBaseZ);
  faceGroup.add(snoutGroup);
  const snout = new THREE.Mesh(new THREE.CylinderGeometry(0.17, 0.27, 0.42, 26, 1, false), faceMat2);
  snout.rotation.x = Math.PI/2;
  snoutGroup.add(snout);
  const mouth = makeMouth();
  mouth.group.position.set(0, 0, 0.23);
  snoutGroup.add(mouth.group);

  const cirri = [];
  const nC = 8;
  for (let i=0;i<nC;i++){
    const a = i/nC * Math.PI*2;
    const c = new THREE.Group();
    c.position.set(Math.cos(a)*0.17, Math.sin(a)*0.17, 0.19);
    c.add(segmentBetween(new THREE.Vector3(0,0,0),
                         new THREE.Vector3(Math.cos(a)*0.03, Math.sin(a)*0.03, 0.07),
                         0.02, 0.006, faceMat2, 5));
    snoutGroup.add(c);
    cirri.push(c);
  }

  const focus = new THREE.Vector3(0, 0.28, zf - 0.16);
  for (let i=0;i<7;i++){
    const a = (i/6 - 0.5) * 2.2;
    const end = new THREE.Vector3(Math.sin(a)*0.50, 0.62 + Math.cos(a)*0.05, zf - 0.62 - Math.cos(a)*0.08);
    faceGroup.add(segmentBetween(focus, end, 0.045, 0.018, faceMat, 6));
  }
  faceGroup.add(segmentBetween(new THREE.Vector3(0,0.32,zf-0.20), new THREE.Vector3(0,0.16,zf-0.02), 0.03, 0.02, faceMat, 6));

  [-1,1].forEach(sx=>{
    const eye = new THREE.Mesh(new THREE.SphereGeometry(0.05, 12, 12), matte(0x1c0611, { roughness:0.3, metalness:0.4 }));
    eye.position.set(sx*0.30, 0.22, zf - 0.24);
    faceGroup.add(eye);
    const hi = new THREE.Mesh(new THREE.SphereGeometry(0.014, 8, 8), matte(0x8a8a90, { roughness:0.2 }));
    hi.position.set(sx*0.32, 0.25, zf - 0.21);
    faceGroup.add(hi);
  });

  face = { snout: snoutGroup, snoutBaseZ, lips, teeth: mouth.teeth, inner: mouth.inner, hole: mouth.hole, cirri };

  const legDefs = [
    { u:0.20, out:0.55, back:0.00, hue:0.14, len1:0.34, len2:0.28, swingAmp:0.30, baseMid:0.20 },
    { u:0.40, out:0.62, back:0.00, hue:0.38, len1:0.36, len2:0.30, swingAmp:0.30, baseMid:0.20 },
    { u:0.60, out:0.60, back:0.05, hue:0.60, len1:0.32, len2:0.27, swingAmp:0.28, baseMid:0.18 },
    { u:0.80, out:0.42, back:0.90, hue:0.80, len1:0.27, len2:0.23, swingAmp:0.15, baseMid:0.10 },
  ];
  legDefs.forEach((d, p)=>{
    const rr = radiusAt(d.u);
    const cfg = Object.assign({}, d, {
      x: rr*0.72,
      y: -rr*0.42,
      z: zHead - d.u*span
    });
    g.add(makeLobopodLeg( 1, Object.assign({}, cfg, { phase: p*1.2 })));
    g.add(makeLobopodLeg(-1, Object.assign({}, cfg, { phase: p*1.2 + Math.PI })));
  });

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

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

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

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

  tardigrade = createTardigrade();
  scene.add(tardigrade);

  scene.add(new THREE.HemisphereLight(0xffffff, 0x141020, 0.65));
  const key  = new THREE.DirectionalLight(0xffffff, 1.3); key.position.set(4,7,6); scene.add(key);
  const rim  = new THREE.PointLight(0x33ccff, 1.4, 60); rim.position.set(-6,3,-4); scene.add(rim);
  const rim2 = new THREE.PointLight(0xff33aa, 1.2, 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.0, 40); glint.position.set(2,4,8); scene.add(glint);

  addPointerControls();
  animate();
}

let gaitSpeed = 0.7, gaitTarget = 0.7, nextChange = 3;
function animate(){
  requestAnimationFrame(animate);
  const now = performance.now();
  const t = 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 (t > nextChange){
    gaitTarget = Math.random() < 0.3 ? 0.08 : 0.55 + Math.random()*0.7;
    nextChange = t + 2.5 + Math.random()*3.5;
  }
  gaitSpeed += (gaitTarget - gaitSpeed) * 0.02;
  const walk = (animate.wt = (animate.wt||0) + gaitSpeed * 0.04);

  legParts.forEach(L=>{
    const ph = walk + L.phase;
    L.swing.rotation.x = L.swingAmp * Math.sin(ph);
    const recover = Math.max(0, Math.cos(ph));
    const ease = recover*recover*(3 - 2*recover);
    L.mid.rotation.x = L.baseMid + 0.55 * ease;
  });

  for (let i=0;i<bodySegs.length;i++){
    const sd = bodySegs[i];
    const pulse = 1 + Math.sin(t*1.5 - sd.u*6.0) * 0.04;
    const breathe = 1 + Math.sin(t*0.9) * 0.02;
    sd.mesh.scale.set(sd.base.x*pulse*breathe, sd.base.y*(2-pulse)*breathe, sd.base.z*(0.5+pulse*0.5));
  }

  if (face){
    face.snout.position.z = face.snoutBaseZ + Math.sin(t*1.1)*0.025 + Math.max(0, Math.sin(t*0.5))*0.025;
    face.teeth.rotation.z =  Math.sin(t*2.4)*0.09;
    face.inner.rotation.z = -Math.sin(t*2.4 + 0.5)*0.14;
    face.hole.scale.setScalar(1 + Math.sin(t*1.7)*0.28);
    face.lips.forEach((lp,i)=>{
      const s = 1 + Math.sin(t*1.7 + i*0.8)*0.06;
      lp.scale.set(lp.userData.bx*s, lp.userData.by*s, lp.userData.bz);
    });
    face.cirri.forEach((c,i)=>{
      c.rotation.x = Math.sin(t*3.0 + i)*0.25;
      c.rotation.y = Math.cos(t*2.3 + i)*0.20;
    });
  }

  const busy = Math.min(1, gaitSpeed/1.0);
  tardigrade.position.y = -0.05 + Math.sin(walk*2)*0.025*busy + Math.sin(t*0.8)*0.02;
  tardigrade.position.x = Math.sin(walk)*0.03*busy;
  tardigrade.rotation.z = Math.sin(walk)*0.035*busy;
  tardigrade.rotation.x = Math.sin(t*0.7)*0.02;

  if (!dragging) cam.autoYaw += 0.0015 + gaitSpeed*0.0004;
  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(4, Math.min(15, 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(4, Math.min(15, 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.