@slithy/prim-lib 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,15 +10,15 @@ Reconstructs an image by iteratively placing geometric shapes. Each step evaluat
10
10
 
11
11
  ### Classes
12
12
 
13
- - **`Canvas`** — wraps `HTMLCanvasElement`; handles image loading, pixel reads, drawing steps, and SVG output
14
- - **`Optimizer`** — runs the rAF loop; calls `onStep` after each shape is placed
13
+ - **`Canvas`** — wraps `HTMLCanvasElement` or `OffscreenCanvas` (environment-detected); handles image loading, pixel reads, drawing steps, and SVG output
14
+ - **`Optimizer`** — runs the step loop; calls `onStep` after each shape is placed. Accepts an optional `schedule` function (defaults to `requestAnimationFrame`; pass `fn => setTimeout(fn, 0)` for worker contexts)
15
15
  - **`State`** — holds the current canvas and its distance from the target
16
16
  - **`Step`** — a single candidate shape placement; computes best color and difference change
17
17
 
18
18
  ### Shapes
19
19
 
20
20
  - **`Shape`** — abstract base
21
- - **`Triangle`**, **`Rectangle`**, **`Ellipse`**, **`Square`**, **`Hexagon`**, **`Glyph`**, **`Debug`**
21
+ - **`Triangle`**, **`Rectangle`**, **`Ellipse`**, **`Circle`**, **`Square`**, **`Hexagon`**, **`Glyph`**, **`Debug`**
22
22
 
23
23
  ### Types
24
24
 
@@ -38,12 +38,14 @@ interface Cfg {
38
38
  allowUpscale?: boolean // allow output larger than source image (default: false)
39
39
  scale?: number // viewSize / computeSize ratio (set by Canvas.original)
40
40
  shapeTypes: Array<new (w: number, h: number) => ShapeInterface>
41
+ shapeWeights?: number[] // per-shape selection weights (same length as shapeTypes)
41
42
  fill: 'auto' | string
42
43
  }
43
44
  ```
44
45
 
45
46
  - **`PreCfg`** — `Cfg` with optional `width`/`height`; used before image dimensions are known
46
47
  - **`ShapeInterface`** — structural interface for shapes; includes `toData(alpha, color): StepData`
48
+ - **`Ctx2D`** — `CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D`; used for canvas operations that work in both main-thread and worker contexts
47
49
  - **`Bbox`**, **`Point`**, **`ImageDataLike`**, **`ShapeImageData`**
48
50
 
49
51
  **`RGB`** — `[number, number, number]` tuple
@@ -55,6 +57,7 @@ type StepData =
55
57
  | { t: 't'; a: number; c: RGB; pts: [number, number][] } // Triangle
56
58
  | { t: 'r'; a: number; c: RGB; pts: [number, number][] } // Rectangle
57
59
  | { t: 'e'; a: number; c: RGB; cx: number; cy: number; rx: number; ry: number } // Ellipse
60
+ | { t: 'c'; a: number; c: RGB; cx: number; cy: number; r: number } // Circle
58
61
  | { t: 's'; a: number; c: RGB; cx: number; cy: number; r: number } // Square
59
62
  | { t: 'h'; a: number; c: RGB; cx: number; cy: number; r: number; angle: number } // Hexagon
60
63
  | { t: 'sm'; a: number; c: RGB; cx: number; cy: number; fs: number; text: string } // Glyph
@@ -77,7 +80,7 @@ interface SerializedOutput {
77
80
 
78
81
  ```ts
79
82
  interface ReplayResult {
80
- raster: HTMLCanvasElement
83
+ raster: HTMLCanvasElement | OffscreenCanvas
81
84
  svg: SVGSVGElement
82
85
  svgString: string
83
86
  }
