/** * Pattern-graph — a drawn curve stretched over the cards. * * The curve's HORIZONTAL axis is the card index: card `i` of `count` reads the * curve at position `t = i/(count−1)`, so a small drawing (100 px, a handful of * points) is interpolated onto however many cards you generate (1, 1000, * 1_000_000). The curve's VERTICAL axis is the value; because a drawing has no * inherent scale, the caller declares the value range with `y_range="min..max"` * — REQUIRED, and the floor and ceiling the drawing's CANVAS becomes. The canvas * is the image for a raster and a percentage board (0..100, grown only to hold * anything drawn outside it) for a list of points, so what a drawing means never * depends on how much of the board it happens to use. * * Mode 1 (this file) is a single curve → a deterministic signal. Corridor mode * (two curves → a random value between them) and raster input (PNG → vector) * build on the same core. Design: `docs/superpowers/specs/ * 2026-07-15-pattern-graph-density-design.md` (mode corrected to signal). */ /** * How the drawing is read BETWEEN two points. * * `linear` follows the straight segment — faithful to a polyline, but when one * segment is stretched over thousands of cards the values climb by an identical * step every time, which reads as obviously artificial. `smooth` runs a * monotone cubic through the points instead: it eases in and out of every point * and, unlike an ordinary spline, never overshoots beyond the drawn values — * important when the drawing is the specification. `step` holds each value * until the next point. */ export type Interp = 'linear' | 'smooth' | 'step'; export interface SignalCurve { /** Point x-coordinates, sorted (the horizontal shape). */ readonly xs: readonly number[]; /** Point y-values (the drawn height at each x). */ readonly ys: readonly number[]; readonly yMin: number; readonly yMax: number; /** Output value range the curve's height maps into; absent → raw y. */ readonly yRange?: readonly [number, number]; readonly decimals: number; readonly interp: Interp; /** Monotone-cubic tangents, precomputed for `smooth`. */ readonly slopes?: readonly number[]; } /** `interp="linear|smooth|step"` (default `linear`). */ export declare function parseInterp(raw: string | undefined): Interp; /** * Parse a points list into `[x,y]` pairs. Robust to both `"x,y x,y"` and SVG's * space-separated `"x y x y"`: extracts every number and pairs them. */ export declare function parsePoints(raw: string): [number, number][]; /** * `y_range="min..max"` → `[min, max]` — the value axis, and REQUIRED. * * A drawing carries no units of its own: a curve exported from one tool runs * 0..100, from another 0..480, from a third 0..10002345345. `y_range` is what * those coordinates mean, so without it there is nothing to bring the picture * into and every answer would be a guess about somebody's export settings. */ export declare function parseYRange(raw: string | undefined): [number, number]; /** * `fit="A..B"` — where a drawing read from a FILE lands on the value axis. * * A file carries a shape and nothing else: not units, not an origin, not even * which way is up. So its own lowest and highest point are the only two things * that can be measured, and `fit=` says what they become. Absent, they become * the ends of `y_range` — the drawing fills the axis. * * Returns the TARGET band, so the caller passes it where `y_range` used to go and * the mapping in `signalValueAt` needs no knowledge of any of this. */ export declare function parseFit(raw: string | undefined, yRange: readonly [number, number]): [number, number]; /** * Build a signal curve from raw points: sort by x, record the canvas the * drawing is read against. `normExtent` overrides that canvas — a raster passes * the image frame, and a corridor passes the SHARED canvas of both curves so * the band between them stays a band. */ export declare function buildSignalCurve(points: readonly (readonly [number, number])[], yRange: readonly [number, number] | undefined, decimals: number, normExtent?: readonly [number, number], interp?: Interp): SignalCurve; /** A corridor: two curves in one value space; the value is random between them. */ export interface Corridor { readonly lower: SignalCurve; readonly upper: SignalCurve; readonly decimals: number; } /** * Build a corridor from an upper curve and an optional lower one (omitted → a * flat floor at the axis minimum). Both curves live in the declared value axis, * so the band between them is read off the same frame the single curve uses. */ export declare function buildCorridor(upperPts: readonly (readonly [number, number])[], lowerPts: readonly (readonly [number, number])[] | undefined, yRange: readonly [number, number], decimals: number, interp?: Interp): Corridor; /** Random value between the two curves at position `t`, using uniform `u`. */ export declare function corridorValueAt(c: Corridor, t: number, u: number): number; /** * The value of the card at position `t ∈ [0,1]`: where the card's vertical line * CROSSES the drawing. Ten cards are ten crossings, measured with a ruler. * * There used to be a second rule beside this one. A card also owned a WINDOW — * the slice of the drawing between it and its neighbours — and whenever a drawn * vertex fell inside that window the card returned the window's average instead * of the crossing. The intent was decent: a thousand-point trace squeezed into * ten rows would summarise its detail rather than drop it. * * The cost was worse than what it bought. Which rule a card used depended on * where the vertices happened to land, so neighbouring cards of one drawing were * computed by different laws and nothing in the picture said which was which. A * card sitting on a stretch running from 49.58 to 49.68 — flat to the eye, 50 by * the ruler — came out as 52, because its window reached back into a slope it * was not standing on. And the averaging never matched its own intent anyway: a * single vertex in the window gave a two-step trapezoid, so a window whose exact * mean was 50.87 was reported as 51.90. * * Ten cards are a request for ten readings, and ten readings are what they get. * Missing a peak that falls between two of them is not a lost measurement — it * is the consequence of having asked for ten. Draw in more detail, or ask for * more cards: on a million cards the same drawing spells itself out completely, * peaks included. What matters more is that a person can now look at their own * drawing and say what will come out. */ export declare function signalValueAt(curve: SignalCurve, t: number): number; export declare function formatSignal(v: number, curve: SignalCurve): string; /** * The SECOND way to read the same drawing (`mode="density"`). * * A signal reads the curve as a TRAJECTORY: the horizontal axis is the card * index and the height is that card's value, so the cards walk along the line * in order. A density asks the opposite question — the horizontal axis is the * VALUE and the height is HOW OFTEN that value comes up. Draw a hump over the * middle and the numbers pile up in the middle, in random order: "draw your own * probability" instead of picking `normal`/`poisson` from a list. * * `xs` is a fine grid across the drawing, `dens` the (non-negative) height above * the baseline there, and `cdf` the normalized running area — inverting it turns * a uniform draw into a value. The grid keeps every drawn vertex and subdivides * between them, so `interp="smooth"` shapes the distribution too. */ export interface Density { readonly xs: readonly number[]; readonly dens: readonly number[]; readonly cdf: readonly number[]; /** Total area under the drawing — the scale `cdf` was normalized by. */ readonly area: number; /** Value range the drawing's WIDTH maps into; absent → the raw x coordinates. */ readonly yRange?: readonly [number, number]; readonly decimals: number; } /** * Turn a curve into a distribution. Zero probability is the curve's BASELINE * (`curve.yMin`) — the picture's floor for a raster, the lowest drawn point * otherwise — so the deepest part of the drawing is the value that never * appears. A drawing with no height at all (a flat line) has nothing to weight * by and degrades to a uniform distribution rather than an error. */ export declare function buildDensity(curve: SignalCurve): Density; /** * Invert the distribution: a uniform `u` becomes a value. Inside a grid cell the * density is a straight line, so the area up to `s` is a quadratic in `s` and the * exact crossing is solved rather than searched — no bias from bucketing. */ export declare function densityValueAt(d: Density, u: number): number; /** `mode="signal|density"` (default `signal`) — which question the drawing answers. */ export declare function parseMode(raw: string | undefined): 'signal' | 'density'; /** * A resolved pattern gen — one curve (a deterministic signal) or a corridor * (a random band) — plus `spread`. * * `spread` widens the reading by ±N in VALUE units, so a single drawn line can * be treated as the CENTRE of a tunnel without drawing the two edges by hand: * `spread="1"` scatters every card ±1 around the curve. It follows whatever * scale `y_range` sets, so on a 0..1 axis a spread of `0.001` is the natural * way to ask for a barely-there wobble. Left at 0 a single line stays exactly * predictable. */ export type PatternGen = { readonly kind: 'signal'; readonly curve: SignalCurve; readonly spread: number; } | { readonly kind: 'corridor'; readonly corridor: Corridor; readonly spread: number; } | { readonly kind: 'density'; readonly density: Density; }; /** * Value of the card at position `t`. `u` is the per-card uniform, used when the * gen has a band to pick from (a corridor, or any curve given a `spread`). */ export declare function patternGenValue(pg: PatternGen, t: number, u: number): string; /** Whether the gen consumes a uniform draw per card (a band, a spread, a density). */ export declare function patternGenDraws(pg: PatternGen): boolean; /** * Re-read an already-built gen as a distribution (`mode="density"`). * * Every input path — inline points, `upper`/`lower`, an SVG, a PNG — has by then * produced a curve in one common shape, so the switch is a single conversion at * the end rather than a branch in each reader. A band contributes its TOP edge: * the outline is the distribution, whatever the drawing's lower edge does. */ export declare function asDensity(pg: PatternGen): PatternGen; export declare function decimalsFromAttrs(attrs: Record): number; /** `spread="N"` — half-width of the random band around the drawing, in values. */ export declare function spreadFromAttrs(attrs: Record): number; /** Build a signal curve straight from gen attributes (`y_range`, `decimals`) + points. */ export declare function signalCurveFromAttrs(attrs: Record, points: readonly (readonly [number, number])[]): SignalCurve; /** Build a corridor straight from gen attributes + the two point lists. */ export declare function corridorFromAttrs(attrs: Record, upperPts: readonly (readonly [number, number])[], lowerPts: readonly (readonly [number, number])[] | undefined): Corridor; /** * Turn a decoded raster image into a pattern gen. Each image column becomes a * point on the horizontal axis; the ink (dark, opaque) pixels in that column * define the height. The image's full height maps to `y_range` (the frame is * the scale — floor of the image = min, top = max), so only the drawing's shape * matters, not the pixel count. * * A pixel is INK by ALPHA when the image has no opaque background (a drawing * exported on transparency): anything not fully transparent counts. Otherwise * ink falls back to luminance, with `ink_threshold` (0..1, default 0.5) as the * dark/light cutoff. * * There is no separate signal-vs-corridor detection step. Every column is * measured twice — from the top down to the first ink and from the bottom up to * the first ink. If both readings meet on the same pixel the column is an EXACT * point on the graph; if they differ the column is a band and the value is * random between them. One drawing can therefore run as a single line and split * into a corridor further along, which is exactly what a hand drawing does. */ export declare function rasterPatternGen(img: { readonly width: number; readonly height: number; readonly rgba: Uint8Array; }, attrs: Record): PatternGen; /** * Turn a per-position "highest point / lowest point" measurement into a gen — * the one rule both inputs share. * * Where the two readings coincide the drawing is a single line at that position, * so the value is exact. Where they stand apart it is a band and the value is * random between them. A picture that never parts is therefore a plain signal * and spends no random draw at all; as soon as it parts anywhere the gen draws * per card, and the positions that still touch keep returning their exact value * because a zero-width band ignores the draw. */ export declare function patternFromEnvelope(top: readonly (readonly [number, number])[], bottom: readonly (readonly [number, number])[], attrs: Record, frame?: readonly [number, number]): PatternGen; /** * Extract the graph curve from an SVG file. * * Handles what real editors actually export: the full path grammar (Bezier * curves, arcs, relative commands), ``/``/``, and * nested ``. Among several shapes the widest one is taken as the * data line (axes and frames are narrower). Y is flipped, because SVG grows * downward while a graph grows upward. See `svg-path.ts`. */ export declare function parseSvgCurve(svg: string): [number, number][]; //# sourceMappingURL=pattern.d.ts.map