guts/web/dev/laneA_molecules.html
jing 9f42a2e9ab [lane A] Molecules as pickups: real gut chemistry, procedural, 1 draw each
PROTOTYPE + proposal, not wired into boot. Pickups are B's systems, their
economy is C's, object art is D's — this is a renderer and an argument, offered
to all three. Bench: web/dev/laneA_molecules.html · docs/MOLECULES.md

THE PITCH: the pickups already in L2 ARE molecules. C authored `nutrient`,
`mucin`, `B12` and `antacid ammo` before any of this existed — those are the real
biochemistry of a digestive tract, and glucose/cobalamin/bicarbonate are real
compounds with known shapes. We don't need to invent a pickup language; we need
to stop hiding the one the fiction handed us. GUTS is already a science game (an
eosinophil is a real white blood cell, candida a real yeast, the hiatus a real
choke point) — it just doesn't LOOK like one, because everything on screen is
tinted tissue.

THE RULE THAT MAKES IT WORK: CPK is the scanner. Everything identified is drawn
in the standard element palette (O red, N blue, P orange, metals pink);
everything else stays monochrome tissue. Against six biomes of tinted wall a CPK
molecule is the ONLY saturated foreign colour on screen — it reads as artificial,
valuable and targetable with NO HUD marker. Proof: round2_molecules_in_canal.png.
That's the ART_BIBLE's synthetic-scanner fiction paying rent, and it hands Lane E
a free HUD palette.

WHY PROCEDURAL AND NOT MODELBEAST — and I'm authorised to burn the GPU: a
molecule's shape is already known exactly. Glucose is a hexagonal ring because it
IS one. FLUX+TRELLIS gives a plausible blob in 3-8 min that a chemist clocks as
wrong and that can never be re-derived. Ball-and-stick from an atom list is exact,
rebuilds in ms, and is ONE DRAW CALL per molecule (measured: 7 molecules = 7
draws / 26.5k tris). The split is a principle: small molecules = procedural
(exact geometry known); proteins/enzymes = MODELBEAST blobs (real ones are
3000-atom blobs nobody reads — and PIPELINE says blobby organics are TRELLIS's
sweet spot); cells/creatures = MODELBEAST, D's lane, unaffected.

