169 lines
14 KiB
HTML
169 lines
14 KiB
HTML
<!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>
|