← Mike Higgins Home
Custom Code Experiment •

Conformal Map 2

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 2</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;
      overflow: hidden;
    }
    canvas {
      display: block;
      max-width: 60vmin;
      max-height: 60vmin;
      width: 60vmin;
      height: 60vmin;
    }
  </style>
</head>
<body>
  <canvas id="c"></canvas>

  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
  <script>
  (function () {
    const canvas = document.getElementById('c');

    function resize() {
      const S = Math.round(Math.min(window.innerWidth, window.innerHeight) * 0.6);
      canvas.width  = S;
      canvas.height = S;
      canvas.style.width  = S + 'px';
      canvas.style.height = S + 'px';
      if (renderer) {
        renderer.setSize(S, S);
        mat.uniforms.uRes.value.set(S, S);
      }
    }

    const S0 = Math.round(Math.min(window.innerWidth, window.innerHeight) * 0.6);
    canvas.width  = S0;
    canvas.height = S0;

    const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
    renderer.setSize(S0, S0);
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0x000000);

    const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 10);
    camera.position.z = 1;

    /* ─── Vertex shader ─── */
    const vertShader = `
varying vec2 vUv;
void main(){
  vUv = uv;
  gl_Position = vec4(position.xy, 0.0, 1.0);
}`;

    /* ─── Fragment shader ─── */
    const fragShader = `
precision highp float;
varying vec2 vUv;
uniform float uTime;
uniform vec2  uRes;

#define PI  3.14159265358979
#define TAU 6.28318530717959
#define ITER 8

/* ── Complex arithmetic ── */
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 cdiv(vec2 a, vec2 b){
  float d = dot(b,b);
  return vec2(dot(a,b), a.y*b.x - a.x*b.y)/d;
}
vec2 clog(vec2 z){ return vec2(log(length(z)), atan(z.y, z.x)); }
vec2 cexp(vec2 z){ return exp(z.x)*vec2(cos(z.y), sin(z.y)); }

/* ── Turtle SDF (signed-distance field) ── */
float sdTurtle(vec2 p){
  p.y -= 0.08;
  float body = length(p / vec2(0.38, 0.28)) - 1.0;
  float neck = length((p - vec2(0.0,  0.27)) / vec2(0.11, 0.14)) - 1.0;
  float head = length((p - vec2(0.0,  0.44)) / vec2(0.13, 0.12)) - 1.0;
  float fl   = length((p - vec2(-0.30, 0.18)) / vec2(0.16, 0.10)) - 1.0;
  float fr   = length((p - vec2( 0.30, 0.18)) / vec2(0.16, 0.10)) - 1.0;
  float bl   = length((p - vec2(-0.28,-0.20)) / vec2(0.14, 0.09)) - 1.0;
  float br   = length((p - vec2( 0.28,-0.20)) / vec2(0.14, 0.09)) - 1.0;
  float tail = length((p - vec2(0.0, -0.34))  / vec2(0.07, 0.10)) - 1.0;
  return min(body, min(neck, min(head, min(min(fl,fr), min(min(bl,br), tail)))));
}

/* ── Tile colour at a given UV ── */
vec3 eschTileColor(vec2 uv){
  vec2 cell = fract(uv * 3.0 + vec2(0.5));
  float turt = sdTurtle((cell - 0.5) * 0.7);

  /* shell scute shading */
  float scute = sin(uv.x * 28.0) * sin(uv.y * 28.0);

  vec3 shellDark  = vec3(0.12, 0.35, 0.14);
  vec3 shellLight = vec3(0.28, 0.60, 0.26);
  vec3 tileDark   = vec3(0.07, 0.08, 0.09);
  vec3 tileLight  = vec3(0.84, 0.81, 0.74);

  vec3 col;
  if(turt < 0.0){
    /* turtle body */
    col = mix(shellDark, shellLight, smoothstep(-0.12, 0.12, scute));
    /* eye */
    float eye = length(cell - vec2(0.53, 0.81)) - 0.025;
    if(eye < 0.0)         col = vec3(0.04, 0.04, 0.05);
    if(abs(eye) < 0.013)  col = vec3(0.88, 0.82, 0.65);
  } else {
    /* background chequerboard-ish */
    float stripe = sin(cell.x * 18.0 + cell.y * 12.0) * 0.5 + 0.5;
    col = mix(tileDark, tileLight, step(0.5, stripe));
    /* subtle grid lines */
    float gx = abs(fract(uv.x * 3.0 + 0.5) - 0.5);
    float gy = abs(fract(uv.y * 3.0 + 0.5) - 0.5);
    col = mix(vec3(0.50, 0.46, 0.38), col, smoothstep(0.0, 0.04, min(gx, gy)));
  }
  /* outline rim */
  col = mix(vec3(0.58, 0.52, 0.28), col, smoothstep(0.0, 0.022, abs(turt)));

  return col;
}

/* ── Main ── */
void main(){
  vec2 uv = vUv * 2.0 - 1.0;
  uv.x *= uRes.x / uRes.y;   /* square aspect */

  float slowTime = uTime * 0.12;

  float R    = 0.82;   /* outer ring radius */
  float r    = 0.18;   /* inner ring radius */
  float logR = log(R / r);

  vec3  finalColor  = vec3(0.0);
  float totalWeight = 0.0;
  float brightness  = 1.0;

  vec2 w = uv;

  for(int i = 0; i < ITER; i++){
    float wlen = length(w);
    if(wlen < 0.001 || wlen > 3.0) break;

    /* conformal logarithm → (angle, radius) coordinates */
    vec2 logW  = clog(w);
    float angle  = logW.y / TAU;
    float radius = logW.x / logR;

    /* animate by drifting angle */
    vec2 tileUV = vec2(angle + slowTime * 0.5, radius);

    vec3 tileCol = eschTileColor(tileUV * vec2(4.0, 2.0));

    float weight = brightness
                 * smoothstep(3.0,  0.5, wlen)
                 * smoothstep(0.0, 0.15, wlen);
    finalColor   += tileCol * weight;
    totalWeight  += weight;

    /* Droste step: multiply by e^logR to recurse inward */
    w          = w * exp(logR);
    brightness *= 0.70;

    if(length(w) < r * 0.5) break;
  }

  if(totalWeight > 0.001) finalColor /= totalWeight;

  /* vignette */
  finalColor *= smoothstep(1.4, 0.6, length(uv) / R);

  /* golden border glow */
  float borderGlow = smoothstep(0.025, 0.0, abs(length(uv) - R * 0.99));
  finalColor += vec3(0.72, 0.62, 0.32) * borderGlow * 0.7;

  /* subtle scanline texture */
  finalColor += sin(vUv.y * uRes.y * 0.5) * 0.012;

  /* gentle gamma */
  finalColor = pow(clamp(finalColor, 0.0, 1.0), vec3(0.88));

  gl_FragColor = vec4(finalColor, 1.0);
}`;

    const geo = new THREE.PlaneGeometry(2, 2);
    const mat = new THREE.ShaderMaterial({
      vertexShader:   vertShader,
      fragmentShader: fragShader,
      uniforms: {
        uTime: { value: 0 },
        uRes:  { value: new THREE.Vector2(S0, S0) }
      }
    });

    scene.add(new THREE.Mesh(geo, mat));

    /* ── Animation loop ── */
    let frame = 0;
    let raf;
    function animate() {
      raf = requestAnimationFrame(animate);
      mat.uniforms.uTime.value = (++frame) * 0.016;
      renderer.render(scene, camera);
    }
    animate();

    /* ── Pause when off-screen ── */
    new IntersectionObserver(entries => {
      if (!entries[0].isIntersecting) cancelAnimationFrame(raf);
      else animate();
    }, { threshold: 0 }).observe(canvas);

    /* ── Responsive resize ── */
    window.addEventListener('resize', resize);
  })();
  </script>
</body>
</html>