• 01 - Critter Models 02
  • 02 - VantaJS Shaders
  • 03 - App Bundle 02
  • 04 - Cathedrals
  • 05 - App Bundle 01
  • 06 - Retroscopic
  • 07 - Games
  • 08 - Voronoi
  • 09 - Terrain
  • 10 - Critter Models 01
  • 11 - Chromatophores
  • 12 - Conformal Maps
  • 13 - Echo Logix
  • 14 - Music
  • 15 - Colour Theory
MIKE'S NOTEBOOK
Notes
Number:
Project:
01Critter Models 02
└ Locust└ Butterfly└ Cockroach└ Spider└ Scorpion└ Dragon└ Brazilian Treehopper└ Tardigrade└ Diatoms└ Bumblebee└ Wasp└ Hammerhead Worm└ Giraffe Weevil└ Trilobite Beetle
02VantaJS Shaders
└ Clouds└ Waves└ Cells└ Rings└ Halo└ Globe└ Net└ Dots└ Birds└ Trunk└ Topology└ About VantaJS
03App Bundle 02
└ Gutindex└ Insect Builder└ OpenLayers
04Cathedrals
└ Lattice└ Lost└ Lucent└ Lumen└ Glass└ Disco└ Hyperion└ Latent
05App Bundle 01
└ Robohash└ PrismJS└ Horoscope└ Binary Jazz└ Morse Code└ URL Extractor└ Entity Escaper└ Phonetic Alphabet└ Toki Pona└ Semaphore└ Cut Up Machine└ Text Mixer└ Currency Freaks└ Host IO└ CrUX API└ Logo Dev└ Web Speech API└ GEO DB
06Retroscopic
└ Galaxy└ Ocean└ Slime└ 24-Cell└ Unicorn└ Polyp└ Oneiric 1└ Oneiric 2└ Node└ Torus Knot└ GLSL Grid└ Liquid Crystal└ Silky└ Butterfly└ Ocean 2└ Icosahedron└ Luma└ Swarm└ Broken Solid└ Hypersphere└ Constraints└ Psycho Killer
01 / 03

LATTICE - WEBGL CATHEDRAL PROCEDURAL SHADER SOURCE CODE

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Lattice Cathedral</title>
<style>
  html, body { margin:0; padding:0; overflow:hidden; background:#000; width:100%; height:100%; }
  canvas { display:block; width:100vw; height:100vh; }
  #hint {
    position:fixed; bottom:14px; left:50%; transform:translateX(-50%);
    color:rgba(255,255,255,0.35); font-family:Georgia, serif; font-size:12px;
    letter-spacing:0.08em; pointer-events:none; transition:opacity 2s ease;
  }
</style>
</head>
<body>
<canvas id="gl"></canvas>
<script>

const canvas = document.getElementById('gl');
let gl = canvas.getContext('webgl2', {antialias:true, alpha:false, powerPreference:'high-performance'})
       || canvas.getContext('webgl', {antialias:true, alpha:false, powerPreference:'high-performance'});

if (!gl) {
  document.body.innerHTML = '<p style="color:#fff;font-family:sans-serif;padding:2em">WebGL is not available on this device.</p>';
  throw new Error('no webgl');
}

const isGL2 = !!gl.LUMINANCE && gl instanceof (window.WebGL2RenderingContext || function(){});
const versionLine = (gl instanceof WebGL2RenderingContext) ? '#version 300 es\n' : '';
const isWebGL2 = gl instanceof WebGL2RenderingContext;

const vertSrc = (isWebGL2 ? '#version 300 es\n' : '') + `
${isWebGL2 ? 'in' : 'attribute'} vec2 aPos;
${isWebGL2 ? 'out' : 'varying'} vec2 vUv;
void main(){
  vUv = aPos * 0.5 + 0.5;
  gl_Position = vec4(aPos, 0.0, 1.0);
}
`;

