← drawing · version 0.4.3
60 public items, in source order. Generated from the registry index, which re-derives them from the package source at publish time — so a signature here is the one that version actually ships.
pub struct Brush { bname: text = "", bw: integer = 0, bh: integer = 0, period: float = 0.0, img: vector<integer> }
A footprint: bw columns across the stroke, bh rows along it, packed 0xAARRGGBB, tiled every period canvas px along the stroke. Column 0 lands on the stroke's RIGHT side walking root→tip (flip mirrors it).
pub struct LockStyle { w0: float = 2.0, w: float = 10.0, swell: float = 0.3, body: float = 0.8, tips: integer = 3, tipvar: float = 0.35, spread: float = 8.0, seed: integer = 1, period: float = 48.0, dark: integer = 0, base: integer = 0, lit: integer = 0, lx: float = -0.5, ly: float = -0.8, lz: float = 0.6, alpha: float = 1.0, flip: boolean = false }
Everything a Lock line says past its points. Widths and period are in canvas px here and scaled to the supersampled canvas by scaled at the raster call, like every other mark.
pub struct Layer { x0: integer = 0, y0: integer = 0, lw: integer = 0, lh: integer = 0, px: vector<integer> }
A stroke as a layer over the canvas: px is lw × lh packed 0xAARRGGBB placed at (x0, y0); lw == 0 means the stroke painted nothing.
pub fn scaled(st: const LockStyle, k: float) -> LockStyle
The style with its canvas-px lengths scaled by k — the supersample factor.
pub fn hair_brush(bw: integer, bh: integer, seed: integer, gap: float) -> vector<integer>
The built-in split-bristle footprint. Columns group into channels 1..3 px wide, each a bristle with its own value; a gap fraction are thin translucent strands that FADE OUT along the run (the broken parallel streaks a worn brush lays); the outermost columns are soft so the silhouette frays. Greyscale value × alpha — the stroke's colour ramp multiplies in at resolve time. @FR-Brush-Image — the built-in footprint.
pub fn load_brush(path: text, bname: text, period: float) -> Brush
An authored footprint from a PNG: rows along the stroke, columns across. bw == 0 when the file could not be read. period 0 means one row per canvas px — the image's own height.
pub fn lock_layer(xs: const vector<float>, ys: const vector<float>, cw: integer, ch: integer, br: const Brush, st: const LockStyle) -> Layer
Drag brush br along the pixel path (xs, ys) as a lock styled by st (pixel units), answering the stroke as a layer clipped to the cw × ch canvas — lw == 0 when it paints nothing. @FR-Brush-Ramp (the resolve) · @FR-Oracle-Order (expression for expression the oracle's)
pub fn composite_layer(cv: graphics::Canvas, lay: const Layer)
Alpha-OVER the layer onto the canvas, straight alpha, in the integer arithmetic the oracle uses — (s·a·255 + d·A·(255−a) + ½) / (a·255 + A·(255−a)) per channel, so an opaque ground and a transparent sprite are one formula. @FR-Brush-Over
pub enum PaintKind { Stroked, Solid, Linear, Radial }
How a mark is filled, if it is filled at all.
pub struct FrondSpec { count: integer = 8, len1: float = 0.1, len2: float = 0.1, wid1: float = 3.0, wid2: float = 3.0, ang1: float = 30.0, ang2: float = 30.0, mirror: boolean = false, jitter: float = 0.5, clump: float = 0.6, fray: float = 0.4, bow: float = 0.0, seed: integer = 1, depth: integer = 1, sub: float = 0.32 }
The settings of one Fronds line: how many strokes run along the spine, how their length, width and angle ramp from the first to the last (len1 → len2, wid1 → wid2, ang1 → ang2), and how far they stray from a regular comb — jitter, clump, fray and bow, all drawn from seed. mirror repeats each stroke on the other side of the spine; depth and sub grow smaller fronds off each one.
pub struct Paint { pk: PaintKind = Stroked, c1: integer = 0, c2: integer = 0, spec: vector<float> }
A mark's fill descriptor.
Stroked is the absence of a fill, not a fill of nothing: it is what sends a Poly down the pen path instead of the polygon path, and the two produce completely different pictures from the same points. c1 is the fill colour, and c2 the second colour of a Linear or Radial gradient.
pub enum OpKind { Sky, Fill, Stroke, Lock }
What an op does. Lock is the brush stroke — see [brush.loft](brush.loft).
pub struct Op { kind: OpKind = Sky, pts: vector<raster::Pt>, paint: Paint, widths: vector<float>, w: integer = 3, color: integer = 0, color2: integer = 0, style: brush::LockStyle, brush: integer = -1 }
One entry in the ordered draw program.
⚠ pts are FRACTIONS of the paper (0..1, origin top-left, y down), never pixels. The scale to pixels happens once, at the raster call, because the paper size is a property of the scene and the supersample factor is a property of the renderer — neither belongs in a parsed mark. @FR-Scene-Fraction
pub struct Elem { ename: text = "", seen: boolean = false, bx0: float = 0.0, by0: float = 0.0, bx1: float = 0.0, by1: float = 0.0, first_at: integer = -1 }
A named group of marks and the box they cover, in paper fractions.
pub struct Sketch { sw: integer = 800, sh: integer = 800, transparent: boolean = false, ops: vector<Op>, elems: vector<Elem>, landmarks: vector<Landmark>, checks: vector<text>, unparsed: vector<text>, deferred: vector<text>, brushes: vector<brush::Brush>, base_dir: text = "" }
A parsed scene: the paper, the ordered draw program, and everything the parser could measure or could not accept.
⚠ NOT called Scene: mesh3d::Scene owns that name and graphics depends on mesh3d, so the collision is a hard error — the same trap graphics::Coord hit with Point. Reach for Sketch in .draw code and the 3-D names stay where they belong.
pub fn grey(gl: float) -> integer
A grey level in 0..1 as an opaque colour, rounded HALF TO EVEN.
Python's round is half-to-even and draw.py uses it here, so fill=0.5 is 128 and not 127 — a difference of one level that no eye would find and a byte diff would.
pub struct PointList { pts: vector<raster::Pt>, smooth: vector<boolean>, widths: vector<float>, has_width: vector<boolean>, any_width: boolean = false }
Every (x,y) on a line, with the two suffixes a point may carry.
pub fn read_points(s: text) -> PointList
\(\s*(n)\s*,\s*(n)\s*\)\s*(~?)\s*(?:@\s*(n))?, found repeatedly along the line.
Scanning for points rather than splitting the line is what lets the same grammar carry options: rgb=140,104,62 has no parentheses, so it is not a point, and no separator has to be agreed between the two.
pub fn circle_pts(cx: float, cy: float, r: float, n: integer, flat: float, pw: integer, ph: integer) -> vector<raster::Pt>
The outline of a Circle, as draw.py builds it: n segments round the centre with the FIRST point repeated at the end, so the polygon closes itself and Pillow adds no edge of its own.
flat squashes the vertical radius; the W/H factor keeps the circle round on non-square paper, since the coordinates are fractions of each axis.
pub fn smooth_applies(pts: const vector<raster::Pt>, flags: const vector<boolean>) -> boolean
Does this outline curve at all? Fewer than three points, or no ~, and the control points ARE the polyline — which is what keeps a plain Poly unchanged.
pub fn smooth_pts(pts: const vector<raster::Pt>, flags: const vector<boolean>, closed: boolean) -> vector<raster::Pt>
Expand control points to a dense polyline. @FR-Mark-Smooth
A point flagged ~ curves (its tangent is half the neighbour chord); a corner uses the segment chord, which makes that segment exactly straight. So one outline mixes straight runs and curves, and a segment between two corners is a line however many ~ the rest of the outline has.
pub fn smooth_vals(vals: const vector<float>, closed: boolean) -> vector<float>
The per-point widths carried across the same expansion, interpolated linearly.
pub fn parse_scene(src: text) -> Sketch
Parse a whole .draw source.
The command order below is draw.py's, and it is not alphabetical or tidy — Circle is tried before Poly, Line last of all. Keep it: a grammar this loose is defined by the order its alternatives are tried in, and re-sorting it silently re-reads lines that sit in two alternatives. @FR-Scene-Order
pub fn parse_scene_at(src: text, base: text) -> Sketch
parse_scene for a scene that lives in a directory: base is what a Brush … file= path is relative to.
pub struct Check { claim: text = "", ok: boolean = false, lhs: float = 0.0, rhs: float = 0.0, tol: float = 0.02, err: text = "", report: text = "" }
One evaluated check line.
pub fn eval_check(sk: const Sketch, raw: text) -> Check
One check line, measured — draw.py's eval_check, verdict for verdict.
pub fn eval_checks(sk: const Sketch) -> vector<Check>
Every check the scene states, measured.
pub fn scene_ok(sk: const Sketch, results: const vector<Check>) -> boolean
Did the scene come out whole — every line understood, every mark drawn, every check passing? This is what --once exits on.
A DEFERRED mark counts as a failure, where draw.py has no such notion: a mark this build recognises and does not draw means the picture is short of something the scene asked for, and a renderer that answers "fine" to that is the one thing this channel exists to prevent.
pub fn metric_report(sk: const Sketch, results: const vector<Check>) -> text
The metric report: what did not parse, where every named element sits, and how each check came out.
draw.py's stats.txt blocks, in its spelling, minus the two that are pictures of the render (the density map and the composition notes) — this channel measures the SCENE, and a picture already has a diff.
pub fn render(sc: const Sketch) -> graphics::Canvas
Draw a parsed scene: at SUPERSAMPLEx, then resampled down with Lanczos.
A transparent background renders on transparent pixels so the resample anti-aliases edges against ALPHA rather than against white — which is what makes the result a sprite that can be composited over anything, with no colour-key hack. @FR-Raster-Transparent · @FR-Mark-Deposit (fills and strokes overwrite; only Lock composites, through brush).
pub fn render_file(src: text, out: text) -> Sketch
Render a .draw file to a PNG, answering the parsed scene so the caller can report what it could not accept. A Brush … file= in it resolves against the scene's own directory.
pub fn hash01(hseed: integer, hidx: integer, hsalt: integer) -> float
A reproducible pseudo-random float in [-1, 1) from three small non-negative integers — noise::seed_hash, kept under this name because the corpus is defined by it. (Public names are one namespace across a package's modules, which is why the implementation carries a different one.) @FR-Seed-Hash
pub fn lowfreq(lseed: integer, lu: float) -> float
A smooth low-frequency wave over [0,1] — noise::seed_wave.
pub struct Frond { fpts: vector<raster::Pt>, fwid: vector<float> }
One frond: a centreline in paper fractions and a width per point.
pub fn fronds(x1: float, y1: float, x2: float, y2: float, sp: const FrondSpec, pw: integer, ph: integer) -> vector<Frond>
A linear array of tapered strokes rooted along the spine (x1,y1)-(x2,y2). @FR-Seed-NonUniform
Every frond is the same template placed at u along the spine and tilted off the normal, then made NON-UNIFORM three ways at once: placement clumps on the seeded low-frequency field, length / width / angle ramp along the run and are then jittered per frond, and the ends FRAY. Zero all three and it is a comb.
⚠ mirror reuses the SAME sample on both sides — length, angle and width are drawn once, outside the side loop, and reflected. Drawing them per side would consume the hash twice as fast and desynchronise every frond after the first.
Built in PIXEL space and handed back in fractions, so the array stays correct on non-square paper.
pub fn seed_hash(hseed: integer, hidx: integer, hsalt: integer) -> float
A reproducible pseudo-random float in [-1, 1) from three small non-negative integers.
⚠ NOT a PRNG, and deliberately not the random package. draw.py says so in its own docstring — *"never random()/hash() (those break reproducible renders)"* — and the corpus is defined by THIS hash: swap in PCG-64 and every jittered frond moves, so the substitution fails the gate it was meant to help pass.
Three details are load-bearing. The masking is 32-bit throughout, and the widest product it forms (0xFFFFFFFF * 1274126177) is 5.47e18 — inside i64 by a hair, so this needs no widening but has none to spare. And the final divide is a REAL division: x / 0xFFFFFFFF in integer arithmetic is 0 for every input and would give a perfectly smooth, entirely wrong field. @FR-Seed-Hash
pub fn seed_wave(lseed: integer, lu: float) -> float
A smooth low-frequency wave over [0,1], two harmonics with seed-derived phases.
Smooth in u is the point: neighbouring fronds see nearby values, so they CLUMP and leave gaps. White noise would scatter them evenly, which reads as man-made — the opposite of what this is for. Periodic in u, which is what lets a brush footprint tile along a stroke without a seam. @FR-Seed-NonUniform — the field an array clumps on.
pub struct Pt { ptx: float, pty: float }
A point in canvas pixels, before Pillow truncates it to a whole pixel.
The truncation is (int) on a C double — toward ZERO, so -0.4 lands on 0 and not on -1. Keep coordinates fractional right up to the rasteriser and the port and the oracle round the same way.
pub fn round_up(ru_v: float) -> integer
ROUND_UP from Draw.c — half AWAY from zero.
Not floor(f + 0.5): the sign lives outside, so the negative side mirrors the positive one exactly (ROUND_UP(-0.5) is -1, not 0).
pub fn round_down(rd_v: float) -> integer
ROUND_DOWN from Draw.c — ceil(f - 0.5), mirrored about zero.
The mirroring is the whole subtlety: ROUND_DOWN(-0.45) is **0**, because the ceil runs on |f| - 0.5 and the sign is applied afterwards. Take the obvious ceil(-0.95) instead and a span that should end on column 0 ends on column -1, which clips away and draws nothing.
pub fn fill_poly(cv: graphics::Canvas, pts: const vector<Pt>, ink: integer)
Fill a polygon the way ImageDraw.polygon(pts, fill=…) does.
The outline closes itself only when the last point is not already the first — a circle handed in with its start repeated gets no extra edge, which is how draw.py passes one.
pub fn wide_line(cv: graphics::Canvas, wx0: float, wy0: float, wx1: float, wy1: float, ink: integer, wwidth: integer)
One thick segment, the way ImageDraw.line(…, width=w) draws it for w > 1.
Pillow does not stroke — it builds a four-cornered quad around the segment and fills that, with the two half-widths rounded APART (ROUND_UP one side, ROUND_DOWN the other) so an even width still lands on whole pixels. A polyline is therefore a series of independent quads with no joins, and the overlap at a bend is what rounds the corner.
pub fn thin_line(cv: graphics::Canvas, pts: const vector<Pt>, ink: integer)
line32 from Draw.c — Bresenham, and it stops one pixel SHORT of the end.
draw_lines puts that missing pixel back after the last segment, which is why this is a polyline call rather than a segment one: split it into segments and each one grows a pixel Pillow never drew.
pub struct Scan { ok: boolean, at: integer, value: float }
A parse position and what was read there.
A scan either lands (ok) or does not, and the caller needs BOTH the value and where to carry on from, so every reader answers all three at once.
pub fn scan_fail() -> Scan
The did-not-land answer, which every reader below returns when it cannot match. A caller tests ok and never has to know a sentinel position.
pub fn is_digit(b: integer) -> boolean
A regex [0-9], ASCII only — which costs nothing here, because a .draw command line is ASCII by construction.
pub fn is_space_byte(b: integer) -> boolean
A regex \s: space, tab, and the line endings a stray \r leaves behind.
pub fn at(s: text, i: integer) -> integer
The byte at i, or -1 past either end — a value no byte can take, so a caller never has to bound-check before asking.
pub fn matches_at(s: text, i: integer, word: text) -> boolean
Does s carry word at byte i, ignoring case? (word must be lowercase.)
pub fn word_boundary(s: text, i: integer) -> boolean
A regex \b before byte i: position 0, or a non-word byte to the left.
pub fn read_number(s: text, i: integer) -> Scan
A regex ([-\d.]+) — a run of digits, dots and minus signs, read as a number.
The character class is what draw.py writes, so this is deliberately loose: the run is taken first and converted second, and a run that is not a number fails the scan rather than being silently read as zero.
pub fn find_option(s: text, key: text) -> Scan
The first \b<key>\s*=\s* on the line, answering where its VALUE starts.
This is the shape every option regex in draw.py opens with, and the word-boundary is load-bearing rather than decorative: without it w= would also be found inside w2=, which is a different option with a different meaning. key must be lowercase; the match is not.
pub fn find_word_from(s: text, word: text, start: integer) -> Scan
The first occurrence of a lowercase word at or after start, ignoring case — a regex literal with no boundary condition, which is what Circle and Line open with.
The start is what makes a failed parse behave like re.search: a command whose first occurrence does not parse is retried from the NEXT one rather than given up on.