PRISMA — For the curious

by Samin Sami

HTML

<!-- Google Fonts -->
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter+Tight:wght@500;600&family=Inter:wght@400;500&family=JetBrains+Mono:wght@400;500&family=Plus+Jakarta+Sans:wght@400;700&display=swap" rel="stylesheet">

    <!-- Fixed Edge Frames (Swiss Style) -->
    <div class="edge-frame edge-frame-left">FOR THE CURIOUS // PRISMA COLLECTIVE</div>
    <div class="edge-frame edge-frame-right">
        <span>LAT. 46.2044° N / LONG. 6.1432° E</span>
        <div class="barcode">
            <span></span><span></span><span></span><span></span><span></span><span></span><span></span>
        </div>
    </div>

    <!-- Custom Cursor -->
    <div class="custom-cursor" id="customCursor">
        <svg viewBox="0 0 40 40" id="cursorSvg">
            <line id="lineH" x1="10" y1="20" x2="30" y2="20" stroke="#f3efe6" stroke-width="1.5" />
            <line id="lineV" x1="20" y1="10" x2="20" y2="30" stroke="#f3efe6" stroke-width="1.5" />
        </svg>
    </div>

    <!-- Background Video -->
    <video class="bg-video" id="bgVideo" src="https://zxdefgavgwfxastwmmjm.supabase.co/storage/v1/object/public/assets/prisma.mp4" autoplay muted loop playsinline></video>

    <!-- Interactive WebGL Fluid Canvas -->
    <canvas id="glcanvas"></canvas>

    <!-- Overlay Scrim Layers -->
    <div class="scrim-layer"></div>
    <div class="vignette-layer"></div>

    <!-- Header Navigation -->
    <header>
        <button class="nav-toggle" id="menuOpenBtn">Menu</button>
        <ul class="nav-links">
            <li style="animation-delay: 0ms;" class="rise"><a href="#story">Our story</a></li>
            <li style="animation-delay: 80ms;" class="rise"><a href="#collective">Collective</a></li>
            <li style="animation-delay: 160ms;" class="rise"><a href="#workshops">Workshops</a></li>
     ...

CSS

/* Base Reset */
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
            user-select: none;
            -webkit-user-select: none;
        }

        /* Custom Cursor Override */
        html, body {
            cursor: none !important;
            background-color: #0d0b09;
            color: #f3efe6;
            font-family: 'Inter', sans-serif;
            overflow-x: hidden;
            scrollbar-width: thin;
            scrollbar-color: rgba(243, 239, 230, 0.2) transparent;
        }

        a, button, select, input, [role="button"] {
            cursor: none !important;
        }

        /* Fixed Edge Frames (Minimalist Swiss Crosshair signature) */
        .edge-frame {
            position: fixed;
            z-index: 99;
            pointer-events: none;
            font-family: 'JetBrains Mono', monospace;
            font-size: 10px;
            letter-spacing: 2px;
            text-transform: uppercase;
            color: rgba(243, 239, 230, 0.46);
        }

        .edge-frame-left {
            left: 2rem;
            top: 50%;
            transform: translateY(-50%) rotate(-90deg);
            transform-origin: left center;
            white-space: nowrap;
        }

        .edge-frame-right {
            right: 2rem;
            top: 50%;
            transform: translateY(-50%) rotate(90deg);
            transform-origin: right center;
            display: flex;
            align-items: center;
            gap: 1.5rem;
        }

        /* Stylized CSS Barcode */
        .barcode {
            display: flex;
            gap: 2px;
            height: 16px;
            opacity: 0.6;
        }
        .barcode span {
            display: inline-block;
            background: #f3efe6;
            height: 100%;
        }
        .barcode span:nth-child(odd) { width: 1px; }
        .barcode span:nth-child(even) { width: 3px; }
    ...

JavaScript

/* 1. Velocity Stretching Custom Cursor */
        const customCursor = document.getElementById('customCursor');
        const cursorSvg = document.getElementById('cursorSvg');
        const lineH = document.getElementById('lineH');
        const lineV = document.getElementById('lineV');

        let mouseX = 0, mouseY = 0;
        let currentX = 0, currentY = 0;
        let lastX = 0, lastY = 0;
        let speed = 0;
        let isHovered = false;

        window.addEventListener('mousemove', (e) => {
            mouseX = e.clientX;
            mouseY = e.clientY;
        });

        function updateCursor() {
            // Spring physics interpolation
            currentX += (mouseX - currentX) * 0.15;
            currentY += (mouseY - currentY) * 0.15;

            // Velocity calculation
            const dx = currentX - lastX;
            const dy = currentY - lastY;
            speed = Math.sqrt(dx * dx + dy * dy);

            lastX = currentX;
            lastY = currentY;

            // Dynamic velocity stretching constraints
            const maxStretch = 1.6;
            const stretchX = 1 + Math.min(speed * 0.04, maxStretch);
            const stretchY = 1 + Math.min(speed * 0.04, maxStretch);

            customCursor.style.left = `${currentX}px`;
            customCursor.style.top = `${currentY}px`;

            if (isHovered) {
                // Rotate 45 degrees into an 'X' on interactive elements
                cursorSvg.style.transform = `scale(${stretchX}, ${stretchY}) rotate(45deg)`;
                lineH.setAttribute('stroke', '#f3efe6');
                lineV.setAttribute('stroke', '#f3efe6');
            } else {
                cursorSvg.style.transform = `scale(${stretchX}, ${stretchY}) rotate(0deg)`;
                lineH.setAttribute('stroke', '#f3efe6');
                lineV.setAttribute('stroke', '#f3efe6');
            }

            requestAnimationFrame(updateCursor);
       ...