A WebGL carousel of panels viewed through a liquid-glass lens post-process, with an optional free-floating architecture-diagram overlay per panel. Three.js + GSAP, no framework, no build step required to use it.
Extracted from sidharthjoly.com's project carousel.
npm install refracted three gsapthree and gsap are peer dependencies — bring your own versions.
import { createRefracted } from "refracted";
import "refracted/style.css";
const rf = createRefracted(document.getElementById("app"), {
projects: [
{
brand: "Aurora",
aspect: 4 / 5, // width / height
color: "#4FD1C5", // used for the card's constellation art and arch-node glow
caption: "One line, shown under the card while it's centered",
desc: "A longer description, shown in the detail panel once opened.",
tags: ["FastAPI", "Oracle Cloud"],
url: "https://example.com", // "#" hides the "View project" link
// optional: draws a free-floating node/edge diagram over the card once focused
architecture: {
nodes: [
{ id: "api", label: "API", role: "core service", x: 0.5, y: 0.5, core: true,
desc: "Shown when this node is clicked." },
{ id: "db", label: "Postgres", role: "storage", x: 0.8, y: 0.3 },
],
edges: [["api", "db"]],
},
},
// ...more projects
],
});The container needs an explicit size (width/height, or fill a sized
parent) — the engine reads container.clientWidth/clientHeight on init and
on window resize.
createRefracted(container, options) -> { applyThemeColors, closeFocus, destroy }
applyThemeColors()— re-reads--rf-bgoff the container and re-syncs the WebGL clear color / resting card tone to it. Call this after you change that CSS variable at runtime (e.g. a theme toggle).closeFocus()— closes the detail panel/focused card, if one is open.destroy()— stops the render loop, removes listeners, and clears the container.
| Option | Default | |
|---|---|---|
projects |
[] |
see shape above |
viewLabel |
"View" |
text in the hover pill |
config |
see DEFAULT_CONFIG in src/refracted.js |
panel size, scroll feel, snap behavior |
interact |
see DEFAULT_INTERACT |
drag/click/touch tuning |
lens |
see DEFAULT_LENS |
the glass shader's look |
focus |
see DEFAULT_FOCUS |
focus-mode animation timing |
entry |
see DEFAULT_ENTRY |
the on-load rise-and-bloom animation |
Each is a partial override merged over the defaults — pass only what you want to change.
If a project sets repo: "owner/name", a node can set:
live: "commit"— last commit time, from the GitHub APIlive: "workflow"— last GitHub Actions run status + timelive: { type: "pypi", package: "name" }— latest PyPI release
Shown in the node's tooltip once fetched. Fails silently (private repo, rate limit, offline) rather than showing an error.
Override these custom properties on the container (or an ancestor):
#app {
--rf-bg: #0a0e1a;
--rf-text: #eaf0ff;
--rf-muted: #8c9bc4;
--rf-accent: #6fe3ff;
--rf-glass-bg: rgba(18,24,46,0.55);
--rf-glass-border: rgba(160,190,255,0.22);
--rf-shadow-color: rgba(0,0,0,0.55);
--rf-glow: 0 0 18px rgba(111,227,255,0.55);
}If you change --rf-bg after init, call rf.applyThemeColors() to sync the
WebGL clear color and card tone to the new value.
npx serve .
# open demo/index.htmlThis is the reusable engine only — it doesn't ship any easter eggs, themes,
audio, or custom cursors. Those are specific to the site it was extracted
from; build your own on top using applyThemeColors() and your own
--rf-* overrides.