Skip to content
kerfjsGitHub

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/veneer

Usage

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

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

PropTypeDefaultDescription
layoutStageLayout'box'box paints its own block, scroll is a scroller and paints its view, page paints the viewport.
childrenunknown—What it contains.
rampRamp—The field's inks, and the ramp of pieces that name none.
kindFieldName | Field—The ground: a named field or your own function of (x, y, t).
groundnumber—How strong the field under the pieces is, 0 to 1. 0 leaves only the pieces.
patternPattern—The dither: an ordered Bayer matrix (bayer2, bayer4, the default, bayer8), blue noise, or halftone dots.
cellnumber—CSS pixels per cell. Default 3.
speednumber—Multiplies the tier's speed. 0 holds the ground still; pieces still answer.
driftnumber—How far the field travels for each pixel scrolled: parallax.
railbooleanfalseA dithered scroll rail on the right, in place of the native bar. Drag it to scroll.
veilnumber—A dithered scrim over everything under the pieces, this dense: for a modal.
ripplefalse | Ramp—The inks a press rings out in, or false for none.
paintersRecord<string, DitherPainter>—Your own skins, drawn with the brush in tones 0 to 1 that the stage turns into inks.
fpsnumber—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