Skip to content

Instantly share code, notes, and snippets.

@softyoda
Last active October 9, 2025 16:25
Show Gist options
  • Save softyoda/5705f1ce0e5de43279d96e7b33cdd493 to your computer and use it in GitHub Desktop.
Save softyoda/5705f1ce0e5de43279d96e7b33cdd493 to your computer and use it in GitHub Desktop.
3-sphere projected to 2D plane
<!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