GIRAFFE WEEVIL - 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: Giraffe-Necked Weevil</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, weevil;
const legParts = [];
const antennaParts = [];
let neck = null; // { lower, upper, head } articulated groups
let elytra = null; // for the occasional wing-case flick
let abdomenMesh = null;
function updateSize(){
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
}
const cam = { dist: 4.0, yaw: 0.55, pitch: 0.22, targetYaw: 0.55, targetPitch: 0.22, 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.35,
Math.cos(total)*Math.cos(p)*cam.dist
);
camera.lookAt(0, 0.15, 0.0);
}
function matte(color, opts={}){
return new THREE.MeshStandardMaterial(Object.assign({
color, roughness:0.55, metalness:0.12, side:THREE.DoubleSide
}, opts));
}
function fuzz(geo, amp, freq){
const pos = geo.attributes.position, v = new THREE.Vector3();
for (let i=0;i<pos.count;i++){
v.fromBufferAttribute(pos, i);
const n = (Math.sin(v.x*freq)*Math.cos(v.y*freq*1.3) + Math.sin(v.z*freq*1.7))*0.5;
const d = 1 + amp*n + amp*0.4*(Math.random()-0.5);
v.multiplyScalar(d);
pos.setXYZ(i, v.x, v.y, v.z);
}
pos.needsUpdate = true;
geo.computeVertexNormals();
}
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;
}
// ---- Giraffe-necked weevil (Trachelophorus giraffa) ----
// Madagascar's crane-necked beetle: glossy jet-black body and an absurdly
// long, hinged neck, with brilliant scarlet elytra (wing cases) over the
// abdomen. The neck articulates like a lever arm - it nods and cranes,
// which is exactly what the animation leans into.
const inkMat = matte(0x0a0a0c, { roughness:0.3, metalness:0.35 }); // polished black chitin
const inkMat2 = matte(0x16161a, { roughness:0.38, metalness:0.3 });
const redMat = matte(0xc41e10, { roughness:0.25, metalness:0.2 }); // scarlet elytra
const redDark = matte(0x8f130a, { roughness:0.3, metalness:0.18 });
function makeLeg(side, cfg){
const s = side;
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 coxaEnd = new THREE.Vector3(s*0.02, -0.05, 0.005);
root.add(segmentBetween(new THREE.Vector3(0,0,0), coxaEnd, 0.032, 0.022, inkMat, 8));
const swing = new THREE.Group();
swing.position.copy(coxaEnd);
root.add(swing);
const A = new THREE.Vector3(s*0.05, -cfg.len1, -0.01);
swing.add(segmentBetween(new THREE.Vector3(0,0,0), A, 0.022, 0.014, inkMat2, 8));
const kneeJoint = new THREE.Mesh(new THREE.SphereGeometry(0.015, 8, 6), inkMat);
kneeJoint.position.copy(A);
swing.add(kneeJoint);
const mid = new THREE.Group();
mid.position.copy(A);
mid.rotation.x = cfg.baseMid;
swing.add(mid);
const B = new THREE.Vector3(s*0.035, -cfg.len2, 0.02);
mid.add(segmentBetween(new THREE.Vector3(0,0,0), B, 0.013, 0.007, inkMat, 8));
const tarsus = new THREE.Group();
tarsus.position.copy(B);
tarsus.rotation.x = -0.25;
mid.add(tarsus);
let prev = new THREE.Vector3(0,0,0);
for (let i=0;i<3;i++){
const segLen = cfg.len3 * (i===0 ? 0.45 : 0.28);
const next = new THREE.Vector3(s*0.004, -segLen, 0.008*i);
tarsus.add(segmentBetween(prev, next, 0.007 - i*0.0015, 0.0055 - i*0.0015, i%2 ? inkMat : inkMat2, 6));
prev = next.clone();
}
for (let c=-1; c<=1; c+=2){
const dir = new THREE.Vector3(c*0.3*s, -1, 0.1).normalize();
tarsus.add(segmentBetween(prev, prev.clone().add(dir.multiplyScalar(0.025)), 0.004, 0.001, inkMat, 4));
}
legParts.push({ swing, mid, phase:cfg.phase, swingAmp:cfg.swingAmp, baseMid:cfg.baseMid });
return root;
}
function makeAntenna(side){
// Short elbowed antennae near the eyes, typical of weevils
const grp = new THREE.Group();
grp.position.set(side*0.035, 0.01, 0.06);
const A = new THREE.Vector3(side*0.05, 0.015, 0.05);
grp.add(segmentBetween(new THREE.Vector3(0,0,0), A, 0.008, 0.006, inkMat, 6));
const flagellum = new THREE.Group();
flagellum.position.copy(A);
grp.add(flagellum);
const B = new THREE.Vector3(side*0.05, -0.03, 0.055);
flagellum.add(segmentBetween(new THREE.Vector3(0,0,0), B, 0.006, 0.004, inkMat, 5));
const club = new THREE.Mesh(new THREE.SphereGeometry(0.011, 6, 6), inkMat2);
club.position.copy(B);
flagellum.add(club);
antennaParts.push({ flagellum, phase: side*0.7 });
return grp;
}
function createWeevil(){
const g = new THREE.Group();
// --- Abdomen with scarlet elytra ---
const abdomen = new THREE.Group();
abdomen.position.set(0, 0.02, -0.52);
g.add(abdomen);
const belly = new THREE.Mesh(new THREE.SphereGeometry(1, 24, 20), inkMat2);
belly.scale.set(0.45, 0.38, 0.60);
abdomen.add(belly);
abdomenMesh = belly;
// Wing cases: a scarlet dome raised over the black underside, with the
// rear tips pinched. Hinged at the front so it can flick slightly.
elytra = new THREE.Group();
elytra.position.set(0, 0.06, 0.28); // hinge at the front edge
abdomen.add(elytra);
const caseGeo = new THREE.SphereGeometry(1, 26, 20);
fuzz(caseGeo, 0.008, 9);
const cases = new THREE.Mesh(caseGeo, redMat);
cases.scale.set(0.47, 0.34, 0.62);
cases.position.set(0, 0.02, -0.28);
elytra.add(cases);
// Seam where the two wing cases meet: a thin black blade down the midline
const seam = new THREE.Mesh(new THREE.SphereGeometry(1, 8, 12), inkMat);
seam.scale.set(0.008, 0.345, 0.625);
seam.position.set(0, 0.02, -0.28);
elytra.add(seam);
// Punctation: rows of tiny darker-red pits along each wing case
for (let row=-3; row<=3; row++){
if (row===0) continue;
for (let k=0;k<9;k++){
const fz = -0.62 + (k/8)*0.58;
const fx = row*0.11;
// project roughly onto the ellipsoid surface
const nx = fx/0.47, nz = (fz+0.28)/0.62;
const nySq = 1 - nx*nx - nz*nz;
if (nySq < 0.05) continue;
const fy = Math.sqrt(nySq)*0.34;
const pit = new THREE.Mesh(new THREE.SphereGeometry(0.011, 5, 5), redDark);
pit.position.set(fx, 0.02 + fy, fz);
elytra.add(pit);
}
}
// --- Thorax ---
const thorax = new THREE.Mesh(new THREE.SphereGeometry(1, 20, 16), inkMat);
thorax.scale.set(0.22, 0.20, 0.30);
thorax.position.set(0, 0.06, 0.12);
g.add(thorax);
// --- The neck: the whole point of this beetle ---
// Two long hinged segments, thick at the base and tapering, ending in a
// small head. Each hinge is a group so the crane action reads clearly.
const lower = new THREE.Group();
lower.position.set(0, 0.14, 0.32);
lower.rotation.x = -0.55; // base pose: raked up and forward
g.add(lower);
const L1 = new THREE.Vector3(0, 0.55, 0.10);
lower.add(segmentBetween(new THREE.Vector3(0,0,0), L1, 0.075, 0.048, inkMat, 12));
// collar flare at the base, like the pronotum extending into the neck
const collar = new THREE.Mesh(new THREE.SphereGeometry(0.085, 12, 10), inkMat);
collar.scale.set(1, 0.7, 1.1);
lower.add(collar);
const hinge1 = new THREE.Mesh(new THREE.SphereGeometry(0.05, 10, 8), inkMat2);
hinge1.position.copy(L1);
lower.add(hinge1);
const upper = new THREE.Group();
upper.position.copy(L1);
upper.rotation.x = 0.85; // fold forward at the elbow
lower.add(upper);
const L2 = new THREE.Vector3(0, 0.44, 0.06);
upper.add(segmentBetween(new THREE.Vector3(0,0,0), L2, 0.042, 0.028, inkMat2, 10));
const hinge2 = new THREE.Mesh(new THREE.SphereGeometry(0.032, 10, 8), inkMat);
hinge2.position.copy(L2);
upper.add(hinge2);
// --- Head at the end of the crane ---
const head = new THREE.Group();
head.position.copy(L2);
head.rotation.x = -0.35; // level the head against the fold
upper.add(head);
const skull = new THREE.Mesh(new THREE.SphereGeometry(1, 16, 12), inkMat);
skull.scale.set(0.065, 0.06, 0.095);
skull.position.set(0, 0.01, 0.04);
head.add(skull);
// Bulging eyes on the sides
[-1,1].forEach(sx=>{
const eye = new THREE.Mesh(new THREE.SphereGeometry(0.026, 10, 8), matte(0x000000, { roughness:0.15, metalness:0.5 }));
eye.position.set(sx*0.055, 0.015, 0.05);
head.add(eye);
const hi = new THREE.Mesh(new THREE.SphereGeometry(0.007, 6, 6), matte(0x8a8a90, { roughness:0.2 }));
hi.position.set(sx*0.066, 0.028, 0.062);
head.add(hi);
});
// Short weevil rostrum (snout) with tiny mandibles at the tip
const rostrum = segmentBetween(new THREE.Vector3(0,0,0.10), new THREE.Vector3(0,-0.015,0.20), 0.022, 0.012, inkMat2, 8);
head.add(rostrum);
[-1,1].forEach(sx=>{
const mand = new THREE.Mesh(new THREE.ConeGeometry(0.007, 0.02, 5), inkMat);
mand.position.set(sx*0.008, -0.018, 0.21);
mand.rotation.x = Math.PI/2 - 0.3;
mand.rotation.z = sx*0.4;
head.add(mand);
});
head.add(makeAntenna(-1));
head.add(makeAntenna(1));
neck = { lower, upper, head, baseLower:-0.55, baseUpper:0.85, baseHead:-0.35 };
// --- Legs: six slender black legs ---
const legDefs = [
{ x:0.18, y:-0.04, z:0.22, out:0.65, back:-0.2, len1:0.24, len2:0.26, len3:0.13, swingAmp:0.35, baseMid:0.5 },
{ x:0.22, y:-0.06, z:-0.10, out:0.75, back:0.1, len1:0.28, len2:0.30, len3:0.14, swingAmp:0.30, baseMid:0.45 },
{ x:0.26, y:-0.10, z:-0.48, out:0.70, back:0.45, len1:0.30, len2:0.34, len3:0.15, swingAmp:0.26, baseMid:0.45 }
];
legDefs.forEach((d,p)=>{
g.add(makeLeg( 1, Object.assign({}, d, { phase: p*2.1 })));
g.add(makeLeg(-1, Object.assign({}, d, { phase: p*2.1 + Math.PI })));
});
g.scale.setScalar(1.5);
g.position.y = -0.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();
weevil = createWeevil();
scene.add(weevil);
scene.add(new THREE.HemisphereLight(0xffffff, 0x141020, 0.6));
const key = new THREE.DirectionalLight(0xffffff, 1.4); key.position.set(4,7,6); scene.add(key);
const rim = new THREE.PointLight(0x33ccff, 1.0, 60); rim.position.set(-6,3,-4); scene.add(rim);
const rim2 = new THREE.PointLight(0xff7733, 0.9, 60); rim2.position.set(6,-2,-3); scene.add(rim2);
const glint= new THREE.PointLight(0xffffff, 1.0, 40); glint.position.set(2,4,8); scene.add(glint);
addPointerControls();
animate();
}
// Behavior: alternates between a slow deliberate walk and pausing to crane
// and bob the neck - the signature display of this species. The head
// counter-rotates against both hinges so it stays roughly level, the way
// real animals stabilize their gaze.
let gaitSpeed = 0.5, gaitTarget = 0.5, nextChange = 3;
let elytraFlick = 0, nextFlick = 6;
function animate(){
requestAnimationFrame(animate);
const t = performance.now() * 0.001;
if (t > nextChange){
gaitTarget = Math.random() < 0.4 ? 0.06 : 0.45 + Math.random()*0.5;
nextChange = t + 2.5 + Math.random()*4.0;
}
gaitSpeed += (gaitTarget - gaitSpeed) * 0.02;
const walk = (animate.wt = (animate.wt||0) + gaitSpeed * 0.05);
// Slow six-legged stepping
legParts.forEach(L=>{
const ph = walk + L.phase;
L.swing.rotation.x = L.swingAmp * Math.sin(ph) * Math.min(1, gaitSpeed*2);
const recover = Math.max(0, Math.cos(ph));
const ease = recover*recover*(3 - 2*recover);
L.mid.rotation.x = L.baseMid + 0.4 * ease * Math.min(1, gaitSpeed*2);
});
// Neck crane: bobbing grows when the weevil pauses (display behavior)
if (neck){
const display = Math.max(0, 1.0 - gaitSpeed*1.4);
const slowBob = Math.sin(t*0.7);
const nod = Math.sin(t*1.9)*0.10*display;
neck.lower.rotation.x = neck.baseLower + slowBob*0.10 + nod + Math.sin(walk*2)*0.02*gaitSpeed;
neck.upper.rotation.x = neck.baseUpper - slowBob*0.14 - nod*1.4;
// Head counter-rotates to stay level, plus a curious side-to-side scan
neck.head.rotation.x = neck.baseHead + slowBob*0.05 + nod*0.5;
neck.head.rotation.y = Math.sin(t*0.5)*0.35*display + Math.sin(t*1.3)*0.05;
// The whole neck sways gently with the walk
neck.lower.rotation.z = Math.sin(walk)*0.03*gaitSpeed + Math.sin(t*0.4)*0.02;
}
antennaParts.forEach(A=>{
A.flagellum.rotation.x = Math.sin(t*3.1 + A.phase)*0.25;
A.flagellum.rotation.z = Math.cos(t*2.4 + A.phase)*0.15;
});
// Occasional quick elytra flick - beetles resettle their wing cases
if (t > nextFlick){ elytraFlick = 1; nextFlick = t + 7 + Math.random()*9; }
elytraFlick = Math.max(0, elytraFlick - 0.03);
if (elytra){
const pop = Math.sin(Math.min(1, (1-elytraFlick))*Math.PI);
elytra.rotation.x = -pop*0.12*(elytraFlick>0?1:0);
}
// Breathing pulse on the abdomen
if (abdomenMesh){
const b = 1 + Math.sin(t*1.1)*0.02;
abdomenMesh.scale.set(0.45*b, 0.38*(2-b), 0.60);
}
// Body: gentle sway and settle with the gait, no floating bob
const busy = Math.min(1, gaitSpeed*1.6);
weevil.rotation.z = Math.sin(walk)*0.03*busy;
weevil.rotation.x = Math.sin(walk*2)*0.012*busy + Math.sin(t*0.6)*0.008;
weevil.position.y = -0.15 + Math.abs(Math.sin(walk))*0.015*busy;
weevil.position.x = Math.sin(t*0.09)*0.15*gaitSpeed;
if (!dragging) cam.autoYaw += 0.0012 + 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>