A single 3,891-line HTML file that renders 50 procedurally-generated humanoid avatars, seven full-body animation modes (including a physics-driven tennis rally and a five-state breakdance sequence), all inside the browser with no build step and no framework. This page is a tour of every meaningful technique used.
WebGL via Three.js — but the postprocessing chain and rig math are all hand-written.
Core WebGLRenderer with PCF soft shadow maps, antialias, and a pixel ratio capped at 2 to keep retina screens fast.
Loaded as r128 examples (CopyShader · LuminosityHighPassShader · EffectComposer · RenderPass · ShaderPass · UnrealBloomPass). Strength controlled live; composer skipped when strength≈0 to save GPU.
Composer needs setPixelRatio() matching the renderer, otherwise the bloom mip-chain samples 1px outside the texture and paints a white strip on the right edge.
Geometry helpers (mkSphere, mkCyl, mkBox, mkLimb) clone a Phong base material with emissive on glow parts (eyes, chest panel, joints, ball sweet-spot) so bloom has something to grab.
Directional sun (shadow caster), ambient blue, two coloured point lights. Each environment preset retunes colour temperature, intensity and FogExp2 density together.
Single PlaneGeometry whose Phong shininess and color are mutated to fake Grid / Mirror / Matte / Hide states without rebuilding meshes.
No GLTF import — every limb is a nested THREE.Group tree assembled at runtime.
root → spine → neck → head, plus shoulder → elbow → hand and hip → knee → ankle → foot. Each Group's local rotation = a joint angle; child position = bone length. Pure forward kinematics.
Each character is a config object indexing into 10 skin tones × 13 hair colours × 11 hair styles × 15 shirts × 10 pants × 8 panels × 8 eyes + accessory + scale. Index hashes use coprime stride multipliers (3, 7, 11, 13, 17) for visual diversity.
Shoulder X = 0.50 of body height; upper arm 0.66 + forearm 0.55 ≈ 32% of body height. Head scaled 0.79 to land near 6.5-head adult proportions.
clampArmsThroughBody tests hand / elbow / forearm-midpoint positions in spine-local space against a torso ellipsoid; if inside, pushes outward along Z. clampLegsThroughBody does the same with knees + feet against a body ellipsoid and a knee-cross check.
applyGroundCollision probes ankles/feet world Y; if any point sinks below floor level, the whole root translates up just enough to bring the lowest point back to ground. Skipped during break-mode floor work where the contact point is choreographed directly.
Built once and parented to rHnd. Has a userData.sweet world-space marker (ball strike origin) that the swing resolver reads to compute exit velocity.
Every pose is a function of t (time) and clamped state — no keyframe storage, no skeletal blend trees.
Cyclic motion driven by sine waves with hand-tuned ω per mode. Walk uses ω=2.8 with phase-locked shoulder counter-swing; Run jumps to ω=6.4 plus a vertical lift ∝ |sin(2ωt)|.
rHip.x = +sin(ωt) × 0.52 lHip.x = −sin(ωt) × 0.52 rKn.x = max(0,+sin(ωt)) × 0.72
Wave is one shoulder lift + a high-frequency forearm waggle. Dance layers a beat envelope |sin(ωt/2)|^0.6 on top of body sway, neck head-bob and arm pump.
Toprock → Windmill → Headspin → Freeze → Get-up. Each state is its own pose function; transitions happen at fixed time stamps. Windmill skips the leg-clamp because legs legitimately sweep through the body-local torso volume each rotation.
Ball updates with gravity 9.81 m/s², 0.999 drag per tick, vertical bounce coefficient −0.62, ground-skid coefficient 0.93. Shot variants (flat / topspin / slice / lob / drop / cross) just pick a target apex height.
v.y' = v.y − g · dt
v = v · 0.999 (drag)
on bounce: v.y ← −0.62·v.y
v.xz ← 0.93·v.xz
predictBall(dtAhead, …) rolls the ball physics forward in a fixed-step loop. predictIntercept(side) walks the prediction until the ball crosses the player's baseline plane, returning {time, position} → AI runs to it.
v_y₀ = √(2g·(h−y₀)) T = (v_y₀+√(v_y₀²+2g·y_x))/g v_x = (xT−x₀) / T v_z = (zT−z₀) / T
Trophy → drive → strike → follow-through. Phases drive shoulder, elbow and torso angles in sequence, magnetising the ball to the racket sweet-spot at strike t.
Glassmorphism rendered with native CSS — no UI framework, no icon library.
Every panel uses backdrop-filter: blur(22px) saturate(160%) over a translucent background, with rgba strokes for the rim-light effect. Falls back gracefully on browsers without backdrop-filter.
Four environment themes (Cyber Night · Daylight · Sunset · Arena) are pure variable swaps on body.theme-*. The same swap also retunes Three.js lights & fog so 2D and 3D stay consistent.
Earlier versions used unicode glyphs (⏸ ⌖ 📸 ⚙ etc) but per-OS font metrics offset them off-centre inside circular buttons. Switching to fill="currentColor" SVG paths fixes alignment and lets the icon inherit hover / active colour.
Native select arrows render at different positions per platform. Inline SVG data-URI background-image + appearance:none gives identical visuals everywhere.
Single versioned key 3dhuman.settings.v1 stores env, floor, FOV, speed, mode, char A/B, formula collapse, bloom strength and per-character colour overrides as a diff. Restored on init in dependency order.
Formula panel uses scrollIntoView({block:'start'}) to keep the active block visible. To avoid the floating 𝑓 toggle covering its top edge, the panel has scroll-padding-top instead of regular padding-top.
Keyboard, mouse and screen recording — all in browser native APIs.
Camera position computed from (R, θ, φ). Mouse drag updates θ/φ; wheel updates R, both with explicit clamps so the camera never enters the floor or flips upside-down.
Keydown flips a flag in camKeys, keyup clears it. A separate rAF tick integrates flag × dt every frame so movement is smooth regardless of OS key-repeat rate.
Force a render → renderer.domElement.toBlob('image/png') → download via dynamic <a>. No server round-trip.
canvas.captureStream(60) piped into MediaRecorder with VP9 / VP8 fallback and 8 Mbps bitrate. Chunks collected on dataavailable, blob saved on stop.
Pressing C used to open the avatar grid and type "c" into the now-focused search field. Fix: e.preventDefault() for any letter hotkey that focuses an input.
Single animClock advances by realDt × timeScale. Pause freezes the clock; the speed slider stretches it; the step button advances 1/60 s while paused. Every animation function trusts the clock.
Animation runs at ~120 FPS on a recent MacBook even with bloom + shadows.
_simP, _simV, _racketHit, _lv, _h3, _e3, _m3, _kr, _kl, _fr, _fl, _wp — never allocating a Vector3 per frame.renderer.render(), skipping the entire postprocessing chain.disposeCharacter walks the old root and calls geometry.dispose() + material.dispose() before adding the new one — avoids GPU memory creep when cycling 50 avatars.An explicit anti-decision worth documenting.
Single-page demo. Zero routing, zero shared component reuse, no team. A framework here would be 100% overhead and 0% benefit.
One index.html. Open in any browser via file:// or any static host. No npm install, no Vite config, no bundle hash. Source view shows everything.
Reactive UI bindings — but the chrome here is small enough that addEventListener + manual DOM updates take 30 lines instead of 300 lines + library. Component re-use — N/A here.
~3,900 lines of working code to migrate, hours of regression testing on the tennis physics and break-state machine, plus a build pipeline to maintain forever.