@@ -87,6 +90,10 @@ interface ReplayResult {
87
90
 
88
91
  **`replayOutput(data: SerializedOutput): ReplayResult`** — reconstructs canvas and SVG natively from serialized output, without re-running the optimizer. Useful for restoring saved results from localStorage or a database.
89
92
 
93
+ **`renderStepToCtx(data: StepData, ctx: Ctx2D): void`** — renders a single `StepData` onto a 2D canvas context. Used internally by `replayOutput` and by `runWorker` to incrementally apply worker-posted steps to the display canvas.
94
+
95
+ **`stepDataToSVGElement(data: StepData): SVGElement`** — creates a DOM `SVGElement` from a `StepData`. Used internally by `replayOutput` and by `runWorker` to build the live SVG incrementally on the main thread.
96
+
90
97
  ### Utilities
91
98
 
92
99
  - **`getFill(data: ImageDataLike): string`** — computes a fill color (average of corner pixels) from image data
@@ -101,14 +108,17 @@ interface ReplayResult {
101
108
  - **ESM only** — no CommonJS; no `importScripts()`
102
109
  - **Canvas reuse** — the original creates a new `<canvas>` element for every shape rasterization call (~58,000 per run at default settings). `prim-lib` reuses a single module-level canvas, growing it only when a larger bbox is seen. This delivered a ~27× speedup (37 s → ~0.8 s rasterize time per run)
103
110
  - **`willReadFrequently: true`** — canvas contexts used for `getImageData` are created with this flag, keeping pixel data in CPU memory and suppressing browser warnings
104
- - **Workers removed** — the original included worker stubs (`importScripts()`-based, not ESM-compatible). Workers were investigated and benchmarked; after the canvas reuse optimization, a batched worker implementation matched single-threaded performance. Workers are not used
111
+ - **Worker-compatible** — `Canvas` detects its environment: in a browser context it creates `HTMLCanvasElement`; in a worker it creates `OffscreenCanvas` (same 2D API). `Canvas.fromBitmap()` loads an image from an `ImageBitmap` (worker-compatible) rather than `new Image()`. `Optimizer` accepts an injectable `schedule` function so callers can substitute `setTimeout` for `requestAnimationFrame` in worker contexts. The original included `importScripts()`-based worker stubs (not ESM-compatible) which were removed; worker support is now done properly via `prim-interface`'s `runWorker()`
105
112
  - **Architecture split** — the original is a single-layer library. Here, `prim-lib` is the pure algorithm (no knowledge of how images arrive or where results go), and `prim-interface` is the adapter that wires it to the browser
106
113
  - **`PreCfg` / `Cfg` distinction** — `Canvas.original()` accepts `PreCfg` (optional `width`/`height`) and resolves to a fully-populated `Cfg`, making the config lifecycle explicit in the types
107
114
  - **Serialization** — `StepData` / `SerializedOutput` allow completed runs to be stored compactly and replayed via `replayOutput()` without re-running the optimizer
115
+ - **Additional shapes** — `Circle`, `Square`, `Hexagon`, and `Glyph` are not in the original; `Circle` is a uniform-radius variant of `Ellipse`
116
+ - **Shape weighting** — `shapeWeights` allows biased shape selection with a guaranteed distribution: the optimizer pre-allocates exact per-shape step counts (largest-remainder rounding) and shuffles them, so the final mix always matches the requested weights
108
117
 
109
118
  ## Architecture notes
110
119
 
111
- - `Shape.rasterize()` reuses a single module-level canvas (`_rasterCanvas`) grown to the max shape size seen; avoids per-call `createElement('canvas')` which was the dominant cost (27x speedup)
112
- - `Cfg.shapeTypes` holds shape constructors; shapes are chosen randomly each step
113
- - `PreCfg` exists to bridge the gap between call time (dimensions unknown) and runtime (dimensions set by `Canvas.original()`)
114
- - `Canvas.svgRoot()` is a static helper shared by `Canvas.empty()` and `replayOutput()` to create the SVG root with clip path and background fill
120
+ - `Shape.rasterize()` reuses a single module-level canvas (`_rasterCanvas`) grown to the max shape size seen; in a worker, this is an `OffscreenCanvas` (environment-detected by the `Canvas` constructor). Avoids per-call canvas creation which was the dominant cost (27x speedup)
121
+ - `Cfg.shapeTypes` holds shape constructors; shapes are chosen randomly each step unless `shapeWeights` is set
122
+ - When `shapeWeights` is provided (same length as `shapeTypes`), `Optimizer` builds a step plan at construction time: each shape type is allocated an exact number of slots proportional to its weight (using largest-remainder rounding), then the plan is Fisher-Yates shuffled. This guarantees the final shape distribution matches the weights, rather than just biasing random selection
123
+ - `PreCfg` exists to bridge the gap between call time (dimensions unknown) and runtime (dimensions set by `Canvas.original()` or `Canvas.fromBitmap()`)
124
+ - `Canvas.svgRoot()` is a static helper shared by `Canvas.empty()` and `replayOutput()` to create the SVG root with clip path and background fill; it uses DOM APIs and is main-thread only
package/dist/index.d.ts CHANGED
@@ -30,7 +30,7 @@ interface ShapeInterface {
30
30
  rasterize(alpha: number): {
31
31
  getImageData(): ImageDataLike;
32
32
  };
33
- render(ctx: CanvasRenderingContext2D): void;
33
+ render(ctx: Ctx2D): void;
34
34
  }
35
35
  interface Cfg {
36
36
  width: number;
@@ -57,6 +57,7 @@ type PreCfg = Omit<Cfg, 'width' | 'height'> & {
57
57
  height?: number;
58
58
  };
59
59
  type RGB = [number, number, number];
60
+ type Ctx2D = CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
60
61
  type StepData = {
61
62
  t: 't';
62
63
  a: number;
@@ -119,17 +120,18 @@ interface DrawableStep {
119
120
  alpha: number;
120
121
  color: string;
121
122
  shape: {
122
- render(ctx: CanvasRenderingContext2D): void;
123
+ render(ctx: Ctx2D): void;
123
124
  };
124
125
  }
125
126
  declare class Canvas {
126
- node: HTMLCanvasElement;
127
- ctx: CanvasRenderingContext2D;
127
+ node: HTMLCanvasElement | OffscreenCanvas;
128
+ ctx: Ctx2D;
128
129
  _imageData: ImageData | null;
129
130
  static svgRoot(width: number, height: number, fill: string): SVGSVGElement;
130
131
  static empty(cfg: Cfg, svg: true): SVGSVGElement;
131
132
  static empty(cfg: Cfg, svg?: false): Canvas;
132
133
  static original(url: string, cfg: PreCfg): Promise<Canvas>;
134
+ static fromBitmap(bitmap: ImageBitmap, cfg: PreCfg): Canvas;
133
135
  static test(cfg: PreCfg): Canvas;
134
136
  constructor(width: number, height: number, willReadFrequently?: boolean);
135
137
  clone(): Canvas;
@@ -176,7 +178,8 @@ declare class Optimizer {
176
178
  _stopped: boolean;
177
179
  _paused: boolean;
178
180
  _stepPlan: ShapeCtor[];
179
- constructor(original: Canvas, cfg: Cfg);
181
+ _schedule: (fn: () => void) => void;
182
+ constructor(original: Canvas, cfg: Cfg, schedule?: (fn: () => void) => void);
180
183
  start(): void;
181
184
  stop(): void;
182
185
  pause(): void;
@@ -198,7 +201,7 @@ declare class Shape implements ShapeInterface {
198
201
  rasterize(alpha: number): {
199
202
  getImageData(): ImageDataLike;
200
203
  };
201
- render(_ctx: CanvasRenderingContext2D): void;
204
+ render(_ctx: Ctx2D): void;
202
205
  }
203
206
  declare class Polygon extends Shape {
204
207
  points: Point[];
@@ -296,11 +299,13 @@ declare function computeColorAndDifferenceChange(offset: Bbox, imageData: ShapeI
296
299
  differenceChange: number;
297
300
  };
298
301
 
302
+ declare function renderStepToCtx(data: StepData, ctx: Ctx2D): void;
303
+ declare function stepDataToSVGElement(data: StepData): SVGElement;
299
304
  interface ReplayResult {
300
- raster: HTMLCanvasElement;
305
+ raster: HTMLCanvasElement | OffscreenCanvas;
301
306
  svg: SVGSVGElement;
302
307
  svgString: string;
303
308
  }
304
309
  declare function replayOutput(data: SerializedOutput): ReplayResult;
305
310
 
306
- export { type Bbox, Canvas, type Cfg, Circle, Debug, Ellipse, Glyph, Hexagon, type ImageDataLike, Optimizer, type Point, type PreCfg, type RGB, Rectangle, type ReplayResult, SVGNS, type SerializedOutput, Shape, type ShapeImageData, type ShapeInterface, Square, State, Step, type StepData, Triangle, clamp, clampColor, computeColorAndDifferenceChange, difference, differenceToDistance, distanceToDifference, getFill, parseColor, replayOutput, stepPerf };
311
+ export { type Bbox, Canvas, type Cfg, Circle, type Ctx2D, Debug, Ellipse, Glyph, Hexagon, type ImageDataLike, Optimizer, type Point, type PreCfg, type RGB, Rectangle, type ReplayResult, SVGNS, type SerializedOutput, Shape, type ShapeImageData, type ShapeInterface, Square, State, Step, type StepData, Triangle, clamp, clampColor, computeColorAndDifferenceChange, difference, differenceToDistance, distanceToDifference, getFill, parseColor, renderStepToCtx, replayOutput, stepDataToSVGElement, stepPerf };
package/dist/index.js CHANGED
@@ -190,6 +190,22 @@ var Canvas = class _Canvas {
190
190
  };
191
191
  });
192
192
  }
193
+ static fromBitmap(bitmap, cfg) {
194
+ const w = bitmap.width;
195
+ const h = bitmap.height;
196
+ const computeScale = getScale(w, h, cfg.computeSize, cfg.allowUpscale);
197
+ cfg.width = w / computeScale;
198
+ cfg.height = h / computeScale;
199
+ const viewScale = getScale(w, h, cfg.viewSize, cfg.allowUpscale);
200
+ cfg.scale = computeScale / viewScale;
201
+ const fullCfg = { ...cfg, width: cfg.width, height: cfg.height };
202
+ const canvas = this.empty(fullCfg);
203
+ canvas.ctx.drawImage(bitmap, 0, 0, fullCfg.width, fullCfg.height);
204
+ if (cfg.fill === "auto") {
205
+ cfg.fill = getFill(canvas.getImageData());
206
+ }
207
+ return canvas;
208
+ }
193
209
  static test(cfg) {
194
210
  cfg.width = cfg.computeSize;
195
211
  cfg.height = cfg.computeSize;
@@ -216,10 +232,17 @@ var Canvas = class _Canvas {
216
232
  return canvas;
217
233
  }
218
234
  constructor(width, height, willReadFrequently = false) {
219
- this.node = document.createElement("canvas");
220
- this.node.width = width;
221
- this.node.height = height;
222
- this.ctx = this.node.getContext("2d", { willReadFrequently });
235
+ if (typeof document !== "undefined") {
236
+ const el = document.createElement("canvas");
237
+ el.width = width;
238
+ el.height = height;
239
+ this.node = el;
240
+ this.ctx = el.getContext("2d", { willReadFrequently });
241
+ } else {
242
+ const el = new OffscreenCanvas(width, height);
243
+ this.node = el;
244
+ this.ctx = el.getContext("2d", { willReadFrequently });
245
+ }
223
246
  this._imageData = null;
224
247
  }
225
248
  clone() {
@@ -878,7 +901,8 @@ var Optimizer = class {
878
901
  _stopped;
879
902
  _paused;
880
903
  _stepPlan;
881
- constructor(original, cfg) {
904
+ _schedule;
905
+ constructor(original, cfg, schedule = (fn) => requestAnimationFrame(fn)) {
882
906
  this.cfg = cfg;
883
907
  this.state = new State(original, Canvas.empty(cfg));
884
908
  this._steps = 0;
@@ -887,6 +911,7 @@ var Optimizer = class {
887
911
  this.onStep = () => {
888
912
  };
889
913
  this._stepPlan = buildStepPlan(cfg);
914
+ this._schedule = schedule;
890
915
  }
891
916
  start() {
892
917
  this._stopped = false;
@@ -922,7 +947,7 @@ var Optimizer = class {
922
947
  return;
923
948
  }
924
949
  if (this._steps < this.cfg.steps) {
925
- requestAnimationFrame(() => this._addShape());
950
+ this._schedule(() => this._addShape());
926
951
  }
927
952
  }
928
953
  _findBestStep() {
@@ -976,7 +1001,7 @@ function hexPoints(cx, cy, r, angle) {
976
1001
  return [~~(cx + r * Math.cos(a)), ~~(cy + r * Math.sin(a))];
977
1002
  });
978
1003
  }
979
- function renderStep(data, ctx) {
1004
+ function renderStepToCtx(data, ctx) {
980
1005
  ctx.globalAlpha = data.a;
981
1006
  ctx.fillStyle = rgbString(data.c);
982
1007
  switch (data.t) {
@@ -1021,7 +1046,7 @@ function renderStep(data, ctx) {
1021
1046
  }
1022
1047
  }
1023
1048
  }
1024
- function stepToSVG(data) {
1049
+ function stepDataToSVGElement(data) {
1025
1050
  const color = rgbString(data.c);
1026
1051
  const opacity = data.a.toFixed(2);
1027
1052
  let node;
@@ -1089,8 +1114,8 @@ function replayOutput(data) {
1089
1114
  svg.setAttribute("width", String(vw));
1090
1115
  svg.setAttribute("height", String(vh));
1091
1116
  for (const step of data.steps) {
1092
- renderStep(step, raster.ctx);
1093
- svg.appendChild(stepToSVG(step));
1117
+ renderStepToCtx(step, raster.ctx);
1118
+ svg.appendChild(stepDataToSVGElement(step));
1094
1119
  }
1095
1120
  const svgString = new XMLSerializer().serializeToString(svg);
1096
1121
  return { raster: raster.node, svg, svgString };
@@ -1118,6 +1143,8 @@ export {
1118
1143
  distanceToDifference,
1119
1144
  getFill,
1120
1145
  parseColor,
1146
+ renderStepToCtx,
1121
1147
  replayOutput,
1148
+ stepDataToSVGElement,
1122
1149
  stepPerf
1123
1150
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slithy/prim-lib",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "Core engine for primitive-based image reconstruction.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -14,11 +14,11 @@
14
14
  ],
15
15
  "sideEffects": false,
16
16
  "devDependencies": {
17
- "@vitest/coverage-v8": "^4.1.0",
18
- "jsdom": "^28",
17
+ "@vitest/coverage-v8": "^4.1.2",
18
+ "jsdom": "^29.0.1",
19
19
  "tsup": "^8",
20
20
  "typescript": "^5",
21
- "vitest": "^4",
21
+ "vitest": "^4.1.2",
22
22
  "@slithy/eslint-config": "0.0.0",
23
23
  "@slithy/tsconfig": "0.0.0"
24
24
  },