The base case: the sphere stands with 0 at the bottom and ∞ at the top, 0, 1, ∞, −1 a quarter turn apart. Three sliders = the three struts (tip about ±i, slide about 0–∞, dial about ±1); four stop buttons for the tipping turn, whose shadows are the charts s/(1−s), 2s−1, 1−1/s, 1/(1−2s). ζ/ξ/χ as a texture on the sphere (worker), the ball and its shadow side by side, beads for the six frame points and the zeros. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FAqjv3n4yABxvJPJHFRhVA
338 lines
24 KiB
HTML
338 lines
24 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>One Sphere, Four Turns</title>
|
||
<meta name="description" content="ζ painted on the Riemann sphere, turned about its three struts: a quarter turn about ±i is the zeta orientation, a half turn is Li's chart. Four real points a quarter turn apart: 0, 1, ∞, −1.">
|
||
<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>
|
||
/* Same dark look as the other explorers. A ball on the left, its shadow (the chart) on the right, three sliders for the three struts. */
|
||
:root{--bg:#0b0a12;--panel:#15131f;--line:#2a2638;--fg:#e9e4f2;--muted:#9a93ad;--up:#f0b44c;--down:#e2559a;--teal:#4fd1c5;
|
||
--display:"Cormorant Garamond",Georgia,serif;--mono:"JetBrains Mono",ui-monospace,monospace;color-scheme:dark}
|
||
html,body{margin:0}
|
||
body{background:var(--bg);color:var(--fg);font-family:var(--mono);font-size:13.5px;line-height:1.6;
|
||
padding:calc(28px + env(safe-area-inset-top,0px)) max(16px,env(safe-area-inset-right,0px)) calc(48px + env(safe-area-inset-bottom,0px)) max(16px,env(safe-area-inset-left,0px))}
|
||
main{max-width:1040px;margin:0 auto;display:flex;flex-direction:column;gap:18px}
|
||
.eyebrow{font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}
|
||
.eyebrow a{color:var(--muted)}
|
||
h1{font-family:var(--display);font-weight:500;font-size:clamp(30px,5.5vw,46px);line-height:1.05;margin:.15em 0 0;text-wrap:balance}
|
||
h1 i{color:var(--down)}
|
||
.lede{color:var(--muted);max-width:72ch;margin:0}
|
||
.lede b{color:var(--fg);font-weight:500}
|
||
.bar{display:flex;flex-wrap:wrap;gap:10px 18px;align-items:center;padding:12px 14px;border:1px solid var(--line);border-radius:6px;background:var(--panel)}
|
||
.seg{display:inline-flex;border:1px solid var(--line);border-radius:5px;overflow:hidden}
|
||
.seg button{background:transparent;color:var(--fg);border:0;padding:6px 11px;font:inherit;cursor:pointer}
|
||
.seg button+button{border-left:1px solid var(--line)}
|
||
.seg button[aria-pressed="true"]{background:var(--up);color:#1a1408}
|
||
label.k{color:var(--muted);font-size:11.5px;letter-spacing:.06em;text-transform:uppercase;margin-right:6px}
|
||
.chk{display:inline-flex;gap:6px;align-items:center;cursor:pointer}
|
||
input[type=range]{accent-color:var(--up);width:130px;vertical-align:middle}
|
||
.sl{display:inline-flex;align-items:center;gap:6px}
|
||
.sl b{font-weight:500;color:var(--fg);min-width:3.2em;display:inline-block}
|
||
.row{display:grid;grid-template-columns:1fr 1fr;gap:14px;align-items:start}
|
||
.pane{border:1px solid var(--line);border-radius:6px;background:var(--panel);overflow:hidden}
|
||
.pane h2{font-family:var(--display);font-weight:500;font-size:20px;margin:0;padding:8px 12px 0}
|
||
.pane p.sub{margin:0;padding:0 12px 8px;color:var(--muted);font-size:12px;min-height:1.6em}
|
||
.view{position:relative;aspect-ratio:1;margin:0 12px 12px;background:#000;overflow:hidden;border-radius:6px}
|
||
.view.lens{border-radius:50%;box-shadow:0 0 0 1px var(--line)}
|
||
.view canvas{position:absolute;inset:0;width:100%;height:100%}
|
||
.busy{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);font-size:12px;color:var(--up);background:rgba(11,10,18,.75);padding:6px 10px;border-radius:4px;opacity:0;transition:opacity .2s;pointer-events:none}
|
||
.busy.on{opacity:1}
|
||
.read{padding:0 12px 10px;color:var(--muted);font-size:12px;min-height:1.6em}
|
||
.read b{color:var(--fg);font-weight:500}
|
||
.notes{display:grid;grid-template-columns:1fr 1fr;gap:14px}
|
||
.card{border:1px solid var(--line);border-radius:6px;background:var(--panel);padding:14px 16px;color:var(--muted);font-size:12.5px}
|
||
.card h3{font-family:var(--display);font-weight:500;font-size:19px;color:var(--fg);margin:0 0 6px}
|
||
.card p{margin:0 0 8px}
|
||
.card p:last-child{margin-bottom:0}
|
||
.card b{color:var(--fg);font-weight:500}
|
||
.dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px;vertical-align:middle}
|
||
.line{display:inline-block;width:14px;height:2px;margin-right:5px;vertical-align:middle}
|
||
@media (max-width:760px){.row,.notes{grid-template-columns:1fr}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<main>
|
||
<header>
|
||
<div class="eyebrow">maths · <a href="../">two poles</a> · <a href="../zeta/">zeta ball</a> · <a href="../flow/">flow</a> · <a href="../dial/">dial</a> · <a href="../lamp/">lamp</a> · four turns</div>
|
||
<h1>One sphere, <i>four turns</i></h1>
|
||
</header>
|
||
<p class="lede">ζ painted on the Riemann sphere, and nothing else: the sphere stands with 0 at the bottom, ∞ at the top, and the four real points <b>0, 1, ∞, −1</b> a quarter turn apart on the real circle — 1 is half way to ∞. The three struts are the three axes you can turn it about. A quarter turn about the ±i strut tips it on its side: 0 and ∞ land on the unit circle together, and the shadow is zeta's picture. Another quarter turn and ∞ is at the bottom: Li's chart. Turning about the 0–∞ strut slides the touch point along the unit circle, through 1, i, −1, −i. Turning about the ±1 strut is the dial whose half turn is s ↦ 1 − s. The ball is the thing; every chart is its shadow from the top.</p>
|
||
|
||
<div class="bar" role="group" aria-label="controls">
|
||
<span><label class="k">paint</label>
|
||
<span class="seg"><button data-fn="0" aria-pressed="true">ζ</button><button data-fn="1" aria-pressed="false">ξ</button><button data-fn="2" aria-pressed="false">χ</button></span></span>
|
||
<span><label class="k">stops</label>
|
||
<span class="seg"><button data-stop="0">0</button><button data-stop="90">¼</button><button data-stop="180">½</button><button data-stop="270">¾</button></span></span>
|
||
<span class="sl"><label class="k">tip · ±i</label><input type="range" id="a" min="0" max="360" step="1" value="0"><b id="aR">0°</b></span>
|
||
<span class="sl"><label class="k">slide · 0–∞</label><input type="range" id="b" min="0" max="360" step="1" value="0"><b id="bR">0°</b></span>
|
||
<span class="sl"><label class="k">dial · ±1</label><input type="range" id="g" min="0" max="360" step="1" value="0"><b id="gR">0°</b></span>
|
||
<span class="sl"><label class="k">chart window</label><input type="range" id="zoom" min="1" max="6" step="0.1" value="2.4"><b id="zR">±2.4</b></span>
|
||
<label class="chk"><input type="checkbox" id="frame" checked> struts</label>
|
||
<label class="chk"><input type="checkbox" id="beads" checked> beads & zeros</label>
|
||
<label class="chk"><input type="checkbox" id="labels" checked> labels</label>
|
||
</div>
|
||
|
||
<div class="row">
|
||
<section class="pane">
|
||
<h2>The ball</h2>
|
||
<p class="sub" id="subL">seen from the i side; the lamp is at the top, the touch point at the bottom</p>
|
||
<div class="view lens" id="vL"><canvas id="pL"></canvas><canvas id="oL"></canvas><span class="busy" id="busy">painting ζ on the sphere…</span></div>
|
||
</section>
|
||
<section class="pane">
|
||
<h2 id="titleR">Its shadow: the chart</h2>
|
||
<p class="sub" id="subR">stop 0 · w = s / (1 − s): ζ's 0 at the centre, its pole at the lamp</p>
|
||
<div class="view" id="vR"><canvas id="pR"></canvas><canvas id="oR"></canvas></div>
|
||
</section>
|
||
</div>
|
||
<p class="read" id="read">hover the chart for the point's two names</p>
|
||
|
||
<section class="notes">
|
||
<div class="card">
|
||
<h3>The four stops are four charts</h3>
|
||
<p>The quarter turn about the ±i strut is w ↦ (w − 1)/(w + 1): it fixes ±i and cycles the real circle 0 → −1 → ∞ → 1 → 0. Read off the shadow at each stop: <b>0</b>: w = s/(1 − s), ζ's 0 at the centre, its pole at the lamp, the critical line the unit circle. <b>¼</b>: w = 2s − 1, the zeta orientation — 0 and ∞ of the sphere on the unit circle at ∓1 (the scale-and-shift puts them on 0 and 1), the critical line standing up through the touch point. <b>½</b>: w = 1 − 1/s, Li's chart, ζ's pole at the centre and ζ's 0 at the lamp. <b>¾</b>: w = 1/(1 − 2s). Then home.</p>
|
||
</div>
|
||
<div class="card">
|
||
<h3>Three struts, three turns</h3>
|
||
<p>Tip about ±i (the stops above). Slide about 0–∞: the point touching the plane runs round the unit circle, 1 → i → −1 → −i. Dial about ±1: its half turn is s ↦ 1 − s, the functional equation. The three half turns are s ↦ (1 − s)/(1 − 2s), s ↦ s/(2s − 1) and s ↦ 1 − s; each is the product of the other two. On the critical line they read t ↦ 1/4t, t ↦ −1/4t, t ↦ −t.</p>
|
||
</div>
|
||
<div class="card">
|
||
<h3>1 is half way to ∞</h3>
|
||
<p>On the sphere the arc from 0 to 1 equals the arc from 1 to ∞: a quarter turn each. The beads on the rim show it. In ζ's names the four real points are 0, ½, 1 and ∞ — the two poles of its picture and its two units — so the point of the s-plane called ∞ is the sphere's −1, a quarter turn from either pole, no farther than ½ is.</p>
|
||
</div>
|
||
<div class="card">
|
||
<h3>Where the zeros are</h3>
|
||
<p>In the sphere's own frame every nontrivial zero sits on the unit circle, and all of them crowd the point −1: the first within 4° of it, the fortieth within half a degree. The trivial zeros sit on the real circle and crowd −1 from the other side, at −2/3, −4/5, −6/7, … So the one place on the ball where ζ is busy is the bead opposite 1. The lamp page is a magnifying glass held over that bead.</p>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
|
||
<script type="text/plain" id="wsrc">
|
||
const TAU=2*Math.PI,LN2=Math.LN2,LNPI=Math.log(Math.PI),HALF_LN_TAU=0.5*Math.log(TAU);
|
||
const BC=[1/12,-1/720,1/30240,-1/1209600,1/47900160,-691/1307674368000,1/74724249600,-3617/10670622842880000,43867/5109094217170944000,-174611/802857662698291200000];
|
||
function hurwitz(sr,si,a,out){
|
||
const N=Math.min(560,Math.max(14,Math.ceil(Math.hypot(sr,si)/Math.PI)+12));
|
||
let re=0,im=0;
|
||
for(let n=0;n<N;n++){const l=Math.log(n+a),m=Math.exp(-sr*l),t=-si*l;re+=m*Math.cos(t);im+=m*Math.sin(t);}
|
||
const lN=Math.log(N+a),m0=Math.exp(-sr*lN),t0=-si*lN,nr=m0*Math.cos(t0),ni=m0*Math.sin(t0);
|
||
re+=nr/2;im+=ni/2;
|
||
const ar=(N+a)*nr,ai=(N+a)*ni,br=sr-1,bi=si,d=br*br+bi*bi;
|
||
re+=(ar*br+ai*bi)/d;im+=(ai*br-ar*bi)/d;
|
||
let pr=sr,pi=si,sc=1/(N+a);
|
||
for(let k=1;k<=BC.length;k++){
|
||
const tr=pr*nr-pi*ni,ti=pr*ni+pi*nr,c=BC[k-1]*sc;re+=c*tr;im+=c*ti;
|
||
if(Math.abs(c)*Math.hypot(tr,ti)<1e-16*Math.hypot(re,im))break;
|
||
const xr=sr+2*k-1,yr=sr+2*k,qr=xr*yr-si*si,qi=si*(xr+yr);
|
||
const npr=pr*qr-pi*qi;pi=pr*qi+pi*qr;pr=npr;sc/=(N+a)*(N+a);
|
||
}
|
||
out[0]=re;out[1]=im;
|
||
}
|
||
function zetaEM(sr,si,out){hurwitz(sr,si,1,out);}
|
||
function lnGamma(zr,zi,out){
|
||
let accR=0,accI=0;
|
||
while(zr<9){accR+=0.5*Math.log(zr*zr+zi*zi);accI+=Math.atan2(zi,zr);zr+=1;}
|
||
const lr=0.5*Math.log(zr*zr+zi*zi),li=Math.atan2(zi,zr);
|
||
let r=(zr-0.5)*lr-zi*li-zr+HALF_LN_TAU,i=(zr-0.5)*li+zi*lr-zi;
|
||
const d=zr*zr+zi*zi,ir=zr/d,ii=-zi/d,i2r=ir*ir-ii*ii,i2i=2*ir*ii;let pr=ir,pi=ii;
|
||
const cs=[1/12,-1/360,1/1260,-1/1680];
|
||
for(let j=0;j<4;j++){r+=cs[j]*pr;i+=cs[j]*pi;const t=pr*i2r-pi*i2i;pi=pr*i2i+pi*i2r;pr=t;}
|
||
out[0]=r-accR;out[1]=i-accI;
|
||
}
|
||
function logSin(x,y,out){
|
||
if(y>15){out[0]=-LN2+y;out[1]=Math.PI/2-x;return;}
|
||
if(y<-15){out[0]=-LN2-y;out[1]=x-Math.PI/2;return;}
|
||
const re=Math.sin(x)*Math.cosh(y),im=Math.cos(x)*Math.sinh(y);
|
||
out[0]=0.5*Math.log(re*re+im*im);out[1]=Math.atan2(im,re);
|
||
}
|
||
const T1=[0,0],T2=[0,0],T3=[0,0],T4=[0,0],T5=[0,0];
|
||
function logChiLeft(sr,si,out){
|
||
logSin(Math.PI*sr/2,Math.PI*si/2,T1);lnGamma(1-sr,-si,T2);
|
||
out[0]=sr*LN2+(sr-1)*LNPI+T1[0]+T2[0];
|
||
out[1]=si*(LN2+LNPI)+T1[1]+T2[1];
|
||
}
|
||
function logChi(sr,si,out){
|
||
if(sr<=0.5){logChiLeft(sr,si,out);return;}
|
||
logChiLeft(1-sr,-si,out);out[0]=-out[0];out[1]=-out[1];
|
||
}
|
||
function logZeta(sr,si,out){
|
||
if(sr>=0.5){zetaEM(sr,si,T1);out[0]=0.5*Math.log(T1[0]*T1[0]+T1[1]*T1[1]);out[1]=Math.atan2(T1[1],T1[0]);return;}
|
||
logChiLeft(sr,si,T3);zetaEM(1-sr,-si,T1);
|
||
out[0]=T3[0]+0.5*Math.log(T1[0]*T1[0]+T1[1]*T1[1]);
|
||
out[1]=T3[1]+Math.atan2(T1[1],T1[0]);
|
||
}
|
||
function logXi(sr,si,out){
|
||
if(sr<0.5){sr=1-sr;si=-si;}
|
||
const pr=0.5*(sr*(sr-1)-si*si),pi=0.5*si*(2*sr-1);
|
||
lnGamma(sr/2,si/2,T4);logZeta(sr,si,T5);
|
||
out[0]=0.5*Math.log(pr*pr+pi*pi)-sr/2*LNPI+T4[0]+T5[0];
|
||
out[1]=Math.atan2(pi,pr)-si/2*LNPI+T4[1]+T5[1];
|
||
}
|
||
function hsv(h,s,v,buf,o){const i=Math.floor(h*6),f=h*6-i,p=v*(1-s),q=v*(1-f*s),t=v*(1-(1-f)*s);let r,g,b;
|
||
switch(((i%6)+6)%6){case 0:r=v;g=t;b=p;break;case 1:r=q;g=v;b=p;break;case 2:r=p;g=v;b=t;break;case 3:r=p;g=q;b=v;break;case 4:r=t;g=p;b=v;break;default:r=v;g=p;b=q;}
|
||
buf[o]=r*255;buf[o+1]=g*255;buf[o+2]=b*255;buf[o+3]=255;}
|
||
const L=[0,0];
|
||
function paint(sr,si,fn,buf,o){
|
||
if(!isFinite(sr)||!isFinite(si)||Math.hypot(sr,si)>1700){buf[o]=buf[o+1]=buf[o+2]=40;buf[o+3]=255;return;}
|
||
if(fn===1)logXi(sr,si,L);else if(fn===2)logChi(sr,si,L);else logZeta(sr,si,L);
|
||
const scale=fn===1?4:fn===2?2:1;
|
||
if(!isFinite(L[0])){const v=L[0]<0?0:255;buf[o]=buf[o+1]=buf[o+2]=v;buf[o+3]=255;return;}
|
||
let h=(L[1]/TAU)%1;if(h<0)h+=1;
|
||
const v=0.14+0.74*(0.5+Math.atan(0.7*L[0]/scale)/Math.PI);hsv(h,0.62,v,buf,o);
|
||
}
|
||
// texture of the sphere in its own frame: 0 at the south pole, ∞ at the north, 1 at +X, i at +Y.
|
||
// stereographic from the north pole: z0 = (X+iY)/(1-Z); ζ's variable: s = z0/(1+z0) = (X+iY)/((1-Z)+X+iY)
|
||
onmessage=(e)=>{
|
||
const m=e.data,NX=m.nx,NY=m.ny,buf=new Uint8ClampedArray(NX*NY*4);
|
||
for(let j=0;j<NY;j++){
|
||
const th=(j+0.5)/NY*Math.PI,st=Math.sin(th),Z=Math.cos(th);
|
||
for(let i=0;i<NX;i++){
|
||
const ph=(i+0.5)/NX*TAU-Math.PI,X=st*Math.cos(ph),Y=st*Math.sin(ph);
|
||
const dr=(1-Z)+X,di=Y,d=dr*dr+di*di;
|
||
let sr,si;
|
||
if(d<1e-18){sr=Infinity;si=0;}else{sr=(X*dr+Y*di)/d;si=(Y*dr-X*di)/d;}
|
||
paint(sr,si,m.fn,buf,(j*NX+i)*4);
|
||
}
|
||
if(j%32===0)postMessage({id:m.id,progress:j/NY});
|
||
}
|
||
postMessage({id:m.id,nx:NX,ny:NY,buf},[buf.buffer]);
|
||
};
|
||
</script>
|
||
<script>
|
||
(()=>{
|
||
const $=id=>document.getElementById(id);
|
||
const ZEROS=[14.1347251,21.0220396,25.0108576,30.4248761,32.9350616,37.5861782,40.918719,43.3270733,48.0051509,49.7738325,52.9703215,56.4462477,59.347044,60.8317785,65.112544,67.0798105,69.5464017,72.0671577,75.7046907,77.1448401,79.337375,82.9103809,84.735493,87.4252746,88.8091112,92.4918993,94.651344,95.8706342,98.8311942,101.317851,103.725538,105.4466231,107.1686112,111.0295355,111.8746592,114.3202209,116.2266803,118.7907829,121.370125,122.9468293,124.2568186,127.5166839,129.5787042,131.0876885,133.4977372,134.7565098,138.1160421,139.736209,141.1237074,143.1118458,146.0009825,147.4227653,150.0535204,150.9252576,153.0246938,156.1129093,157.5975918,158.8499882,161.1889641,163.0307097,165.5370692,167.18444,169.0945154,169.9119765,173.4115365,174.7541915,176.4414343,178.3774078,179.916484,182.2070785,184.8744678,185.5987837,187.2289226,189.4161587,192.0266564,193.0797266,195.2653967,196.8764818,198.0153097,201.2647519,202.4935945,204.1896718,205.3946972,207.9062589,209.5765097,211.6908626,213.3479194,214.5470448,216.1695385,219.0675963,220.7149188,221.4307056,224.0070003,224.9833247,227.4214443,229.3374133,231.2501887,231.9872353,233.6934042,236.5242297,237.7698205,239.5554776,241.0491578,242.8232719,244.0708985,247.1369901,248.1019901,249.5736896,251.0149478,253.0699867,255.3062565,256.3807137,258.6104395,259.874407,260.8050845,263.5738939,265.5578518,266.6149738,267.9219151,269.970449];
|
||
const st={fn:0,a:0,b:0,g:0,zoom:2.4,frame:true,beads:true,labels:true};
|
||
const NX=1024,NY=512;
|
||
let TEX=null,texFn=-1,jobId=0;
|
||
const W=new Worker(URL.createObjectURL(new Blob([$("wsrc").textContent],{type:"text/javascript"})));
|
||
function paintTexture(){const id=++jobId;$("busy").classList.add("on");$("busy").textContent=`painting ${["ζ","ξ","χ"][st.fn]} on the sphere…`;W.postMessage({id,fn:st.fn,nx:NX,ny:NY});}
|
||
W.onmessage=(e)=>{const m=e.data;if(m.id!==jobId)return;
|
||
if(m.progress!==undefined){$("busy").textContent=`painting ${["ζ","ξ","χ"][st.fn]} on the sphere… ${Math.round(m.progress*100)}%`;return;}
|
||
TEX=m.buf;texFn=st.fn;$("busy").classList.remove("on");render();};
|
||
// rotation: world = Ry(a) Rz(b) Rx(g) body ; we need body = Rx(-g) Rz(-b) Ry(-a) world
|
||
function mats(){
|
||
const a=st.a*Math.PI/180,b=st.b*Math.PI/180,g=st.g*Math.PI/180;
|
||
const ca=Math.cos(a),sa=Math.sin(a),cb=Math.cos(b),sb=Math.sin(b),cg=Math.cos(g),sg=Math.sin(g);
|
||
const Ry=[[ca,0,sa],[0,1,0],[-sa,0,ca]],Rz=[[cb,-sb,0],[sb,cb,0],[0,0,1]],Rx=[[1,0,0],[0,cg,-sg],[0,sg,cg]];
|
||
const mul=(A,B)=>A.map((r,i)=>[0,1,2].map(j=>r[0]*B[0][j]+r[1]*B[1][j]+r[2]*B[2][j]));
|
||
const R=mul(Ry,mul(Rz,Rx)),Ri=R[0].map((_,j)=>R.map(r=>r[j])); // inverse = transpose
|
||
return{R,Ri};
|
||
}
|
||
const ap=(M,v)=>[M[0][0]*v[0]+M[0][1]*v[1]+M[0][2]*v[2],M[1][0]*v[0]+M[1][1]*v[1]+M[1][2]*v[2],M[2][0]*v[0]+M[2][1]*v[1]+M[2][2]*v[2]];
|
||
// texture lookup, bilinear, for a body point
|
||
function look(X,Y,Z,out,o,shade){
|
||
const th=Math.acos(Math.max(-1,Math.min(1,Z))),ph=Math.atan2(Y,X);
|
||
let u=(ph+Math.PI)/(2*Math.PI)*NX-0.5,v=th/Math.PI*NY-0.5;
|
||
if(u<0)u+=NX;const u0=Math.floor(u),v0=Math.max(0,Math.min(NY-2,Math.floor(v))),fu=u-u0,fv=Math.max(0,Math.min(1,v-v0));
|
||
const u1=(u0+1)%NX,i00=(v0*NX+u0)*4,i01=(v0*NX+u1)*4,i10=((v0+1)*NX+u0)*4,i11=((v0+1)*NX+u1)*4;
|
||
for(let c=0;c<3;c++){const t=TEX[i00+c]*(1-fu)*(1-fv)+TEX[i01+c]*fu*(1-fv)+TEX[i10+c]*(1-fu)*fv+TEX[i11+c]*fu*fv;out[o+c]=t*shade;}
|
||
out[o+3]=255;
|
||
}
|
||
function size(c){const S=Math.round(c.clientWidth*devicePixelRatio);if(c.width!==S){c.width=c.height=S;}return S;}
|
||
let imgL=null,imgR=null;
|
||
function renderBall(Ri){
|
||
const c=$("pL"),S=size(c),g=c.getContext("2d");const N=Math.min(S,560);c.width=c.height=N;
|
||
if(!imgL||imgL.width!==N)imgL=g.createImageData(N,N);const d=imgL.data;
|
||
for(let j=0;j<N;j++)for(let i=0;i<N;i++){
|
||
const o=(j*N+i)*4,x=(i+0.5)/N*2-1,y=1-(j+0.5)/N*2,r2=x*x+y*y;
|
||
if(r2>1){d[o]=d[o+1]=d[o+2]=0;d[o+3]=0;continue;}
|
||
const Yw=Math.sqrt(1-r2); // camera on the +Y side looking in: screen x = X, screen y = Z
|
||
const b=ap(Ri,[x,Yw,y]);
|
||
look(b[0],b[1],b[2],d,o,0.55+0.45*Math.max(0,(0.25*x+0.3*y+0.9*Yw)/1.0));
|
||
}
|
||
g.putImageData(imgL,0,0);
|
||
}
|
||
function renderChart(Ri){
|
||
const c=$("pR"),S=size(c),g=c.getContext("2d");const N=Math.min(S,560);c.width=c.height=N;
|
||
if(!imgR||imgR.width!==N)imgR=g.createImageData(N,N);const d=imgR.data,h=st.zoom;
|
||
for(let j=0;j<N;j++)for(let i=0;i<N;i++){
|
||
const o=(j*N+i)*4,x=((i+0.5)/N*2-1)*h,y=(1-(j+0.5)/N*2)*h,q=x*x+y*y;
|
||
const Xw=2*x/(q+1),Yw=2*y/(q+1),Zw=(q-1)/(q+1); // inverse stereographic from the world's top
|
||
const b=ap(Ri,[Xw,Yw,Zw]);
|
||
look(b[0],b[1],b[2],d,o,1);
|
||
}
|
||
g.putImageData(imgR,0,0);
|
||
}
|
||
// overlays: struts (three great circles), beads, zeros, labels
|
||
const BODY={"0":[0,0,-1],"∞":[0,0,1],"1":[1,0,0],"−1":[-1,0,0],"i":[0,1,0],"−i":[0,-1,0]};
|
||
const ZNAME={"0":"s = 0","∞":"s = 1","1":"s = ½","−1":"s = ∞","i":"s = ½ + i/2","−i":"s = ½ − i/2"};
|
||
const BCOL={"0":"#4fd1c5","∞":"#4fd1c5","1":"#ffffff","−1":"#f0b44c","i":"#9a93ad","−i":"#9a93ad"};
|
||
function circlePts(kind){const P=[];for(let k=0;k<=240;k++){const t=k/240*2*Math.PI;
|
||
P.push(kind==="real"?[Math.cos(t),0,Math.sin(t)]:kind==="unit"?[Math.cos(t),Math.sin(t),0]:[0,Math.cos(t),Math.sin(t)]);}return P;}
|
||
const CIRC={real:circlePts("real"),unit:circlePts("unit"),imag:circlePts("imag")};
|
||
const CCOL={real:"rgba(230,230,230,.75)",unit:"rgba(240,180,76,.9)",imag:"rgba(154,147,173,.7)"};
|
||
function ballXY(w,S){return[(w[0]+1)/2*S,(1-w[2])/2*S,w[1]];} // screen x = X, y = Z, depth = Y
|
||
function chartXY(w,S){const den=1-w[2];if(den<1e-6)return null;const x=w[0]/den,y=w[1]/den;const h=st.zoom;return[(x/h+1)/2*S,(1-y/h)/2*S,Math.hypot(x,y)];}
|
||
function overlays(R){
|
||
const dpr=devicePixelRatio;
|
||
for(const side of["L","R"]){
|
||
const c=$("o"+side),S=size(c),g=c.getContext("2d");g.clearRect(0,0,S,S);
|
||
const proj=side==="L"?(w=>ballXY(w,S)):(w=>chartXY(w,S));
|
||
if(st.frame){
|
||
for(const k of["real","unit","imag"]){
|
||
g.strokeStyle=CCOL[k];g.lineWidth=(k==="unit"?1.6:1.2)*dpr;
|
||
let pen=false;g.beginPath();
|
||
for(const p of CIRC[k]){const w=ap(R,p),q=proj(w);
|
||
if(!q||(side==="R"&&q[2]>40)){pen=false;continue;}
|
||
if(side==="L"&&q[2]<0){ // hidden side of the ball: draw faint
|
||
if(pen){g.stroke();pen=false;}continue;}
|
||
if(!pen){g.moveTo(q[0],q[1]);pen=true;}else g.lineTo(q[0],q[1]);}
|
||
g.stroke();
|
||
if(side==="L"){g.strokeStyle=CCOL[k].replace(/[\d.]+\)$/,".22)");g.setLineDash([3*dpr,4*dpr]);let pen2=false;g.beginPath();
|
||
for(const p of CIRC[k]){const w=ap(R,p),q=proj(w);if(q[2]>=0){if(pen2){g.stroke();pen2=false;}continue;}if(!pen2){g.moveTo(q[0],q[1]);pen2=true;}else g.lineTo(q[0],q[1]);}
|
||
g.stroke();g.setLineDash([]);}
|
||
}
|
||
}
|
||
if(st.beads){
|
||
// zeros on the unit circle of the sphere's own frame, thinned by screen distance
|
||
g.lineWidth=1.1*dpr;let last=null;
|
||
for(const t of ZEROS){const th=2*Math.atan(2*t);
|
||
for(const sg of[1,-1]){const w=ap(R,[Math.cos(th),sg*Math.sin(th),0]),q=proj(w);if(!q)continue;
|
||
if(side==="L"&&q[2]<0)continue;if(side==="R"&&q[2]>40)continue;
|
||
if(last&&Math.hypot(q[0]-last[0],q[1]-last[1])<5*dpr){continue;}
|
||
g.strokeStyle="#fff";g.beginPath();g.arc(q[0],q[1],2.4*dpr,0,7);g.stroke();if(sg===1)last=q;}
|
||
}
|
||
last=null;
|
||
for(let n=1;n<=80;n++){const z0=-2*n/(2*n+1),q0=z0*z0,X=2*z0/(q0+1),Z=(q0-1)/(q0+1);const w=ap(R,[X,0,Z]),q=proj(w);if(!q)continue;
|
||
if(side==="L"&&q[2]<0)continue;if(side==="R"&&q[2]>40)continue;
|
||
if(last&&Math.hypot(q[0]-last[0],q[1]-last[1])<5*dpr)continue;last=q;
|
||
g.strokeStyle="#4fd1c5";g.beginPath();g.arc(q[0],q[1],2.1*dpr,0,7);g.stroke();}
|
||
for(const k in BODY){const w=ap(R,BODY[k]),q=proj(w);if(!q||(side==="R"&&q[2]>40))continue;
|
||
const hidden=side==="L"&&q[2]<0;
|
||
g.fillStyle=BCOL[k];g.globalAlpha=hidden?0.35:1;g.beginPath();g.arc(q[0],q[1],(k==="0"||k==="∞"?4.2:3.6)*dpr,0,7);g.fill();g.globalAlpha=1;
|
||
if(st.labels&&!hidden){g.fillStyle="#e9e4f2";g.font=`${12*dpr}px JetBrains Mono`;g.textAlign="left";
|
||
const lx=Math.min(q[0]+7*dpr,S-110*dpr),ly=Math.max(14*dpr,Math.min(S-6*dpr,q[1]-6*dpr));
|
||
g.fillText(k,lx,ly);g.fillStyle="#9a93ad";g.font=`${10*dpr}px JetBrains Mono`;g.fillText(ZNAME[k],lx,ly+12*dpr);}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
function stopText(){
|
||
const z=(st.b%360===0&&st.g%360===0)?{0:"stop 0 · w = s / (1 − s): ζ's 0 at the centre, its pole at the lamp; the critical line is the unit circle",90:"stop ¼ · w = 2s − 1: the zeta orientation — 0 and ∞ of the sphere at ∓1, the critical line through the touch point",180:"stop ½ · w = 1 − 1/s: Li's chart — ζ's pole at the centre, ζ's 0 at the lamp",270:"stop ¾ · w = 1/(1 − 2s): the lamp page's chart, ζ's ∞ at the centre"}[st.a%360]:null;
|
||
$("subR").textContent=z||`tip ${st.a}°, slide ${st.b}°, dial ${st.g}° · the shadow of the turned ball`;
|
||
}
|
||
function render(){if(!TEX)return;const{R,Ri}=mats();renderBall(Ri);renderChart(Ri);overlays(R);stopText();}
|
||
let raf=0;function queue(){if(raf)return;raf=requestAnimationFrame(()=>{raf=0;render();});}
|
||
for(const k of["a","b","g"]){$(k).oninput=e=>{st[k]=+e.target.value;$(k+"R").textContent=st[k]+"°";queue();};}
|
||
$("zoom").oninput=e=>{st.zoom=+e.target.value;$("zR").textContent="±"+st.zoom.toFixed(1);queue();};
|
||
for(const k of["frame","beads","labels"])$(k).onchange=e=>{st[k]=e.target.checked;queue();};
|
||
document.querySelectorAll("[data-fn]").forEach(b=>b.onclick=()=>{st.fn=+b.dataset.fn;document.querySelectorAll("[data-fn]").forEach(x=>x.setAttribute("aria-pressed",x===b));paintTexture();});
|
||
let anim=0;
|
||
document.querySelectorAll("[data-stop]").forEach(b=>b.onclick=()=>{const target=+b.dataset.stop;let from=st.a;let delta=((target-from)%360+540)%360-180;const t0=performance.now();cancelAnimationFrame(anim);
|
||
const step=now=>{const u=Math.min(1,(now-t0)/900),s=u<.5?2*u*u:1-Math.pow(-2*u+2,2)/2;st.a=((from+delta*s)%360+360)%360;$("a").value=Math.round(st.a);$("aR").textContent=Math.round(st.a)+"°";render();if(u<1)anim=requestAnimationFrame(step);else{st.a=target%360;$("a").value=st.a;$("aR").textContent=st.a+"°";render();}};
|
||
anim=requestAnimationFrame(step);});
|
||
$("vR").onmousemove=e=>{const r=$("vR").getBoundingClientRect(),h=st.zoom;const x=((e.clientX-r.left)/r.width*2-1)*h,y=(1-(e.clientY-r.top)/r.height*2)*h,q=x*x+y*y;
|
||
const{Ri}=mats();const b=ap(Ri,[2*x/(q+1),2*y/(q+1),(q-1)/(q+1)]);
|
||
const dr=(1-b[2])+b[0],di=b[1],d=dr*dr+di*di;const sr=(b[0]*dr+b[1]*di)/d,si=(b[1]*dr-b[0]*di)/d;
|
||
const z0r=b[0]/(1-b[2]),z0i=b[1]/(1-b[2]);
|
||
const f=v=>Math.abs(v)>1e4?"∞":(Math.abs(v)<1e-4?v.toExponential(1):v.toFixed(3));
|
||
$("read").innerHTML=`sphere's own chart <b>${f(z0r)} ${z0i<0?"−":"+"} ${f(Math.abs(z0i))}i</b> · ζ's variable s = <b>${f(sr)} ${si<0?"−":"+"} ${f(Math.abs(si))}i</b>`;};
|
||
$("vR").onmouseleave=()=>{$("read").textContent="hover the chart for the point's two names";};
|
||
addEventListener("resize",queue);
|
||
paintTexture();
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|