2026-10-28 04:17:08 +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 > 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.5 px ; line-height : 1.6 ;
padding : calc ( 28 px + env ( safe - area - inset - top , 0 px )) max ( 16 px , env ( safe - area - inset - right , 0 px )) calc ( 48 px + env ( safe - area - inset - bottom , 0 px )) max ( 16 px , env ( safe - area - inset - left , 0 px ))}
main { max-width : 1040 px ; margin : 0 auto ; display : flex ; flex-direction : column ; gap : 18 px }
. eyebrow { font-size : 11 px ; letter-spacing : .12 em ; text-transform : uppercase ; color : var ( -- muted )}
. eyebrow a { color : var ( -- muted )}
h1 { font-family : var ( -- display ); font-weight : 500 ; font-size : clamp ( 30 px , 5.5 vw , 46 px ); line-height : 1.05 ; margin : .15 em 0 0 ; text-wrap : balance }
h1 i { color : var ( -- down )}
. lede { color : var ( -- muted ); max-width : 72 ch ; margin : 0 }
. lede b { color : var ( -- fg ); font-weight : 500 }
. bar { display : flex ; flex-wrap : wrap ; gap : 10 px 18 px ; align-items : center ; padding : 12 px 14 px ; border : 1 px solid var ( -- line ); border-radius : 6 px ; background : var ( -- panel )}
. seg { display : inline-flex ; border : 1 px solid var ( -- line ); border-radius : 5 px ; overflow : hidden }
. seg button { background : transparent ; color : var ( -- fg ); border : 0 ; padding : 6 px 11 px ; font : inherit ; cursor : pointer }
. seg button + button { border-left : 1 px solid var ( -- line )}
. seg button [ aria-pressed = "true" ] { background : var ( -- up ); color : #1a1408 }
label . k { color : var ( -- muted ); font-size : 11.5 px ; letter-spacing : .06 em ; text-transform : uppercase ; margin-right : 6 px }
. chk { display : inline-flex ; gap : 6 px ; align-items : center ; cursor : pointer }
input [ type = range ] { accent-color : var ( -- up ); width : 150 px ; vertical-align : middle }
. row { display : grid ; grid-template-columns : 1.35 fr 1 fr ; gap : 14 px ; align-items : start }
. pane { border : 1 px solid var ( -- line ); border-radius : 6 px ; background : var ( -- panel ); overflow : hidden }
. pane h2 { font-family : var ( -- display ); font-weight : 500 ; font-size : 20 px ; margin : 0 ; padding : 8 px 12 px 0 }
. pane p . sub { margin : 0 ; padding : 0 12 px 8 px ; color : var ( -- muted ); font-size : 12 px ; min-height : 1.6 em }
. lens { position : relative ; aspect-ratio : 1 ; margin : 0 12 px 12 px ; border-radius : 50 % ; overflow : hidden ; background : #000 ; box-shadow : 0 0 0 1 px var ( -- line )}
. lens canvas { position : absolute ; inset : 0 ; width : 100 % ; height : 100 % ; transform-origin : 50 % 50 % }
. busy { position : absolute ; left : 50 % ; bottom : 10 px ; transform : translateX ( -50 % ); font-size : 11 px ; color : var ( -- up ); opacity : 0 ; transition : opacity .2 s ; pointer-events : none }
. busy . on { opacity : 1 }
. read { padding : 0 12 px 10 px ; color : var ( -- muted ); font-size : 12 px ; min-height : 1.6 em }
. read b { color : var ( -- fg ); font-weight : 500 }
. side { display : flex ; flex-direction : column ; gap : 14 px }
. card { border : 1 px solid var ( -- line ); border-radius : 6 px ; background : var ( -- panel ); padding : 14 px 16 px ; color : var ( -- muted ); font-size : 12.5 px }
. card h3 { font-family : var ( -- display ); font-weight : 500 ; font-size : 19 px ; color : var ( -- fg ); margin : 0 0 6 px }
. card p { margin : 0 0 8 px }
. 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 : 4 px ; margin : 4 px 0 8 px }
. notes { display : grid ; grid-template-columns : 1 fr 1 fr ; gap : 14 px }
. dot { display : inline-block ; width : 8 px ; height : 8 px ; border-radius : 50 % ; margin-right : 5 px ; vertical-align : middle }
. line { display : inline-block ; width : 14 px ; height : 2 px ; margin-right : 5 px ; vertical-align : middle }
@ media ( max-width : 760px ) {. row , . notes { grid-template-columns : 1 fr }}
</ 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 >
2026-10-28 05:13:26 +00:00
< 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 >
2026-10-28 04:17:08 +00:00
< 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 & 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> 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 >