<!-- SAGA2 · part 39 -->
 -2, -gridH/2);
}

function clearAll() {
  faces.forEach(f => scene.remove(f));
  faces = [];
  document.getElementById('count').textContent = '0';
}

function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}
animate();

// Start with 100
generate(100);
</script>
<div id="carousel" style="position:fixed; bottom:20px; left:0; right:0; text-align:center; z-index:20; display:none;">
  <button onclick="prevFace()" style="font-size:32px; padding:20px 30px;">◀</button>
  <span id="faceNum" style="color:#ffcc44; font-size:24px; margin:0 20px;">1 / 0</span>
  <button onclick="nextFace()" style="font-size:32px; padding:20px 30px;">▶</button>
  <div><input type="range" id="faceSlider" min="0" max="0" value="0" oninput="jumpTo(this.value)" style="width:80%; margin-top:10px;"></div>
</div>
<button id="carouselBtn" onclick="toggleCarousel()" style="position:fixed; top:80px; left:10px; z-index:20;">Slide View</button>
<script>
var carouselMode = false;
var currentFace = 0;
function toggleCarousel() {
  carouselMode = !carouselMode;
  document.getElementById('carousel').style.display = carouselMode ? 'block' : 'none';
  document.getElementById('carouselBtn').textContent = carouselMode ? 'Grid View' : 'Slide View';
  if(carouselMode && faces.length>0) showFace(0);
  else {
    var cols=10, rows=Math.ceil(faces.length/cols);
    var gridW=cols*12, gridH=rows*14;
    var dist=Math.max(gridW,gridH)*0.9;
    camera.position.set(0,-2,dist);
    camera.lookAt(0,-2,-gridH/2);
  }
}
function showFace(i) {
  if(faces.length==0) return;
  currentFace = ((i % faces.length) + faces.length) % faces.length;
  var f = faces[currentFace];
  camera.position.set(f.position.x, -1, f.position.z + 18);
  camera.lookAt(f.position.x, -2, f.position.z);
  document.getElementById('faceNum').textContent = (currentFace+1) + ' / ' + faces.length;
  document.getElementById('faceSlider').max = faces.length-1;
  document.getElementById('faceSlider').value = currentFace;
}
function nextFace() { showFace(currentFace+1); }
function prevFace() { showFace(currentFace-1); }
function jumpTo(v) { showFace(parseInt(v)); }
var touchX = 0;
document.addEventListener('touchstart', function(e){ touchX = e.touches[0].clientX; }, {passive:true});
document.addEventListener('touchend', function(e){
  if(!carouselMode) return;
  var dx = e.changedTouches[0].clientX - touchX;
  if(dx < -50) nextFace();
  else if(dx > 50) prevFace();
}, {passive:true});
</script>
</body>
</html>


===== FILE: public/saga2/head-elder.js =====
// test
  (function(){
    var ig = new THREE.Group();
    var iC = new THREE.LineBasicMaterial({color:0x66eeff, transparent:true, opacity:0.55});
    var iG = new THREE.LineBasicMaterial({color:0xffcc44, transparent:true, opacity:0.55});
    var iD = new THREE.LineBasicMaterial({color:0x226688, transparent:true, opacity:0.35});
    // Ribbon materials (match line colors, for meshes)
    var riC=new THREE.MeshBasicMaterial({color:0x00cccc,transparent:true,opacity:0.55,side:THREE.DoubleSide});
    var riG=new THREE.MeshBasicMaterial({color:0xffcc44,transparent:true,opacity:0.55,side:THREE.DoubleSide});
    var riD=new THREE.MeshBasicMaterial({color:0x008888,transparent:true,opacity:0.35,side:THREE.DoubleSide});
    function L(pts, mat){
      // King: lines → ribbons (width 0.05)
      var rm=riC; if(mat===iG) rm=riG; else if(mat===iD) rm=riD;
      ig.add(ribbon(pts, 0.05, rm));
    }
    // CRANIUM — 12 meridians (cut bloat, keep form)
    for(var i=0;i<12;i++){ var a=(i/12)*Math.PI*2, pts=[];
      for(var j=0;j<=16;j++){ var ph=(j/16)*Math.PI, r=5*Math.sin(ph);
        pts.push([Math.cos(a)*r, 5*Math.cos(ph)+2, Math.sin(a)*r*0.92]);
      }
      L(pts, i%3==0?iG:iC);
    }
    // CRANIUM — 10 latitude rings (skip front face, no grid over face)
    for(var k=0;k<10;k++){ var ly=7-k*1.4, lr=5*Math.sqrt(Math.max(0,1-Math.pow((ly-2)/5,2)));
      // Draw back 270 degrees only (skip front 90 where face is)
      var rp1=[], rp2=[];
      for(var n=0;n<=21;n++){ var an=(n/21)*Math.PI*1.5 + Math.PI*0.75; // back arc
        var px=Math.cos(an)*lr, pz=Math.sin(an)*lr*0.92;
        if(pz < 2.0 || Math.abs(px) > 3.5){ rp1.push([px, ly, pz]); } // skip face region
        else { if(rp1.length>1){ L(rp1, k%3==0?iG:(k%2?iC:iD)); rp1=[]; } }
      }
      if(rp1.length>1) L(rp1, k%3==0?iG:(k%2?iC:iD));
    }
    // BROW RIDGE — folds back onto cranium (continuous, no seam)
    for(var br=0;br<3;br++){ var bp=[];
      for(var bx=-5.5;bx<=5.5;bx+=0.25){
        var ax=Math.abs(bx);
        var by, bz;
        if(ax<=3.5){
          by=4.2+br*0.35 - Math.pow(bx/3.5,2)*0.8;
          bz=4.9 - ax*0.12 + br*0.05;
        } else {
          // fold back: curve around to meet cranium
          var t=(ax-3.5)/2.0; // 0 to 1
          by=4.2+br*0.35 - 0.8 - t*1.5; // drops as it goes back
          bz=4.9 - 3.5*0.12 - t*3.0; // recedes back
        }
        bp.push([bx, by, bz]);
      }
      L(bp, br==1?iG:iD