KQ-1785 · BISTABLE/SEA
Bistable Sea
A slim e-paper display showing an open-sea swell under a low sun, rendered at e-paper resolution as a real perspective ocean (four wave trains, sky Fresnel, a sun-glitter path, foam on steep crests, cloud bands) and error-diffused to pure ink and paper. It refreshes like e-paper rather than video: each step of the sea rolls down the panel in bands, changed pixels flipping through their inverse before they land and keeping a faint ghost of what they were, and every few partials a full refresh flashes inverse, blanks and lands clean. The pointer tilts the device toward it on the glide spring with a sheen across the glass, a horizontal drag turns the bearing 1:1, and the device is a slider whose arrow, Page and Home keys turn the sea, each turn kicking it into a small tilt that settles on glide.
Left and Right turn the display 15 degrees, Shift or Page keys 45, Home faces north. Drag sideways to turn it by hand. Facing SSW.
Basinworks Tide
Harbour Display, 6 in
The swell off the breakwater on paper that holds its picture without power. A month on a charge.
partial refresh · facing SSW 200° · drag or use the arrow keys
Install
One command — the source lands in your repo. Or copy it from the Code tab.
$ pnpm dlx shadcn@latest add @kinetiq/bistable-sea
Props
- refresh
- number
- default 1
- Partial refreshes a second, 0.25 to 3: how often the sea steps on.
- ghosting
- number
- default 0.5
- How much of each old frame a refresh leaves behind, 0 (clean) to 1 (heavy ghosts).
- swell
- number
- default 0.55
- How big the swell runs, 0 (glassy) to 1 (heavy).
- value / defaultValue
- number
- default 200
- The bearing the display faces, in degrees (0 north, 90 east); controlled or initial.
- onValueChange
- (bearing: number) => void
- default —
- Fires from the drag or key that turned the display, with the new bearing.
- onRefresh
- (kind: "partial" | "full") => void
- default —
- Fires as each refresh lands on the panel.
- fullEvery
- number
- default 6
- Partial refreshes between full ones, which flash and clear every ghost; 0 never.
- bands
- number
- default 8
- How many bands a partial refresh rolls down the panel in, 3 to 16.
- dither
- "floyd" | "atkinson"
- default "floyd"
- The error-diffusion kernel: smooth Floyd–Steinberg or crisp, lighter Atkinson.
- dot
- number
- default 2
- CSS px per e-paper pixel, 1 to 4.
- chop
- number
- default 0.35
- Wind chop on top of the swell, 0 to 1.
- tilt
- number
- default 0.6
- How far the device tilts toward the pointer, 0 to 1.
- caption
- boolean
- default true
- Show the readout strip at the top of the paper.
- paperColor
- string
- default a warm light grey
- The paper's colour, any CSS colour.
- inkColor
- string
- default a soft black
- The ink's colour, any CSS colour.
- label
- string
- default "Swell display"
- The display's accessible name.
- interactive
- boolean
- default true
- The pointer tilts and turns the display. Keys still turn it.
- paused
- boolean
- default false
- Stop refreshing: the panel keeps its last frame.
- seed
- number
- default 4
- Seeds the sea's phases and the clouds.
- disabled
- boolean
- default false
- The display stops answering; it stays focusable and says so.
- className
- string
- default "w-72"
- Sizes the device; its height follows from a 3:2 aspect.
| Prop | Type | Default | Description |
|---|---|---|---|
| refresh | number | 1 | Partial refreshes a second, 0.25 to 3: how often the sea steps on. |
| ghosting | number | 0.5 | How much of each old frame a refresh leaves behind, 0 (clean) to 1 (heavy ghosts). |
| swell | number | 0.55 | How big the swell runs, 0 (glassy) to 1 (heavy). |
| value / defaultValue | number | 200 | The bearing the display faces, in degrees (0 north, 90 east); controlled or initial. |
| onValueChange | (bearing: number) => void | — | Fires from the drag or key that turned the display, with the new bearing. |
| onRefresh | (kind: "partial" | "full") => void | — | Fires as each refresh lands on the panel. |
| fullEvery | number | 6 | Partial refreshes between full ones, which flash and clear every ghost; 0 never. |
| bands | number | 8 | How many bands a partial refresh rolls down the panel in, 3 to 16. |
| dither | "floyd" | "atkinson" | "floyd" | The error-diffusion kernel: smooth Floyd–Steinberg or crisp, lighter Atkinson. |
| dot | number | 2 | CSS px per e-paper pixel, 1 to 4. |
| chop | number | 0.35 | Wind chop on top of the swell, 0 to 1. |
| tilt | number | 0.6 | How far the device tilts toward the pointer, 0 to 1. |
| caption | boolean | true | Show the readout strip at the top of the paper. |
| paperColor | string | a warm light grey | The paper's colour, any CSS colour. |
| inkColor | string | a soft black | The ink's colour, any CSS colour. |
| label | string | "Swell display" | The display's accessible name. |
| interactive | boolean | true | The pointer tilts and turns the display. Keys still turn it. |
| paused | boolean | false | Stop refreshing: the panel keeps its last frame. |
| seed | number | 4 | Seeds the sea's phases and the clouds. |
| disabled | boolean | false | The display stops answering; it stays focusable and says so. |
| className | string | "w-72" | Sizes the device; its height follows from a 3:2 aspect. |