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>
166 lines
6.0 KiB
HTML
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>
|