LOCUST - 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>Locust</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 DARK = { eye: 0x0a0604, gloss: 0xffffff, spine: 0x140b09, claw: 0x0e0805 };
const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
const bgCanvas = document.getElementById('bgCanvas');
const bgCtx = bgCanvas.getContext('2d');
function initBackground(){
bgCanvas.width = innerWidth; bgCanvas.height = innerHeight;
const g = bgCtx.createRadialGradient(
bgCanvas.width*0.5, bgCanvas.height*0.55, 0,
bgCanvas.width*0.5, bgCanvas.height*0.55, Math.max(bgCanvas.width, bgCanvas.height)*0.75
);
g.addColorStop(0, '#0d0716');
g.addColorStop(1, '#000000');
bgCtx.fillStyle = g;
bgCtx.fillRect(0, 0, bgCanvas.width, bgCanvas.height);
}
const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('animationCanvas'), antialias:true, alpha:true });
let scene, camera, locust;
const walkLegs = [];
const hindLegs = [];
const antennaJoints = [];
const palps = [];
const tegmina = [];
const hindWings = [];
function updateSize(){
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
}
const cam = { dist: 9.5, yaw: 0.55, pitch: 0.26, targetYaw: 0.55, targetPitch: 0.26, targetDist: 9.5, autoYaw: 0 };
function updateCamera(){
const w = innerWidth;
if (w < 480) cam.targetDist = Math.min(Math.max(cam.targetDist, 10), 15);
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.5,
Math.cos(total)*Math.cos(p)*cam.dist
);
camera.lookAt(0, 0.05, -0.25);
}
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;
}
const V = (x,y,z)=>new THREE.Vector3(x,y,z);
function addSpines(parent, a, b, count, rad, s, mat, dirY=-1){
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(V(0, dirY*rad, 0))
.add(along.clone().multiplyScalar(-rad*0.5*s));
parent.add(segmentBetween(base, tip, rad*0.45, 0.001, mat, 4));
}
}
function makeWalkLeg(side, cfg){
const s = side;
const legMat = iri(cfg.hue, { roughness:0.34, metalness:0.5 });
const legMat2 = iri(cfg.hue+0.05, { roughness:0.30, metalness:0.55, light:0.56 });
const darkMat = iri(cfg.hue-0.05, { roughness:0.42, metalness:0.45, light:0.38 });
const spineMat = chitin(DARK.spine, { roughness:0.6, metalness:0.1 });
const femDroop = 0.72, kneeBend = 1.30;
const root = new THREE.Group();
root.position.set(s*cfg.hipX, cfg.hipY, cfg.hipZ);
root.rotation.y = cfg.yaw;
const coxaEnd = V(s*0.15, -0.05, 0);
root.add(segmentBetween(V(0,0,0), coxaEnd, 0.070, 0.052, darkMat, 8));
const femur = new THREE.Group();
femur.position.copy(coxaEnd);
femur.rotation.z = -s*femDroop;
root.add(femur);
const femEnd = V(s*cfg.fem, 0, 0);
femur.add(segmentBetween(V(0,0,0), femEnd, 0.055, 0.036, legMat, 10));
const knee = new THREE.Group();
knee.position.copy(femEnd);
knee.rotation.z = -s*kneeBend;
femur.add(knee);
const tibEnd = V(s*cfg.tib, 0, 0);
knee.add(segmentBetween(V(0,0,0), tibEnd, 0.033, 0.020, legMat2, 9));
addSpines(knee, V(0,0,0), tibEnd, 5, 0.05, s, spineMat);
let prev = tibEnd.clone();
for (let k=0;k<3;k++){
const nt = prev.clone().add(V(s*0.055, -0.055 - k*0.008, 0));
knee.add(segmentBetween(prev, nt, 0.026 - k*0.004, 0.022 - k*0.004, darkMat, 6));
prev = nt;
}
[-1,1].forEach(cs=>{
knee.add(segmentBetween(prev, prev.clone().add(V(s*0.035, -0.03, cs*0.03)), 0.013, 0.001, chitin(DARK.claw), 4));
});
walkLegs.push({
root, femur, knee, side:s,
baseYaw:cfg.yaw, baseFemur:-s*femDroop, baseKnee:-s*kneeBend,
phase:cfg.phase, swing:cfg.swing
});
return root;
}
function makeHindLeg(side){
const s = side;
const femMat = iri(0.62, { roughness:0.28, metalness:0.62, light:0.50 });
const ridgeMat= iri(0.56, { roughness:0.38, metalness:0.5, light:0.34 });
const kneeMat = iri(0.50, { roughness:0.34, metalness:0.6, light:0.40 });
const tibMat = iri(0.70, { roughness:0.30, metalness:0.55, light:0.56 });
const darkMat = iri(0.66, { roughness:0.44, metalness:0.45, light:0.36 });
const spineMat= chitin(DARK.spine, { roughness:0.6, metalness:0.1 });
const Lf = 1.34, Lt = 1.46;
const root = new THREE.Group();
root.position.set(s*0.26, -0.02, 0.10);
const baseYaw = s*1.30;
root.rotation.y = baseYaw;
const femur = new THREE.Group();
const baseFem = s*0.62;
femur.rotation.z = baseFem;
root.add(femur);
const bulge = new THREE.Mesh(new THREE.SphereGeometry(1, 22, 16), femMat);
bulge.scale.set(Lf*0.46, 0.30, 0.21);
bulge.position.set(s*Lf*0.40, 0.01, 0);
femur.add(bulge);
femur.add(segmentBetween(V(s*Lf*0.62,0.01,0), V(s*Lf,0,0), 0.16, 0.075, femMat, 12));
for (let i=0;i<7;i++){
const x = s*(0.16 + i*0.145);
[-1,1].forEach(f=>{
const z = f*(0.175 - i*0.012);
const top = V(x, 0.17 - i*0.012, z);
const bot = V(x - s*0.115, -0.17 + i*0.010, z*0.85);
femur.add(segmentBetween(top, bot, 0.021, 0.021, ridgeMat, 4));
});
}
const kneeKnob = new THREE.Mesh(new THREE.SphereGeometry(0.115, 14, 12), kneeMat);
kneeKnob.position.set(s*Lf, 0, 0);
femur.add(kneeKnob);
const knee = new THREE.Group();
knee.position.set(s*Lf, 0, 0);
const baseKnee = -s*2.45;
knee.rotation.z = baseKnee;
femur.add(knee);
const tibEnd = V(s*Lt, 0, 0);
knee.add(segmentBetween(V(0,0,0), tibEnd, 0.072, 0.046, tibMat, 10));
addSpines(knee, V(0.0,0,0), tibEnd, 9, 0.075, s, spineMat, 1);
addSpines(knee, V(0.0,0,0), tibEnd, 9, 0.062, s, spineMat, -1);
[-1,1].forEach(cs=>{
knee.add(segmentBetween(tibEnd, tibEnd.clone().add(V(s*0.14, cs*0.09, cs*0.05)), 0.028, 0.001, spineMat, 5));
});
let prev = tibEnd.clone();
for (let k=0;k<3;k++){
const nt = prev.clone().add(V(s*0.10, -0.045, 0));
knee.add(segmentBetween(prev, nt, 0.038 - k*0.006, 0.032 - k*0.006, darkMat, 6));
prev = nt;
}
[-1,1].forEach(cs=>{
knee.add(segmentBetween(prev, prev.clone().add(V(s*0.05, -0.04, cs*0.04)), 0.016, 0.001, chitin(DARK.claw), 4));
});
hindLegs.push({ root, femur, knee, side:s, baseYaw, baseFem, baseKnee });
return root;
}
function makeAntenna(side){
const root = new THREE.Group();
root.position.set(side*0.11, 0.30, 1.62);
root.rotation.set(-0.55, side*0.30, 0);
const joints = [];
const N = 12;
let parent = root;
for (let i=0;i<N;i++){
const t = i/N;
const antMat = iri(0.04 + t*0.5, { roughness:0.4, metalness:0.42, light: i%2 ? 0.58 : 0.46 });
const j = new THREE.Group();
j.userData.bx = -0.035;
j.userData.by = side*0.015;
j.rotation.set(j.userData.bx, j.userData.by, 0);
const segLen = 0.10*(1 - t*0.30);
const r0 = 0.048*(1-t*0.55);
const r1 = 0.048*(1-(t+1/N)*0.55);
j.add(segmentBetween(V(0,0,0), V(0,0,segLen), r0, r1, antMat, 7));
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 makeTegmen(side){
const s = side;
const len = 3.05, wid = 0.80;
const shape = new THREE.Shape();
shape.moveTo(0, 0.02);
shape.bezierCurveTo(len*0.10, wid*0.70, len*0.30, wid*0.96, len*0.55, wid*0.95);
shape.bezierCurveTo(len*0.78, wid*0.94, len*0.94, wid*0.68, len, wid*0.28);
shape.bezierCurveTo(len*0.99, wid*0.10, len*0.90, wid*0.01, len*0.70, 0.0);
shape.bezierCurveTo(len*0.45,-0.02, len*0.20,-0.01, 0, 0.02);
const geo = new THREE.ExtrudeGeometry(shape, {
depth:0.02, bevelEnabled:true, bevelThickness:0.015, bevelSize:0.015, bevelSegments:2, steps:1, curveSegments:26
});
const mat = iri(s>0 ? 0.14 : 0.19, { roughness:0.22, metalness:0.7, light:0.52, opacity:0.88, transparent:true });
const flap = new THREE.Group();
flap.position.set(s*0.10, 0.34, 0.80);
const yawG = new THREE.Group();
yawG.rotation.y = Math.PI/2 - s*0.10;
flap.add(yawG);
const rollG = new THREE.Group();
rollG.rotation.x = -s*0.58;
yawG.add(rollG);
rollG.add(new THREE.Mesh(geo, mat));
for (let vi=0; vi<7; vi++){
const off = vi/6;
const vm = iri(0.20 + off*0.30, { roughness:0.3, metalness:0.6, light:0.62 });
const curve = new THREE.QuadraticBezierCurve3(
V(len*0.03, 0.05 + off*wid*0.30, 0.032),
V(len*0.45, 0.10 + off*wid*0.78, 0.032),
V(len*0.96, 0.08 + off*wid*0.24, 0.032)
);
rollG.add(new THREE.Mesh(new THREE.TubeGeometry(curve, 12, 0.011, 4, false), vm));
}
const cvMat = iri(0.34, { roughness:0.35, metalness:0.5, light:0.55 });
for (let i=0;i<11;i++){
const x = len*(0.10 + i*0.075);
const y0 = 0.10 + (i%2 ? 0.06 : 0.24);
rollG.add(segmentBetween(V(x, y0, 0.030), V(x + len*0.015, y0 + wid*0.36, 0.030), 0.008, 0.008, cvMat, 3));
}
tegmina.push({ flap, side:s });
return flap;
}
function makeHindWing(side){
const s = side;
const R = 2.5, a0 = -0.30, a1 = 0.42;
const shape = new THREE.Shape();
shape.moveTo(0,0);
shape.absarc(0, 0, R, a0, a1, false);
shape.lineTo(0,0);
const geo = new THREE.ShapeGeometry(shape, 42);
const mat = iri(s>0 ? 0.86 : 0.92, { roughness:0.16, metalness:0.4, light:0.62, opacity:0.42, transparent:true });
const flap = new THREE.Group();
flap.position.set(s*0.09, 0.24, 0.66);
const openG = new THREE.Group();
openG.scale.y = 0.04;
flap.add(openG);
const yawG = new THREE.Group();
yawG.rotation.y = Math.PI/2 - s*0.14;
openG.add(yawG);
const rollG = new THREE.Group();
rollG.rotation.x = s*1.30;
yawG.add(rollG);
rollG.add(new THREE.Mesh(geo, mat));
for (let i=0;i<=9;i++){
const ang = a0 + (a1-a0)*(i/9);
const rm = iri(0.80 + i*0.02, { roughness:0.3, metalness:0.55, light:0.66 });
rollG.add(segmentBetween(V(0,0,0.004), V(Math.cos(ang)*R, Math.sin(ang)*R, 0.004), 0.016, 0.004, rm, 4));
}
hindWings.push({ flap, openG, side:s });
return flap;
}
function createLocust(){
const g = new THREE.Group();
const nSeg = 10;
for (let i=0;i<nSeg;i++){
const t = i/(nSeg-1);
const w = 0.42 * (1 - t*0.55);
const z = 0.28 - i*0.245;
const y = -0.02 + t*t*0.30;
const hue = 0.30 + t*0.30;
const geo = new THREE.SphereGeometry(1, 20, 14);
const seg = new THREE.Mesh(geo, iri(hue, { light: i%2 ? 0.56 : 0.47, metalness:0.55 }));
seg.scale.set(w, w*0.92, 0.20);
seg.position.set(0, y, z);
g.add(seg);
const belly = new THREE.Mesh(geo, iri(hue+0.06, { light:0.64, metalness:0.42 }));
belly.scale.set(w*0.82, w*0.42, 0.17);
belly.position.set(0, y - w*0.55, z);
g.add(belly);
if (i<nSeg-1) [-1,1].forEach(sx=>{
const b = new THREE.Mesh(new THREE.SphereGeometry(0.035,8,8), iri(hue-0.06,{light:0.34}));
b.position.set(sx*w*0.96, y - w*0.25, z);
g.add(b);
});
}
[-1,1].forEach(sx=>{
const ty = new THREE.Mesh(new THREE.CircleGeometry(0.13, 16), iri(0.24,{roughness:0.15,metalness:0.7,light:0.42}));
ty.position.set(sx*0.40, 0.0, 0.20);
ty.rotation.y = sx*Math.PI/2;
g.add(ty);
});
const tailZ = 0.28 - (nSeg-1)*0.245, tailY = 0.28;
[-1,1].forEach(sx=>{
g.add(segmentBetween(V(sx*0.08, tailY, tailZ), V(sx*0.13, tailY+0.03, tailZ-0.20), 0.045, 0.012, iri(0.60,{metalness:0.5}), 6));
[-1,1].forEach(vy=>{
g.add(segmentBetween(V(sx*0.05, tailY+vy*0.07, tailZ-0.05), V(sx*0.09, tailY+vy*0.16, tailZ-0.34), 0.05, 0.015, iri(0.64,{light:0.44}), 6));
});
});
const thorax = new THREE.Mesh(new THREE.SphereGeometry(0.44, 24, 18), iri(0.24,{roughness:0.3,metalness:0.55}));
thorax.scale.set(0.98, 1.02, 1.55);
thorax.position.set(0, 0.02, 0.72);
g.add(thorax);
const saddle = new THREE.Group();
const back = new THREE.Mesh(new THREE.SphereGeometry(0.50, 28, 20), iri(0.10,{roughness:0.24,metalness:0.62,light:0.54}));
back.scale.set(0.92, 0.86, 1.30);
saddle.add(back);
const keel = new THREE.Mesh(new THREE.SphereGeometry(0.42, 20, 14), iri(0.04,{roughness:0.2,metalness:0.72,light:0.46}));
keel.scale.set(0.16, 0.52, 1.32);
keel.position.set(0, 0.24, 0);
saddle.add(keel);
[-1,1].forEach(sx=>{
const lobe = new THREE.Mesh(new THREE.SphereGeometry(0.42, 20, 16), iri(0.14,{roughness:0.26,metalness:0.6,light:0.50}));
lobe.scale.set(0.30, 0.88, 1.24);
lobe.position.set(sx*0.36, -0.10, -0.02);
saddle.add(lobe);
});
[-0.28, 0.02, 0.30].forEach((zz,i)=>{
const sul = new THREE.Mesh(new THREE.TorusGeometry(0.40, 0.022, 6, 20, Math.PI), iri(0.0 + i*0.04,{roughness:0.4,light:0.32}));
sul.rotation.y = Math.PI/2; sul.rotation.x = Math.PI;
sul.position.set(0, 0.06, zz);
sul.scale.set(1, 1.05, 1);
saddle.add(sul);
});
saddle.position.set(0, 0.16, 0.92);
saddle.rotation.x = -0.05;
g.add(saddle);
const head = new THREE.Group();
const skull = new THREE.Mesh(new THREE.SphereGeometry(0.30, 24, 20), iri(0.0,{roughness:0.28,metalness:0.55}));
skull.scale.set(0.90, 1.30, 0.92);
head.add(skull);
const fast = new THREE.Mesh(new THREE.ConeGeometry(0.13, 0.22, 4), iri(0.06,{roughness:0.3,light:0.5}));
fast.position.set(0, 0.30, 0.16); fast.rotation.x = 1.35;
head.add(fast);
[-1,1].forEach(sx=>{
const eye = new THREE.Mesh(new THREE.SphereGeometry(0.15, 20, 16), chitin(DARK.eye,{roughness:0.13,metalness:0.6}));
eye.scale.set(0.62, 1.35, 0.85);
eye.position.set(sx*0.24, 0.14, 0.02);
head.add(eye);
const gloss = new THREE.Mesh(new THREE.SphereGeometry(0.045,10,10), chitin(DARK.gloss,{roughness:0.1,metalness:0.3}));
gloss.position.set(sx*0.26, 0.28, 0.11);
head.add(gloss);
});
const face = new THREE.Mesh(new THREE.SphereGeometry(0.22,18,14), iri(0.90,{roughness:0.32,light:0.50}));
face.scale.set(0.86, 1.05, 0.42);
face.position.set(0, -0.02, 0.20);
head.add(face);
[-1,1].forEach(sx=>{
const mand = new THREE.Mesh(new THREE.SphereGeometry(0.10,12,10), iri(0.80,{roughness:0.25,metalness:0.66,light:0.36}));
mand.scale.set(0.8, 1.15, 0.7);
mand.position.set(sx*0.11, -0.30, 0.15);
head.add(mand);
});
[-1,1].forEach(sx=>{
const palp = new THREE.Group();
palp.position.set(sx*0.16, -0.30, 0.16);
let prev = V(0,0,0);
for (let k=0;k<3;k++){
const nt = prev.clone().add(V(sx*0.04, -0.075, 0.05));
palp.add(segmentBetween(prev, nt, 0.030-k*0.006, 0.024-k*0.006, iri(0.94+k*0.03,{roughness:0.4}), 5));
prev = nt;
}
head.add(palp);
palps.push({ palp, side:sx });
});
head.position.set(0, 0.10, 1.42);
head.rotation.x = 0.14;
g.add(head);
g.add(makeAntenna(-1));
g.add(makeAntenna( 1));
g.add(makeHindWing( 1));
g.add(makeHindWing(-1));
g.add(makeTegmen( 1));
g.add(makeTegmen(-1));
const legCfg = [
{ hipX:0.24, hipY:-0.14, hipZ:1.00, yaw:-0.60, fem:0.50, tib:0.60, swing:0.22, phase:0, hue:0.02 },
{ hipX:0.27, hipY:-0.16, hipZ:0.52, yaw:-0.05, fem:0.54, tib:0.66, swing:0.22, phase:Math.PI, hue:0.16 },
];
legCfg.forEach(c=>{
g.add(makeWalkLeg( 1, c));
g.add(makeWalkLeg(-1, Object.assign({}, c, { yaw:-c.yaw, phase:c.phase + Math.PI })));
});
g.add(makeHindLeg( 1));
g.add(makeHindLeg(-1));
g.scale.setScalar(0.98);
return g;
}
function init(){
initBackground(); updateSize();
scene = new THREE.Scene();
scene.background = null;
scene.fog = new THREE.FogExp2(0x000000, 0.028);
camera = new THREE.PerspectiveCamera(46, innerWidth/innerHeight, 0.1, 1000);
updateCamera();
locust = createLocust();
scene.add(locust);
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();
}
const clamp = (v,a,b)=>Math.max(a, Math.min(b, v));
const sstep = (a,b,x)=>{ const t = clamp((x-a)/(b-a), 0, 1); return t*t*(3-2*t); };
const JUMP_END = 2.6;
let jumpStart = reduceMotion ? Infinity : 5;
let gaitSpeed = 0.5, gaitTarget = 0.5, nextPause = 3;
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);
}
const jt = t - jumpStart;
let hindExt = 0, lift = 0, surge = 0, pitch = 0, wingOpen = 0, airborne = 0;
if (jt >= 0 && jt < JUMP_END){
hindExt = -0.35*sstep(0, 0.45, jt) + 1.35*sstep(0.48, 0.68, jt) - 1.0*sstep(2.05, 2.5, jt);
lift = -0.20*(sstep(0,0.45,jt) - sstep(0.5,0.66,jt))
+ 1.30*(sstep(0.52,0.88,jt) - sstep(1.15,2.15,jt));
surge = 0.34*(sstep(0.52,0.90,jt) - sstep(1.30,2.20,jt));
pitch = -0.42*(sstep(0.5,0.82,jt) - sstep(1.05,1.95,jt))
+ 0.16*(sstep(1.25,1.85,jt) - sstep(2.05,2.45,jt));
wingOpen = sstep(0.62, 0.88, jt) - sstep(1.90, 2.30, jt);
airborne = sstep(0.48, 0.72, jt) - sstep(2.00, 2.45, jt);
} else if (jt >= JUMP_END){
jumpStart = t + 2.5 + Math.random()*4.5;
}
if (!reduceMotion && t > nextPause){
gaitTarget = Math.random() < 0.45 ? 0.05 : 0.8 + Math.random()*0.7;
nextPause = t + 1.5 + Math.random()*3;
}
gaitSpeed += (gaitTarget - gaitSpeed) * 0.03;
const stride = gaitSpeed * (1 - airborne);
const walk = (animate.wt = (animate.wt||0) + stride * 0.05);
walkLegs.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) * (1-airborne) - L.side*0.35*airborne;
L.femur.rotation.z = L.baseFemur + L.side * (0.30 * ease * (1-airborne) + 0.55*airborne);
L.knee.rotation.z = L.baseKnee + L.side * (0.28 * ease * (1-airborne) + 0.45*airborne);
});
hindLegs.forEach(L=>{
const s = L.side;
const breathe = Math.sin(t*1.6 + s)*0.02;
L.root.rotation.y = L.baseYaw + s*0.16*hindExt;
L.femur.rotation.z = L.baseFem - s*0.46*hindExt + s*breathe;
L.knee.rotation.z = L.baseKnee + s*(hindExt > 0 ? 2.28*hindExt : 0.85*hindExt);
});
antennaJoints.forEach(({joints, side})=>{
for (let i=0;i<joints.length;i++){
const tt = i/joints.length;
const j = joints[i];
const damp = 1 - airborne*0.7;
j.rotation.x = j.userData.bx + Math.sin(t*2.4 - i*0.4) * 0.05 * tt * damp - airborne*0.05*tt;
j.rotation.y = j.userData.by + Math.sin(t*1.9 - i*0.45 + side*1.3) * 0.07 * tt * damp;
}
});
palps.forEach(({palp, side})=>{ palp.rotation.x = Math.sin(t*7 + side)*0.22; });
const beat = Math.sin(t*42) * wingOpen;
tegmina.forEach(({flap, side})=>{
flap.rotation.z = side * (0.02 + wingOpen*0.55 + beat*0.30) + Math.sin(t*1.4 + side)*0.012*(1-wingOpen);
});
hindWings.forEach(({flap, openG, side})=>{
openG.scale.y = 0.04 + 0.96*wingOpen;
flap.rotation.z = side * (wingOpen*0.30 + beat*0.42);
flap.visible = wingOpen > 0.01;
});
const busy = Math.min(1, gaitSpeed);
locust.position.y = Math.sin(walk*2)*0.025*busy - 0.02 + lift;
locust.position.x = Math.sin(walk)*0.015*busy;
locust.position.z = surge;
locust.rotation.z = Math.sin(walk*2)*0.015*busy + beat*0.03;
locust.rotation.x = Math.sin(t*0.8)*0.015 + pitch;
if (!dragging && !reduceMotion) cam.autoYaw += 0.0014 + gaitSpeed*0.0003;
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(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(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>