Skip to content
Corey Fedde
← All work

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
Inkline's completed Rue Sainte-Anne drawing, a seeded two-storey shopfront with an awning, windows, cafe furniture, and plants
Rue Sainte-Anne mixes authored architectural paths with seeded layout, foliage, glass shading, and facade details.
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.

Inkline's completed Terra Incognita scene showing a generated island with coast contours, mountains, rivers, forests, and a compass rose
One seeded height field drives the coast, sea contours, summits, forest bands, and downhill rivers, so every mark agrees on the same terrain.
Inkline sandbox midway through drawing a branching botanical scene, with type, seed, reroll, play, and save-link controls
A timed playthrough makes iteration immediate while the seed and type in the address bar remain the durable source of truth.

03

Architecture

  1. 01Seed + scene type
  2. 02Pure geometry generators
  3. 03Shared Scene descriptor
  4. 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

  1. 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.
  2. 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.
  3. 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.
  4. 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?

Let’s make the complicated part feel simple.

Start a conversation