DitherStage
A CanvasStage in the dithered look: every skin drawn in whole cells of the theme's inks.
SourcePackage @kerf-js/veneer
Preview
tsx
import { DitherStage, Grid, Row, Stack, StageSelect} from '@kerf-js/veneer'export function StageSkins() { return ( <DitherStage kind="drift" ground={0.35} style="padding:28px" > <Skins group="look-dither" /> </DitherStage> )}function Skins(props: { group: string }) { return ( <Grid min="md" cols={2} gap="group" > <Stack gap="related" data-skin="card" > <Row gap="related" wrap > <button data-skin="button">Deploy</button> <button data-skin="button" data-look="outline" > Preview </button> <button data-skin="button" disabled > Paused </button> </Row> <label data-part="label"> Project <input data-skin="field" value="storefront" /> </label> <label data-part="label"> Region <StageSelect options={['eu-west', 'us-east']} /> </label> </Stack> {skinControls(() => props.group)} </Grid> )}function skinControls(group: () => string) { return ( <Stack gap="related" data-skin="card" > <label data-part="option"> <input type="checkbox" data-skin="check" checked /> Preview every push </label> <Row gap="group"> <label data-part="option"> <input type="radio" name={group()} data-skin="check" checked /> Node </label> <label data-part="option"> <input type="radio" name={group()} data-skin="check" /> Bun </label> </Row> <label data-part="option"> <input type="checkbox" role="switch" data-skin="switch" checked /> Deploy on push </label> <label data-part="label"> Build minutes <input type="range" data-skin="range" value="60" /> </label> <Row gap="tight" wrap > <button data-skin="chip" aria-pressed="true" > Live </button> <button data-skin="chip" aria-pressed="false" > Building </button> </Row> </Stack> )}Installation
npm install @kerf-js/veneerUsage
tsx
import { DitherStage } from '@kerf-js/veneer'<DitherStage kind="drift" ground={0.35} style="padding:28px"> <Skins group="look-dither" /></DitherStage>Hybrid and your own
Anything without a skin paints itself over the stage, so Veneer components and canvas pieces share a page.
Veneer components on a stage
The card on the left is veneer's; the one on the right is drawn in cells, with dithered type and a live art field.
tsx
import { Badge, Button, Card, DitherStage, Row, Split, Stack, Text} from '@kerf-js/veneer'export function StageHybrid() { return ( <DitherStage kind="flow" ramp="info" style="padding:28px" > <Split side="end" width="list" gap="group" > <Card title="storefront" eyebrow="Live in eu-west" > <Stack gap="related"> <Text subtle> 412 deploys, 99.98% uptime, 84 ms at p95. </Text> <Row gap="related"> <Button>Open</Button> <Button emphasis="outline" tone="neutral" > Logs </Button> <Badge tone="positive">Healthy</Badge> </Row> </Stack> </Card> <Stack gap="related" data-skin="card" > <h3 data-skin="type" style="font-size:28px" > Drawn in cells </h3> <div data-skin="art" data-kind="rings" role="img" aria-label={[ 'Rings spreading over a ', 'dithered field', ].join('')} ></div> <button data-skin="button">Promote</button> </Stack> </Split> </DitherStage> )}A skin of your own
painters maps a skin to a function that draws its piece with the brush, in tones that the stage dithers. This battery reads a <meter>.
tsx
import { state } from '@kerf-js/core'import { DitherStage, LEVEL, Stack} from '@kerf-js/veneer'import type { DitherPainter } from '@kerf-js/veneer'export const StageOwnSkin = () => { const charge = state(64) return ( <DitherStage ground={0.25} painters={PAINT_BATTERY} style="padding:28px" > <Stack gap="related" align="start" > <meter data-skin="battery" data-ramp="positive" min="0" max="100" value={charge.value} aria-label={`Battery at ${charge.value}%`} style="inline-size:180px;block-size:72px" ></meter> <label data-part="label"> Charge {charge.value}% <input type="range" data-skin="range" data-ramp="positive" value={charge.value} onInput={(e: Event) => (charge.value = +( e.currentTarget as HTMLInputElement ).value) } style="inline-size:240px" /> </label> </Stack> </DitherStage> )}const battery: DitherPainter = (p, b) => { const nub = Math.max(2, Math.round(p.h / 4)), body = p.w - nub - 1, bars = 5, gap = 1, inner = body - 4, bar = (inner - gap * (bars - 1)) / bars b.ring( { x: p.x, y: p.y, w: body, h: p.h, r: 2 }, LEVEL.ink, ) b.paint( { x: p.x + body, y: p.y + nub, w: nub, h: p.h - nub * 2, }, LEVEL.ink, ) for (let i = 0; i < bars; i++) { const lit = p.value * bars - i b.paint( { x: p.x + 2 + i * (bar + gap), y: p.y + 2, w: bar, h: p.h - 4, }, lit >= 1 ? LEVEL.tone : lit > 0 ? lit * LEVEL.tone : LEVEL.soft * p.hover, ) }}const PAINT_BATTERY = { battery }A modal
A native <dialog> with its own stage: veil dithers over the page, and the panel rises in each time it opens.
tsx
import { DitherStage, Row, Stack } from '@kerf-js/veneer'export const StageModal = () => { let dialog: HTMLDialogElement | undefined return ( <DitherStage ground={0.3} style="padding:28px" > <button data-skin="button" onClick={() => dialog?.showModal()} > Rename project </button> <dialog ref={(n) => (dialog = n)} aria-label="Rename project" > <DitherStage layout="page" veil={0.5} ground={0} onClick={(e: MouseEvent) => e.target === e.currentTarget && dialog?.close() } > <form method="dialog" data-skin="panel" > <div data-skin="bar"> <span>Rename project</span> </div> <Stack gap="related"> <label data-part="label"> Name <input data-skin="field" value="storefront" autofocus /> </label> <Row gap="related" justify="end" > <button data-skin="button" data-look="outline" value="cancel" > Cancel </button> <button data-skin="button" value="save" > Save </button> </Row> </Stack> </form> </DitherStage> </dialog> </DitherStage> )}When to use
Use it for
- A page or section in the dithered look, ready made
Reach for something else
- CanvasStage, when you want another look, or your own
- DitherField, when only the background should move
Accessibility
- Everything CanvasStage promises holds: real controls, plain borders before it paints and under forced colours.
- Reduced motion holds the field still and drops the dissolves and rings; every state still shows.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
layout | StageLayout | 'box' | box paints its own block, scroll is a scroller and paints its view, page paints the viewport. |
children | unknown | — | What it contains. |
ramp | Ramp | — | The field's inks, and the ramp of pieces that name none. |
kind | FieldName | Field | — | The ground: a named field or your own function of (x, y, t). |
ground | number | — | How strong the field under the pieces is, 0 to 1. 0 leaves only the pieces. |
pattern | Pattern | — | The dither: an ordered Bayer matrix (bayer2, bayer4, the default, bayer8), blue noise, or halftone dots. |
cell | number | — | CSS pixels per cell. Default 3. |
speed | number | — | Multiplies the tier's speed. 0 holds the ground still; pieces still answer. |
drift | number | — | How far the field travels for each pixel scrolled: parallax. |
rail | boolean | false | A dithered scroll rail on the right, in place of the native bar. Drag it to scroll. |
veil | number | — | A dithered scrim over everything under the pieces, this dense: for a modal. |
ripple | false | Ramp | — | The inks a press rings out in, or false for none. |
painters | Record<string, DitherPainter> | — | Your own skins, drawn with the brush in tones 0 to 1 that the stage turns into inks. |
fps | number | — | Caps the frame rate while it animates. Uncapped by default, at the display's rate. |
Parts
Every element carries a stable name, so you can style it from your own CSS: [data-scope="stage"] [data-part="paint"]
paint
RelatedCanvas stageDither field