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>