• 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

BUTTERFLY - 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>Butterfly Abstraction</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { background: #000; overflow: hidden; height: 100vh; }
        #stage {
            position: fixed;
            inset: 0;
            width: 100vw;
            height: 100vh;
            filter: grayscale(1) sepia(1) hue-rotate(80deg) saturate(5) brightness(1.15) contrast(1.2);
            z-index: 2;
        }
        #bgCanvas        { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; }
        #animationCanvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; 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>
    <div id="stage">
        <canvas id="bgCanvas"></canvas>
        <canvas id="animationCanvas"></canvas>
    </div>
<script>

const C = {
    amber:      0xe8700a,
    amberLight: 0xf5a623,
    border:     0x1a0d00,
    vein:       0x150900,
    cream:      0xfff5d6,
    white:      0xffffff,
    apexDark:   0x0d0600,
    hindAmber:  0xd4600a,
    bodyDark:   0x1a0a00,
    bodyMid:    0x3d1a00,
    bg:         0x000000,
};

const bgCanvas = document.getElementById('bgCanvas');
const bgCtx    = bgCanvas.getContext('2d');
const PIX = 8;
let cols, rows, pixels = [];
function initBG() {
    bgCanvas.width  = window.innerWidth;
    bgCanvas.height = window.innerHeight;
    cols = Math.ceil(bgCanvas.width  / PIX);
    rows = Math.ceil(bgCanvas.height / PIX);
    pixels = Array.from({ length: cols * rows }, () => ({
        b: Math.random() * 0.3, s: 0.001 + Math.random() * 0.003
    }));
}
function drawBG() {
    pixels.forEach((p, i) => {
        p.b += p.s;
        if (p.b > 0.4) p.b = 0;
        const x = (i % cols) * PIX, y = Math.floor(i / cols) * PIX;
        bgCtx.fillStyle = `rgb(${p.b*3|0},${p.b*6|0},${p.b*10|0})`;
        bgCtx.fillRect(x, y, PIX, PIX);
    });
    requestAnimationFrame(drawBG);
}

const renderer = new THREE.WebGLRenderer({
    canvas: document.getElementById('animationCanvas'), antialias: true, alpha: true
});
let scene, camera, butterfly;

function updateSize() {
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
}
function updateCam() {
    const w = window.innerWidth;
    camera.fov = w < 480 ? 62 : w < 768 ? 56 : 50;
    camera.position.set(0, 2.2, w < 480 ? 5.5 : w < 768 ? 5.0 : 4.5);
    camera.lookAt(0, -0.2, 0);
    camera.updateProjectionMatrix();
}

const mSolid = (c, op = 1.0) => new THREE.MeshBasicMaterial({
    color: c, side: THREE.DoubleSide, transparent: op < 1, opacity: op
});
const mWire  = c => new THREE.MeshBasicMaterial({ color: c, wireframe: true });
const mEdge  = c => new THREE.MeshBasicMaterial({ color: c });  // solid, single-side, for raised veins

function wingMesh(shape, color, opacity = 1.0, bow = 0.16) {
    const geo = new THREE.ShapeGeometry(shape, 20);
    const pos = geo.attributes.position;
    for (let i = 0; i < pos.count; i++) {
        const x = pos.getX(i);
        const t = Math.max(0, Math.min(x / 2.2, 1));
        pos.setZ(i, bow * Math.sin(Math.PI * t) * 0.5);
    }
    pos.needsUpdate = true;
    geo.computeVertexNormals();
    const mesh = new THREE.Mesh(geo, mSolid(color, opacity));
    mesh.rotation.x = -Math.PI / 2;
    return mesh;
}

function overlayMesh(shape, color, opacity = 1.0, yOffset = 0.01) {
    const mesh = new THREE.Mesh(new THREE.ShapeGeometry(shape, 16), mSolid(color, opacity));
    mesh.rotation.x = -Math.PI / 2;
    mesh.position.y = yOffset;
    return mesh;
}

function vein(parent, pts2d, yHeight = 0.025, radius = 0.010, color = C.vein) {
    const v3 = pts2d.map(([x,y]) => new THREE.Vector3(x, yHeight, -y));
    const curve = pts2d.length === 3
        ? new THREE.QuadraticBezierCurve3(v3[0], v3[1], v3[2])
        : new THREE.CatmullRomCurve3(v3);
    parent.add(new THREE.Mesh(
        new THREE.TubeGeometry(curve, 14, radius, 5, false), mEdge(color)
    ));
}

