• 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

DRAGON - 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>Dragon</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 UP = new THREE.Vector3(0,1,0);

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, dragon;
const iriMats = [];
let hueShift = 0;

const NODES = 46;
const nodeGroups = [];
const spinePts = [];
let head, jaw, neckGroup, wingL, wingR, tailFin;
const legs = [];
const horns = [];

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

const cam = { dist:3.6, yaw:0.7, pitch:0.18, targetYaw:0.7, targetPitch:0.18, targetDist:3.6, autoYaw:0 };
function updateCamera(){
  const w = innerWidth;
  if (w < 480){ cam.targetDist = Math.min(Math.max(cam.targetDist, 3.6), 20); 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.6,
    Math.cos(total)*Math.cos(p)*cam.dist
  );
  camera.lookAt(0, 0.1, 0);
}

function iri(offset, opts={}){
  const { sat=0.85, light=0.5, roughness=0.32, metalness=0.6, 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 scaledSphere(amp, nPhi, nTheta){
  const geo = new THREE.SphereGeometry(1, 30, 22);
  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), th = Math.atan2(v.z, v.x);
    const d = 1 - amp*(0.5+0.5*Math.cos(phi*nPhi))*(0.5+0.5*Math.cos(th*nTheta));
    v.multiplyScalar(d);
    pos.setXYZ(i, v.x, v.y, v.z);
  }
  pos.needsUpdate = true; geo.computeVertexNormals();
  return geo;
}
const SCALE_GEO = scaledSphere(0.14, 7, 12);

function radiusAt(t){
  const bulk = 0.17 + 0.62*Math.exp(-Math.pow((t-0.30)/0.20, 2));
  const taper = t < 0.45 ? 1 : Math.pow(1-(t-0.45)/0.55, 0.85);
  return Math.max(0.035, bulk*taper);
}

function spineAt(t, time){
  const z = 3.4 - t*9.0;
  let y = Math.exp(-Math.pow((t-0.05)/0.10, 2))*0.75
        - Math.pow(Math.max(0, t-0.55)/0.45, 2)*1.15;
  let x = 0;
  const swim = Math.sin(time*1.5 - t*5.6);
  x += swim * (0.12 + t*1.25);
  y += Math.sin(time*1.2 - t*4.2) * (0.06 + t*0.34);
  return new THREE.Vector3(x, y, z);
}

function makeWing(side){
  const shoulder = new THREE.Group();
  const memb = iri(0.55, { opacity:0.55, transparent:true, roughness:0.25, metalness:0.45, light:0.6 });
  const bone = iri(0.48, { roughness:0.4, metalness:0.55, light:0.55 });

  const O  = new THREE.Vector3(0,0,0);
  const WR = new THREE.Vector3(1.5, 0.30, 0.25);
  const F1 = new THREE.Vector3(3.5, 0.55, 0.75);
  const F2 = new THREE.Vector3(3.5, 0.20,-0.45);
  const F3 = new THREE.Vector3(2.9,-0.10,-1.45);
  const F4 = new THREE.Vector3(1.9,-0.25,-2.15);
  const BODY = new THREE.Vector3(0.15,-0.10,-1.55);

  shoulder.add(segmentBetween(O, WR, 0.16, 0.10, bone, 12));
  [F1,F2,F3,F4].forEach((F,i)=>{
    const mid = WR.clone().lerp(F, 0.5).add(new THREE.Vector3(0, 0.1, 0));
    shoulder.add(segmentBetween(WR, mid, 0.085-i*0.008, 0.055-i*0.006, bone, 10));
    shoulder.add(segmentBetween(mid, F,  0.055-i*0.006, 0.014, bone, 10));
    if (i===0) shoulder.add(segmentBetween(F, F.clone().add(new THREE.Vector3(0.28,0.1,0.1)), 0.03, 0.002, bone, 6));
  });

  const panels = [[WR,F1,F2],[WR,F2,F3],[WR,F3,F4],[WR,F4,BODY],[O,WR,BODY]];
  panels.forEach(([a,b,c])=>{
    const g = new THREE.BufferGeometry();
    const bulge = b.clone().add(c).multiplyScalar(0.5).sub(a).multiplyScalar(0.12);
    const mid = b.clone().add(c).multiplyScalar(0.5).sub(bulge);
    const verts = new Float32Array([
      a.x,a.y,a.z,  b.x,b.y,b.z,  mid.x,mid.y,mid.z,
      a.x,a.y,a.z,  mid.x,mid.y,mid.z,  c.x,c.y,c.z
    ]);
    g.setAttribute('position', new THREE.BufferAttribute(verts,3));
    g.computeVertexNormals();
    shoulder.add(new THREE.Mesh(g, memb));
  });

  shoulder.position.set(0.42, 0.34, 0);
  const wrap = new THREE.Group();
  wrap.add(shoulder);
  if (side < 0) wrap.scale.x = -1;
  wrap.userData.shoulder = shoulder;
  return wrap;
}

