// Direction A — UTILITY-ROOM v2 // The current site's vibe, refined. Same warm-dark amber palette, but the // boot theatrics are replaced with a quieter "session resumed" opener that // gets out of the way faster. Wireframes are promoted from background // distractions to deliberate ambient ornament. Adds availability + contact // sections so the site actually pitches commission work. (function () { const W = window.IM_WIRE; const C = window.IM_CONTENT; const { useState, useEffect, useRef } = React; // Short, dry boot lines. The original site's sequence was 21 jokes — // user noted it "gets in the way of content" — so this one keeps four // lines of plausible session-restore chatter and gets out of the way. const BOOT_LINES = [ { txt: '[ok] session restored' }, { txt: '[ok] tmux: 4 panes · 1 dirty buffer (sketchbook.md)' }, { txt: '[ok] last seen 14m ago — chair by the bench' }, { txt: '[ok] welcome back' }, ]; // ── three.js background scene ───────────────────────────────── // Recreates the original site's wireframe background: five rotating // platonic solids + a torus knot, with subtle mouse parallax. The // amber/green palette + low opacities keep it deep in the back. function BgScene() { const canvasRef = useRef(null); useEffect(() => { if (!canvasRef.current || !window.THREE) return; const THREE = window.THREE; const canvas = canvasRef.current; const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true }); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setClearColor(0x000000, 0); const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 100); camera.position.z = 8; const mAmber = new THREE.LineBasicMaterial({ color: 0xe8a030, opacity: 0.14, transparent: true }); const mGreen = new THREE.LineBasicMaterial({ color: 0x39d353, opacity: 0.07, transparent: true }); const mDim = new THREE.LineBasicMaterial({ color: 0xe8a030, opacity: 0.05, transparent: true }); const wf = (geo, mat, x, y, z) => { const mesh = new THREE.LineSegments(new THREE.WireframeGeometry(geo), mat); mesh.position.set(x, y, z); scene.add(mesh); return mesh; }; const ico = wf(new THREE.IcosahedronGeometry(2.8, 1), mAmber, -1.5, 0.5, -3); const tknot = wf(new THREE.TorusKnotGeometry(1.1, 0.3, 120, 16), mGreen, 4.5, -1.2, -4); const oct = wf(new THREE.OctahedronGeometry(1.5, 2), mDim, -5.5, -2.5, -2); const dodec = wf(new THREE.DodecahedronGeometry(1.0, 0), mAmber, 5.0, 2.8, -1.5); const tet = wf(new THREE.TetrahedronGeometry(0.8, 2), mGreen, -3.5, 3.0, -1); // Each body gets its own slow rotation rate + a slight positional // drift driven by sine waves so nothing reads as orbiting a single // axis. Rates picked low — like watching the storms of Jupiter // from a porthole, not a screensaver. Times are in radians/frame // at 60fps; ~0.0004 ≈ a full revolution every ~4 minutes. const bodies = [ { m: ico, rx: 0.00028, ry: 0.00041, rz: 0, bob: [0.6, 0.0009, 0], home: [-1.5, 0.5, -3] }, { m: tknot, rx: 0.00044, ry: 0.00033, rz: 0.00018, bob: [0.4, 0.0011, 1.7], home: [ 4.5,-1.2,-4] }, { m: oct, rx:-0.00031, ry: 0, rz: 0.00038, bob: [0.5, 0.0008, 3.1], home: [-5.5,-2.5,-2] }, { m: dodec, rx: 0.00018, ry: 0.00046, rz: 0, bob: [0.7, 0.00065,4.6], home: [ 5.0, 2.8,-1.5] }, { m: tet, rx: 0.00036, ry: 0, rz:-0.00041, bob: [0.55,0.00095,2.3], home: [-3.5, 3.0,-1] }, ]; let mx = 0, my = 0; const onMove = (e) => { // Halve the parallax too — the original was punchy, this should // breathe. mx = (e.clientX / window.innerWidth - 0.5) * 0.35; my = -(e.clientY / window.innerHeight - 0.5) * 0.35; }; const onResize = () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }; document.addEventListener('mousemove', onMove); window.addEventListener('resize', onResize); const start = performance.now(); let raf, alive = true; const tick = () => { if (!alive) return; raf = requestAnimationFrame(tick); const t = (performance.now() - start) / 1000; for (const b of bodies) { b.m.rotation.x += b.rx; b.m.rotation.y += b.ry; b.m.rotation.z += b.rz; // sine-wave drift around each body's home position — amplitude // ~0.4–0.7 world units, periods of 90–150s. Reads as floating. const [amp, freq, phase] = b.bob; const [hx, hy, hz] = b.home; b.m.position.x = hx + Math.sin(t * freq * Math.PI * 2 + phase) * amp; b.m.position.y = hy + Math.cos(t * freq * 0.8 * Math.PI * 2 + phase * 1.3) * amp * 0.6; b.m.position.z = hz + Math.sin(t * freq * 0.5 * Math.PI * 2 + phase * 0.7) * amp * 0.3; } camera.position.x += (mx - camera.position.x) * 0.02; camera.position.y += (my - camera.position.y) * 0.02; camera.lookAt(scene.position); renderer.render(scene, camera); }; tick(); return () => { alive = false; cancelAnimationFrame(raf); document.removeEventListener('mousemove', onMove); window.removeEventListener('resize', onResize); renderer.dispose(); }; }, []); return ; } // Font URL — any .ttf/.otf/.woff (NOT .woff2; opentype.js can't decode // that natively). VT323 is the same CRT-terminal face used in the hero // name, so the monogram reads as drawn by the same hand. Swap freely // to any TTF — typewriter (Special Elite, Cutive Mono), antique book // (Old Standard TT), or your own font hosted anywhere CORS-friendly. // raw.githubusercontent.com is used instead of jsdelivr's @gh proxy — // the google/fonts repo as a whole exceeds jsdelivr's 50MB package // size limit, so most files in it 403. raw.githubusercontent.com // serves the files directly with permissive CORS. const FONT_URL = 'https://raw.githubusercontent.com/google/fonts/main/ofl/russoone/RussoOne-Regular.ttf'; let FONT_CACHE = null; // promise, reused across mounts // Convert opentype.js path commands to THREE.Shape objects via ShapePath. // opentype uses y-down (screen-style); THREE.Shape uses y-up — we negate // y as we go. The y flip also reverses winding direction, so we pass // isCCW=true to toShapes() so outer outlines (now CCW post-flip) are // treated as outer rather than as holes. function shapesFromOpentype(font, text, fontSize) { const otPath = font.getPath(text, 0, 0, fontSize); const sp = new THREE.ShapePath(); for (const c of otPath.commands) { if (c.type === 'M') sp.moveTo(c.x, -c.y); else if (c.type === 'L') sp.lineTo(c.x, -c.y); else if (c.type === 'Q') sp.quadraticCurveTo(c.x1, -c.y1, c.x, -c.y); else if (c.type === 'C') sp.bezierCurveTo(c.x1, -c.y1, c.x2, -c.y2, c.x, -c.y); // 'Z' (close) is implicit — the next 'M' starts a new sub-path. } return sp.toShapes(true); } // Small in-card Three.js scene for the hero wireframe slot. // // Builds "IM" by extruding glyph shapes from a real font (Helvetiker // Bold ≈ Arial Bold) — replaces the previous hand-traced polygons, // which got the M's diagonals tangled. Rendered as wireframe edges + // vertex dots to match the rest of the site's aesthetic. // // Interaction: idle sine wobble by default; pointer drag rotates the // monogram (Y from horizontal drag, X from vertical, clamped so it // can't flip upside-down). Idle smoothly fades out while dragging and // back in on release, so the user's pose isn't snapped away. Double- // click resets to the default orientation. function HeroWire() { const ref = useRef(null); useEffect(() => { if (!ref.current || !window.THREE) return; const THREE = window.THREE; const host = ref.current; let w = host.clientWidth, h = host.clientHeight; const canvas = document.createElement('canvas'); host.innerHTML = ''; host.appendChild(canvas); const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true }); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); renderer.setSize(w, h); renderer.setClearColor(0x000000, 0); const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(40, w / h, 0.1, 100); camera.position.z = 8; // Each edge is rendered as a thin cylinder rather than a line, so // we can control thickness (LineBasicMaterial.linewidth is capped // to 1px in WebGL on most browsers). MeshBasicMaterial keeps the // flat amber look — no shading on the tubes. const wireMat = new THREE.MeshBasicMaterial({ color: 0xe8a030, transparent: true, opacity: 0.75 }); const pivot = new THREE.Group(); scene.add(pivot); // Drag state — accumulated user rotation, blended with idle wobble. let dragging = false, lastX = 0, lastY = 0; let userRotY = 0, userRotX = 0; let idleAmt = 1; // 0..1, fades to 0 while dragging const X_CLAMP = 0.7; // ~40°, keeps the monogram from flipping // Fetch font (cached across mounts) then build geometry. Component // may unmount before fetch resolves; `alive` guards the add to scene. // opentype.parse needs the raw bytes — hence arrayBuffer, not json. let alive = true; if (!FONT_CACHE) { FONT_CACHE = fetch(FONT_URL) .then(r => r.arrayBuffer()) .then(buf => window.opentype.parse(buf)); } FONT_CACHE.then((font) => { if (!alive) return; // fontSize=100 keeps the path values in a comfortable numeric range; // we re-fit to the camera frustum after computing the bbox anyway. // fontSize=100 puts path coords in the ~0–100 range; the depth is // expressed in those same units (gets normalized by the fit-scale // below alongside the rest of the geometry). const shapes = shapesFromOpentype(font, 'IM', 100); const geo = new THREE.ExtrudeGeometry(shapes, { depth: 30, bevelEnabled: false, curveSegments: 12, }); geo.computeBoundingBox(); const bb = geo.boundingBox; // Center the monogram on the pivot's origin so rotation feels right. geo.translate( -(bb.max.x + bb.min.x) / 2, -(bb.max.y + bb.min.y) / 2, -(bb.max.z + bb.min.z) / 2, ); // Fit width to the camera frustum at z=8. ~3.5 world units wide // reads well at the in-card scale. const targetW = 3.5; const fitScale = targetW / (bb.max.x - bb.min.x); geo.scale(fitScale, fitScale, fitScale); // Silhouette/hard edges only — no face triangulation. const edges = new THREE.EdgesGeometry(geo, 1); const posArr = edges.attributes.position.array; const edgeCount = posArr.length / 6; // Build a single unit cylinder, then instance it once per edge // with a per-edge matrix that positions + orients + stretches it // between the two endpoints. Radius is the line thickness — tune // here to taste; ~2% of letter width reads as a confident stroke. const radius = 0.028; const base = new THREE.CylinderGeometry(radius, radius, 1, 8, 1, false); base.translate(0, 0.5, 0); // origin at one end so scale.y = length const inst = new THREE.InstancedMesh(base, wireMat, edgeCount); const dummy = new THREE.Object3D(); const up = new THREE.Vector3(0, 1, 0); const dir = new THREE.Vector3(); const quat = new THREE.Quaternion(); for (let i = 0; i < edgeCount; i++) { const o = i * 6; const ax = posArr[o], ay = posArr[o+1], az = posArr[o+2]; const bx = posArr[o+3], by = posArr[o+4], bz = posArr[o+5]; dir.set(bx - ax, by - ay, bz - az); const len = dir.length(); dir.normalize(); quat.setFromUnitVectors(up, dir); dummy.position.set(ax, ay, az); dummy.quaternion.copy(quat); dummy.scale.set(1, len, 1); dummy.updateMatrix(); inst.setMatrixAt(i, dummy.matrix); } inst.instanceMatrix.needsUpdate = true; pivot.add(inst); }).catch(() => { /* fetch failure — leave the stage empty */ }); const start = performance.now(); let raf = 0; const tick = () => { if (!alive) return; raf = requestAnimationFrame(tick); const t = (performance.now() - start) / 1000; // Ease idleAmt toward 0 when dragging, 1 otherwise — avoids // snapping the pose when a drag starts or ends. idleAmt += ((dragging ? 0 : 1) - idleAmt) * 0.08; const idleY = Math.sin(t * 0.28) * 0.55 * idleAmt; const idleX = Math.sin(t * 0.22 + 0.7) * 0.18 * idleAmt; const idleZ = Math.sin(t * 0.17 + 1.4) * 0.04 * idleAmt; pivot.rotation.y = userRotY + idleY; pivot.rotation.x = THREE.MathUtils.clamp(userRotX + idleX, -X_CLAMP, X_CLAMP); pivot.rotation.z = idleZ; renderer.render(scene, camera); }; tick(); // Pointer drag — single handler set covers mouse + touch + pen. // Capture on pointerdown so the drag survives the cursor leaving // the canvas; touch-action: none (set in CSS) suppresses scroll. const onDown = (e) => { dragging = true; lastX = e.clientX; lastY = e.clientY; host.classList.add('grabbing'); try { host.setPointerCapture(e.pointerId); } catch {} }; const onMove = (e) => { if (!dragging) return; const dx = e.clientX - lastX, dy = e.clientY - lastY; lastX = e.clientX; lastY = e.clientY; userRotY += dx * 0.01; userRotX = THREE.MathUtils.clamp(userRotX + dy * 0.01, -X_CLAMP, X_CLAMP); }; const onUp = (e) => { dragging = false; host.classList.remove('grabbing'); try { host.releasePointerCapture(e.pointerId); } catch {} }; const onDouble = () => { userRotY = 0; userRotX = 0; }; host.addEventListener('pointerdown', onDown); host.addEventListener('pointermove', onMove); host.addEventListener('pointerup', onUp); host.addEventListener('pointercancel', onUp); host.addEventListener('dblclick', onDouble); const onResize = () => { w = host.clientWidth; h = host.clientHeight; if (!w || !h) return; camera.aspect = w / h; camera.updateProjectionMatrix(); renderer.setSize(w, h); }; window.addEventListener('resize', onResize); return () => { alive = false; cancelAnimationFrame(raf); window.removeEventListener('resize', onResize); host.removeEventListener('pointerdown', onDown); host.removeEventListener('pointermove', onMove); host.removeEventListener('pointerup', onUp); host.removeEventListener('pointercancel', onUp); host.removeEventListener('dblclick', onDouble); renderer.dispose(); }; }, []); return
; } // ── prompt fragment ──────────────────────────────────────────── function Prompt({ cmd, blink = false }) { return ( {C.identity.user} @ {C.identity.host} :~$ {cmd && {cmd}} {blink && } ); } // ── ambient gutter — slow log ticker ─────────────────────────── function AmbientLog() { const [lines, setLines] = useState(() => C.ambient.slice(0, 5)); useEffect(() => { let i = 5; const id = setInterval(() => { setLines((prev) => { const next = prev.slice(1); next.push(C.ambient[i % C.ambient.length]); i++; return next; }); }, 4200); return () => clearInterval(id); }, []); return (| {k} | {v} |
{C.identity.pitch}
{p}
)}| {name} | {sub} |
{ex.description}
{o.body}