• 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

DISCO - WEBGL CATHEDRAL PROCEDURAL SHADER SOURCE CODE

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Disco Cathedral</title>
<style>
  html,body{margin:0;padding:0;overflow:hidden;background:#000;width:100%;height:100%;cursor:crosshair;}
  canvas{display:block;width:100vw;height:100vh;}
  #hint{
    position:fixed;bottom:10px;left:14px;color:rgba(255,255,255,0.45);
    font-family:'Courier New',monospace;font-size:12px;letter-spacing:1px;
    pointer-events:none;user-select:none;text-shadow:0 0 6px rgba(255,255,255,0.4);
    transition:opacity 1.5s ease;
  }
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
const canvas = document.getElementById('c');
const gl = canvas.getContext('webgl2', {antialias:false, alpha:false}) ||
           canvas.getContext('webgl', {antialias:false, alpha:false});
if(!gl){ document.body.innerHTML = '<p style="color:#fff;font-family:sans-serif;padding:2em">WebGL not available.</p>'; }

const isWebGL2 = !!gl.DRAW_FRAMEBUFFER || gl instanceof WebGL2RenderingContext;

const vertSrc = `
attribute vec2 aPos;
void main(){ gl_Position = vec4(aPos,0.0,1.0); }
`;

const fragSrc = `
precision highp float;

uniform vec2  uRes;
uniform float uTime;
uniform vec2  uMouse;
uniform float uMouseActive;
uniform float uPulses[8];
uniform vec2  uPulsePos[8];

#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);
}

float vnoise(vec2 p){
  vec2 i = floor(p), f = fract(p);
  float a = hash21(i);
  float b = hash21(i+vec2(1.0,0.0));
  float c = hash21(i+vec2(0.0,1.0));
  float d = hash21(i+vec2(1.0,1.0));
  vec2 u = f*f*(3.0-2.0*f);
  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(17.1,9.7);
    amp *= 0.55;
  }
  return v;
}

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

vec2 kaleido(vec2 p, float n, float twist){
  float a = atan(p.y, p.x) + twist;
  float r = length(p);
  float seg = TAU / n;
  a = mod(a, seg);
  a = abs(a - seg*0.5);
  return vec2(cos(a), sin(a)) * r;
}

float fractalField(vec2 p, float t, out float structure){
  structure = 0.0;
  float scale = 1.0;
  float accum = 0.0;
  vec2 q = p;
  for(int i=0;i<7;i++){
    q = abs(q) - 0.65 - 0.05*sin(t*0.07 + float(i));
    q = rot(0.35 + 0.12*sin(t*0.05 + float(i)*1.3)) * q;
    float r = length(q)+1e-4;
    q *= (1.0 + 0.18*sin(r*3.0 - t*0.3)) ;
    scale *= 1.55;
    q *= 1.55;
    accum += exp(-r*1.6) / scale;
    structure += accum;
  }
  return accum;
}

float quasicrystal(vec2 p, float t){
  float sum = 0.0;
  const int N = 6;
  for(int i=0;i<N;i++){
    float ang = float(i) * PI / float(N) + t*0.015;
    vec2 dir = vec2(cos(ang), sin(ang));
    sum += cos(dot(p, dir)*6.0 + t*0.25 + float(i));
  }
  return sum / float(N);
}

float cellField(vec2 p, float t, out vec2 cellId){
  vec2 g = p*1.6;
  vec2 i = floor(g);
  float minD = 10.0;
  vec2 best = vec2(0.0);
  for(int y=-1;y<=1;y++){
    for(int x=-1;x<=1;x++){
      vec2 o = vec2(float(x), float(y));
      vec2 cellP = i+o;
      vec2 jitter = vec2(hash21(cellP), hash21(cellP+17.0)) - 0.5;
      jitter *= 0.9 + 0.3*sin(t*0.2 + hash21(cellP)*TAU);
      vec2 pos = cellP + 0.5 + jitter;
      float d = length(g - pos);
      if(d < minD){ minD = d; best = cellP; }
    }
  }
  cellId = best;
  return minD;
}

vec3 hsv2rgb(vec3 c){
  vec3 p = abs(fract(c.xxx + vec3(0.0,1.0/3.0,2.0/3.0))*6.0-3.0);
  vec3 rgb = clamp(p-1.0, 0.0, 1.0);
  return c.z * mix(vec3(1.0), rgb, c.y);
}

void main(){
  vec2 uv = (gl_FragCoord.xy - 0.5*uRes) / min(uRes.x,uRes.y);

  float t = uTime;

  float breathe = 0.5 + 0.5*sin(t*0.08) * 0.5 + 0.5*sin(t*0.0137 + 1.7);

  vec2 m = uMouse;
  vec2 toM = uv - m*0.6;
  float mDist = length(toM);
  float mPull = uMouseActive * 0.18 / (1.0 + mDist*mDist*6.0);
  uv -= normalize(toM+1e-5) * mPull;

  float beat = pow(0.5+0.5*sin(t*1.4), 6.0);
  float beatSlow = pow(0.5+0.5*sin(t*0.31+2.0), 3.0);
  float pulseEnergy = beat*0.6 + beatSlow*0.4;

  float clickRipple = 0.0;
  for(int i=0;i<8;i++){
    float age = t - uPulses[i];
    if(uPulses[i] > -0.5 && age > 0.0 && age < 4.0){
      vec2 pp = uPulsePos[i];
      float d = length(uv - pp);
      float wave = sin(d*22.0 - age*9.0) * exp(-age*1.0) * exp(-d*2.2);
      clickRipple += wave * (1.0 - age/4.0);
    }
  }

  float zoomPulse = 1.0 + 0.12*pulseEnergy + 0.08*clickRipple;
  vec2 p = uv * (1.6 / zoomPulse);

  float twist = t*0.05 + 0.3*sin(t*0.021);
  p = rot(twist) * p;
  float wedges = 9.0 + 3.0*sin(t*0.013);
  vec2 kp = kaleido(p, wedges, 0.15*sin(t*0.04));

  float r = length(kp)+0.0001;
  float ang = atan(kp.y,kp.x);
  float lr = log(r+0.3);
  vec2 hp = vec2(lr, ang) * vec2(1.4, 1.0);
  hp += 0.15*vec2(sin(t*0.09), cos(t*0.07));

  float structure;
  float frac = fractalField(kp*1.3 + 0.05*sin(t*0.03), t, structure);

  float q = quasicrystal(kp*2.2 + hp*0.3, t);

  vec2 cellId;
  float cell = cellField(kp*1.1 + 0.1*q, t, cellId);
  float cellEdge = smoothstep(0.0, 0.06, 0.5 - cell*0.5);

  float n = fbm(kp*2.0 + frac*1.5 + t*0.015);
  float n2 = fbm(kp*4.0 - n*2.0 - t*0.01);

  float field = frac*1.4 + q*0.25 + n*0.6 - n2*0.3 + cellEdge*0.8;
  field += clickRipple*0.8 + pulseEnergy*0.25;

  float edge = abs(fract(field*3.0)-0.5)*2.0;
  edge = 1.0 - smoothstep(0.0, 0.08, edge);

  float depth = clamp(structure*0.6 + n*0.3, 0.0, 1.4);

  float hue = fract(t*0.04 + field*0.18 + cellId.x*0.06 + cellId.y*0.04
              + 0.12*pulseEnergy + 0.2*clickRipple);
  float sat = clamp(0.65 + 0.3*sin(t*0.5+field*2.0) + 0.25*pulseEnergy, 0.0, 1.0);
  float val = clamp(0.12 + depth*0.35 + edge*0.45 + pulseEnergy*0.25 + clickRipple*0.35, 0.0, 0.85);

  vec3 col = hsv2rgb(vec3(hue, sat, val));

  vec3 spectral = hsv2rgb(vec3(fract(hue+0.5), 1.0, edge));
  col += spectral * edge * 0.20 * (0.5+0.5*pulseEnergy);

  float bloomCore = smoothstep(0.9, 1.4, val);
  col += vec3(0.8, 0.7, 0.8) * bloomCore * 0.25;

  col *= mix(0.25, 0.90, smoothstep(-0.4, 0.6, field));

  vec2 pixUv = gl_FragCoord.xy;
  float pixSize = mix(3.0, 6.0, 0.5+0.5*sin(t*0.2));
  vec2 snapped = floor(pixUv/pixSize)*pixSize;
  col = mix(col, col, 0.0);

  float vig = smoothstep(1.3, 0.2, length(uv));
  col *= vig;

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

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

function compile(type, src){
  const sh = gl.createShader(type);
  gl.shaderSource(sh, src);
  gl.compileShader(sh);
  if(!gl.getShaderParameter(sh, gl.COMPILE_STATUS)){
    console.error(gl.getShaderInfoLog(sh));
  }
  return sh;
}

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));
}
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 uMouse = gl.getUniformLocation(prog, 'uMouse');
const uMouseActive = gl.getUniformLocation(prog, 'uMouseActive');
const uPulses = gl.getUniformLocation(prog, 'uPulses');
const uPulsePos = gl.getUniformLocation(prog, 'uPulsePos');

let mouseX=0, mouseY=0, mouseActive=0.0;
let pulses = new Array(8).fill(-10.0);
let pulsePos = new Array(16).fill(0.0);
let pulseIdx = 0;
let startTime = performance.now();

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

function toNDC(clientX, clientY){
  const x = (clientX/window.innerWidth)*2-1;
  const y = -((clientY/window.innerHeight)*2-1);
  return [x,y];
}

window.addEventListener('pointermove', e=>{
  const [x,y] = toNDC(e.clientX, e.clientY);
  mouseX = x; mouseY = y;
  mouseActive = 1.0;
});
window.addEventListener('pointerdown', e=>{
  const [x,y] = toNDC(e.clientX, e.clientY);
  const now = (performance.now()-startTime)/1000.0;
  pulses[pulseIdx] = now;
  pulsePos[pulseIdx*2] = x;
  pulsePos[pulseIdx*2+1] = y;
  pulseIdx = (pulseIdx+1)%8;
  const hintEl = document.getElementById('hint');
  if (hintEl) hintEl.style.opacity = '0';
});
window.addEventListener('pointerleave', ()=>{ mouseActive = 0.0; });

function render(){
  const t = (performance.now()-startTime)/1000.0;
  gl.uniform2f(uRes, canvas.width, canvas.height);
  gl.uniform1f(uTime, t);
  gl.uniform2f(uMouse, mouseX, mouseY);
  gl.uniform1f(uMouseActive, mouseActive);
  gl.uniform1fv(uPulses, new Float32Array(pulses));
  gl.uniform2fv(uPulsePos, new Float32Array(pulsePos));
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  requestAnimationFrame(render);
}
render();

setTimeout(()=>{ const hintEl = document.getElementById('hint'); if (hintEl) hintEl.style.opacity = '0'; }, 6000);
</script>
</body>
</html>
MikeHiggins.me.uk LinkedIn GitHub Bluesky Contra Upwork
© 2026 Mike Higgins. All rights reserved.