an.stage.path_wobble#

A hand-drawn wobble for stroked paths, applied at compile time (an#161).

A ruled line reads as a diagram; a line that wanders a little reads as drawn by hand. The wobble displaces the FLATTENED polyline sideways (along its normal) by a smooth seeded noise, before it reaches the wire, so the runtime and its parity spec (an.stage.path_geometry) never learn about it: trim, dashes and arrowheads work on the wobbly polyline exactly as on any other.

  • Deterministic everywhere. The noise is value noise over knots one wavelength apart, each knot’s value read from sha256(seed:k), joined by a cubic smoothstep: only + - * /, floor and sqrt, no trigonometry, so the compiled points are the same bytes on every machine.

  • Seeded by the entity ("<entity id>:<wobble_seed>"), like a character’s blink phase: two arrows sharing one document wobble differently, and the same arrow wobbles the same way every render.

  • The ends stay put. The amplitude ramps up over the first and down over the last wavelength, so a route still starts and ends where it was drawn to, and an arrowhead’s tip lands on its target.

  • Static. One displacement per path. A boil (a wobble that changes on twos) is several displaced polylines on a step channel; not built.

>>> pts = wobble_polyline([(0.0, 0.0), (200.0, 0.0)], amplitude=4.0, wavelength=50.0, seed="route:0")
>>> pts[0], pts[-1]  # the ends are where they were
((0.0, 0.0), (200.0, 0.0))
>>> 0.0 < max(abs(y) for _, y in pts) <= 4.0
True
>>> pts == wobble_polyline([(0.0, 0.0), (200.0, 0.0)], amplitude=4.0, wavelength=50.0, seed="route:0")
True

Module Attributes

WOBBLE_SAMPLES_PER_WAVELENGTH

enough that the wobble reads as a curve, not a zig-zag, at any amplitude a stroke should take.

MAX_WOBBLE_POINTS

The most points a wobbled path may carry.

Functions

wobble_polyline(points, *, amplitude, ...)

points resampled and wobbled; see wobble_with_vertices().

wobble_with_vertices(points, *, amplitude, ...)

points resampled every wavelength / 8 px and each sample moved up to amplitude px along the path's normal by seeded smooth noise.

wobble_point_count(length, wavelength)

How many samples a wobble of wavelength puts along length px.

an.stage.path_wobble.MAX_WOBBLE_POINTS: int = 20000#

The most points a wobbled path may carry. Every point is redrawn each frame a trim or dash moves; past this the wavelength is too short for the path’s length (a hair-fine wobble over a long route).

an.stage.path_wobble.WOBBLE_SAMPLES_PER_WAVELENGTH: int = 8#

enough that the wobble reads as a curve, not a zig-zag, at any amplitude a stroke should take.

Type:

Samples per wavelength of the noise

an.stage.path_wobble.wobble_point_count(length, wavelength)[source]#

How many samples a wobble of wavelength puts along length px.

Return type:

int

an.stage.path_wobble.wobble_polyline(points, *, amplitude, wavelength, seed)[source]#

points resampled and wobbled; see wobble_with_vertices().

Return type:

list[tuple[float, float]]

an.stage.path_wobble.wobble_with_vertices(points, *, amplitude, wavelength, seed)[source]#

points resampled every wavelength / 8 px and each sample moved up to amplitude px along the path’s normal by seeded smooth noise.

The original vertices are kept as samples (a corner stays a corner) and move along the bisector of their two legs. Raises ValueError when the result would carry more than MAX_WOBBLE_POINTS points.

Also returns, for each input point, its index in the output (where a vertex landed), so a caller can find the authored points on the wobbled line (an.stage.paths.drawn_polyline()).

Return type:

tuple[list[tuple[float, float]], list[int]]