Last active
October 9, 2025 16:25
-
-
Save softyoda/5705f1ce0e5de43279d96e7b33cdd493 to your computer and use it in GitHub Desktop.
3-sphere projected to 2D plane
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width,initial-scale=1" /> | |
| <title>S3 great-circle wire — strictly rate-integrated tour (no teleports)</title> | |
| <style> | |
| :root { color-scheme: dark; } | |
| html,body { margin:0; height:100%; background:#000; } | |
| canvas { display:block; width:100vw; height:100vh; touch-action:none; } | |
| .hud { | |
| position:fixed; inset:0 auto auto 0; padding:8px 10px; | |
| color:#ddd; font:12px/1.35 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; | |
| text-shadow:0 1px 2px #000a; pointer-events:none; | |
| background:linear-gradient(#000a,#0000); | |
| max-width:min(900px, 95vw); | |
| } | |
| .hud b { color:#fff; } | |
| .hud code { color:#fff; background:#222; padding:0 4px; border-radius:3px; } | |
| </style> | |
| </head> | |
| <body> | |
| <canvas id="c"></canvas> | |
| <div class="hud" id="hud"></div> | |
| <script> | |
| const canvas = document.getElementById('c'); | |
| const hud = document.getElementById('hud'); | |
| const gl = canvas.getContext('webgl', {antialias:false}); | |
| if(!gl) alert('WebGL not supported'); | |
| function resize(){ | |
| const dpr = Math.min(2, window.devicePixelRatio || 1); | |
| canvas.width = Math.floor(innerWidth * dpr); | |
| canvas.height = Math.floor(innerHeight * dpr); | |
| } | |
| addEventListener('resize', resize); resize(); | |
| // Interaction | |
| let dragging = false, lx=0, ly=0; | |
| let userDeltaXY = 0.0, userDeltaXZ = 0.0; | |
| canvas.addEventListener('pointerdown', e => { dragging=true; lx=e.clientX; ly=e.clientY; canvas.setPointerCapture(e.pointerId); }); | |
| canvas.addEventListener('pointerup', e => { dragging=false; try{canvas.releasePointerCapture(e.pointerId);}catch{} }); | |
| canvas.addEventListener('pointermove', e => { | |
| if(!dragging) return; | |
| const s = 0.004; | |
| userDeltaXZ += (e.clientX - lx) * s; | |
| userDeltaXY += (e.clientY - ly) * s; | |
| userDeltaXY = Math.max(-Math.PI/2, Math.min(Math.PI/2, userDeltaXY)); | |
| lx = e.clientX; ly = e.clientY; | |
| }); | |
| // Parameters (defaults per your request) | |
| let thickness = 0.0012; | |
| let autoSpeed = 0.25; // only scales derivatives; never used directly in shader | |
| let lineCount = 28; // base number of great-circle bands per family | |
| // Toggles and projection | |
| let showX = 1, showY = 1, showZ = 1, showW = 1; | |
| let projMode = 5; | |
| // Soft multiplicative stepping that supports exact zero | |
| function stepMulSoft(value, factor, increase, min, max){ | |
| const eps = 1e-12; | |
| if (increase && value === 0) value = eps; | |
| const v = Math.max(value, eps); | |
| const f = increase ? (1.0/factor) : factor; | |
| let out = v * f; | |
| if (!increase && out < eps*0.5) out = 0.0; | |
| return Math.max(min, Math.min(max, out)); | |
| } | |
| // Wheel controls: | |
| // - Wheel: thickness | |
| // - Shift+Wheel: speed | |
| // - Alt+Wheel: number of lines (band count) | |
| addEventListener('wheel', e => { | |
| const increase = e.deltaY < 0; | |
| if (e.shiftKey) { | |
| autoSpeed = stepMulSoft(autoSpeed, 1.12, increase, 0.0, 5.0); | |
| } else if (e.altKey) { | |
| // modify line count logarithmically; clamp to a sensible range | |
| const old = lineCount; | |
| const factor = 1.12; | |
| const mult = increase ? (1/factor) : factor; | |
| let nc = Math.round(old * mult); | |
| nc = Math.max(4, Math.min(256, nc)); | |
| if (nc === old) nc += increase ? -1 : 1; // ensure change on small ranges | |
| lineCount = nc; | |
| } else { | |
| thickness = stepMulSoft(thickness, 1.12, increase, 0.0, 0.25); | |
| } | |
| e.preventDefault(); | |
| updateHUD(); | |
| }, {passive:false}); | |
| addEventListener('keydown', e => { | |
| if(e.key==='1') showX ^= 1; | |
| if(e.key==='2') showY ^= 1; | |
| if(e.key==='3') showZ ^= 1; | |
| if(e.key==='4') showW ^= 1; | |
| if(e.key==='5') projMode = 5; | |
| if(e.key==='6') projMode = 6; | |
| if(e.key==='7') projMode = 7; | |
| if(e.key==='8') projMode = 8; | |
| if(e.key==='9') projMode = 9; | |
| updateHUD(); | |
| }); | |
| function updateHUD(){ | |
| const names = { | |
| 5: "Hopf torus (seam-free α,β; χ spread)", | |
| 6: "Hyperspherical χ, θ, φ (Euler-like)", | |
| 7: "Double-equirectangular (α, β) + χ(v)", | |
| 8: "Dual stereographic chart blend", | |
| 9: "Clifford torus emphasis (χ≈π/4)" | |
| }; | |
| hud.innerHTML = ` | |
| <b>S3 great-circle wire</b> — families (X red, Y green, Z blue, W yellow) | |
| • 1–4: toggle families • 5–9: projection | |
| • Wheel: thickness • <code>Shift+Wheel</code>: speed • <code>Alt+Wheel</code>: lines | |
| • Drag: nudge | |
| <br><b>Projection:</b> ${projMode} — ${names[projMode]} | |
| <br><b>Speed</b> (Shift+Wheel): ${autoSpeed.toFixed(3)}× | |
| • <b>Thickness</b> (Wheel): ${thickness.toFixed(4)} | |
| • <b>Lines</b> (Alt+Wheel): ${lineCount} | |
| `; | |
| } | |
| updateHUD(); | |
| // Strictly rate-integrated state (no absolute-time dependence) | |
| let lastTS = performance.now(); | |
| let phi1 = 0.0; // camera angle for XY plane | |
| let phi2 = 0.0; // camera angle for ZW plane | |
| let linePhase = 0.0; // decorative family phase | |
| // Base angular velocities at speed=1 | |
| const w1 = Math.PI/6; // rad/s | |
| const w2 = Math.PI/9; // rad/s | |
| const wLine = 0.8; // family phase cycles per second (scaled by speed) | |
| // CPU matrices | |
| function mat4mul(a,b){ | |
| const r = new Float32Array(16); | |
| for(let i=0;i<4;i++)for(let j=0;j<4;j++){ | |
| let s=0; for(let k=0;k<4;k++) s+=a[i*4+k]*b[k*4+j]; | |
| r[i*4+j]=s; | |
| } | |
| return r; | |
| } | |
| function mat4trans(m){ | |
| const r=new Float32Array(16); | |
| for(let i=0;i<4;i++)for(let j=0;j<4;j++) r[i*4+j]=m[j*4+i]; | |
| return r; | |
| } | |
| function rotXYm(a){ const c=Math.cos(a),s=Math.sin(a); | |
| return new Float32Array([c,-s,0,0, s,c,0,0, 0,0,1,0, 0,0,0,1]);} | |
| function rotZWm(a){ const c=Math.cos(a),s=Math.sin(a); | |
| return new Float32Array([1,0,0,0, 0,1,0,0, 0,0,c,-s, 0,0,s,c]);} | |
| function rotXZm(a){ const c=Math.cos(a),s=Math.sin(a); | |
| return new Float32Array([c,0,-s,0, 0,1,0,0, s,0,c,0, 0,0,0,1]);} | |
| function identity(){ return new Float32Array([1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1]);} | |
| // Fixed pleasant basis Q and its inverse | |
| const Q = (() => { | |
| let M = identity(); | |
| M = mat4mul(rotXZm(0.35), M); | |
| M = mat4mul(rotZWm(-0.22), M); | |
| M = mat4mul(rotXYm(0.17), M); | |
| return M; | |
| })(); | |
| const Qinv = mat4trans(Q); | |
| // Shaders (GLSL ES 1.00) | |
| const vs = ` | |
| attribute vec2 a; | |
| void main(){ gl_Position = vec4(a,0.0,1.0); } | |
| `; | |
| const fs = ` | |
| precision highp float; | |
| uniform vec2 uRes; | |
| uniform float uThick; | |
| uniform vec4 uShow; | |
| uniform vec2 uUserRot; | |
| uniform int uMode; | |
| uniform vec2 uPhi; // camera angles (integrated on CPU) | |
| uniform float uLinePhase; // family phase (integrated on CPU) | |
| uniform mat4 uQ; | |
| uniform mat4 uQinv; | |
| uniform float uLineCount; // number of lines per family (float for GLSL loop math) | |
| const float PI = 3.141592653589793; | |
| // 4D rotation matrices | |
| mat4 rotXY(float a){ float c=cos(a), s=sin(a); | |
| return mat4( c,-s,0.,0., s,c,0.,0., 0.,0.,1.,0., 0.,0.,0.,1.); } | |
| mat4 rotXZ(float a){ float c=cos(a), s=sin(a); | |
| return mat4( c,0.,-s,0., 0.,1.,0.,0., s,0.,c,0., 0.,0.,0.,1.); } | |
| mat4 rotZW(float a){ float c=cos(a), s=sin(a); | |
| return mat4( 1.,0.,0.,0., 0.,1.,0.,0., 0.,0.,c,-s, 0.,0.,s,c); } | |
| mat4 userNudge(vec2 del){ | |
| float w = 0.85; // subtle | |
| return rotXZ(del.y*(1.0-w)) * rotXY(del.x*(1.0-w)); | |
| } | |
| // Parameterizations → S3 | |
| vec4 s3_hopf(vec2 uv){ | |
| float alpha = (uv.x*2.0-1.0)*PI; | |
| float beta = (uv.y*2.0-1.0)*PI; | |
| float e = smoothstep(0.0,1.0,uv.y); | |
| float chi = mix(0.25, 1.32, e); | |
| float c = cos(chi), s = sin(chi); | |
| return vec4(c*cos(alpha), c*sin(alpha), s*cos(beta), s*sin(beta)); | |
| } | |
| vec4 s3_hypersph(vec2 uv){ | |
| float theta = (uv.x*2.0-1.0)*PI; | |
| float phi = (uv.y*2.0-1.0)*PI; | |
| float chi = mix(0.15, 1.42, smoothstep(0.0,1.0,uv.y)); | |
| float c = cos(chi), s = sin(chi); | |
| return vec4(c*cos(theta), c*sin(theta), s*cos(phi), s*sin(phi)); | |
| } | |
| vec4 s3_doubleEQ(vec2 uv){ | |
| float a = (uv.x*2.0-1.0)*PI; | |
| float b = (uv.y*2.0-1.0)*PI; | |
| float chi = mix(0.10, 1.47, uv.y); | |
| float c = cos(chi), s = sin(chi); | |
| return vec4(c*cos(a), c*sin(a), s*cos(b), s*sin(b)); | |
| } | |
| vec4 s3_dualStereo(vec2 uv){ | |
| vec2 xy = uv*2.0-1.0; | |
| vec3 r = vec3(xy, 0.8*(0.5 - distance(uv, vec2(0.5)))); | |
| float R2 = dot(r,r); | |
| vec4 p1 = vec4(2.0*r, R2-1.0) / (R2+1.0); | |
| vec4 p2 = vec4(2.0*r, 1.0-R2) / (R2+1.0); | |
| float w = smoothstep(0.2, 0.8, uv.y); | |
| vec4 p = normalize(mix(p1, p2, w)); | |
| return p; | |
| } | |
| vec4 s3_clifford(vec2 uv){ | |
| float a = (uv.x*2.0-1.0)*PI; | |
| float b = (uv.y*2.0-1.0)*PI; | |
| float chi = 0.25*PI + 0.22*sin(2.0*PI*uv.x) * cos(2.0*PI*uv.y); | |
| float c = cos(chi), s = sin(chi); | |
| return vec4(c*cos(a), c*sin(a), s*cos(b), s*sin(b)); | |
| } | |
| vec4 s3Point(vec2 uv, int mode){ | |
| if(mode==5) return s3_hopf(uv); | |
| if(mode==6) return s3_hypersph(uv); | |
| if(mode==7) return s3_doubleEQ(uv); | |
| if(mode==8) return s3_dualStereo(uv); | |
| return s3_clifford(uv); | |
| } | |
| // Great circle band in 4D | |
| float bandMask4(vec4 p, vec4 n, float w){ | |
| if (w <= 0.0) return 0.0; | |
| float d = abs(dot(p, n)); | |
| float t = sin(w); | |
| return smoothstep(t, t*0.6, d); | |
| } | |
| // Families via normals in orthogonal subspace | |
| float familyMask4(vec4 p, int axis, float w, float phase, float Kf){ | |
| int K = int(Kf); | |
| if (w <= 0.0 || K <= 0) return 0.0; | |
| float m = 0.0; | |
| for(int i=0;i<512;i++){ | |
| if(i>=K) break; | |
| float a = (float(i)/float(K))*PI + phase; | |
| vec4 n; | |
| if(axis==0){ float u=a, v=a*1.61803398875; | |
| n = normalize(vec4(0.0, cos(u)*cos(v), cos(u)*sin(v), sin(u))); | |
| }else if(axis==1){ float u=a, v=a*1.324717957; | |
| n = normalize(vec4(cos(u)*cos(v), 0.0, sin(u), cos(u)*sin(v))); | |
| }else if(axis==2){ float u=a, v=a*1.2207440846; | |
| n = normalize(vec4(cos(u)*cos(v), cos(u)*sin(v), 0.0, sin(u))); | |
| }else{ float u=a, v=a*1.4655712319; | |
| n = normalize(vec4(cos(u)*cos(v), sin(u), cos(u)*sin(v), 0.0)); | |
| } | |
| m = max(m, bandMask4(p, n, w)); | |
| } | |
| return m; | |
| } | |
| void main(){ | |
| vec2 uv = gl_FragCoord.xy / uRes; | |
| vec4 p = s3Point(uv, uMode); | |
| // Camera: R = Q * RZW(phi2) * RXY(phi1) * Qinv (angles pre-integrated on CPU) | |
| mat4 R = uQ * rotZW(uPhi.y) * rotXY(uPhi.x) * uQinv; | |
| // User nudge (subtle) | |
| mat4 U = userNudge(uUserRot); | |
| p = U * (R * p); | |
| float w = uThick; | |
| // Family phases use only the CPU-accumulated uLinePhase | |
| float phase = uLinePhase; | |
| float Kf = uLineCount; | |
| float mx = (uShow.x>0.5)? familyMask4(p, 0, w, phase*0.80, Kf) : 0.0; | |
| float my = (uShow.y>0.5)? familyMask4(p, 1, w, phase*0.67, Kf) : 0.0; | |
| float mz = (uShow.z>0.5)? familyMask4(p, 2, w, phase*0.51, Kf) : 0.0; | |
| float mw = (uShow.w>0.5)? familyMask4(p, 3, w, phase*0.93, Kf) : 0.0; | |
| vec3 col = vec3(0.0); | |
| col += mx * vec3(1.0, 0.25, 0.25); | |
| col += my * vec3(0.25, 1.0, 0.25); | |
| col += mz * vec3(0.30, 0.70, 1.0); | |
| col += mw * vec3(1.0, 0.95, 0.30); | |
| // No vignette | |
| gl_FragColor = vec4(col + col*col*0.85, 1.0); | |
| } | |
| `; | |
| // GL program | |
| function shader(type, src){ | |
| const s = gl.createShader(type); | |
| gl.shaderSource(s, src); | |
| gl.compileShader(s); | |
| if(!gl.getShaderParameter(s, gl.COMPILE_STATUS)){ | |
| console.error(gl.getShaderInfoLog(s)); | |
| throw new Error('Shader compile error'); | |
| } | |
| return s; | |
| } | |
| const prog = gl.createProgram(); | |
| gl.attachShader(prog, shader(gl.VERTEX_SHADER, vs)); | |
| gl.attachShader(prog, shader(gl.FRAGMENT_SHADER, fs)); | |
| gl.linkProgram(prog); | |
| if(!gl.getProgramParameter(prog, gl.LINK_STATUS)){ | |
| console.error(gl.getProgramInfoLog(prog)); | |
| throw new Error('Program link error'); | |
| } | |
| const buf = gl.createBuffer(); | |
| gl.bindBuffer(gl.ARRAY_BUFFER, buf); | |
| gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, 1,1]), gl.STATIC_DRAW); | |
| const aLoc = gl.getAttribLocation(prog, 'a'); | |
| const uRes = gl.getUniformLocation(prog, 'uRes'); | |
| const uThick = gl.getUniformLocation(prog, 'uThick'); | |
| const uShow = gl.getUniformLocation(prog, 'uShow'); | |
| const uUserRot = gl.getUniformLocation(prog, 'uUserRot'); | |
| const uMode = gl.getUniformLocation(prog, 'uMode'); | |
| const uPhi = gl.getUniformLocation(prog, 'uPhi'); | |
| const uLinePhase = gl.getUniformLocation(prog, 'uLinePhase'); | |
| const uQ = gl.getUniformLocation(prog, 'uQ'); | |
| const uQinv = gl.getUniformLocation(prog, 'uQinv'); | |
| const uLineCount = gl.getUniformLocation(prog, 'uLineCount'); | |
| gl.useProgram(prog); | |
| gl.enableVertexAttribArray(aLoc); | |
| gl.vertexAttribPointer(aLoc, 2, gl.FLOAT, false, 0, 0); | |
| // Render loop: strictly integrate; speed only scales derivatives | |
| function render(ts){ | |
| const now = ts; | |
| const dt = Math.max(0, (now - lastTS) * 0.001); | |
| lastTS = now; | |
| phi1 += w1 * autoSpeed * dt; | |
| phi2 += w2 * autoSpeed * dt; | |
| linePhase += wLine * autoSpeed * dt; | |
| gl.viewport(0,0,canvas.width,canvas.height); | |
| gl.uniform2f(uRes, canvas.width, canvas.height); | |
| gl.uniform1f(uThick, thickness); | |
| gl.uniform4f(uShow, showX, showY, showZ, showW); | |
| gl.uniform2f(uUserRot, userDeltaXY, userDeltaXZ); | |
| gl.uniform1i(uMode, projMode); | |
| gl.uniform2f(uPhi, phi1, phi2); | |
| gl.uniform1f(uLinePhase, linePhase); | |
| gl.uniform1f(uLineCount, lineCount); | |
| gl.uniformMatrix4fv(uQ, false, Q); | |
| gl.uniformMatrix4fv(uQinv, false, Qinv); | |
| gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); | |
| requestAnimationFrame(render); | |
| } | |
| requestAnimationFrame(render); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment