Custom Code Experiment •
ThreeJS: Bumble Bee
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: Bumble Bee</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(255, 200, 40, 0.5); 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 = {
yellow: 0xffc020,
black: 0x1a1200,
thorax: 0xe8a000,
wing: 0xffe090,
vein: 0xfff4c0,
eye: 0x203000,
leg: 0x2a1a00,
pollen: 0xffdd00,
stinger: 0x3a2800,
fuzz: 0xffd060,
stripe: 0xfff0a0,
};
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*50)},${Math.floor(p.b*32)},${Math.floor(p.b*5)})`;
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, bee;
let wingFL, wingFR, wingHL, wingHR;
let legGroups = [];
const orbit = {
theta: 0.4, phi: 1.0, radius: 8,
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 = 20;
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 ? 12 : w < 768 ? 10 : 8;
camera.fov = 52;
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
setCameraFromOrbit();
}
function mat(color) {
return new THREE.MeshBasicMaterial({ color, wireframe: true });
}
function addFuzz(group, cx, cy, cz, radius, count, color) {
for (let i = 0; i < count; i++) {
const phi = Math.acos(2 * Math.random() - 1);
const theta = Math.random() * Math.PI * 2;
const r = radius + 0.015 + Math.random() * 0.04;
const fGeo = new THREE.SphereGeometry(0.018 + Math.random()*0.014, 4, 4);
const fuzz = new THREE.Mesh(fGeo, mat(color));
fuzz.position.set(
cx + r * Math.sin(phi) * Math.cos(theta),
cy + r * Math.sin(phi) * Math.sin(theta),
cz + r * Math.cos(phi)
);
group.add(fuzz);
}
}
function makeWing(side, isFore) {
const group = new THREE.Group();
const s = side;
if (isFore) {
const shape = new THREE.Shape();
shape.moveTo(0, 0);
shape.bezierCurveTo(s*0.4, 0.30, s*1.1, 0.55, s*1.7, 0.40);
shape.bezierCurveTo(s*2.1, 0.28, s*2.2, 0.05, s*2.0, -0.18);
shape.bezierCurveTo(s*1.7, -0.38, s*1.0, -0.45, s*0.4, -0.28);
shape.bezierCurveTo(s*0.12, -0.16, 0, -0.06, 0, 0);
const geo = new THREE.ShapeGeometry(shape, 14);
group.add(new THREE.Mesh(geo, mat(colors.wing)));
group.add(new THREE.LineSegments(new THREE.EdgesGeometry(geo),
new THREE.LineBasicMaterial({ color: colors.vein })));
[
{ pts: [[0,0],[s*0.7,0.36],[s*1.5,0.36],[s*2.0,0.16]], r: 0.012 },
{ pts: [[s*0.05,-0.05],[s*0.6,0.22],[s*1.3,0.22],[s*1.85,0.04]], r: 0.009 },
{ pts: [[s*0.08,-0.08],[s*0.5,0.06],[s*1.1,0.02],[s*1.7,-0.12]], r: 0.008 },
{ pts: [[s*0.1,-0.12],[s*0.5,-0.10],[s*1.0,-0.18],[s*1.6,-0.30]], r: 0.007 },
].forEach(v => {
const pts = v.pts.map(p => new THREE.Vector3(p[0], p[1], 0.01));
const geo2 = new THREE.TubeGeometry(new THREE.CatmullRomCurve3(pts), 8, v.r, 4, false);
group.add(new THREE.Mesh(geo2, mat(colors.vein)));
});
[[s*0.55,0.22,s*0.60,-0.04],[s*1.0,0.20,s*1.05,-0.10],[s*1.4,0.14,s*1.45,-0.20]].forEach(cv => {
const pts = [new THREE.Vector3(cv[0],cv[1],0.01), new THREE.Vector3(cv[2],cv[3],0.01)];
group.add(new THREE.Mesh(
new THREE.TubeGeometry(new THREE.CatmullRomCurve3(pts), 3, 0.005, 4, false),
mat(colors.vein)
));
});
} else {
const shape2 = new THREE.Shape();
shape2.moveTo(0, 0);
shape2.bezierCurveTo(s*0.3, 0.18, s*0.9, 0.32, s*1.3, 0.20);
shape2.bezierCurveTo(s*1.6, 0.10, s*1.6, -0.10, s*1.4, -0.22);
shape2.bezierCurveTo(s*1.1, -0.34, s*0.5, -0.30, s*0.15, -0.14);
shape2.bezierCurveTo(s*0.04, -0.06, 0, 0, 0, 0);
const geo3 = new THREE.ShapeGeometry(shape2, 10);
group.add(new THREE.Mesh(geo3, mat(colors.wing)));
group.add(new THREE.LineSegments(new THREE.EdgesGeometry(geo3),
new THREE.LineBasicMaterial({ color: colors.vein })));
[[s*0.05,-0.03,s*0.8,0.18,s*1.25,0.06],[s*0.08,-0.08,s*0.6,0.00,s*1.1,-0.14]].forEach(v => {
const pts = [
new THREE.Vector3(v[0],v[1],0.01),
new THREE.Vector3(v[2],v[3],0.01),
new THREE.Vector3(v[4],v[5],0.01)
];
group.add(new THREE.Mesh(
new THREE.TubeGeometry(new THREE.CatmullRomCurve3(pts), 6, 0.007, 4, false),
mat(colors.vein)
));
});
}
return group;
}
function makeLeg(side, index) {
const group = new THREE.Group();
const s = side;
const isHind = index === 2;
const cPts = [new THREE.Vector3(0,0,0), new THREE.Vector3(s*0.22,-0.10,0.02)];
group.add(new THREE.Mesh(
new THREE.TubeGeometry(new THREE.CatmullRomCurve3(cPts), 4, 0.030, 6, false),
mat(colors.black)
));
const fPts = [
new THREE.Vector3(s*0.22,-0.10,0.02),
new THREE.Vector3(s*0.50,-0.38,0.02),
new THREE.Vector3(s*0.70,-0.55,0.00)
];
group.add(new THREE.Mesh(
new THREE.TubeGeometry(new THREE.CatmullRomCurve3(fPts), 6, 0.028, 6, false),
mat(isHind ? colors.yellow : colors.black)
));
const tPts = [
new THREE.Vector3(s*0.70,-0.55,0.00),
new THREE.Vector3(s*0.95,-0.82,-0.02),
new THREE.Vector3(s*1.10,-1.10,-0.04)
];
group.add(new THREE.Mesh(
new THREE.TubeGeometry(new THREE.CatmullRomCurve3(tPts), 6, isHind ? 0.032 : 0.022, 6, false),
mat(isHind ? colors.yellow : colors.black)
));
if (isHind) {
const pbGeo = new THREE.SphereGeometry(0.10, 10, 8);
const pb = new THREE.Mesh(pbGeo, mat(colors.pollen));
pb.scale.set(0.6, 1.1, 0.55);
pb.position.set(s*0.92, -0.80, -0.02);
group.add(pb);
for (let i = 0; i < 5; i++) {
const ang = (i / 5) * Math.PI * 0.8 - 0.2;
const sPts = [
new THREE.Vector3(s*0.92, -0.80, -0.02),
new THREE.Vector3(s*(0.92 + Math.cos(ang)*0.08), -0.80 + Math.sin(ang)*0.08, -0.10)
];
group.add(new THREE.Mesh(
new THREE.TubeGeometry(new THREE.CatmullRomCurve3(sPts), 3, 0.004, 3, false),
mat(colors.pollen)
));
}
}
let tStart = new THREE.Vector3(s*1.10,-1.10,-0.04);
for (let ti = 0; ti < 3; ti++) {
const tEnd = new THREE.Vector3(
tStart.x + s*0.18,
tStart.y - 0.18 + ti*0.04,
tStart.z - 0.02
);
group.add(new THREE.Mesh(
new THREE.TubeGeometry(new THREE.CatmullRomCurve3([tStart, tEnd]), 4,
0.016 - ti*0.003, 5, false),
mat(ti % 2 === 0 ? colors.black : colors.yellow)
));
tStart = tEnd;
}
const clGeo = new THREE.ConeGeometry(0.010, 0.06, 5);
const cl = new THREE.Mesh(clGeo, mat(colors.fuzz));
cl.rotation.z = s * -0.5;
cl.position.copy(tStart).addScaledVector(new THREE.Vector3(s,0,-0.5).normalize(), 0.04);
group.add(cl);
return group;
}
function createBee() {
const group = new THREE.Group();
legGroups = [];
const abdData = [
{ r: 0.30, col: colors.yellow, z: 0.12 },
{ r: 0.36, col: colors.black, z: 0.42 },
{ r: 0.38, col: colors.yellow, z: 0.72 },
{ r: 0.33, col: colors.black, z: 1.00 },
{ r: 0.22, col: colors.yellow, z: 1.26 },
{ r: 0.12, col: colors.black, z: 1.48 },
];
abdData.forEach((s, i) => {
const next = abdData[i + 1];
const rTop = next ? next.r : s.r * 0.5;
const geo = new THREE.CylinderGeometry(rTop * 0.92, s.r, 0.32, 16, 1);
const seg = new THREE.Mesh(geo, mat(s.col));
seg.rotation.x = Math.PI * 0.5;
seg.position.set(0, 0, s.z);
group.add(seg);
if (s.col === colors.yellow) {
addFuzz(group, 0, 0, s.z, s.r, 18, colors.fuzz);
}
const ringGeo = new THREE.TorusGeometry(s.r + 0.01, 0.010, 6, 18);
const ring = new THREE.Mesh(ringGeo, mat(colors.stripe));
ring.position.set(0, 0, s.z);
group.add(ring);
});
const stingPts = [
new THREE.Vector3(0, 0, 1.62),
new THREE.Vector3(0, -0.04, 1.76),
new THREE.Vector3(0, -0.06, 1.88),
];
group.add(new THREE.Mesh(
new THREE.TubeGeometry(new THREE.CatmullRomCurve3(stingPts), 6, 0.016, 5, false),
mat(colors.stinger)
));
const stingTip = new THREE.Mesh(new THREE.ConeGeometry(0.010, 0.05, 5), mat(colors.stinger));
stingTip.rotation.x = 0.4;
stingTip.position.set(0, -0.07, 1.92);
group.add(stingTip);
const txGeo = new THREE.SphereGeometry(0.42, 18, 16);
const thorax = new THREE.Mesh(txGeo, mat(colors.thorax));
thorax.scale.set(1.0, 1.05, 1.15);
thorax.position.set(0, 0.06, -0.28);
group.add(thorax);
addFuzz(group, 0, 0.06, -0.28, 0.44, 60, colors.fuzz);
const proGeo = new THREE.TorusGeometry(0.30, 0.040, 8, 20);
const pro = new THREE.Mesh(proGeo, mat(colors.yellow));
pro.rotation.x = Math.PI * 0.5;
pro.position.set(0, 0.08, -0.48);
group.add(pro);
const headGeo = new THREE.SphereGeometry(0.26, 16, 14);
const head = new THREE.Mesh(headGeo, mat(colors.black));
head.scale.set(1.0, 0.95, 1.0);
head.position.set(0, 0.00, -0.78);
group.add(head);
addFuzz(group, 0, 0.00, -0.78, 0.27, 22, colors.fuzz);
[-1, 1].forEach(side => {
const eyeGeo = new THREE.SphereGeometry(0.14, 14, 12);
const eye = new THREE.Mesh(eyeGeo, mat(colors.eye));
eye.scale.set(0.70, 0.90, 0.80);
eye.position.set(side * 0.21, 0.04, -0.80);
group.add(eye);
for (let fi = 0; fi < 6; fi++) {
const fGeo = new THREE.CircleGeometry(0.035, 6);
const facet = new THREE.Mesh(fGeo, mat(colors.fuzz));
const ang = (fi / 6) * Math.PI * 1.6 - 0.4;
facet.position.set(
side * (0.24 + Math.cos(ang)*0.04),
0.04 + Math.sin(ang)*0.06,
-0.78
);
group.add(facet);
}
});
[-1, 1].forEach(side => {
const scPts = [
new THREE.Vector3(side*0.08, 0.14, -0.90),
new THREE.Vector3(side*0.14, 0.28, -0.96),
new THREE.Vector3(side*0.18, 0.40, -0.98),
];
group.add(new THREE.Mesh(
new THREE.TubeGeometry(new THREE.CatmullRomCurve3(scPts), 6, 0.016, 5, false),
mat(colors.black)
));
const pelGeo = new THREE.SphereGeometry(0.028, 7, 6);
const pel = new THREE.Mesh(pelGeo, mat(colors.yellow));
pel.position.set(side*0.18, 0.42, -0.98);
group.add(pel);
const flPts = [
new THREE.Vector3(side*0.18, 0.42, -0.98),
new THREE.Vector3(side*0.32, 0.46, -0.96),
new THREE.Vector3(side*0.44, 0.40, -0.90),
new THREE.Vector3(side*0.52, 0.32, -0.84),
];
group.add(new THREE.Mesh(
new THREE.TubeGeometry(new THREE.CatmullRomCurve3(flPts), 8, 0.011, 5, false),
mat(colors.black)
));
const knob = new THREE.Mesh(new THREE.SphereGeometry(0.022, 6, 6), mat(colors.yellow));
knob.position.set(side*0.52, 0.32, -0.84);
group.add(knob);
});
const probPts = [
new THREE.Vector3(0, -0.14, -0.90),
new THREE.Vector3(0, -0.22, -1.00),
new THREE.Vector3(0, -0.26, -1.12),
];
group.add(new THREE.Mesh(
new THREE.TubeGeometry(new THREE.CatmullRomCurve3(probPts), 6, 0.018, 5, false),
mat(colors.yellow)
));
const tongueGeo = new THREE.SphereGeometry(0.025, 7, 6);
const tongue = new THREE.Mesh(tongueGeo, mat(colors.fuzz));
tongue.position.set(0, -0.28, -1.14);
group.add(tongue);
[-1, 1].forEach(side => {
const mPts = [
new THREE.Vector3(side*0.08, -0.10, -0.92),
new THREE.Vector3(side*0.16, -0.18, -1.02),
];
group.add(new THREE.Mesh(
new THREE.TubeGeometry(new THREE.CatmullRomCurve3(mPts), 3, 0.014, 4, false),
mat(colors.black)
));
});
wingFL = new THREE.Group();
wingFL.position.set(0, 0.16, -0.30);
wingFL.rotation.z = 0.08;
wingFL.rotation.x = -0.12;
wingFL.add(makeWing(-1, true));
group.add(wingFL);
wingFR = new THREE.Group();
wingFR.position.set(0, 0.16, -0.30);
wingFR.rotation.z = -0.08;
wingFR.rotation.x = -0.12;
wingFR.add(makeWing(1, true));
group.add(wingFR);
wingHL = new THREE.Group();
wingHL.position.set(0, 0.10, -0.10);
wingHL.rotation.z = 0.12;
wingHL.rotation.x = -0.10;
wingHL.add(makeWing(-1, false));
group.add(wingHL);
wingHR = new THREE.Group();
wingHR.position.set(0, 0.10, -0.10);
wingHR.rotation.z = -0.12;
wingHR.rotation.x = -0.10;
wingHR.add(makeWing(1, false));
group.add(wingHR);
[-1, 1].forEach(side => {
for (let li = 0; li < 3; li++) {
const legGroup = makeLeg(side, li);
legGroup.position.set(side * 0.38, -0.08, -0.45 + li * 0.30);
group.add(legGroup);
legGroups.push({ g: legGroup, side, li });
}
});
group.position.set(0, 0.2, 0.2);
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;
const speed = 0.005;
orbit.velTheta = -dx * speed;
orbit.velPhi = -dy * speed;
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();
bee = createBee();
scene.add(bee);
scene.add(new THREE.AmbientLight(0xffcc60, 0.55));
const pl1 = new THREE.PointLight(0xffa020, 1.8, 100);
pl1.position.set(4, 5, 3);
scene.add(pl1);
const pl2 = new THREE.PointLight(0xffd060, 0.9, 100);
pl2.position.set(-4, -2, 2);
scene.add(pl2);
const pl3 = new THREE.PointLight(0xffffff, 0.4, 100);
pl3.position.set(0, 3, -8);
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 (bee) {
bee.position.y = 0.2 + Math.sin(t * 2.2) * 0.08;
bee.rotation.x = Math.sin(t * 1.6) * 0.04;
bee.rotation.z = Math.sin(t * 2.0 + 0.8) * 0.03;
const buzz = t * 14.0;
if (wingFL) {
wingFL.rotation.z = 0.08 + Math.sin(buzz) * 0.60;
wingFL.rotation.x = -0.12 + Math.sin(buzz * 0.97) * 0.10;
}
if (wingFR) {
wingFR.rotation.z = -0.08 - Math.sin(buzz + 0.05) * 0.60;
wingFR.rotation.x = -0.12 + Math.sin(buzz * 0.97) * 0.10;
}
if (wingHL) {
wingHL.rotation.z = 0.12 + Math.sin(buzz + 0.3) * 0.45;
wingHL.rotation.x = -0.10 + Math.sin(buzz * 0.95) * 0.08;
}
if (wingHR) {
wingHR.rotation.z = -0.12 - Math.sin(buzz + 0.35) * 0.45;
wingHR.rotation.x = -0.10 + Math.sin(buzz * 0.95) * 0.08;
}
legGroups.forEach(({ g, side, li }) => {
const phase = li * 0.9 + (side > 0 ? 1.4 : 0);
g.rotation.z = side * (0.05 + Math.sin(t * 2.0 + phase) * 0.05);
g.rotation.x = Math.sin(t * 1.7 + phase) * 0.04;
});
}
renderer.render(scene, camera);
}
window.addEventListener('resize', () => {
updateSize();
initBackground();
if (camera) updateCamera();
});
window.addEventListener('load', init);
</script>
</body>
</html>