<!DOCTYPE html>

<html lang="en">

<head>

    <meta charset="UTF-8">

    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>JBF // KINETIC GIS MASTER</title>


    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js"></script>

    

    <style>

        /* --- SWISS BRUTALIST CSS --- */

        :root {

            --swiss-red: #E63946;

            --swiss-black: #111;

            --swiss-white: #F4F4F4;

        }


        body {

            margin: 0;

            background-color: var(--swiss-white);

            color: var(--swiss-black);

            font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;

            overflow-x: hidden;

            cursor: crosshair;

        }


        /* FIXED UI LAYER (The "Hud") */

        .ui-layer {

            position: fixed;

            top: 20px; left: 20px; right: 20px; bottom: 20px;

            border: 3px solid var(--swiss-black);

            pointer-events: none;

            z-index: 100;

            display: flex;

            justify-content: space-between;

            align-items: flex-start;

            padding: 20px;

        }


        .brand {

            font-weight: 900;

            font-size: 2rem;

            letter-spacing: -2px;

            background: var(--swiss-white);

            padding: 0 10px;

        }


        .status {

            font-family: monospace;

            font-size: 10px;

            letter-spacing: 2px;

            text-align: right;

            background: var(--swiss-white);

            padding: 5px;

        }


        /* BACKGROUND CANVAS (The "TIN" Field) */

        #tin-canvas {

            position: fixed;

            top: 0; left: 0;

            width: 100%; height: 100vh;

            z-index: 0;

            opacity: 0.4;

        }


        /* SCROLL CONTAINER */

        .scrolly-container {

            position: relative;

            z-index: 10;

        }


        /* THE "MOBILE" (Kinetic Objects) */

        .mobile-viewport {

            height: 100vh;

            display: flex;

            justify-content: center;

            align-items: center;

            perspective: 1000px;

            position: sticky;

            top: 0;

        }


        .object-cluster {

            position: relative;

            width: 400px; 

            height: 400px;

            transform-style: preserve-3d;

        }


        .node {

            position: absolute;

            display: flex;

            justify-content: center;

            align-items: center;

            font-weight: bold;

            font-family: monospace;

            font-size: 10px;

            color: white;

            box-shadow: 10px 10px 20px rgba(0,0,0,0.1);

        }


        /* Swiss Shapes */

        .circle { border-radius: 50%; width: 60px; height: 60px; }

        .rect { width: 80px; height: 80px; }

        .red { background: var(--swiss-red); }

        .black { background: var(--swiss-black); }


        /* NARRATIVE TEXT SECTIONS */

        .step {

            height: 100vh;

            display: flex;

            flex-direction: column;

            justify-content: center;

            padding-left: 10%;

            position: relative;

            z-index: 20;

            pointer-events: none; /* Let clicks pass through to canvas */

        }


        h1 {

            font-size: 8vw;

            line-height: 0.8;

            margin: 0;

            letter-spacing: -4px;

            text-transform: uppercase;

            mix-blend-mode: exclusion; 

            color: #fff; /* Inversion effect */

        }


        p {

            font-size: 1.2rem;

            font-weight: bold;

            background: var(--swiss-black);

            color: white;

            display: inline-block;

            padding: 5px 10px;

            margin-top: 20px;

            max-width: 400px;

        }


        /* Spacer for scroll length */

        .spacer { height: 50vh; }


    </style>

</head>

