• 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

ONEIRIC 2 - RETROSCOPIC CRT SHADER SOURCE CODE

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Oneiric Sculpture 2</title>
<style>
  html, body { margin:0; padding:0; overflow:hidden; background:#000; height:100%; width:100%; cursor: crosshair; font-family: -apple-system, "Segoe UI", sans-serif; }
  canvas { 
    display:block; 
    width:100vw; 
    height:100vh; 
    filter: grayscale(1) sepia(1) hue-rotate(80deg) saturate(5) brightness(1.15) contrast(1.2);
    position: relative;
    z-index: 2;
  }
  
  /* Retroscopic Overlay */
  #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="c"></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('c');
const gl = canvas.getContext('webgl2', { antialias:false, depth:false, stencil:false, powerPreference:'high-performance' });
if (!gl) { document.body.innerHTML = '<div style="color:#fff;font-family:sans-serif;padding:40px;">WebGL2 is required for this piece.</div>'; }

function resize(){
  const scale = 0.6; // Downscale to improve performance
  const w = Math.floor(window.innerWidth * scale);
  const h = Math.floor(window.innerHeight * scale);
  if (canvas.width !== w || canvas.height !== h) {
    canvas.width = w; canvas.height = h;
  }
}
window.addEventListener('resize', resize);
resize();

const vsSrc = `#version 300 es
precision highp float;
const vec2 verts[3] = vec2[3](vec2(-1.0,-1.0), vec2(3.0,-1.0), vec2(-1.0,3.0));
out vec2 vUv;
void main(){
  vec2 p = verts[gl_VertexID];
  vUv = p;
  gl_Position = vec4(p, 0.0, 1.0);
}`;

const fsSrc = `#version 300 es
precision highp float;
in vec2 vUv;
out vec4 fragColor;

uniform vec2 uRes;
uniform float uTime;
uniform vec2 uMouse;      // normalized -1..1, critically-damped smoothed
uniform vec2 uMouseVel;   // velocity of smoothed mouse
uniform float uBloomState; // 0..1 evolving "wakefulness" of the structure
uniform float uClickEnergy[8];
uniform vec2 uClickPos[8];
uniform float uClickTime[8];

#define PI 3.14159265359
#define TAU 6.28318530718

// ---------- hashing / noise ----------
float hash11(float p){ p = fract(p*0.1031); p*=p+33.33; p*=p+p; return fract(p); }
float hash21(vec2 p){
  vec3 p3 = fract(vec3(p.xyx) * 0.1031);
  p3 += dot(p3, p3.yzx + 33.33);
  return fract((p3.x + p3.y) * p3.z);
}
float vnoise(vec3 p){
  vec3 i = floor(p); vec3 f = fract(p);
  vec3 u = f*f*(3.0-2.0*f);
  float n00 = hash21(i.xy + i.z*57.0);
  float n10 = hash21(i.xy + vec2(1,0) + i.z*57.0);
  float n01 = hash21(i.xy + vec2(0,1) + i.z*57.0);
  float n11 = hash21(i.xy + vec2(1,1) + i.z*57.0);
  float nz0 = mix(mix(n00,n10,u.x), mix(n01,n11,u.x), u.y);
  float n00b = hash21(i.xy + (i.z+1.0)*57.0);
  float n10b = hash21(i.xy + vec2(1,0) + (i.z+1.0)*57.0);
  float n01b = hash21(i.xy + vec2(0,1) + (i.z+1.0)*57.0);
  float n11b = hash21(i.xy + vec2(1,1) + (i.z+1.0)*57.0);
  float nz1 = mix(mix(n00b,n10b,u.x), mix(n01b,n11b,u.x), u.y);
  return mix(nz0, nz1, u.z);
}
float fbm(vec3 p, int oct){
  float a = 0.5, s = 0.0;
  for(int i=0;i<5;i++){
    if(i>=oct) break;
    s += a*vnoise(p);
    p = p*2.03 + vec3(5.2,9.1,3.4);
    a *= 0.55;
  }
  return s;
}

mat2 rot2(float a){ float s=sin(a), c=cos(a); return mat2(c,-s,s,c); }
mat3 rotAxis(vec3 axis, float angle){
  axis = normalize(axis);
  float s = sin(angle), c = cos(angle), oc = 1.0-c;
  return mat3(
    oc*axis.x*axis.x+c,        oc*axis.x*axis.y-axis.z*s, oc*axis.z*axis.x+axis.y*s,
    oc*axis.x*axis.y+axis.z*s, oc*axis.y*axis.y+c,        oc*axis.y*axis.z-axis.x*s,
    oc*axis.z*axis.x-axis.y*s, oc*axis.y*axis.z+axis.x*s, oc*axis.z*axis.z+c
  );
}

// ---------- SDF primitives ----------
float sdSphere(vec3 p, float r){ return length(p)-r; }
float sdTorus(vec3 p, vec2 t){
  vec2 q = vec2(length(p.xz)-t.x, p.y);
  return length(q)-t.y;
}
float sdCapsule(vec3 p, vec3 a, vec3 b, float r){
  vec3 pa = p-a, ba = b-a;
  float h = clamp(dot(pa,ba)/dot(ba,ba),0.0,1.0);
  return length(pa - ba*h) - r;
}
// torus-knot-ish curve sampled distance (approx via parametric capsule chain done in caller)
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);
}
float smax(float a, float b, float k){
  return -smin(-a,-b,k);
}

// global click-driven displacement field ("blooming ruptures")
vec3 clickField(vec3 p, float t){
  vec3 disp = vec3(0.0);
  for(int i=0;i<8;i++){
    float e = uClickEnergy[i];
    if (e <= 0.001) continue;
    float age = t - uClickTime[i];
    vec3 cpos = vec3(uClickPos[i]*4.5, 0.0);
    float d = length(p - cpos);
    float wave = sin(d*3.2 - age*5.5) * exp(-age*0.55) * exp(-d*0.32);
    disp += normalize(p - cpos + 0.0001) * wave * e * 0.32;
  }
  return disp;
}
float clickGlowField(vec3 p, float t){
  float g = 0.0;
  for(int i=0;i<8;i++){
    float e = uClickEnergy[i];
    if (e <= 0.001) continue;
    float age = t - uClickTime[i];
    vec3 cpos = vec3(uClickPos[i]*4.5, 0.0);
    float d = length(p - cpos);
    float ring = exp(-pow((d - age*2.1)*2.0, 2.0)) * exp(-age*0.4);
    g += ring * e;
  }
  return g;
}

float matId;
float colorPhase;

// Structure: a "blooming coral knot" — a torus-knot-like braided core
// wrapped in melting, fusing spherical petals that orbit and merge.
float deStructure(vec3 p, float t){
  vec3 mInfluence = vec3(uMouse*2.4, 0.5*sin(t*0.11));
  float distToMouse = length(p - mInfluence);
  vec3 warp = (p - mInfluence) * (0.15 * exp(-distToMouse*0.65)) * (1.0 + length(uMouseVel)*5.0);
  p -= warp;
  p += clickField(p, t);

  float wake = uBloomState;

  // gentle non-euclidean radial bend, slowed for a dreamlike drift
  float r = length(p);
  float bend = sin(r*0.22 + t*0.028)*0.32*wake;
  p.xz = rot2(bend) * p.xz;

  // slow breathing dilation — the whole form gently swells and recedes,
  // like something seen in a dream rather than a fixed object
  float breathe = 1.0 + 0.045*sin(t*0.12) + 0.025*sin(t*0.07+1.7);
  p /= breathe;

  // --- braided knot core: chain of capsules following a (2,3) torus-knot-like curve ---
  float knotR = 1.7;
  float knotD = 0.0;
  const int SEG = 8;
  vec3 prevPt;
  for(int i=0;i<=SEG;i++){
    float u = float(i)/float(SEG) * TAU;
    float tw = t*0.18 + wake*0.6;
    float cx = (knotR + 0.55*cos(3.0*u + tw))*cos(2.0*u + tw*0.5);
    float cz = (knotR + 0.55*cos(3.0*u + tw))*sin(2.0*u + tw*0.5);
    float cy = 0.55*sin(3.0*u + tw) + 0.25*sin(u*5.0 - t*0.3);
    vec3 pt = vec3(cx,cy,cz);
    if (i>0){
      float segD = sdCapsule(p, prevPt, pt, 0.34 + 0.08*sin(u*4.0+t*0.4));
      knotD = (i==1) ? segD : smin(knotD, segD, 0.5);
    }
    prevPt = pt;
  }

  // --- fusing orbiting petals (spheres that blob into the knot) ---
  float petals = 1e5;
  const int NPET = 3;
  for(int k=0;k<NPET;k++){
    float fk = float(k);
    float orbAngle = t*0.22 + fk*TAU/float(NPET);
    float orbR = 2.6 + 0.5*sin(t*0.15+fk*1.3);
    vec3 center = vec3(cos(orbAngle)*orbR, sin(orbAngle*1.4+fk)*1.4, sin(orbAngle)*orbR);
    vec3 op = p - center;
    op = rotAxis(vec3(0.2,1.0,0.3), t*0.25+fk) * op;
    float petal = sdSphere(op, (0.45 + 0.22*wake) * (0.75+0.25*sin(t*0.5+fk*1.7)));
    // ribbed texture via subtle torus accent
    petal = smin(petal, sdTorus(op.xzy, vec2(0.3,0.07)), 0.18);
    petals = smin(petals, petal, 0.55);
    if (k==0) matId = 0.0;
  }

  float core = smin(knotD, petals, 0.85);

  // fine organic noise displacement for a "living coral" skin - optimized for performance
  float fine = fbm(p*4.2 + t*0.12, 1) * 0.035 * (0.5+0.5*wake); // reduced FBM octaves
  core += fine;

  matId = fract(length(p)*0.7 + t*0.02);
  colorPhase = r*0.45 + sin(t*0.1)*0.3;

  return core * 0.9;
}

float deScene(vec3 p, float t){
  return deStructure(p, t);
}

vec3 calcNormal(vec3 p, float t){
  float eps = 0.0016;
  vec2 e = vec2(1.0,-1.0)*eps;
  return normalize(
    e.xyy*deScene(p+e.xyy,t) + e.yyx*deScene(p+e.yyx,t) +
    e.yxy*deScene(p+e.yxy,t) + e.xxx*deScene(p+e.xxx,t)
  );
}

float softShadow(vec3 ro, vec3 rd, float t){
  float res = 1.0;
  float dist = 0.05;
  for(int i=0;i<6;i++){ // reduced from 8
    float h = deScene(ro + rd*dist, t);
    res = min(res, h*10.0/dist);
    dist += clamp(h, 0.03, 0.5);
    if (h < 0.002 || dist > 8.0) break;
  }
  return clamp(res, 0.0, 1.0);
}

float aoFake(float shadowVal){
  // cheap stand-in for ambient occlusion using the already-computed shadow term
  return mix(0.7, 1.0, shadowVal);
}

// warm ember-to-aurora palette — amber core, magenta-teal accents
vec3 emberPalette(float phase, float wake, float fres){
  vec3 a = vec3(0.55,0.42,0.40);
  vec3 b = vec3(0.45,0.35,0.30);
  vec3 c = vec3(1.1,0.85,0.55);
  vec3 d = vec3(0.05,0.18,0.35) + wake*vec3(0.25,0.05,-0.05);
  vec3 col = a + b*cos(TAU*(c*phase*0.16 + d));
  // push toward warm amber/ember at low fresnel, teal/magenta rim at grazing angles
  col = mix(col, vec3(1.0,0.55,0.85), fres*0.5);
  return col;
}

void main(){
  vec2 uv = vUv;
  float aspect = uRes.x/uRes.y;
  vec2 p = uv;
  p.x *= aspect;

  float t = uTime;

  float camAngle = t*0.065 + uMouse.x*0.45;
  float camHeight = sin(t*0.045)*0.55 + uMouse.y*0.7;
  float camRadius = 6.2 + sin(t*0.03)*0.7;

  vec3 ro = vec3(cos(camAngle)*camRadius, camHeight + 0.8, sin(camAngle)*camRadius);
  vec3 target = vec3(sin(t*0.02)*0.5, 0.15*sin(t*0.05), cos(t*0.018)*0.35);

  vec3 fwd = normalize(target - ro);
  vec3 right = normalize(cross(fwd, vec3(0.0,1.0,0.0)));
  vec3 up = cross(right, fwd);

  float fov = 3.66; // 3x zoom as requested
  vec3 rd = normalize(fwd*fov + right*p.x + up*p.y);

  float dist = 0.0;
  float totalDist = 0.0;
  bool hit = false;
  vec3 pos = ro;
  float glowAccum = 0.0;

  for(int i=0;i<50;i++){ // reduced from 60
    pos = ro + rd*totalDist;
    dist = deScene(pos, t);
    glowAccum += exp(-abs(dist)*5.0) * 0.02;
    if (dist < 0.002){ hit = true; break; }
    if (totalDist > 40.0) break;
    totalDist += dist * 0.9;
  }

  vec3 col = vec3(0.0);

  // background: deep warm-dark void, ember haze low, cool dark high
  vec3 bgA = vec3(0.045,0.018,0.03);
  vec3 bgB = vec3(0.01,0.012,0.04);
  float bgGrad = 0.5 + 0.5*rd.y;
  vec3 bg = mix(bgA, bgB, bgGrad);
  // slow drifting haze layers in place of stars — soft, dreamlike, never sharp
  float haze1 = fbm(rd*2.2 + vec3(0.0,0.0,t*0.025), 1); // reduced FBM octaves
  float haze2 = fbm(rd*4.5 - vec3(t*0.018,0.0,0.0), 1);
  bg += haze1*0.10*vec3(0.9,0.55,0.65);
  bg += haze2*0.07*vec3(0.4,0.55,0.9);
  bg += glowAccum * vec3(0.55,0.25,0.30);

  if (hit){
    vec3 n = calcNormal(pos, t);
    vec3 viewDir = -rd;
    float fres = pow(1.0 - clamp(dot(n,viewDir),0.0,1.0), 3.0);

    vec3 lightP1 = vec3(sin(t*0.19)*3.0, cos(t*0.16)*2.0+1.0, cos(t*0.12)*3.0);
    vec3 lightP2 = vec3(uMouse*3.0, 1.4);
    vec3 l1 = normalize(lightP1 - pos);
    vec3 l2 = normalize(lightP2 - pos);

    float diff1 = clamp(dot(n,l1),0.0,1.0);
    float diff2 = clamp(dot(n,l2),0.0,1.0);

    float sh1 = softShadow(pos+n*0.02, l1, t);
    float occ = aoFake(sh1);

    vec3 baseCol = emberPalette(colorPhase, uBloomState, fres);

    // cheap stand-in for subsurface translucency (avoids an extra full SDF evaluation)
    vec3 sss = baseCol * fres * 0.5;

    vec3 lit = baseCol * (0.22 + diff1*0.75*sh1) * vec3(1.05,0.95,0.85);
    lit += baseCol * diff2 * 0.4 * vec3(0.7,0.85,1.05);
    lit += sss;
    lit += fres * vec3(1.0,0.7,0.9) * (0.6 + 0.45*uBloomState);
    lit *= mix(occ, 1.0, 0.35); // soften ambient occlusion so shadows feel hazy, not carved

    float vein = fbm(pos*3.3 + t*0.13, 1); // reduced FBM octaves
    float veinMask = smoothstep(0.55, 0.62, vein) * (0.5+0.5*uBloomState);
    lit += veinMask * emberPalette(colorPhase+1.6, uBloomState, 0.0) * 1.3;

    float cg = clickGlowField(pos, t);
    lit += cg * vec3(1.1,0.6,0.95) * 1.5;

    col = lit;

    float fog = 1.0 - exp(-totalDist*0.065);
    col = mix(col, bg*1.3, fog*0.65);
  } else {
    col = bg;
  }

  col = col / (1.0 + col*0.4);
  vec2 vc = uv;
  float vig = 1.0 - 0.25*dot(vc,vc);
  col *= vig;

  col = pow(col, vec3(0.85, 0.88, 0.92));
  col += vec3(0.03,0.012,0.018) * (1.0-vig);

  float grain = (hash21(uv*uRes.xy*0.5 + t*60.0) - 0.5) * 0.025;
  col += grain;

  fragColor = vec4(clamp(col,0.0,1.5), 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, vsSrc));
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, fsSrc));
gl.linkProgram(prog);
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)){
  console.error(gl.getProgramInfoLog(prog));
}
gl.useProgram(prog);

const vao = gl.createVertexArray();
gl.bindVertexArray(vao);

const uRes = gl.getUniformLocation(prog, 'uRes');
const uTime = gl.getUniformLocation(prog, 'uTime');
const uMouse = gl.getUniformLocation(prog, 'uMouse');
const uMouseVel = gl.getUniformLocation(prog, 'uMouseVel');
const uBloomState = gl.getUniformLocation(prog, 'uBloomState');
const uClickEnergy = gl.getUniformLocation(prog, 'uClickEnergy[0]');
const uClickPos = gl.getUniformLocation(prog, 'uClickPos[0]');
const uClickTime = gl.getUniformLocation(prog, 'uClickTime[0]');

// ---------- interaction state ----------
let mouseTarget = [0,0];

// Critically-damped spring smoothing for the mouse — much smoother than
// exponential lerp: no overshoot, no jitter, consistent feel across framerates.
let springPos = [0,0];
let springVel = [0,0];
const SPRING_STIFFNESS = 38.0; // higher = snappier but still smooth
const SPRING_DAMP = 2.0 * Math.sqrt(SPRING_STIFFNESS); // critical damping

let mouseVelSmooth = [0,0];

const MAX_CLICKS = 8;
let clickEnergy = new Float32Array(MAX_CLICKS);
let clickPos = new Float32Array(MAX_CLICKS*2);
let clickTime = new Float32Array(MAX_CLICKS);
let clickSlot = 0;

let wakefulness = 0.05;
let interactionScore = 0;

window.addEventListener('mousemove', (e)=>{
  const x = (e.clientX / window.innerWidth)*2 - 1;
  const y = -((e.clientY / window.innerHeight)*2 - 1);
  mouseTarget = [x,y];
  interactionScore += 0.0008;
}, {passive:true});

window.addEventListener('touchmove', (e)=>{
  if (e.touches.length>0){
    const t0 = e.touches[0];
    const x = (t0.clientX / window.innerWidth)*2 - 1;
    const y = -((t0.clientY / window.innerHeight)*2 - 1);
    mouseTarget = [x,y];
    interactionScore += 0.0008;
  }
}, {passive:true});

function spawnClick(nx, ny){
  clickEnergy[clickSlot] = 1.0;
  clickPos[clickSlot*2] = nx;
  clickPos[clickSlot*2+1] = ny;
  clickTime[clickSlot] = performance.now()/1000.0;
  clickSlot = (clickSlot+1) % MAX_CLICKS;
  interactionScore += 0.06;
}

window.addEventListener('mousedown', (e)=>{
  const x = (e.clientX / window.innerWidth)*2 - 1;
  const y = -((e.clientY / window.innerHeight)*2 - 1);
  spawnClick(x,y);
});
window.addEventListener('touchstart', (e)=>{
  if (e.touches.length>0){
    const t0 = e.touches[0];
    const x = (t0.clientX / window.innerWidth)*2 - 1;
    const y = -((t0.clientY / window.innerHeight)*2 - 1);
    spawnClick(x,y);
  }
}, {passive:true});

const startTime = performance.now();
let lastFrame = startTime;

function frame(){
  resize();
  const now = performance.now();
  const dt = Math.min((now - lastFrame)/1000.0, 0.05);
  lastFrame = now;
  const t = (now - startTime)/1000.0;

  // --- critically damped spring toward mouseTarget (semi-implicit Euler) ---
  // This gives buttery, overshoot-free smoothing that feels consistent
  // regardless of how fast the cursor moves.
  for (let axis=0; axis<2; axis++){
    const x = springPos[axis];
    const v = springVel[axis];
    const targetX = mouseTarget[axis];
    const accel = SPRING_STIFFNESS*(targetX - x) - SPRING_DAMP*v;
    springVel[axis] = v + accel*dt;
    springPos[axis] = x + springVel[axis]*dt;
  }
  mouseVelSmooth[0] += (springVel[0]*0.02 - mouseVelSmooth[0])*0.25;
  mouseVelSmooth[1] += (springVel[1]*0.02 - mouseVelSmooth[1])*0.25;

  for (let i=0;i<MAX_CLICKS;i++){
    if (clickEnergy[i] > 0.0){
      clickEnergy[i] -= dt*0.18;
      if (clickEnergy[i] < 0.0) clickEnergy[i] = 0.0;
    }
  }

  interactionScore *= 0.999;
  const target = Math.min(0.15 + interactionScore*1.4, 1.0);
  wakefulness += (target - wakefulness) * Math.min(dt*0.5, 1.0);

  gl.viewport(0,0,canvas.width, canvas.height);
  gl.uniform2f(uRes, canvas.width, canvas.height);
  gl.uniform1f(uTime, t);
  gl.uniform2f(uMouse, springPos[0], springPos[1]);
  gl.uniform2f(uMouseVel, mouseVelSmooth[0], mouseVelSmooth[1]);
  gl.uniform1f(uBloomState, wakefulness);
  gl.uniform1fv(uClickEnergy, clickEnergy);
  gl.uniform2fv(uClickPos, clickPos);
  gl.uniform1fv(uClickTime, clickTime);

  gl.drawArrays(gl.TRIANGLES, 0, 3);

  requestAnimationFrame(frame);
}

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