• 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

POLYP - RETROSCOPIC CRT SHADER SOURCE CODE

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Bogie</title>
<style>
  html, body { margin:0; height:100%; background:#000; overflow:hidden; font-family: -apple-system, "Segoe UI", sans-serif; }
  canvas { 
    display:block; 
    width:100%; 
    height:100%; 
    cursor: grab; 
    filter: grayscale(1) sepia(1) hue-rotate(80deg) saturate(5) brightness(1.15) contrast(1.2);
    position: relative;
    z-index: 2;
  }
  canvas:active { cursor: grabbing; }

  #scope {
    position: fixed;
    inset: 0;
    z-index: 4;
    pointer-events: none;
    display: block;
  }
  #scope>div {
    position: absolute;
    inset: 0;
  }
  #scope .glow {
    background: radial-gradient(circle at 50% 46%, rgba(30, 255, 130, .08), rgba(0, 0, 0, 0) 62%);
    animation: nvflick 3.6s steps(24) infinite;
  }
  #scope .scan {
    background: repeating-linear-gradient(to bottom, rgba(0, 0, 0, 0) 0 2px, rgba(0, 25, 8, .45) 2px 3px);
    animation: nvscan 7s linear infinite;
  }
  #scope .vig {
    box-shadow: inset 0 0 200px 50px rgba(0, 0, 0, .8);
  }
  #scope .mask {
    background: radial-gradient(circle at 50% 50%, rgba(0, 0, 0, 0) 54%, rgba(0, 0, 0, .5) 72%, rgba(0, 0, 0, .94) 100%);
  }
  @keyframes nvscan {
    from { background-position: 0 0; }
    to { background-position: 0 240px; }
  }
  @keyframes nvflick {
    0%, 100% { opacity: .55; }
    40% { opacity: .9; }
    62% { opacity: .4; }
    80% { opacity: .75; }
  }
</style>
</head>
<body>
<canvas id="gl"></canvas>
<div id="scope">
  <div class="glow"></div>
  <div class="scan"></div>
  <div class="vig"></div>
  <div class="mask"></div>
</div>

<script>
const canvas = document.getElementById('gl');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (!gl) { document.body.innerHTML = '<p style="color:#fff;padding:40px;font-family:sans-serif">WebGL not available in this browser.</p>'; }

function resize() {
  const scale = 0.6; // internal render scale — canvas stays full size on screen, just fewer pixels to raymarch
  canvas.width = Math.round(innerWidth * scale);
  canvas.height = Math.round(innerHeight * scale);
  canvas.style.width = innerWidth + 'px';
  canvas.style.height = innerHeight + 'px';
  gl.viewport(0, 0, canvas.width, canvas.height);
}
window.addEventListener('resize', resize);
resize();

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

