Every colour, size, radius and font of a canvas comes from a --hakushi-* custom property. Set them on .hakushi (or higher):

.hakushi {
  --hakushi-accent: #0f766e;
  --hakushi-background: #fafaf9;
}

The defaults have no specificity, so a value set on :root, on an ancestor or on .hakushi itself wins. They are Masu's, Kotoba's and Koyomi's own, so that a page with them all looks like one thing.

Your nodes are your HEEx, styled as the rest of your app: these properties are the canvas's own parts (the paper, the edges, the handles, the controls), and the default node.

The properties

PropertyIs
--hakushi-font, --hakushi-font-sizethe type of the canvas's own words
--hakushi-text, --hakushi-mutedtext, and quieter text
--hakushi-backgroundthe paper
--hakushi-pattern, --hakushi-pattern-strongthe pattern of the paper: the dots and crosses, and the lines (the strong one every five squares)
--hakushi-surface, --hakushi-borderthe controls, the minimap and the default node
--hakushi-accent, --hakushi-accent-textthe focus, the selection, the edge being drawn
--hakushi-node-background, --hakushi-node-border, --hakushi-node-radius, --hakushi-node-shadowthe default node; the radius is also the selection ring's
--hakushi-selectionthe ring of a selected node, and the resize handles
--hakushi-handle-size, --hakushi-handle, --hakushi-handle-borderthe handles
--hakushi-edge, --hakushi-edge-width, --hakushi-edge-selected, --hakushi-edge-labelthe edges, a selected one, and the background of the labels
--hakushi-guide, --hakushi-boxthe helper lines, and the box that selects
--hakushi-control, --hakushi-control-shadowthe controls and the minimap
--hakushi-minimap-node, --hakushi-minimap-maskthe nodes in the minimap, and the veil outside the view
--hakushi-radius, --hakushi-transitionthe controls' radius, and the speed of the small transitions
--hakushi-embed-heightthe height of an embed frame (canvas_embed/1) whose parent has none, 24rem

The kits have their own: Ink's --hakushi-ink-<colour> and --hakushi-highlighter-opacity; Shapes' --hakushi-shape-<colour> (ink, gray, blue, green, yellow, red, violet, and your own), whose fills are a tint of them on the surface (--hakushi-shape-tint, 16%); Notes' --hakushi-note-<colour> (yellow, rose, sky, mint, lilac), a tint of them on the surface (--hakushi-note-tint, 42%); Schema's primary keys and Flow's conditions, --hakushi-kit-alt; Images' --hakushi-image-radius (the corners of an image, 4px).

--hakushi-person is a person's colour, on their avatar, their cursor and the ring of the nodes they selected or drag.

The paper

background is :dots (the default), :lines (graph paper, a stronger line every five squares), :cross or :none, or a paper of your own, and grid the size of a square at 100% (20 pixels unless set). The pattern scales and moves with the view.

People choose the paper

papers puts a picker in the controls: the papers people choose from, and the size of their squares (small, medium, large: 10, 20 and 40 pixels). It is off unless you give it.

<.canvas id="board" graph={@graph} papers={true} on_change={&save/2} />
<.canvas id="board" graph={@graph} papers={[:dots, :lines, %{name: "iso", label: "Isometric"}]} />

The paper people choose is the graph's (Hakushi.Graph's paper), and choosing it is a change as any other (Hakushi.Change.paper/1): it goes through on_change, which saves it with the graph, ⌘Z takes it back, and on a shared canvas everyone sees it. It wins over the canvas's background and grid until it is taken back. Your app changes it the same way, with Hakushi.Live.change/2 or Hakushi.Issho.change/4:

Hakushi.Live.change("board", Hakushi.Change.paper(background: :lines, grid: 20))

A paper of your own

A paper is a pattern of CSS, on the canvas's paper and on its swatch in the picker. Draw it with the canvas's properties: --hakushi-grid-size (a square, at the zoom), --hakushi-pattern and --hakushi-pattern-strong; the canvas moves it with the view.

.hakushi[data-background="iso"] .hakushi-background,
.hakushi-paper-swatch[data-background="iso"] {
  background: var(--hakushi-pattern-strong);
  mask-image: url("data:image/svg+xml,...");
  mask-size: calc(var(--hakushi-grid-size) * 1.732) var(--hakushi-grid-size);
}

Name it in background (background="iso") or in papers. The development app's moodboard has one, isometric (see dev.exs); see Hakushi.Paper.

An exported image has no paper unless asked: Hakushi.Live.export/2 with paper: true, or the canvas's download({paper: true}).

Parts of the canvas

The canvas marks its states with attributes, for your own CSS:

  • .hakushi-node[data-ui-selected], a selected node;
  • .hakushi-node[data-ui-peer], a node another person selected or drags;
  • .hakushi[data-ui-connecting], an edge being drawn, and .hakushi-handle[data-ui-valid], a handle it may end on;
  • .hakushi[data-ui-dragging], nodes being dragged;
  • .hakushi[data-ui-zoom="far"], a zoom under 50%, where the handles hide;
  • .hakushi[data-ui-readonly], and .hakushi[data-ui-arrange] when people still move the nodes (readonly={:arrange});
  • .hakushi[data-tidy]:not([data-ui-tidied]), a canvas laying its nodes out as it opens (tidy), whose nodes hide behind a spinner;
  • .hakushi-edge[data-ui-faint], an edge of a node whose content has data-hakushi-faint (a Schema stub), dashed and fainter.

The handles of a node can show only on hover:

.my-node .hakushi-handle { opacity: 0; }
.my-node:hover .hakushi-handle,
.hakushi[data-ui-connecting] .hakushi-handle { opacity: 1; }

Dark mode

The canvas follows the app's own switch: a dark class or data-theme="dark" on an ancestor, as the <html> of Phoenix's and Tailwind's apps has. A page that follows the system's setting maps its media query to one of them. Set the properties for dark yourself under the same selectors.

The Sumi theme

hakushi-sumi.css sets the properties from the Sumi tokens of the page (Sumi is the design system of Hattori AI), and from a Sumi palette of its own on a page without them; it follows the Sumi themes' light and dark, or data-theme, or the system's setting. Load it after hakushi.css:

@import "../../deps/hakushi/priv/static/hakushi.css";
@import "../../deps/hakushi/priv/static/hakushi-sumi.css";

Each of Sumi's seven themes (Ink, Smoke, Stone and Lacquer; Paper, Snow and Silk) has its own accent, which selects, and the canvases follow it:

  • the dark themes have the dark palette, and the kits' dark colours;
  • a colour of the kits of the accent's hue is another in that theme, so that a selected note, shape or stroke shows: on Ink and Paper (blade) the orange pen is amber and red a crimson; on Smoke (jade) green and the mint note a leaf green; on Stone (gold) yellow lemon, and the primary keys and conditions orange; on Lacquer (vermilion) red a lighter crimson and the rose note pinker; on Snow (indigo) blue azure; on Silk (lotus pink) the rose note coral;
  • on Smoke the notes are a lighter tint, and on the light themes the primary keys a darker amber, for 4.5:1.

The development app (mix dev) shows each: ?sumi=smoke, or the links in its panel; /colours has every colour of the kits on one canvas.

Forced colours and motion

With forced colours (Windows' high contrast), the edges, handles and selection use the system's colours. With reduced motion, nothing transitions and the animated edges stand still.