COCKROACH - 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: Cockroach</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 COL = {
wing: 0x5d2c14,
wingEdge: 0x7a3d1c,
carapace: 0x6b3517,
pronotDark: 0x2c150a,
pronotEdge: 0xba8236,
belly: 0x8a5528,
leg: 0x4a2513,
legLight: 0x6a3a1e,
legDark: 0x35190c,
spine: 0x1c0e06,
antenna: 0x3d1d0e,
eye: 0x0a0503,
eyeGloss: 0x5a3016,
cerci: 0x774223
};
const bgCanvas = document.getElementById('bgCanvas');
const bgCtx = bgCanvas.getContext('2d');
const pixelSize = 9;
let cols, rows, pix = [];
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, roach;
const legParts = [];
const antennaJoints = [];
const palps = [];
let wings = [];
function updateSize(){
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
}
const cam = { dist: 8.5, yaw: 0.5, pitch: 0.32, targetYaw: 0.5, targetPitch: 0.32, targetDist: 8.5, autoYaw: 0 };
function updateCamera(){
const w = innerWidth;
if (w < 480) cam.targetDist = Math.min(Math.max(cam.targetDist, 8.5), 13);
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.4,
Math.cos(total)*Math.cos(p)*cam.dist
);
camera.lookAt(0, -0.1, -0.1);
}
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;
}
function makeLeg(side, cfg){
const s = side;
const h = cfg.hue;
const legMat = iri(h, { roughness:0.34, metalness:0.5 });
const legMat2 = iri(h+0.05, { roughness:0.30, metalness:0.55, light:0.56 });
const darkMat = iri(h-0.04, { roughness:0.4, metalness:0.45, light:0.4 });
const spineMat= chitin(0x140b09, { roughness:0.6, metalness:0.1 });
const Lf = cfg.fem;
const Lt = cfg.tib;
const femDroop = 0.55;
const kneeBend = 1.05;
const root = new THREE.Group();
root.position.set(s*cfg.hipX, cfg.hipY, cfg.hipZ);
root.rotation.y = cfg.yaw;
const coxaEnd = new THREE.Vector3(s*0.16, -0.04, 0);
root.add(segmentBetween(new THREE.Vector3(0,0,0), coxaEnd, 0.055, 0.05, darkMat, 8));
const femur = new THREE.Group();
femur.position.copy(coxaEnd);
femur.rotation.z = -s*femDroop;
root.add(femur);
const femEnd = new THREE.Vector3(s*Lf, 0, 0);
femur.add(segmentBetween(new THREE.Vector3(0,0,0), femEnd, 0.05, 0.036, legMat, 10));
const knee = new THREE.Group();
knee.position.copy(femEnd);
knee.rotation.z = -s*kneeBend;
femur.add(knee);
const tibEnd = new THREE.Vector3(s*Lt, 0, 0);
knee.add(segmentBetween(new THREE.Vector3(0,0,0), tibEnd, 0.034, 0.020, legMat2, 9));
function addSpines(parent, a, b, count, rad){
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(new THREE.Vector3(0,-rad,0))
.add(along.clone().multiplyScalar(-rad*0.5*s));
parent.add(segmentBetween(base, tip, rad*0.45, 0.001, spineMat, 4));
}
}
addSpines(femur, new THREE.Vector3(0,0,0), femEnd, 3, 0.055);
addSpines(knee, new THREE.Vector3(0,0,0), tibEnd, 6, 0.06);
let prev = tibEnd.clone();
for (let k=0;k<5;k++){
const nt = prev.clone().add(new THREE.Vector3(s*(0.045-k*0.004), -0.045 - k*0.006, 0.0));
knee.add(segmentBetween(prev, nt, 0.020 - k*0.002, 0.016 - k*0.002, darkMat, 6));
prev = nt;
}
[-1,1].forEach(cs=>{
const claw = prev.clone().add(new THREE.Vector3(s*0.03, -0.03, cs*0.03));
knee.add(segmentBetween(prev, claw, 0.012, 0.001, spineMat, 4));
});
legParts.push({
root, femur, knee, side:s,
baseYaw:cfg.yaw, baseFemur:-s*femDroop, baseKnee:-s*kneeBend,
phase:cfg.phase, swing:cfg.swing
});
return root;
}
function makeAntenna(side){
const root = new THREE.Group();
root.position.set(side*0.16, 0.16, 1.42);
root.rotation.set(-0.35, side*0.45, 0);
const joints = [];
const N = 18;
let parent = root;
for (let i=0;i<N;i++){
const t = i/N;
const antMat = iri(t*0.9, { roughness:0.4, metalness:0.4, light:0.55 });
const j = new THREE.Group();
j.userData.bx = -0.06 - 0.02*Math.sin(i*0.6);
j.userData.by = side*0.02;
j.rotation.set(j.userData.bx, j.userData.by, 0);
const segLen = 0.30*(1 - t*0.55);
const r0 = 0.030*(1-t) + 0.006;
const r1 = 0.030*(1-(t+1/N)) + 0.006;
j.add(segmentBetween(new THREE.Vector3(0,0,0), new THREE.Vector3(0,0,segLen), r0, r1, antMat, 6));
const ring = new THREE.Mesh(new THREE.TorusGeometry(r1*1.15, r1*0.35, 5, 8), antMat);
ring.position.set(0,0,segLen); ring.rotation.x = Math.PI/2;
j.add(ring);
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 createRoach(){
const g = new THREE.Group();
const abdomen = new THREE.Group();
const nSeg = 8;
for (let i=0;i<nSeg;i++){
const t = i/(nSeg-1);
const w = 0.62 * (1 - t*0.5);
const segHue = 0.42 + t*0.55;
const geo = new THREE.SphereGeometry(1, 20, 12);
const seg = new THREE.Mesh(geo, iri(segHue, { light: i%2?0.55:0.46, metalness:0.55 }));
seg.scale.set(w, 0.20*(1-t*0.25), 0.30);
seg.position.set(0, -0.05 - t*0.05, -0.35 - i*0.28);
abdomen.add(seg);
const belly = new THREE.Mesh(geo, iri(segHue-0.03, { light:0.6, metalness:0.45 }));
belly.scale.set(w*0.9, 0.14, 0.26);
belly.position.set(0, -0.16 - t*0.05, -0.35 - i*0.28);
abdomen.add(belly);
if (i<nSeg-1) [-1,1].forEach(sx=>{
const b = new THREE.Mesh(new THREE.SphereGeometry(0.045,8,8), iri(segHue+0.05,{light:0.62}));
b.position.set(sx*w*0.98, -0.08 - t*0.05, -0.35 - i*0.28);
abdomen.add(b);
});
}
[-1,1].forEach(sx=>{
const p0 = new THREE.Vector3(sx*0.14, -0.14, -0.35 - (nSeg-1)*0.28);
const p1 = new THREE.Vector3(sx*0.30, -0.20, -0.35 - (nSeg-1)*0.28 - 0.55);
abdomen.add(segmentBetween(p0, p1, 0.05, 0.005, iri(0.98,{roughness:0.4,metalness:0.5}), 6));
});
g.add(abdomen);
const thorax = new THREE.Mesh(new THREE.SphereGeometry(0.55, 24, 18), iri(0.30,{roughness:0.3,metalness:0.55}));
thorax.scale.set(1, 0.42, 1.05);
thorax.position.set(0, 0.02, 0.5);
g.add(thorax);
const shieldGroup = new THREE.Group();
const marginGeo = new THREE.SphereGeometry(0.72, 32, 20);
const margin = new THREE.Mesh(marginGeo, iri(0.14,{roughness:0.25,metalness:0.6,light:0.55}));
margin.scale.set(1.0, 0.32, 0.92);
shieldGroup.add(margin);
const centreGeo = new THREE.SphereGeometry(0.58, 32, 20);
const centre = new THREE.Mesh(centreGeo, iri(0.06,{roughness:0.22,metalness:0.7,light:0.48}));
centre.scale.set(1.0, 0.40, 0.9);
centre.position.set(0, 0.055, -0.02);
shieldGroup.add(centre);
[-1,1].forEach(sx=>{
const lobe = new THREE.Mesh(new THREE.SphereGeometry(0.16,16,12), iri(0.18,{roughness:0.25,light:0.6}));
lobe.scale.set(1,0.4,1.2);
lobe.position.set(sx*0.2, 0.19, 0.1);
shieldGroup.add(lobe);
});
shieldGroup.position.set(0, 0.16, 0.78);
shieldGroup.rotation.x = -0.06;
g.add(shieldGroup);
const head = new THREE.Group();
const skull = new THREE.Mesh(new THREE.SphereGeometry(0.26, 24, 20), iri(0.0,{roughness:0.28,metalness:0.55}));
skull.scale.set(1, 1.15, 0.85);
head.add(skull);
[-1,1].forEach(sx=>{
const eye = new THREE.Mesh(new THREE.SphereGeometry(0.13, 18, 16), chitin(0x0a0503,{roughness:0.15,metalness:0.6}));
eye.scale.set(0.75, 1.1, 0.7);
eye.position.set(sx*0.2, 0.05, 0.06);
head.add(eye);
const gloss = new THREE.Mesh(new THREE.SphereGeometry(0.05,10,10), chitin(0xffffff,{roughness:0.1,metalness:0.3}));
gloss.position.set(sx*0.24, 0.11, 0.12);
head.add(gloss);
});
const face = new THREE.Mesh(new THREE.SphereGeometry(0.14,16,12), iri(0.94,{roughness:0.35,light:0.42}));
face.scale.set(1,0.8,0.7); face.position.set(0,-0.18,0.12);
head.add(face);
[-1,1].forEach(sx=>{
const palp = new THREE.Group();
palp.position.set(sx*0.12, -0.22, 0.14);
let prev = new THREE.Vector3(0,0,0);
for (let k=0;k<3;k++){
const nt = prev.clone().add(new THREE.Vector3(sx*0.03, -0.05, 0.07));
palp.add(segmentBetween(prev, nt, 0.028-k*0.006, 0.02-k*0.006, iri(0.9+k*0.03,{roughness:0.4}),5));
prev = nt;
}
head.add(palp);
palps.push({ palp, side:sx });
});
head.position.set(0, 0.02, 1.28);
head.rotation.x = 0.5;
g.add(head);
g.add(makeAntenna(-1));
g.add(makeAntenna( 1));
function makeWing(side){
const len = 2.5, wid = 0.62;
const shape = new THREE.Shape();
shape.moveTo(0,0);
shape.bezierCurveTo(len*0.18, wid*0.55, len*0.5, wid*0.72, len*0.72, wid*0.6);
shape.bezierCurveTo(len*0.88, wid*0.5, len*0.99, wid*0.18, len, 0);
shape.bezierCurveTo(len*0.99,-wid*0.28, len*0.6,-wid*0.4, len*0.25,-wid*0.28);
shape.bezierCurveTo(len*0.1,-wid*0.18, 0.02,-wid*0.05, 0,0);
const geo = new THREE.ExtrudeGeometry(shape, { depth:0.03, bevelEnabled:true, bevelThickness:0.02, bevelSize:0.02, bevelSegments:2, steps:1, curveSegments:24 });
const wingMat = iri(side>0 ? 0.48 : 0.55, { roughness:0.18, metalness:0.85, light:0.55, opacity:0.9, transparent:true });
const wing = new THREE.Mesh(geo, wingMat);
const pivot = new THREE.Group();
pivot.add(wing);
wing.rotation.set(-Math.PI/2 + 0.06, 0, 0);
wing.rotation.z = 0;
pivot.rotation.set(0, side>0 ? -0.14 : Math.PI+0.14, 0);
pivot.rotation.z = side>0 ? 0.03 : -0.03;
pivot.position.set(side*0.03, 0.30, 0.62);
for (let vi=0; vi<7; vi++){
const off = (vi/6);
const veinMat = iri(0.55 + off*0.4, { roughness:0.3, metalness:0.6, light:0.6 });
const vc = new THREE.QuadraticBezierCurve3(
new THREE.Vector3(len*0.05, off*wid*0.5*(off<0.5?1:-0.6)+0.01, 0.05),
new THREE.Vector3(len*0.5, (off-0.4)*wid*0.9, 0.05),
new THREE.Vector3(len*0.95, (off-0.5)*wid*0.4, 0.05)
);
const vg = new THREE.TubeGeometry(vc, 10, 0.010, 4, false);
const vm = new THREE.Mesh(vg, veinMat);
vm.rotation.copy(wing.rotation);
pivot.add(vm);
}
wings.push(pivot);
return pivot;
}
g.add(makeWing(1));
g.add(makeWing(-1));
const legCfg = [
{ hipX:0.32, hipY:-0.02, hipZ:0.86, yaw:-0.80, fem:0.60, tib:0.66, swing:0.20, phase:0, hue:0.02 },
{ hipX:0.38, hipY:-0.04, hipZ:0.30, yaw: 0.00, fem:0.74, tib:0.82, swing:0.20, phase:Math.PI, hue:0.42 },
{ hipX:0.38, hipY:-0.05, hipZ:-0.28, yaw: 0.82, fem:0.92, tib:1.02, swing:0.22, phase:0, hue:0.75 },
];
legCfg.forEach(c=>{
g.add(makeLeg( 1, c));
g.add(makeLeg(-1, Object.assign({}, c, { yaw:-c.yaw, phase:c.phase + 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.03);
camera = new THREE.PerspectiveCamera(46, innerWidth/innerHeight, 0.1, 1000);
updateCamera();
roach = createRoach();
scene.add(roach);
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();
}
let gaitSpeed = 0.9, gaitTarget = 0.9, nextBurst = 2;
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 > nextBurst){
gaitTarget = Math.random() < 0.4 ? 0.15 : 2.2 + Math.random()*2.5;
nextBurst = t + 0.8 + Math.random()*2.2;
}
gaitSpeed += (gaitTarget - gaitSpeed) * 0.04;
const walk = (animate.wt = (animate.wt||0) + gaitSpeed * 0.05);
legParts.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);
L.femur.rotation.z = L.baseFemur + L.side * 0.34 * ease;
L.knee.rotation.z = L.baseKnee + L.side * 0.30 * ease;
});
antennaJoints.forEach(({joints, side})=>{
for (let i=0;i<joints.length;i++){
const tt = i/joints.length;
const j = joints[i];
j.rotation.x = j.userData.bx + Math.sin(t*3.0 - i*0.5) * 0.14 * tt;
j.rotation.y = j.userData.by + Math.sin(t*2.3 - i*0.55 + side*1.3) * 0.20 * tt
+ Math.sin(t*0.7 + side) * 0.10 * tt;
}
});
palps.forEach(({palp, side})=>{ palp.rotation.x = Math.sin(t*6 + side)*0.25; });
wings.forEach((w,i)=>{ w.rotation.z = (i===0?0.03:-0.03) + Math.sin(t*1.5 + i)*0.015; });
const busy = Math.min(1, gaitSpeed/2);
roach.position.y = Math.sin(walk*2)*0.03*busy - 0.05;
roach.position.x = Math.sin(walk)*0.02*busy;
roach.rotation.z = Math.sin(walk*2)*0.02*busy;
roach.rotation.x = Math.sin(t*0.9)*0.02;
if (!dragging) cam.autoYaw += 0.0016 + gaitSpeed*0.00025;
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.5, Math.min(16, 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.5, Math.min(16, 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>