@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 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. Shape constructors in `config.shapeTypes` are mapped to names internally; only the built-in shapes (`Triangle`, `Rectangle`, `Ellipse`, `Circle`, `Square`, `Hexagon`, `Glyph`) are supported.
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`, `stepPerf`, `replayOutput`, `RGB`, `SerializedOutput`, `StepData`, `ReplayResult`
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
- const computeCfg = { ...cfg, width: cfg.width, height: cfg.height };
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
- console.error("[prim] run failed:", err);
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
- console.error("[prim worker]", e);
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((ctor) => ctor.name)
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,
@@ -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: string[];
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 };
@@ -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((name) => {
21
- const ctor = SHAPES[name];
22
- if (!ctor) throw new Error(`[prim worker] Unknown shape type: ${name}`);
23
- if (name === "Glyph" && glyphChar) {
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
- const blob = await fetch(url).then((r) => r.blob());
35
- const bitmap = await createImageBitmap(blob);
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
- const computeCfg = { ...preCfg, width: preCfg.width, height: preCfg.height };
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
- optimizer = new Optimizer(original, computeCfg, (fn) => setTimeout(fn, 0));
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: computeCfg.steps,
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 >= computeCfg.steps) {
83
+ if (stepCount >= cc.steps) {
66
84
  const serialized = {
67
85
  v: 1,
68
- w: computeCfg.width,
69
- h: computeCfg.height,
70
- scale: computeCfg.scale,
71
- fill: parseColor(computeCfg.fill),
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.3",
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.4.1"
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/tsconfig": "0.0.0",
30
- "@slithy/eslint-config": "0.0.0"
29
+ "@slithy/eslint-config": "0.0.0",
30
+ "@slithy/tsconfig": "0.0.0"
31
31
  },
32
32
  "author": {
33
33
  "name": "Matthew Campagna",