const fragSrc = (isWebGL2 ? '#version 300 es\n' : '') + `
precision highp float;
${isWebGL2 ? 'in' : 'varying'} vec2 vUv;
${isWebGL2 ? 'out vec4 outColor;' : ''}

uniform vec2  uRes;
uniform float uTime;
uniform vec2  uPointer;
uniform float uPointerEnergy;

#define PI 3.14159265359
#define TAU 6.28318530718

float hash21(vec2 p){
  p = fract(p*vec2(123.34, 456.21));
  p += dot(p, p+45.32);
  return fract(p.x*p.y);
}
vec2 hash22(vec2 p){
  float n = hash21(p);
  float n2 = hash21(p+17.13);
  return vec2(n, n2);
}
float vnoise(vec2 p){
  vec2 i = floor(p), f = fract(p);
  vec2 u = f*f*(3.0-2.0*f);
  float a = hash21(i);
  float b = hash21(i+vec2(1,0));
  float c = hash21(i+vec2(0,1));
  float d = hash21(i+vec2(1,1));
  return mix(mix(a,b,u.x), mix(c,d,u.x), u.y);
}
float fbm(vec2 p){
  float v = 0.0, amp = 0.5;
  for(int i=0;i<5;i++){
    v += amp*vnoise(p);
    p = p*2.03 + vec2(13.1,7.7);
    amp *= 0.55;
  }
  return v;
}

mat2 rot(float a){ float s=sin(a), c=cos(a); return mat2(c,-s,s,c); }

vec2 hyperbolicWarp(vec2 p, float t){
  float r = length(p) + 1e-5;
  float ang = atan(p.y, p.x);
  float rr = sinh(r*0.9) / (cosh(r*0.9)+1.0);
  rr = mix(r, rr*1.8, 0.55 + 0.25*sin(t*0.05));
  ang += 0.18*sin(r*2.0 - t*0.07) + 0.06*sin(t*0.013);
  return vec2(cos(ang), sin(ang)) * rr;
}

vec2 kfold(vec2 p, float t, out float foldEnergy){
  foldEnergy = 0.0;
  float scale = 1.0;
  for(int i=0;i<6;i++){
    float fi = float(i);
    float order = 6.0 + 2.5*sin(t*0.021 + fi*1.7) + 1.0*sin(t*0.0049*fi);
    float wedge = PI/order;
    float a = atan(p.y, p.x);
    a = mod(a, 2.0*wedge);
    a = abs(a - wedge);
    float r = length(p);
    p = vec2(cos(a), sin(a)) * r;
    p *= rot(0.15*sin(t*0.017 + fi*2.3));
    p = p*1.45 - vec2(0.55 + 0.08*sin(t*0.009*fi), 0.0);
    foldEnergy += exp(-r*1.3) / (fi+1.0);
    scale *= 1.45;
  }
  return p / scale * 1.0;
}

float quasi(vec2 p, float t, int n){
  float sum = 0.0;
  float baseAngle = 0.0;
  float golden = 2.39996323;
  for(int i=0;i<8;i++){
    if(i>=n) break;
    float fi = float(i);
    float ang = baseAngle + fi*golden*0.5 + 0.02*sin(t*0.011 + fi);
    vec2 dir = vec2(cos(ang), sin(ang));
    float freq = 3.0 + fi*0.6;
    sum += cos(dot(p, dir)*freq + t*0.03*(0.3+fi*0.07));
  }
  return sum / float(n);
}

vec3 cellField(vec2 p, float t){
  vec2 ip = floor(p);
  vec2 fp = fract(p);
  float minD = 8.0;
  float secondD = 8.0;
  vec2 bestId = vec2(0.0);
  for(int y=-1;y<=1;y++){
    for(int x=-1;x<=1;x++){
      vec2 cell = vec2(float(x), float(y));
      vec2 id = ip + cell;
      vec2 jitter = hash22(id);
      vec2 seedMotion = 0.5 + 0.45*vec2(
        sin(t*0.021 + jitter.x*TAU),
        cos(t*0.017 + jitter.y*TAU)
      );
      vec2 pt = cell + seedMotion;
      float d = length(fp - pt);
      if(d < minD){ secondD = minD; minD = d; bestId = id; }
      else if(d < secondD){ secondD = d; }
    }
  }
  float edge = secondD - minD;
  return vec3(edge, hash21(bestId), minD);
}

float reactionDiffusion(vec2 p, float t){
  float a = fbm(p*1.3 + vec2(t*0.015, -t*0.011));
  float b = fbm(p*2.1 - vec2(-t*0.009, t*0.014) + 4.0);
  float rd = a - 0.6*b;
  rd += 0.15*sin(a*8.0 - t*0.05);
  return rd;
}

vec3 palette(float tcol, float shift){
  vec3 a = vec3(0.32, 0.28, 0.32);
  vec3 b = vec3(0.28, 0.26, 0.30);
  vec3 c = vec3(1.0, 0.9, 0.6);
  vec3 d = vec3(0.30+0.1*sin(shift*0.7), 0.55+0.1*cos(shift*0.5), 0.75+0.15*sin(shift*0.3));
  return a + b*cos(TAU*(c*tcol + d));
}

void main(){
  vec2 res = uRes;
  float aspect = res.x/res.y;
  vec2 uv = (vUv - 0.5) * vec2(aspect, 1.0) * 2.0;

  float t = uTime;

  vec2 pointer = (uPointer - 0.5) * vec2(aspect,1.0) * 2.0;
  float pd = length(uv - pointer);
  float pointerInfluence = uPointerEnergy * exp(-pd*pd*2.2);
  vec2 ripple = normalize(uv - pointer + 1e-4) * pointerInfluence * 0.18 * sin(pd*8.0 - t*1.5);

  vec2 p = uv + ripple;

  vec2 warped = hyperbolicWarp(p, t);

  float foldEnergy;
  vec2 folded = kfold(warped*0.9, t, foldEnergy);

  float q1 = quasi(folded*2.1, t, 5);
  float q2 = quasi(folded*0.8 + 3.7, t, 3);
  float interference = q1*0.6 + q2*0.4;

  float repScale = 2.4 + 0.6*sin(t*0.013) + 0.25*interference;
  vec2 domain = folded * repScale + vec2(0.3*sin(t*0.0071), 0.3*cos(t*0.0067));

  vec3 cellsBig   = cellField(domain*1.0, t);
  vec3 cellsSmall = cellField(domain*3.4 + 11.3, t*1.4);

  float edgeBig   = cellsBig.x;
  float edgeSmall = cellsSmall.x;

  float rd = reactionDiffusion(domain*0.6, t);

  float glowBig   = 1.0/(1.0 + 60.0*edgeBig*edgeBig);
  float glowSmall = 0.5/(1.0 + 180.0*edgeSmall*edgeSmall);

  float foldLines = smoothstep(0.0, 0.06, 0.05 - abs(fract(length(warped)*3.0 - t*0.02) - 0.5)*0.1);

  float eps = 0.0025;
  float dx = cellField(domain + vec2(eps,0.0), t).z - cellField(domain - vec2(eps,0.0), t).z;
  float dy = cellField(domain + vec2(0.0,eps), t).z - cellField(domain - vec2(0.0,eps), t).z;
  vec3 normal = normalize(vec3(-dx, -dy, eps*2.0));
  vec3 lightDir = normalize(vec3(0.4, 0.6, 0.7));
  float diff = clamp(dot(normal, lightDir), 0.0, 1.0);
  float spec = pow(diff, 24.0);

  float colorIndex = cellsBig.y*0.7 + interference*0.3 + rd*0.4 + foldEnergy*0.5;
  float globalShift = t*0.01;
  vec3 baseColor = palette(colorIndex*0.6 + 0.15*sin(t*0.0027), globalShift);

  float depthFalloff = exp(-length(warped)*0.55);
  vec3 col = baseColor * (0.25 + 0.7*diff);
  col += baseColor * glowBig * 0.8;
  col += vec3(0.7,0.65,0.6) * glowSmall * 0.4;
  col += vec3(0.7) * spec * 0.3;
  col += baseColor * foldLines * 0.15;
  col *= 0.4 + 0.5*depthFalloff;

  float shimmer = smoothstep(0.45, 0.5, rd) * (0.5+0.5*sin(t*0.4 + colorIndex*10.0));
  col += vec3(0.5,0.6,0.7) * shimmer * 0.12;

  col += vec3(0.8,0.5,0.3) * pointerInfluence * 0.18 * (0.5+0.5*sin(pd*10.0 - t*2.0));

  float vig = smoothstep(1.5, 0.2, length(uv)/aspect*0.85);
  col *= mix(0.35, 0.95, vig);

  col = col / (1.0 + col*1.0);
  col = pow(col, vec3(1.1));

  float dither = (hash21(gl_FragCoord.xy + fract(t)) - 0.5) / 255.0 * 2.0;
  col += dither;

  ${isWebGL2 ? 'outColor' : 'gl_FragColor'} = vec4(clamp(col,0.0,1.0), 1.0);
}
`;

