Catalog / Technique
Generative mark
Use when a page wants a drawing made by a program rather than by hand or by shader: an emblem per project, a divider, a figure beside a heading, a share image. Covers making it at build time and shipping an SVG, seeding from the slug, making marks differ in kind, writing the smallest path, where a mark may sit, and drawing it on once.
- Loads as
technique-generative-mark- Written for
- An agent, to follow
Generative mark
A drawing made by a small program: lines that follow a field, the contour lines of a made-up terrain. The program runs once, in the build, and what ships is an SVG. On the page it is a picture: no script, no frames, no GPU, sharp at any size, printable. It is the one creative-coding family whose rest state is the whole thing.
When to use
- An emblem per project or per track, seeded from the page’s slug, so every page has a mark of its own and both languages share it.
- A section divider or a quiet figure beside a heading, where the task is scanning and the page can afford a few kilobytes of picture.
- The still that stands in for a shader where there is no GPU.
- A share image, rendered to a bitmap at build time.
Do not use it for:
- A ground under body text. Thin lines at the spacing of small type destroy legibility. Tastes is firm and right.
- Anywhere a real image of the project exists and would say more.
- Beside a real chart or map, where fake cartography reads as data.
- Generating in the browser on a reader’s page. It costs the script, the main-thread time and determinism across engines, and buys nothing a build cannot give. A lab page, where the reader turns the dials, is the exception.
Rules
- Build time first. The generator is called from a component script or a build step and is stripped from the page. What reaches the browser is the SVG.
- Seed from the route slug. Not the title, not the locale. A slug with Turkish letters hashes differently from its ASCII spelling, and both language versions of a page must get one mark.
- The seed chooses structure, not only noise. Eight marks that differ only in their noise seed read as eight prints of one thumb (seen, 2026-10-06). The seed must reach the dials that change the silhouette: frequency, turns, density, the kind of field. Better still, let a true fact choose the kind, on the consumer’s side: the track picks the field, the year picks the density. Then the mark encodes something and can be said to.
- Simplify, relative coordinates, one decimal, one path. The same drawing went from 40 kB to 5 kB gzipped by those four (measured). Simplification is the largest saving and SVGO does not do it. One path with many subpaths also removes dozens of nameless graphics elements from the accessibility tree and lets the whole mark draw on with one animation.
fill="none",stroke="currentColor",aria-hidden="true". The mark takes the colour of the text around it and says nothing to a reader. For a second colour, a class on the root and a rule in the skin; the generator stays ignorant of tokens.- Generate for the display size. A stroke scales with the drawing;
a mark designed at 520 px and shown at 150 px washed out, and with
non-scaling-strokeit became a solid mass (seen). One unit is one CSS pixel at the size the mark is shown, the spacing is chosen in those pixels, and a spacing under about 6 stops reading as lines. - A mark has its own box. A mark in a box beside a heading is a drawing. The same lines bleeding off every edge are wallpaper. A shape, a margin, a place in the layout.
- Inline when small and alone, a file when large or many. Up to
about 3 kB gzipped per mark and 8 kB per page inline; above that,
or a listing with an emblem per row, a file through
imgwithalt="", themed as amask-imageovercurrentColorif it must follow the text colour. - Pin one mark’s hash in a test, or commit the output. The noise
and the tracing use
Math.sin,Math.cosandMath.atan2, whose last bits are not promised across engines. A Node upgrade can reroute a line. Pinned, the change is noticed; committed, the build does not depend on the generator at all. - Clamp every dial, and cap the walk. A spacing of nearly zero or a box of a million units must return, not hang a build.
- If it draws on: once, linear, nothing hidden outside the animation, not at all under reduced motion. The dash exists only inside the animation, with a backwards fill for the delay; before and after it, the mark is as styled. One mark a page at most; a listing of drawn-on marks is a dozen animations at once.
- Do not claim a mark encodes data unless it does. A mark whose
kind follows the track is fine unexplained, since nothing is lost
by missing it. A mark presented as data needs the data as text
beside it. Information is never hidden in something
aria-hidden. - What reads as craft. One stroke weight, round ends. Even spacing that comes from the method. Paper showing: tone from line density, never from opacity or fill. A visible rule the reader can sense. A frame. A reason. One ink, perhaps one line in the accent.
- What reads as stock. Noise-angle lines from random starts, crossing and clumping. Contour lines at low opacity behind a headline. Gradients, glow, blur, several colours, varying stroke. A loop: a plotter drawing is finished. The same mark on every page. Wobble filters and paper textures: ink that skips and pools is what a screen cannot fake, so do not try.
Decisions
| Question | Choose | Because |
|---|---|---|
| Where a generator lives | One recipe, two ways in: generate for a build, a thin mount for the lab |
Tuning by eye is the work and the catalog is the tool; no check or stage code changes |
What generate returns |
A string | It is what a build inlines |
| Committed, or made on every build | Made on every build, one hash pinned | Committing adds generated files to review; pinning notices a change |
| Does the seed choose structure | Yes, a toggle, on | Seed-only marks are indistinguishable |
| Line or fill | Line | Fills are larger, need a palette, and are a different taste |
| Still, or drawn on | Still; drawing on is a separate recipe for one mark a page | Drawn-on marks in a listing are a dozen animations |
| Pen speed | One speed: every line at the same rate, short ones finish first | It is what a pen does and it needs one element |
| Index contours | Off, a toggle | Varying stroke leaves the taste; it is real cartographic convention |
| Inline or file | Inline for one mark on a page; files for a listing | Tens of kB in a listing’s HTML otherwise |
Tuning
| Dial | Start | Range | One end | The other |
|---|---|---|---|---|
| Spacing, in CSS px at display size | 10 | 6 to 24 | A texture; over 10 kB | A few stray lines; reads as unfinished |
| Test distance, as a share of spacing | 0.5 | 0.3 to 0.9 | Long lines that pinch together | Short broken lines, gaps |
| Field frequency, cycles across the width | 1.2 | 0.5 to 3 | One broad sweep | Busy curls, many short lines, a bigger file |
| Turns | 1 | 0.35 to 2.5 | Nearly parallel, combed | Tight whorls, lines crowd into sinks |
| Octaves | 2 | 1 to 3 | Very smooth | Wobbling lines |
| Minimum extent, in spacings | 2 | 1 to 6 | Stubble | Holes where short lines were |
| Contour levels | 12 | 3 to 30 | A few rings | A dense map |
| Stroke width, in CSS px | 1 | 0.75 to 1.5 | Disappears on a 1x screen | Stops reading as a pen |
| Simplify tolerance, in units | 0.25 | 0.1 to 0.6 | A larger file | Visible corners unless smoothed |
| Draw-on duration (ms) | 1,600 | 600 to 4,000 | A flick | The reader waits for a picture |
Ranges are estimates from stills; halving the spacing doubles the lines and the bytes.
Rest state
The mark. The same without script, under reduced motion, without
WebGL, in print, and after a draw-on has ended. A .svg file that
fails to load is an empty box with alt="", announcing nothing.
Cost
- To build: the algorithms took an hour each; every later minute was dials. Tuning is the work.
- To run: in the browser, parsing and one paint. On the page, 1.5 to 6 kB gzipped a mark at the densities that look right, which is a small image. In the build, 5 to 30 ms a mark on a laptop. A draw-on repaints the path on the main thread each frame for its duration, which is small for one mark and unmeasured.
- To the reader: nothing, unless it sits under their text.
Checklist
- The budget check passes, and the recipe’s own test pins one mark’s hash and holds the default mark under a stated size.
-
npm test: the same seed gives the same bytes; absurd dials return in bounded time. -
npm run check:browser: each box on the stage holds onesvgwith onepath, sized to its box, hidden from readers; a changed seed changes every mark; a draw-on held mid-way is half drawn, and finished leaves no animation and no style. - Looked at on the stage: every version, three seeds each, at the emblem, the divider and the figure; and once at phone size.
- The mark is beside the text on the fixture, never under it.
- Run once in Safari and once in Firefox before it is marked passed.
Recipes
flow-mark: evenly spaced streamlines of a seeded field, as one path; the seed chooses the kind.contour-mark: the contour lines of a seeded height field, joined without comparing coordinates.draw-on: the strokes of any inline SVG drawn in once, on first view, with nothing left behind.
Sources
tyler-hobbs-flow-fields: the method, and the warning that noise mapped to angle is overdonejobard-lefer-streamlines: evenly spaced streamlines, the two distances, the collision grid, seeding from what is drawnanvaka-streamlines: a JavaScript implementation of the same method, read for its parameters, not copiedwikipedia-marching-squares: contour extraction, the sixteen cases, the ambiguous pairwikipedia-ramer-douglas-peucker: polyline simplificationbridson-poisson-disk: even random points, for a stipple not built herebryc-prngs: small seedable generators and seeding from a string hashmattdesl-pen-plotter: the plotter as a taste, and what physical ink addsgithub-identicons: the production precedent for a mark from a hash of an identifierwai-decorative-images: a decorative image takes an empty altroselli-dont-split-words: dozens of nameless graphics elements are the split-text hazard in another materialmdn-element-animate: the draw-on’s one callmdn-prefers-reduced-motion: decided in script, in one placeastro-docs-components: a component script runs at build time and is stripped from the pagebcd-svg-path:patheverywhere; read forpathLength, which it does not listbcd-svg-global-attributes:stroke-dasharrayandstroke-dashoffseteverywherebcd-mask-image: theming a file through a mask