BUILT: water (chaff) · bicarbonate (antacid ammo — HCO3- + HCl is the real
neutralisation, it genuinely fizzes CO2) · glucose (nutrient/score) · caffeine
(overdrive — really gut-absorbed, needs no tutorial) · ATP (boost — the orange
triphosphate tail IS the charge, a pickup that's a diagram of its own mechanic) ·
capsaicin (burn hazard — long greasy tail reads wrong on sight) · B12 (rare
treasure — a cobalt in a corrin cage, the only metal vitamin; C authored it as a
pickup already). Size is a free rarity signal: `unit` mode keeps TRUE relative
sizes, so water is a speck and B12 a chandelier.

Found by rendering it, which is why the bench exists: fuseRing returns vertices
starting adjacent to B, and bonding them from A instead draws a chord across the
ring — still a valid 5-cycle so nothing errors, it just looked like a squashed
pentagon. Fixed in caffeine + ATP's adenine, adjacency documented.

Stated plainly: geometry is idealised (correct connectivity/rings/bond orders,
believable angles, authored flat) NOT crystallographic — real glucose is a chair,
not a hexagon. The B12 is a simplified corrin core. Untested above ~7 on screen;
fifty would want instancing.

qa GREEN.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 19:52:33 +10:00

166 lines
6.0 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>GUTS — Lane A molecule harness</title>
<!--
Lane A's molecule bench (web/dev/, per ruling #5 — dev-only, never shipped).
PROTOTYPE for a cross-lane proposal: see docs/MOLECULES.md and web/js/world/molecule.js.
http://localhost:8145/dev/laneA_molecules.html spin every molecule in a grid
...?mol=atp one molecule, big
...?shots=1 hide the overlay
DBG.one(id) show a single molecule, framed
DBG.grid() back to the grid
DBG.pose(t) deterministic frame at rotation t (rAF is dead in a hidden pane)
DBG.post(name) POST the canvas to pipeline/shot_sink.py (house evidence tool)
-->
<style>
html, body { margin: 0; height: 100%; overflow: hidden; background: #05070a; }
canvas { display: block; }
#dbg { position: fixed; left: 10px; bottom: 10px; color: #7fdfff;
font: 12px/1.5 ui-monospace, monospace; text-shadow: 0 0 4px #000;
pointer-events: none; white-space: pre; }
</style>
<script type="importmap">
{ "imports": { "three": "../vendor/three.module.js",
"three/addons/": "../vendor/addons/" } }
</script>
</head>
<body>
<div id="dbg"></div>
<script type="module">
import * as THREE from 'three';
import { buildMolecule, listMolecules, createMoleculeMaterial, MOLECULES } from '../js/world/molecule.js';
const p = new URLSearchParams(location.search);
const on = (k) => p.has(k) && p.get(k) !== '0';
const renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(innerWidth || 1280, innerHeight || 720);
document.body.appendChild(renderer.domElement);
renderer.info.autoReset = false;
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x05070a);
const camera = new THREE.PerspectiveCamera(45, (innerWidth || 1280) / (innerHeight || 720), 0.1, 200);
// ONE material for every molecule on screen — colour is per-vertex, so they still batch.
const material = createMoleculeMaterial();
const root = new THREE.Group();
scene.add(root);
const ids = listMolecules();
let spinners = [];
function clear() {
for (const c of [...root.children]) { root.remove(c); c.geometry.dispose(); }
spinners = [];
}
function grid() {
clear();
const cols = Math.ceil(Math.sqrt(ids.length));
const rows = Math.ceil(ids.length / cols);
const pitch = 2.6;
ids.forEach((id, i) => {
const m = buildMolecule(id, { fit: 1, detail: 2, material });
m.position.set((i % cols - (cols - 1) / 2) * pitch, -((i / cols | 0) - (rows - 1) / 2) * pitch, 0);
root.add(m); spinners.push(m);
});
camera.position.set(0, 0, cols * pitch * 1.25);
camera.lookAt(0, 0, 0);
}
/** Every molecule at its TRUE relative size, in a row — the "what is this worth" read. */
function trueScale(unit = 0.42, gap = 0.5) {
clear();
const order = [...ids].sort((a, b) => MOLECULES[a].atoms.length - MOLECULES[b].atoms.length);
const built = order.map((id) => {
const m = buildMolecule(id, { unit, detail: 2, material });
m.geometry.computeBoundingSphere();
return { m, r: m.geometry.boundingSphere.radius };
});
const total = built.reduce((a, b) => a + b.r * 2 + gap, 0) - gap;
let x = -total / 2;
for (const { m, r } of built) {
x += r; m.position.set(x, 0, 0); x += r + gap;
root.add(m); spinners.push(m);
}
camera.position.set(0, 0, total * 0.62); // frame the row, whatever it adds up to
camera.lookAt(0, 0, 0);
return total;
}
function one(id) {
clear();
const m = buildMolecule(id, { fit: 1, detail: 3, material });
root.add(m); spinners.push(m);
camera.position.set(0, 0, 3.1);
camera.lookAt(0, 0, 0);
return m.userData;
}
if (p.has('mol')) one(p.get('mol')); else if (on('true')) trueScale(); else grid();
const dbgEl = document.getElementById('dbg');
if (on('shots')) dbgEl.style.display = 'none';
window.DBG = {
scene, camera, renderer, root, MOLECULES,
one, grid, trueScale, ids,
size(w = 1280, h = 720) {
renderer.setPixelRatio(1);
renderer.setSize(w, h, false);
camera.aspect = w / h; camera.updateProjectionMatrix();
return [renderer.domElement.width, renderer.domElement.height];
},
/** Deterministic frame: every molecule at rotation t. The pane runs tabs hidden, so rAF
* never fires and nothing renders unless we ask — same trap as the world harness. */
pose(t = 0.6, w = 1280, h = 720) {
this.size(w, h);
for (const m of spinners) { m.rotation.set(t * 0.55, t, t * 0.2); }
renderer.render(scene, camera);
const r = { draws: renderer.info.render.calls, tris: renderer.info.render.triangles };
renderer.info.reset();
return r;
},
post(name, port = 8144) {
return new Promise((res, rej) => renderer.domElement.toBlob((b) => {
if (!b || b.size < 8) return rej(new Error('empty canvas — pose() first?'));
fetch(`http://localhost:${port}/shot/${name}.png`, { method: 'POST', body: b })
.then((r) => (r.ok ? res(`${name}.png ${(b.size / 1024) | 0} KB`) : rej(new Error(`sink ${r.status}`))))
.catch(rej);
}, 'image/png'));
},
};
addEventListener('resize', () => {
camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
let t = 0, last = performance.now();
function frame(now) {
const dt = Math.min((now - last) / 1000, 0.05); last = now; t += dt * 0.6;
for (const m of spinners) m.rotation.set(t * 0.55, t, t * 0.2);
renderer.render(scene, camera);
if (!on('shots')) {
const u = spinners.length === 1 ? spinners[0].userData : null;
dbgEl.textContent = u
? `${u.name} ${u.formula}\nrole: ${u.role}\n${u.atoms} atoms · ${u.tris} tris · ${renderer.info.render.calls} draw`
: `${spinners.length} molecules · ${renderer.info.render.calls} draws · ${renderer.info.render.triangles} tris`;
}
renderer.info.reset();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>
</body>
</html>