function spot(parent, sx, sy, rx, rz, color, yOff = 0.028, tubeR = 0.012) {
    const mesh = new THREE.Mesh(new THREE.TorusGeometry(1, tubeR, 6, 22), mEdge(color));
    mesh.rotation.x = Math.PI / 2;
    mesh.scale.set(rx, 1, rz);
    mesh.position.set(sx, yOff, -sy);
    parent.add(mesh);
}

function solidSpot(parent, sx, sy, rx, rz, color, yOff = 0.026) {
    const geo = new THREE.CircleGeometry(1, 16);
    const mesh = new THREE.Mesh(geo, mSolid(color));
    mesh.rotation.x = -Math.PI / 2;
    mesh.scale.set(rx, 1, rz);
    mesh.position.set(sx, yOff, -sy);
    parent.add(mesh);
}

function buildForewing() {
    const W = 2.2, H = 2.6;
    const grp = new THREE.Group();
    const base = new THREE.Shape();
    base.moveTo(0, -H*0.15);
    base.bezierCurveTo(W*0.18,-H*0.08, W*0.38,H*0.05,  W*0.55,H*0.28);
    base.bezierCurveTo(W*0.68,H*0.46,  W*0.85,H*0.72,  W*0.92,H*0.92);
    base.bezierCurveTo(W*0.96,H*1.02,  W*1.00,H*1.05,  W,     H);
    base.bezierCurveTo(W*0.98,H*0.88,  W*0.95,H*0.72,  W*0.88,H*0.55);
    base.bezierCurveTo(W*0.78,H*0.32,  W*0.62,H*0.08,  W*0.45,-H*0.12);
    base.bezierCurveTo(W*0.28,-H*0.28, W*0.12,-H*0.30, 0,     -H*0.15);
    base.closePath();
    grp.add(wingMesh(base, C.border, 1.0, 0.16));

    const inner = new THREE.Shape();
    inner.moveTo(0, -H*0.10);
    inner.bezierCurveTo(W*0.14,-H*0.04, W*0.32,H*0.06,  W*0.46,H*0.26);
    inner.bezierCurveTo(W*0.58,H*0.43,  W*0.72,H*0.64,  W*0.78,H*0.80);
    inner.bezierCurveTo(W*0.80,H*0.88,  W*0.82,H*0.94,  W*0.80,H*0.96);
    inner.bezierCurveTo(W*0.75,H*0.88,  W*0.68,H*0.72,  W*0.60,H*0.55);
    inner.bezierCurveTo(W*0.52,H*0.34,  W*0.38,H*0.10,  W*0.24,-H*0.08);
    inner.bezierCurveTo(W*0.14,-H*0.18, W*0.06,-H*0.20, 0,     -H*0.10);
    inner.closePath();
    grp.add(overlayMesh(inner, C.amber, 1.0, 0.012));

    const hilight = new THREE.Shape();
    hilight.moveTo(0, -H*0.05);
    hilight.bezierCurveTo(W*0.10,H*0.00, W*0.24,H*0.10, W*0.36,H*0.28);
    hilight.bezierCurveTo(W*0.46,H*0.44, W*0.58,H*0.60, W*0.62,H*0.72);
    hilight.bezierCurveTo(W*0.56,H*0.68, W*0.46,H*0.52, W*0.36,H*0.36);
    hilight.bezierCurveTo(W*0.26,H*0.20, W*0.14,H*0.06, W*0.04,-H*0.06);
    hilight.closePath();
    grp.add(overlayMesh(hilight, C.amberLight, 0.55, 0.015));

    const primaryVeins = [
        [[0,-H*0.05],[W*0.36,H*0.52],[W*0.70,H*0.94]],
        [[0,-H*0.05],[W*0.40,H*0.36],[W*0.78,H*0.60]],
        [[0,-H*0.05],[W*0.44,H*0.18],[W*0.86,H*0.26]],
        [[0,-H*0.05],[W*0.42,H*0.02],[W*0.84,-H*0.02]],
        [[0,-H*0.05],[W*0.36,-H*0.12],[W*0.68,-H*0.16]],
        [[0,-H*0.05],[W*0.26,-H*0.18],[W*0.46,-H*0.22]],
    ];
    primaryVeins.forEach(v => vein(grp, v, 0.028, 0.013, C.vein));

    const crossVeins = [
        [[W*0.18,H*0.14],[W*0.20,-H*0.00],[W*0.20,-H*0.14]],
        [[W*0.38,H*0.44],[W*0.40,H*0.24],[W*0.40,H*0.04]],
        [[W*0.55,H*0.66],[W*0.57,H*0.44],[W*0.57,H*0.22]],
    ];
    crossVeins.forEach(v => vein(grp, v, 0.026, 0.009, C.vein));

    const marginalSpots = [
        [W*0.94,H*0.76, 0.042,0.058],
        [W*0.87,H*0.52, 0.038,0.052],
        [W*0.80,H*0.28, 0.040,0.055],
        [W*0.72,H*0.05, 0.040,0.055],
        [W*0.61,-H*0.09,0.040,0.048],
        [W*0.49,-H*0.18,0.042,0.046],
        [W*0.37,-H*0.24,0.040,0.044],
    ];
    marginalSpots.forEach(([sx,sy,rx,rz]) => solidSpot(grp, sx, sy, rx, rz, C.cream));

    const submargSpots = [
        [W*0.80,H*0.67, 0.058,0.080],
        [W*0.72,H*0.42, 0.055,0.075],
        [W*0.62,H*0.18, 0.055,0.070],
        [W*0.50,-H*0.04,0.052,0.065],
    ];
    submargSpots.forEach(([sx,sy,rx,rz]) => solidSpot(grp, sx, sy, rx, rz, C.cream, 0.028));

    const apexSpots = [
        [W*0.72,H*0.95, 0.062,0.048],
        [W*0.82,H*0.88, 0.035,0.030],
        [W*0.60,H*0.98, 0.048,0.036],
        [W*0.50,H*0.94, 0.038,0.030],
        [W*0.88,H*0.78, 0.028,0.024],
        [W*0.78,H*1.00, 0.030,0.025],
    ];
    apexSpots.forEach(([sx,sy,rx,rz]) => solidSpot(grp, sx, sy, rx, rz, C.white, 0.030));
    marginalSpots.forEach(([sx,sy,rx,rz]) => spot(grp, sx, sy, rx*1.05, rz*1.05, C.vein, 0.032, 0.008));

    return grp;
}

