• 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

GLSL GRID - RETROSCOPIC CRT SHADER SOURCE CODE

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>GLSL Grid</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        html, body { width: 100%; height: 100%; background: #000; overflow: hidden; }
        
        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>
    <script type="importmap">
    { "imports": { "three": "https://cdn.jsdelivr.net/npm/three@0.165.0/build/three.module.js" } }
    </script>
</head>
<body>
<div id="scope">
    <div class="glow"></div>
    <div class="scan"></div>
    <div class="vig"></div>
    <div class="mask"></div>
</div>
<script type="module">
import * as THREE from 'three';

const U = v => ({ value: v });

const renderer = new THREE.WebGLRenderer({ antialias: false });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

const scene  = new THREE.Scene();
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);

const mat = new THREE.ShaderMaterial({
    uniforms: {
        time:       U(0.0),
        res:        U(new THREE.Vector2(window.innerWidth, window.innerHeight)),
        fov:        U(1.2),
        maxSteps:   U(80),
        maxDist:    U(30.0),
        hitEps:     U(0.002),
        cellSize:   U(2.0),
        wallThick:  U(0.07),
        sphereR:    U(0.28),
        pillarR:    U(0.10),
        rotX:       U(0.0),
        rotY:       U(0.0),
        camZ:       U(0.0),
        hue1:       U(0.60),
        hue2:       U(0.55),
        edgePow:    U(4.0),
    },

    vertexShader: `void main(){ gl_Position=vec4(position.xy,0.,1.); }`,

    fragmentShader: `
        precision highp float;

        uniform float time;
        uniform vec2  res;
        uniform float fov;
        uniform int   maxSteps;
        uniform float maxDist;
        uniform float hitEps;
        uniform float cellSize;
        uniform float wallThick;
        uniform float sphereR;
        uniform float pillarR;
        uniform float rotX;
        uniform float rotY;
        uniform float camZ;
        uniform float hue1;
        uniform float hue2;
        uniform float edgePow;

        mat3 Rx(float a){ float c=cos(a),s=sin(a); return mat3(1,0,0, 0,c,-s, 0,s,c); }
        mat3 Ry(float a){ float c=cos(a),s=sin(a); return mat3(c,0,s, 0,1,0,-s,0,c); }

        float smin(float a,float b,float k){
            float h=max(k-abs(a-b),0.)/k;
            return min(a,b)-h*h*k*.25;
        }

        float sdSphere(vec3 p, float r){ return length(p)-r; }

        float sdCapsule(vec3 p, vec3 a, vec3 b, float r){
            vec3 ab=b-a, ap=p-a;
            float t=clamp(dot(ap,ab)/dot(ab,ab),0.,1.);
            return length(ap-t*ab)-r;
        }

        float sdLattice(vec3 p){
            float C  = cellSize;
            float W  = wallThick * C * 0.5;
            float SR = sphereR   * C * 0.5;
            float PR = pillarR   * C * 0.5;

            vec3 q = mod(p, C) - C*0.5;

            float wx = abs(q.x) - W;
            float wy = abs(q.y) - W;
            float wz = abs(q.z) - W;

            float pillarX = length(q.yz) - PR;
            float pillarY = length(q.xz) - PR;
            float pillarZ = length(q.xy) - PR;

            vec3 corner = sign(q) * C * 0.5;
            float sphere = length(q - corner) - SR;

            float d = min(sphere, min(min(pillarX, pillarY), pillarZ));

            float faceXY = max(wz, max(-abs(q.x)+C*0.5, -abs(q.y)+C*0.5));
            float faceYZ = max(wx, max(-abs(q.y)+C*0.5, -abs(q.z)+C*0.5));
            float faceXZ = max(wy, max(-abs(q.x)+C*0.5, -abs(q.z)+C*0.5));

            d = min(d, min(min(faceXY, faceYZ), faceXZ));

            return d;
        }

        vec3 normal(vec3 p){
            float e = 0.001;
            return normalize(vec3(
                sdLattice(p+vec3(e,0,0)) - sdLattice(p-vec3(e,0,0)),
                sdLattice(p+vec3(0,e,0)) - sdLattice(p-vec3(0,e,0)),
                sdLattice(p+vec3(0,0,e)) - sdLattice(p-vec3(0,0,e))
            ));
        }

        vec3 hsv(float h,float s,float v){
            vec4 K=vec4(1.,2./3.,1./3.,3.);
            vec3 p=abs(fract(vec3(h)+K.xyz)*6.-K.www);
            return v*mix(K.xxx,clamp(p-K.xxx,0.,1.),s);
        }

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

            vec3 ro = vec3(0., 0., -8.0 + camZ);
            vec3 rd = normalize(vec3(uv * fov, 1.0));

            mat3 R = Ry(rotY) * Rx(rotX);
            ro = R * ro;
            rd = R * rd;

            float t   = 0.0;
            float dist = 0.0;
            bool  hit  = false;
            for(int i=0; i<256; i++){
                if(i >= maxSteps) break;
                dist = sdLattice(ro + rd*t);
                if(dist < hitEps){ hit=true; break; }
                if(t > maxDist) break;
                t += dist * 0.7;
            }

            if(!hit){
                gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
                return;
            }

            vec3 pos  = ro + rd*t;
            vec3 nor  = normal(pos);

            vec3 lDir  = normalize(vec3(sin(time*0.3), 0.6, cos(time*0.3)));
            float diff = max(dot(nor, lDir), 0.0);

            float rim  = pow(1.0 - abs(dot(nor, -rd)), edgePow);

            float fres = pow(1.0 - abs(dot(nor, -rd)), 2.0);

            vec3 faceCol = hsv(hue1, 0.9, 0.15 + diff * 0.55);

            vec3 rimCol  = hsv(hue2, 0.6, 1.0);

            vec3 refl    = reflect(rd, nor);
            float spec   = pow(max(dot(refl, lDir), 0.0), 32.0);
            vec3 specCol = vec3(0.8, 0.95, 1.0) * spec * 0.8;

            vec3 col = faceCol + rimCol * rim * 0.9 + specCol;

            float fog = exp(-max(t - 4.0, 0.0) * 0.18);
            col *= fog;

            col = pow(max(col, vec3(0.0)), vec3(0.45));

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

scene.add(new THREE.Mesh(new THREE.PlaneGeometry(2, 2), mat));

let dragX = 0.0,  dragY = 0.0;
let velX  = 0.0,  velY  = 0.0;
let autoX = 0.0,  autoY = 0.0;
let isDragging  = false;
let lastMX = 0, lastMY = 0;
const DRAG_SENS  = 0.005;
const INERTIA    = 0.92;
const AUTO_SPEED = { x: 0.13, y: 0.09 };
let autoFrozen   = false;
let idleTimer    = 0;

let zoomDist  = 8.0;
let zoomVel   = 0.0;
const ZOOM_MIN        = 2.5;
const ZOOM_MAX        = 20.0;
const ZOOM_SENS_WHEEL = 0.0012;
const ZOOM_SENS_PINCH = 0.018;
const ZOOM_INERTIA    = 0.88;

const canvas = renderer.domElement;
canvas.style.cursor = 'grab';

canvas.addEventListener('mousedown', e => {
    isDragging = true;
    autoFrozen = true;
    idleTimer  = 0;
    velX = velY = 0;
    lastMX = e.clientX;
    lastMY = e.clientY;
    canvas.style.cursor = 'grabbing';
});
window.addEventListener('mousemove', e => {
    if (!isDragging) return;
    const dx = e.clientX - lastMX;
    const dy = e.clientY - lastMY;
    velY = dx * DRAG_SENS;
    velX = dy * DRAG_SENS;
    dragY += velY;
    dragX += velX;
    lastMX = e.clientX;
    lastMY = e.clientY;
});
window.addEventListener('mouseup', () => {
    isDragging = false;
    canvas.style.cursor = 'grab';
});

let lastTX = 0, lastTY = 0;
canvas.addEventListener('touchstart', e => {
    e.preventDefault();
    isDragging = true;
    autoFrozen = true;
    idleTimer  = 0;
    velX = velY = 0;
    lastTX = e.touches[0].clientX;
    lastTY = e.touches[0].clientY;
}, { passive: false });
canvas.addEventListener('touchmove', e => {
    e.preventDefault();
    if (!isDragging) return;
    const dx = e.touches[0].clientX - lastTX;
    const dy = e.touches[0].clientY - lastTY;
    velY = dx * DRAG_SENS;
    velX = dy * DRAG_SENS;
    dragY += velY;
    dragX += velX;
    lastTX = e.touches[0].clientX;
    lastTY = e.touches[0].clientY;
}, { passive: false });
canvas.addEventListener('touchend', e => {
    if (e.touches.length < 2) lastPinchDist = null;
    if (e.touches.length === 0) isDragging = false;
});

canvas.addEventListener('wheel', e => {
    e.preventDefault();
    zoomVel += e.deltaY * ZOOM_SENS_WHEEL * zoomDist * 0.3;
}, { passive: false });

let lastPinchDist = null;
canvas.addEventListener('touchmove', e => {
    if (e.touches.length !== 2) return;
    const dx = e.touches[0].clientX - e.touches[1].clientX;
    const dy = e.touches[0].clientY - e.touches[1].clientY;
    const d  = Math.sqrt(dx*dx + dy*dy);
    if (lastPinchDist !== null) {
        zoomVel += (lastPinchDist - d) * ZOOM_SENS_PINCH;
    }
    lastPinchDist = d;
}, { passive: false });

const clock = new THREE.Clock();
let prevElapsed = 0;

(function animate(){
    requestAnimationFrame(animate);
    const elapsed = clock.getElapsedTime();
    const dt = elapsed - prevElapsed;
    prevElapsed = elapsed;

    if (!isDragging) {
        dragX += velX;
        dragY += velY;
        velX  *= INERTIA;
        velY  *= INERTIA;

        const speed = Math.abs(velX) + Math.abs(velY);
        if (speed < 0.0001) {
            idleTimer++;
            if (idleTimer > 30) autoFrozen = false;
        } else {
            idleTimer = 0;
        }
    }

    if (!autoFrozen) {
        autoX += dt * AUTO_SPEED.x;
        autoY += dt * AUTO_SPEED.y;
    }

    const rotX = dragX + (autoFrozen ? 0 : Math.sin(autoX) * 0.55);
    const rotY = dragY + (autoFrozen ? 0 : autoY);

    mat.uniforms.time.value   = elapsed;
    mat.uniforms.rotX.value   = rotX;
    mat.uniforms.rotY.value   = rotY;
    zoomDist = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, zoomDist + zoomVel));
    zoomVel *= ZOOM_INERTIA;
    mat.uniforms.camZ.value = -(zoomDist - 8.0) + Math.sin(elapsed * 0.07) * 0.6;

    mat.uniforms.wallThick.value = 0.04 + 0.03 * Math.sin(elapsed * 0.19);
    mat.uniforms.pillarR.value   = 0.08 + 0.04 * Math.sin(elapsed * 0.27 + 1.0);
    mat.uniforms.sphereR.value   = 0.22 + 0.08 * Math.sin(elapsed * 0.17 + 2.0);
    mat.uniforms.hue1.value      = 0.55 + 0.12 * Math.sin(elapsed * 0.05);
    mat.uniforms.hue2.value      = 0.50 + 0.15 * Math.cos(elapsed * 0.04);
    mat.uniforms.edgePow.value   = 3.0  + 2.0  * Math.sin(elapsed * 0.11);

    renderer.render(scene, camera);
})();

window.addEventListener('resize', () => {
    renderer.setSize(window.innerWidth, window.innerHeight);
    mat.uniforms.res.value.set(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>
MikeHiggins.me.uk LinkedIn GitHub Bluesky Contra Upwork
© 2026 Mike Higgins. All rights reserved.