Files
site/lamp/index.html
T

306 lines
27 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>Lamp at ½</title>
<meta name="description" content="The zeta ball projected from the other pole: lamp at the touch point ½, screen at ∞. Same frame, the dial a plain rotation, and every zero falls into the centre.">
<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. Controls bar, then a round lens (the u-plane) beside a cross-section and key, then notes. */
:root{--bg:#0b0a12;--panel:#15131f;--line:#2a2638;--fg:#e9e4f2;--muted:#9a93ad;--up:#f0b44c;--down:#e2559a;--bindu:#ff3b4e;--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:150px;vertical-align:middle}
.row{display:grid;grid-template-columns:1.35fr 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}
.lens{position:relative;aspect-ratio:1;margin:0 12px 12px;border-radius:50%;overflow:hidden;background:#000;box-shadow:0 0 0 1px var(--line)}
.lens canvas{position:absolute;inset:0;width:100%;height:100%;transform-origin:50% 50%}
.busy{position:absolute;left:50%;bottom:10px;transform:translateX(-50%);font-size:11px;color:var(--up);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}
.side{display:flex;flex-direction:column;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}
.card svg{width:100%;height:auto;display:block;background:#000;border-radius:4px;margin:4px 0 8px}
.notes{display:grid;grid-template-columns:1fr 1fr;gap:14px}
.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="../strut/">strut</a> · <a href="../dial/">dial</a> · <a href="../flow/">flow</a> · lamp at ½</div>
<h1>Lamp at <i>½</i></h1>
</header>
<p class="lede" style="margin-top:-6px">Three-minute narrated version on the <a href="../#lamp" style="color:var(--up)">front page</a>: two lamps, the swap, the zeros falling in, and ξ’s light cone.</p>
<p class="lede">The zeta ball, projected from the other pole. Put the lamp at the touch point ½ and the screen at the top, tangent at ∞: the chart is <b>u = ¼ / (s − ½)</b>. The frame does not move — the poles still sit at ±½, the third strut at ±i/2, the circle |u| = ½ is where both charts agree — only the two ends of the ½–∞ strut have swapped. The old lamp, ∞, is now the centre; the old touch point, ½, is now at infinity. In this chart the functional equation is u ↦ −u and the whole dial is a rotation about the centre. And every zero, trivial and critical, falls into the origin: the trivial ones along the real axis from one side, the critical ones along the strut from both.</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">χ = ζ(s)/ζ(1−s)</button></span></span>
<span><label class="k">zoom</label><input type="range" id="zoom" min="0" max="100" step="1" value="0"> <span id="zoomRead">±0.60</span></span>
<span><label class="k">dial</label><input type="range" id="dial" min="0" max="360" step="1" value="0"> <span id="dialRead">0°</span>
<span class="seg" style="margin-left:6px"><button data-stop="0">0</button><button data-stop="90">¼</button><button data-stop="180">½</button><button data-stop="270">¾</button></span></span>
<label class="chk"><input type="checkbox" id="zeros" checked> zeros</label>
<label class="chk"><input type="checkbox" id="frame" checked> frame</label>
<label class="chk"><input type="checkbox" id="grid"> the ball's latitude &amp; longitude</label>
</div>
<div class="row">
<section class="pane">
<h2 id="title">u = ¼ / (s − ½), centred on ∞</h2>
<p class="sub" id="sub">window ±0.60 · strut gold, mirror white, poles teal, zeros white</p>
<div class="lens" id="lens">
<canvas id="paint"></canvas>
<canvas id="marks"></canvas>
<canvas id="fixed"></canvas>
<span class="busy" id="busy">painting</span>
</div>
<p class="read" id="read">hover for u and s</p>
</section>
<div class="side">
<div class="card">
<h3>Cross-section through the real axis</h3>
<svg id="xs" viewBox="0 0 100 58" aria-label="cross-section of the ball with both projections"></svg>
<p>The ball of diameter ½ on the s-plane. <b>Old lamp</b> at the top, ∞, casting onto the floor: the poles land on 0 and 1. <b>New lamp</b> at the touch point ½, casting onto the ceiling: the same poles land directly above, at u = ∓½, and ∞ itself lands at the centre. The trivial zeros, bunched near the top of the ball, fall onto the ceiling at −1/(8n+2): −0.1, −0.056, −0.038, … into the centre from the left.</p>
</div>
<div class="card">
<h3>Key</h3>
<p><span class="line" style="background:var(--up)"></span>the strut: critical line Re s = ½, now the imaginary axis</p>
<p><span class="line" style="background:#ddd"></span>the mirror: real axis of s, still the real axis</p>
<p><span class="dot" style="background:var(--teal)"></span>the ball's poles s = 0, 1 at u = ∓½ (ζ's pole is the right one)</p>
<p><span class="dot" style="background:#fff"></span>nontrivial zeros at u = ∓i/(4t)</p>
<p><span class="dot" style="background:var(--teal);width:6px;height:6px"></span>trivial zeros at u = −1/(8n+2)</p>
<p><span class="dot" style="background:var(--down)"></span>poles of χ at u = +1/(8n+2), the mirror images of its zeros</p>
<p><span class="line" style="background:#777"></span>the circle |u| = ½: the ball's horizontal equator, the one circle both charts draw in the same place</p>
<p><span class="line" style="background:var(--teal);opacity:.6"></span>the strip's edges Re s = 0 and Re s = 1: two circles of diameter ½ kissing at the centre. The critical strip is everything outside them, pinched to nothing at the lamp</p>
<p>Hue is the argument of the value, brightness its size. The dial turns the ball about the ½–∞ strut; the chart's own crosshair stays put.</p>
</div>
</div>
</div>
<section class="notes">
<div class="card">
<h3>Same frame, other lamp</h3>
<p>Stereographic projection from the top of a ball of diameter d onto the floor, and from the bottom onto the ceiling, differ by z ↦ d²/z̄. With d = ½ that is u = ¼/(s − ½), read from inside the ball so the mirror comes out the right way round. Points on the circle |s − ½| = ½ — through 0, 1 and ½ ± i/2 — are the ball's horizontal equator and land in the same place under both lamps. Everything else trades near for far: the small disc around ½ becomes the whole outside of the lens, and the whole far field of s becomes the small disc around the centre.</p>
<p>So this is the chart centred on the point the s-plane does not have. The real origin ½ has gone to infinity and the imaginary origin ∞ sits at 0.</p>
</div>
<div class="card">
<h3>The dial is a rotation</h3>
<p>s ↦ 1 − s is u ↦ −u, a point reflection through the centre. Turning the ball by θ about the ½–∞ strut is u ↦ e<sup>−iθ</sup>u, a rotation of the plane. The quarter turn is the Wick rotation: it lays the strut, with every nontrivial zero on it, along the real axis. No Möbius map, no moving circles. Switch the paint to ξ and the half turn leaves the picture exactly as it was; with ζ it does not, and χ is the difference.</p>
</div>
<div class="card">
<h3>The zeros fall in</h3>
<p>Nontrivial zeros ½ ± it sit at u = ∓i/(4t): on the strut, approaching the centre from above and below. Trivial zeros −2n sit at u = −1/(8n + 2): on the mirror, approaching from the left only — the positive real axis carries ζ(2), ζ(3), … → 1 and no zeros at all. One-sided along the real line, two-sided along the imaginary one: the real line reaches the lamp as a ray, the imaginary line as a circle through it. Both sequences converge to a point of this chart. In the s-chart that point is missing, which is the only reason they looked like they ran away.</p>
<p>The strip itself is the outside of two kissing circles, the images of Re s = 0 and Re s = 1. Zoom in and the strip narrows like a horn, its two edges tangent to the strut at the centre: in this chart the strip has no width at the lamp. That is a fact about the chart, not about the zeros — Davenport–Heilbronn's off-line zeros would fall into the same cusp — but it is why, seen from ∞, on the line and near the line are the same thing.</p>
</div>
<div class="card">
<h3>χ, what the half turn leaves over</h3>
<p>χ(s) = ζ(s)/ζ(1 − s) = 2<sup>s</sup>π<sup>s−1</sup> sin(πs/2) Γ(1 − s): the gamma factor by itself. Every nontrivial zero cancels out of it. What remains lives on the mirror — zeros at u = −½, −1/(8n+2) on the left, poles at u = +½, +1/(8n+2) on the right, each the point reflection of the other, since χ(s)χ(1 − s) = 1. On the strut |χ| = 1 exactly: pure phase, winding as the Riemann–Siegel θ(t). ζ on the line is that phase times a real amplitude, Hardy's Z. Fold χ in and the picture becomes ξ, symmetric under the half turn.</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];
// Hurwitz zeta(s,a) by Euler-Maclaurin; N grows with |s| so the Bernoulli tail converges
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){ // Stirling with shift; Re z should not be very negative (callers reflect first)
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];
// log chi(s), chi = 2^s pi^(s-1) sin(pi s/2) Gamma(1-s), for Re s <= 1/2 (Gamma argument then has Re >= 1/2)
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){ // chi(s) chi(1-s) = 1
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);
}
onmessage=(e)=>{
const m=e.data,n=m.n,buf=new Uint8ClampedArray(n*n*4);
for(let j=0;j<n;j++)for(let i=0;i<n;i++){
const x=(i/(n-1)-0.5)*2*m.hw,y=-(j/(n-1)-0.5)*2*m.hw;
const d=4*(x*x+y*y); // s = 1/2 + 1/(4u)
paint(0.5+x/d,-y/d,m.fn,buf,(j*n+i)*4);
}
postMessage({id:m.id,n,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,271.4940556,273.4596092,275.5874926,276.4520495,278.2507435,279.2292509,282.4651148,283.2111857,284.835964,286.6674454,287.9119205,289.5798549,291.8462913,293.5584341,294.9653696,295.5732549,297.9792771,299.8403261,301.6493255,302.6967496,304.8643713,305.7289126,307.2194961,310.1094631,311.1651415,312.4278012,313.9852857,315.4756161,317.7348059,318.8531043,321.1601343,322.1445587,323.4669696,324.8628661,327.4439013,329.0330717,329.9532397,331.4744676,333.6453785,334.2113548,336.8418504,338.3399929,339.8582167,341.0422611,342.0548775,344.6617029,346.3478706,347.2726776,349.3162609,350.4084193,351.878649,353.4889005,356.017575,357.1513023,357.9526851,359.743755,361.2893617,363.3313306,364.7360241,366.2127103,367.9935755,368.9684381,370.0509192,373.0619284,373.8648739,375.8259128,376.3240922,378.4366802,379.8729753,381.4844686,383.4435294,384.9561168,385.8613008,387.2228902,388.8461284,391.4560836,392.2450833,393.4277438,395.58287,396.3818542];
const st={fn:0,zoom:0,theta:0,zeros:true,frame:true,grid:false};
const hw=()=>0.6*Math.pow(10,-st.zoom/100*1.78); // ±0.60 down to ±0.01
const src=$("wsrc").textContent,W=new Worker(URL.createObjectURL(new Blob([src],{type:"text/javascript"})));
let jobId=0;const N_LO=150,N_HI=440;
function draw(hi){const id=++jobId;$("busy").classList.add("on");W.postMessage({id,n:hi?N_HI:N_LO,fn:st.fn,hw:hw()});}
W.onmessage=(e)=>{const m=e.data;if(m.id!==jobId)return;const c=$("paint");c.width=c.height=m.n;c.getContext("2d").putImageData(new ImageData(m.buf,m.n,m.n),0,0);$("busy").classList.remove("on");};
function sizeOf(c){const S=Math.round(c.clientWidth*devicePixelRatio);if(c.width!==S){c.width=c.height=S;}return S;}
const px=(x,y,S)=>[(x/(2*hw())+0.5)*S,(0.5-y/(2*hw()))*S];
function marks(){ // everything attached to the ball: turns with the dial
const c=$("marks"),S=sizeOf(c),g=c.getContext("2d");g.clearRect(0,0,S,S);const dpr=devicePixelRatio,h=hw();
const circ=(cx,cy,r,col,wd)=>{g.strokeStyle=col;g.lineWidth=wd*dpr;g.beginPath();const[X,Y]=px(cx,cy,S);g.arc(X,Y,r/(2*h)*S,0,7);g.stroke();};
const seg=(x0,y0,x1,y1,col,wd)=>{g.strokeStyle=col;g.lineWidth=wd*dpr;g.beginPath();let[a,b]=px(x0,y0,S);g.moveTo(a,b);[a,b]=px(x1,y1,S);g.lineTo(a,b);g.stroke();};
const dot=(x,y,col,r)=>{const[X,Y]=px(x,y,S);if(X<-5||Y<-5||X>S+5||Y>S+5)return;g.fillStyle=col;g.beginPath();g.arc(X,Y,r*dpr,0,7);g.fill();};
const ring=(x,y,col,r)=>{const[X,Y]=px(x,y,S);if(X<-5||Y<-5||X>S+5||Y>S+5)return;g.strokeStyle=col;g.lineWidth=1.1*dpr;g.beginPath();g.arc(X,Y,r*dpr,0,7);g.stroke();};
if(st.grid){ // bipolar grid, foci ±1/2: the ball's own latitude (Apollonian circles) and longitude (circles through the poles)
const a=0.5;
for(let m=1;m<=10;m++){const k=Math.pow(2,m/2),cx=a*(k*k+1)/(k*k-1),r=2*a*k/(k*k-1);circ(cx,0,r,"rgba(160,150,190,.28)",0.8);circ(-cx,0,r,"rgba(160,150,190,.28)",0.8);}
for(let m=1;m<12;m++){if(m===6)continue;const al=m*Math.PI/12,cy=a/Math.tan(al),r=Math.hypot(a,cy);circ(0,cy,r,"rgba(160,150,190,.28)",0.8);}
}
if(st.frame){
seg(-3,0,3,0,"rgba(230,230,230,.55)",1); // mirror: real axis of s
seg(0,-3,0,3,"rgba(240,180,76,.7)",1); // strut: critical line
g.setLineDash([3*dpr,3*dpr]);circ(0.25,0,0.25,"rgba(79,209,197,.55)",0.9);circ(-0.25,0,0.25,"rgba(79,209,197,.55)",0.9);g.setLineDash([]); // Re s = 1 and Re s = 0: the strip's edges, kissing at the lamp
dot(-0.5,0,"#4fd1c5",3.4);dot(0.5,0,"#4fd1c5",3.4); // poles of the ball, s = 0 and s = 1
dot(0,0.5,"#9a93ad",2.6);dot(0,-0.5,"#9a93ad",2.6); // third strut, s = 1/2 ∓ i/2
}
if(st.zeros){ // rings, thinned so that at any zoom neighbours stay ≥ 5px apart
const gap=5*dpr,scale=S/(2*h);let last=1e9;
for(const t of ZEROS){const u=1/(4*t),p=u*scale;if(last-p<gap)continue;last=p;ring(0,-u,"#fff",2.6);ring(0,u,"#fff",2.6);}
if(st.fn!==1){last=1e9;for(let n=1;n<=200;n++){const u=1/(8*n+2),p=u*scale;if(last-p<gap)continue;last=p;ring(-u,0,"#4fd1c5",2.2);if(st.fn===2)ring(u,0,"#e2559a",2.2);}}
if(st.fn===2){ring(0.5,0,"#e2559a",4.4);ring(-0.5,0,"#4fd1c5",4.4);}
}
}
function fixed(){ // the chart's own crosshair and the circle both charts share
const c=$("fixed"),S=sizeOf(c),g=c.getContext("2d");g.clearRect(0,0,S,S);const dpr=devicePixelRatio,h=hw();
g.strokeStyle="rgba(255,255,255,.14)";g.lineWidth=dpr;g.beginPath();g.moveTo(0,S/2);g.lineTo(S,S/2);g.moveTo(S/2,0);g.lineTo(S/2,S);g.stroke();
g.strokeStyle="rgba(140,140,150,.7)";g.lineWidth=1.1*dpr;g.setLineDash([3*dpr,4*dpr]);g.beginPath();g.arc(S/2,S/2,0.5/(2*h)*S,0,7);g.stroke();g.setLineDash([]);
g.fillStyle="#f0b44c";g.beginPath();g.arc(S/2,S/2,3*dpr,0,7);g.fill(); // the centre: s = ∞, the old lamp
g.fillStyle="#9a93ad";g.font=`${11*dpr}px JetBrains Mono`;g.textAlign="center";
g.fillText("∞",S/2+9*dpr,S/2-6*dpr);
g.textAlign="left";g.fillText("½ is out past the rim",S/2+10*dpr,S-30*dpr);
}
function turn(){const r=`rotate(${st.theta}deg)`;$("paint").style.transform=r;$("marks").style.transform=r;}
function all(){draw(false);clearTimeout(all._t);all._t=setTimeout(()=>draw(true),220);marks();fixed();
$("sub").textContent=`window ±${hw().toFixed(2)} · `+(st.fn===2?"χ: zeros left, poles right, pure phase on the strut":st.fn===1?"ξ: symmetric under the half turn":"strut gold, mirror white, poles teal, zeros white");}
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));all();});
document.querySelectorAll("[data-stop]").forEach(b=>b.onclick=()=>{st.theta=+b.dataset.stop;$("dial").value=st.theta;$("dialRead").textContent=st.theta+"°";turn();});
$("zoom").oninput=e=>{st.zoom=+e.target.value;$("zoomRead").textContent="±"+hw().toFixed(2);marks();fixed();clearTimeout($("zoom")._t);$("zoom")._t=setTimeout(all,120);};
$("dial").oninput=e=>{st.theta=+e.target.value;$("dialRead").textContent=st.theta+"°";turn();};
$("zeros").onchange=e=>{st.zeros=e.target.checked;marks();};
$("frame").onchange=e=>{st.frame=e.target.checked;marks();};
$("grid").onchange=e=>{st.grid=e.target.checked;marks();};
$("lens").onmousemove=e=>{const r=$("lens").getBoundingClientRect(),h=hw();
let x=((e.clientX-r.left)/r.width-0.5)*2*h,y=-((e.clientY-r.top)/r.height-0.5)*2*h;
const th=st.theta*Math.PI/180,xr=x*Math.cos(th)-y*Math.sin(th),yr=x*Math.sin(th)+y*Math.cos(th); // undo the turn: screen → painted u
const d=4*(xr*xr+yr*yr),sr=0.5+xr/d,si=-yr/d,f=v=>Math.abs(v)<1e-4?v.toExponential(2):v.toFixed(4);
$("read").innerHTML=`u = <b>${f(xr)} ${yr<0?"−":"+"} ${f(Math.abs(yr))}i</b> &nbsp; s = ½ + ¼/u = <b>${f(sr)} ${si<0?"−":"+"} ${f(Math.abs(si))}i</b>`;};
$("lens").onmouseleave=()=>{$("read").textContent="hover for u and s";};
// cross-section: ball of diameter 1/2 resting at 1/2; floor y=0 (s), ceiling y=1/2 (u). SVG units: x' = 20 + 60x, y' = 48 - 60y
(function xs(){const svg=$("xs"),X=x=>20+60*x,Y=y=>48-60*y,out=[];
const el=(t,a,inner="")=>out.push(`<${t} ${Object.entries(a).map(([k,v])=>`${k}="${v}"`).join(" ")}>${inner}</${t}>`);
el("line",{x1:X(-0.3),y1:Y(0),x2:X(1.3),y2:Y(0),stroke:"#ddd","stroke-width":.5});
el("line",{x1:X(-0.3),y1:Y(0.5),x2:X(1.3),y2:Y(0.5),stroke:"#f0b44c","stroke-width":.5});
el("circle",{cx:X(0.5),cy:Y(0.25),r:15,fill:"none",stroke:"#9a93ad","stroke-width":.6});
const P0=[0.25,0.25],P1=[0.75,0.25],S=[0.5,0],Nn=[0.5,0.5];
// old lamp: from N to the floor (dashed, muted)
for(const P of[P0,P1]){const t=0.5/(0.5-P[1]);const fx=0.5+(P[0]-0.5)*t;el("line",{x1:X(Nn[0]),y1:Y(Nn[1]),x2:X(fx),y2:Y(0),stroke:"#777","stroke-width":.45,"stroke-dasharray":"1.2 1.2"});}
// new lamp: from S through the poles to the ceiling (gold)
for(const P of[P0,P1]){const t=0.5/P[1];const cx=0.5+(P[0]-0.5)*t;el("line",{x1:X(S[0]),y1:Y(S[1]),x2:X(cx),y2:Y(0.5),stroke:"#f0b44c","stroke-width":.6});}
el("line",{x1:X(0.5),y1:Y(0),x2:X(0.5),y2:Y(0.5),stroke:"#f0b44c","stroke-width":.4,"stroke-dasharray":"1 1.4"});
// trivial zeros on the ball's left arc, and their fall onto the ceiling
for(let n=1;n<=5;n++){const ph=2*Math.atan(4*n+1),bx=0.5-0.25*Math.sin(ph),by=0.25-0.25*Math.cos(ph),cx=0.5-1/(8*n+2);
el("line",{x1:X(S[0]),y1:Y(S[1]),x2:X(cx),y2:Y(0.5),stroke:"#4fd1c5","stroke-width":.35,opacity:.8});
el("circle",{cx:X(bx),cy:Y(by),r:.9,fill:"#4fd1c5"});el("circle",{cx:X(cx),cy:Y(0.5),r:.8,fill:"#4fd1c5"});}
for(const P of[P0,P1])el("circle",{cx:X(P[0]),cy:Y(P[1]),r:1.1,fill:"#4fd1c5"});
el("circle",{cx:X(0.5),cy:Y(0),r:1.3,fill:"#f0b44c"});el("circle",{cx:X(0.5),cy:Y(0.5),r:1.1,fill:"#e9e4f2"});
const tx=(x,y,s,a={})=>el("text",{x:X(x),y:Y(y),fill:a.fill||"#9a93ad","font-size":a.fs||3.1,"text-anchor":a.anchor||"middle","font-family":"JetBrains Mono, monospace"},s);
tx(0,-0.055,"0");tx(0.5,-0.055,"½");tx(1,-0.055,"1");tx(1.26,-0.055,"s",{fill:"#ddd"});
tx(0,0.55,"−½");tx(0.5,0.55,"0");tx(1,0.55,"½");tx(1.26,0.55,"u",{fill:"#f0b44c"});
tx(0.5,-0.125,"new lamp · the touch point ½",{fill:"#f0b44c",fs:2.8});tx(0.5,0.63,"old lamp · ∞ · lands at the centre",{fill:"#e9e4f2",fs:2.8});
tx(0.19,0.24,"0",{fill:"#4fd1c5",anchor:"end"});tx(0.81,0.24,"1",{fill:"#4fd1c5",anchor:"start"});
tx(0.34,0.44,"−2, −4, …",{fill:"#4fd1c5",fs:2.6,anchor:"end"});
svg.innerHTML=out.join("");})();
addEventListener("resize",()=>{marks();fixed();});
all();
})();
</script>
</body>
</html>