Files
site/sphere/index.html
T

169 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Sphere Yantra</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,500;1,500&family=JetBrains+Mono:wght@400;500&display=swap">
<style>
/* Layout: full-bleed night canvas, sphere centred; one compact control tray along the bottom. Single dark look by choice. */
:root{
--bg:#0b0a12; --panel:#15131f; --line:#2a2638; --fg:#e9e4f2; --muted:#9a93ad;
--up:#f0b44c; --down:#e2559a; --bindu:#ff3b4e; --grid:#3a3550;
--display:"Cormorant Garamond",Georgia,serif; --mono:"JetBrains Mono",ui-monospace,monospace;
color-scheme:dark;
}
html,body{height:100%;margin:0}
body{background:var(--bg);color:var(--fg);font-family:var(--mono);font-size:14px;overflow:hidden}
#stage{position:fixed;inset:0}
header{position:fixed;top:0;left:0;right:0;padding-top:calc(12px + env(safe-area-inset-top,0px));padding-left:max(16px,env(safe-area-inset-left,0px));padding-right:max(16px,env(safe-area-inset-right,0px));pointer-events:none}
.home{pointer-events:auto;display:inline-block;margin-bottom:4px;color:var(--muted);font-size:11px;text-decoration:none;letter-spacing:.05em}
h1{font-family:var(--display);font-weight:500;font-size:26px;margin:0;letter-spacing:.01em}
h1 i{color:var(--down)}
.sub{color:var(--muted);font-size:11px;margin-top:2px;letter-spacing:.04em}
.tray{position:fixed;left:0;right:0;bottom:0;background:linear-gradient(transparent,var(--bg) 30%);padding-bottom:calc(14px + env(safe-area-inset-bottom,0px));padding-top:28px;padding-left:max(16px,env(safe-area-inset-left,0px));padding-right:max(16px,env(safe-area-inset-right,0px))}
.inner{max-width:560px;margin:0 auto;display:flex;flex-direction:column;gap:12px}
.row{display:flex;gap:8px;flex-wrap:wrap}
.seg{display:flex;border:1px solid var(--line);border-radius:999px;overflow:hidden}
.seg button{background:transparent;color:var(--muted);border:0;font:inherit;font-size:11px;padding:7px 12px;cursor:pointer;letter-spacing:.05em}
.seg button{opacity:.55}
.seg button[aria-pressed="true"]{background:var(--panel);color:var(--fg);opacity:1}
.seg button[aria-pressed="true"]::before{content:"";display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--down);margin-right:7px;vertical-align:middle}
button:focus-visible,input:focus-visible{outline:2px solid var(--down);outline-offset:2px}
label.sl{display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:10px;font-size:11px;color:var(--muted);letter-spacing:.05em}
input[type=range]{width:100%;accent-color:var(--down)}
.val{font-variant-numeric:tabular-nums;color:var(--fg);min-width:3.5em;text-align:right}
.chip{font-size:11px;color:var(--muted);display:flex;align-items:center;gap:6px}
.dot{width:8px;height:8px;border-radius:50%}
.note{font-size:10.5px;color:var(--muted);line-height:1.5}
</style>
</head>
<body>
<div id="stage"></div>
<header><a class="home" href="/">← maths</a>
<h1>Śrī Yantra <i>on the sphere</i></h1>
<div class="sub">drag to spin · pinch or scroll to zoom</div>
</header>
<div class="tray"><div class="inner">
<div class="row">
<div class="seg" role="group" aria-label="Projection">
<button id="m-stereo" aria-pressed="true">stereographic</button>
<button id="m-gnomo" aria-pressed="false">great circles</button>
</div>
<div class="seg" role="group" aria-label="Show">
<button id="t-sphere" aria-pressed="true">sphere</button>
<button id="t-inv" aria-pressed="true">inverse</button>
<button id="t-plane" aria-pressed="true">flat shadow</button>
<button id="t-outer" aria-pressed="true">outer</button>
<button id="t-spin" aria-pressed="true">auto-spin</button>
</div>
</div>
<label class="sl" for="lift">lift <input id="lift" type="range" min="-3" max="4" step="0.01" value="0"> <span class="val" id="liftv">1.00</span></label>
<div class="row">
<span class="chip"><span class="dot" style="background:var(--up)"></span>4 up · Śiva</span>
<span class="chip"><span class="dot" style="background:var(--down)"></span>5 down · Śakti</span>
<span class="chip"><span class="dot" style="background:var(--bindu)"></span>bindu at the pole</span>
</div>
<div class="note" id="note"></div>
</div></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
<script>
// Triangle data (Tripathi / texample, after Kulaichev & Huet): left_x, base_y, apex_y, right_x, type (1=up)
// SVG frame: centre (150,150), radius 100, y down.
const T=[
[53.65669559977147,123.20508075688774,250,246.34330440022853,0],
[52.984011026495736,174.24660560764943,50,247.01598897350425,1],
[98.71823312733801,220.03828947357886,123.20508075688774,201.281766872662,1],
[78.26467997914015,197.92315674002487,78.10499177949904,221.73532002085986,1],
[90.4856922951427,78.10499177949904,160.66014976539617,209.51430770485734,0],
[80.98384838952128,103.12199145016105,220.03828947357886,219.0161516104787,0],
[114.9488500600036,160.66014976539617,103.12199145016105,185.0511499399964,1],
[116.35142605010424,134.30757626706648,197.92315674002487,183.64857394989576,0],
[124.61190803072795,144.79777263138968,174.24660560764943,175.38809196927207,0]];
const P=(X,Y)=>[(X-150)/100,-(Y-150)/100];
const css=n=>getComputedStyle(document.documentElement).getPropertyValue(n).trim();
const C={up:new THREE.Color(css('--up')),down:new THREE.Color(css('--down')),bindu:new THREE.Color(css('--bindu')),grid:new THREE.Color(css('--grid')),fg:new THREE.Color(css('--fg'))};
function seg(a,b,n=72){const o=[];for(let i=0;i<=n;i++){const t=i/n;o.push([a[0]+(b[0]-a[0])*t,a[1]+(b[1]-a[1])*t])}return o}
const tris=T.map(([lx,by,ay,rx,ty])=>{const A=P(lx,by),B=P(150,ay),Cc=P(rx,by);return{up:!!ty,pts:[...seg(A,B),...seg(B,Cc).slice(1),...seg(Cc,A).slice(1)]}});
const circ=(r,n=256)=>Array.from({length:n+1},(_,i)=>{const a=i/n*Math.PI*2;return[r*Math.cos(a),r*Math.sin(a)]});
function petals(r0,r1,k){const o=[];for(let i=0;i<k;i++){const a0=i/k*Math.PI*2,a1=(i+1)/k*Math.PI*2,am=(a0+a1)/2;const pts=[];for(let j=0;j<=24;j++){const t=j/24;const a=a0+(am-a0)*t;const r=r0+(r1-r0)*Math.sin(t*Math.PI/2);pts.push([r*Math.cos(a),r*Math.sin(a)])}for(let j=0;j<=24;j++){const t=j/24;const a=am+(a1-am)*t;const r=r1-(r1-r0)*(1-Math.cos(t*Math.PI/2));pts.push([r*Math.cos(a),r*Math.sin(a)])}o.push(pts)}return o}
function bhupura(){const q=[[1.6,-1.6],[1.6,-0.6],[1.73,-0.6],[1.73,-1.2],[1.97,-1.2],[1.97,1.2],[1.73,1.2],[1.73,0.6],[1.6,0.6],[1.6,1.6]];const c=[];for(let k=0;k<4;k++){const co=Math.cos(k*Math.PI/2),si=Math.sin(k*Math.PI/2);q.forEach(([x,y])=>c.push([x*co-y*si,x*si+y*co]))}c.push(c[0]);const o=[];for(let i=0;i<c.length-1;i++)o.push(...seg(c[i],c[i+1],60).slice(i?1:0));return o}
const decorInner=[circ(1)];
const decorOuter=[circ(1.2),circ(1.4),circ(1.5),...petals(1,1.2,8),...petals(1.2,1.4,16),bhupura()];
let showOuter=true;
let decor=[...decorInner,...decorOuter];
let mode='stereo',lift=1;
function toSphere([x,y]){x*=lift;y*=lift;if(mode==='stereo'){const r2=x*x+y*y,d=r2+1;return new THREE.Vector3(2*x/d,(r2-1)/d,2*y/d)}return new THREE.Vector3(x,-1,y).normalize()}
function toPlane([x,y]){return new THREE.Vector3(x*lift,-1.001,y*lift)}
const W=()=>innerWidth,H=()=>innerHeight;
const renderer=new THREE.WebGLRenderer({antialias:true,alpha:false});
renderer.setPixelRatio(Math.min(devicePixelRatio,1.5));renderer.setClearColor(new THREE.Color(css('--bg')));
document.getElementById('stage').appendChild(renderer.domElement);
const scene=new THREE.Scene();
const cam=new THREE.PerspectiveCamera(40,1,0.01,100);cam.position.set(2.6,-2.4,3.2);
const ctl=new THREE.OrbitControls(cam,renderer.domElement);ctl.enableDamping=true;ctl.autoRotate=true;ctl.autoRotateSpeed=0.6;ctl.minDistance=1.6;ctl.maxDistance=12;
scene.add(new THREE.Mesh(new THREE.SphereGeometry(0.995,64,48),new THREE.MeshBasicMaterial({color:new THREE.Color(css('--panel')),transparent:true,opacity:.55})));
const gmat=new THREE.LineBasicMaterial({color:C.grid,transparent:true,opacity:.55});
for(let i=1;i<12;i++){const ph=i/12*Math.PI;const pts=[];for(let j=0;j<=128;j++){const a=j/128*Math.PI*2;pts.push(new THREE.Vector3(Math.sin(ph)*Math.cos(a),Math.cos(ph),Math.sin(ph)*Math.sin(a)))}scene.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts),gmat))}
for(let i=0;i<12;i++){const a=i/12*Math.PI;const pts=[];for(let j=0;j<=128;j++){const t=j/128*Math.PI*2;pts.push(new THREE.Vector3(Math.sin(t)*Math.cos(a),Math.cos(t),Math.sin(t)*Math.sin(a)))}scene.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts),gmat))}
const group=new THREE.Group(),planeG=new THREE.Group();scene.add(group,planeG);
const bindu=new THREE.Mesh(new THREE.SphereGeometry(.022,16,12),new THREE.MeshBasicMaterial({color:C.bindu}));scene.add(bindu);
const northPt=new THREE.Mesh(new THREE.SphereGeometry(.014,12,8),new THREE.MeshBasicMaterial({color:C.fg}));northPt.position.set(0,1,0);scene.add(northPt);
let showPlane=true,showInv=true,showSphere=true;
const invP=([x,y])=>{const r=x*x+y*y;return r<1e-12?[1e6,0]:[x/r,y/r]};
const trisDense=T.map(([lx,by,ay,rx,ty])=>{const A=P(lx,by),B=P(150,ay),Cc=P(rx,by);return{up:!!ty,pts:[...seg(A,B,600),...seg(B,Cc,600).slice(1),...seg(Cc,A,600).slice(1)]}});
const bindu2=new THREE.Mesh(new THREE.SphereGeometry(.022,16,12),new THREE.MeshBasicMaterial({color:C.bindu}));scene.add(bindu2);
const drop=g=>{g.traverse(o=>{if(o.geometry)o.geometry.dispose();if(o.material)o.material.dispose()});g.clear()};
let pending=false;
function build(){if(pending)return;pending=true;requestAnimationFrame(()=>{pending=false;rebuild()})}
function rebuild(){
drop(group);drop(planeG);
const mk=(pts,col,op,g,f)=>g.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts.map(f)),new THREE.LineBasicMaterial({color:col,transparent:op<1,opacity:op})));
tris.forEach(t=>mk(t.pts,t.up?C.up:C.down,1,group,toSphere));
decor.forEach(d=>mk(d,C.fg,.45,group,toSphere));
bindu.position.set(0,-1,0);
if(showInv){trisDense.forEach(t=>mk(t.pts.map(invP),t.up?C.up:C.down,.8,group,toSphere));decor.forEach(d=>mk(d.map(invP),C.fg,.3,group,toSphere));}
bindu2.visible=showInv;bindu2.position.copy(toSphere([1e6,0]));
const pOp=showSphere?.35:1;
scene.children.forEach(o=>{if(o!==planeG)o.visible=showSphere});
if(!showSphere)bindu2.visible=false;
if(showPlane){const pb=new THREE.Mesh(new THREE.CircleGeometry(.022,24),new THREE.MeshBasicMaterial({color:C.bindu,side:THREE.DoubleSide}));pb.rotation.x=-Math.PI/2;pb.position.copy(toPlane([0,0])).add(new THREE.Vector3(0,.002,0));planeG.add(pb);
tris.forEach(t=>mk(t.pts,t.up?C.up:C.down,pOp,planeG,toPlane));decor.forEach(d=>mk(d,C.fg,pOp*.45,planeG,toPlane));
if(showInv){const R=6/lift;const clip=(pts,col,op)=>{let run=[];const flush=()=>{if(run.length>1)mk(run,col,op,planeG,toPlane);run=[]};for(const p of pts){if(Math.hypot(p[0],p[1])>R)flush();else run.push(p)}flush()};
trisDense.forEach(t=>clip(t.pts.map(invP),t.up?C.up:C.down,showSphere?pOp*.85:.55));decor.forEach(d=>clip(d.map(invP),C.fg,pOp*.35));}}
const deg=mode==='stereo'?2*Math.atan(lift)*180/Math.PI:Math.atan(lift)*180/Math.PI;
document.getElementById('note').textContent=(!showSphere?'The plane on its own: the yantra and its inversion in the outer circle. ':'')+(mode==='stereo'
?'Straight sides lift to circles through the top pole. Concurrences are kept exactly; only curvature changes.'
:'Straight sides lift to great circles (central projection), the Kulaichev–Rao spherical form. Lift → 0 is the flat limit.')
+` Yantra rim at ${deg.toFixed(1)}° from the bindu pole.`+(lift===0?' At zero lift every finite point, the yantra and its inverse alike, shrinks into the bindu at the south pole; only infinity stays north. It mirrors lift ∞.':'')+(mode==='stereo'&&lift>1e6?' The rim has closed onto the north pole: 0 and ∞ hold the bindu and its dual.':'')+(mode!=='stereo'&&lift>20?' Great circles cap at the equator; switch to stereographic to reach the north pole.':'');
}
function resize(){renderer.setSize(W(),H());cam.aspect=W()/H();cam.updateProjectionMatrix()}
addEventListener('resize',resize);resize();build();
const reduce=matchMedia('(prefers-reduced-motion: reduce)').matches;
if(reduce){ctl.autoRotate=false;document.getElementById('t-spin').setAttribute('aria-pressed','false')}
(function loop(){requestAnimationFrame(loop);if(document.hidden)return;ctl.update();renderer.render(scene,cam)})();
const $=id=>document.getElementById(id);
function setMode(m){mode=m;$('m-stereo').setAttribute('aria-pressed',m==='stereo');$('m-gnomo').setAttribute('aria-pressed',m==='gnomo');build()}
$('m-stereo').onclick=()=>setMode('stereo');$('m-gnomo').onclick=()=>setMode('gnomo');
$('t-sphere').onclick=e=>{showSphere=!showSphere;e.currentTarget.setAttribute('aria-pressed',showSphere);if(!showSphere&&!showPlane){showPlane=true;$('t-plane').setAttribute('aria-pressed','true')}
if(!showSphere){cam.position.set(0,3.6,0.0001);ctl.target.set(0,-1,0);ctl.autoRotate=false;$('t-spin').setAttribute('aria-pressed','false')}else{cam.position.set(2.6,-2.4,3.2);ctl.target.set(0,0,0)}
ctl.update();build()};
$('t-outer').onclick=e=>{showOuter=!showOuter;e.currentTarget.setAttribute('aria-pressed',showOuter);decor=showOuter?[...decorInner,...decorOuter]:[...decorInner];build()};
$('t-inv').onclick=e=>{showInv=!showInv;e.currentTarget.setAttribute('aria-pressed',showInv);build()};
$('t-plane').onclick=e=>{showPlane=!showPlane;e.currentTarget.setAttribute('aria-pressed',showPlane);build()};
$('t-spin').onclick=e=>{ctl.autoRotate=!ctl.autoRotate;e.currentTarget.setAttribute('aria-pressed',ctl.autoRotate)};
$('lift').oninput=e=>{const v=+e.target.value;lift=v>=3.99?1e7:v<=-2.99?0:Math.pow(10,v);$('liftv').textContent=v>=3.99?'∞':v<=-2.99?'0':(lift<0.01?lift.toFixed(3):lift<10?lift.toFixed(2):lift.toFixed(0));build()};
</script>
</body>
</html>