KQ-1813 · CATSEYE/MARBLE

Catseye Marble

A glass marble in a felt tray, ray traced per pixel in one WebGL2 pass: the studio reflected by Fresnel with a highlight as sharp as gloss, three twisted vanes found by marching the refracted ray, the felt seen magnified and flipped through the far side, and on the felt a soft shadow holding a ribbon-tinted caustic. Drag it and it follows 1:1 with its orientation integrated as true rolling, so the ribbon turns the way a real marble's does; released, it coasts on rolling friction, bounces off the wall with a soft clack and settles. Arrow keys nudge it with the same physics and Home rolls it back on springs.glide.

KQ-1813 · CATSEYE/MARBLE

Drag to roll it. Arrow keys nudge it, Shift harder; Home rolls it back.

Basinworks · No. 14

resting · drag to roll

Install

One command — the source lands in your repo. Or copy it from the Code tab.

$ pnpm dlx shadcn@latest add @kinetiq/catseye-marble

Props

ribbon
"ember" | "lagoon" | "moss" | "ink"
default "ember"
The ribbon's colourway.
colors
string[]
default —
Up to three CSS colours, one per vane, that replace ribbon.
gloss
number
default 0.6
How glossy the glass is: the reflection's strength and the highlight's sharpness, 0 to 1.
roll
number
default 0.4
Rolling friction, 0 (a slick table that lets it run) to 1 (felt that stops it quickly).
size
number
default 64
The marble's diameter, px; never more than 36% of the tray's shorter side.
twist
number
default 0.5
How far the vanes turn from one pole to the other, 0 (flat) to 1 (a turn and a half).
tint
string
default "oklch(from var(--accent-bright) 0.95 0.02 calc(h - 50))"
The glass's colour, any CSS colour; the thicker the glass, the more it shows.
table
string
default "color-mix(in oklab, var(--bg-2) 86%, var(--ink-3))"
The tray's felt, any CSS colour.
defaultPosition
{ x: number; y: number }
default { x: 0.42, y: 0.5 }
Where the marble starts, as fractions of the tray.
resetKey
string | number
default —
Any change rolls the marble back to where it started.
onRoll
() => void
default —
Fires when the marble starts rolling from rest.
onRest
(position: { x: number; y: number }) => void
default —
Fires when the marble comes to rest, with where (fractions of the tray).
onBounce
(speed: number) => void
default —
Fires when the marble hits the tray's wall, with its speed into the wall, px/s.
interactive
boolean
default true
Dragging and the keys roll it.
sound
boolean
default false
A soft clack off the tray's wall. Off unless asked for.
dpr
number
default 2
Cap on the canvas's device pixel ratio, at most 2.
label
string
default "Glass marble"
The tray's accessible name.
disabled
boolean
default false
Nothing rolls it.
className
string
default "h-56 w-full"
Sizes the tray.