function buildHindwing() {
    const W = 1.8, H = 1.6;
    const grp = new THREE.Group();
    const base = new THREE.Shape();
    base.moveTo(0, H*0.10);
    base.bezierCurveTo(W*0.22,H*0.18,  W*0.52,H*0.28,  W*0.75,H*0.20);
    base.bezierCurveTo(W*0.92,H*0.12,  W*1.05,H*0.00,  W,    -H*0.18);
    base.bezierCurveTo(W*1.02,-H*0.42, W*0.98,-H*0.72, W*0.88,-H*0.92);
    base.bezierCurveTo(W*0.78,-H*1.05, W*0.62,-H*1.12, W*0.45,-H*1.08);
    base.bezierCurveTo(W*0.30,-H*1.04, W*0.18,-H*0.92, W*0.10,-H*0.78);
    base.bezierCurveTo(W*0.04,-H*0.62, 0,     -H*0.40, 0,      H*0.10);
    base.closePath();
    grp.add(wingMesh(base, C.border, 1.0, 0.13));
    const fill = new THREE.Shape();
    fill.moveTo(0, H*0.08);
    fill.bezierCurveTo(W*0.18,H*0.16,  W*0.42,H*0.22,  W*0.62,H*0.14);
    fill.bezierCurveTo(W*0.78,H*0.08,  W*0.88,-H*0.02, W*0.85,-H*0.18);
    fill.bezierCurveTo(W*0.88,-H*0.40, W*0.82,-H*0.62, W*0.72,-H*0.80);
    fill.bezierCurveTo(W*0.62,-H*0.94, W*0.48,-H*0.98, W*0.36,-H*0.92);
    fill.bezierCurveTo(W*0.24,-H*0.86, W*0.14,-H*0.72, W*0.08,-H*0.56);
    fill.bezierCurveTo(W*0.03,-H*0.42, 0,     -H*0.24, 0,      H*0.08);
    fill.closePath();
    grp.add(overlayMesh(fill, C.hindAmber, 1.0, 0.012));

    const fanVeins = [
        [[0,H*0.06],[W*0.32,H*0.18],[W*0.72,H*0.08]],
        [[0,H*0.06],[W*0.36,H*0.06],[W*0.84,-H*0.12]],
        [[0,H*0.06],[W*0.38,-H*0.08],[W*0.82,-H*0.28]],
        [[0,H*0.06],[W*0.36,-H*0.22],[W*0.78,-H*0.48]],
        [[0,H*0.06],[W*0.32,-H*0.36],[W*0.70,-H*0.68]],
        [[0,H*0.06],[W*0.26,-H*0.50],[W*0.58,-H*0.88]],
        [[0,H*0.06],[W*0.18,-H*0.58],[W*0.38,-H*0.96]],
        [[0,H*0.06],[W*0.10,-H*0.52],[W*0.16,-H*0.80]],
    ];
    fanVeins.forEach(v => vein(grp, v, 0.026, 0.011, C.vein));

    [[W*0.26,H*0.12],[W*0.28,-H*0.06],[W*0.26,-H*0.22]],
    [[W*0.46,H*0.10],[W*0.48,-H*0.14],[W*0.46,-H*0.40]],
    [[W*0.64,H*0.02],[W*0.66,-H*0.22],[W*0.64,-H*0.55]],
    [
        [[W*0.26,H*0.12],[W*0.28,-H*0.06],[W*0.26,-H*0.22]],
        [[W*0.46,H*0.10],[W*0.48,-H*0.14],[W*0.46,-H*0.40]],
        [[W*0.64,H*0.02],[W*0.66,-H*0.22],[W*0.64,-H*0.55]],
    ].forEach(v => vein(grp, v, 0.024, 0.008, C.vein));

    const innerBorder = [
        [W*0.78,-H*0.08, 0.052,0.068],
        [W*0.82,-H*0.28, 0.050,0.065],
        [W*0.80,-H*0.50, 0.050,0.064],
        [W*0.74,-H*0.68, 0.048,0.062],
        [W*0.62,-H*0.85, 0.046,0.060],
        [W*0.48,-H*0.94, 0.046,0.058],
        [W*0.34,-H*0.90, 0.044,0.056],
        [W*0.22,-H*0.82, 0.040,0.052],
    ];
    innerBorder.forEach(([sx,sy,rx,rz]) => solidSpot(grp, sx, sy, rx, rz, C.cream));

    const outerBorder = [
        [W*0.88,-H*0.08, 0.032,0.042],
        [W*0.92,-H*0.28, 0.030,0.040],
        [W*0.90,-H*0.50, 0.030,0.040],
        [W*0.84,-H*0.68, 0.030,0.038],
        [W*0.74,-H*0.84, 0.028,0.036],
        [W*0.60,-H*0.96, 0.028,0.034],
        [W*0.44,-H*1.01, 0.028,0.034],
        [W*0.30,-H*0.97, 0.026,0.032],
        [W*0.17,-H*0.88, 0.026,0.030],
    ];
    outerBorder.forEach(([sx,sy,rx,rz]) => solidSpot(grp, sx, sy, rx, rz, C.cream, 0.029));

    [[0.090,C.border],[0.062,C.amber],[0.034,C.vein]].forEach(([r,c]) => {
        solidSpot(grp, W*0.20, -H*0.38, r, r, c, 0.030 + (0.090-r)*0.1);
    });

    return grp;
}

