DRAGON - 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>Dragon</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 TAU = Math.PI*2;
const UP = new THREE.Vector3(0,1,0);
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, dragon;
const iriMats = [];
let hueShift = 0;
const NODES = 46;
const nodeGroups = [];
const spinePts = [];
let head, jaw, neckGroup, wingL, wingR, tailFin;
const legs = [];
const horns = [];
function updateSize(){
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
}
const cam = { dist:3.6, yaw:0.7, pitch:0.18, targetYaw:0.7, targetPitch:0.18, targetDist:3.6, autoYaw:0 };
function updateCamera(){
const w = innerWidth;
if (w < 480){ cam.targetDist = Math.min(Math.max(cam.targetDist, 3.6), 20); 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.6,
Math.cos(total)*Math.cos(p)*cam.dist
);
camera.lookAt(0, 0.1, 0);
}
function iri(offset, opts={}){
const { sat=0.85, light=0.5, roughness=0.32, metalness=0.6, 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 scaledSphere(amp, nPhi, nTheta){
const geo = new THREE.SphereGeometry(1, 30, 22);
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.y/len; c = c<-1?-1:c>1?1:c;
const phi = Math.acos(c), th = Math.atan2(v.z, v.x);
const d = 1 - amp*(0.5+0.5*Math.cos(phi*nPhi))*(0.5+0.5*Math.cos(th*nTheta));
v.multiplyScalar(d);
pos.setXYZ(i, v.x, v.y, v.z);
}
pos.needsUpdate = true; geo.computeVertexNormals();
return geo;
}
const SCALE_GEO = scaledSphere(0.14, 7, 12);
function radiusAt(t){
const bulk = 0.17 + 0.62*Math.exp(-Math.pow((t-0.30)/0.20, 2));
const taper = t < 0.45 ? 1 : Math.pow(1-(t-0.45)/0.55, 0.85);
return Math.max(0.035, bulk*taper);
}
function spineAt(t, time){
const z = 3.4 - t*9.0;
let y = Math.exp(-Math.pow((t-0.05)/0.10, 2))*0.75
- Math.pow(Math.max(0, t-0.55)/0.45, 2)*1.15;
let x = 0;
const swim = Math.sin(time*1.5 - t*5.6);
x += swim * (0.12 + t*1.25);
y += Math.sin(time*1.2 - t*4.2) * (0.06 + t*0.34);
return new THREE.Vector3(x, y, z);
}
function makeWing(side){
const shoulder = new THREE.Group();
const memb = iri(0.55, { opacity:0.55, transparent:true, roughness:0.25, metalness:0.45, light:0.6 });
const bone = iri(0.48, { roughness:0.4, metalness:0.55, light:0.55 });
const O = new THREE.Vector3(0,0,0);
const WR = new THREE.Vector3(1.5, 0.30, 0.25);
const F1 = new THREE.Vector3(3.5, 0.55, 0.75);
const F2 = new THREE.Vector3(3.5, 0.20,-0.45);
const F3 = new THREE.Vector3(2.9,-0.10,-1.45);
const F4 = new THREE.Vector3(1.9,-0.25,-2.15);
const BODY = new THREE.Vector3(0.15,-0.10,-1.55);
shoulder.add(segmentBetween(O, WR, 0.16, 0.10, bone, 12));
[F1,F2,F3,F4].forEach((F,i)=>{
const mid = WR.clone().lerp(F, 0.5).add(new THREE.Vector3(0, 0.1, 0));
shoulder.add(segmentBetween(WR, mid, 0.085-i*0.008, 0.055-i*0.006, bone, 10));
shoulder.add(segmentBetween(mid, F, 0.055-i*0.006, 0.014, bone, 10));
if (i===0) shoulder.add(segmentBetween(F, F.clone().add(new THREE.Vector3(0.28,0.1,0.1)), 0.03, 0.002, bone, 6));
});
const panels = [[WR,F1,F2],[WR,F2,F3],[WR,F3,F4],[WR,F4,BODY],[O,WR,BODY]];
panels.forEach(([a,b,c])=>{
const g = new THREE.BufferGeometry();
const bulge = b.clone().add(c).multiplyScalar(0.5).sub(a).multiplyScalar(0.12);
const mid = b.clone().add(c).multiplyScalar(0.5).sub(bulge);
const verts = new Float32Array([
a.x,a.y,a.z, b.x,b.y,b.z, mid.x,mid.y,mid.z,
a.x,a.y,a.z, mid.x,mid.y,mid.z, c.x,c.y,c.z
]);
g.setAttribute('position', new THREE.BufferAttribute(verts,3));
g.computeVertexNormals();
shoulder.add(new THREE.Mesh(g, memb));
});
shoulder.position.set(0.42, 0.34, 0);
const wrap = new THREE.Group();
wrap.add(shoulder);
if (side < 0) wrap.scale.x = -1;
wrap.userData.shoulder = shoulder;
return wrap;
}
function makeLeg(side, cfg){
const m = iri(cfg.hue, { roughness:0.38, metalness:0.55 });
const wrap = new THREE.Group();
const hip = new THREE.Group();
wrap.add(hip);
const A = new THREE.Vector3(0.42, -0.55, 0.10);
const B = new THREE.Vector3(0.60, -1.15,-0.18);
const F = new THREE.Vector3(0.66, -1.45, 0.10);
hip.add(segmentBetween(new THREE.Vector3(0,0,0), A, 0.19, 0.13, m, 12));
const knee = new THREE.Group(); knee.position.copy(A); hip.add(knee);
knee.add(segmentBetween(new THREE.Vector3(0,0,0), B.clone().sub(A), 0.12, 0.075, m, 12));
const ankle = new THREE.Group(); ankle.position.copy(B.clone().sub(A)); knee.add(ankle);
ankle.add(segmentBetween(new THREE.Vector3(0,0,0), F.clone().sub(B), 0.075, 0.055, m, 10));
const foot = F.clone().sub(B);
for (let i=0;i<3;i++){
const a = (i-1)*0.45;
const toe = foot.clone().add(new THREE.Vector3(Math.sin(a)*0.16, -0.05, Math.cos(a)*0.30));
ankle.add(segmentBetween(foot, toe, 0.05, 0.03, m, 8));
ankle.add(segmentBetween(toe, toe.clone().add(new THREE.Vector3(Math.sin(a)*0.10,-0.14,Math.cos(a)*0.16)), 0.028, 0.002, m, 6));
}
if (side < 0) wrap.scale.x = -1;
legs.push({ wrap, hip, knee, ankle, phase:cfg.phase||0 });
return wrap;
}
function makeHead(){
const g = new THREE.Group();
const skin = iri(0.02, { roughness:0.3, metalness:0.6 });
const dark = iri(0.95, { roughness:0.4, metalness:0.5, light:0.4 });
const skull = new THREE.Mesh(SCALE_GEO, skin);
skull.scale.set(0.34, 0.34, 0.52); g.add(skull);
const snout = new THREE.Mesh(SCALE_GEO, skin);
snout.scale.set(0.22, 0.20, 0.42); snout.position.set(0, -0.06, 0.72); g.add(snout);
[-1,1].forEach(sx=>{
const b = new THREE.Mesh(SCALE_GEO, skin);
b.scale.set(0.13,0.09,0.24); b.position.set(sx*0.20, 0.20, 0.26); g.add(b);
const eye = new THREE.Mesh(new THREE.SphereGeometry(0.115, 20, 16), iri(0.16, { roughness:0.12, metalness:0.75, light:0.6 }));
eye.position.set(sx*0.26, 0.12, 0.30); g.add(eye);
const pupil = new THREE.Mesh(new THREE.SphereGeometry(0.05, 12, 10), dark);
pupil.scale.set(0.5,1.25,0.5); pupil.position.set(sx*0.33, 0.12, 0.36); g.add(pupil);
const n = new THREE.Mesh(new THREE.SphereGeometry(0.035,10,8), dark);
n.position.set(sx*0.10, 0.02, 1.06); g.add(n);
const hg = new THREE.Group();
let prev = new THREE.Vector3(sx*0.20, 0.28, 0.05);
const hm = iri(0.72 + (sx>0?0.02:0), { roughness:0.3, metalness:0.7, light:0.55 });
for (let k=0;k<7;k++){
const f = k/7;
const nxt = prev.clone().add(new THREE.Vector3(sx*(0.07+f*0.05), 0.11-f*0.14, -0.22-f*0.05));
hg.add(segmentBetween(prev, nxt, 0.085*(1-f*0.85)+0.012, 0.085*(1-(f+0.14)*0.85)+0.008, hm, 9));
prev = nxt;
}
g.add(hg); horns.push(hg);
g.add(segmentBetween(new THREE.Vector3(sx*0.26,-0.10,0.10), new THREE.Vector3(sx*0.44,-0.22,-0.22), 0.05,0.004, hm, 7));
});
jaw = new THREE.Group();
jaw.position.set(0, -0.14, 0.12);
const jm = new THREE.Mesh(SCALE_GEO, skin);
jm.scale.set(0.20, 0.12, 0.46); jm.position.set(0,-0.06,0.44); jaw.add(jm);
g.add(jaw);
for (let i=0;i<7;i++){
const f = i/6, zz = 0.30 + f*0.72;
[-1,1].forEach(sx=>{
const up = segmentBetween(new THREE.Vector3(sx*0.17,-0.13,zz), new THREE.Vector3(sx*0.17,-0.26,zz), 0.028,0.002, dark, 5);
g.add(up);
const lo = segmentBetween(new THREE.Vector3(sx*0.15,-0.02,zz-0.14), new THREE.Vector3(sx*0.15,0.10,zz-0.14), 0.026,0.002, dark, 5);
jaw.add(lo);
});
}
return g;
}
function createDragon(){
const g = new THREE.Group();
for (let i=0;i<NODES;i++){
const t = i/(NODES-1);
const r = radiusAt(t);
const node = new THREE.Group();
const mat = iri(t*0.9, { light: (i%2 ? 0.54 : 0.48), roughness:0.34, metalness:0.6 });
const body = new THREE.Mesh(SCALE_GEO, mat);
body.scale.set(r*1.02, r*1.12, Math.max(r*1.35, 0.24));
node.add(body);
if (i>2 && i<NODES-2 && i%2===0){
const sp = Math.max(0.05, r*0.95) * (t<0.6?1:0.7);
node.add(segmentBetween(new THREE.Vector3(0, r*0.95, 0), new THREE.Vector3(0, r*0.95+sp, -sp*0.35), sp*0.34, 0.004,
iri(t*0.9+0.5, { roughness:0.3, metalness:0.7, light:0.6 }), 7));
}
if (i>3 && i<NODES-6 && i%2===1){
const pl = new THREE.Mesh(new THREE.BoxGeometry(r*1.15, r*0.16, r*0.9),
iri(t*0.9+0.12, { roughness:0.4, metalness:0.5, light:0.62 }));
pl.position.set(0, -r*0.98, 0); node.add(pl);
}
g.add(node); nodeGroups.push(node);
}
head = makeHead();
nodeGroups[0].add(head);
head.position.set(0, 0.10, 0.42);
const shoulderIdx = Math.round(0.22*(NODES-1));
wingR = makeWing(1); wingL = makeWing(-1);
nodeGroups[shoulderIdx].add(wingR);
nodeGroups[shoulderIdx].add(wingL);
const frontIdx = Math.round(0.32*(NODES-1));
const rearIdx = Math.round(0.50*(NODES-1));
nodeGroups[frontIdx].add(makeLeg( 1, { hue:0.30, phase:0 }));
nodeGroups[frontIdx].add(makeLeg(-1, { hue:0.30, phase:Math.PI }));
nodeGroups[rearIdx ].add(makeLeg( 1, { hue:0.40, phase:Math.PI }));
nodeGroups[rearIdx ].add(makeLeg(-1, { hue:0.40, phase:0 }));
tailFin = new THREE.Group();
const fm = iri(0.80, { opacity:0.6, transparent:true, roughness:0.25, metalness:0.5, light:0.6 });
const fs = new THREE.Shape();
fs.moveTo(0,0);
fs.bezierCurveTo(0.15, 0.55, 0.45, 0.85, 0.80, 0.55);
fs.bezierCurveTo(0.55, 0.30, 0.42,-0.30, 0.72,-0.72);
fs.bezierCurveTo(0.36,-0.60, 0.12,-0.35, 0,0);
const fin = new THREE.Mesh(new THREE.ShapeGeometry(fs, 24), fm);
fin.rotation.y = Math.PI/2;
tailFin.add(fin);
nodeGroups[NODES-3].add(tailFin);
g.scale.setScalar(0.45);
return g;
}
function init(){
initBackground(); updateSize();
scene = new THREE.Scene(); scene.background = null;
camera = new THREE.PerspectiveCamera(46, innerWidth/innerHeight, 0.1, 1000);
updateCamera();
dragon = createDragon();
scene.add(dragon);
scene.add(new THREE.HemisphereLight(0xffffff, 0x101018, 0.55));
const key = new THREE.DirectionalLight(0xffffff, 1.35); key.position.set(5,8,6); scene.add(key);
const rim = new THREE.PointLight(0x33ccff, 1.6, 90); rim.position.set(-8,4,-5); scene.add(rim);
const rim2 = new THREE.PointLight(0xff33aa, 1.4, 90); rim2.position.set(8,-2,-4); scene.add(rim2);
const under= new THREE.PointLight(0x8855ff, 0.6, 60); under.position.set(0,-6,3); scene.add(under);
const glint= new THREE.PointLight(0xffffff, 1.0, 70); glint.position.set(3,7,10); scene.add(glint);
addPointerControls(); animate();
}
const _m = new THREE.Matrix4(), _x = new THREE.Vector3(), _y = new THREE.Vector3(), _z = new THREE.Vector3(), _q = new THREE.Quaternion();
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); }
for (let i=0;i<NODES;i++) spinePts[i] = spineAt(i/(NODES-1), t);
for (let i=0;i<NODES;i++){
const a = spinePts[Math.max(0,i-1)], b = spinePts[Math.min(NODES-1,i+1)];
_z.subVectors(a, b).normalize();
_x.crossVectors(UP, _z).normalize();
if (_x.lengthSq() < 1e-6) _x.set(1,0,0);
_y.crossVectors(_z, _x);
_m.makeBasis(_x, _y, _z);
_q.setFromRotationMatrix(_m);
const bank = -Math.cos(t*1.5 - (i/(NODES-1))*5.6) * 0.30;
_q.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0,0,1), bank));
nodeGroups[i].position.copy(spinePts[i]);
nodeGroups[i].quaternion.copy(_q);
}
const flap = Math.sin(t*2.3);
const twist = Math.sin(t*2.3 - 0.9)*0.35;
if (wingR){ wingR.userData.shoulder.rotation.z = 0.25 + flap*0.85; wingR.userData.shoulder.rotation.x = twist; }
if (wingL){ wingL.userData.shoulder.rotation.z = 0.25 + flap*0.85; wingL.userData.shoulder.rotation.x = twist; }
for (let i=0;i<legs.length;i++){
const L = legs[i];
L.hip.rotation.x = -0.35 + Math.sin(t*1.4 + L.phase)*0.12;
L.knee.rotation.x = 0.75 + Math.sin(t*1.4 + L.phase + 0.6)*0.14;
L.ankle.rotation.x = -0.30 + Math.sin(t*1.4 + L.phase + 1.2)*0.10;
}
if (head){
head.rotation.y = Math.sin(t*0.7)*0.25;
head.rotation.x = Math.sin(t*0.9)*0.12;
}
if (jaw){
const roar = Math.max(0, Math.sin(t*0.55 - 1.2));
jaw.rotation.x = 0.05 + Math.pow(roar, 3)*0.55;
}
if (tailFin) tailFin.rotation.z = Math.sin(t*1.5)*0.25;
if (dragon){
dragon.rotation.y = Math.sin(t*0.22)*0.5 + t*0.06;
dragon.position.y = Math.sin(t*0.9)*0.22 - flap*0.12;
dragon.rotation.z = Math.sin(t*0.5)*0.06;
}
if (!dragging) cam.autoYaw += 0.0012;
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(2, Math.min(24, cam.targetDist + e.deltaY*0.012)); }, {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(2, Math.min(24, 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>