wu-xing-pair-breaker.html raw

   1  <!DOCTYPE html>
   2  <html lang="en">
   3  <head>
   4  <meta charset="UTF-8">
   5  <meta name="viewport" content="width=device-width, initial-scale=1.0">
   6  <title>否 The Star — Wu Xing Pair-Breaker</title>
   7  <style>
   8  @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;600&display=swap');
   9  *{margin:0;padding:0;box-sizing:border-box}
  10  body{background:#0a0a0a;color:#c0c0c0;font-family:'JetBrains Mono',monospace;font-size:12px;overflow:hidden;width:100vw;height:100vh}
  11  #c{display:block}
  12  #panel{position:fixed;top:0;right:0;width:280px;height:100vh;background:rgba(8,8,8,0.94);border-left:1px solid #1a1a1a;padding:14px;overflow-y:auto;z-index:10;backdrop-filter:blur(12px)}
  13  h1{font-size:11px;font-weight:600;color:#e8d5a3;letter-spacing:.14em;text-transform:uppercase;border-bottom:1px solid #2a2218;padding-bottom:7px;margin-bottom:10px}
  14  .gt{font-size:9px;font-weight:600;color:#555;text-transform:uppercase;letter-spacing:.14em;margin:11px 0 5px}
  15  .p{display:flex;align-items:center;justify-content:space-between;margin-bottom:4px}
  16  .p label{color:#777;font-size:10px}
  17  .pr{display:flex;align-items:center;gap:5px}
  18  input[type=range]{-webkit-appearance:none;width:85px;height:2px;background:#333;outline:none}
  19  input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:10px;height:10px;background:#e8d5a3;border-radius:50%;cursor:pointer}
  20  .val{color:#e8d5a3;font-size:10px;min-width:30px;text-align:right;font-variant-numeric:tabular-nums}
  21  .btn{background:#1a1714;border:1px solid #2a2218;color:#e8d5a3;font-family:inherit;font-size:10px;padding:5px 10px;cursor:pointer;letter-spacing:.08em;text-transform:uppercase;margin-top:3px;width:100%;transition:all .15s}
  22  .btn:hover{background:#2a2218;border-color:#e8d5a3}
  23  .btn.active{background:#e8d5a3;color:#0a0a0a}
  24  .sep{height:1px;background:#1a1a1a;margin:8px 0}
  25  .legend{display:flex;flex-wrap:wrap;gap:5px;margin-top:5px}
  26  .leg{display:flex;align-items:center;gap:3px;font-size:9px;color:#777}
  27  .leg .dot{width:7px;height:7px;border-radius:50%}
  28  .nb{display:flex;align-items:center;gap:3px;margin-bottom:3px;font-size:9px}
  29  .nb .icon{width:14px;height:14px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:7px;color:#000;font-weight:700}
  30  .nb .nm{width:36px;color:#888}
  31  .nb .bg{flex:1;height:10px;background:#111;border-radius:2px;overflow:hidden;position:relative}
  32  .nb .fl{height:100%;transition:width 0.04s linear;border-radius:2px}
  33  .nb .th{position:absolute;top:0;height:100%;width:2px;background:#e8d5a3;opacity:0.6}
  34  .nb .vl{width:22px;text-align:right;color:#666;font-size:8px;font-variant-numeric:tabular-nums}
  35  #info{position:fixed;bottom:14px;left:14px;z-index:10;font-size:9px;color:#444}
  36  #tl{position:fixed;top:18px;left:22px;z-index:10}
  37  #tl h2{font-size:14px;font-weight:300;color:#e8d5a3;letter-spacing:.2em;text-transform:uppercase;margin-bottom:3px}
  38  #tl .sub{font-size:9px;color:#555;letter-spacing:.12em;text-transform:uppercase}
  39  #rotor-label{font-size:9px;color:#e8d5a3;margin-top:4px;letter-spacing:.06em;text-align:center}
  40  #fire-seq{font-size:8px;color:#555;margin-top:4px;line-height:1.6;max-height:55px;overflow-y:auto}
  41  #fire-seq b{color:#e8d5a3}
  42  .res-row{display:flex;align-items:center;gap:4px;font-size:8px;margin-bottom:3px}
  43  .res-row .r-label{color:#888;width:70px}
  44  .res-row .r-bar{flex:1;height:4px;border-radius:2px;background:#111;overflow:hidden}
  45  .res-row .r-fill{height:100%;border-radius:2px;transition:width .08s}
  46  .steady{font-size:9px;color:#e8d5a3;text-align:center;margin:4px 0;letter-spacing:.06em}
  47  .cb{display:flex;align-items:center;gap:3px;margin-bottom:2px;font-size:8px;height:12px}
  48  .cb .cl{width:50px;color:#555;text-align:right}
  49  .cb .bg{flex:1;height:6px;background:#111;border-radius:1px;overflow:hidden}
  50  .cb .fl{height:100%;border-radius:1px}
  51  .cb .vl{width:22px;text-align:right;color:#666;font-variant-numeric:tabular-nums}
  52  </style>
  53  </head>
  54  <body>
  55  <canvas id="c"></canvas>
  56  <div id="tl"><h2>否 The Star</h2><div class="sub">Wu Xing Pair-Breaker · Kè Cycle Rotor · 5 nodes · 19μ</div></div>
  57  
  58  <div id="panel">
  59  <h1>否 Pi Device — Solve</h1>
  60  
  61  <div class="gt">Geometry</div>
  62  <div class="p"><label>Radius</label><div class="pr"><input type=range id="sR" min=2 max=8 step=.1 value=4><span class=val id="vR">4.0</span></div></div>
  63  <div class="p"><label>Vert. Spread</label><div class="pr"><input type=range id="sV" min=.5 max=4 step=.1 value=1.8><span class=val id="vV">1.8</span></div></div>
  64  <div class="p"><label>Bar Thick</label><div class="pr"><input type=range id="sT" min=.05 max=.3 step=.01 value=.12><span class=val id="vT">0.12</span></div></div>
  65  <div class="p"><label>Coil Turns/m</label><div class="pr"><input type=range id="sD" min=8 max=40 step=1 value=20><span class=val id="vD">20</span></div></div>
  66  <div class="p"><label>Core μᵣ</label><div class="pr"><input type=range id="sMu" min=100 max=5000 step=100 value=1000><span class=val id="vMu">1000</span></div></div>
  67  
  68  <div class="sep"></div>
  69  <div class="gt">Ignition</div>
  70  <div class="p"><label>Seed Node</label><div class="pr"><input type=range id="sSeed" min=0 max=4 step=1 value=0><span class=val id="vSeed">木</span></div></div>
  71  <div class="p"><label>Coupling k</label><div class="pr"><input type=range id="sK" min=.1 max=.95 step=.05 value=.6><span class=val id="vK">0.60</span></div></div>
  72  <div class="p"><label>Damping</label><div class="pr"><input type=range id="sDamp" min=.001 max=.06 step=.001 value=.008><span class=val id="vDamp">0.008</span></div></div>
  73  <div class="p"><label>Pair Energy</label><div class="pr"><input type=range id="sPair" min=0 max=1 step=.01 value=.35><span class=val id="vPair">0.35</span></div></div>
  74  <div class="p"><label>Load (extract)</label><div class="pr"><input type=range id="sLoad" min=0 max=.5 step=.01 value=0><span class=val id="vLoad">0.00</span></div></div>
  75  <div class="p"><label>Glow</label><div class="pr"><input type=range id="sGlow" min=0 max=2 step=.1 value=1.2><span class=val id="vGlow">1.2</span></div></div>
  76  <button class="btn" id="btnMag">🧲 Wave Magnet</button>
  77  <button class="btn" id="btnReset">Reset</button>
  78  
  79  <div class="sep"></div>
  80  <div class="gt">Dimmer (木 potentiometer)</div>
  81  <div class="p"><label>木 R<sub>pot</sub></label><div class="pr"><input type=range id="sDim" min=0 max=1 step=.01 value=0><span class=val id="vDim">0.00</span></div></div>
  82  <div style="font-size:8px;color:#444;margin-top:2px">0 = full bright · 1 = off (short to ground)</div>
  83  
  84  <div class="sep"></div>
  85  <div class="gt">Steady State</div>
  86  <div class="steady">ΣC = 19 · μᵣ = <span id="muDisp">1000</span> · I₀ = <span id="steadyI">19000</span></div>
  87  
  88  <div class="sep"></div>
  89  <div class="gt">Node Capacitors (Fibonacci)</div>
  90  <div id="node-caps"></div>
  91  
  92  <div class="sep"></div>
  93  <div class="gt">Three Resonances</div>
  94  <div id="resonances">
  95    <div class="res-row"><div class="r-label" style="color:#c4a040">木1+火2=土3</div><div class="r-bar"><div class="r-fill" id="res0" style="background:#c4a040;width:0"></div></div></div>
  96    <div class="res-row"><div class="r-label" style="color:#a0a0a8">火2+土3=金5</div><div class="r-bar"><div class="r-fill" id="res1" style="background:#a0a0a8;width:0"></div></div></div>
  97    <div class="res-row"><div class="r-label" style="color:#4060c4">土3+金5=水8</div><div class="r-bar"><div class="r-fill" id="res2" style="background:#4060c4;width:0"></div></div></div>
  98  </div>
  99  
 100  <div class="sep"></div>
 101  <div class="gt">Central Vortex</div>
 102  <div class="cb"><div class="cl" style="color:#a060e0">克 flux</div><div class="bg"><div class="fl" id="fluxBar" style="background:linear-gradient(90deg,#4422aa,#8844ff);width:0"></div></div><div class="vl" id="fluxV">0</div></div>
 103  <div class="cb"><div class="cl" style="color:#44cc66">pair → ΣC</div><div class="bg"><div class="fl" id="pairBar" style="background:linear-gradient(90deg,#226633,#44cc66);width:0"></div></div><div class="vl" id="pairV">0</div></div>
 104  
 105  <div class="sep"></div>
 106  <div class="gt">Hysteresis (Core Memory)</div>
 107  <div class="cb"><div class="cl" style="color:#cc8844">core B̄</div><div class="bg"><div class="fl" id="satBar" style="background:linear-gradient(90deg,#663311,#cc8844);width:0"></div></div><div class="vl" id="satV">0</div></div>
 108  <div id="rem-bars"></div>
 109  <div style="font-size:8px;color:#444;margin-top:3px;text-align:center" id="hystState">dormant</div>
 110  
 111  <div class="sep"></div>
 112  <div class="gt">Kè Rotor</div>
 113  <div style="text-align:center"><canvas id="rc" width="130" height="130"></canvas></div>
 114  <div id="rotor-label">awaiting ignition</div>
 115  
 116  <div class="sep"></div>
 117  <div class="gt">Fire Sequence (克 dominant)</div>
 118  <div id="fire-seq"></div>
 119  
 120  <div class="sep"></div>
 121  <div class="gt">Display</div>
 122  <div class="p"><label>Fractal Depth</label><div class="pr"><input type=range id="sFrac" min=0 max=5 step=1 value=3><span class=val id="vFrac">3</span></div></div>
 123  <button class="btn" id="btnVac">Vacuum Chamber</button>
 124  <button class="btn active" id="btnFlux">Flux Lines</button>
 125  <button class="btn" id="btnWire">Wireframe</button>
 126  
 127  <div class="sep"></div>
 128  <div class="legend">
 129  <div class="leg"><div class="dot" style="background:#4a9e4a"></div>木 C=1</div>
 130  <div class="leg"><div class="dot" style="background:#c44040"></div>火 C=2</div>
 131  <div class="leg"><div class="dot" style="background:#c4a040"></div>土 C=3</div>
 132  <div class="leg"><div class="dot" style="background:#a0a0a8"></div>金 C=5</div>
 133  <div class="leg"><div class="dot" style="background:#4060c4"></div>水 C=8</div>
 134  </div>
 135  <div style="font-size:8px;color:#333;margin-top:6px;text-align:center">克 Wood→Earth→Water→Fire→Metal→Wood</div>
 136  </div>
 137  
 138  <div id="info">Drag to rotate · Scroll to zoom · 否 The Star · Kè Rotor</div>
 139  
 140  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
 141  <script>
 142  const PHI=(1+Math.sqrt(5))/2;
 143  const P=[
 144    {n:'Wood',c:'木',col:0x4a9e4a,css:'#4a9e4a',a:-Math.PI/2, cap:1},
 145    {n:'Fire',c:'火',col:0xc44040,css:'#c44040',a:-Math.PI/2+2*Math.PI/5, cap:2},
 146    {n:'Earth',c:'土',col:0xc4a040,css:'#c4a040',a:-Math.PI/2+4*Math.PI/5, cap:3},
 147    {n:'Metal',c:'金',col:0xa0a0a8,css:'#a0a0a8',a:-Math.PI/2+6*Math.PI/5, cap:5},
 148    {n:'Water',c:'水',col:0x4060c4,css:'#4060c4',a:-Math.PI/2+8*Math.PI/5, cap:8}
 149  ];
 150  const SUM_C=19; // 1+2+3+5+8 — prime
 151  
 152  // Kè cycle order: 0→2→4→1→3→0 (Wood→Earth→Water→Fire→Metal)
 153  const KE_ORDER=[0,2,4,1,3];
 154  // Edges: 0-4 shēng (i→i+1%5), 5-9 kè (i→i+2%5)
 155  function eNodes(e){return e<5?[e,(e+1)%5]:[e-5,(e-5+2)%5]}
 156  
 157  const G={R:4,V:1.8,T:.12,D:20,mu:1000,seed:0,k:.6,damp:.008,pair:.35,load:0,dim:0,glow:1.2,frac:3,vac:false,wire:false,showFlux:true};
 158  const THRESHOLD=0.7;
 159  
 160  const sim={
 161    charge:[0,0,0,0,0],
 162    fireIntensity:[0,0,0,0,0],
 163    edgeGlow:new Float64Array(10),
 164    flux:0, running:false,
 165    fireHistory:[],
 166    rotorAngle:0, rotorSpeed:0, totalFirings:0,
 167    resonance:[0,0,0],
 168    totalEnergy:0,
 169    pairFeedback:0,
 170    refractory:[0,0,0,0,0],
 171    barMag:new Float64Array(10),
 172    remnant:new Float64Array(5),
 173    coreSaturation:0
 174  };
 175  
 176  // 3D
 177  const canvas=document.getElementById('c');
 178  const scene=new THREE.Scene();
 179  scene.fog=new THREE.FogExp2(0x0a0a0a,0.02);
 180  const camera=new THREE.PerspectiveCamera(50,innerWidth/innerHeight,0.1,200);
 181  const renderer=new THREE.WebGLRenderer({canvas,antialias:true});
 182  renderer.setSize(innerWidth,innerHeight);
 183  renderer.setPixelRatio(Math.min(devicePixelRatio,2));
 184  renderer.toneMapping=THREE.ACESFilmicToneMapping;
 185  renderer.toneMappingExposure=1.2;
 186  
 187  scene.add(new THREE.AmbientLight(0x1a1410,0.6));
 188  const dL=new THREE.DirectionalLight(0xffe8c0,0.8);dL.position.set(5,8,4);scene.add(dL);
 189  const ctrL=new THREE.PointLight(0x8844ff,0,20);scene.add(ctrL);
 190  // Kè-colored point light for the star pattern
 191  const keL=new THREE.PointLight(0xff4444,0,15);keL.position.set(0,1,0);scene.add(keL);
 192  scene.add(new THREE.GridHelper(20,40,0x111111,0x0d0d0d)).position.y=-3;
 193  
 194  const devG=new THREE.Group();scene.add(devG);
 195  
 196  const coreM=new THREE.MeshStandardMaterial({color:0x2a2a2a,metalness:.85,roughness:.35});
 197  const coreMW=new THREE.MeshStandardMaterial({color:0x2a2a2a,metalness:.85,roughness:.35,wireframe:true});
 198  const copM=new THREE.MeshStandardMaterial({color:0xb87333,metalness:.9,roughness:.25});
 199  const copMW=new THREE.MeshStandardMaterial({color:0xb87333,metalness:.9,roughness:.25,wireframe:true});
 200  const nodeMs=P.map(p=>new THREE.MeshBasicMaterial({color:p.col,transparent:true,opacity:.5}));
 201  const edgeGMs=[];
 202  for(let e=0;e<10;e++){edgeGMs.push(new THREE.MeshBasicMaterial({color:P[eNodes(e)[0]].col,transparent:true,opacity:0}))}
 203  const cntM=new THREE.MeshBasicMaterial({color:0x8844ff,transparent:true,opacity:0,side:2});
 204  const cntM2=new THREE.MeshBasicMaterial({color:0xff4466,transparent:true,opacity:0,side:2});
 205  
 206  function nPos(i,R,y){return new THREE.Vector3(Math.cos(P[i].a)*R,y||0,Math.sin(P[i].a)*R)}
 207  
 208  // === NONLINEAR CORE PHYSICS ===
 209  // Base coupling topology (kè-dominant)
 210  // Continuous ferromagnetic core = tight coupling. 
 211  // kè path shares a bar segment — very high coupling.
 212  // shēng path goes through adjacent bar — still good.
 213  function computeBaseCoupling(){
 214    const C=[];
 215    for(let i=0;i<5;i++){
 216      C[i]=[];
 217      for(let j=0;j<5;j++){
 218        if(i===j){C[i][j]=0;continue;}
 219        const isKe=(i+2)%5===j;
 220        const isSheng=(i+1)%5===j;
 221        const isKeRecv=(i+3)%5===j;
 222        if(isKe) C[i][j]=G.k*0.9;          // primary kè: tight
 223        else if(isSheng) C[i][j]=G.k*0.45;  // shēng: regenerative
 224        else if(isKeRecv) C[i][j]=G.k*0.2;  // secondary kè
 225        else C[i][j]=G.k*0.1;               // residual through core
 226      }
 227    }
 228    return C;
 229  }
 230  let baseCoupling=computeBaseCoupling();
 231  
 232  // Per-bar magnetization state (0..1, fraction of saturation)
 233  sim.barMag=new Float64Array(10);
 234  // Core remnant field per node (hysteresis — what's left after discharge)
 235  sim.remnant=new Float64Array(5);
 236  
 237  // B-H saturation curve: soft knee, doesn't kill coupling until very high saturation
 238  // A continuous core distributes flux — individual bars don't saturate easily
 239  function muEff(magnetization){
 240    // Gentle cubic: stays above 0.5 until magnetization > 0.8
 241    const x=Math.min(1,magnetization);
 242    return 1.0 - x*x*x*0.7;
 243  }
 244  
 245  // Effective coupling: saturation is GLOBAL (continuous core), not per-bar
 246  function effectiveCoupling(i,j){
 247    const base=baseCoupling[i][j];
 248    if(base===0)return 0;
 249    // Global core saturation — flux distributes across whole core
 250    let totalMag=0;for(let e=0;e<10;e++)totalMag+=sim.barMag[e];
 251    const globalSat=totalMag/10;
 252    return base*muEff(globalSat);
 253  }
 254  
 255  function simStep(dt){
 256    if(!sim.running)return;
 257    const nc=[...sim.charge];
 258    
 259    // === FIRING (with nonlinear coupling) ===
 260    for(let i=0;i<5;i++){
 261      const cap=P[i].cap;
 262      const norm=sim.charge[i]/cap;
 263      
 264      if(sim.refractory[i]>0){
 265        sim.refractory[i]-=dt;
 266        continue;
 267      }
 268      
 269      if(norm>=THRESHOLD){
 270        const energy=sim.charge[i]; // full discharge
 271        nc[i]=0;
 272        sim.refractory[i]=0.008*cap; // short refractory — fast recovery
 273        sim.fireIntensity[i]=1.0;
 274        sim.totalFirings++;
 275        sim.fireHistory.push({node:i,t:performance.now()});
 276        if(sim.fireHistory.length>60)sim.fireHistory.shift();
 277        
 278        sim.edgeGlow[i+5]=1.0;
 279        sim.edgeGlow[i]=0.4;
 280        
 281        // Magnetize outgoing bars (gradual — continuous core distributes)
 282        sim.barMag[i]=Math.min(1,sim.barMag[i]+energy*0.06);     // shēng bar
 283        sim.barMag[i+5]=Math.min(1,sim.barMag[i+5]+energy*0.1);  // kè bar
 284        
 285        // Distribute via STATE-DEPENDENT coupling
 286        for(let j=0;j<5;j++){
 287          if(i===j)continue;
 288          nc[j]+=energy*effectiveCoupling(i,j);
 289        }
 290        
 291        // Central flux — nonlinear: constructive interference
 292        sim.flux+=energy*0.5;
 293        
 294        // Rotor impulse
 295        sim.rotorSpeed+=energy*0.12;
 296        
 297        // Remnant field at firing node (hysteresis memory)
 298        sim.remnant[i]=Math.min(0.4,sim.remnant[i]+energy*0.06);
 299        
 300        // Resonance tracking
 301        if(i===2)sim.resonance[0]=1.0;
 302        if(i===3)sim.resonance[1]=1.0;
 303        if(i===4)sim.resonance[2]=1.0;
 304      }
 305    }
 306    
 307    // === PAIR-BOND ENERGY FEEDBACK ===
 308    // Above flux threshold, the quasiperiodic field disrupts dark pairs.
 309    // Released energy couples back through the core.
 310    // Linear base + quadratic bonus as coherence builds.
 311    const fluxThreshold=0.12;
 312    if(sim.flux>fluxThreshold){
 313      const excess=sim.flux-fluxThreshold;
 314      const pairEnergy=(excess*G.pair*3 + excess*excess*G.pair*8)*dt;
 315      sim.pairFeedback=pairEnergy;
 316      for(let i=0;i<5;i++){
 317        // Pair energy couples proportional to cap + remnant bias (hysteresis directs it)
 318        const weight=(P[i].cap/SUM_C)*(1+sim.remnant[i]*3);
 319        nc[i]+=pairEnergy*weight;
 320      }
 321      // Breaking pairs costs some flux, but less than what's returned
 322      sim.flux-=pairEnergy*0.2;
 323    } else {
 324      sim.pairFeedback=0;
 325    }
 326    
 327    // === LOAD EXTRACTION ===
 328    if(G.load>0){
 329      for(let i=0;i<5;i++){
 330        nc[i]*=(1-G.load*dt*3);
 331      }
 332      sim.rotorSpeed*=(1-G.load*dt*2);
 333    }
 334    
 335    // === DIMMER: variable resistor to ground on 木 (node 0) ===
 336    // At dim=0, no drain (full bright). At dim=1, all charge bleeds instantly (off).
 337    // The potentiometer drains charge from Wood continuously.
 338    // This weakens the cascade because Wood initiates each kè revolution.
 339    if(G.dim>0){
 340      const drainRate=G.dim*G.dim*40; // quadratic: gentle at low, savage at high
 341      nc[0]*=(1/(1+drainRate*dt));
 342      // At high resistance, Wood's threshold is effectively unreachable,
 343      // which starves the rest of the Fibonacci sequence
 344    }
 345    
 346    // === DECAY (nonlinear — saturated bars decay slower due to remnant magnetization) ===
 347    for(let i=0;i<5;i++){
 348      nc[i]*=(1-G.damp*dt*10);
 349      // Remnant field slowly charges caps (residual flux in core)
 350      nc[i]+=sim.remnant[i]*dt*0.5;
 351      nc[i]=Math.max(0,Math.min(P[i].cap,nc[i]));
 352      sim.fireIntensity[i]*=(1-dt*12);
 353      sim.edgeGlow[i]*=(1-dt*14);
 354      sim.edgeGlow[i+5]*=(1-dt*10);
 355    }
 356    // Bar demagnetization — slower when more saturated (hysteresis)
 357    for(let e=0;e<10;e++){
 358      const demagRate=1.5*(1-sim.barMag[e]*0.7); // very slow when saturated
 359      sim.barMag[e]*=(1-dt*demagRate);
 360    }
 361    // Remnant field decays very slowly (this is the hysteresis diode effect)
 362    for(let i=0;i<5;i++){
 363      sim.remnant[i]*=(1-dt*0.15);
 364    }
 365    sim.flux=Math.max(0,sim.flux*(1-dt*1.5));
 366    sim.rotorSpeed*=(1-dt*0.12);
 367    sim.rotorAngle+=sim.rotorSpeed*dt;
 368    for(let r=0;r<3;r++)sim.resonance[r]*=(1-dt*4);
 369    
 370    sim.charge=nc;
 371    sim.totalEnergy=nc.reduce((a,b)=>a+b,0)+sim.flux;
 372    
 373    // Track core saturation for display
 374    let totalMag=0;for(let e=0;e<10;e++)totalMag+=sim.barMag[e];
 375    sim.coreSaturation=totalMag/10;
 376    
 377    if(sim.totalEnergy<0.0005&&sim.rotorSpeed<0.005&&sim.coreSaturation<0.01)sim.running=false;
 378  }
 379  
 380  let glowBars=[],nodeMeshes=[],capMeshes=[],glassNodes=[],centralS,centralI;
 381  // Flux visualization storage
 382  let barEndpoints=[]; // {from, to, dir, len} per edge
 383  let fluxParticles=[]; // [edge][particle] = {mesh, u}
 384  let fieldLineLoops=[]; // [edge][loop] = mesh
 385  let vortexLines=[]; // central vortex spiral meshes
 386  const PARTICLES_PER_BAR=7;
 387  const LOOPS_PER_BAR=3;
 388  
 389  function mkCyl(f,t,th,mat){
 390    const d=new THREE.Vector3().subVectors(t,f),l=d.length(),
 391      m=new THREE.Vector3().addVectors(f,t).multiplyScalar(.5),
 392      g=new THREE.CylinderGeometry(th,th,l,12),mesh=new THREE.Mesh(g,mat);
 393    mesh.position.copy(m);mesh.quaternion.setFromUnitVectors(new THREE.Vector3(0,1,0),d.normalize());
 394    return mesh;
 395  }
 396  
 397  function mkCoils(f,t,th,dens){
 398    const d=new THREE.Vector3().subVectors(t,f),l=d.length(),dn=d.clone().normalize(),
 399      cR=th*2.5,turns=Math.round(dens*l/G.R),steps=Math.max(16,turns*16),pts=[];
 400    for(let i=0;i<=steps;i++){
 401      const u=i/steps,a=u*turns*Math.PI*2;
 402      pts.push(new THREE.Vector3(Math.cos(a)*cR,(u-.5)*l*.85,Math.sin(a)*cR));
 403    }
 404    const curve=new THREE.CatmullRomCurve3(pts),
 405      g=new THREE.TubeGeometry(curve,Math.min(steps*2,512),th*.35,6,false),
 406      mesh=new THREE.Mesh(g,G.wire?copMW:copM),
 407      m=new THREE.Vector3().addVectors(f,t).multiplyScalar(.5);
 408    mesh.position.copy(m);mesh.quaternion.setFromUnitVectors(new THREE.Vector3(0,1,0),dn);
 409    return mesh;
 410  }
 411  
 412  function buildDevice(){
 413    while(devG.children.length)devG.remove(devG.children[0]);
 414    glowBars=[];nodeMeshes=[];capMeshes=[];glassNodes=[];
 415    baseCoupling=computeBaseCoupling();
 416    
 417    const R=G.R,V=G.V,T=G.T;
 418    const bG=new THREE.Group(),cG=new THREE.Group(),nG=new THREE.Group(),
 419          casG=new THREE.Group(),cenG=new THREE.Group();
 420    
 421    // === JUNCTION GEOMETRY ===
 422    // Each node has two junction points where bars physically meet:
 423    //   Upper: shēng incoming arrives + kè outgoing departs
 424    //   Lower: shēng outgoing departs + kè incoming arrives
 425    const jH=V*.4;
 426    function jPos(ni,upper){return new THREE.Vector3(Math.cos(P[ni].a)*R,upper?jH:-jH,Math.sin(P[ni].a)*R)}
 427    
 428    const capUnitMat=new THREE.MeshStandardMaterial({color:0x1a1a2a,metalness:.5,roughness:.5});
 429    const capUnitSize=T*1.0;
 430    
 431    function fibPolyVerts(n){
 432      const s=capUnitSize*2.5;
 433      if(n===1)return[new THREE.Vector3(0,0,0)];
 434      if(n===2)return[new THREE.Vector3(-s*.5,0,0),new THREE.Vector3(s*.5,0,0)];
 435      if(n===3){const v=[];for(let k=0;k<3;k++){const a=k*Math.PI*2/3-Math.PI/2;v.push(new THREE.Vector3(Math.cos(a)*s*.6,0,Math.sin(a)*s*.6))}return v}
 436      if(n===5){const v=[];for(let k=0;k<5;k++){const a=k*Math.PI*2/5-Math.PI/2;v.push(new THREE.Vector3(Math.cos(a)*s*.8,0,Math.sin(a)*s*.8))}return v}
 437      if(n===8){const h=s*.55,v=[];for(let x=-1;x<=1;x+=2)for(let y=-1;y<=1;y+=2)for(let z=-1;z<=1;z+=2)v.push(new THREE.Vector3(x*h,y*h,z*h));return v}
 438      return[new THREE.Vector3(0,0,0)];
 439    }
 440    function fibPolyEdges(n,verts){
 441      const pts=[];
 442      if(n===2){pts.push(verts[0],verts[1])}
 443      else if(n===3){for(let k=0;k<3;k++){pts.push(verts[k],verts[(k+1)%3])}}
 444      else if(n===5){for(let k=0;k<5;k++){pts.push(verts[k],verts[(k+1)%5])}}
 445      else if(n===8){for(let a=0;a<8;a++)for(let b=a+1;b<8;b++){const d=verts[a].clone().sub(verts[b]);if((d.x!==0?1:0)+(d.y!==0?1:0)+(d.z!==0?1:0)===1)pts.push(verts[a],verts[b])}}
 446      return pts;
 447    }
 448    
 449    const nodeR=T*2.8;
 450    for(let i=0;i<5;i++){
 451      const posUp=jPos(i,true),posLo=jPos(i,false);
 452      const posMid=new THREE.Vector3().addVectors(posUp,posLo).multiplyScalar(.5);
 453      
 454      // Upper junction sphere (core metal)
 455      const sUp=new THREE.Mesh(new THREE.SphereGeometry(nodeR,16,16),G.wire?coreMW:coreM);
 456      sUp.position.copy(posUp);nG.add(sUp);
 457      // Lower junction sphere (core metal)
 458      const sLo=new THREE.Mesh(new THREE.SphereGeometry(nodeR,16,16),G.wire?coreMW:coreM);
 459      sLo.position.copy(posLo);nG.add(sLo);
 460      nodeMeshes.push(sUp);
 461      
 462      // Vertical core bar connecting upper to lower
 463      const vLen=jH*2-nodeR*2;
 464      if(vLen>0){
 465        const vb=mkCyl(new THREE.Vector3(posUp.x,jH-nodeR,posUp.z),new THREE.Vector3(posLo.x,-jH+nodeR,posLo.z),T*.8,G.wire?coreMW:coreM);
 466        nG.add(vb);
 467      }
 468      
 469      // Glass sheaths
 470      const glassR=nodeR*1.5;
 471      const gMatUp=new THREE.MeshPhysicalMaterial({color:P[i].col,transparent:true,opacity:.06,roughness:.05,metalness:0,transmission:.88,side:2});
 472      const gMatLo=new THREE.MeshPhysicalMaterial({color:P[i].col,transparent:true,opacity:.06,roughness:.05,metalness:0,transmission:.88,side:2});
 473      const glUp=new THREE.Mesh(new THREE.SphereGeometry(glassR,16,16),gMatUp);
 474      glUp.position.copy(posUp);nG.add(glUp);
 475      const glLo=new THREE.Mesh(new THREE.SphereGeometry(glassR,16,16),gMatLo);
 476      glLo.position.copy(posLo);nG.add(glLo);
 477      glassNodes.push({matUp:gMatUp,matLo:gMatLo});
 478      
 479      // Fibonacci cap polyhedron — between junction balls
 480      const outD=posMid.clone().normalize();
 481      const polyPos=posMid.clone().add(outD.multiplyScalar(T*3));
 482      const capGroup=new THREE.Group();
 483      capGroup.position.copy(polyPos);
 484      const verts=fibPolyVerts(P[i].cap);
 485      for(let v=0;v<verts.length;v++){
 486        const cu=new THREE.Mesh(new THREE.CylinderGeometry(capUnitSize*.7,capUnitSize*.7,capUnitSize*1.4,8),capUnitMat.clone());
 487        cu.position.copy(verts[v]);capGroup.add(cu);
 488        const dot=new THREE.Mesh(new THREE.SphereGeometry(capUnitSize*.25,4,4),new THREE.MeshBasicMaterial({color:0xcc3333}));
 489        dot.position.copy(verts[v]);dot.position.y+=capUnitSize*.8;
 490        capGroup.add(dot);
 491      }
 492      const edgePts=fibPolyEdges(P[i].cap,verts);
 493      if(edgePts.length>0)capGroup.add(new THREE.LineSegments(new THREE.BufferGeometry().setFromPoints(edgePts),new THREE.LineBasicMaterial({color:P[i].col,transparent:true,opacity:.4})));
 494      nG.add(capGroup);
 495      capMeshes.push(capGroup);
 496    }
 497    
 498    // Bars — all terminate at junction spheres
 499    // Shēng (i→i+1): lower(i) → upper(i+1)
 500    // Kè (i→i+2): upper(i) → lower(i+2)
 501    for(let e=0;e<10;e++){
 502      const[a,b]=eNodes(e);
 503      let f,t;
 504      if(e<5){f=jPos(a,false);t=jPos(b,true)}
 505      else{f=jPos(a,true);t=jPos(b,false)}
 506      const isKe=e>=5;
 507      const thick=isKe?T*.85:T;
 508      bG.add(mkCyl(f,t,thick,G.wire?coreMW:coreM));
 509      cG.add(mkCoils(f,t,thick,G.D));
 510      const gm=edgeGMs[e];gm.opacity=0;
 511      const gThick=isKe?thick*2.5:thick*2;
 512      const d=new THREE.Vector3().subVectors(t,f),l=d.length(),
 513        mid=new THREE.Vector3().addVectors(f,t).multiplyScalar(.5),
 514        gg=new THREE.CylinderGeometry(gThick,gThick,l,8),
 515        gb=new THREE.Mesh(gg,gm);
 516      gb.position.copy(mid);gb.quaternion.setFromUnitVectors(new THREE.Vector3(0,1,0),d.normalize());
 517      glowBars.push(gb);bG.add(gb);
 518      barEndpoints[e]={from:f.clone(),to:t.clone(),dir:d.clone().normalize(),len:l};
 519    }
 520    
 521    // === FLUX PARTICLES along each bar ===
 522    const fluxG=new THREE.Group();
 523    fluxParticles=[];
 524    for(let e=0;e<10;e++){
 525      const bp=barEndpoints[e];
 526      const isKe=e>=5;
 527      const col=P[eNodes(e)[0]].col;
 528      const pSize=isKe?G.T*.6:G.T*.45;
 529      fluxParticles[e]=[];
 530      for(let p=0;p<PARTICLES_PER_BAR;p++){
 531        const mat=new THREE.MeshBasicMaterial({color:col,transparent:true,opacity:0});
 532        const mesh=new THREE.Mesh(new THREE.SphereGeometry(pSize,4,4),mat);
 533        const u=p/PARTICLES_PER_BAR;
 534        mesh.position.lerpVectors(bp.from,bp.to,u);
 535        fluxG.add(mesh);
 536        fluxParticles[e].push({mesh,mat,u});
 537      }
 538    }
 539    
 540    // === FIELD LINE LOOPS around each bar (B-field visualization) ===
 541    fieldLineLoops=[];
 542    for(let e=0;e<10;e++){
 543      const bp=barEndpoints[e];
 544      const isKe=e>=5;
 545      const col=new THREE.Color(P[eNodes(e)[0]].col);
 546      fieldLineLoops[e]=[];
 547      for(let li=0;li<LOOPS_PER_BAR;li++){
 548        const u=(li+1)/(LOOPS_PER_BAR+1); // position along bar
 549        const center=new THREE.Vector3().lerpVectors(bp.from,bp.to,u);
 550        // Build elliptical loop in plane perpendicular to bar direction
 551        const loopPts=[];
 552        const loopR=isKe?G.R*.18:G.R*.15;
 553        const loopH=loopR*.6; // elliptical: taller along bar
 554        // Need a perpendicular basis
 555        const up=bp.dir;
 556        let perp1=new THREE.Vector3(1,0,0);
 557        if(Math.abs(up.dot(perp1))>.9)perp1.set(0,1,0);
 558        perp1.crossVectors(up,perp1).normalize();
 559        const perp2=new THREE.Vector3().crossVectors(up,perp1).normalize();
 560        const segments=24;
 561        for(let s=0;s<=segments;s++){
 562          const a=s/segments*Math.PI*2;
 563          const r=loopR*(1+.15*Math.sin(a*2)); // slight figure-8 distortion
 564          const pt=center.clone()
 565            .add(perp1.clone().multiplyScalar(Math.cos(a)*r))
 566            .add(perp2.clone().multiplyScalar(Math.sin(a)*r))
 567            .add(up.clone().multiplyScalar(Math.sin(a)*loopH));
 568          loopPts.push(pt);
 569        }
 570        const loopGeo=new THREE.BufferGeometry().setFromPoints(loopPts);
 571        const loopMat=new THREE.LineBasicMaterial({color:col,transparent:true,opacity:0});
 572        const loopMesh=new THREE.Line(loopGeo,loopMat);
 573        fluxG.add(loopMesh);
 574        fieldLineLoops[e].push({mesh:loopMesh,mat:loopMat});
 575      }
 576    }
 577    
 578    // === CENTRAL VORTEX FIELD LINES ===
 579    vortexLines=[];
 580    // Two counter-rotating spirals: kè (inward, star pattern) and shēng (inward, pentagon)
 581    for(let v=0;v<2;v++){
 582      const isKe=v===0;
 583      const col=isKe?0xff4466:0x6644ff;
 584      const spiralPts=[];
 585      const turns=2.5;
 586      const rMax=G.R*.5;
 587      const rMin=G.R*.05;
 588      const hMax=isKe?G.V*.4:-G.V*.4;
 589      const dir=isKe?1:-1;
 590      const steps=80;
 591      for(let s=0;s<=steps;s++){
 592        const u=s/steps;
 593        const a=u*turns*Math.PI*2*dir;
 594        const r=rMax*(1-u)+rMin*u;
 595        const y=hMax*u*(1-u)*4; // parabolic height profile
 596        spiralPts.push(new THREE.Vector3(Math.cos(a)*r,y,Math.sin(a)*r));
 597      }
 598      const curve=new THREE.CatmullRomCurve3(spiralPts);
 599      const tubeGeo=new THREE.TubeGeometry(curve,steps,G.T*.2,4,false);
 600      const tubeMat=new THREE.MeshBasicMaterial({color:col,transparent:true,opacity:0});
 601      const tubeMesh=new THREE.Mesh(tubeGeo,tubeMat);
 602      fluxG.add(tubeMesh);
 603      vortexLines.push({mesh:tubeMesh,mat:tubeMat,isKe,baseAngle:0});
 604      // Second arm offset by π
 605      const spiralPts2=spiralPts.map(p=>{
 606        const p2=p.clone();p2.x=-p2.x;p2.z=-p2.z;return p2;
 607      });
 608      const curve2=new THREE.CatmullRomCurve3(spiralPts2);
 609      const tubeGeo2=new THREE.TubeGeometry(curve2,steps,G.T*.2,4,false);
 610      const tubeMat2=new THREE.MeshBasicMaterial({color:col,transparent:true,opacity:0});
 611      const tubeMesh2=new THREE.Mesh(tubeGeo2,tubeMat2);
 612      fluxG.add(tubeMesh2);
 613      vortexLines.push({mesh:tubeMesh2,mat:tubeMat2,isKe,baseAngle:Math.PI});
 614    }
 615    
 616    // Central — star-shaped for kè dominance
 617    centralS=new THREE.Mesh(new THREE.SphereGeometry(R*.2,32,32),cntM);
 618    cenG.add(centralS);
 619    // Icosahedron rotates with kè rotor
 620    centralI=new THREE.Mesh(new THREE.IcosahedronGeometry(R*.3,1),cntM2);
 621    cenG.add(centralI);
 622    
 623    // Pentagram wireframe at center (the star pattern)
 624    const starPts=[];
 625    for(let i=0;i<5;i++){
 626      const j=(i+2)%5;
 627      const a1=P[i].a,a2=P[j].a,sr=R*.45;
 628      starPts.push(new THREE.Vector3(Math.cos(a1)*sr,0,Math.sin(a1)*sr));
 629      starPts.push(new THREE.Vector3(Math.cos(a2)*sr,0,Math.sin(a2)*sr));
 630    }
 631    const starGeo=new THREE.BufferGeometry().setFromPoints(starPts);
 632    cenG.add(new THREE.LineSegments(starGeo,new THREE.LineBasicMaterial({color:0x442222,transparent:true,opacity:.3})));
 633    
 634    // Fractal cascade
 635    if(G.frac>0){
 636      for(let lv=0;lv<G.frac;lv++){
 637        const sc=1/Math.pow(PHI,2*lv),r=R*.65*sc,rot=lv*Math.PI/5,yy=(lv%2?.04:-.04)*lv;
 638        const lm=new THREE.LineBasicMaterial({
 639          color:new THREE.Color().setHSL(.08+lv*.06,.7,.55),
 640          transparent:true,opacity:.4-lv*.06
 641        });
 642        const pp=[];
 643        for(let i=0;i<=5;i++){const aa=rot+(i%5)*2*Math.PI/5-Math.PI/2;pp.push(new THREE.Vector3(Math.cos(aa)*r,yy,Math.sin(aa)*r))}
 644        casG.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(pp),lm));
 645        for(let i=0;i<5;i++){
 646          const j=(i+2)%5,a1=rot+i*2*Math.PI/5-Math.PI/2,a2=rot+j*2*Math.PI/5-Math.PI/2;
 647          casG.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints([
 648            new THREE.Vector3(Math.cos(a1)*r,yy,Math.sin(a1)*r),
 649            new THREE.Vector3(Math.cos(a2)*r,yy,Math.sin(a2)*r)
 650          ]),lm.clone()));
 651        }
 652      }
 653    }
 654    
 655    if(G.vac){
 656      cenG.add(new THREE.Mesh(new THREE.SphereGeometry(R*1.5,48,48),
 657        new THREE.MeshPhysicalMaterial({color:0x222233,transparent:true,opacity:.07,roughness:.1,side:2,transmission:.9})));
 658      const vr=new THREE.Mesh(new THREE.TorusGeometry(R*1.5,T*.8,8,64),G.wire?coreMW:coreM);
 659      vr.rotation.x=Math.PI/2;cenG.add(vr);
 660    }
 661    
 662    devG.add(bG);devG.add(cG);devG.add(nG);devG.add(fluxG);devG.add(casG);devG.add(cenG);
 663  }
 664  
 665  buildDevice();
 666  
 667  // UI: cap bars
 668  function buildCapUI(){
 669    const el=document.getElementById('node-caps');let h='';
 670    for(let i=0;i<5;i++){
 671      h+=`<div class="nb"><div class="icon" style="background:${P[i].css}">${P[i].c}</div>
 672      <div class="nm">C=${P[i].cap}</div>
 673      <div class="bg"><div class="fl" id="cf${i}" style="background:${P[i].css};width:0"></div>
 674      <div class="th" style="left:${THRESHOLD*100}%"></div></div>
 675      <div class="vl" id="cv${i}">0</div></div>`;
 676    }
 677    el.innerHTML=h;
 678  }
 679  buildCapUI();
 680  
 681  // Build remnant field bars
 682  function buildRemUI(){
 683    const el=document.getElementById('rem-bars');let h='';
 684    for(let i=0;i<5;i++){
 685      h+=`<div class="cb"><div class="cl" style="color:${P[i].css};font-size:7px">${P[i].c} Bᵣ</div>
 686      <div class="bg"><div class="fl" id="rem${i}" style="background:${P[i].css};width:0;opacity:.6"></div></div>
 687      <div class="vl" id="remV${i}" style="font-size:7px">0</div></div>`;
 688    }
 689    el.innerHTML=h;
 690  }
 691  buildRemUI();
 692  
 693  // Orbit
 694  let drag=false,pm={x:0,y:0},sph={t:Math.PI/4,p:Math.PI/3,r:14};
 695  function updCam(){
 696    camera.position.set(sph.r*Math.sin(sph.p)*Math.cos(sph.t),sph.r*Math.cos(sph.p),sph.r*Math.sin(sph.p)*Math.sin(sph.t));
 697    camera.lookAt(0,0,0);
 698  }
 699  updCam();
 700  canvas.onmousedown=e=>{drag=true;pm={x:e.clientX,y:e.clientY}};
 701  canvas.onmousemove=e=>{if(!drag)return;sph.t-=(e.clientX-pm.x)*.005;sph.p=Math.max(.2,Math.min(Math.PI-.2,sph.p+(e.clientY-pm.y)*.005));pm={x:e.clientX,y:e.clientY};updCam()};
 702  canvas.onmouseup=canvas.onmouseleave=()=>drag=false;
 703  canvas.onwheel=e=>{sph.r=Math.max(4,Math.min(30,sph.r+e.deltaY*.01));updCam()};
 704  canvas.ontouchstart=e=>{if(e.touches.length===1){drag=true;pm={x:e.touches[0].clientX,y:e.touches[0].clientY}}};
 705  canvas.ontouchmove=e=>{if(!drag||e.touches.length!==1)return;sph.t-=(e.touches[0].clientX-pm.x)*.005;sph.p=Math.max(.2,Math.min(Math.PI-.2,sph.p+(e.touches[0].clientY-pm.y)*.005));pm={x:e.touches[0].clientX,y:e.touches[0].clientY};updCam()};
 706  canvas.ontouchend=()=>drag=false;
 707  
 708  // Rotor canvas
 709  const rc=document.getElementById('rc'),rx=rc.getContext('2d');
 710  function drawRotor(){
 711    rx.clearRect(0,0,130,130);
 712    const cx=65,cy=65,r=48;
 713    // Pentagram (kè path) — this is the rotor pattern
 714    rx.strokeStyle='#331111';rx.lineWidth=1;rx.beginPath();
 715    for(let i=0;i<5;i++){
 716      const ki=KE_ORDER[i],kj=KE_ORDER[(i+1)%5];
 717      const a1=P[ki].a-Math.PI/2,a2=P[kj].a-Math.PI/2;
 718      rx.moveTo(cx+Math.cos(a1)*r,cy+Math.sin(a1)*r);
 719      rx.lineTo(cx+Math.cos(a2)*r,cy+Math.sin(a2)*r);
 720    }
 721    rx.stroke();
 722    // Pentagon (shēng) faint
 723    rx.strokeStyle='#181818';rx.beginPath();
 724    for(let i=0;i<=5;i++){const a=P[i%5].a-Math.PI/2;rx.lineTo(cx+Math.cos(a)*r,cy+Math.sin(a)*r)}
 725    rx.stroke();
 726    // Nodes
 727    for(let i=0;i<5;i++){
 728      const a=P[i].a-Math.PI/2,x=cx+Math.cos(a)*r,y=cy+Math.sin(a)*r;
 729      const fi=sim.fireIntensity[i];
 730      rx.fillStyle=P[i].css;rx.globalAlpha=.25+fi*.75;
 731      rx.beginPath();rx.arc(x,y,3+fi*7+P[i].cap,0,Math.PI*2);rx.fill();
 732      rx.globalAlpha=1;
 733      // Cap value
 734      rx.fillStyle='#888';rx.font='8px JetBrains Mono';rx.textAlign='center';
 735      rx.fillText(P[i].cap,x,y+14+P[i].cap);
 736    }
 737    // Rotor arrow — follows kè cycle
 738    if(sim.rotorSpeed>0.01){
 739      const ra=sim.rotorAngle;
 740      rx.strokeStyle='#e8d5a3';rx.lineWidth=2;
 741      rx.globalAlpha=Math.min(1,sim.rotorSpeed);
 742      rx.beginPath();rx.moveTo(cx,cy);
 743      rx.lineTo(cx+Math.cos(ra)*35,cy+Math.sin(ra)*35);rx.stroke();
 744      const hx=cx+Math.cos(ra)*35,hy=cy+Math.sin(ra)*35;
 745      rx.beginPath();rx.moveTo(hx,hy);
 746      rx.lineTo(hx+Math.cos(ra+2.7)*8,hy+Math.sin(ra+2.7)*8);
 747      rx.lineTo(hx+Math.cos(ra-2.7)*8,hy+Math.sin(ra-2.7)*8);
 748      rx.closePath();rx.fillStyle='#e8d5a3';rx.fill();
 749      rx.globalAlpha=1;
 750    }
 751    // Center: flux indicator
 752    if(sim.flux>0.05){
 753      rx.fillStyle='#8844ff';rx.globalAlpha=sim.flux*.6;
 754      rx.beginPath();rx.arc(cx,cy,5+sim.flux*15,0,Math.PI*2);rx.fill();
 755      rx.globalAlpha=1;
 756    }
 757  }
 758  
 759  // Sliders
 760  const SL=[
 761    ['sR','R','vR',v=>v.toFixed(1),true],['sV','V','vV',v=>v.toFixed(1),true],
 762    ['sT','T','vT',v=>v.toFixed(2),true],['sD','D','vD',v=>''+v,true],
 763    ['sMu','mu','vMu',v=>''+v,true],['sSeed','seed','vSeed',v=>P[v].c,false],
 764    ['sK','k','vK',v=>v.toFixed(2),false],['sDamp','damp','vDamp',v=>v.toFixed(3),false],
 765    ['sPair','pair','vPair',v=>v.toFixed(2),false],['sLoad','load','vLoad',v=>v.toFixed(2),false],
 766    ['sDim','dim','vDim',v=>v.toFixed(2),false],
 767    ['sGlow','glow','vGlow',v=>v.toFixed(1),false],['sFrac','frac','vFrac',v=>''+v,true],
 768  ];
 769  SL.forEach(([id,key,vid,fmt,reb])=>{
 770    document.getElementById(id).oninput=function(){
 771      G[key]=parseFloat(this.value);
 772      document.getElementById(vid).textContent=fmt(G[key]);
 773      if(key==='mu'){
 774        document.getElementById('muDisp').textContent=G.mu;
 775        document.getElementById('steadyI').textContent=SUM_C*G.mu;
 776      }
 777      if(reb){buildDevice();baseCoupling=computeBaseCoupling()}
 778      if(key==='k')baseCoupling=computeBaseCoupling();
 779    };
 780  });
 781  
 782  document.getElementById('btnMag').onclick=()=>{
 783    const seed=G.seed;
 784    sim.charge=[0,0,0,0,0];
 785    // A magnet waved past the device sweeps the entire core.
 786    // Every node gets induced charge — strongest at the seed (closest pass),
 787    // falling off with angular distance around the pentagon.
 788    // The pulse is strong: rare earth magnet, close pass, high dB/dt.
 789    const basePulse=3.5; // enough to bootstrap the cascade
 790    for(let i=0;i<5;i++){
 791      // Angular distance from seed node (0 to 2 steps away on pentagon)
 792      const steps=Math.min(Math.abs(i-seed),5-Math.abs(i-seed));
 793      // Falloff: ~1.0 at seed, ~0.5 at adjacent, ~0.25 at opposite
 794      const proximity=1.0/Math.pow(2,steps);
 795      sim.charge[i]=basePulse*proximity;
 796    }
 797    sim.running=true;sim.fireHistory=[];sim.totalFirings=0;
 798    sim.rotorAngle=P[seed].a;sim.rotorSpeed=0;
 799    sim.resonance=[0,0,0];sim.pairFeedback=0;sim.refractory=[0,0,0,0,0];
 800    document.getElementById('rotor-label').textContent='magnet sweep from '+P[seed].c+' — cascade starting';
 801    document.getElementById('fire-seq').innerHTML='';
 802  };
 803  document.getElementById('btnReset').onclick=()=>{
 804    sim.charge=[0,0,0,0,0];sim.fireIntensity=[0,0,0,0,0];
 805    sim.edgeGlow=new Float64Array(10);sim.flux=0;sim.running=false;
 806    sim.rotorSpeed=0;sim.fireHistory=[];sim.totalFirings=0;sim.resonance=[0,0,0];sim.pairFeedback=0;sim.refractory=[0,0,0,0,0];
 807    sim.barMag=new Float64Array(10);sim.remnant=new Float64Array(5);sim.coreSaturation=0;
 808    document.getElementById('rotor-label').textContent='awaiting ignition';
 809    document.getElementById('fire-seq').innerHTML='';
 810  };
 811  document.getElementById('btnVac').onclick=function(){G.vac=!G.vac;this.classList.toggle('active');buildDevice()};
 812  document.getElementById('btnFlux').onclick=function(){G.showFlux=!G.showFlux;this.classList.toggle('active')};
 813  document.getElementById('btnWire').onclick=function(){G.wire=!G.wire;this.classList.toggle('active');buildDevice()};
 814  
 815  let lastT=0;
 816  function animate(t){
 817    requestAnimationFrame(animate);
 818    const dt=Math.min(.05,(t-lastT)/1000);lastT=t;
 819    for(let s=0;s<10;s++)simStep(dt/10);
 820    
 821    const glow=G.glow;
 822    for(let i=0;i<5;i++){
 823      // Glass sheath glow on fire
 824      if(glassNodes[i]){
 825        const fi=sim.fireIntensity[i];
 826        glassNodes[i].matUp.opacity=.06+fi*.45*glow;
 827        glassNodes[i].matLo.opacity=.06+fi*.45*glow;
 828      }
 829      if(capMeshes[i]){
 830        const norm=sim.charge[i]/P[i].cap;
 831        capMeshes[i].traverse(ch=>{
 832          if(ch.isMesh&&ch.material&&ch.material.emissive){
 833            ch.material.emissive=new THREE.Color(P[i].col);
 834            ch.material.emissiveIntensity=norm*.6*glow;
 835          }
 836        });
 837      }
 838    }
 839    for(let e=0;e<10;e++){edgeGMs[e].opacity=sim.edgeGlow[e]*.1*glow}
 840    
 841    // === FLUX PARTICLES: travel along bars when active ===
 842    const showF=G.showFlux;
 843    for(let e=0;e<10;e++){
 844      const intensity=sim.edgeGlow[e];
 845      const mag=sim.barMag[e]||0; // remanent magnetization
 846      const vis=Math.max(intensity,mag*0.5); // visible from either active glow or residual mag
 847      const bp=barEndpoints[e];
 848      if(!bp)continue;
 849      const speed=(vis*3+sim.rotorSpeed*0.3)*dt;
 850      for(let p=0;p<fluxParticles[e].length;p++){
 851        const fp=fluxParticles[e][p];
 852        fp.u+=speed;
 853        if(fp.u>1)fp.u-=1;
 854        fp.mesh.position.lerpVectors(bp.from,bp.to,fp.u);
 855        fp.mat.opacity=showF?vis*0.7*glow:0;
 856        const sc=0.5+vis*0.7;
 857        fp.mesh.scale.setScalar(sc);
 858      }
 859    }
 860    
 861    // === FIELD LINE LOOPS: show B-field around active bars ===
 862    for(let e=0;e<10;e++){
 863      const intensity=sim.edgeGlow[e];
 864      const mag=sim.barMag[e]||0;
 865      const vis=Math.max(intensity,mag*0.4);
 866      for(let li=0;li<fieldLineLoops[e].length;li++){
 867        fieldLineLoops[e][li].mat.opacity=showF?vis*0.3*glow:0;
 868      }
 869    }
 870    
 871    // === CENTRAL VORTEX: rotate and show when flux is present ===
 872    for(const vl of vortexLines){
 873      vl.mat.opacity=showF?sim.flux*0.6*glow:0;
 874      vl.mesh.rotation.y+=sim.rotorSpeed*dt*(vl.isKe?1:-0.618);
 875    }
 876    
 877    const fl=sim.flux;
 878    cntM.opacity=fl*.4*glow;
 879    cntM2.opacity=fl*.35*glow;
 880    if(centralS){centralS.scale.setScalar(1+fl*.25)}
 881    if(centralI){
 882      centralI.rotation.y+=sim.rotorSpeed*.025;
 883      centralI.rotation.x+=sim.rotorSpeed*.015;
 884    }
 885    ctrL.intensity=fl*2.5*glow;
 886    ctrL.color.setHSL(.75+Math.sin(t*.001)*.1,.6,.5);
 887    keL.intensity=sim.rotorSpeed*.8*glow;
 888    
 889    if(!drag){sph.t+=.0008;updCam()}
 890    
 891    // UI
 892    for(let i=0;i<5;i++){
 893      const norm=Math.min(1.5,sim.charge[i]/P[i].cap);
 894      const bar=document.getElementById('cf'+i),val=document.getElementById('cv'+i);
 895      if(bar)bar.style.width=(norm/1.5*100)+'%';
 896      if(val)val.textContent=norm.toFixed(2);
 897    }
 898    document.getElementById('fluxBar').style.width=Math.min(100,fl*120)+'%';
 899    document.getElementById('fluxV').textContent=fl.toFixed(2);
 900    document.getElementById('pairBar').style.width=Math.min(100,sim.pairFeedback*500)+'%';
 901    document.getElementById('pairV').textContent=sim.pairFeedback.toFixed(3);
 902    
 903    // Hysteresis display
 904    document.getElementById('satBar').style.width=(sim.coreSaturation*100)+'%';
 905    document.getElementById('satV').textContent=sim.coreSaturation.toFixed(2);
 906    for(let i=0;i<5;i++){
 907      const rb=document.getElementById('rem'+i);
 908      const rv=document.getElementById('remV'+i);
 909      if(rb)rb.style.width=(sim.remnant[i]/0.3*100)+'%';
 910      if(rv)rv.textContent=sim.remnant[i].toFixed(3);
 911    }
 912    // Hysteresis state label
 913    const hs=document.getElementById('hystState');
 914    if(sim.coreSaturation>0.4)hs.textContent='⚡ locked in — cannot stop';
 915    else if(sim.coreSaturation>0.15)hs.textContent='🔄 building — approaching lock-in';
 916    else if(sim.coreSaturation>0.01)hs.textContent='⏳ residual magnetization';
 917    else hs.textContent='dormant';
 918    
 919    // Resonances
 920    for(let r=0;r<3;r++){
 921      const el=document.getElementById('res'+r);
 922      if(el)el.style.width=Math.min(100,sim.resonance[r]*100)+'%';
 923    }
 924    
 925    // Fire sequence
 926    if(sim.fireHistory.length>0){
 927      const el=document.getElementById('fire-seq');
 928      let h='';
 929      sim.fireHistory.slice(-30).forEach(f=>{h+=`<b style="color:${P[f.node].css}">${P[f.node].c}</b> `});
 930      el.innerHTML=h;
 931      if(sim.rotorSpeed>0.05){
 932        const rpm=(sim.rotorSpeed/(2*Math.PI)*60).toFixed(0);
 933        const state=sim.pairFeedback>0.01?'⚡ self-sustaining':'⏳ decaying';
 934        document.getElementById('rotor-label').textContent=
 935          '克 '+rpm+' rpm · '+state+' · E='+sim.totalEnergy.toFixed(2);
 936      } else if(sim.totalFirings>0&&!sim.running){
 937        document.getElementById('rotor-label').textContent='stopped · '+sim.totalFirings+' fires total';
 938      }
 939    }
 940    
 941    drawRotor();
 942    renderer.render(scene,camera);
 943  }
 944  animate(0);
 945  window.onresize=()=>{camera.aspect=innerWidth/innerHeight;camera.updateProjectionMatrix();renderer.setSize(innerWidth,innerHeight)};
 946  </script>
 947  </body>
 948  </html>
 949