EIDOLON - Samin Sami
by Samin Sami
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>EIDOLON — A museum of lost things, rendered in dust</title>
<meta name="description" content="EIDOLON is a museum whose artifacts are ghosts: point-cloud sculptures of things that no longer exist, each assembled from sixty thousand grains of drifting light. An amphora, a fallen column, a moth, a hand — sculpture from pure mathematics." />
<meta property="og:title" content="EIDOLON — A museum of lost things" />
<meta property="og:description" content="Four artifacts that no longer exist, reconstructed from dust as you descend. Point-cloud sculpture rendered live in your browser." />
<meta name="theme-color" content="#0b0a0c" />
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%230b0a0c'/%3E%3Cg fill='%23efe7d6'%3E%3Ccircle cx='16' cy='7' r='1.1'/%3E%3Ccircle cx='11' cy='12' r='1'/%3E%3Ccircle cx='21' cy='12' r='1'/%3E%3Ccircle cx='16' cy='14' r='1.2'/%3E%3Ccircle cx='13' cy='19' r='1'/%3E%3Ccircle cx='19' cy='19' r='1'/%3E%3Ccircle cx='16' cy='24' r='1.1'/%3E%3C/g%3E%3Ccircle cx='16' cy='16' r='11' fill='none' stroke='%23c98a3c' stroke-width='0.6' opacity='0.7'/%3E%3C/svg%3E" />
<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=Fraunces:ital,opsz,wght@0,9..144,300;0,9..144,400;0,9..144,500;1,9..144,300;1,9..144,400&family=Space+Grotesk:wght@300;400;500;600&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<div class="grain" aria-hidden="true"></div>
<div class="vignette" aria-hidden="true"></div>
<!-- fixed WebGL stage — the exhibit hall -->
<canvas id="scene" aria-hidden="true"></canvas>
<div class="webgl-fallback"...
CSS
/* ============================================================
EIDOLON — museum of lost things
Deep hall dark · bone-white points · amber plaque type
============================================================ */
:root {
--hall: #0b0a0c; /* near-black gallery dark */
--hall-2: #131117; /* faintly warmer pool of light */
--bone: #efe7d6; /* point / heading white */
--bone-dim: #b8b0a3; /* body text */
--bone-faint: #6d675e; /* meta / captions */
--amber: #d79a4a; /* plaque accent */
--amber-deep: #b47a2f;
--plaque-bg: rgba(18,15,12,0.58);
--rule: rgba(215,154,74,0.35);
--maxw: 1440px;
--ease: cubic-bezier(0.22, 1, 0.36, 1);
--ease-in: cubic-bezier(0.7, 0, 0.84, 0);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
[hidden] { display: none !important; }
html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; }
body {
background: var(--hall);
color: var(--bone-dim);
font-family: "Space Grotesk", system-ui, sans-serif;
font-weight: 300;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow-x: hidden;
cursor: default;
}
::selection { background: rgba(215,154,74,0.28); color: var(--bone); }
a { color: inherit; text-decoration: none; }
/* thin custom scrollbar */
::-webkit-scrollbar { width: 9px; }
::-webkit-scrollbar-track { background: #060506; }
::-webkit-scrollbar-thumb { background: #2a2620; border-radius: 9px; border: 2px solid #060506; }
::-webkit-scrollbar-thumb:hover { background: var(--amber-deep); }
/* ---------- fixed atmosphere overlays ---------- */
.grain {
position: fixed; inset: 0; z-index: 40; pointer-events: none;
opacity: 0.05; mix-blend-mode: screen;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence...
JavaScript
// ============================================================================
// EIDOLON — point-cloud museum of lost things
//
// Each exhibit is ~60k points whose HOME positions are sampled parametrically
// (lathe-revolved profile, fluted+fractured column, parametric moth, anatomical
// hand). Points live in one big BufferGeometry; a per-vertex "assembly" uniform
// (0 = scattered dust, 1 = fully assembled) lerps each grain between a drifting
// dust cloud and its home. Scroll drives assembly per exhibit; a curl-noise field
// (evaluated in the vertex shader) makes the dust breathe and lets the cursor
// stir it. Hold-to-focus tightens + brightens the whole cloud.
//
// No external models. Everything is math.
// ============================================================================
import * as THREE from 'three';
const canvas = document.getElementById('scene');
const fallback = document.getElementById('fallback');
const focusRing= document.getElementById('focusRing');
const hallIdx = document.getElementById('hallIdx');
const scrollCue= document.getElementById('scrollCue');
const live = document.getElementById('live');
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
const PER = 60000; // points per exhibit (approx; each generator returns its own count)
// ---- WebGL capability check -> graceful fallback -------------------------
let renderer;
try {
renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: false, preserveDrawingBuffer: true, powerPreference: 'high-performance' });
const gl = renderer.getContext();
if (!gl) throw new Error('no gl');
} catch (e) {
canvas.hidden = true;
fallback.hidden = false;
console.warn('EIDOLON: WebGL unavailable, showing fallback.');
}
if (renderer) boot();
function boot() {
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(innerWidth, innerHeight);
// Opaque clear (hall...