@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 +19 -9
- package/dist/index.d.ts +13 -8
- package/dist/index.js +37 -10
- package/package.json +4 -4
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
|
|
14
|
-
- **`Optimizer`** — runs the
|
|
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
|
-
- **
|
|
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;
|
|
112
|
-
- `Cfg.shapeTypes` holds shape constructors; shapes are chosen randomly each step
|
|
113
|
-
- `
|
|
114
|
-
- `
|
|
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:
|
|
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:
|
|
123
|
+
render(ctx: Ctx2D): void;
|
|
123
124
|
};
|
|
124
125
|
}
|
|
125
126
|
declare class Canvas {
|
|
126
|
-
node: HTMLCanvasElement;
|
|
127
|
-
ctx:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1093
|
-
svg.appendChild(
|
|
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
|
+
"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.
|
|
18
|
-
"jsdom": "^
|
|
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
|
},
|