EARTH — Trefoil Hopfion

by Kikem

HTML

<div id="app">
  <header>
    <h1>Trefoil Hopfion</h1>
    <span>Topological soliton — every field line is linked</span>
  </header>

  <div id="center">
    <canvas id="canvas"></canvas>
    <div id="overlay">
      <div id="qBadge">Q = 1</div>
      <div id="qLabel">HOPF INVARIANT</div>
    </div>
  </div>

  <div id="right">
    <div class="st">Display</div>
    <label class="toggle"><input type="checkbox" id="chkTrefoil" checked><span>Trefoil knot T(2,3)</span></label>
    <label class="toggle"><input type="checkbox" id="chkFibers" checked><span>Hopf fibers</span></label>
    <label class="toggle"><input type="checkbox" id="chkTorus"><span>Reference torus</span></label>
    <label class="toggle"><input type="checkbox" id="chkAuto" checked><span>Auto-rotate</span></label>

    <div class="sg">
      <label>Fiber count: <span class="v" id="fiberCountVal">18</span></label>
      <input type="range" id="fiberCount" min="3" max="36" step="3" value="18">
    </div>
    <div class="sg">
      <label>Fiber opacity: <span class="v" id="fiberOpVal">0.70</span></label>
      <input type="range" id="fiberOp" min="0.1" max="1" step="0.05" value="0.7">
    </div>

    <div class="st">Topology</div>
    <div class="pr"><span class="l">Knot type</span><span class="v">T(2, 3) trefoil</span></div>
    <div class="pr"><span class="l">Crossing number</span><span class="v">3</span></div>
    <div class="pr"><span class="l">Eisenstein norm</span><span class="v">N(2,3) = 19</span></div>
    <div class="pr"><span class="l">Hopf invariant Q</span><span class="v">1</span></div>
    <div class="pr"><span class="l">Fiber linking Lk</span><span class="v">1 (pairwise)</span></div>
    <div class="pr"><span class="l">Generation</span><span class="v">1st (electron, u, d)</span></div>

    <div class="st">What You See</div>
    <div class="ib">
      <strong>Gold curve</strong> — the trefoil knot. This is the topology that defines first-generation...

CSS

*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'Segoe UI',system-ui,sans-serif;background:#060810;color:#e0e0e0;overflow:hidden}
#app{display:grid;grid-template-columns:1fr 280px;grid-template-rows:52px 1fr;height:100vh}
header{grid-column:1/-1;background:#0e1320;display:flex;align-items:center;padding:0 24px;border-bottom:1px solid #1a2540}
header h1{font-size:17px;font-weight:600;color:#ffcc44}
header span{margin-left:16px;font-size:12px;color:#6a7a94}
#center{position:relative;overflow:hidden;cursor:grab}
#center:active{cursor:grabbing}
#canvas{width:100%;height:100%;display:block}
#right{background:#0b0f1a;border-left:1px solid #1a2540;overflow-y:auto;padding:16px}
.st{font-size:10px;text-transform:uppercase;letter-spacing:1.5px;color:#4a6a9a;margin:16px 0 8px;font-weight:600}
.st:first-child{margin-top:0}
.toggle{display:flex;align-items:center;gap:8px;font-size:12px;color:#8ab4e0;cursor:pointer;margin-bottom:6px;user-select:none}
.toggle input{accent-color:#4a8aff;width:14px;height:14px}
.sg{margin-bottom:12px}
.sg label{display:block;font-size:11px;color:#5a7a9a;margin-bottom:3px}
.sg input[type=range]{width:100%;accent-color:#4a8aff}
.sg .v{font-size:11px;color:#5a8abf;font-family:Consolas,monospace}
.pr{display:flex;justify-content:space-between;padding:4px 0;font-size:12px;border-bottom:1px solid #111825}
.pr .l{color:#5a7a9a}.pr .v{font-family:Consolas,monospace;color:#a0d0ff;font-size:11px}
.ib{background:#080c16;border:1px solid #162030;border-radius:6px;padding:12px;margin-bottom:14px;font-size:11px;line-height:1.7;color:#6a8aaa}
.ib strong{color:#c0a040}.ib em{color:#88bbff;font-style:normal}
.ib code{color:#a0c0f0;font-family:Consolas,monospace}
#overlay{position:absolute;top:16px;left:20px;pointer-events:none}
#qBadge{font-size:42px;font-weight:800;color:#ffcc44;text-shadow:0 0 30px #ffaa0060,0 0 60px #ff880030;opacity:0.7}
#qLabel{font-size:11px;color:#8a7a50;letter-spacing:1px;margin-top:-4px}

JavaScript

// ═══════════════════════════════════════════════════════════
// 3D Rotation
// ═══════════════════════════════════════════════════════════
function rX(p,a){const c=Math.cos(a),s=Math.sin(a);return{x:p.x,y:p.y*c-p.z*s,z:p.y*s+p.z*c}}
function rY(p,a){const c=Math.cos(a),s=Math.sin(a);return{x:p.x*c+p.z*s,y:p.y,z:-p.x*s+p.z*c}}

// ═══════════════════════════════════════════════════════════
// Geometry generators
// ═══════════════════════════════════════════════════════════
const TORUS_R = 1.1, TORUS_r = 0.45;

function genTrefoil(n) {
  const pts = [];
  for (let i = 0; i <= n; i++) {
    const t = (i / n) * Math.PI * 2;
    pts.push({
      x: (TORUS_R + TORUS_r * Math.cos(3 * t)) * Math.cos(2 * t),
      y: (TORUS_R + TORUS_r * Math.cos(3 * t)) * Math.sin(2 * t),
      z: TORUS_r * Math.sin(3 * t)
    });
  }
  return pts;
}

function genHopfFiber(theta, phi, n) {
  const pts = [];
  const ct = Math.cos(theta / 2), st = Math.sin(theta / 2);
  const SCALE = 2.2;
  for (let i = 0; i <= n; i++) {
    const a = (i / n) * Math.PI * 2;
    const x1 = ct * Math.cos(a);
    const x2 = ct * Math.sin(a);
    const x3 = st * Math.cos(a - phi);
    const x4 = st * Math.sin(a - phi);
    const denom = 1 + x4;
    if (denom < 0.12) { pts.push(null); continue; }
    const s = SCALE / denom;
    pts.push({ x: x1 * s, y: x2 * s, z: x3 * s });
  }
  return pts;
}

function genTorusWire(nM, nL, ppc) {
  const curves = [];
  // Meridians
  for (let i = 0; i < nM; i++) {
    const theta = (i / nM) * Math.PI * 2;
    const pts = [];
    for (let j = 0; j <= ppc; j++) {
      const phi = (j / ppc) * Math.PI * 2;
      pts.push({
        x: (TORUS_R + TORUS_r * Math.cos(phi)) * Math.cos(theta),
        y: (TORUS_R + TORUS_r * Math.cos(phi)) * Math.sin(theta),
        z: TORUS_r * Math.sin(phi)
      });
    }
    curves.push(pts);
  }
  // Longitudes
  for (let i = 0; i < nL; i++) {
    const phi = (i / nL) * Math.PI * 2;
...