Files
site/strut/index.html
T

322 lines
22 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>The Zeta Strut</title>
<meta name="description" content="ζ, ξ and the Davenport–Heilbronn function painted in two charts: the s-plane, and w = 1/(s − ½), where the critical line is the strut through 0 and ∞.">
<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 yantra pages. One column: controls, two painted panes side by side, the Z strip, 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:70ch;margin:0}
.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:140px}
.panes{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.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 6px;color:var(--muted);font-size:12px}
.view{position:relative;aspect-ratio:1;background:#000}
.view canvas{position:absolute;inset:0;width:100%;height:100%}
.busy{position:absolute;right:8px;top:6px;font-size:11px;color:var(--up);opacity:0;transition:opacity .2s}
.busy.on{opacity:1}
.strip{border:1px solid var(--line);border-radius:6px;background:var(--panel);padding:8px 12px 10px}
.strip h2{font-family:var(--display);font-weight:500;font-size:20px;margin:0}
.strip p{margin:0 0 6px;color:var(--muted);font-size:12px}
.strip canvas{width:100%;height:150px;display:block;background:#000;border-radius:4px}
.notes{display:grid;grid-template-columns:1.3fr 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 b{color:var(--fg);font-weight:500}
.dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px;vertical-align:middle}
@media (max-width:760px){.panes,.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> · the strut</div>
<h1>The zeta <i>strut</i></h1>
</header>
<p class="lede">Put ½ at the centre: <b>w = 1/(s − ½)</b>. The critical line becomes the imaginary axis, the strut through 0 and ∞; the functional equation s ↦ 1 − s becomes w ↦ −w, a sign flip through the centre; the pole s = 1 and the point s = 0 land at w = ±2, a dipole on the real axis. Then switch ζ for a function with the same symmetry and no Euler product, and watch zeros leave the strut.</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">Davenport–Heilbronn</button></span></span>
<span><label class="k">left pane</label>
<span class="seg"><button data-chart="s" aria-pressed="true">s-plane</button><button data-chart="w" aria-pressed="false">w = 1/(s−½)</button></span></span>
<label class="chk"><input type="checkbox" id="zeros" checked> mark zeros</label>
<label class="chk"><input type="checkbox" id="lines" checked> strut &amp; mirror</label>
<span><label class="k">height</label><input type="range" id="span" min="10" max="120" step="1" value="40"> <span id="spanRead">±40</span></span>
</div>
<div class="panes">
<section class="pane">
<h2 id="leftTitle">s-plane</h2>
<p class="sub" id="leftSub">critical line gold, real axis white, zeros as dots</p>
<div class="view"><canvas id="cL"></canvas><canvas id="oL"></canvas><span class="busy" id="bL">painting</span></div>
</section>
<section class="pane">
<h2>Riemann sphere, ½ in front, ∞ behind</h2>
<p class="sub">the s-plane wrapped: the strut is the vertical great circle, the mirror the horizontal one, zeros march up the strut toward ∞</p>
<div class="view"><canvas id="cR"></canvas><canvas id="oR"></canvas><span class="busy" id="bR">painting</span></div>
</section>
</div>
<section class="strip">
<h2>Reading the strut: Z(t), the completed function made real</h2>
<p id="stripSub">ξ(½ + it), the completed ζ: real on the line; its sign changes are the nontrivial zeros.</p>
<canvas id="cZ" height="150"></canvas>
</section>
<section class="notes">
<div class="card">
<h3>What the three paints show</h3>
<p><b>ζ.</b> In w the pole is the bright point at +2 and the trivial zeros −2, −4, … crowd in toward −2 from the other side: the dipole. Every nontrivial zero sits on the strut.</p>
<p><b>ξ.</b> Pole and trivial zeros gone; ξ(s) = ξ(1 − s) makes the picture exactly symmetric under w ↦ −w. On the strut ξ is real and swings like cos; on the real axis it grows like cosh. The strip plots it divided by its own growth, Hardy's Z(t).</p>
<p><b>Davenport–Heilbronn.</b> f = ((1 − iκ) L(s, χ) + (1 + iκ) L(s, χ̄))/2 with χ the odd character mod 5, χ(2) = i, and κ = (√(10 − 2√5) − 2)/(√5 − 1) ≈ 0.2841. It has the same completed symmetry Λ(s) = Λ(1 − s), so the strut and the mirror are still there, but no Euler product. Its first off-line zero is at s ≈ 0.8085 + 85.70i, marked in pink: in w it has left the strut. Raise the height past 86 to see it.</p>
<p>So the symmetry alone is not what pins zeros to the strut. The Euler product is the extra incidence, the way the yantra's 31 triple points were. Checked numerically to 30 digits in <code>dh_check.py</code>.</p>
</div>
<div class="card">
<h3>Key</h3>
<p><span class="dot" style="background:var(--up)"></span>critical line Re s = ½ (the strut, Im w)</p>
<p><span class="dot" style="background:#ddd"></span>real axis (the mirror, Re w)</p>
<p><span class="dot" style="background:#fff"></span>zeros on the line</p>
<p><span class="dot" style="background:var(--down)"></span>zeros off the line (Davenport–Heilbronn)</p>
<p><span class="dot" style="background:var(--teal)"></span>the dipole: pole s = 1 (w = 2) and s = 0 (w = −2)</p>
<p>Hue is the argument of the value, brightness its size. The sphere is the s-plane wrapped by stereographic projection with s = ½ at the front and ∞ at the back; the equator is |s − ½| = height/4.</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 LN5=Math.log(5);
// Bernoulli B_{2k}/(2k)! for Euler-Maclaurin
const BC=[1/12,-1/720,1/30240,-1/1209600,1/47900160,-691/1307674368000,1/74724249600,-3617/10670622842880000,43867/5109094217170944000,-174611/802857662698291200000];
const LOGN=new Float64Array(600);for(let n=1;n<600;n++)LOGN[n]=Math.log(n);
// Hurwitz zeta(s,a) by Euler-Maclaurin, valid for Re s > -2*BC.length roughly, any Im with N ~ |s|
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); // (N+a)^-s
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; // (N+a)^{1-s}/(s-1)
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){ // any z off the non-positive integers
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 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;}
logSin(Math.PI*sr/2,Math.PI*si/2,T1);lnGamma(1-sr,-si,T2);zetaEM(1-sr,-si,T3);
out[0]=sr*LN2+(sr-1)*LNPI+T1[0]+T2[0]+0.5*Math.log(T3[0]*T3[0]+T3[1]*T3[1]);
out[1]=si*(LN2+LNPI)+T1[1]+T2[1]+Math.atan2(T3[1],T3[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];
}
// Davenport-Heilbronn. chi mod 5 odd, chi(2)=i: chi(1)=1,chi(2)=i,chi(3)=-i,chi(4)=-1
const KAPPA=(Math.sqrt(10-2*Math.sqrt(5))-2)/(Math.sqrt(5)-1);
const H=[[0,0],[0,0],[0,0],[0,0]];
function dhRaw(sr,si,out){ // for Re s >= 0.5 directly; returns f(s) (not log)
for(let a=1;a<=4;a++)hurwitz(sr,si,a/5,H[a-1]);
// L(chi) = 5^-s * (H1 + i H2 - i H3 - H4); L(chibar) = 5^-s * (H1 - i H2 + i H3 - H4)
const Ar=H[0][0]-H[3][0],Ai=H[0][1]-H[3][1]; // H1 - H4
const Br=H[1][0]-H[2][0],Bi=H[1][1]-H[2][1]; // H2 - H3
// L = A + iB ; Lbar-type = A - iB (as functions, not conjugates)
const Lr=Ar-Bi,Li=Ai+Br, Mr=Ar+Bi,Mi=Ai-Br;
// f = ((1-ik)L + (1+ik)M)/2 = (L+M)/2 + ik(M-L)/2
let fr=(Lr+Mr)/2 - KAPPA*(Mi-Li)/2, fi=(Li+Mi)/2 + KAPPA*(Mr-Lr)/2;
const m=Math.exp(-sr*LN5),t=-si*LN5,cr=m*Math.cos(t),ci=m*Math.sin(t); // 5^-s
out[0]=fr*cr-fi*ci;out[1]=fr*ci+fi*cr;
}
function logDH(sr,si,out){
if(sr>=0.5){dhRaw(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;}
// f(s) = X(s) f(1-s), X = (5/pi)^{1/2-s} Gamma((2-s)/2)/Gamma((1+s)/2)
dhRaw(1-sr,-si,T1);
lnGamma((2-sr)/2,-si/2,T2);lnGamma((1+sr)/2,si/2,T3);
const lq=LN5-LNPI;
out[0]=(0.5-sr)*lq+T2[0]-T3[0]+0.5*Math.log(T1[0]*T1[0]+T1[1]*T1[1]);
out[1]=-si*lq+T2[1]-T3[1]+Math.atan2(T1[1],T1[0]);
}
// completed DH: Lambda = (5/pi)^{s/2} Gamma((1+s)/2) f(s)
function logLamDH(sr,si,out){lnGamma((1+sr)/2,si/2,T4);logDH(sr,si,T5);const lq=LN5-LNPI;out[0]=sr/2*lq+T4[0]+T5[0];out[1]=si/2*lq+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,shade){
if(!isFinite(sr)||!isFinite(si)||Math.hypot(sr,si)>900){buf[o]=buf[o+1]=buf[o+2]=40*shade;buf[o+3]=255;return;}
if(fn===1)logXi(sr,si,L);else if(fn===2)logDH(sr,si,L);else logZeta(sr,si,L);
const scale=fn===1?4: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.2+0.65*(0.5+Math.atan(0.7*L[0]/scale)/Math.PI))*shade;hsv(h,0.62,v,buf,o);
}
onmessage=(e)=>{
const m=e.data,n=m.n,buf=new Uint8ClampedArray(n*n*4);
if(m.job==="plane"){ // chart 's' or 'w', window [cx±hw]
for(let j=0;j<n;j++)for(let i=0;i<n;i++){
const x=m.cx+(i/(n-1)-0.5)*2*m.hw,y=m.cy-(j/(n-1)-0.5)*2*m.hw;
let sr,si;
if(m.chart==="s"){sr=x;si=y;}else{const d=x*x+y*y;sr=0.5+x/d;si=-y/d;} // s = 1/2 + 1/w
paint(sr,si,m.fn,buf,(j*n+i)*4,1);
}
}else if(m.job==="sphere"){ // unit sphere; s = ½ at the front, ∞ at the back
for(let j=0;j<n;j++)for(let i=0;i<n;i++){
const X=(i/(n-1)-0.5)*2.08,Y=-(j/(n-1)-0.5)*2.08,o=(j*n+i)*4,r2=X*X+Y*Y;
if(r2>1){buf[o]=buf[o+1]=buf[o+2]=0;buf[o+3]=0;continue;}
const Z=Math.sqrt(1-r2); // toward viewer; s=½ at the front (Z=1), s=∞ at the back
const ur=X/(1+Z),ui=Y/(1+Z); // stereographic from the back pole
const sr=0.5+ur/m.R,si=ui/m.R; // s − ½ = u/R: the equator is |s − ½| = 1/R
const shade=0.45+0.55*Math.max(0,(0.3*X+0.35*Y+0.9*Z)/1.02);
paint(sr,si,m.fn,buf,o,shade);
}
}else if(m.job==="strip"){
const out=new Float64Array(m.pts.length);
// Hardy-style Z(t): the real completed function divided by the modulus of its completing factor,
// so Z = ±|ζ(½+it)| (or ±|f(½+it)|): O(1) growth, every on-line zero a crossing.
for(let k=0;k<m.pts.length;k++){const t=m.pts[k];let E;
if(m.fn===2){logLamDH(0.5,t,L);lnGamma(0.75,t/2,T2);E=0.25*(LN5-LNPI)+T2[0];}
else{logXi(0.5,t,L);lnGamma(0.25,t/2,T2);E=Math.log((t*t+0.25)/2)-0.25*LNPI+T2[0];} // |s(s−1)/2| = (t²+¼)/2 at s = ½+it
out[k]=Math.exp(L[0]-E)*Math.cos(L[1]);}
postMessage({id:m.id,job:"strip",out},[out.buffer]);return;
}
postMessage({id:m.id,job:m.job,n,buf},[buf.buffer]);
};
</script>
<script>
(()=>{
const $=id=>document.getElementById(id);
const ZEROS=[14.134725,21.022040,25.010858,30.424876,32.935062,37.586178,40.918719,43.327073,48.005151,49.773832,52.970321,56.446248,59.347044,60.831779,65.112544,67.079811,69.546402,72.067158,75.704691,77.144840,79.337375,82.910381,84.735493,87.425275,88.809111,92.491899,94.651344,95.870634,98.831194,101.317851,103.725538,105.446623,107.168611,111.029536,111.874659,114.320221,116.226680,118.790783,121.370125];
// Davenport-Heilbronn zeros: on-line ones found by sign change at runtime; the off-line one below 120 is known
const DH_OFF=[[0.808517,85.699348]];
const st={fn:0,chart:"s",span:40,zeros:true,lines:true};
const src=$("wsrc").textContent,blob=new Blob([src],{type:"text/javascript"}),url=URL.createObjectURL(blob);
const W={L:new Worker(url),R:new Worker(url),Z:new Worker(url)};
let ids={L:0,R:0,Z:0};
const N_LO=160,N_HI=420;
function win(){ // left pane window
if(st.chart==="s")return{cx:0.5,cy:0,hw:st.span};
return{cx:0,cy:0,hw:st.span<=12?2.6:8/st.span}; // w-chart: low height shows the dipole at ±2; higher zooms on the strut, where the zeros crowd 0
}
function sToXY(sr,si){ // to left-pane fraction coords
const w=win();let x,y;
if(st.chart==="s"){x=sr;y=si;}else{const d=(sr-0.5)*(sr-0.5)+si*si;x=(sr-0.5)/d;y=-si/d;}
return[(x-w.cx)/(2*w.hw)+0.5,0.5-(y-w.cy)/(2*w.hw)];
}
function sToSphere(sr,si,R){const ur=(sr-0.5)*R,ui=si*R,q=ur*ur+ui*ui;
const Z=(1-q)/(1+q),X=2*ur/(1+q),Y=2*ui/(1+q);return[X/2.08+0.5,0.5-Y/2.08,Z];}
function sphereR(){return 4/st.span;} // equator at |s − ½| = span/4, so the zeros in view spread over the front
function draw(k,hi){
const n=hi?N_HI:N_LO,id=++ids[k];
$(k==="L"?"bL":"bR").classList.add("on");
if(k==="L"){const w=win();W.L.postMessage({id,job:"plane",n,fn:st.fn,chart:st.chart,cx:w.cx,cy:w.cy,hw:w.hw});}
else W.R.postMessage({id,job:"sphere",n,fn:st.fn,R:sphereR()});
}
for(const k of["L","R"]){W[k].onmessage=(e)=>{const m=e.data;if(m.id!==ids[k])return;
const c=$(k==="L"?"cL":"cR");c.width=c.height=m.n;c.getContext("2d").putImageData(new ImageData(m.buf,m.n,m.n),0,0);
$(k==="L"?"bL":"bR").classList.remove("on");overlay(k);};}
function overlay(k){
const o=$(k==="L"?"oL":"oR"),S=o.clientWidth*devicePixelRatio;o.width=o.height=S;const g=o.getContext("2d");g.clearRect(0,0,S,S);
const P=(sr,si)=>{if(k==="L"){const[x,y]=sToXY(sr,si);return[x*S,y*S,1];}const[x,y,z]=sToSphere(sr,si,sphereR());return[x*S,y*S,z];};
g.lineWidth=1.2*devicePixelRatio;
if(st.lines){
// strut: Re s = 1/2 ; mirror: Im s = 0
const path=(pts,col)=>{g.strokeStyle=col;g.beginPath();let pen=false;for(const[sr,si]of pts){const[x,y,z]=P(sr,si);if(z<-0.02||!isFinite(x)){pen=false;continue;}if(!pen){g.moveTo(x,y);pen=true;}else g.lineTo(x,y);}g.stroke();};
const strut=[],mirror=[];for(let t=-400;t<=400;t+=0.25){strut.push([0.5,t]);}for(let x=-400;x<=400;x+=0.25){mirror.push([x,0]);}
path(strut,"rgba(240,180,76,.85)");path(mirror,"rgba(230,230,230,.6)");
}
const dot=(sr,si,col,r)=>{const[x,y,z]=P(sr,si);if(z<-0.02||!isFinite(x))return;g.fillStyle=col;g.beginPath();g.arc(x,y,r*devicePixelRatio,0,Math.PI*2);g.fill();};
if(st.fn===0){dot(1,0,"#4fd1c5",3.2);dot(0,0,"#4fd1c5",3.2);}
if(st.zeros){
if(st.fn===2){for(const t of DH_ON)for(const sg of[1,-1])dot(0.5,sg*t,"#fff",2.4);for(const[a,b]of DH_OFF)for(const sg of[1,-1])for(const sr of[a,1-a])dot(sr,sg*b,"#e2559a",3.4);}
else for(const t of ZEROS)for(const sg of[1,-1])dot(0.5,sg*t,"#fff",2.4);
if(st.fn===0)for(let n=2;n<=40;n+=2)dot(-n,0,"#4fd1c5",1.8);
}
}
let DH_ON=[];
// strip
let stripData=null,stripT=null;
function strip(){
const T=Math.max(st.span,10),pts=new Float64Array(1200);for(let k=0;k<pts.length;k++)pts[k]=k/(pts.length-1)*T;
stripT=pts;const id=++ids.Z;W.Z.postMessage({id,job:"strip",fn:st.fn,pts});
}
W.Z.onmessage=(e)=>{if(e.data.id!==ids.Z)return;stripData=e.data.out;
if(st.fn===2){DH_ON=[];for(let k=1;k<stripData.length;k++)if(stripData[k-1]*stripData[k]<0)DH_ON.push(stripT[k]);overlay("L");overlay("R");}
drawStrip();};
function drawStrip(){
const c=$("cZ"),Wd=c.clientWidth*devicePixelRatio,Hd=150*devicePixelRatio;c.width=Wd;c.height=Hd;const g=c.getContext("2d");g.clearRect(0,0,Wd,Hd);
if(!stripData)return;const T=stripT[stripT.length-1];
let mx=0;for(const v of stripData)mx=Math.max(mx,Math.abs(v));mx=mx||1;
const Y=v=>Hd/2-v/mx*(Hd/2-8);
g.strokeStyle="rgba(230,230,230,.25)";g.lineWidth=devicePixelRatio;g.beginPath();g.moveTo(0,Hd/2);g.lineTo(Wd,Hd/2);g.stroke();
g.strokeStyle="#f0b44c";g.lineWidth=1.4*devicePixelRatio;g.beginPath();
for(let k=0;k<stripData.length;k++){const x=stripT[k]/T*Wd,y=Y(stripData[k]);k?g.lineTo(x,y):g.moveTo(x,y);}g.stroke();
// crossings
g.fillStyle="#fff";for(let k=1;k<stripData.length;k++)if(stripData[k-1]*stripData[k]<0){const x=stripT[k]/T*Wd;g.beginPath();g.arc(x,Hd/2,2.2*devicePixelRatio,0,7);g.fill();}
if(st.fn===2)for(const[a,b]of DH_OFF)if(b<=T){const x=b/T*Wd;g.strokeStyle="#e2559a";g.setLineDash([4*devicePixelRatio,4*devicePixelRatio]);g.beginPath();g.moveTo(x,0);g.lineTo(x,Hd);g.stroke();g.setLineDash([]);
g.fillStyle="#e2559a";g.font=`${11*devicePixelRatio}px JetBrains Mono`;g.fillText("off-line zero, no crossing",x+6*devicePixelRatio,14*devicePixelRatio);}
g.fillStyle="#9a93ad";g.font=`${11*devicePixelRatio}px JetBrains Mono`;g.fillText("t = 0",4*devicePixelRatio,Hd-6*devicePixelRatio);g.fillText("t = "+T,Wd-70*devicePixelRatio,Hd-6*devicePixelRatio);
}
function all(){draw("L",false);draw("R",false);clearTimeout(all._t);all._t=setTimeout(()=>{draw("L",true);draw("R",true);},250);strip();
$("leftTitle").textContent=st.chart==="s"?"s-plane":"w = 1/(s − ½)";
$("leftSub").textContent=st.chart==="s"?"critical line gold, real axis white; window ±"+st.span+" around ½":st.span<=12?"the strut is vertical through the centre; ±2 are the pole and s = 0 (ζ only); zeros crowd w = 0":"zoomed on w = 0, where the zeros crowd: window ±"+(8/st.span).toFixed(2)+" (height ≤ 12 shows the dipole)";}
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));
$("stripSub").textContent=st.fn===2?"Z(t) for Davenport–Heilbronn: real on the line because the symmetry holds, but at t ≈ 85.7 the zero has stepped off the strut, so the curve does not cross.":st.fn===1?"ξ(½ + it) over its growth: cos-like, every zero a crossing.":"ξ(½ + it), the completed ζ: real on the line; its sign changes are the nontrivial zeros.";all();});
document.querySelectorAll("[data-chart]").forEach(b=>b.onclick=()=>{st.chart=b.dataset.chart;document.querySelectorAll("[data-chart]").forEach(x=>x.setAttribute("aria-pressed",x===b));all();});
$("zeros").onchange=e=>{st.zeros=e.target.checked;overlay("L");overlay("R");};
$("lines").onchange=e=>{st.lines=e.target.checked;overlay("L");overlay("R");};
$("span").oninput=e=>{st.span=+e.target.value;$("spanRead").textContent="±"+st.span;clearTimeout($("span")._t);$("span")._t=setTimeout(all,120);};
addEventListener("resize",()=>{overlay("L");overlay("R");drawStrip();});
all();
})();
</script>
</body>
</html>