Takumi

Drawing

Place nodes with their transform, then replay the steps.

A renderer reads two things from the tree: where each node sits, and the draw order.

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 rectangle on the canvas. 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 handles z-index and negative stacking. Outlines that paint after later siblings are in place too.

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
strokeStroke shape with stroke and paint
shadowBlur a copy of shape, filled or stroked with stroke, moved by offset, visible only outside or inside its box
glyphsFill the run's outline with paint, blurred by blur, or stroke it when stroke is set
imageDraw image into rect, clipped to clip
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. It is half the CSS blur radius.

Every drawable 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

Gradient stops interpolate in sRGB. Takumi adds stops where the CSS color space would differ. Oklab is the CSS default.

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

Background and mask layers arrive as pattern paints. A pattern draws one tile at every x and y, 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, before the node draws.

Last updated on

On this page