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