const fragSrc = `
precision highp float;

uniform vec2  u_res;
uniform float u_time;
uniform vec2  u_lightDir;   // mouse-driven azimuth/elevation offsets
uniform vec2  u_mousePoke;  // eased cursor position, deforms the blob surface
uniform vec2  u_camRot;     // drag-driven camera yaw/pitch
uniform vec3  u_slimeColor;
uniform vec3  u_bubbleColor;
uniform vec3  u_rimColor;
uniform float u_oozeSpeed;
uniform float u_noiseAmt;
uniform float u_fresnelPow;
uniform float u_translucency;
uniform float u_bubbleCount;

// ---------- hash / noise ----------
float hash1(float n){ return fract(sin(n)*43758.5453123); }
vec3 hash3(float n){
  return fract(sin(vec3(n,n+1.0,n+2.0))*vec3(43758.5453123,22578.1459123,19642.3490423));
}
float noise(vec3 p){
  vec3 i = floor(p); vec3 f = fract(p);
  f = f*f*(3.0-2.0*f);
  float n = i.x + i.y*157.0 + 113.0*i.z;
  return mix(
    mix(mix(hash1(n+0.0),hash1(n+1.0),f.x), mix(hash1(n+157.0),hash1(n+158.0),f.x), f.y),
    mix(mix(hash1(n+113.0),hash1(n+114.0),f.x), mix(hash1(n+270.0),hash1(n+271.0),f.x), f.y),
    f.z);
}
float fbm(vec3 p){
  float s = 0.0, a = 0.5;
  for(int i=0;i<2;i++){ s += a*noise(p); p *= 2.02; a *= 0.5; }
  return s;
}

// ---------- sdf ----------
float sdSphere(vec3 p, float r){ return length(p) - r; }
float smin(float a, float b, float k){
  float h = clamp(0.5 + 0.5*(b-a)/k, 0.0, 1.0);
  return mix(b, a, h) - k*h*(1.0-h);
}

vec3 bubblePos(float idx){
  vec3 h = hash3(idx*17.13);
  float t = u_time*u_oozeSpeed*0.6 + h.x*40.0;
  float r = 0.55 + h.y*0.55;
  float ang = h.x*6.2831 + t*0.5;
  float y = -0.7 + 1.1*fract(h.z + t*0.07);
  float yFold = abs(fract(h.z + t*0.07)*2.0 - 1.0); // bounce 0..1..0
  return vec3(cos(ang)*r*(1.0-0.3*yFold), -0.9 + yFold*1.5, sin(ang)*r*(1.0-0.3*yFold));
}

float blobSDF(vec3 p){
  vec3 q = p;
  float n = fbm(q*1.7 + vec3(0.0, u_time*u_oozeSpeed*0.8, u_time*u_oozeSpeed*0.3));
  float d = sdSphere(p, 1.25) + (n-0.5)*u_noiseAmt;

  // subtle cursor interaction: a soft dimple that follows the eased
  // mouse position across the front of the blob, like a fingertip
  // pressing into it
  vec3 pokePos = vec3(u_mousePoke.x*1.05, 0.15 + u_mousePoke.y*0.55, 0.95);
  float pokeDist = length(p - pokePos);
  float dimple = smoothstep(0.85, 0.0, pokeDist) * 0.10;
  d += dimple;

  return d;
}

float map(vec3 p){
  float blob = blobSDF(p);
  float floorPlane = p.y + 1.45;
  return smin(blob, floorPlane, 0.55);
}

vec3 calcNormal(vec3 p){
  const vec2 e = vec2(1.0,-1.0)*0.0015;
  return normalize(
    e.xyy*map(p+e.xyy)+
    e.yyx*map(p+e.yyx)+
    e.yxy*map(p+e.yxy)+
    e.xxx*map(p+e.xxx));
}

float softShadow(vec3 ro, vec3 rd, float mint, float maxt, float k){
  float res = 1.0; float t = mint;
  for(int i=0;i<12;i++){
    if(t>=maxt) break;
    float h = map(ro+rd*t);
    if(h<0.001) return 0.0;
    res = min(res, k*h/t);
    t += clamp(h, 0.05, 0.5);
  }
  return clamp(res,0.0,1.0);
}

float calcAO(vec3 p, vec3 n){
  float occ = 0.0, sca = 1.0;
  for(int i=0;i<3;i++){
    float h = 0.02 + 0.16*float(i);
    float d = map(p + n*h);
    occ += (h-d)*sca;
    sca *= 0.7;
  }
  return clamp(1.0 - 1.6*occ, 0.0, 1.0);
}

void main(){
  vec2 uv = (gl_FragCoord.xy - 0.5*u_res) / u_res.y;

  // camera, orbit-controlled
  float yaw = u_camRot.x;
  float pitch = clamp(u_camRot.y, -1.3, 1.1);
  float dist = 4.3;
  vec3 camPos = vec3(sin(yaw)*cos(pitch), sin(pitch), cos(yaw)*cos(pitch)) * dist;
  camPos.y += 0.3;
  vec3 camTarget = vec3(0.0, -0.3, 0.0);

  vec3 fwd = normalize(camTarget - camPos);
  vec3 right = normalize(cross(fwd, vec3(0.0,1.0,0.0)));
  vec3 up = cross(right, fwd);
  vec3 rd = normalize(fwd*1.6 + uv.x*right + uv.y*up);
  vec3 ro = camPos;

  // light position driven by mouse
  vec3 lightPos = vec3(u_lightDir.x*4.0, 1.4 + u_lightDir.y*2.2, 2.6);

  // raymarch
  float t = 0.0; bool hit = false; vec3 p;
  for(int i=0;i<64;i++){
    p = ro + rd*t;
    float d = map(p);
    if(d < 0.0015){ hit = true; break; }
    t += d * 0.9;
    if(t > 16.0) break;
  }

  vec3 col;
  vec3 bg1 = vec3(0.02,0.035,0.045);
  vec3 bg2 = vec3(0.06,0.10,0.10);
  col = mix(bg1, bg2, smoothstep(-1.0,1.0,uv.y)) * (1.0 - 0.35*length(uv));

  if(hit){
    vec3 n = calcNormal(p);
    vec3 V = normalize(ro - p);
    vec3 L = normalize(lightPos - p);
    vec3 H = normalize(L + V);

    float diff = max(dot(n,L), 0.0);
    float spec = pow(max(dot(n,H),0.0), 70.0);
    float fres = pow(1.0 - max(dot(n,V),0.0), u_fresnelPow);
    float ao = calcAO(p, n);
    float shadow = softShadow(p + n*0.02, L, 0.02, 6.0, 12.0);

    // fake subsurface: cheap noise-based thinness estimate instead of
    // extra map() samples
    float thin = clamp(0.5 + 0.6*(fbm(p*2.0)-0.5), 0.0, 1.0);
    float wrap = max(0.0, dot(-n, L)*0.5 + 0.5);
    float sss = wrap * (0.4 + 0.6*thin) * u_translucency;

    vec3 base = u_slimeColor;
    vec3 ambient = base * 0.16 * ao;
    vec3 diffuse = base * diff * shadow * 0.9;
    vec3 sssCol = mix(base, vec3(1.0), 0.4) * sss * 0.8;
    vec3 specCol = vec3(1.0) * spec * shadow;
    vec3 rim = u_rimColor * fres * (0.5 + 0.5*diff) ;

    // emissive internal bubbles glowing through the slime
    vec3 bubbleGlow = vec3(0.0);
    int maxB = int(u_bubbleCount);
    for(int i=0;i<8;i++){
      if(i >= maxB) break;
      vec3 bc = bubblePos(float(i));
      float d = length(p - bc);
      float pulse = 0.6 + 0.4*sin(u_time*1.7 + float(i)*2.1);
      float g = exp(-d*d*9.0) * pulse;
      bubbleGlow += u_bubbleColor * g;
    }
    bubbleGlow *= (0.5 + 0.5*thin);

    // faint glint where the cursor is pressing into the surface
    vec3 pokePos = vec3(u_mousePoke.x*1.05, 0.15 + u_mousePoke.y*0.55, 0.95);
    float glintD = length(p - pokePos);
    float glint = exp(-glintD*glintD*9.0) * 0.35;
    vec3 glintCol = vec3(1.0) * glint;

    col = ambient + diffuse + sssCol + specCol + rim + bubbleGlow + glintCol;

    // floor checker fade where blob meets ground
    if(p.y < -1.4 + 0.02){
      float chk = mod(floor(p.x*1.5)+floor(p.z*1.5), 2.0);
      col = mix(col, vec3(0.05,0.07,0.08)*(0.7+0.3*chk), 0.0);
    }

    // distance fog
    col = mix(col, bg2*0.6, smoothstep(6.0, 16.0, t));
  }

  // tonemap + gamma
  col = col / (1.0 + col);
  col = pow(col, vec3(0.4545));

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

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

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

const U = {};
['u_res','u_time','u_lightDir','u_mousePoke','u_camRot','u_slimeColor','u_bubbleColor','u_rimColor',
 'u_oozeSpeed','u_noiseAmt','u_fresnelPow','u_translucency','u_bubbleCount'].forEach(name=>{
  U[name] = gl.getUniformLocation(prog, name);
});

function hsl2rgb(h, s, l){
  h = ((h % 1) + 1) % 1;
  const k = n => (n + h*12) % 12;
  const a = s * Math.min(l, 1-l);
  const f = n => l - a*Math.max(-1, Math.min(k(n)-3, Math.min(9-k(n), 1)));
  return [f(0), f(8), f(4)];
}

// ---- state ----
let mouseLight = [0,0];
let lightSmooth = [0,0];
let pokeSmooth = [0,0];
let userMoved = false;
let camRot = [0.4, 0.15];
let dragging = false, lastX=0, lastY=0;

canvas.addEventListener('mousemove', (e)=>{
  userMoved = true;
  const nx = (e.clientX / innerWidth)*2 - 1;
  const ny = (e.clientY / innerHeight)*2 - 1;
  mouseLight = [nx, -ny];
  if(dragging){
    camRot[0] += (e.clientX - lastX) * 0.006;
    camRot[1] += (e.clientY - lastY) * 0.006;
    lastX = e.clientX; lastY = e.clientY;
  }
});
canvas.addEventListener('mousedown', (e)=>{ dragging = true; lastX = e.clientX; lastY = e.clientY; });
window.addEventListener('mouseup', ()=>{ dragging = false; });
canvas.addEventListener('touchstart', (e)=>{ dragging=true; lastX=e.touches[0].clientX; lastY=e.touches[0].clientY; }, {passive:true});
canvas.addEventListener('touchmove', (e)=>{
  const t = e.touches[0];
  const nx = (t.clientX/innerWidth)*2-1, ny=(t.clientY/innerHeight)*2-1;
  mouseLight=[nx,-ny];
  camRot[0]+=(t.clientX-lastX)*0.006; camRot[1]+=(t.clientY-lastY)*0.006;
  lastX=t.clientX; lastY=t.clientY;
}, {passive:true});
window.addEventListener('touchend', ()=>{ dragging=false; });

const params = {
  slime: [0.1, 0.7, 0.5],
  bubble: [1.0, 0.9, 0.4],
  rim: [0.5, 1.0, 0.85],
  ooze: 0.35, noiseAmt: 0.18, fres: 2.4, trans: 1.0, bub: 7
};

// Drives every parameter on its own slow, organic curve so the whole
// shader looks like it's being live-tweaked, without any UI at all.
function animateParams(time){
  params.ooze     = 0.25 + 0.30 * (0.5 + 0.5*Math.sin(time*0.17));
  params.noiseAmt = 0.10 + 0.16 * (0.5 + 0.5*Math.sin(time*0.09 + 1.3));
  params.fres     = 1.2  + 3.2  * (0.5 + 0.5*Math.sin(time*0.06 + 2.6));
  params.trans    = 0.5  + 1.1  * (0.5 + 0.5*Math.sin(time*0.11 + 0.4));
  params.bub      = Math.round(2 + 6 * (0.5 + 0.5*Math.sin(time*0.05 + 4.1)));

  // colours cruise slowly through the hue wheel, offset from each other
  // for a vivid, ever-shifting palette
  const hueBase = time * 0.025;
  params.slime  = hsl2rgb(hueBase,        0.70, 0.50 + 0.08*Math.sin(time*0.13));
  params.bubble = hsl2rgb(hueBase + 0.36, 0.85, 0.55 + 0.10*Math.sin(time*0.21 + 1.0));
  params.rim    = hsl2rgb(hueBase + 0.68, 0.75, 0.65 + 0.08*Math.sin(time*0.17 + 2.0));
}

const start = performance.now();
function frame(){
  const time = (performance.now() - start) / 1000;
  if(!dragging) camRot[0] += 0.0022;
  animateParams(time);
  if(!userMoved){
    mouseLight = [Math.sin(time*0.31)*0.8, Math.sin(time*0.21+1.4)*0.7];
  }
  lightSmooth[0] += (mouseLight[0]-lightSmooth[0])*0.08;
  lightSmooth[1] += (mouseLight[1]-lightSmooth[1])*0.08;
  pokeSmooth[0]  += (mouseLight[0]-pokeSmooth[0])*0.035;
  pokeSmooth[1]  += (mouseLight[1]-pokeSmooth[1])*0.035;

  gl.uniform2f(U.u_res, canvas.width, canvas.height);
  gl.uniform1f(U.u_time, time);
  gl.uniform2f(U.u_lightDir, lightSmooth[0], lightSmooth[1]);
  gl.uniform2f(U.u_mousePoke, pokeSmooth[0], pokeSmooth[1]);
  gl.uniform2f(U.u_camRot, camRot[0], camRot[1]);
  gl.uniform3f(U.u_slimeColor, params.slime[0], params.slime[1], params.slime[2]);
  gl.uniform3f(U.u_bubbleColor, params.bubble[0], params.bubble[1], params.bubble[2]);
  gl.uniform3f(U.u_rimColor, params.rim[0], params.rim[1], params.rim[2]);
  gl.uniform1f(U.u_oozeSpeed, params.ooze);
  gl.uniform1f(U.u_noiseAmt, params.noiseAmt);
  gl.uniform1f(U.u_fresnelPow, params.fres);
  gl.uniform1f(U.u_translucency, params.trans);
  gl.uniform1f(U.u_bubbleCount, params.bub);

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