function makeLeg(side, cfg){
  const m = iri(cfg.hue, { roughness:0.38, metalness:0.55 });
  const wrap = new THREE.Group();
  const hip = new THREE.Group();
  wrap.add(hip);
  const A = new THREE.Vector3(0.42, -0.55, 0.10);
  const B = new THREE.Vector3(0.60, -1.15,-0.18);
  const F = new THREE.Vector3(0.66, -1.45, 0.10);
  hip.add(segmentBetween(new THREE.Vector3(0,0,0), A, 0.19, 0.13, m, 12));
  const knee = new THREE.Group(); knee.position.copy(A); hip.add(knee);
  knee.add(segmentBetween(new THREE.Vector3(0,0,0), B.clone().sub(A), 0.12, 0.075, m, 12));
  const ankle = new THREE.Group(); ankle.position.copy(B.clone().sub(A)); knee.add(ankle);
  ankle.add(segmentBetween(new THREE.Vector3(0,0,0), F.clone().sub(B), 0.075, 0.055, m, 10));
  const foot = F.clone().sub(B);
  for (let i=0;i<3;i++){
    const a = (i-1)*0.45;
    const toe = foot.clone().add(new THREE.Vector3(Math.sin(a)*0.16, -0.05, Math.cos(a)*0.30));
    ankle.add(segmentBetween(foot, toe, 0.05, 0.03, m, 8));
    ankle.add(segmentBetween(toe, toe.clone().add(new THREE.Vector3(Math.sin(a)*0.10,-0.14,Math.cos(a)*0.16)), 0.028, 0.002, m, 6));
  }
  if (side < 0) wrap.scale.x = -1;
  legs.push({ wrap, hip, knee, ankle, phase:cfg.phase||0 });
  return wrap;
}

