Skip to content
Sample of Plotter Trajectories (Hébert)

generative_pioneers · hebert_plotter_trajectory

Plotter Trajectories (Hébert)

Jean-Pierre Hébert's plotter-trajectory idiom — parametric Lissajous-like curves drawn over multiple passes, each pass slightly phase-shifted, each segment modulated by the local velocity of the pen so that slow regions lay down heavier pigment and fast regions trace a ghost line. Hébert's practice (begun on an HP 9830 in 1974, first plotter in 1978) is the inverse of screen-rendering: the algorithm is a 'score' that the plotter executes in a single unbroken movement, and the ink flow — variable, semi-physical — is part of the piece. The visual result is a dense calligraphic vortex that sits between gesture and computation.

Original: https://digitalartmuseum.org/hebert/practice.html

▶ Try the live demo (JS port) ▶ Run reference.py via Pyodide 10 formulas have instant JS ports. The Pyodide path runs the actual Python in your browser — first load is ~6 MB, then instant.

Family
generative_pioneers
Source
generative pioneers
Evolution
static_arrangement
Render
ink_lines_on_paper
Complexity
medium
Agents
0
Year
1978
Author
Jean-Pierre Hébert

Math essence

Each curve is x(t) = cx + r(t)·cos(2π·b·t)·cos(2π·a·t + δ) − r(t)·sin(2π·b·t)·sin(2π·a·t + δ)·½ (and y symmetric), with r(t) a slow radial modulation. Local pixel alpha ∝ 1 / (1 + speed / v_ref), so the line darkens where the pen slows.

Parameters

Name Type Default Range Description
dim int 512 200–1200 Square canvas side length
n_passes int 3 1–8 Passes over the same area (each phase-shifted)
n_curves int 18 1–80 Curves drawn per pass
length float 12000 1000–60000 Polyline samples per curve
a float 3 1–12 Lissajous-x frequency
b float 4 1–12 Lissajous-y frequency
delta float 1.5707963 0–6.28318 Phase offset (radians)
d_phase_per_pass float 0.05 -0.5–0.5 Phase increment per pass
velocity_amp float 0.45 0–1 How much velocity modulates alpha
thickness float 1 0.5–3 Ink line thickness
bg_color tuple 245,240,230 null–null Paper RGB
ink_color tuple 20,20,30 null–null Ink RGB
seed int null 0–999999 RNG seed

Reference

First 60 lines of reference.py. Full source: library_external/generative_pioneers/hebert_plotter_trajectory/reference.py

from __future__ import annotations

from typing import ClassVar

import imageio.v2 as imageio
import numpy as np


class HebertPlotterTrajectory:
    family: ClassVar[str] = "generative_pioneers"
    sketch: ClassVar[str] = "hebert_plotter_trajectory"

    def __init__(
        self,
        dim: int = 512,
        n_passes: int = 3,
        n_curves: int = 18,
        length: float = 12000.0,
        a: float = 3.0,
        b: float = 4.0,
        delta: float = 1.5707963,
        d_phase_per_pass: float = 0.05,
        velocity_amp: float = 0.45,
        thickness: float = 1.0,
        bg_color: tuple[int, int, int] = (245, 240, 230),
        ink_color: tuple[int, int, int] = (20, 20, 30),
        seed: int | None = None,
    ) -> None:
        self.dim = dim
        self.n_passes = n_passes
        self.n_curves = n_curves
        self.length = length
        self.a = a
        self.b = b
        self.delta = delta
        self.d_phase_per_pass = d_phase_per_pass
        self.velocity_amp = velocity_amp
        self.thickness = thickness
        self.bg_color = np.array(bg_color, dtype=np.uint8)
        self.ink_color = np.array(ink_color, dtype=np.uint8)
        self._seed = seed
        self.rng = np.random.default_rng(seed)
        self.canvas = np.full((dim, dim, 3), self.bg_color, dtype=np.uint8)
        self.time = 0
        self._dirty = True

    def reset(self, seed: int | None = None) -> None:
        if seed is not None:
            self._seed = seed
        self.rng = np.random.default_rng(self._seed)
        self.canvas = np.full((self.dim, self.dim, 3), self.bg_color, dtype=np.uint8)
        self.time = 0
        self._dirty = True

    def _plot_one_curve(
        self,
        a: float,
        b: float,
        delta: float,
        length: float,

Try running it locally

The reference implementation is self-contained Python with numpy. Install the package and run:

python library_external/generative_pioneers/hebert_plotter_trajectory/reference.py

Need help? See Learn for the standard environment setup, or Playground for an in-browser sketchbook.

More from generative pioneers

Sample of Interruptions (Molnár)

Interruptions (Molnár)

generative_pioneers · molnar_interruptions

Vera Molnár's 1968 'Interruptions' series: a regular grid of rectangle cells where each cell's four border edges are drawn independently, each with its own random probability of being omitted.

View →
Sample of Line Arrangements (Mohr)

Line Arrangements (Mohr)

generative_pioneers · mohr_line_arrangement

Manfred Mohr's 1969 plotter 'Line Arrangements': a fixed grid of columns and rows, each carrying a small set of nearly-parallel lines whose exact positions are random within the cell, overlaid with a layer of 45° diagonals whose length, position and orientation are independently random.

View →
Sample of Paint by Numbers (Haeberli)

Paint by Numbers (Haeberli)

generative_pioneers · haeberli_painterly

Paul Haeberli's 1990 SIGGRAPH 'Paint by Numbers' — the founding paper of stroke-based rendering.

View →
Sample of Walk-through-Raster (Nake)

Walk-through-Raster (Nake)

generative_pioneers · nake_walk_sequence

Frieder Nake's 1966 'Walk-through-Raster' program (Algol 60 on a Telefunken TR4, plotted on a ZUSE Graphomat Z64): a repertoire of six signs — empty, vshort, vlong, hshort, hlong, dot — is sampled into a Markov chain whose transition probabilities vary with chain position so that 'vertical strokes are high at the chain's middle, empties dominate at the ends'.

View →

See all generative_pioneers artworks →