Takumi

Drawing

Place nodes with their transform, then replay the steps.

A renderer reads where each node sits and the order to draw it in.

Place a node

Each node has a local space from (0, 0) to (width, height). Its drawables use that space. node.transform maps the space onto the canvas.

Pick a rotation below. The drawables stay the same. Only the transform changes.

xy
width
160
height
72
transform
[0.87, 0.5, -0.5, 0.87, 76.72, 12.82]
bounds
40.72, 12.82, 174.56 × 142.35

node.bounds is the node's bounding rectangle on the canvas, so it grows when the node rotates.

Canvas takes the matrix in the same order: ctx.setTransform(...node.transform).

NodeLocal space
boxIts border box
textThe border box of the block its lines fill
imageIts content box

Replay the steps

tree.steps lists every step, bottom to top. The demo draws the card from Paint JSX one step at a time.

The order already accounts for z-index, including negative values. An outline that paints after later siblings has its own draw step in the right place.

StepDo
drawDraw drawables under node.transform
begin-group / end-groupDraw the steps between them into a layer, then blend
begin-clip / end-clipClip the steps between them to clip, in node space

This loop replays the steps onto a canvas:

replay.ts
import type { , PaintTree } from "takumi-paint";

declare function (: , : ): void;
declare function (): ;

function (: , : PaintTree) {
  const : [] = [];
  let  = ;

  for (const  of .) {
    switch (.) {
      case "draw":
        .(.....); 
        for (const  of .) (, ); 
        break;
      case "begin-clip":
        .();
        .(.....);
        // Clip to step.clip.
        break;
      case "end-clip":
        .();
        break;
      case "begin-group":
        .(); 
         = (); 
        break;
      case "end-group": {
        const  = .() ?? ;
        // Filter, clip, and mask the layer per step.node.effects, then blend it into parent.
        .();
        . = ..?. ?? 1;
        .(., 0, 0); 
        . = 1;
         = ;
        break;
      }
    }
  }
}

A group needs its own layer. Setting globalAlpha per draw darkens the spots where draws overlap.

Draw a drawable

TypeDraw
fillFill shape with paint, clipped to every shape in clips. Blend with blendMode when set.
strokeStroke shape with stroke and paint, clipped to every shape in clips
shadowBlur a copy of shape, filled or stroked with stroke, moved by offset, visible only outside or inside its box
glyphsFill the run's outlines with paint, moved by offset and blurred by blur. Stroke them instead when stroke is set.
imageDraw image into rect, clipped to clip, sampled per sampling
maskedDraw content into a layer, keep it only where mask covers, then composite it
groupDraw drawables into a layer, then composite it at opacity

A shadow or glyphs blur is a Gaussian standard deviation, half the CSS blur radius. A glyphs drawable with a blur is a text shadow.

Every drawable except group has a role, such as background, border, or text. Exporters use it to rebuild editable objects. For example, a slide writer can merge a box's background, border, and shadow into one shape.

Read paints

Interpolate gradient stops linearly in sRGB. Takumi adds stops wherever the CSS interpolation color space, Oklab by default, would differ.

A repeating gradient has spread: "repeat". Its stops cover one period, as with SVG's spreadMethod. Canvas gradients only pad, so repeat the stops across the area yourself.

Background and mask layers arrive as pattern paints. A pattern draws one tile at every x and y pair, clipped to area.

Apply effects

A box with effects composites as a group. Apply them in this order:

  1. Render the group's steps into a layer.
  2. Run filters over it.
  3. Clip it to clip, and multiply it by the alpha of mask.
  4. Blend it with opacity and blendMode.

backdropFilters apply to what is already drawn behind the node. They run inside backdropClip, the node's border box, before the node draws.

Last updated on