Creative coding · Procedural SVG
Inkline
A procedural line-art engine that turns deterministic geometry into drawings that build themselves as you scroll.
- Role
- Design and build
- Stack
- TypeScript · SVG · Vite · Procedural geometry

- 0
- runtime dependencies
- 15.5 KB
- JavaScript, gzipped
- 2,708
- strokes across the showcase
- 1 write
- per visible scene per frame
01
The problem
Drawing thousands of SVG strokes is straightforward. Making them arrive in a deliberate order as someone scrolls—without turning every frame into thousands of JavaScript updates—is the real problem. The reveal also has to remain art-directable: an outline should establish the subject before detail, shading, and colour appear.
Inkline is a small procedural line-art engine built around that constraint. Three scene families generate an unlimited number of deterministic drawings, then the same rendering system stages every stroke whether its geometry was authored by hand or produced by an algorithm.
02
Design
The interface gets out of the drawing's way. Each showcase section becomes a long scroll surface with a sticky, full-height stage, so the artwork stays still while the visitor supplies the timeline. Restrained paper tones, imperfect linework, engraving-style hatching, and late colour washes make generated geometry feel illustrated rather than diagrammed.
The sandbox exposes the system without exposing its machinery. A visitor chooses Map, Nature, or Street, edits a human-readable seed, rerolls, plays the reveal, and saves a durable link. The visible controls describe creative choices; implementation concepts such as noise octaves, contour thresholds, and stroke windows stay behind the canvas.


03
Architecture
- 01Seed + scene type
- 02Pure geometry generators
- 03Shared Scene descriptor
- 04SVG + CSS progress
Every scene is a pure function of a seed. Hand-authored paths are sampled into point arrays at startup; procedural generators already emit points. Once geometry crosses the Scene boundary, the renderer does not know—or need to know—where a stroke came from. Analytic polyline lengths avoid layout reads, while seeded fingerprints make it possible to verify that a saved link reproduces exactly the same drawing.
The scroll driver computes one progress number for each visible scene and writes it to a registered CSS custom property. Layer windows and per-stroke delays derive their local progress in CSS, which drives stroke dash offsets and fill opacity. IntersectionObserver gates inactive scenes and one requestAnimationFrame loop handles the page, so runtime work scales with visible scenes rather than stroke count.
04
Interesting decisions
- 01
Make progress a CSS contract
Updating thousands of path styles from JavaScript on every scroll frame would make the richest drawing the slowest one.
- Choice
- JavaScript writes one registered numeric custom property per visible scene; CSS derives layer timing, stagger, dash offset, and fades from it.
- Tradeoff
- The timing math is less obvious than an imperative animation timeline, but a 2,000-stroke scene has the same hot-loop cost as a 20-stroke scene.
- 02
Normalize everything to polylines
Authored Bézier paths and generated linework need one length, jitter, rendering, and future export pipeline.
- Choice
- Sample authored paths once and require generators to emit point arrays, then calculate lengths and path data without layout-dependent SVG APIs.
- Tradeoff
- Dense curves increase path payload, but the engine gets one predictable geometry model and a clean route to a future canvas renderer.
- 03
Treat the URL as saved artwork
A save button is misleading if the same link can produce different geometry after a refresh or refactor.
- Choice
- Define a drawing as a sanitized seed plus a stable public type token, and keep generation pure and fingerprintable.
- Tradeoff
- Public tokens become a compatibility contract, but saved drawings need no database, account, or serialized path payload.
- 04
Generate one coherent world
Independently plausible coastlines, mountains, forests, and rivers still look wrong when they disagree with one another.
- Choice
- Derive the entire map from one height field, including zero-contour coastlines, separated peaks, terrain bands, and streamlines descending to the sea.
- Tradeoff
- The generator needs failure guards for flat fields, stalled rivers, and stable orbits, but the output reads as a place instead of a stack of symbols.
05
Accessibility
- Reduced-motion preferences skip the scroll animation and present every scene in its completed state, so the content never depends on motion to exist.
- The sandbox uses labelled text inputs and native buttons for scene type, seed, reroll, playback, and saving; every creative action remains available without a pointer.
- Scene titles and captions remain real document text outside the SVG, and the restrained palette adapts to the visitor's light or dark colour-scheme preference.
- Timed playback yields immediately to wheel, touch, or keyboard input, so animation never traps control of the page.
06
What I'd do differently
The strongest abstraction is the Scene descriptor between generation and rendering. A future SVG import tool becomes another producer of that format, while scroll timing, playback, sharing, and reduced-motion behavior remain untouched.
The next pass would add property-based generation tests across a large seed corpus and screenshot regressions at fixed progress values. If a scene eventually outgrows DOM SVG, I would add a canvas renderer behind the same descriptor rather than change the generator contract.
Have a similar project?