<body>


    <div class="ui-layer">

        <div class="brand">JBF<span style="color:var(--swiss-red)">.</span>GIS</div>

        <div class="status">

            SYSTEM_STATUS: ONLINE<br>

            LAT: <span id="lat">00.000</span> / LON: <span id="lon">00.000</span><br>

            MODE: INTERACTIVE_RECOVERY

        </div>

    </div>


    <canvas id="tin-canvas"></canvas>


    <div class="scrolly-container">

        

        <div class="mobile-viewport">

            <div class="object-cluster" id="cluster">

                <div class="node red circle" style="top:20%; left:20%;">RISK</div>

                <div class="node black rect" style="top:50%; left:50%;">DATA</div>

                <div class="node red rect" style="top:10%; left:60%;">H2O</div>

                <div class="node black circle" style="top:70%; left:30%;">ZONE</div>

                <div class="node black rect" style="top:40%; left:80%;">SHELTER</div>

                <div class="node red circle" style="top:80%; left:70%;">GRID</div>

            </div>

        </div>


        <section class="step">

            <h1>Chaos<br>Theory.</h1>

            <p>Disasters are unstructured. We see the noise before the pattern.</p>

        </section>


        <section class="step">

            <h1>Data<br>Scanning.</h1>

            <p>Identifying variables: Flood depth, socio-economic risk, infrastructure load.</p>

        </section>


        <section class="step">

            <h1>Precision<br>Recovery.</h1>

            <p>The JBF System aligns chaos into a structured response grid.</p>

        </section>

        

        <div class="spacer"></div>

    </div>


    <script>

        gsap.registerPlugin(ScrollTrigger);


        /* --- 1. THE "TIN" BACKGROUND (Interactive Canvas) --- */

        const canvas = document.getElementById('tin-canvas');

        const ctx = canvas.getContext('2d');

        let width, height;

        let points = [];

        const mouse = { x: -1000, y: -1000 };


        function resize() {

            width = canvas.width = window.innerWidth;

            height = canvas.height = window.innerHeight;

            initPoints();

        }


        function initPoints() {

            points = [];

            // Create a grid of points

            for(let x=0; x<width; x+=60) {

                for(let y=0; y<height; y+=60) {

                    points.push({

                        ox: x, oy: y, // Original position

                        x: x, y: y,   // Current position

                        vx: 0, vy: 0  // Velocity

                    });

                }

            }

        }


        function animateCanvas() {

            ctx.clearRect(0, 0, width, height);

            

            // Draw abstract connections

            ctx.beginPath();

            points.forEach(p => {

                // Physics: Return to original position (Hooke's Law)

                const dkx = p.ox - p.x;

                const dky = p.oy - p.y;

                p.vx += dkx * 0.05;

                p.vy += dky * 0.05;

                p.vx *= 0.85; // Friction

                p.vy *= 0.85;


                // Physics: Mouse Repulsion

                const dmx = p.x - mouse.x;

                const dmy = p.y - mouse.y;

                const dist = Math.sqrt(dmx*dmx + dmy*dmy);

                if(dist < 150) {

                    const force = (150 - dist) / 150;

                    p.vx += (dmx/dist) * force * 15;

                    p.vy += (dmy/dist) * force * 15;

                }


                p.x += p.vx;

                p.y += p.vy;


                // Draw Point

                ctx.moveTo(p.x, p.y);

                ctx.rect(p.x, p.y, 2, 2);

            });

            ctx.fillStyle = "#CCC";

            ctx.fill();

            requestAnimationFrame(animateCanvas);

        }


        // Mouse Listeners

        window.addEventListener('resize', resize);

        window.addEventListener('mousemove', e => {

            mouse.x = e.clientX; 

            mouse.y = e.clientY;

            // Update UI

            document.getElementById('lat').innerText = (e.clientX/100).toFixed(3);

            document.getElementById('lon').innerText = (e.clientY/100).toFixed(3);

        });

        

        resize();

        animateCanvas();



        /* --- 2. THE SCROLL STORY (GSAP) --- */

        

        // Initial "Floating" State (Calder Mobile)

        gsap.to(".node", {

            y: "random(-50, 50)",

            x: "random(-50, 50)",

            rotation: "random(-20, 20)",

            duration: 3,

            repeat: -1,

            yoyo: true,

            ease: "sine.inOut"

        });


        const tl = gsap.timeline({

            scrollTrigger: {

                trigger: ".scrolly-container",

                start: "top top",

                end: "bottom bottom",

                scrub: 1

            }

        });


        // STEP 1: Explode outward (Analysis)

        tl.to(".node", {

            scale: 1.5,

            x: (i) => Math.cos(i) * 300,

            y: (i) => Math.sin(i) * 300,

            rotation: 0,

            duration: 2

        })

        

        // STEP 2: Snap to Grid (Recovery)

        .to(".node", {

            x: (i) => (i % 3) * 120 - 120, // Grid X

            y: (i) => Math.floor(i / 3) * 120 - 60, // Grid Y

            scale: 1,

            borderRadius: "0%", // Turn circles to squares (Data Blocks)

            backgroundColor: (i) => i % 2 === 0 ? "#E63946" : "#111", // Organize colors

            duration: 2

        });


    </script>

</body>

</html>