function compile(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, compile(gl.VERTEX_SHADER, vertSrc));
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, fragSrc));
gl.linkProgram(prog);
if(!gl.getProgramParameter(prog, gl.LINK_STATUS)){
  console.error(gl.getProgramInfoLog(prog));
  throw new Error('program link error');
}
gl.useProgram(prog);

const quad = new Float32Array([-1,-1, 1,-1, -1,1, 1,1]);
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, quad, gl.STATIC_DRAW);
const aPos = gl.getAttribLocation(prog, 'aPos');
gl.enableVertexAttribArray(aPos);
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);

const uRes = gl.getUniformLocation(prog, 'uRes');
const uTime = gl.getUniformLocation(prog, 'uTime');
const uPointer = gl.getUniformLocation(prog, 'uPointer');
const uPointerEnergy = gl.getUniformLocation(prog, 'uPointerEnergy');

function resize(){
  const dpr = Math.min(window.devicePixelRatio || 1, 2.0);
  const w = Math.floor(window.innerWidth * dpr);
  const h = Math.floor(window.innerHeight * dpr);
  if(canvas.width !== w || canvas.height !== h){
    canvas.width = w;
    canvas.height = h;
  }
  gl.viewport(0, 0, canvas.width, canvas.height);
}
window.addEventListener('resize', resize);
resize();

