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);
...