← Mike Higgins Home
Custom Code Experiment •

Bees Background

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>Bees Background</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body { width: 100vw; height: 100vh; overflow: hidden; background: #0d0700; }
  canvas { display: block; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
const canvas = document.getElementById('c');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(innerWidth, innerHeight);
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.4;

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0d0700);
scene.fog = new THREE.FogExp2(0x0d0700, 0.012);

const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 300);
camera.position.set(0, 0, 60);

scene.add(new THREE.AmbientLight(0x201000, 0.8));

const keyLight = new THREE.DirectionalLight(0xffe066, 2.5);
keyLight.position.set(20, 40, 30);
scene.add(keyLight);

const fillLight = new THREE.DirectionalLight(0xff9900, 1.0);
fillLight.position.set(-30, 10, 20);
scene.add(fillLight);

const rimLight = new THREE.DirectionalLight(0xffcc00, 0.8);
rimLight.position.set(0, -20, -20);
scene.add(rimLight);

const yellowMat  = new THREE.MeshPhongMaterial({ color: 0xffbb00, specular: 0xffee88, shininess: 80, emissive: 0x331100 });
const blackMat   = new THREE.MeshPhongMaterial({ color: 0x0a0500, specular: 0x443322, shininess: 40, emissive: 0x000000 });
const darkBrownMat = new THREE.MeshPhongMaterial({ color: 0x2a1400, specular: 0x443322, shininess: 30 });
const eyeMat     = new THREE.MeshPhongMaterial({ color: 0x331100, specular: 0xffffff, shininess: 200 });
const stingerMat = new THREE.MeshPhongMaterial({ color: 0x0a0500, shininess: 20 });
const wingMat    = new THREE.MeshPhongMaterial({
  color: 0xc8eeff, transparent: true, opacity: 0.28,
  side: THREE.DoubleSide, shininess: 200, specular: 0xffffff,
  emissive: 0x112233
});

const S = 2.0;

function makeBee() {
  const g = new THREE.Group();

  const abdGeo = new THREE.SphereGeometry(S*0.62, 14, 10);
  abdGeo.scale(1.0, 0.72, 1.72);
  const abd = new THREE.Mesh(abdGeo, yellowMat.clone());
  abd.position.z = -S*0.52;
  g.add(abd);

  const tipGeo = new THREE.ConeGeometry(S*0.18, S*0.38, 8);
  const tip = new THREE.Mesh(tipGeo, stingerMat.clone());
  tip.position.z = -S*1.32;
  tip.rotation.x = Math.PI/2;
  g.add(tip);

  const stripeData = [
    { z: -S*0.95, col: 0x0a0500, w: 0.13 },
    { z: -S*0.62, col: 0x0a0500, w: 0.13 },
    { z: -S*0.28, col: 0x0a0500, w: 0.10 },
  ];
  stripeData.forEach(sd => {
    const sg = new THREE.TorusGeometry(S*0.52, S*sd.w, 8, 20);
    sg.scale(1.0, 0.45, 1.55);
    const sm = new THREE.Mesh(sg, new THREE.MeshPhongMaterial({
      color: sd.col, specular: 0x221100, shininess: 30, emissive: 0x000000
    }));
    sm.position.z = sd.z;
    sm.rotation.x = Math.PI/2;
    g.add(sm);
  });

  const underGeo = new THREE.SphereGeometry(S*0.61, 14, 6, 0, Math.PI*2, Math.PI/2, Math.PI/2);
  underGeo.scale(1.0, 0.72, 1.72);
  const underMesh = new THREE.Mesh(underGeo, new THREE.MeshPhongMaterial({
    color: 0xcc8800, shininess: 20, side: THREE.BackSide, transparent: true, opacity: 0.5
  }));
  underMesh.position.z = -S*0.52;
  g.add(underMesh);

  const petGeo = new THREE.SphereGeometry(S*0.14, 8, 8);
  petGeo.scale(1, 0.9, 1.1);
  const pet = new THREE.Mesh(petGeo, blackMat.clone());
  pet.position.z = S*0.24;
  g.add(pet);

  const thorGeo = new THREE.SphereGeometry(S*0.46, 14, 10);
  thorGeo.scale(1.0, 0.92, 1.05);
  const thorax = new THREE.Mesh(thorGeo, darkBrownMat.clone());
  thorax.position.z = S*0.62;
  g.add(thorax);

  const tsg = new THREE.TorusGeometry(S*0.43, S*0.09, 8, 18);
  tsg.scale(1.0, 0.5, 0.9);
  const thorStripe = new THREE.Mesh(tsg, new THREE.MeshPhongMaterial({ color: 0xffaa00, shininess: 40 }));
  thorStripe.position.z = S*0.68;
  thorStripe.rotation.x = Math.PI/2;
  g.add(thorStripe);

  const headGeo = new THREE.SphereGeometry(S*0.36, 12, 10);
  headGeo.scale(1.05, 1.0, 0.92);
  const head = new THREE.Mesh(headGeo, blackMat.clone());
  head.position.z = S*1.18;
  g.add(head);

  [-1, 1].forEach(side => {
    const eyeGeo = new THREE.SphereGeometry(S*0.145, 10, 8);
    eyeGeo.scale(0.85, 1.0, 0.6);
    const eye = new THREE.Mesh(eyeGeo, new THREE.MeshPhongMaterial({
      color: 0x3d1a00, specular: 0xff8800, shininess: 180, emissive: 0x110500
    }));
    eye.position.set(side * S*0.27, S*0.14, S*1.22);
    eye.rotation.y = side * 0.45;
    g.add(eye);

    const hlGeo = new THREE.SphereGeometry(S*0.055, 6, 6);
    const hl = new THREE.Mesh(hlGeo, new THREE.MeshPhongMaterial({
      color: 0xffffff, emissive: 0xaaaaaa, shininess: 300
    }));
    hl.position.set(side * S*0.3, S*0.19, S*1.26);
    g.add(hl);
  });

  [-1,1].forEach(side => {
    const mGeo = new THREE.SphereGeometry(S*0.055, 6, 5);
    mGeo.scale(0.7, 0.5, 1.2);
    const m = new THREE.Mesh(mGeo, blackMat.clone());
    m.position.set(side*S*0.13, -S*0.14, S*1.47);
    g.add(m);
  });

  [-1, 1].forEach(side => {
    const seg1Geo = new THREE.CylinderGeometry(S*0.028, S*0.028, S*0.45, 6);
    const seg1 = new THREE.Mesh(seg1Geo, blackMat.clone());
    seg1.position.set(side*S*0.14, S*0.28, S*1.22);
    seg1.rotation.z = side * -0.55;
    seg1.rotation.x = -0.55;
    g.add(seg1);

    const elGeo = new THREE.SphereGeometry(S*0.038, 6, 6);
    const el = new THREE.Mesh(elGeo, blackMat.clone());
    el.position.set(side*S*0.26, S*0.56, S*1.06);
    g.add(el);

    const seg2Geo = new THREE.CylinderGeometry(S*0.022, S*0.022, S*0.52, 6);
    const seg2 = new THREE.Mesh(seg2Geo, blackMat.clone());
    seg2.position.set(side*S*0.38, S*0.8, S*0.94);
    seg2.rotation.z = side * -0.3;
    seg2.rotation.x = -0.85;
    g.add(seg2);

    const clubGeo = new THREE.SphereGeometry(S*0.065, 7, 7);
    const club = new THREE.Mesh(clubGeo, blackMat.clone());
    club.position.set(side*S*0.46, S*1.06, S*0.76);
    g.add(club);
  });

  const legPositions = [
    { z: S*0.5,  yOff: -S*0.35 },
    { z: S*0.68, yOff: -S*0.38 },
    { z: S*0.82, yOff: -S*0.32 },
  ];
  legPositions.forEach((lp, li) => {
    [-1,1].forEach(side => {
      const legGeo = new THREE.CylinderGeometry(S*0.022, S*0.018, S*0.55, 5);
      const leg = new THREE.Mesh(legGeo, blackMat.clone());
      leg.position.set(side*S*0.48, lp.yOff, lp.z);
      leg.rotation.z = side * (0.9 + li*0.1);
      leg.rotation.x = 0.2;
      g.add(leg);

      const llGeo = new THREE.CylinderGeometry(S*0.018, S*0.012, S*0.48, 5);
      const ll = new THREE.Mesh(llGeo, blackMat.clone());
      ll.position.set(side*S*0.75, lp.yOff - S*0.3, lp.z + S*0.1);
      ll.rotation.z = side * (1.2 + li*0.08);
      ll.rotation.x = 0.35;
      g.add(ll);
    });
  });

  const wings = [];
  const wingDefs = [
    { scX:1.9, scY:0.75, x:-S*0.55, y:S*0.42, z:S*0.72, rx:-0.25, ry:-0.12, rz:-0.38, side:-1, fore:true },
    { scX:1.9, scY:0.75, x: S*0.55, y:S*0.42, z:S*0.72, rx:-0.25, ry: 0.12, rz: 0.38, side: 1, fore:true },
    { scX:1.35, scY:0.55, x:-S*0.48, y:S*0.28, z:S*0.48, rx:-0.18, ry:-0.08, rz:-0.55, side:-1, fore:false },
    { scX:1.35, scY:0.55, x: S*0.48, y:S*0.28, z:S*0.48, rx:-0.18, ry: 0.08, rz: 0.55, side: 1, fore:false },
  ];

  wingDefs.forEach(wd => {
    const wGeo = new THREE.PlaneGeometry(S*wd.scX, S*wd.scY, 6, 4);
    const pos = wGeo.attributes.position;
    for (let vi = 0; vi < pos.count; vi++) {
      const nx = pos.getX(vi) / (S*wd.scX*0.5);
      const taper = 1.0 - Math.abs(nx) * 0.35;
      pos.setY(vi, pos.getY(vi) * taper);
    }
    pos.needsUpdate = true;
    wGeo.computeVertexNormals();

    const wMat = new THREE.MeshPhongMaterial({
      color: wd.fore ? 0xd4f0ff : 0xc0e8ff,
      transparent: true, opacity: wd.fore ? 0.32 : 0.25,
      side: THREE.DoubleSide, shininess: 180, specular: 0xffffff,
      emissive: 0x081520
    });
    const wing = new THREE.Mesh(wGeo, wMat);
    wing.position.set(wd.x, wd.y, wd.z);
    wing.rotation.set(wd.rx, wd.ry, wd.rz);
    g.add(wing);
    wings.push({ mesh: wing, baseRZ: wd.rz, side: wd.side });
  });

  g.userData = {
    wings,
    phase: Math.random() * Math.PI * 2,
    vel: new THREE.Vector3((Math.random()-0.5)*0.12,(Math.random()-0.5)*0.08,(Math.random()-0.5)*0.12),
    acc: new THREE.Vector3(),
    wobble: Math.random() * Math.PI * 2
  };
  return g;
}

