• 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

NODE - 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, user-scalable=no">
    <title>3D Net — Live Reconnecting Network</title>
    <style>
        html, body {
            margin: 0;
            overflow: hidden;
            width: 100%;
            height: 100%;
            background: #000;
            font-family: 'Segoe UI', 'Inter', system-ui, -apple-system, sans-serif;
        }
        canvas { 
            display: block; outline: none; touch-action: none;
            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>
    <div id="scope">
        <div class="glow"></div>
        <div class="scan"></div>
        <div class="vig"></div>
        <div class="mask"></div>
    </div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <script>
        // ---------------------------------------------------------------
        // Core setup
        // ---------------------------------------------------------------
        const scene = new THREE.Scene();
        scene.background = new THREE.Color(0x000000);
        scene.fog = new THREE.FogExp2(0x000000, 0.0022);

        const camera = new THREE.PerspectiveCamera(
            65, window.innerWidth / window.innerHeight, 0.1, 2000
        );

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

        // ---------------------------------------------------------------
        // Lightweight custom orbit controls (drag to rotate, wheel to zoom,
        // gentle auto-rotate when idle) — avoids relying on an external
        // OrbitControls module that may be blocked by CSP.
        // ---------------------------------------------------------------
        const orbit = {
            radius: 630,
            minRadius: 180,
            maxRadius: 1440,
            theta: 0,
            phi: Math.PI / 2.35,
            autoRotate: true,
            autoRotateSpeed: 0.06,
            dragging: false,
            lastX: 0,
            lastY: 0
        };

        function updateCameraFromOrbit() {
            const sinPhi = Math.sin(orbit.phi);
            camera.position.set(
                orbit.radius * sinPhi * Math.sin(orbit.theta),
                orbit.radius * Math.cos(orbit.phi),
                orbit.radius * sinPhi * Math.cos(orbit.theta)
            );
            camera.lookAt(0, 0, 0);
        }
        updateCameraFromOrbit();

        function onDragStart(x, y) {
            orbit.dragging = true;
            orbit.lastX = x;
            orbit.lastY = y;
        }
        function onDragMove(x, y) {
            if (!orbit.dragging) return;
            const dx = x - orbit.lastX;
            const dy = y - orbit.lastY;
            orbit.theta -= dx * 0.0045;
            orbit.phi -= dy * 0.0045;
            orbit.phi = Math.max(0.25, Math.min(Math.PI - 0.25, orbit.phi));
            orbit.lastX = x;
            orbit.lastY = y;
        }
        function onDragEnd() { orbit.dragging = false; }

        renderer.domElement.addEventListener('mousedown', (e) => onDragStart(e.clientX, e.clientY));
        window.addEventListener('mousemove', (e) => onDragMove(e.clientX, e.clientY));
        window.addEventListener('mouseup', onDragEnd);

        renderer.domElement.addEventListener('touchstart', (e) => {
            if (e.touches.length === 1) onDragStart(e.touches[0].clientX, e.touches[0].clientY);
        }, { passive: true });
        renderer.domElement.addEventListener('touchmove', (e) => {
            if (e.touches.length === 1) onDragMove(e.touches[0].clientX, e.touches[0].clientY);
        }, { passive: true });
        renderer.domElement.addEventListener('touchend', onDragEnd);

        renderer.domElement.addEventListener('wheel', (e) => {
            e.preventDefault();
            orbit.radius += e.deltaY * 0.12;
            orbit.radius = Math.max(orbit.minRadius, Math.min(orbit.maxRadius, orbit.radius));
        }, { passive: false });

        // ---------------------------------------------------------------
        // Lights
        // ---------------------------------------------------------------
        scene.add(new THREE.AmbientLight(0xffffff, 0.35));

        const keyLight = new THREE.DirectionalLight(0xffffff, 0.7);
        keyLight.position.set(1, 2, 1.5);
        scene.add(keyLight);

        const backLight = new THREE.PointLight(0xff8855, 0.7, 500);
        backLight.position.set(-60, 40, -80);
        scene.add(backLight);

        const fillLight = new THREE.PointLight(0x6688ff, 0.5, 500);
        fillLight.position.set(0, -120, 0);
        scene.add(fillLight);

        const movingLight = new THREE.PointLight(0xff9c66, 1.1, 400);
        scene.add(movingLight);

        // ---------------------------------------------------------------
        // Node field: multiple concentric "shells" (layers), evenly
        // distributed with a spherical Fibonacci lattice so the network
        // reads as a dense organic brain-like volume.
        // ---------------------------------------------------------------
        const SHELLS = 7;
        const BASE_RADIUS = 102;
        const SHELL_GAP = 45;
        const BASE_COUNT = 34;
        const COUNT_GROWTH = 10;

        const nodes = [];
        const nodeGroup = new THREE.Group();
        scene.add(nodeGroup);

        function fibonacciSpherePoints(count, radius) {
            const pts = [];
            const golden = Math.PI * (3 - Math.sqrt(5));
            for (let i = 0; i < count; i++) {
                const y = 1 - (i / (count - 1)) * 2;
                const r = Math.sqrt(1 - y * y);
                const theta = golden * i;
                const x = Math.cos(theta) * r;
                const z = Math.sin(theta) * r;
                pts.push(new THREE.Vector3(x * radius, y * radius, z * radius));
            }
            return pts;
        }

        function shellColor(shellIdx, total) {
            const hue = 0.06 + (shellIdx / (total - 1)) * 0.5;
            const color = new THREE.Color();
            color.setHSL(hue, 0.75, 0.58);
            return color;
        }

        const sharedNodeGeo = new THREE.SphereGeometry(1, 12, 10);

        for (let s = 0; s < SHELLS; s++) {
            const radius = BASE_RADIUS + s * SHELL_GAP;
            const count = BASE_COUNT + s * COUNT_GROWTH;
            const color = shellColor(s, SHELLS);
            const points = fibonacciSpherePoints(count, radius);

            const mat = new THREE.MeshPhongMaterial({
                color: color,
                shininess: 90,
                emissive: color.clone().multiplyScalar(0.35),
                flatShading: false
            });

            points.forEach((p) => {
                const jitter = 1 + (Math.random() - 0.5) * 0.06;
                p.multiplyScalar(jitter);
                const size = 0.55 + Math.random() * 0.9;
                const mesh = new THREE.Mesh(sharedNodeGeo, mat);
                mesh.position.copy(p);
                mesh.scale.setScalar(size);
                mesh.userData.baseScale = size;
                mesh.userData.pulse = 0;
                nodeGroup.add(mesh);
                nodes.push({ mesh, shell: s, color });
            });
        }

        // ---------------------------------------------------------------
        // Dynamic connection pool
        // ---------------------------------------------------------------
        const MAX_LINK_DIST = 46;
        const LINK_POOL_SIZE = window.innerWidth < 700 ? 320 : 650;
        const FADE_TIME = 0.9;
        const MIN_LIFE = 2.5;
        const MAX_LIFE = 7.5;

        const linkGeometry = new THREE.BufferGeometry();
        const linkPositions = new Float32Array(LINK_POOL_SIZE * 2 * 3);
        const linkColors = new Float32Array(LINK_POOL_SIZE * 2 * 3);
        linkGeometry.setAttribute('position', new THREE.BufferAttribute(linkPositions, 3));
        linkGeometry.setAttribute('color', new THREE.BufferAttribute(linkColors, 3));

        const linkMaterial = new THREE.LineBasicMaterial({
            vertexColors: true,
            transparent: true,
            opacity: 0.85,
            blending: THREE.AdditiveBlending,
            depthWrite: false
        });
        const linkLines = new THREE.LineSegments(linkGeometry, linkMaterial);
        scene.add(linkLines);

        function randomNearbyPair() {
            let a = Math.floor(Math.random() * nodes.length);
            for (let tries = 0; tries < 10; tries++) {
                const b = Math.floor(Math.random() * nodes.length);
                if (b === a) continue;
                const dist = nodes[a].mesh.position.distanceTo(nodes[b].mesh.position);
                if (dist < MAX_LINK_DIST || Math.random() < 0.04) return [a, b];
            }
            return [a, (a + 1) % nodes.length];
        }

        function makeLink() {
            const [a, b] = randomNearbyPair();
            return { a, b, age: 0, life: MIN_LIFE + Math.random() * (MAX_LIFE - MIN_LIFE) };
        }

        const links = [];
        for (let i = 0; i < LINK_POOL_SIZE; i++) links.push(makeLink());

        let rewireEvents = 0;

        function linkAlpha(link) {
            const t = link.age, life = link.life;
            if (t < FADE_TIME) return t / FADE_TIME;
            if (t > life - FADE_TIME) return Math.max(0, (life - t) / FADE_TIME);
            return 1;
        }

        function writeLinkVertex(offset, position, color, alpha) {
            linkPositions[offset * 3] = position.x;
            linkPositions[offset * 3 + 1] = position.y;
            linkPositions[offset * 3 + 2] = position.z;
            linkColors[offset * 3] = color.r * alpha;
            linkColors[offset * 3 + 1] = color.g * alpha;
            linkColors[offset * 3 + 2] = color.b * alpha;
        }

        function rewireLink(link) {
            const [a, b] = randomNearbyPair();
            link.a = a;
            link.b = b;
            link.age = 0;
            link.life = MIN_LIFE + Math.random() * (MAX_LIFE - MIN_LIFE);
            rewireEvents++;
        }

        function rewireLinksTouchingNode(nodeIdx) {
            let count = 0;
            for (const link of links) {
                if (link.a === nodeIdx || link.b === nodeIdx) {
                    rewireLink(link);
                    count++;
                    if (count > 60) break;
                }
            }
        }

        function burstRandomRewire(fraction) {
            const n = Math.floor(links.length * fraction);
            for (let i = 0; i < n; i++) rewireLink(links[Math.floor(Math.random() * links.length)]);
        }

        // ---------------------------------------------------------------
        // Traveling signal pulses
        // ---------------------------------------------------------------
        const PULSE_COUNT = Math.floor(LINK_POOL_SIZE * 0.4);
        const pulseGeometry = new THREE.BufferGeometry();
        const pulsePositions = new Float32Array(PULSE_COUNT * 3);
        const pulseColors = new Float32Array(PULSE_COUNT * 3);
        pulseGeometry.setAttribute('position', new THREE.BufferAttribute(pulsePositions, 3));
        pulseGeometry.setAttribute('color', new THREE.BufferAttribute(pulseColors, 3));

        const pulseMaterial = new THREE.PointsMaterial({
            size: 2.1,
            vertexColors: true,
            transparent: true,
            opacity: 0.95,
            blending: THREE.AdditiveBlending,
            depthWrite: false,
            sizeAttenuation: true
        });
        const pulsePoints = new THREE.Points(pulseGeometry, pulseMaterial);
        scene.add(pulsePoints);

        const pulseAssignments = [];
        for (let i = 0; i < PULSE_COUNT; i++) {
            pulseAssignments.push({
                linkIndex: Math.floor(Math.random() * links.length),
                t: Math.random(),
                speed: 0.25 + Math.random() * 0.5
            });
        }

        // ---------------------------------------------------------------
        // Outer aura + starfield
        // ---------------------------------------------------------------
        const starGeometry = new THREE.BufferGeometry();
        const starCount = 2400;
        const starPositions = new Float32Array(starCount * 3);
        for (let i = 0; i < starCount; i++) {
            starPositions[i * 3] = (Math.random() - 0.5) * 900;
            starPositions[i * 3 + 1] = (Math.random() - 0.5) * 600;
            starPositions[i * 3 + 2] = (Math.random() - 0.5) * 900;
        }
        starGeometry.setAttribute('position', new THREE.BufferAttribute(starPositions, 3));
        const starMaterial = new THREE.PointsMaterial({ color: 0xaaccff, size: 0.35, transparent: true, opacity: 0.45 });
        const stars = new THREE.Points(starGeometry, starMaterial);
        scene.add(stars);

        const auraGeo = new THREE.SphereGeometry(BASE_RADIUS + SHELL_GAP * (SHELLS - 1) + 14, 40, 24);
        const auraMat = new THREE.MeshBasicMaterial({ color: 0x336699, wireframe: true, transparent: true, opacity: 0.055 });
        const aura = new THREE.Mesh(auraGeo, auraMat);
        scene.add(aura);

        // ---------------------------------------------------------------
        // Node click / tap interaction
        // ---------------------------------------------------------------
        const raycaster = new THREE.Raycaster();
        const pointer = new THREE.Vector2();
        let autoBurstEnabled = true;
        let clickMoved = false;

        function flashNode(idx) { nodes[idx].mesh.userData.pulse = 1; }

        function handleTap(clientX, clientY) {
            pointer.x = (clientX / window.innerWidth) * 2 - 1;
            pointer.y = -(clientY / window.innerHeight) * 2 + 1;
            raycaster.setFromCamera(pointer, camera);
            const intersects = raycaster.intersectObjects(nodeGroup.children, false);

            if (intersects.length > 0) {
                const idx = nodeGroup.children.indexOf(intersects[0].object);
                if (idx !== -1) {
                    rewireLinksTouchingNode(idx);
                    flashNode(idx);
                }
            } else {
                burstRandomRewire(0.16);
            }
        }

        renderer.domElement.addEventListener('mousedown', () => { clickMoved = false; });
        renderer.domElement.addEventListener('mousemove', () => { if (orbit.dragging) clickMoved = true; });
        renderer.domElement.addEventListener('mouseup', (e) => { if (!clickMoved) handleTap(e.clientX, e.clientY); });

        renderer.domElement.addEventListener('touchend', (e) => {
            if (e.changedTouches.length === 0) return;
            const t = e.changedTouches[0];
            handleTap(t.clientX, t.clientY);
        });

        // ---------------------------------------------------------------
        // Animation loop
        // ---------------------------------------------------------------
        const clock = new THREE.Clock();
        let rewireAccumTime = 0;
        let periodicBurstTimer = 0;

        function updateLinks(dt) {
            for (let i = 0; i < links.length; i++) {
                const link = links[i];
                link.age += dt;
                if (link.age >= link.life) rewireLink(link);

                const alpha = linkAlpha(link);
                const nodeA = nodes[link.a].mesh.position;
                const nodeB = nodes[link.b].mesh.position;
                const mixed = nodes[link.a].color.clone().lerp(nodes[link.b].color, 0.5);

                writeLinkVertex(i * 2, nodeA, mixed, alpha);
                writeLinkVertex(i * 2 + 1, nodeB, mixed, alpha);
            }
            linkGeometry.attributes.position.needsUpdate = true;
            linkGeometry.attributes.color.needsUpdate = true;
        }

        function updatePulses(dt) {
            for (let i = 0; i < pulseAssignments.length; i++) {
                const pa = pulseAssignments[i];
                pa.t += dt * pa.speed;
                if (pa.t > 1) {
                    pa.t = 0;
                    pa.linkIndex = Math.floor(Math.random() * links.length);
                }
                const link = links[pa.linkIndex];
                const nodeA = nodes[link.a].mesh.position;
                const nodeB = nodes[link.b].mesh.position;
                const alpha = linkAlpha(link);

                pulsePositions[i * 3] = nodeA.x + (nodeB.x - nodeA.x) * pa.t;
                pulsePositions[i * 3 + 1] = nodeA.y + (nodeB.y - nodeA.y) * pa.t;
                pulsePositions[i * 3 + 2] = nodeA.z + (nodeB.z - nodeA.z) * pa.t;

                const glow = alpha * (0.6 + 0.4 * Math.sin(pa.t * Math.PI));
                pulseColors[i * 3] = 1.0 * glow;
                pulseColors[i * 3 + 1] = 0.68 * glow;
                pulseColors[i * 3 + 2] = 0.42 * glow;
            }
            pulseGeometry.attributes.position.needsUpdate = true;
            pulseGeometry.attributes.color.needsUpdate = true;
        }

        function updateNodePulses(dt) {
            for (const n of nodes) {
                if (n.mesh.userData.pulse > 0) {
                    n.mesh.userData.pulse -= dt * 2.2;
                    const p = Math.max(0, n.mesh.userData.pulse);
                    n.mesh.scale.setScalar(n.mesh.userData.baseScale * (1 + p * 1.8));
                } else if (n.mesh.scale.x !== n.mesh.userData.baseScale) {
                    n.mesh.scale.setScalar(n.mesh.userData.baseScale);
                }
            }
        }

        function animate() {
            requestAnimationFrame(animate);
            const dt = Math.min(clock.getDelta(), 0.05);
            const elapsed = clock.getElapsedTime();

            if (!orbit.dragging && orbit.autoRotate) orbit.theta += orbit.autoRotateSpeed * dt;
            updateCameraFromOrbit();

            updateLinks(dt);
            updatePulses(dt);
            updateNodePulses(dt);

            nodeGroup.rotation.y += 0.0006;
            stars.rotation.y += 0.0003;
            stars.rotation.x += 0.00015;
            aura.rotation.y -= 0.0004;
            aura.rotation.x += 0.0002;

            movingLight.position.set(
                70 * Math.sin(elapsed * 0.35),
                40 * Math.cos(elapsed * 0.27),
                70 * Math.cos(elapsed * 0.35)
            );
            movingLight.intensity = 0.9 + Math.sin(elapsed * 1.4) * 0.3;

            if (autoBurstEnabled) {
                periodicBurstTimer += dt;
                if (periodicBurstTimer > 4.5) {
                    periodicBurstTimer = 0;
                    burstRandomRewire(0.05);
                }
            }

            renderer.render(scene, camera);
        }

        animate();

        window.addEventListener('resize', onWindowResize, false);
        function onWindowResize() {
            const width = window.innerWidth;
            const height = window.innerHeight;
            renderer.setSize(width, height);
            camera.aspect = width / height;
            camera.updateProjectionMatrix();
        }

        console.log('3D Net ready | nodes: ' + nodes.length + ' | link pool: ' + LINK_POOL_SIZE + ' | pulses: ' + PULSE_COUNT);
    </script>
</body>
</html>
MikeHiggins.me.uk LinkedIn GitHub Bluesky Contra Upwork
© 2026 Mike Higgins. All rights reserved.