2026-10-23 20:11:32 +00:00
<!doctype html>
< html lang = "en" >
< head >
< meta charset = "utf-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1, viewport-fit=cover" >
< title > Yantra Overlays</ 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 canvas; title top-left, control tray at the bottom. Single dark look, matching the sphere page. */
: root { --bg : #0b0a12 ; --panel : #15131f ; --line : #2a2638 ; --fg : #e9e4f2 ; --muted : #9a93ad ; --up : #f0b44c ; --down : #e2559a ; --bindu : #ff3b4e ; --ring : #7d74a0 ; --fit : #6ec5d6 ;
--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 : 14 px ; overflow : hidden }
canvas { position : fixed ; inset : 0 ; width : 100 % ; height : 100 % ; touch-action : none }
header { position : fixed ; top : 0 ; left : 0 ; right : 0 ; padding-top : calc ( 12 px + env ( safe - area - inset - top , 0 px )); padding-left : max ( 16 px , env ( safe - area - inset - left , 0 px )); padding-right : max ( 16 px , env ( safe - area - inset - right , 0 px )); pointer-events : none }
. home { pointer-events : auto ; display : inline-block ; margin-bottom : 4 px ; color : var ( -- muted ); font-size : 11 px ; text-decoration : none ; letter-spacing : .05 em }
h1 { font-family : var ( -- display ); font-weight : 500 ; font-size : 26 px ; margin : 0 }
h1 i { color : var ( -- down )}
. sub { color : var ( -- muted ); font-size : 11 px ; margin-top : 2 px ; letter-spacing : .04 em }
. tray { position : fixed ; left : 0 ; right : 0 ; bottom : 0 ; background : linear-gradient ( transparent , var ( -- bg ) 30 % ); padding-top : 28 px ; padding-bottom : calc ( 14 px + env ( safe - area - inset - bottom , 0 px )); padding-left : max ( 16 px , env ( safe - area - inset - left , 0 px )); padding-right : max ( 16 px , env ( safe - area - inset - right , 0 px ))}
. inner { max-width : 560 px ; margin : 0 auto ; display : flex ; flex-direction : column ; gap : 12 px }
. row { display : flex ; gap : 8 px ; flex-wrap : wrap ; align-items : center }
. seg { display : flex ; border : 1 px solid var ( -- line ); border-radius : 999 px ; overflow : hidden ; flex-wrap : wrap ; width : max-content ; max-width : 100 % }
. seg button , . btn { background : transparent ; color : var ( -- muted ); border : 0 ; font : inherit ; font-size : 11 px ; padding : 7 px 12 px ; cursor : pointer ; letter-spacing : .05 em ; 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 : 6 px ; height : 6 px ; border-radius : 50 % ; background : var ( -- sw , var ( -- down )); margin-right : 7 px ; vertical-align : middle }
. btn { border : 1 px solid var ( -- line ); border-radius : 999 px ; opacity : 1 ; color : var ( -- fg )}
button : focus-visible , input : focus-visible { outline : 2 px solid var ( -- down ); outline-offset : 2 px }
label . sl { display : grid ; grid-template-columns : auto 1 fr auto ; align-items : center ; gap : 10 px ; font-size : 11 px ; color : var ( -- muted )}
input [ type = range ] { width : 100 % ; accent-color : var ( -- fit )}
. val { font-variant-numeric : tabular-nums ; color : var ( -- fg ); min-width : 4.5 em ; text-align : right }
. note { font-size : 10.5 px ; color : var ( -- muted ); line-height : 1.5 }
</ style >
</ head >
< body >
< canvas id = "c" ></ canvas >
< header >< a class = "home" href = "/" > ← maths</ a >< h1 > Śrī Yantra < i > overlays</ i ></ h1 >< div class = "sub" > drag to pan · pinch or scroll to zoom</ div ></ header >
< div class = "tray" >< div class = "inner" >
< div class = "seg" role = "group" aria-label = "Layers" >
< button id = "l-orig" aria-pressed = "true" style = "--sw:var(--fg)" > yantra</ button >
< button id = "l-unit" aria-pressed = "false" style = "--sw:var(--ring)" > inverse · rim</ button >
< button id = "l-fit" aria-pressed = "true" style = "--sw:var(--fit)" > inverse · ρ </ button >
2026-10-23 20:22:32 +00:00
< button id = "l-outer" aria-pressed = "true" style = "--sw:var(--muted)" > outer</ button >
2026-10-23 20:11:32 +00:00
</ div >
< div class = "row" >
< label class = "sl" for = "rho" style = "flex:1;min-width:220px" > ρ < input id = "rho" type = "range" min = "-1" max = "0.6" step = "0.001" value = "-0.31" > < span class = "val" id = "rv" ></ span ></ label >
< button class = "btn" id = "snap" > fit trefoil</ button >
</ div >
< div class = "note" id = "note" ></ div >
</ div ></ div >
< script >
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 col = { up : css ( '--up' ), down : css ( '--down' ), bindu : css ( '--bindu' ), ring : css ( '--ring' ), fit : css ( '--fit' ), fg : css ( '--fg' )};
function seg ( a , b , n = 500 ){ 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 sides = []; T . forEach (([ lx , by , ay , rx , ty ])=>{ const A = P ( lx , by ), B = P ( 150 , ay ), C = P ( rx , by );[[ A , B ],[ B , C ],[ C , A ]]. forEach (([ a , b ])=> sides . push ({ up :!! ty , pts : seg ( a , b )}))});
// Fit: send the outer downward triangle's vertices (where two outer lobes meet) onto the
// side midpoints of the outer upward triangle, which lie on the same three rays: rho^2 = |V||M|.
2026-10-23 20:22:32 +00:00
const V0 = P ( 150 , 160.66014976539617 ), M1 = P ( 150 , 174.24660560764943 );
// outer yantra: lotus rings, petals, bhupura (square with four gates), in yantra units (rim = 1)
const circ = ( r , n = 360 )=> 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 , pts = []; for ( let j = 0 ; j <= 40 ; j ++ ){ const t = j / 40 ; 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 <= 40 ; j ++ ){ const t = j / 40 ; 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 corners = []; for ( let k = 0 ; k < 4 ; k ++ ){ const c = Math . cos ( k * Math . PI / 2 ), s = Math . sin ( k * Math . PI / 2 ); q . forEach (([ x , y ])=> corners . push ([ x * c - y * s , x * s + y * c ]))} corners . push ( corners [ 0 ]); const o = []; for ( let i = 0 ; i < corners . length - 1 ; i ++ ) o . push (... seg ( corners [ i ], corners [ i + 1 ], 120 ). slice ( i ? 1 : 0 )); return o }
const OUTER = [ circ ( 1.2 ), circ ( 1.4 ), circ ( 1.5 ),... petals ( 1 , 1.2 , 8 ),... petals ( 1.2 , 1.4 , 16 ), bhupura ()];
2026-10-23 20:11:32 +00:00
const RHO_FIT = Math . sqrt ( Math . hypot (... V0 ) * Math . hypot (... M1 ));
const inv = ( p , r2 )=>{ const r = p [ 0 ] * p [ 0 ] + p [ 1 ] * p [ 1 ]; return r < 1e-12 ? null : [ r2 * p [ 0 ] / r , r2 * p [ 1 ] / r ]};
2026-10-23 20:22:32 +00:00
let L = { orig : true , unit : false , fit : true , outer : true }, rho = RHO_FIT , zoom = 1 , pan = [ 0 , 0 ];
2026-10-23 20:11:32 +00:00
const cv = document . getElementById ( 'c' ), ctx = cv . getContext ( '2d' );
function size (){ const d = Math . min ( devicePixelRatio , 2 ); cv . width = innerWidth * d ; cv . height = innerHeight * d ; ctx . setTransform ( d , 0 , 0 , d , 0 , 0 ); draw ()}
function draw (){
const w = innerWidth , h = innerHeight , cx = w / 2 + pan [ 0 ], cy = h * 0.44 + pan [ 1 ], s = Math . min ( w , h ) * 0.34 * zoom ;
ctx . fillStyle = css ( '--bg' ); ctx . fillRect ( 0 , 0 , w , h );
const X = p => cx + p [ 0 ] * s , Y = p => cy - p [ 1 ] * s ;
const ring = ( r , c )=>{ ctx . strokeStyle = c ; ctx . globalAlpha = . 8 ; ctx . lineWidth = 1 ; ctx . setLineDash ([ 4 , 4 ]); ctx . beginPath (); ctx . arc ( cx , cy , r * s , 0 , Math . PI * 2 ); ctx . stroke (); ctx . setLineDash ([]); ctx . globalAlpha = 1 };
const path = ( pts , c , a , lw )=>{ ctx . strokeStyle = c ; ctx . globalAlpha = a ; ctx . lineWidth = lw ; ctx . beginPath (); let pen = false ; for ( const p of pts ){ if ( ! p || Math . abs ( p [ 0 ]) > 200 || Math . abs ( p [ 1 ]) > 200 ){ pen = false ; continue } pen ? ctx . lineTo ( X ( p ), Y ( p )) : ctx . moveTo ( X ( p ), Y ( p )); pen = true } ctx . stroke (); ctx . globalAlpha = 1 };
2026-10-23 20:22:32 +00:00
const O = L . outer ? OUTER : [];
if ( L . unit ){ ring ( 1 , col . ring ); sides . forEach ( sd => path ( sd . pts . map ( p => inv ( p , 1 )), col . ring ,. 7 , 1.1 )); O . forEach ( o => path ( o . map ( p => inv ( p , 1 )), col . ring ,. 4 , 1 ))}
if ( L . fit ){ ring ( rho , col . fit ); sides . forEach ( sd => path ( sd . pts . map ( p => inv ( p , rho * rho )), col . fit ,. 85 , 1.2 )); O . forEach ( o => path ( o . map ( p => inv ( p , rho * rho )), col . fit ,. 45 , 1 ))}
if ( L . orig ){ sides . forEach ( sd => path ( sd . pts , sd . up ? col . up : col . down , 1 , 1.3 )); O . forEach ( o => path ( o , col . fg ,. 45 , 1 )); path ( circ ( 1 ), col . fg ,. 6 , 1 )}
2026-10-23 20:11:32 +00:00
if ( L . fit && Math . abs ( rho - RHO_FIT ) < 1e-3 ){ ctx . fillStyle = col . fit ; const A = Math . atan2 ( M1 [ 1 ], M1 [ 0 ]), m = Math . hypot (... M1 ); for ( let k = 0 ; k < 3 ; k ++ ){ const a = A + k * 2 * Math . PI / 3 ; ctx . beginPath (); ctx . arc ( X ([ m * Math . cos ( a ), 0 ]), Y ([ 0 , m * Math . sin ( a )]), 3.5 , 0 , 7 ); ctx . fill ()}}
ctx . fillStyle = col . bindu ; ctx . beginPath (); ctx . arc ( cx , cy , 2.4 , 0 , Math . PI * 2 ); ctx . fill ();
document . getElementById ( 'rv' ). textContent = rho . toFixed ( 4 );
document . getElementById ( 'note' ). textContent =
'Yantra in gold and pink; its inversion in the rim (ρ = 1) in violet; its inversion in a circle of radius ρ in teal. ' +
2026-10-23 20:22:32 +00:00
( Math . abs ( rho - RHO_FIT ) < 1e-3 ? `Fit: ρ = ${ RHO_FIT . toFixed ( 4 ) } . Two big lobes and one inner lobe meet at the image of a triple point (the T4 apex on the T6 base); this ρ lands it on the side midpoints of the outer upward triangle (teal dots).` : 'Fit trefoil snaps ρ so the lobe meeting points land on the outer triangle’ s side midpoints.' );
2026-10-23 20:11:32 +00:00
}
const $ = id => document . getElementById ( id );
2026-10-23 20:22:32 +00:00
[ 'orig' , 'unit' , 'fit' , 'outer' ]. forEach ( k => $ ( 'l-' + k ). onclick = e =>{ L [ k ] =! L [ k ]; e . currentTarget . setAttribute ( 'aria-pressed' , L [ k ]); draw ()});
2026-10-23 20:11:32 +00:00
$ ( 'rho' ). oninput = e =>{ rho = Math . pow ( 10 , + e . target . value ); draw ()};
$ ( 'snap' ). onclick = ()=>{ rho = RHO_FIT ; $ ( 'rho' ). value = Math . log10 ( rho ); if ( ! L . fit ){ L . fit = true ; $ ( 'l-fit' ). setAttribute ( 'aria-pressed' , 'true' )} draw ()};
$ ( 'rho' ). value = Math . log10 ( RHO_FIT );
const ptrs = new Map (); let pinch = null ;
cv . addEventListener ( 'pointerdown' , e =>{ cv . setPointerCapture ( e . pointerId ); ptrs . set ( e . pointerId ,[ e . clientX , e . clientY ])});
cv . addEventListener ( 'pointermove' , e =>{ if ( ! ptrs . has ( e . pointerId )) return ; const prev = ptrs . get ( e . pointerId ); ptrs . set ( e . pointerId ,[ e . clientX , e . clientY ]);
if ( ptrs . size === 1 ){ pan [ 0 ] += e . clientX - prev [ 0 ]; pan [ 1 ] += e . clientY - prev [ 1 ]; draw ()}
else if ( ptrs . size === 2 ){ const [ a , b ] = [... ptrs . values ()]; const d = Math . hypot ( a [ 0 ] - b [ 0 ], a [ 1 ] - b [ 1 ]); if ( pinch ){ zoom = Math . min ( 40 , Math . max (. 05 , zoom * d / pinch )); draw ()} pinch = d }});
const up = e =>{ ptrs . delete ( e . pointerId ); if ( ptrs . size < 2 ) pinch = null };
cv . addEventListener ( 'pointerup' , up ); cv . addEventListener ( 'pointercancel' , up );
cv . addEventListener ( 'wheel' , e =>{ e . preventDefault (); zoom = Math . min ( 40 , Math . max (. 05 , zoom * Math . exp ( - e . deltaY * 0.0015 ))); draw ()},{ passive : false });
addEventListener ( 'resize' , size ); size ();
</ script >
</ body >
</ html >