Custom Code Experiment •
ThreeJS: Locust
Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ThreeJS 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; }
body { background: #000; overflow: hidden; height: 100vh; }
#bgCanvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; }
#animationCanvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 2; cursor: grab; }
#animationCanvas.dragging { cursor: grabbing; }
#hint { position: absolute; bottom: 18px; left: 50%; transform: translateX(-50%); z-index: 10;
color: rgba(180, 210, 80, 0.55); font-family: monospace; font-size: 12px; letter-spacing: 0.12em;
pointer-events: none; transition: opacity 2s; user-select: none; }
</style>
</head>
<body>
<canvas id="bgCanvas"></canvas>
<canvas id="animationCanvas"></canvas>
<script>
const colors = {
body: 0x8a7a30,
dark: 0x2e2800,
light: 0xc8b84a,
tegmen: 0xa09030,
hindwing:0x70c040,
vein: 0xd8e878,
eye: 0xd0e000,
spine: 0xe8d060,
pronotum:0x6a5e20,
stripe: 0xf0e870,
tarsus: 0x3a3010,
};
const bgCanvas = document.getElementById('bgCanvas');
const bgCtx = bgCanvas.getContext('2d');
const px = 7;
let cols, rows, pixels = [];
function initBackground() {
bgCanvas.width = window.innerWidth;
bgCanvas.height = window.innerHeight;
cols = Math.ceil(bgCanvas.width / px);
rows = Math.ceil(bgCanvas.height / px);
pixels = [];
for (let i = 0; i < cols * rows; i++)
pixels.push({ b: Math.random() * 0.3, speed: 0.001 + Math.random() * 0.003 });
}
function animateBackground() {
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
const p = pixels[y * cols + x];
p.b += p.speed;
if (p.b > 0.4) p.b = 0;
bgCtx.fillStyle = `rgb(${Math.floor(p.b*44)},${Math.floor(p.b*38)},${Math.floor(p.b*8)})`;
bgCtx.fillRect(x * px, y * px, px, px);
}
}
requestAnimationFrame(animateBackground);
}
const renderer = new THREE.WebGLRenderer({
canvas: document.getElementById('animationCanvas'),
antialias: true, alpha: true
});
let scene, camera, locust;
let tegmenL, tegmenR, hindwingL, hindwingR;
let hindLegL, hindLegR;
let legGroups = [];
const orbit = {
theta: 0.5, phi: 1.15, radius: 9,
isDragging: false, lastX: 0, lastY: 0,
autoRotate: true, autoRotateSpeed: 0.003,
velTheta: 0, velPhi: 0,
};
const PHI_MIN = 0.05, PHI_MAX = Math.PI * 0.95;
const RADIUS_MIN = 3, RADIUS_MAX = 22;
function setCameraFromOrbit() {
const x = orbit.radius * Math.sin(orbit.phi) * Math.sin(orbit.theta);
const y = orbit.radius * Math.cos(orbit.phi);
const z = orbit.radius * Math.sin(orbit.phi) * Math.cos(orbit.theta);
camera.position.set(x, y, z);
camera.lookAt(0, 0, 0);
camera.updateProjectionMatrix();
}
function updateSize() {
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
}
function updateCamera() {
const w = window.innerWidth;
orbit.radius = w < 480 ? 13 : w < 768 ? 11 : 9;
camera.fov = 52;
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
setCameraFromOrbit();
}
function mat(color) {
return new THREE.MeshBasicMaterial({ color, wireframe: true });
}
function makeTegmen(side) {
const s = side, group = new THREE.Group();
const W = 0.38;
const L = 2.20;
const shape = new THREE.Shape();
shape.moveTo(0, 0);
shape.bezierCurveTo(s*W*0.55, 0.05, s*W*0.90, 0.10, s*W, L*0.35);
shape.bezierCurveTo(s*W*1.02, L*0.55, s*W*0.98, L*0.80, s*W*0.60, L);
shape.bezierCurveTo(s*W*0.40, L*1.02, s*W*0.10, L*0.98, 0, L*0.95);
shape.bezierCurveTo(s*(-W*0.08), L*0.70, s*(-W*0.06), L*0.30, 0, 0);
const geo = new THREE.ShapeGeometry(shape, 16);
group.add(new THREE.Mesh(geo, mat(colors.tegmen)));
group.add(new THREE.LineSegments(
new THREE.EdgesGeometry(geo),
new THREE.LineBasicMaterial({ color: colors.vein })
));
const veinDefs = [
{ xFrac: 0.95, label: 'costa', r: 0.010 },
{ xFrac: 0.78, label: 'radius', r: 0.008 },
{ xFrac: 0.55, label: 'medial', r: 0.007 },
{ xFrac: 0.30, label: 'cubitus', r: 0.006 },
{ xFrac: 0.05, label: 'anal', r: 0.005 },
];
veinDefs.forEach(v => {
const xBase = s * W * v.xFrac;
const pts = [
new THREE.Vector3(xBase * 0.1, 0.005, 0.05),
new THREE.Vector3(xBase * 0.7, 0.005, L * 0.4),
new THREE.Vector3(xBase * 0.9, 0.005, L * 0.75),
new THREE.Vector3(xBase * 0.75, 0.005, L * 0.96),
];
group.add(new THREE.Mesh(
new THREE.TubeGeometry(new THREE.CatmullRomCurve3(pts), 8, v.r, 4),
mat(colors.vein)
));
});
const cvPositions = [0.30, 0.52, 0.68, 0.80, 0.89, 0.95];
cvPositions.forEach(zf => {
const zp = zf * L;
const pts = [
new THREE.Vector3(s*W*0.90, 0.005, zp),
new THREE.Vector3(s*W*0.55, 0.005, zp + 0.06),
new THREE.Vector3(s*W*0.28, 0.005, zp + 0.04),
];
group.add(new THREE.Mesh(
new THREE.TubeGeometry(new THREE.CatmullRomCurve3(pts), 4, 0.005, 4),
mat(colors.vein)
));
});
return group;
}
function makeHindwing(side) {
const s = side, group = new THREE.Group();
const W = 0.18;
const L = 2.00;
const shape = new THREE.Shape();
shape.moveTo(0, 0);
shape.bezierCurveTo(s*W*0.6, 0.02, s*W, L*0.3, s*W*0.95, L*0.7);
shape.bezierCurveTo(s*W*0.85, L*0.9, s*W*0.50, L, s*W*0.15, L*0.96);
shape.bezierCurveTo(s*(-W*0.05), L*0.65, s*(-W*0.05), L*0.25, 0, 0);
const geo = new THREE.ShapeGeometry(shape, 12);
group.add(new THREE.Mesh(geo, mat(colors.hindwing)));
group.add(new THREE.LineSegments(
new THREE.EdgesGeometry(geo),
new THREE.LineBasicMaterial({ color: colors.vein })
));
for (let fi = 0; fi < 4; fi++) {
const xf = s * W * (0.2 + fi * 0.18);
const pts = [
new THREE.Vector3(xf * 0.2, 0.004, L * 0.05),
new THREE.Vector3(xf, 0.004, L * 0.55),
new THREE.Vector3(xf * 0.9, 0.004, L * 0.92),
];
group.add(new THREE.Mesh(
new THREE.TubeGeometry(new THREE.CatmullRomCurve3(pts), 6, 0.005, 4),
mat(colors.vein)
));
}
return group;
}
function makeHindLeg(side) {
const s = side, group = new THREE.Group();
group.add(new THREE.Mesh(
new THREE.TubeGeometry(new THREE.CatmullRomCurve3([
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(s*0.14, -0.06, 0.04),
]), 3, 0.025, 5),
mat(colors.dark)
));
const femPts = [
new THREE.Vector3(s*0.14, -0.06, 0.04),
new THREE.Vector3(s*0.44, -0.10, 0.30),
new THREE.Vector3(s*0.70, -0.18, 0.60),
new THREE.Vector3(s*0.82, -0.30, 0.80),
];
group.add(new THREE.Mesh(
new THREE.TubeGeometry(new THREE.CatmullRomCurve3(femPts), 10, 0.060, 8),
mat(colors.body)
));
const keelPts = femPts.map((p, i) =>
new THREE.Vector3(p.x + s*0.01, p.y + 0.07, p.z)
);
group.add(new THREE.Mesh(
new THREE.TubeGeometry(new THREE.CatmullRomCurve3(keelPts), 8, 0.018, 4),
mat(colors.light)
));
const tibPts = [
new THREE.Vector3(s*0.82, -0.30, 0.80),
new THREE.Vector3(s*0.90, -0.65, 1.20),
new THREE.Vector3(s*0.88, -1.05, 1.55),
new THREE.Vector3(s*0.82, -1.40, 1.80),
];
group.add(new THREE.Mesh(
new THREE.TubeGeometry(new THREE.CatmullRomCurve3(tibPts), 10, 0.022, 6),
mat(colors.light)
));
for (let si = 0; si < 7; si++) {
const t = si / 6;
const base = new THREE.Vector3(
s*0.82 + (s*0.82-s*0.82)*t,
-0.30 + (-1.40+0.30)*t,
0.80 + (1.80-0.80)*t
);
[0.06, -0.06].forEach(offset => {
const spPts = [
base.clone(),
new THREE.Vector3(base.x + s*0.04, base.y + offset, base.z + 0.08),
];
group.add(new THREE.Mesh(
new THREE.TubeGeometry(new THREE.CatmullRomCurve3(spPts), 3, 0.008, 3),
mat(colors.spine)
));
});
}
let tPos = new THREE.Vector3(s*0.82, -1.40, 1.80);
for (let ti = 0; ti < 3; ti++) {
const tEnd = new THREE.Vector3(tPos.x + s*0.08, tPos.y - 0.14 + ti*0.03, tPos.z + 0.14);
group.add(new THREE.Mesh(
new THREE.TubeGeometry(new THREE.CatmullRomCurve3([tPos, tEnd]), 4, 0.018 - ti*0.004, 5),
mat(ti % 2 === 0 ? colors.dark : colors.tarsus)
));
const padGeo = new THREE.SphereGeometry(0.020, 6, 5);
const pad = new THREE.Mesh(padGeo, mat(colors.spine));
pad.position.copy(tEnd);
group.add(pad);
tPos = tEnd;
}
const clPts = [tPos.clone(), new THREE.Vector3(tPos.x + s*0.06, tPos.y - 0.06, tPos.z + 0.06)];
group.add(new THREE.Mesh(
new THREE.TubeGeometry(new THREE.CatmullRomCurve3(clPts), 3, 0.010, 4),
mat(colors.dark)
));
return group;
}
function makeWalkingLeg(side, index) {
const s = side, group = new THREE.Group();
const zOff = index * 0.40;
group.add(new THREE.Mesh(
new THREE.TubeGeometry(new THREE.CatmullRomCurve3([
new THREE.Vector3(0,0,0),
new THREE.Vector3(s*0.15,-0.08,0.02),
]), 3, 0.022, 5),
mat(colors.dark)
));
const fPts = [
new THREE.Vector3(s*0.15,-0.08,0.02),
new THREE.Vector3(s*0.38,-0.32,0.04),
new THREE.Vector3(s*0.52,-0.52,0.02),
];
group.add(new THREE.Mesh(
new THREE.TubeGeometry(new THREE.CatmullRomCurve3(fPts), 6, 0.026, 6),
mat(colors.body)
));
const tPts = [
new THREE.Vector3(s*0.52,-0.52,0.02),
new THREE.Vector3(s*0.72,-0.82,-0.02),
new THREE.Vector3(s*0.80,-1.10,-0.06),
];
group.add(new THREE.Mesh(
new THREE.TubeGeometry(new THREE.CatmullRomCurve3(tPts), 6, 0.018, 6),
mat(colors.light)
));
for (let si = 0; si < 3; si++) {
const t = 0.3 + si * 0.25;
const bx = s*0.52 + (s*0.80 - s*0.52)*t;
const by = -0.52 + (-1.10+0.52)*t;
const bz = 0.02 + (-0.06-0.02)*t;
group.add(new THREE.Mesh(
new THREE.TubeGeometry(new THREE.CatmullRomCurve3([
new THREE.Vector3(bx, by, bz),
new THREE.Vector3(bx + s*0.04, by + 0.06, bz + 0.04),
]), 3, 0.006, 3),
mat(colors.spine)
));
}
let tp = new THREE.Vector3(s*0.80,-1.10,-0.06);
for (let ti = 0; ti < 3; ti++) {
const te = new THREE.Vector3(tp.x + s*0.07, tp.y - 0.10 + ti*0.02, tp.z - 0.03);
group.add(new THREE.Mesh(
new THREE.TubeGeometry(new THREE.CatmullRomCurve3([tp,te]), 3, 0.013-ti*0.003, 4),
mat(ti%2===0?colors.dark:colors.tarsus)
));
tp = te;
}
return group;
}
function createLocust() {
const group = new THREE.Group();
legGroups = [];
const abdData = [
{ r:0.22, z:0.05 }, { r:0.24, z:0.35 }, { r:0.23, z:0.64 },
{ r:0.21, z:0.92 }, { r:0.18, z:1.18 }, { r:0.15, z:1.42 },
{ r:0.11, z:1.62 }, { r:0.07, z:1.78 },
];
abdData.forEach((s, i) => {
const next = abdData[i+1];
const rTop = next ? next.r : s.r * 0.5;
const isAlt = i % 2 === 0;
const geo = new THREE.CylinderGeometry(rTop*0.9, s.r, 0.30, 14, 1);
const seg = new THREE.Mesh(geo, mat(isAlt ? colors.body : colors.dark));
seg.rotation.x = Math.PI * 0.5;
seg.position.set(0, 0, s.z);
group.add(seg);
[-1,1].forEach(side => {
const spGeo = new THREE.TorusGeometry(0.020, 0.006, 5, 10);
const sp = new THREE.Mesh(spGeo, mat(colors.light));
sp.position.set(side * s.r * 0.92, 0.04, s.z);
group.add(sp);
});
if (isAlt) {
const ridgeGeo = new THREE.BoxGeometry(0.04, 0.025, 0.28);
const ridge = new THREE.Mesh(ridgeGeo, mat(colors.stripe));
ridge.position.set(0, s.r + 0.01, s.z);
group.add(ridge);
}
});
[-1,1].forEach(side => {
const cPts = [
new THREE.Vector3(side*0.06, 0, 1.90),
new THREE.Vector3(side*0.10, 0.04, 2.04),
new THREE.Vector3(side*0.12, 0.06, 2.14),
];
group.add(new THREE.Mesh(
new THREE.TubeGeometry(new THREE.CatmullRomCurve3(cPts), 5, 0.012, 4),
mat(colors.dark)
));
});
const mGeo = new THREE.CylinderGeometry(0.24, 0.28, 0.72, 14, 2);
const meso = new THREE.Mesh(mGeo, mat(colors.body));
meso.rotation.x = Math.PI * 0.5;
meso.position.set(0, 0.04, -0.42);
group.add(meso);
const proGeo = new THREE.CylinderGeometry(0.22, 0.30, 0.55, 14, 3);
const pro = new THREE.Mesh(proGeo, mat(colors.pronotum));
pro.rotation.x = Math.PI * 0.5;
pro.position.set(0, 0.06, -0.86);
group.add(pro);
const keelPts = [
new THREE.Vector3(0, 0.30, -0.62),
new THREE.Vector3(0, 0.32, -0.86),
new THREE.Vector3(0, 0.30, -1.10),
];
group.add(new THREE.Mesh(
new THREE.TubeGeometry(new THREE.CatmullRomCurve3(keelPts), 6, 0.022, 5),
mat(colors.stripe)
));
[-1,1].forEach(side => {
const cPts = [
new THREE.Vector3(side*0.20, 0.14, -0.64),
new THREE.Vector3(side*0.24, 0.10, -0.86),
new THREE.Vector3(side*0.20, 0.14, -1.08),
];
group.add(new THREE.Mesh(
new THREE.TubeGeometry(new THREE.CatmullRomCurve3(cPts), 5, 0.012, 4),
mat(colors.light)
));
});
const headGeo = new THREE.SphereGeometry(0.24, 16, 14);
const head = new THREE.Mesh(headGeo, mat(colors.body));
head.scale.set(0.85, 1.10, 0.95);
head.position.set(0, 0.04, -1.28);
group.add(head);
const fastGeo = new THREE.BoxGeometry(0.28, 0.08, 0.16);
const fast = new THREE.Mesh(fastGeo, mat(colors.pronotum));
fast.position.set(0, 0.24, -1.28);
group.add(fast);
[-1,1].forEach(side => {
const eyeGeo = new THREE.SphereGeometry(0.13, 14, 12);
const eye = new THREE.Mesh(eyeGeo, mat(colors.eye));
eye.scale.set(0.65, 1.0, 0.80);
eye.position.set(side*0.20, 0.08, -1.28);
group.add(eye);
for (let fi = 0; fi < 8; fi++) {
const ang = (fi/8) * Math.PI * 2;
const fGeo = new THREE.CircleGeometry(0.028, 6);
const facet = new THREE.Mesh(fGeo, mat(colors.stripe));
facet.position.set(
side*(0.22 + Math.cos(ang)*0.04),
0.08 + Math.sin(ang)*0.06,
-1.26
);
group.add(facet);
}
});
const fronsGeo = new THREE.BoxGeometry(0.26, 0.28, 0.08);
const frons = new THREE.Mesh(fronsGeo, mat(colors.light));
frons.position.set(0, 0.00, -1.48);
group.add(frons);
const clyGeo = new THREE.BoxGeometry(0.22, 0.12, 0.06);
const cly = new THREE.Mesh(clyGeo, mat(colors.pronotum));
cly.position.set(0, -0.14, -1.50);
group.add(cly);
[-1,1].forEach(side => {
const mPts = [
new THREE.Vector3(side*0.08, -0.18, -1.50),
new THREE.Vector3(side*0.16, -0.24, -1.60),
new THREE.Vector3(side*0.18, -0.28, -1.68),
];
group.add(new THREE.Mesh(
new THREE.TubeGeometry(new THREE.CatmullRomCurve3(mPts), 5, 0.020, 5),
mat(colors.dark)
));
});
[-1,1].forEach(side => {
const antSegs = 20;
let aPos = new THREE.Vector3(side*0.12, 0.26, -1.38);
for (let ai = 0; ai < antSegs; ai++) {
const t = ai / antSegs;
const spread = side * (0.04 + t * 0.06);
const aEnd = new THREE.Vector3(
aPos.x + spread,
aPos.y + 0.05 - t*0.02,
aPos.z - 0.12 - t*0.02
);
const r = 0.010 - t*0.007;
if (r > 0.002) {
group.add(new THREE.Mesh(
new THREE.TubeGeometry(new THREE.CatmullRomCurve3([aPos.clone(), aEnd.clone()]), 3, r, 4),
mat(ai%2===0 ? colors.dark : colors.light)
));
}
if (ai % 3 === 0) {
const nodeGeo = new THREE.SphereGeometry(0.014 - t*0.008, 5, 4);
const node = new THREE.Mesh(nodeGeo, mat(colors.body));
node.position.copy(aEnd);
group.add(node);
}
aPos = aEnd;
}
});
tegmenL = new THREE.Group();
tegmenL.position.set(-0.10, 0.24, -0.72);
tegmenL.rotation.z = 0.08;
tegmenL.rotation.x = 0.04;
tegmenL.add(makeTegmen(-1));
group.add(tegmenL);
tegmenR = new THREE.Group();
tegmenR.position.set( 0.10, 0.24, -0.72);
tegmenR.rotation.z = -0.08;
tegmenR.rotation.x = 0.04;
tegmenR.add(makeTegmen(1));
group.add(tegmenR);
hindwingL = new THREE.Group();
hindwingL.position.set(-0.08, 0.21, -0.70);
hindwingL.rotation.z = 0.06;
hindwingL.rotation.x = 0.05;
hindwingL.add(makeHindwing(-1));
group.add(hindwingL);
hindwingR = new THREE.Group();
hindwingR.position.set( 0.08, 0.21, -0.70);
hindwingR.rotation.z = -0.06;
hindwingR.rotation.x = 0.05;
hindwingR.add(makeHindwing(1));
group.add(hindwingR);
[-1,1].forEach(side => {
const fl = makeWalkingLeg(side, 0);
fl.position.set(side*0.28, -0.06, -0.92);
group.add(fl);
legGroups.push({ g: fl, side, li: 0, isHind: false });
const ml = makeWalkingLeg(side, 1);
ml.position.set(side*0.28, -0.06, -0.50);
group.add(ml);
legGroups.push({ g: ml, side, li: 1, isHind: false });
});
hindLegL = makeHindLeg(-1);
hindLegL.position.set(-0.26, 0.00, -0.08);
group.add(hindLegL);
hindLegR = makeHindLeg(1);
hindLegR.position.set( 0.26, 0.00, -0.08);
group.add(hindLegR);
group.position.set(0, 0.4, 0.6);
return group;
}
function initOrbitControls() {
const canvas = document.getElementById('animationCanvas');
const hint = document.getElementById('hint');
function onDragStart(x, y) {
orbit.isDragging = true; orbit.autoRotate = false;
orbit.lastX = x; orbit.lastY = y;
orbit.velTheta = 0; orbit.velPhi = 0;
canvas.classList.add('dragging');
hint.style.opacity = '0';
}
function onDragMove(x, y) {
if (!orbit.isDragging) return;
const dx = x - orbit.lastX, dy = y - orbit.lastY;
orbit.velTheta = -dx * 0.005; orbit.velPhi = -dy * 0.005;
orbit.theta += orbit.velTheta;
orbit.phi = Math.max(PHI_MIN, Math.min(PHI_MAX, orbit.phi + orbit.velPhi));
orbit.lastX = x; orbit.lastY = y;
setCameraFromOrbit();
}
function onDragEnd() {
orbit.isDragging = false;
canvas.classList.remove('dragging');
setTimeout(() => { orbit.autoRotate = true; }, 2000);
}
canvas.addEventListener('mousedown', e => onDragStart(e.clientX, e.clientY));
window.addEventListener('mousemove', e => onDragMove(e.clientX, e.clientY));
window.addEventListener('mouseup', () => onDragEnd());
canvas.addEventListener('touchstart', e => { e.preventDefault(); onDragStart(e.touches[0].clientX, e.touches[0].clientY); }, { passive: false });
window.addEventListener('touchmove', e => { e.preventDefault(); onDragMove(e.touches[0].clientX, e.touches[0].clientY); }, { passive: false });
window.addEventListener('touchend', () => onDragEnd());
canvas.addEventListener('wheel', e => {
e.preventDefault();
orbit.radius = Math.max(RADIUS_MIN, Math.min(RADIUS_MAX, orbit.radius + e.deltaY * 0.01));
setCameraFromOrbit();
}, { passive: false });
setTimeout(() => { hint.style.opacity = '0'; }, 3000);
}
function init() {
initBackground();
animateBackground();
updateSize();
scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000);
camera = new THREE.PerspectiveCamera(52, window.innerWidth / window.innerHeight, 0.1, 1000);
updateCamera();
locust = createLocust();
scene.add(locust);
scene.add(new THREE.AmbientLight(0xd0c870, 0.5));
const pl1 = new THREE.PointLight(0xffe060, 1.8, 100);
pl1.position.set(5, 6, 3); scene.add(pl1);
const pl2 = new THREE.PointLight(0x80d040, 0.7, 100);
pl2.position.set(-4, -2, 2); scene.add(pl2);
const pl3 = new THREE.PointLight(0xfff0a0, 0.5, 100);
pl3.position.set(0, 3, -10); scene.add(pl3);
animate();
initOrbitControls();
}
function animate() {
requestAnimationFrame(animate);
const t = Date.now() * 0.001;
if (!orbit.isDragging) {
if (orbit.autoRotate) {
orbit.theta += orbit.autoRotateSpeed;
} else {
orbit.velTheta *= 0.92; orbit.velPhi *= 0.92;
orbit.theta += orbit.velTheta;
orbit.phi = Math.max(PHI_MIN, Math.min(PHI_MAX, orbit.phi + orbit.velPhi));
}
setCameraFromOrbit();
}
if (locust) {
legGroups.forEach(({ g, side, li }) => {
const phase = li * 1.2 + (side > 0 ? 0.8 : 0);
g.rotation.x = Math.sin(t * 0.8 + phase) * 0.012;
g.rotation.z = side * (0.04 + Math.sin(t * 0.6 + phase) * 0.010);
});
if (hindLegL) hindLegL.rotation.x = Math.sin(t * 0.7) * 0.010;
if (hindLegR) hindLegR.rotation.x = Math.sin(t * 0.7 + 0.4) * 0.010;
}
renderer.render(scene, camera);
}
window.addEventListener('resize', () => {
updateSize(); initBackground();
if (camera) updateCamera();
});
window.addEventListener('load', init);
</script>
</body>
</html>