Menu
Public repository / f6f5dc7059b40f7a4042c85f7c49597638a67522
AkurAI-Framework / site/frontend/hero3d.js
Latest change a4a1786d2033efcf9c512fc869ab3f45d2bc2c4e - feat(hero): live Three.js 3D hero — glass planes converge into glowing binary by Ólafur Búi Ólafsson
/* hero3d.js — live Three.js recreation of /img/hero.svg.
*
* "Layered HTTP, database, and UI planes converge into a glowing single binary."
* Same object as the original illustration, rendered live with modern r185 effects:
* glass transmission + chromatic dispersion + iridescence, bloom, ACES tone mapping.
*
* Self-hosted Three.js (vendored under /vendor/three) — no CDN, in keeping with the
* framework's zero-runtime-dependency promise. Decorative + non-blocking: it only
* upgrades the static SVG when WebGL2 is present, honours prefers-reduced-motion,
* and pauses when scrolled out of view.
*/
import * as THREE from '/vendor/three/three.module.js';
import { EffectComposer } from '/vendor/three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from '/vendor/three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from '/vendor/three/addons/postprocessing/UnrealBloomPass.js';
import { OutputPass } from '/vendor/three/addons/postprocessing/OutputPass.js';
const BRAND = { blue: 0x5b8cff, purple: 0x8a6bff, light: 0xe7ebf3, green: 0x3ddc84 };
const mount = document.getElementById('hero3d');
const fallback = document.querySelector('.hero-art img');
const webgl2 = (() => { try { return !!document.createElement('canvas').getContext('webgl2'); }
catch { return false; } })();
if (mount && webgl2) boot();
function boot() {
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
if (fallback) fallback.style.display = 'none';
mount.hidden = false;
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 1.75));
renderer.setClearColor(0x0a0c11, 1); // dark stage, independent of page theme
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.15;
mount.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0c11); // dark stage baked in → reads on light or dark themes
const camera = new THREE.PerspectiveCamera(33, 1, 0.1, 100);
camera.position.set(0, 0.4, 10);
camera.lookAt(0, 0.3, 0);
// coloured aurora behind the object → the glass has colour to refract
[[0x16224f, -3.2, 1.4, -7.5, 3.0], [0x241650, 3.2, -1.2, -7.5, 3.2]]
.forEach(([c, x, y, z, r]) => {
const m = new THREE.Mesh(new THREE.SphereGeometry(r, 24, 24),
new THREE.MeshBasicMaterial({ color: c }));
m.position.set(x, y, z); scene.add(m);
});
// studio env (procedural, core-only) so the glass has something to refract
const envScene = new THREE.Scene();
envScene.add(new THREE.HemisphereLight(0xbcd0ff, 0x0a0c11, 3));
[[BRAND.blue, -5, 3, 4], [BRAND.purple, 5, -2, 3], [0xffffff, 0, 6, -4]].forEach(([c, x, y, z]) => {
const m = new THREE.Mesh(new THREE.SphereGeometry(2.4, 16, 16),
new THREE.MeshBasicMaterial({ color: c }));
m.position.set(x, y, z); envScene.add(m);
});
const pmrem = new THREE.PMREMGenerator(renderer);
scene.environment = pmrem.fromScene(envScene, 0.03).texture;
const key = new THREE.DirectionalLight(0xffffff, 2.4); key.position.set(4, 7, 6); scene.add(key);
const rim = new THREE.DirectionalLight(0x88aaff, 1.6); rim.position.set(-6, -2, 4); scene.add(rim);
scene.add(new THREE.AmbientLight(0x2a3350, 0.7));
/* ── the object ─────────────────────────────────────────────────────────── */
const obj = new THREE.Group();
obj.rotation.x = -0.32;
scene.add(obj);
// shared glass material — identity carried by tint only (the binary cube)
const glass = (tint, opts = {}) => new THREE.MeshPhysicalMaterial({
color: 0xffffff, metalness: 0, roughness: 0.08,
transmission: 1, ior: 1.45, thickness: 1.4, dispersion: 2.2,
iridescence: 0.5, iridescenceIOR: 1.4,
attenuationColor: new THREE.Color(tint), attenuationDistance: 0.9,
clearcoat: 1, clearcoatRoughness: 0.15, envMapIntensity: 1.35,
transparent: true, ...opts,
});
// tinted translucent panel — the HTTP/DB/UI planes read as filled glass, not wireframe
const panel = (tint) => new THREE.MeshPhysicalMaterial({
color: new THREE.Color(tint).multiplyScalar(0.22),
emissive: new THREE.Color(tint), emissiveIntensity: 0.10,
metalness: 0, roughness: 0.16,
transmission: 0.55, thickness: 0.6, ior: 1.4,
attenuationColor: new THREE.Color(tint), attenuationDistance: 0.6,
clearcoat: 1, clearcoatRoughness: 0.2, envMapIntensity: 1.15,
transparent: true, opacity: 0.94,
});
// three stacked, converging planes — HTTP / DB / UI
const PLANES = [
{ label: 'HTTP', tint: BRAND.blue, y: 1.95 },
{ label: 'DB', tint: BRAND.purple, y: 1.0 },
{ label: 'UI', tint: BRAND.light, y: 0.05 },
];
const slabGeo = new THREE.BoxGeometry(4.0, 0.13, 2.7);
slabGeo.translate(0, 0, 0);
PLANES.forEach((p, i) => {
const slab = new THREE.Mesh(slabGeo, panel(p.tint));
slab.position.set(0, p.y, -0.25);
// converge: each plane a touch smaller + pulled toward the binary below
const s = 1 - i * 0.07; slab.scale.set(s, 1, s);
obj.add(slab);
// glowing edge wireframe so the planes read on any background (bloom catches it)
const edge = new THREE.LineSegments(new THREE.EdgesGeometry(slabGeo),
new THREE.LineBasicMaterial({ color: p.tint, transparent: true, opacity: 0.55 }));
edge.position.copy(slab.position); edge.scale.copy(slab.scale); obj.add(edge);
// monospace label on the near-left corner
obj.add(makeLabel(p.label, -1.7, p.y + 0.28, 1.45, p.tint));
});
// central glowing binary cube
const cubeGeo = new THREE.BoxGeometry(1.75, 1.75, 1.75);
const cube = new THREE.Mesh(cubeGeo, glass(0x6f7cff,
{ thickness: 1.8, dispersion: 3.0, attenuationDistance: 1.2 }));
cube.position.set(0, -1.15, 0.55);
obj.add(cube);
const cubeEdges = new THREE.LineSegments(new THREE.EdgesGeometry(cubeGeo),
new THREE.LineBasicMaterial({ color: 0xeaf0ff }));
cubeEdges.position.copy(cube.position); obj.add(cubeEdges);
// bright core → bloom halo (the "glow")
const core = new THREE.Mesh(new THREE.IcosahedronGeometry(0.3, 2),
new THREE.MeshBasicMaterial({ color: 0x86a8ff }));
core.position.copy(cube.position); obj.add(core);
// "binary / code" lines on the front face
const codeMat = new THREE.MeshBasicMaterial({ color: 0xc8d6ff });
[[0.0, 0.46], [-0.14, 0.16], [0.06, -0.14]].forEach(([dx, dy], i) => {
const w = [0.78, 0.5, 0.64][i];
const bar = new THREE.Mesh(new THREE.PlaneGeometry(w, 0.1), codeMat);
bar.position.set(cube.position.x + dx, cube.position.y + dy, cube.position.z + 0.9);
obj.add(bar);
});
// green accent nodes + flow lines to the binary
const nodeMat = new THREE.MeshBasicMaterial({ color: BRAND.green });
const nodePos = [
[-3.2, 1.7, 0.4], [3.4, 0.9, -0.2], [-3.5, -0.9, 0.6],
[3.3, -1.4, 0.3], [0.2, 2.7, -0.6], [-0.4, -2.4, 0.5],
];
const nodes = [];
const flowMat = new THREE.LineBasicMaterial({ color: BRAND.green, transparent: true, opacity: 0.4 });
nodePos.forEach((p) => {
const n = new THREE.Mesh(new THREE.SphereGeometry(0.1, 16, 16), nodeMat);
n.position.set(...p); obj.add(n); nodes.push(n);
const mid = new THREE.Vector3(...p).lerp(cube.position, 0.5);
mid.z += 0.8; mid.y += 0.3;
const curve = new THREE.QuadraticBezierCurve3(new THREE.Vector3(...p), mid, cube.position.clone());
const line = new THREE.Line(new THREE.BufferGeometry().setFromPoints(curve.getPoints(28)), flowMat.clone());
obj.add(line);
});
// faint brand flow arcs in the background (echo the SVG's data-flow curves)
const arcMat = new THREE.LineBasicMaterial({ color: BRAND.blue, transparent: true, opacity: 0.18 });
for (let i = 0; i < 4; i++) {
const a = (i / 4) * Math.PI * 2;
const r = 4.4;
const c = new THREE.QuadraticBezierCurve3(
new THREE.Vector3(Math.cos(a) * r, Math.sin(a) * 2.6, -2.5),
new THREE.Vector3(0, 0, -1),
new THREE.Vector3(Math.cos(a + 2) * r, Math.sin(a + 2) * 2.6, -2.5));
const l = new THREE.Line(new THREE.BufferGeometry().setFromPoints(c.getPoints(40)),
i % 2 ? arcMat : arcMat.clone());
if (i % 2) l.material.color.set(BRAND.purple);
obj.add(l);
}
/* ── post processing ──────────────────────────────────────────────────────── */
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
const bloom = new UnrealBloomPass(new THREE.Vector2(1, 1), 0.8, 0.55, 0.62);
composer.addPass(bloom);
composer.addPass(new OutputPass());
/* ── size to container ──────────────────────────────────────────────────── */
function resize() {
const w = mount.clientWidth || 560;
const h = Math.round(w * 0.78);
mount.style.height = h + 'px';
renderer.setSize(w, h); composer.setSize(w, h);
bloom.setSize(w, h);
camera.aspect = w / h; camera.updateProjectionMatrix();
}
resize();
new ResizeObserver(resize).observe(mount);
/* ── motion + render ────────────────────────────────────────────────────── */
const pointer = { x: 0, y: 0, tx: 0, ty: 0 };
if (!reduce) addEventListener('pointermove', (e) => {
pointer.tx = (e.clientX / innerWidth - 0.5);
pointer.ty = (e.clientY / innerHeight - 0.5);
}, { passive: true });
const clock = new THREE.Timer();
let visible = true;
new IntersectionObserver(([e]) => { visible = e.isIntersecting; },
{ threshold: 0.01 }).observe(mount);
function frame(ts) {
clock.update(ts);
const t = clock.getElapsed();
if (visible) {
pointer.x += (pointer.tx - pointer.x) * 0.05;
pointer.y += (pointer.ty - pointer.y) * 0.05;
// gentle front-facing oscillation (keeps the 3/4 view + code face toward camera)
obj.rotation.y = -0.32 + (reduce ? 0 : Math.sin(t * 0.32) * 0.34) + pointer.x * 0.4;
obj.rotation.x = -0.3 + pointer.y * 0.2;
const pulse = 1 + Math.sin(t * 2) * 0.12;
core.scale.setScalar(reduce ? 1 : pulse);
nodes.forEach((n, i) => n.scale.setScalar(reduce ? 1 : 1 + Math.sin(t * 2.4 + i) * 0.25));
composer.render();
}
if (reduce) { composer.render(); return; } // single settle frame, then stop
requestAnimationFrame(frame);
}
if (reduce) { obj.rotation.y = 0.5; requestAnimationFrame(frame); }
else renderer.setAnimationLoop(frame);
}
/* monospace label rendered to a CanvasTexture sprite-plane */
function makeLabel(text, x, y, z, color) {
const c = document.createElement('canvas'); c.width = 256; c.height = 64;
const g = c.getContext('2d');
g.fillStyle = '#' + new THREE.Color(color).getHexString();
g.font = '600 34px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace';
g.textBaseline = 'middle'; g.globalAlpha = 0.9;
g.fillText(text, 8, 34);
const tex = new THREE.CanvasTexture(c); tex.colorSpace = THREE.SRGBColorSpace;
const mat = new THREE.MeshBasicMaterial({ map: tex, transparent: true, depthWrite: false });
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1.0, 0.25), mat);
mesh.position.set(x, y, z);
return mesh;
}