let pointerTarget = [0.5, 0.5];
let pointerSmooth = [0.5, 0.5];
let pointerEnergy = 0.0;

function setPointer(x, y){
  pointerTarget[0] = x / window.innerWidth;
  pointerTarget[1] = 1.0 - y / window.innerHeight;
  pointerEnergy = Math.min(pointerEnergy + 0.35, 1.5);
}
window.addEventListener('mousemove', e => setPointer(e.clientX, e.clientY));
window.addEventListener('touchmove', e => {
  if(e.touches.length>0) setPointer(e.touches[0].clientX, e.touches[0].clientY);
}, {passive:true});

const hint = document.getElementById('hint');
setTimeout(()=>{ if(hint) hint.style.opacity = '0'; }, 6000);

let startTime = performance.now();
function frame(){
  const now = performance.now();
  const t = (now - startTime) / 1000.0;

  pointerSmooth[0] += (pointerTarget[0]-pointerSmooth[0])*0.06;
  pointerSmooth[1] += (pointerTarget[1]-pointerSmooth[1])*0.06;
  pointerEnergy *= 0.965;

  gl.uniform2f(uRes, canvas.width, canvas.height);
  gl.uniform1f(uTime, t);
  gl.uniform2f(uPointer, pointerSmooth[0], pointerSmooth[1]);
  gl.uniform1f(uPointerEnergy, pointerEnergy);

  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>
</body>
</html>
MikeHiggins.me.uk LinkedIn GitHub Bluesky Contra Upwork
© 2026 Mike Higgins. All rights reserved.