function createButterfly() {
    const group = new THREE.Group();

    const head = new THREE.Mesh(new THREE.SphereGeometry(0.13, 16, 16), mEdge(C.bodyDark));
    head.position.set(0, 0, 0.52);
    group.add(head);

    const thorax = new THREE.Mesh(new THREE.SphereGeometry(0.16, 20, 20), mEdge(C.bodyDark));
    thorax.position.set(0, 0, 0.28);
    thorax.scale.set(0.80, 1.10, 1.2);
    group.add(thorax);

    const tHiCurve = new THREE.QuadraticBezierCurve3(
        new THREE.Vector3(-0.08, 0.16, 0.38),
        new THREE.Vector3(0,     0.18, 0.28),
        new THREE.Vector3(0.08,  0.16, 0.18)
    );
    group.add(new THREE.Mesh(new THREE.TubeGeometry(tHiCurve, 8, 0.012, 5, false), mEdge(C.amber)));

    const abdSegs = 8;
    for (let i = 0; i < abdSegs; i++) {
        const t = i / (abdSegs - 1);
        const r = 0.12 * (1 - t * 0.82);
        const seg = new THREE.Mesh(
            new THREE.SphereGeometry(r, 14, 10),
            mEdge(i % 2 === 0 ? C.bodyDark : C.bodyMid)
        );
        seg.position.set(0, 0, 0.08 - i * 0.24);
        seg.scale.set(0.72, 1.05, 1.0);
        group.add(seg);

        if (i > 0 && i < abdSegs - 1) {
            const stripe = new THREE.Mesh(
                new THREE.SphereGeometry(r * 0.45, 8, 6),
                mEdge(C.amber)
            );
            stripe.position.set(0, r * 0.9, 0.08 - i * 0.24);
            stripe.scale.set(0.4, 0.5, 1.4);
            group.add(stripe);
        }
    }

    [-1, 1].forEach(s => {
        const pts = [];
        for (let i = 0; i < 18; i++) {
            const t = i / 17;
            pts.push(new THREE.Vector3(
                s * (0.05 + t * 0.38),
                t * 0.05,
                0.52 - t * 0.55 + t * t * 0.15
            ));
        }
        const crv = new THREE.CatmullRomCurve3(pts);
        group.add(new THREE.Mesh(
            new THREE.TubeGeometry(crv, 16, 0.006, 5, false), mEdge(C.bodyDark)
        ));
        const club = new THREE.Mesh(new THREE.SphereGeometry(0.024, 8, 8), mEdge(C.amber));
        club.position.copy(pts[pts.length - 1]);
        group.add(club);
    });

    [1, -1].forEach(side => {
        const fwGrp = buildForewing();
        if (side === -1) fwGrp.scale.x = -1;
        const fwPivot = new THREE.Group();
        fwPivot.position.set(0, 0.02, 0.22);
        fwPivot.rotation.y = side * (-Math.PI * 0.08);
        fwPivot.rotation.x = -Math.PI * 0.12;
        fwPivot.add(fwGrp);
        group.add(fwPivot);

        const hwGrp = buildHindwing();
        if (side === -1) hwGrp.scale.x = -1;
        const hwPivot = new THREE.Group();
        hwPivot.position.set(0, -0.01, 0.05);
        hwPivot.rotation.y = side * (Math.PI * 0.10);
        hwPivot.rotation.x = -Math.PI * 0.10;
        hwPivot.add(hwGrp);
        group.add(hwPivot);
    });

    return group;
}

