← Mike Higgins Home
Custom Code Experiment •

Conformal Map 1

Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>conformal map</title>
  <style>
    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
    html, body {
      width: 100%; height: 100%;
      background: #000;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      font-family: system-ui, -apple-system, sans-serif;
      color: #ccc;
    }
    #wrap {
      width: min(96vw, 900px);
      display: flex;
      flex-direction: column;
      align-items: center;
    }
    canvas {
      display: block;
      border-radius: 6px;
      width: 60%;
    }
    #ctrl {
      display: flex;
      gap: 20px;
      align-items: center;
      justify-content: center;
      flex-wrap: wrap;
      padding: 10px 2px 0;
      font-size: 13px;
      color: #888;
    }
    #ctrl label {
      display: flex;
      align-items: center;
      gap: 6px;
    }
    #ctrl input[type=range] {
      width: 80px;
      accent-color: #aaa;
    }
    #ctrl input[type=checkbox] {
      accent-color: #aaa;
    }
    #ctrl span {
      min-width: 30px;
      color: #bbb;
    }
  </style>
</head>
<body>
<div id="wrap">
  <canvas id="c"></canvas>
  <div id="ctrl">
    <label>Zoom <input type="range" id="zoom" min="0.5" max="3" step="0.01" value="1.3"> <span id="zv">1.30</span></label>
    <label>Spiral <input type="range" id="spiral" min="-2" max="2" step="0.01" value="0.5"> <span id="sv">0.50</span></label>
    <label>Recursion <input type="range" id="depth" min="1" max="6" step="1" value="4"> <span id="dv">4</span></label>
    <label>Animate <input type="checkbox" id="anim" checked></label>
  </div>
</div>

<script>
const canvas = document.getElementById('c');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');

function resize() {
  const wrapW = canvas.parentElement.clientWidth || 900;
  const w = Math.round(wrapW * 0.6);
  const h = Math.round(w * 0.85);
  canvas.width = w;
  canvas.height = h;
  canvas.style.height = h + 'px';
  if (gl) gl.viewport(0, 0, w, h);
}
resize();
window.addEventListener('resize', () => { resize(); if (!animOn) requestAnimationFrame(render); });

const vs = `attribute vec2 a; void main(){ gl_Position=vec4(a,0,1); }`;

const fs = `
precision highp float;
uniform vec2 uRes; uniform float uTime,uZoom,uSpiral; uniform int uDepth;
#define PI 3.14159265358979
vec2 cmul(vec2 a,vec2 b){return vec2(a.x*b.x-a.y*b.y,a.x*b.y+a.y*b.x);}
vec2 cexp(vec2 z){return exp(z.x)*vec2(cos(z.y),sin(z.y));}
vec2 clog(vec2 z){return vec2(log(length(z)),atan(z.y,z.x));}
vec2 droste(vec2 z,float s,float r){return cexp(cmul(vec2(s,r),clog(z)));}
float tile(vec2 p){
  p=fract(p)-0.5; float d=1e9;
  for(int k=0;k<6;k++){
    float a=float(k)*PI/3.0;
    vec2 q=vec2(cos(a)*p.x-sin(a)*p.y,sin(a)*p.x+cos(a)*p.y);
    float f=length(q-vec2(0.25,0.0))-0.22;
    f=max(f,-length(q-vec2(0.38,0.0))+0.12); d=min(d,f);
  }
  vec2 h=vec2(p.x+p.y*0.577,p.y*1.1547);
  vec2 hf=fract(h*1.5)-0.5;
  float hex=max(abs(hf.x),abs(hf.x*0.5+hf.y*0.866));
  return min(d,hex-0.45);
}
float scene(vec2 uv){
  float v=0.0; vec2 z=uv;
  for(int i=0;i<8;i++){
    if(i>=uDepth) break;
    if(length(z)<0.001) break;
    z=droste(z,1.0,uSpiral+uTime*0.12)*uZoom;
    float d=tile(z*0.5);
    float w=exp(-float(i)*0.4)*smoothstep(0.05,-0.05,d);
    v+=w*(0.5+0.5*sin(float(i)*1.3+uTime*0.3));
  }
  return v;
}
void main(){
  vec2 uv=(gl_FragCoord.xy-uRes*0.5)/min(uRes.x,uRes.y)*2.2;
  vec2 px=1.0/uRes; float v=0.0;
  v+=scene(uv+vec2(-0.25,-0.25)*px); v+=scene(uv+vec2(0.25,-0.25)*px);
  v+=scene(uv+vec2(-0.25,0.25)*px);  v+=scene(uv+vec2(0.25,0.25)*px);
  v*=0.25; float r=length(uv);
  v*=smoothstep(0.05,0.35,r);
  vec3 ink=vec3(0.96,0.96,0.94),paper=vec3(0.03,0.03,0.04),mid=vec3(0.45,0.55,0.60);
  vec3 col=mix(paper,ink,smoothstep(0.0,1.0,v));
  col=mix(col,mid,0.18*sin(v*PI));
  col+=(fract(sin(dot(gl_FragCoord.xy,vec2(127.1,311.7)))*43758.5453)-0.5)*0.025;
  col=mix(col,paper,smoothstep(0.9,1.1,r));
  gl_FragColor=vec4(clamp(col,0.0,1.0),1.0);
}`;

function mkShader(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));
  return s;
}

const prog = gl.createProgram();
gl.attachShader(prog, mkShader(gl.VERTEX_SHADER, vs));
gl.attachShader(prog, mkShader(gl.FRAGMENT_SHADER, fs));
gl.linkProgram(prog);
gl.useProgram(prog);

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 a = gl.getAttribLocation(prog, 'a');
gl.enableVertexAttribArray(a);
gl.vertexAttribPointer(a, 2, gl.FLOAT, false, 0, 0);

const uResL    = gl.getUniformLocation(prog, 'uRes');
const uTimeL   = gl.getUniformLocation(prog, 'uTime');
const uZoomL   = gl.getUniformLocation(prog, 'uZoom');
const uSpiralL = gl.getUniformLocation(prog, 'uSpiral');
const uDepthL  = gl.getUniformLocation(prog, 'uDepth');

let t0 = performance.now(), animOn = true;

function render(now){
  const t = animOn ? (now - t0) * 0.001 : 0;
  gl.uniform2f(uResL, canvas.width, canvas.height);
  gl.uniform1f(uTimeL, t);
  gl.uniform1f(uZoomL, parseFloat(document.getElementById('zoom').value));
  gl.uniform1f(uSpiralL, parseFloat(document.getElementById('spiral').value));
  gl.uniform1i(uDepthL, parseInt(document.getElementById('depth').value));
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  if (animOn) requestAnimationFrame(render);
}
requestAnimationFrame(render);

document.getElementById('zoom').oninput = function(){
  document.getElementById('zv').textContent = parseFloat(this.value).toFixed(2);
  if (!animOn) requestAnimationFrame(render);
};
document.getElementById('spiral').oninput = function(){
  document.getElementById('sv').textContent = parseFloat(this.value).toFixed(2);
  if (!animOn) requestAnimationFrame(render);
};
document.getElementById('depth').oninput = function(){
  document.getElementById('dv').textContent = this.value;
  if (!animOn) requestAnimationFrame(render);
};
document.getElementById('anim').onchange = function(){
  animOn = this.checked;
  if (animOn){ t0 = performance.now(); requestAnimationFrame(render); }
};
</script>
</body>
</html>