• 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 1 - RETROSCOPIC CRT SHADER SOURCE CODE

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Oneiric Sculpture 1</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;
  }
  #hint {
    position:fixed; left:50%; bottom:28px; transform:translateX(-50%);
    color:rgba(220,230,255,0.55); font-family: Georgia, 'Times New Roman', serif;
    font-size:13px; letter-spacing:0.12em; text-transform:uppercase;
    pointer-events:none; transition:opacity 2.5s ease; opacity:1; text-align:center;
    text-shadow: 0 0 12px rgba(120,160,255,0.6);
    z-index: 5;
  }
  #hint.fade { opacity:0; }

  /* 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>
<div id="hint">Click to interact</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, smoothed
uniform vec2 uMouseVel;   // velocity of mouse, smoothed
uniform float uBloomState; // 0..1 evolving "wakefulness" of the lattice
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);
}
vec3 hash33(vec3 p){
  p = vec3(dot(p,vec3(127.1,311.7,74.7)),
           dot(p,vec3(269.5,183.3,246.1)),
           dot(p,vec3(113.5,271.9,124.6)));
  return fract(sin(p)*43758.5453123);
}
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.02 + vec3(7.1,3.3,5.7);
    a *= 0.55;
  }
  return s;
}

// ---------- rotation ----------
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 sdBox(vec3 p, vec3 b){
  vec3 q = abs(p)-b;
  return length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0);
}
float sdTorus(vec3 p, vec2 t){
  vec2 q = vec2(length(p.xz)-t.x, p.y);
  return length(q)-t.y;
}
float sdOctahedron(vec3 p, float s){
  p = abs(p);
  float m = p.x+p.y+p.z-s;
  vec3 q;
  if (3.0*p.x < m) q = p.xyz;
  else if (3.0*p.y < m) q = p.yzx;
  else if (3.0*p.z < m) q = p.zxy;
  else return m*0.57735027;
  float k = clamp(0.5*(q.z-q.y+s),0.0,s);
  return length(vec3(q.x,q.y-s+k,q.z-k));
}
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);
}

// fold helpers for self-similarity
void foldAbs(inout vec3 p, vec3 n){ p -= 2.0*min(dot(p,n),0.0)*n; }

// global click-driven displacement field (warps space, "growth", "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.0 - age*5.0) * exp(-age*0.6) * exp(-d*0.35);
    disp += normalize(p - cpos + 0.0001) * wave * e * 0.35;
  }
  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.2)*2.0, 2.0)) * exp(-age*0.4);
    g += ring * e;
  }
  return g;
}

// the evolving lattice: a recursive, folded, mouse-perturbed SDF
// returns distance and writes a material id channel via global
float matId;
float latticeColorPhase;

float deLattice(vec3 p, float t){
  // mouse perturbs the underlying field - "spatial distortion"
  vec3 mInfluence = vec3(uMouse*2.6, 0.6*sin(t*0.13));
  float distToMouse = length(p - mInfluence);
  vec3 warp = (p - mInfluence) * (0.16 * exp(-distToMouse*0.7)) * (1.0 + length(uMouseVel)*6.0);
  p -= warp;
  p += clickField(p, t);

  // slow global breathing / morphogenesis driven by uBloomState
  float wake = uBloomState;

  // domain repetition with non-euclidean-ish scale drift across radius
  float r = length(p);
  float ang = atan(p.z, p.x);
  float bend = sin(r*0.35 + t*0.05)*0.35*wake;
  p.xz = rot2(bend) * p.xz;

  vec3 q = p;
  float scale = 1.0;
  float d = 1e5;
  float idAccum = 0.0;

  // recursive folded kaleidoscopic chamber structure
  for(int i=0;i<4;i++){
    float fi = float(i);
    q = abs(q) - (0.85 + 0.15*sin(t*0.07+fi));
    q.xy *= rot2(0.35 + 0.12*sin(t*0.11+fi*1.7) + 0.25*wake);
    q.yz *= rot2(0.21*cos(t*0.05+fi*2.3));
    float s = 1.55 + 0.25*sin(fi*1.3 + t*0.04);
    q *= s; scale *= s;

    float local = sdOctahedron(q, 0.92 + 0.18*sin(t*0.2+fi));
    local = smin(local, sdTorus(q.xzy, vec2(0.65,0.18+0.06*sin(t*0.3+fi))), 0.45);
    local /= scale;
    if (local < d){ d = local; idAccum = fi; }
  }

  // a slower "geological/architectural" base mass binding everything
  vec3 bp = p;
  float base = sdBox(bp, vec3(2.6,1.3,2.6));
  base = smax(base, -sdBox(bp, vec3(2.2,2.6,1.0)), 0.5);
  base = smax(base, -sdBox(bp, vec3(1.0,2.6,2.2)), 0.5);
  float noiseDetail = fbm(p*1.7 + t*0.02, 2) * 0.18 * (0.4+0.6*wake);
  base += noiseDetail;

  float result = smin(d, base, 1.1);

  // Microscopic ornamentation removed for performance in retro filter
  // float fine = fbm(p*9.0 + t*0.1, 2) * 0.022;
  // result += fine;

  // floating satellite fragments — biological/alien growths that bloom with wakefulness
  for(int k=0;k<2;k++){
    float fk = float(k);
    vec3 orbitP = p;
    float orbAngle = t*0.18 + fk*TAU/3.0;
    vec3 center = vec3(cos(orbAngle)*3.2, sin(orbAngle*1.3)*1.6 + sin(t*0.1+fk)*0.6, sin(orbAngle)*3.2);
    vec3 op = orbitP - center;
    op = rotAxis(vec3(0.3,1.0,0.2), t*0.3+fk) * op;
    float satellite = sdOctahedron(op, (0.25 + 0.18*wake) * (0.7+0.3*sin(t*0.6+fk*2.0)));
    satellite = smin(satellite, sdTorus(op.xzy, vec2(0.18,0.05)), 0.12);
    result = smin(result, satellite, 0.35);
  }

  matId = idAccum;
  latticeColorPhase = r*0.4 + idAccum*1.3;

  return result * 0.92;
}

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

vec3 calcNormal(vec3 p, float t){
  float eps = 0.0015;
  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)
  );
}

// soft shadow via raymarch
float softShadow(vec3 ro, vec3 rd, float t){
  float res = 1.0;
  float dist = 0.05;
  for(int i=0;i<8;i++){
    float h = deScene(ro + rd*dist, t);
    res = min(res, h*10.0/dist);
    dist += clamp(h, 0.03, 0.6);
    if (h < 0.002 || dist > 6.0) break;
  }
  return clamp(res, 0.0, 1.0);
}

float ao(vec3 p, vec3 n, float t){
  float occ = 0.0; float sca = 1.0;
  for(int i=0;i<2;i++){
    float h = 0.04 + 0.15*float(i);
    float d = deScene(p + n*h, t);
    occ += (h-d)*sca;
    sca *= 0.62;
  }
  return clamp(1.0 - 2.0*occ, 0.0, 1.0);
}

// dreamy iridescent palette — color depends on phase + normal + wakefulness
vec3 dreamPalette(float phase, float wake, float fres){
  vec3 a = vec3(0.55,0.5,0.7);
  vec3 b = vec3(0.45,0.4,0.45);
  vec3 c = vec3(1.0,0.9,1.3);
  vec3 d = vec3(0.30,0.55,0.85) + wake*vec3(0.15,0.05,-0.1);
  vec3 col = a + b*cos(TAU*(c*phase*0.15 + d));
  col = mix(col, vec3(1.0,0.85,1.0), fres*0.4);
  return col;
}

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

  float t = uTime;

  // cinematic drifting orbital camera, gently influenced by mouse
  float camAngle = t*0.07 + uMouse.x*0.5;
  float camHeight = sin(t*0.05)*0.6 + uMouse.y*0.8;
  float camRadius = 6.6 + sin(t*0.035)*0.8;

  vec3 ro = vec3(cos(camAngle)*camRadius, camHeight + 1.0, sin(camAngle)*camRadius);
  vec3 target = vec3(sin(t*0.025)*0.6, 0.2*sin(t*0.06), cos(t*0.02)*0.4);

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

  float fov = 1.25;
  vec3 rd = normalize(fwd*fov + right*p.x + up*p.y);

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

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

  vec3 col = vec3(0.0);

  // background: deep dreamy void with layered fog and faint star-noise
  vec3 bgA = vec3(0.01,0.012,0.035);
  vec3 bgB = vec3(0.06,0.04,0.10);
  float bgGrad = 0.5 + 0.5*rd.y;
  vec3 bg = mix(bgA, bgB, bgGrad);
  float stars = pow(hash21(floor((rd.xy+rd.z*0.3)*400.0)), 28.0);
  bg += vec3(stars)*0.6*vec3(0.7,0.8,1.0);
  bg += glowAccum * vec3(0.35,0.25,0.55);

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

    // multiple soft emissive light sources roaming through the structure
    vec3 lightP1 = vec3(sin(t*0.21)*3.0, cos(t*0.17)*2.0+1.0, cos(t*0.13)*3.0);
    vec3 lightP2 = vec3(uMouse*3.0, 1.5);
    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 = ao(pos, n, t);

    vec3 baseCol = dreamPalette(latticeColorPhase, uBloomState, fres);

    // subsurface-like translucency approximation
    float thickness = clamp(deScene(pos - n*0.3, t), 0.0, 1.0);
    vec3 sss = baseCol * (1.0 - thickness) * 0.6;

    vec3 lit = baseCol * (0.15 + diff1*0.9*sh1) * vec3(0.9,0.95,1.1);
    lit += baseCol * diff2 * 0.5 * vec3(1.0,0.7,0.9);
    lit += sss;
    lit += fres * vec3(1.0,0.95,1.1) * (0.6 + 0.4*uBloomState);
    lit *= occ;

    // internal glow / emissive veins via fbm threshold (simplified)
    float vein = fbm(pos*3.5 + t*0.15, 2);
    float veinMask = smoothstep(0.55, 0.62, vein) * (0.5+0.5*uBloomState);
    lit += veinMask * dreamPalette(latticeColorPhase+2.0, uBloomState, 0.0) * 1.4;

    // click rupture glow
    float cg = clickGlowField(pos, t);
    lit += cg * vec3(1.0,0.85,1.2) * 1.6;

    col = lit;

    // atmospheric depth fog blending toward background
    float fog = 1.0 - exp(-totalDist*0.05);
    col = mix(col, bg*1.3, fog*0.55);
  } else {
    col = bg;
  }

  // soft bloom approximation: boost highlights, gentle vignette
  col = col / (1.0 + col*0.4);
  vec2 vc = uv;
  float vig = 1.0 - 0.25*dot(vc,vc);
  col *= vig;

  // gentle chromatic dreaminess + tone mapping
  col = pow(col, vec3(0.85, 0.88, 0.92));
  col += vec3(0.02,0.015,0.03) * (1.0-vig);

  // film-like grain to keep it feeling alive/organic
  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];
let mouseSmooth = [0,0];
let mousePrevSmooth = [0,0];
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;

// persistent evolving "wakefulness" — grows with interaction, never fully resets
let wakefulness = 0.05;
let interactionScore = 0; // accumulates over the session

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;
  hideHint();
}, {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;
  hideHint();
}

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 hintEl = document.getElementById('hint');
let hintHidden = false;
function hideHint(){
  if (!hintHidden && hintEl){
    hintHidden = true;
    setTimeout(()=>{ hintEl.classList.add('fade'); }, 1800);
  }
}
setTimeout(hideHint, 9000);

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;

  // smooth mouse
  mouseSmooth[0] += (mouseTarget[0]-mouseSmooth[0]) * Math.min(dt*4.0,1.0);
  mouseSmooth[1] += (mouseTarget[1]-mouseSmooth[1]) * Math.min(dt*4.0,1.0);
  const vx = (mouseSmooth[0]-mousePrevSmooth[0])/Math.max(dt,0.0001);
  const vy = (mouseSmooth[1]-mousePrevSmooth[1])/Math.max(dt,0.0001);
  mouseVelSmooth[0] += (vx*0.02 - mouseVelSmooth[0])*0.2;
  mouseVelSmooth[1] += (vy*0.02 - mouseVelSmooth[1])*0.2;
  mousePrevSmooth[0]=mouseSmooth[0]; mousePrevSmooth[1]=mouseSmooth[1];

  // decay click energies
  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;
    }
  }

  // wakefulness evolves slowly toward a target derived from accumulated interaction,
  // and slowly decays so the piece can rest if left alone — but never fully resets,
  // giving the sense of a world remembering the viewer across the session.
  interactionScore *= 0.999; // slow forgetting
  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, mouseSmooth[0], mouseSmooth[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.