function init() {
    initBG();
    drawBG();
    updateSize();

    scene = new THREE.Scene();
    scene.background = new THREE.Color(C.bg);

    camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000);
    updateCam();

    butterfly = createButterfly();
    scene.add(butterfly);

    scene.add(new THREE.AmbientLight(0xffe0a0, 0.7));
    const key = new THREE.PointLight(0xffd080, 2.8, 100);
    key.position.set(3, 4, 3); scene.add(key);
    const fill = new THREE.PointLight(0xa0c8ff, 1.2, 100);
    fill.position.set(-4, 2, 2); scene.add(fill);
    const rim = new THREE.PointLight(0xff9040, 1.0, 100);
    rim.position.set(0, -1, -5); scene.add(rim);

    animate();
}

function animate() {
    requestAnimationFrame(animate);
    const T = Date.now();
    if (butterfly) {
        butterfly.rotation.y += 0.004;
        butterfly.position.y  = Math.sin(T * 0.0015) * 0.06;
        butterfly.rotation.z  = Math.sin(T * 0.0011) * 0.012;
    }
    renderer.render(scene, camera);
}

window.addEventListener('resize', () => {
    updateSize(); initBG();
    if (camera) { camera.aspect = window.innerWidth / window.innerHeight; updateCam(); }
});
window.addEventListener('load', init);
</script>
</body>
</html>
MikeHiggins.me.uk LinkedIn GitHub Bluesky Contra Upwork
© 2026 Mike Higgins. All rights reserved.