function makeHead(){
  const g = new THREE.Group();
  const skin = iri(0.02, { roughness:0.3, metalness:0.6 });
  const dark = iri(0.95, { roughness:0.4, metalness:0.5, light:0.4 });

  const skull = new THREE.Mesh(SCALE_GEO, skin);
  skull.scale.set(0.34, 0.34, 0.52); g.add(skull);
  const snout = new THREE.Mesh(SCALE_GEO, skin);
  snout.scale.set(0.22, 0.20, 0.42); snout.position.set(0, -0.06, 0.72); g.add(snout);
  [-1,1].forEach(sx=>{
    const b = new THREE.Mesh(SCALE_GEO, skin);
    b.scale.set(0.13,0.09,0.24); b.position.set(sx*0.20, 0.20, 0.26); g.add(b);
    const eye = new THREE.Mesh(new THREE.SphereGeometry(0.115, 20, 16), iri(0.16, { roughness:0.12, metalness:0.75, light:0.6 }));
    eye.position.set(sx*0.26, 0.12, 0.30); g.add(eye);
    const pupil = new THREE.Mesh(new THREE.SphereGeometry(0.05, 12, 10), dark);
    pupil.scale.set(0.5,1.25,0.5); pupil.position.set(sx*0.33, 0.12, 0.36); g.add(pupil);
    const n = new THREE.Mesh(new THREE.SphereGeometry(0.035,10,8), dark);
    n.position.set(sx*0.10, 0.02, 1.06); g.add(n);
    const hg = new THREE.Group();
    let prev = new THREE.Vector3(sx*0.20, 0.28, 0.05);
    const hm = iri(0.72 + (sx>0?0.02:0), { roughness:0.3, metalness:0.7, light:0.55 });
    for (let k=0;k<7;k++){
      const f = k/7;
      const nxt = prev.clone().add(new THREE.Vector3(sx*(0.07+f*0.05), 0.11-f*0.14, -0.22-f*0.05));
      hg.add(segmentBetween(prev, nxt, 0.085*(1-f*0.85)+0.012, 0.085*(1-(f+0.14)*0.85)+0.008, hm, 9));
      prev = nxt;
    }
    g.add(hg); horns.push(hg);
    g.add(segmentBetween(new THREE.Vector3(sx*0.26,-0.10,0.10), new THREE.Vector3(sx*0.44,-0.22,-0.22), 0.05,0.004, hm, 7));
  });

  jaw = new THREE.Group();
  jaw.position.set(0, -0.14, 0.12);
  const jm = new THREE.Mesh(SCALE_GEO, skin);
  jm.scale.set(0.20, 0.12, 0.46); jm.position.set(0,-0.06,0.44); jaw.add(jm);
  g.add(jaw);
  for (let i=0;i<7;i++){
    const f = i/6, zz = 0.30 + f*0.72;
    [-1,1].forEach(sx=>{
      const up = segmentBetween(new THREE.Vector3(sx*0.17,-0.13,zz), new THREE.Vector3(sx*0.17,-0.26,zz), 0.028,0.002, dark, 5);
      g.add(up);
      const lo = segmentBetween(new THREE.Vector3(sx*0.15,-0.02,zz-0.14), new THREE.Vector3(sx*0.15,0.10,zz-0.14), 0.026,0.002, dark, 5);
      jaw.add(lo);
    });
  }
  return g;
}

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

  for (let i=0;i<NODES;i++){
    const t = i/(NODES-1);
    const r = radiusAt(t);
    const node = new THREE.Group();
    const mat = iri(t*0.9, { light: (i%2 ? 0.54 : 0.48), roughness:0.34, metalness:0.6 });
    const body = new THREE.Mesh(SCALE_GEO, mat);
    body.scale.set(r*1.02, r*1.12, Math.max(r*1.35, 0.24));
    node.add(body);

    if (i>2 && i<NODES-2 && i%2===0){
      const sp = Math.max(0.05, r*0.95) * (t<0.6?1:0.7);
      node.add(segmentBetween(new THREE.Vector3(0, r*0.95, 0), new THREE.Vector3(0, r*0.95+sp, -sp*0.35), sp*0.34, 0.004,
               iri(t*0.9+0.5, { roughness:0.3, metalness:0.7, light:0.6 }), 7));
    }
    if (i>3 && i<NODES-6 && i%2===1){
      const pl = new THREE.Mesh(new THREE.BoxGeometry(r*1.15, r*0.16, r*0.9),
                 iri(t*0.9+0.12, { roughness:0.4, metalness:0.5, light:0.62 }));
      pl.position.set(0, -r*0.98, 0); node.add(pl);
    }
    g.add(node); nodeGroups.push(node);
  }

  head = makeHead();
  nodeGroups[0].add(head);
  head.position.set(0, 0.10, 0.42);

  const shoulderIdx = Math.round(0.22*(NODES-1));
  wingR = makeWing(1); wingL = makeWing(-1);
  nodeGroups[shoulderIdx].add(wingR);
  nodeGroups[shoulderIdx].add(wingL);

  const frontIdx = Math.round(0.32*(NODES-1));
  const rearIdx  = Math.round(0.50*(NODES-1));
  nodeGroups[frontIdx].add(makeLeg( 1, { hue:0.30, phase:0 }));
  nodeGroups[frontIdx].add(makeLeg(-1, { hue:0.30, phase:Math.PI }));
  nodeGroups[rearIdx ].add(makeLeg( 1, { hue:0.40, phase:Math.PI }));
  nodeGroups[rearIdx ].add(makeLeg(-1, { hue:0.40, phase:0 }));

  tailFin = new THREE.Group();
  const fm = iri(0.80, { opacity:0.6, transparent:true, roughness:0.25, metalness:0.5, light:0.6 });
  const fs = new THREE.Shape();
  fs.moveTo(0,0);
  fs.bezierCurveTo(0.15, 0.55, 0.45, 0.85, 0.80, 0.55);
  fs.bezierCurveTo(0.55, 0.30, 0.42,-0.30, 0.72,-0.72);
  fs.bezierCurveTo(0.36,-0.60, 0.12,-0.35, 0,0);
  const fin = new THREE.Mesh(new THREE.ShapeGeometry(fs, 24), fm);
  fin.rotation.y = Math.PI/2;
  tailFin.add(fin);
  nodeGroups[NODES-3].add(tailFin);

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

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

  scene.add(new THREE.HemisphereLight(0xffffff, 0x101018, 0.55));
  const key  = new THREE.DirectionalLight(0xffffff, 1.35); key.position.set(5,8,6); scene.add(key);
  const rim  = new THREE.PointLight(0x33ccff, 1.6, 90); rim.position.set(-8,4,-5); scene.add(rim);
  const rim2 = new THREE.PointLight(0xff33aa, 1.4, 90); rim2.position.set(8,-2,-4); scene.add(rim2);
  const under= new THREE.PointLight(0x8855ff, 0.6, 60); under.position.set(0,-6,3); scene.add(under);
  const glint= new THREE.PointLight(0xffffff, 1.0, 70); glint.position.set(3,7,10); scene.add(glint);

  addPointerControls(); animate();
}

