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