A seal pressed into the paper. Vermilion paste catches the crests of the grain first and fills the valleys as the hand presses harder; one side prints darker where the hand leaned; old rubber leaves nicks; a stamp that rocked leaves a faint second strike beside the first.
import { PAPERS, stampShape, WetGpu } from "shodo/paint";
const sheet = new WetGpu(canvas, 600, 400, { paper: PAPERS.coldPress });
// Draw the seal in dark ink on any canvas, then take its pixels.
const art = new OffscreenCanvas(160, 160);
const g = art.getContext("2d");
g.lineWidth = 10;
g.strokeRect(10, 10, 140, 140);
g.font = "900 60px serif";
g.fillText("書道", 20, 105);
const seal = stampShape(160, 160, g.getImageData(0, 0, 160, 160).data);
sheet.stamp(seal, 220, 120, { pigment: { color: [0.92, 0.25, 0.16], granulation: 0.1 }, feel: { ink: 0.8, wear: 0.3 } });
sheet.show();
The ink settles into the paper where it lands, as seal paste does: it does not run, and washes painted over it later glaze on top. A clean wet brush lifts some of it, as it lifts any dried paint, how much set by the sheet’s staining.
Feel
Five dials, each from 0 to 1. A dial you don’t set stays where it rests: ink 0.7, pressure 0.6, wear 0, slip 0, squeeze 0.3, which is a fresh seal pressed firmly.
inkstarvedloadedHow much the pad gave: a starved print breaks up on the paper’s grain, a loaded one fills it.
pressurekissheavyHow hard the hand pressed, leaning so one side prints darker.
wearfreshwornHow old the rubber is: nicks and soft patches that print pale.
2 more dials
slipstillrockedHow much the stamp rocked as it lifted, leaving a faint second strike.
squeezecleanrimmedHow much ink was pushed to the edges of the relief, drawing a darker line round it.
The same words live in STAMP_FEEL. seed picks the stamp’s wear and which way the hand leans; the same seed prints the same way, so pass a new one for each press to make a sheet of seals that are not copies of each other.
Things worth knowing
- The paper decides a lot. On
coldPressa starved seal breaks up into the grain; onlaidthe same seal prints almost solid. See Paper. - Draw the shape in dark ink: coverage is alpha times darkness, so white prints nothing. For a carved-out seal (the character left as paper), fill the face and cut the character out of it.
- One stamp, one pigment. For a two-colour seal, press twice with two shapes.
From a picture
Any picture can be a stamp. A rubber stamp is one raised layer, inked or not, so a photo is carved first, in one of three styles:
| Style | What you see |
|---|---|
carve | A bold silhouette: the dark parts raised, cut where the picture’s own light and dark split. |
dots | A halftone screen: dots that grow with the dark, so the picture’s tones survive. |
lines | The picture’s strongest edges as lines, like a woodcut drawn from it. |
import { stampFromImage } from "shodo/paint";
const bitmap = await createImageBitmap(file);
const g = new OffscreenCanvas(200, 150).getContext("2d");
g.drawImage(bitmap, 0, 0, 200, 150);
const shape = stampFromImage(200, 150, g.getImageData(0, 0, 200, 150).data, { style: "dots", detail: 0.7 });
sheet.stamp(shape, 100, 80, { pigment: vermilion });
detail runs from bold (0: soft, wide marks, big dots) to fine (1), resting at 0.5. negative: true swaps raised and cut, leaving the picture as paper in an inked block. Transparent pixels are paper. It all runs where the page runs: the picture never leaves the device. In the wet lab, choose a picture (or drop one anywhere), pick a style and press.
Type as material
Text can be a stamp too, so type sits in the paper and paint goes over it: a wash runs across the letters, clear water lifts them pale, a second coat covers them. Once pressed it is pigment in the sheet, not a layer on top.
import { stampFromText } from "shodo/paint";
await document.fonts.load("900 40px 'Noto Serif JP'");
const shape = stampFromText("the 8:14 to
Kyoto, platform 3", { family: "'Noto Serif JP', serif", size: 40, weight: 900, width: 360 });
sheet.stamp(shape, 140, 200, { pigment: ink });
| Option | What it does | Rests at |
|---|---|---|
family | CSS font family list. Load the font first. | serif |
size | Type size in cells (sheet pixels). | 48 |
weight | CSS font weight. Heavy type prints better. | 700 |
leading | Line height as a multiple of size. | 1.15 |
width | Wrap width in cells. Without it, lines break only at newlines. | none |
align | left, center or right. | left |
mirror | Flip the face, as on a real stamp. | false |
The stamp’s feel applies as with any stamp: starved ink breaks the letters up on the grain, wear nicks them. wrapText(text, measure, width) is the line breaker on its own. In the wet lab, set the seal to type, write in the type field (a / starts a new line) and press.
Underneath: the ink model
stampPrint(shape, x, y, width, height, tooth, feel, seed) returns the amount of pigment each cell gets, an amount of 1 showing the pigment’s own colour. WetSurface.stamp and WetGpu.stamp both settle exactly that amount, so the CPU and GPU sheets print the same seal. Use it directly to draw stamps somewhere other than a wet sheet.