const _m = new THREE.Matrix4(), _x = new THREE.Vector3(), _y = new THREE.Vector3(), _z = new THREE.Vector3(), _q = new THREE.Quaternion();
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); }

  for (let i=0;i<NODES;i++) spinePts[i] = spineAt(i/(NODES-1), t);
  for (let i=0;i<NODES;i++){
    const a = spinePts[Math.max(0,i-1)], b = spinePts[Math.min(NODES-1,i+1)];
    _z.subVectors(a, b).normalize();
    _x.crossVectors(UP, _z).normalize();
    if (_x.lengthSq() < 1e-6) _x.set(1,0,0);
    _y.crossVectors(_z, _x);
    _m.makeBasis(_x, _y, _z);
    _q.setFromRotationMatrix(_m);
    const bank = -Math.cos(t*1.5 - (i/(NODES-1))*5.6) * 0.30;
    _q.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0,0,1), bank));
    nodeGroups[i].position.copy(spinePts[i]);
    nodeGroups[i].quaternion.copy(_q);
  }

  const flap = Math.sin(t*2.3);
  const twist = Math.sin(t*2.3 - 0.9)*0.35;
  if (wingR){ wingR.userData.shoulder.rotation.z = 0.25 + flap*0.85; wingR.userData.shoulder.rotation.x = twist; }
  if (wingL){ wingL.userData.shoulder.rotation.z = 0.25 + flap*0.85; wingL.userData.shoulder.rotation.x = twist; }

  for (let i=0;i<legs.length;i++){
    const L = legs[i];
    L.hip.rotation.x   = -0.35 + Math.sin(t*1.4 + L.phase)*0.12;
    L.knee.rotation.x  =  0.75 + Math.sin(t*1.4 + L.phase + 0.6)*0.14;
    L.ankle.rotation.x = -0.30 + Math.sin(t*1.4 + L.phase + 1.2)*0.10;
  }

  if (head){
    head.rotation.y = Math.sin(t*0.7)*0.25;
    head.rotation.x = Math.sin(t*0.9)*0.12;
  }
  if (jaw){
    const roar = Math.max(0, Math.sin(t*0.55 - 1.2));
    jaw.rotation.x = 0.05 + Math.pow(roar, 3)*0.55;
  }
  if (tailFin) tailFin.rotation.z = Math.sin(t*1.5)*0.25;

  if (dragon){
    dragon.rotation.y = Math.sin(t*0.22)*0.5 + t*0.06;
    dragon.position.y = Math.sin(t*0.9)*0.22 - flap*0.12;
    dragon.rotation.z = Math.sin(t*0.5)*0.06;
  }

  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(2, Math.min(24, cam.targetDist + e.deltaY*0.012)); }, {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(2, Math.min(24, 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.