const NUM = 80, BOUNDS = 45;
const bees = [];
for (let i = 0; i < NUM; i++) {
  const b = makeBee();
  b.position.set(
    (Math.random()-0.5)*BOUNDS*2,
    (Math.random()-0.5)*BOUNDS*1.1,
    (Math.random()-0.5)*BOUNDS*2
  );
  b.rotation.y = Math.random() * Math.PI * 2;
  scene.add(b);
  bees.push(b);
}

const mouseWorld = new THREE.Vector3();
window.addEventListener('mousemove', e => {
  mouseWorld.set((e.clientX/innerWidth*2-1)*40,(-(e.clientY/innerHeight)*2+1)*25,0);
});
window.addEventListener('resize', () => {
  camera.aspect = innerWidth/innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
});

const tmp = new THREE.Vector3(), sep = new THREE.Vector3(), ali = new THREE.Vector3(), coh = new THREE.Vector3();
const SEP=5.5, NEI=14, MAXSPD=0.2, MAXF=0.006;
function limit(v,m){ if(v.length()>m) v.setLength(m); }

function flock(bee, i, t) {
  const pos = bee.position, vel = bee.userData.vel, acc = bee.userData.acc;
  acc.set(0,0,0); sep.set(0,0,0); ali.set(0,0,0); coh.set(0,0,0);
  let sc=0, nc=0;
  for (let j=0;j<bees.length;j++) {
    if(j===i) continue;
    const d = pos.distanceTo(bees[j].position);
    if(d<SEP&&d>0){ tmp.subVectors(pos,bees[j].position).normalize().divideScalar(d); sep.add(tmp); sc++; }
    if(d<NEI){ ali.add(bees[j].userData.vel); coh.add(bees[j].position); nc++; }
  }
  if(sc>0){ sep.divideScalar(sc); limit(sep,MAXF*2.5); acc.add(sep); }
  if(nc>0){
    ali.divideScalar(nc); limit(ali,MAXF); acc.add(ali);
    coh.divideScalar(nc); tmp.subVectors(coh,pos).setLength(MAXSPD); tmp.sub(vel); limit(tmp,MAXF*0.5); acc.add(tmp);
  }
  tmp.subVectors(mouseWorld,pos);
  if(tmp.length()<35){ tmp.setLength(MAXSPD*0.6); tmp.sub(vel); limit(tmp,0.005); acc.add(tmp); }
  acc.y += Math.sin(t*1.1+bee.userData.wobble)*0.0015;

  const B=BOUNDS;
  if(pos.x>B) pos.x=-B; if(pos.x<-B) pos.x=B;
  if(pos.y>B*0.9) pos.y=-B*0.9; if(pos.y<-B*0.9) pos.y=B*0.9;
  if(pos.z>B) pos.z=-B; if(pos.z<-B) pos.z=B;
  vel.add(acc); limit(vel,MAXSPD); pos.add(vel);
}

const clock = new THREE.Clock();
function animate() {
  requestAnimationFrame(animate);
  const t = clock.getElapsedTime();

  bees.forEach((bee,i) => {
    flock(bee,i,t);
    if(bee.userData.vel.length()>0.001){
      const d = bee.userData.vel.clone().normalize();
      bee.rotation.y = Math.atan2(d.x,d.z);
      bee.rotation.z = -d.x*0.35;
      bee.rotation.x = Math.asin(Math.max(-1,Math.min(1,-d.y)))*0.5;
    }
    const flap = Math.sin(t*32+bee.userData.phase)*0.75;
    bee.userData.wings.forEach(w => {
      w.mesh.rotation.z = w.baseRZ + flap*w.side*0.9;
      w.mesh.rotation.x = -0.25 + Math.abs(flap)*0.12;
      w.mesh.material.opacity = 0.28 + Math.abs(flap)*0.14;
    });
  });

  camera.position.x = Math.sin(t*0.035)*10;
  camera.position.y = Math.cos(t*0.028)*5;
  camera.lookAt(0,0,0);
  renderer.render(scene,camera);
}
animate();
</script>
</body>
</html>