Three Ratios · Same Eyes
Simulator
id
2604055175478
title
Three Ratios · Same Eyes
date
04/05/2026
text
Show source code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Three Ratios · Same Eyes</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;600&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #0a0a0f;
color: #c8b875;
font-family: 'JetBrains Mono', monospace;
overflow: hidden;
height: 100vh;
}
#header {
position: absolute; top: 12px; left: 0; width: 100%;
text-align: center; font-size: 11px; font-weight: 600;
letter-spacing: 3px; text-transform: uppercase;
color: #c8b875; z-index: 20;
}
#header span { color: #8a7a40; font-weight: 300; }
#controls {
position: absolute; top: 36px; left: 0; width: 100%;
display: flex; justify-content: center; gap: 6px; z-index: 20;
}
.btn {
background: rgba(200,184,117,0.08);
border: 1px solid rgba(200,184,117,0.25);
color: #c8b875;
font-family: 'JetBrains Mono', monospace;
font-size: 9px; letter-spacing: 1px;
padding: 6px 10px; cursor: pointer;
text-transform: uppercase; transition: all 0.3s;
}
.btn:hover { background: rgba(200,184,117,0.18); border-color: #c8b875; }
.btn.active { background: rgba(200,184,117,0.25); border-color: #c8b875; }
#container { display: flex; width: 100%; height: 100vh; }
.panel {
flex: 1; position: relative;
border-right: 1px solid rgba(200,184,117,0.08);
}
.panel:last-child { border-right: none; }
.panel:nth-child(2) {
border-left: 1px solid rgba(200,184,117,0.2);
border-right: 1px solid rgba(200,184,117,0.2);
}
.label {
position: absolute; bottom: 72px;
left: 0; width: 100%; text-align: center;
font-size: 15px; font-weight: 600;
z-index: 10; pointer-events: none; color: #c8b875;
}
.sublabel {
position: absolute; bottom: 54px;
left: 0; width: 100%; text-align: center;
font-size: 9px; letter-spacing: 1px;
color: #8a7a40; z-index: 10; pointer-events: none;
}
.stats {
position: absolute; bottom: 8px;
left: 0; width: 100%; text-align: center;
font-size: 8px; letter-spacing: 0.5px;
color: #8a7a40; z-index: 10; pointer-events: none;
line-height: 1.9;
}
.stats .v { color: #c8b875; }
</style>
</head>
<body>
<div id="header">CCFU <span>· Same rendering · Different λ · See the difference</span></div>
<div id="controls">
<button class="btn active" onclick="setMode('full')">Full Body</button>
<button class="btn" onclick="setMode('t1t3')">T₁+T₃</button>
<button class="btn" onclick="setMode('t2')">T₂</button>
<button class="btn" onclick="setMode('explode')">Explode</button>
<button class="btn" onclick="setMode('wireframe')">Wireframe</button>
<button class="btn" onclick="toggleSpin()">Spin</button>
</div>
<div id="container">
<div class="panel" id="panel-0">
<div class="label">λ = 1.2</div>
<div class="sublabel">vertex = (0, 0.833, 1.2)</div>
<div class="stats" id="stats-0"></div>
</div>
<div class="panel" id="panel-1">
<div class="label">λ = φ</div>
<div class="sublabel">vertex = (0, 1/φ, φ)</div>
<div class="stats" id="stats-1"></div>
</div>
<div class="panel" id="panel-2">
<div class="label">λ = 2.1</div>
<div class="sublabel">vertex = (0, 0.476, 2.1)</div>
<div class="stats" id="stats-2"></div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
const PHI = (1 + Math.sqrt(5)) / 2;
const RATIOS = [1.2, PHI, 2.1];
const COL_T1T3 = 0xc8a030;
const COL_T2 = 0x30a0c8;
const COL_EDGE = 0xffeedd;
const panelIds = ['panel-0','panel-1','panel-2'];
const allData = [];
let rotation = {x:0.3, y:0};
let isDragging = false, prevMouse = {x:0,y:0};
let autoSpin = true, currentMode = 'full';
let explodeAmt = 0, targetExplode = 0;
// Build vertices for any lambda — SAME indexing order
function buildVerts(lam) {
const il = 1/lam, v = [];
// 0-7: cube
for (let a of [-1,1]) for (let b of [-1,1]) for (let c of [-1,1])
v.push(new THREE.Vector3(a,b,c));
// 8-11: (0, ±1/λ, ±λ)
for (let s1 of [-1,1]) for (let s2 of [-1,1])
v.push(new THREE.Vector3(0, s1*il, s2*lam));
// 12-15: (±λ, 0, ±1/λ)
for (let s1 of [-1,1]) for (let s2 of [-1,1])
v.push(new THREE.Vector3(s1*lam, 0, s2*il));
// 16-19: (±1/λ, ±λ, 0)
for (let s1 of [-1,1]) for (let s2 of [-1,1])
v.push(new THREE.Vector3(s1*il, s2*lam, 0));
return v;
}
// Find faces using φ topology, return index arrays
function findFaceTopology() {
const vs = buildVerts(PHI);
const el = 2/PHI; // known edge length for φ
const eps = 0.01;
const near = (a,b) => Math.abs(a.distanceTo(b) - el) < eps;
const adj = [];
for (let i=0;i<vs.length;i++) {
adj[i] = [];
for (let j=0;j<vs.length;j++)
if (i!==j && near(vs[i],vs[j])) adj[i].push(j);
}
const faces = [], set = new Set();
for (let i=0;i<vs.length;i++)
for (let j of adj[i]) { if(j<=i) continue;
for (let k of adj[j]) { if(k<=i||k===i) continue;
for (let l of adj[k]) { if(l<=i||l===j||l===i) continue;
for (let m of adj[l]) { if(m<=i||m===k||m===j) continue;
if (adj[m].includes(i)) {
const f=[i,j,k,l,m].sort((a,b)=>a-b);
const key=f.join(',');
if (!set.has(key)) {
const n=new THREE.Vector3().crossVectors(
new THREE.Vector3().subVectors(vs[f[1]],vs[f[0]]),
new THREE.Vector3().subVectors(vs[f[2]],vs[f[0]])
).normalize();
let ok=true;
for(let fi=3;fi<5;fi++)
if(Math.abs(new THREE.Vector3().subVectors(vs[f[fi]],vs[f[0]]).dot(n))>eps){ok=false;break;}
if(ok){set.add(key);faces.push(f);}
}
}
}
}
}
}
// Order each face cyclically
const ordered = [];
for (let face of faces) {
const o = [face[0]], r = new Set(face.slice(1));
while (r.size > 0) {
let found = false;
for (let x of r) {
if (near(vs[o[o.length-1]], vs[x])) {
o.push(x); r.delete(x); found=true; break;
}
}
if (!found) break;
}
if (o.length === 5) ordered.push(o);
}
return ordered;
}
const FACE_TOPOLOGY = findFaceTopology();
function triAngles(v0,v1,v2) {
const a=v1.distanceTo(v2), b=v0.distanceTo(v2), c=v0.distanceTo(v1);
const A=Math.acos(Math.max(-1,Math.min(1,(b*b+c*c-a*a)/(2*b*c))))*180/Math.PI;
const B=Math.acos(Math.max(-1,Math.min(1,(a*a+c*c-b*b)/(2*a*c))))*180/Math.PI;
return [A,B,180-A-B].sort((a,b)=>a-b);
}
function triRatio(v0,v1,v2) {
const s=[v0.distanceTo(v1),v1.distanceTo(v2),v2.distanceTo(v0)].sort((a,b)=>a-b);
return s[2]/s[0];
}
function buildPanel(idx) {
const lam = RATIOS[idx];
const panel = document.getElementById(panelIds[idx]);
const scene = new THREE.Scene();
const cam = new THREE.PerspectiveCamera(40, panel.clientWidth/panel.clientHeight, 0.1, 100);
cam.position.set(4.5, 3.5, 5.5); cam.lookAt(0,0,0);
const ren = new THREE.WebGLRenderer({antialias:true, alpha:true});
ren.setSize(panel.clientWidth, panel.clientHeight);
ren.setPixelRatio(Math.min(window.devicePixelRatio, 2));
ren.setClearColor(0x0a0a0f, 1);
panel.appendChild(ren.domElement);
scene.add(new THREE.AmbientLight(0x444444, 0.6));
const d1=new THREE.DirectionalLight(0xffeedd, 0.8); d1.position.set(5,8,6); scene.add(d1);
const d2=new THREE.DirectionalLight(0x6666aa, 0.3); d2.position.set(-5,-3,-4); scene.add(d2);
const group = new THREE.Group();
const vs = buildVerts(lam);
const tris = [];
// Use SAME topology for ALL lambdas
for (let face of FACE_TOPOLOGY) {
const pts = face.map(i => vs[i]);
const fc = new THREE.Vector3();
for (let p of pts) fc.add(p);
fc.divideScalar(5);
// Fan: T1(0,1,2) T2(0,2,3) T3(0,3,4) — same structure always
const triDefs = [
{v:[pts[0],pts[1],pts[2]], role:'t1t3'},
{v:[pts[0],pts[2],pts[3]], role:'t2'},
{v:[pts[0],pts[3],pts[4]], role:'t1t3'}
];
for (let td of triDefs) {
const color = td.role==='t1t3' ? COL_T1T3 : COL_T2;
const geom = new THREE.BufferGeometry();
const pos = new Float32Array([
td.v[0].x,td.v[0].y,td.v[0].z,
td.v[1].x,td.v[1].y,td.v[1].z,
td.v[2].x,td.v[2].y,td.v[2].z
]);
geom.setAttribute('position', new THREE.BufferAttribute(pos, 3));
geom.computeVertexNormals();
const mat = new THREE.MeshPhongMaterial({
color, side: THREE.DoubleSide,
transparent: true, opacity: 0.85,
shininess: 70, specular: 0x333333
});
const mesh = new THREE.Mesh(geom, mat);
const eGeom = new THREE.BufferGeometry();
const ep = new Float32Array([
td.v[0].x,td.v[0].y,td.v[0].z, td.v[1].x,td.v[1].y,td.v[1].z,
td.v[1].x,td.v[1].y,td.v[1].z, td.v[2].x,td.v[2].y,td.v[2].z,
td.v[2].x,td.v[2].y,td.v[2].z, td.v[0].x,td.v[0].y,td.v[0].z
]);
eGeom.setAttribute('position', new THREE.BufferAttribute(ep, 3));
const eMat = new THREE.LineBasicMaterial({color: COL_EDGE, transparent: true, opacity: 0.4});
const edges = new THREE.LineSegments(eGeom, eMat);
const cont = new THREE.Group();
cont.add(mesh); cont.add(edges);
group.add(cont);
tris.push({cont,mesh,edges,mat,eMat,role:td.role,fc:fc.clone(),
angles: triAngles(td.v[0],td.v[1],td.v[2]),
ratio: triRatio(td.v[0],td.v[1],td.v[2])
});
}
}
// Vertex spheres
for (let i=0; i<vs.length; i++) {
const sg = new THREE.SphereGeometry(0.035, 8, 8);
const sm = new THREE.MeshPhongMaterial({
color: i<8 ? 0xffffff : 0xc8b875,
emissive: i<8 ? 0x222222 : 0x332200
});
const s = new THREE.Mesh(sg, sm);
s.position.copy(vs[i]); group.add(s);
}
scene.add(group);
// Stats
const statsEl = document.getElementById('stats-'+idx);
const s1 = tris.find(t=>t.role==='t1t3');
const s2 = tris.find(t=>t.role==='t2');
if (s1 && s2) {
const a1 = s1.angles.map(a=>a.toFixed(1)+'°').join(' · ');
const a2 = s2.angles.map(a=>a.toFixed(1)+'°').join(' · ');
const r1 = s1.ratio.toFixed(4);
const r2 = s2.ratio.toFixed(4);
const isGolden1 = Math.abs(s1.ratio - PHI) < 0.01;
const isGolden2 = Math.abs(s2.ratio - PHI) < 0.01;
statsEl.innerHTML =
`T₁: <span class="v">${a1}</span> ratio <span class="v">${r1}</span>${isGolden1?' = φ':''}<br>`+
`T₂: <span class="v">${a2}</span> ratio <span class="v">${r2}</span>${isGolden2?' = φ':''}<br>`+
`φ = <span class="v">1.6180</span>`;
}
return {scene, cam, ren, group, tris};
}
for (let i=0; i<3; i++) allData.push(buildPanel(i));
function setMode(mode) {
currentMode = mode;
document.querySelectorAll('.btn').forEach(b=>b.classList.remove('active'));
event.target.classList.add('active');
targetExplode = mode==='explode' ? 1.0 : 0;
for (let sd of allData) for (let t of sd.tris) {
const is13 = t.role==='t1t3', is2 = t.role==='t2';
switch(mode) {
case 'full':
t.mat.opacity=0.85;
t.mat.color.setHex(is13?COL_T1T3:COL_T2);
t.eMat.opacity=0.4; t.mat.wireframe=false; break;
case 't1t3':
t.mat.opacity=is13?0.9:0.05;
t.mat.color.setHex(is13?COL_T1T3:0x111111);
t.eMat.opacity=is13?0.6:0.03; t.mat.wireframe=false; break;
case 't2':
t.mat.opacity=is2?0.9:0.05;
t.mat.color.setHex(is2?COL_T2:0x111111);
t.eMat.opacity=is2?0.6:0.03; t.mat.wireframe=false; break;
case 'explode':
t.mat.opacity=0.75;
t.mat.color.setHex(is13?COL_T1T3:COL_T2);
t.eMat.opacity=0.45; t.mat.wireframe=false; break;
case 'wireframe':
t.mat.opacity=0.12; t.mat.wireframe=true;
t.eMat.opacity=0.7; break;
}
}
}
function toggleSpin() {
autoSpin = !autoSpin;
const b = document.querySelectorAll('.btn');
b[b.length-1].classList.toggle('active', autoSpin);
}
document.addEventListener('mousedown', e=>{isDragging=true; prevMouse={x:e.clientX,y:e.clientY};});
document.addEventListener('mousemove', e=>{if(isDragging){
rotation.y+=(e.clientX-prevMouse.x)*0.005;
rotation.x+=(e.clientY-prevMouse.y)*0.005;
prevMouse={x:e.clientX,y:e.clientY};}});
document.addEventListener('mouseup', ()=>isDragging=false);
document.addEventListener('touchstart', e=>{isDragging=true;
prevMouse={x:e.touches[0].clientX,y:e.touches[0].clientY};});
document.addEventListener('touchmove', e=>{if(isDragging){
rotation.y+=(e.touches[0].clientX-prevMouse.x)*0.005;
rotation.x+=(e.touches[0].clientY-prevMouse.y)*0.005;
prevMouse={x:e.touches[0].clientX,y:e.touches[0].clientY};}});
document.addEventListener('touchend', ()=>isDragging=false);
document.addEventListener('wheel', e=>{
for(let sd of allData) sd.cam.position.multiplyScalar(e.deltaY>0?1.05:0.95);
});
function animate() {
requestAnimationFrame(animate);
if (autoSpin && !isDragging) rotation.y += 0.003;
explodeAmt += (targetExplode - explodeAmt) * 0.05;
for (let sd of allData) {
for (let t of sd.tris) {
const d = t.fc.clone().normalize().multiplyScalar(explodeAmt*1.5);
t.cont.position.copy(d);
}
sd.group.rotation.x = rotation.x;
sd.group.rotation.y = rotation.y;
sd.ren.render(sd.scene, sd.cam);
}
}
animate();
window.addEventListener('resize', ()=>{
for (let i=0; i<3; i++) {
const p = document.getElementById(panelIds[i]);
allData[i].cam.aspect = p.clientWidth/p.clientHeight;
allData[i].cam.updateProjectionMatrix();
allData[i].ren.setSize(p.clientWidth, p.clientHeight);
}
});
</script>
</body>
</html>tweet_url
SHA-256