@slithy/prim-interface 0.3.3 → 0.4.1
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 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +25 -8
- package/dist/worker-entry.d.ts +14 -3
- package/dist/worker-entry.js +42 -17
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -33,6 +33,7 @@ const job = run(imageUrl, config, { // main-thread
|
|
|
33
33
|
onStep({ raster, svg, svgString, stepData, progress }) {},
|
|
34
34
|
onComplete(serialized) {},
|
|
35
35
|
onStop() {},
|
|
36
|
+
onError(message) {},
|
|
36
37
|
});
|
|
37
38
|
|
|
38
39
|
const job = runWorker(imageUrl, config, { // off-thread
|
|
@@ -40,6 +41,7 @@ const job = runWorker(imageUrl, config, { // off-thread
|
|
|
40
41
|
onStep({ raster, svg, svgString, stepData, progress }) {},
|
|
41
42
|
onComplete(serialized) {},
|
|
42
43
|
onStop() {},
|
|
44
|
+
onError(message) {},
|
|
43
45
|
});
|
|
44
46
|
|
|
45
47
|
job.stop();
|
|
@@ -49,7 +51,7 @@ job.resume();
|
|
|
49
51
|
|
|
50
52
|
**`run()`** — runs the optimizer on the main thread via `requestAnimationFrame`. Simpler; no worker overhead. UI may feel sluggish during compute-heavy steps at high `shapes` / `mutations` settings.
|
|
51
53
|
|
|
52
|
-
**`runWorker()`** — moves all computation into a Web Worker using `OffscreenCanvas`. The main thread only handles DOM updates (appending the canvas/SVG, calling callbacks). Requires a bundler that understands `new Worker(new URL(..., import.meta.url))` — Vite handles this, but see the Vite setup section above for a required config change.
|
|
54
|
+
**`runWorker()`** — moves all computation into a Web Worker using `OffscreenCanvas`. The main thread only handles DOM updates (appending the canvas/SVG, calling callbacks). Requires a bundler that understands `new Worker(new URL(..., import.meta.url))` — Vite handles this, but see the Vite setup section above for a required config change. Both fixed shapes (`Triangle`, `Rectangle`, etc.) and factory shapes (`makeNGon`, `makeRect`) are supported in `config.shapeTypes`.
|
|
53
55
|
|
|
54
56
|
### Config
|
|
55
57
|
|
|
@@ -64,6 +66,7 @@ interface Config {
|
|
|
64
66
|
viewSize: number // display/output resolution (px, longest edge)
|
|
65
67
|
allowUpscale?: boolean // allow output larger than source image (default: false)
|
|
66
68
|
pixelRatio?: number // device pixel ratio for the raster canvas (default: 1)
|
|
69
|
+
glyphChar?: string // Unicode character for the Glyph shape (default: '☺')
|
|
67
70
|
shapeTypes: ShapeConstructor[]
|
|
68
71
|
shapeWeights?: number[] // per-shape selection weights (same length as shapeTypes)
|
|
69
72
|
fill: 'auto' | string
|
|
@@ -76,6 +79,8 @@ interface Config {
|
|
|
76
79
|
|
|
77
80
|
**`pixelRatio`** — pass `window.devicePixelRatio` to produce a HiDPI raster canvas. The canvas pixel dimensions are `viewSize × pixelRatio`; set its CSS size to `viewSize` for a 1:1 device pixel mapping. Does not affect the SVG output or the optimizer; only the raster display canvas.
|
|
78
81
|
|
|
82
|
+
**`glyphChar`** — Unicode character used by the `Glyph` shape when running via `runWorker()`. Defaults to `'☺'` when omitted. Has no effect with `run()`, since the main-thread path accepts shape constructors directly (subclass `Glyph` with a custom character if needed).
|
|
83
|
+
|
|
79
84
|
**`shapeWeights`** — optional array of weights, one per entry in `shapeTypes`, controlling how often each shape type is used. Weights are relative (e.g. `[3, 1]` means 75% / 25%). When provided, the optimizer pre-allocates an exact number of slots per shape type (proportional to the weights) and shuffles them, guaranteeing the final distribution matches — rather than just biasing random selection.
|
|
80
85
|
|
|
81
86
|
### Callbacks
|
|
@@ -100,6 +105,10 @@ interface StepResult {
|
|
|
100
105
|
|
|
101
106
|
**`onComplete(serialized)`** — called when all steps finish. Receives a `SerializedOutput` — a compact, storage-ready representation of the full run that can be replayed via `replayOutput()`.
|
|
102
107
|
|
|
108
|
+
**`onStop()`** — called when the job is stopped via `job.stop()`.
|
|
109
|
+
|
|
110
|
+
**`onError(message)`** — called if the job fails (e.g. image failed to load or decode). The job is considered stopped after this; no further callbacks will fire.
|
|
111
|
+
|
|
103
112
|
### Exit helpers
|
|
104
113
|
|
|
105
114
|
Higher-level (rasterize SVG at save time for crisp output):
|
|
@@ -137,4 +146,12 @@ const { raster, svg, svgString } = replayOutput(serializedData);
|
|
|
137
146
|
|
|
138
147
|
### Re-exports from prim-lib
|
|
139
148
|
|
|
140
|
-
`Triangle`, `Rectangle`, `Ellipse`, `Circle`, `Square`, `Hexagon`, `Glyph
|
|
149
|
+
**Shapes:** `Triangle`, `Rectangle`, `Ellipse`, `Circle`, `Square`, `Hexagon`, `Glyph`
|
|
150
|
+
|
|
151
|
+
**Factories:** `makeNGon`, `makeRect`
|
|
152
|
+
|
|
153
|
+
**Factory option types:** `NGonOptions`, `RectOptions`
|
|
154
|
+
|
|
155
|
+
**Functions:** `stepPerf`, `replayOutput`
|
|
156
|
+
|
|
157
|
+
**Types:** `RGB`, `SerializedOutput`, `StepData`, `ReplayResult`, `ShapeInterface`, `Bbox`
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ShapeInterface, StepData, SerializedOutput } from '@slithy/prim-lib';
|
|
2
|
-
export { Bbox, Circle, Ellipse, Glyph, Hexagon, RGB, Rectangle, ReplayResult, SerializedOutput, ShapeInterface, Square, StepData, Triangle, replayOutput, stepPerf } from '@slithy/prim-lib';
|
|
2
|
+
export { Bbox, Circle, Ellipse, Glyph, Hexagon, NGonOptions, RGB, RectOptions, Rectangle, ReplayResult, SerializedOutput, ShapeInterface, Square, StepData, Triangle, makeNGon, makeRect, replayOutput, stepPerf } from '@slithy/prim-lib';
|
|
3
3
|
|
|
4
4
|
interface Config {
|
|
5
5
|
steps: number;
|
|
@@ -34,7 +34,8 @@ interface RunCallbacks {
|
|
|
34
34
|
onStart?: (raster: HTMLCanvasElement, svg: SVGSVGElement) => void;
|
|
35
35
|
onStep?: (result: StepResult) => void;
|
|
36
36
|
onComplete?: (serialized: SerializedOutput) => void;
|
|
37
|
-
onStop?: () => void;
|
|
37
|
+
onStop?: (serialized: SerializedOutput | null) => void;
|
|
38
|
+
onError?: (message: string) => void;
|
|
38
39
|
}
|
|
39
40
|
interface JobHandle {
|
|
40
41
|
stop(): void;
|
package/dist/index.js
CHANGED
|
@@ -4,13 +4,15 @@ function run(source, cfg, callbacks = {}) {
|
|
|
4
4
|
const url = source instanceof File ? URL.createObjectURL(source) : source;
|
|
5
5
|
let optimizer = null;
|
|
6
6
|
let stopped = false;
|
|
7
|
+
let computeCfg = null;
|
|
8
|
+
let accumulatedSteps = [];
|
|
7
9
|
Canvas.original(url, cfg).then((original) => {
|
|
8
10
|
if (stopped) {
|
|
9
11
|
if (source instanceof File) URL.revokeObjectURL(url);
|
|
10
12
|
return;
|
|
11
13
|
}
|
|
12
14
|
if (source instanceof File) URL.revokeObjectURL(url);
|
|
13
|
-
|
|
15
|
+
computeCfg = { ...cfg, width: cfg.width, height: cfg.height };
|
|
14
16
|
const pixelRatio = cfg.pixelRatio ?? 1;
|
|
15
17
|
const viewCfg = { ...computeCfg, width: computeCfg.scale * computeCfg.width, height: computeCfg.scale * computeCfg.height };
|
|
16
18
|
const deviceViewCfg = { ...viewCfg, width: viewCfg.width * pixelRatio, height: viewCfg.height * pixelRatio };
|
|
@@ -23,7 +25,6 @@ function run(source, cfg, callbacks = {}) {
|
|
|
23
25
|
svg.setAttribute("height", String(viewCfg.height));
|
|
24
26
|
callbacks.onStart?.(rasterNode, svg);
|
|
25
27
|
const serializer = new XMLSerializer();
|
|
26
|
-
const accumulatedSteps = [];
|
|
27
28
|
optimizer = new Optimizer(original, computeCfg);
|
|
28
29
|
let stepCount = 0;
|
|
29
30
|
optimizer.onStep = (step) => {
|
|
@@ -58,13 +59,21 @@ function run(source, cfg, callbacks = {}) {
|
|
|
58
59
|
};
|
|
59
60
|
optimizer.start();
|
|
60
61
|
}).catch((err) => {
|
|
61
|
-
|
|
62
|
+
if (source instanceof File) URL.revokeObjectURL(url);
|
|
63
|
+
callbacks.onError?.(err instanceof Error ? err.message : String(err));
|
|
62
64
|
});
|
|
63
65
|
return {
|
|
64
66
|
stop: () => {
|
|
65
67
|
stopped = true;
|
|
66
68
|
optimizer?.stop();
|
|
67
|
-
callbacks.onStop?.(
|
|
69
|
+
callbacks.onStop?.(computeCfg ? {
|
|
70
|
+
v: 1,
|
|
71
|
+
w: computeCfg.width,
|
|
72
|
+
h: computeCfg.height,
|
|
73
|
+
scale: computeCfg.scale,
|
|
74
|
+
fill: parseColor(cfg.fill),
|
|
75
|
+
steps: accumulatedSteps
|
|
76
|
+
} : null);
|
|
68
77
|
},
|
|
69
78
|
pause: () => optimizer?.pause(),
|
|
70
79
|
resume: () => optimizer?.resume()
|
|
@@ -122,16 +131,22 @@ function runWorker(source, cfg, callbacks = {}) {
|
|
|
122
131
|
callbacks.onComplete?.(msg.serialized);
|
|
123
132
|
} else if (msg.type === "stopped") {
|
|
124
133
|
if (isBlob) URL.revokeObjectURL(url);
|
|
125
|
-
callbacks.onStop?.();
|
|
134
|
+
callbacks.onStop?.(msg.serialized);
|
|
135
|
+
} else if (msg.type === "error") {
|
|
136
|
+
if (isBlob) URL.revokeObjectURL(url);
|
|
137
|
+
callbacks.onError?.(msg.message);
|
|
126
138
|
}
|
|
127
139
|
};
|
|
128
140
|
worker.onerror = (e) => {
|
|
129
|
-
|
|
141
|
+
if (isBlob) URL.revokeObjectURL(url);
|
|
142
|
+
callbacks.onError?.(e.message ?? "Unknown worker error");
|
|
130
143
|
};
|
|
131
144
|
const { shapeTypes, pixelRatio: _pr, width: _w, height: _h, scale: _s, ...rest } = cfg;
|
|
132
145
|
const workerCfg = {
|
|
133
146
|
...rest,
|
|
134
|
-
shapeTypeNames: shapeTypes.map(
|
|
147
|
+
shapeTypeNames: shapeTypes.map(
|
|
148
|
+
(ctor) => "_shapeSpec" in ctor ? ctor._shapeSpec : ctor.name
|
|
149
|
+
)
|
|
135
150
|
};
|
|
136
151
|
worker.postMessage({ type: "start", url, cfg: workerCfg });
|
|
137
152
|
return {
|
|
@@ -226,7 +241,7 @@ async function share(canvas, options) {
|
|
|
226
241
|
}
|
|
227
242
|
|
|
228
243
|
// src/index.ts
|
|
229
|
-
import { Triangle, Rectangle, Ellipse, Circle, Square, Hexagon, Glyph, stepPerf, replayOutput } from "@slithy/prim-lib";
|
|
244
|
+
import { Triangle, Rectangle, Ellipse, Circle, Square, Hexagon, Glyph, stepPerf, replayOutput, makeNGon, makeRect } from "@slithy/prim-lib";
|
|
230
245
|
export {
|
|
231
246
|
Circle,
|
|
232
247
|
Ellipse,
|
|
@@ -238,6 +253,8 @@ export {
|
|
|
238
253
|
copyRaster,
|
|
239
254
|
copyRasterFromVector,
|
|
240
255
|
copyVector,
|
|
256
|
+
makeNGon,
|
|
257
|
+
makeRect,
|
|
241
258
|
replayOutput,
|
|
242
259
|
run,
|
|
243
260
|
runWorker,
|
package/dist/worker-entry.d.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
import { PreCfg, StepData, SerializedOutput } from '@slithy/prim-lib';
|
|
1
|
+
import { NGonOptions, RectOptions, PreCfg, StepData, SerializedOutput } from '@slithy/prim-lib';
|
|
2
2
|
|
|
3
|
+
type ShapeTypeSpec = string | {
|
|
4
|
+
f: 'ngon';
|
|
5
|
+
o: NGonOptions;
|
|
6
|
+
} | {
|
|
7
|
+
f: 'rect';
|
|
8
|
+
o: RectOptions;
|
|
9
|
+
};
|
|
3
10
|
type WorkerCfg = Omit<PreCfg, 'shapeTypes'> & {
|
|
4
|
-
shapeTypeNames:
|
|
11
|
+
shapeTypeNames: ShapeTypeSpec[];
|
|
5
12
|
glyphChar?: string;
|
|
6
13
|
};
|
|
7
14
|
type WorkerInbound = {
|
|
@@ -34,6 +41,10 @@ type WorkerOutbound = {
|
|
|
34
41
|
serialized: SerializedOutput;
|
|
35
42
|
} | {
|
|
36
43
|
type: 'stopped';
|
|
44
|
+
serialized: SerializedOutput | null;
|
|
45
|
+
} | {
|
|
46
|
+
type: 'error';
|
|
47
|
+
message: string;
|
|
37
48
|
};
|
|
38
49
|
|
|
39
|
-
export type { WorkerCfg, WorkerInbound, WorkerOutbound };
|
|
50
|
+
export type { ShapeTypeSpec, WorkerCfg, WorkerInbound, WorkerOutbound };
|
package/dist/worker-entry.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/worker-entry.ts
|
|
2
|
-
import { Canvas, Optimizer, parseColor } from "@slithy/prim-lib";
|
|
2
|
+
import { Canvas, Optimizer, parseColor, makeNGon, makeRect } from "@slithy/prim-lib";
|
|
3
3
|
import { Triangle, Rectangle, Ellipse, Circle, Square, Hexagon, Glyph } from "@slithy/prim-lib";
|
|
4
4
|
var SHAPES = {
|
|
5
5
|
Triangle,
|
|
@@ -11,16 +11,26 @@ var SHAPES = {
|
|
|
11
11
|
Glyph
|
|
12
12
|
};
|
|
13
13
|
var optimizer = null;
|
|
14
|
+
var computeCfg = null;
|
|
15
|
+
var accumulatedSteps = [];
|
|
14
16
|
self.onmessage = async (e) => {
|
|
15
17
|
const msg = e.data;
|
|
16
18
|
switch (msg.type) {
|
|
17
19
|
case "start": {
|
|
18
20
|
const { url, cfg: workerCfg } = msg;
|
|
19
21
|
const { shapeTypeNames, glyphChar, ...rest } = workerCfg;
|
|
20
|
-
const shapeTypes = shapeTypeNames.map((
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
const shapeTypes = shapeTypeNames.map((spec) => {
|
|
23
|
+
if (typeof spec === "object") {
|
|
24
|
+
switch (spec.f) {
|
|
25
|
+
case "ngon":
|
|
26
|
+
return makeNGon(spec.o);
|
|
27
|
+
case "rect":
|
|
28
|
+
return makeRect(spec.o);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const ctor = SHAPES[spec];
|
|
32
|
+
if (!ctor) throw new Error(`[prim worker] Unknown shape type: ${spec}`);
|
|
33
|
+
if (spec === "Glyph" && glyphChar) {
|
|
24
34
|
const char = glyphChar;
|
|
25
35
|
return class GlyphWithChar extends Glyph {
|
|
26
36
|
constructor(w, h) {
|
|
@@ -31,11 +41,19 @@ self.onmessage = async (e) => {
|
|
|
31
41
|
return ctor;
|
|
32
42
|
});
|
|
33
43
|
const preCfg = { ...rest, shapeTypes };
|
|
34
|
-
|
|
35
|
-
|
|
44
|
+
let blob;
|
|
45
|
+
let bitmap;
|
|
46
|
+
try {
|
|
47
|
+
blob = await fetch(url).then((r) => r.blob());
|
|
48
|
+
bitmap = await createImageBitmap(blob);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
self.postMessage({ type: "error", message: err instanceof Error ? err.message : String(err) });
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
36
53
|
const original = Canvas.fromBitmap(bitmap, preCfg);
|
|
37
54
|
bitmap.close();
|
|
38
|
-
|
|
55
|
+
computeCfg = { ...preCfg, width: preCfg.width, height: preCfg.height };
|
|
56
|
+
accumulatedSteps = [];
|
|
39
57
|
self.postMessage({
|
|
40
58
|
type: "ready",
|
|
41
59
|
width: computeCfg.width,
|
|
@@ -43,9 +61,9 @@ self.onmessage = async (e) => {
|
|
|
43
61
|
scale: computeCfg.scale,
|
|
44
62
|
fill: computeCfg.fill
|
|
45
63
|
});
|
|
46
|
-
const accumulatedSteps = [];
|
|
47
64
|
let stepCount = 0;
|
|
48
|
-
|
|
65
|
+
const cc = computeCfg;
|
|
66
|
+
optimizer = new Optimizer(original, cc, (fn) => setTimeout(fn, 0));
|
|
49
67
|
optimizer.onStep = (step) => {
|
|
50
68
|
stepCount++;
|
|
51
69
|
if (step) {
|
|
@@ -56,19 +74,19 @@ self.onmessage = async (e) => {
|
|
|
56
74
|
stepData,
|
|
57
75
|
progress: {
|
|
58
76
|
current: stepCount,
|
|
59
|
-
total:
|
|
77
|
+
total: cc.steps,
|
|
60
78
|
similarity: parseFloat((100 * (1 - step.distance)).toFixed(2))
|
|
61
79
|
}
|
|
62
80
|
};
|
|
63
81
|
self.postMessage(outbound);
|
|
64
82
|
}
|
|
65
|
-
if (stepCount >=
|
|
83
|
+
if (stepCount >= cc.steps) {
|
|
66
84
|
const serialized = {
|
|
67
85
|
v: 1,
|
|
68
|
-
w:
|
|
69
|
-
h:
|
|
70
|
-
scale:
|
|
71
|
-
fill: parseColor(
|
|
86
|
+
w: cc.width,
|
|
87
|
+
h: cc.height,
|
|
88
|
+
scale: cc.scale,
|
|
89
|
+
fill: parseColor(cc.fill),
|
|
72
90
|
steps: accumulatedSteps
|
|
73
91
|
};
|
|
74
92
|
self.postMessage({ type: "complete", serialized });
|
|
@@ -85,7 +103,14 @@ self.onmessage = async (e) => {
|
|
|
85
103
|
break;
|
|
86
104
|
case "stop":
|
|
87
105
|
optimizer?.stop();
|
|
88
|
-
self.postMessage({ type: "stopped"
|
|
106
|
+
self.postMessage({ type: "stopped", serialized: computeCfg ? {
|
|
107
|
+
v: 1,
|
|
108
|
+
w: computeCfg.width,
|
|
109
|
+
h: computeCfg.height,
|
|
110
|
+
scale: computeCfg.scale,
|
|
111
|
+
fill: parseColor(computeCfg.fill),
|
|
112
|
+
steps: accumulatedSteps
|
|
113
|
+
} : null });
|
|
89
114
|
break;
|
|
90
115
|
}
|
|
91
116
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slithy/prim-interface",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Browser-facing API for primitive-based image reconstruction.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
],
|
|
19
19
|
"sideEffects": false,
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@slithy/prim-lib": "0.
|
|
21
|
+
"@slithy/prim-lib": "0.5.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@vitest/coverage-v8": "^4.1.2",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"tsup": "^8",
|
|
27
27
|
"typescript": "^5",
|
|
28
28
|
"vitest": "^4.1.2",
|
|
29
|
-
"@slithy/
|
|
30
|
-
"@slithy/
|
|
29
|
+
"@slithy/eslint-config": "0.0.0",
|
|
30
|
+
"@slithy/tsconfig": "0.0.0"
|
|
31
31
|
},
|
|
32
32
|
"author": {
|
|
33
33
|
"name": "Matthew Campagna",
|