@slithy/prim-interface 1.0.3 → 1.0.5

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
@@ -105,7 +105,7 @@ interface StepResult {
105
105
 
106
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()`.
107
107
 
108
- **`onStop()`** — called when the job is stopped via `job.stop()`.
108
+ **`onStop(serialized)`** — called when the job is stopped via `job.stop()`. Receives the `SerializedOutput` of shapes placed so far, or `null` if stopped before any shapes were placed.
109
109
 
110
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
111
 
@@ -113,13 +113,13 @@ interface StepResult {
113
113
 
114
114
  Higher-level (rasterize SVG at save time for crisp output):
115
115
 
116
- - **`saveRasterFromVector(svgString, width, height, format?, quality?, options?, background?)`** — downloads PNG or JPEG rasterized from the SVG. `background` fills the canvas before drawing: JPEG defaults to `'white'` (prevents black transparent areas); PNG defaults to transparent. Pass a `Background` value to override.
116
+ - **`saveRasterFromVector(svgString, width, height, options?)`** — downloads PNG or JPEG rasterized from the SVG. `background` fills the canvas before drawing: JPEG defaults to `'white'` (prevents black transparent areas); PNG defaults to transparent.
117
117
  - **`copyRasterFromVector(svgString, width, height)`** — copies a transparent PNG rasterized from the SVG to the clipboard
118
118
  - **`shareFromVector(svgString, width, height, options?)`** — shares a transparent PNG rasterized from the SVG via the Web Share API
119
119
 
120
120
  Lower-level (operate directly on a canvas or SVG string):
121
121
 
122
- - **`saveRaster(canvas, format?, quality?, options?)`** — downloads PNG or JPEG from a canvas
122
+ - **`saveRaster(canvas, options?)`** — downloads PNG or JPEG from a canvas
123
123
  - **`saveVector(svgString, options?)`** — downloads SVG
124
124
  - **`copyRaster(canvas)`** — copies PNG to clipboard
125
125
  - **`copyVector(svgString)`** — copies SVG markup to clipboard
@@ -134,7 +134,13 @@ interface SaveOptions {
134
134
  }
135
135
  ```
136
136
 
137
- Example: `saveRaster(canvas, 'png', 0.92, { filename: 'mountains', suffix: 'v2' })` `mountains--v2.png`
137
+ **`SaveRasterOptions`** extends `SaveOptions` with `format?: 'png' | 'jpeg'` and `quality?: number`.
138
+
139
+ **`SaveRasterFromVectorOptions`** extends `SaveOptions` with `format?`, `quality?`, and `background?: Background`.
140
+
141
+ **`ShareFromVectorOptions`** extends `SaveOptions` with `background?: Background`.
142
+
143
+ Example: `saveRaster(canvas, { format: 'png', quality: 0.92, filename: 'mountains', suffix: 'v2' })` → `mountains--v2.png`
138
144
 
139
145
  ### `replayOutput(data: SerializedOutput): ReplayResult`
140
146
 
@@ -148,13 +154,13 @@ const { raster, svg, svgString } = replayOutput(serializedData);
148
154
 
149
155
  **Shapes:** `Triangle`, `Rectangle`, `Ellipse`, `Circle`, `Square`, `Hexagon`, `Glyph`
150
156
 
151
- **Factories:** `makeNGon`, `makeRect`
157
+ **Factories:** `makeNGon`, `makeRect`, `makeCircle`, `makeEllipse`, `makeGlyph`
152
158
 
153
- **Factory option types:** `NGonOptions`, `RectOptions`
159
+ **Factory option types:** `NGonOptions`, `RectOptions`, `CircleOptions`, `EllipseOptions`, `GlyphOptions`
154
160
 
155
161
  **Functions:** `stepPerf`, `replayOutput`
156
162
 
157
- **Types:** `RGB`, `SerializedOutput`, `StepData`, `ReplayResult`, `ShapeInterface`, `Bbox`, `Background`
163
+ **Types:** `RGB`, `SerializedOutput`, `StepData`, `ReplayResult`, `ShapeInterface`, `Bbox`, `Background`, `SaveRasterOptions`, `SaveRasterFromVectorOptions`, `ShareFromVectorOptions`
158
164
 
159
165
  `Background` controls the canvas fill for `saveRasterFromVector`:
160
166
 
package/dist/index.d.ts CHANGED
@@ -62,6 +62,8 @@ interface SaveRasterFromVectorOptions extends SaveOptions {
62
62
  }
63
63
  interface ShareFromVectorOptions extends SaveOptions {
64
64
  background?: Background;
65
+ format?: 'png' | 'jpeg';
66
+ quality?: number;
65
67
  }
66
68
  type Background = string | [png: string, jpeg: string];
67
69
  declare function saveRaster(canvas: HTMLCanvasElement, options?: SaveRasterOptions): void;
@@ -71,6 +73,6 @@ declare function shareFromVector(svgString: string, width: number, height: numbe
71
73
  declare function saveVector(svgString: string, options?: SaveOptions): void;
72
74
  declare function copyVector(svgString: string): Promise<void>;
73
75
  declare function copyRaster(canvas: HTMLCanvasElement): Promise<void>;
74
- declare function share(canvas: HTMLCanvasElement, options?: SaveOptions): Promise<void>;
76
+ declare function share(canvas: HTMLCanvasElement, options?: SaveRasterOptions): Promise<void>;
75
77
 
76
78
  export { type Background, type Config, type JobHandle, type RunCallbacks, type SaveOptions, type SaveRasterFromVectorOptions, type SaveRasterOptions, type ShareFromVectorOptions, type StepResult, copyRaster, copyRasterFromVector, copyVector, run, runWorker, saveRaster, saveRasterFromVector, saveVector, share, shareFromVector };
package/dist/index.js CHANGED
@@ -87,11 +87,23 @@ function runWorker(source, cfg, callbacks = {}) {
87
87
  const url = source instanceof File ? URL.createObjectURL(source) : new URL(source, globalThis.location.href).href;
88
88
  const isBlob = source instanceof File;
89
89
  let stopped = false;
90
+ let settled = false;
90
91
  let result = null;
91
92
  let svgEl = null;
92
93
  const serializer = new XMLSerializer();
93
94
  const worker = new Worker(new URL("./worker-entry.js", import.meta.url), { type: "module" });
95
+ const finalize = (cb) => {
96
+ if (settled) return;
97
+ settled = true;
98
+ if (isBlob) URL.revokeObjectURL(url);
99
+ try {
100
+ cb();
101
+ } finally {
102
+ worker.terminate();
103
+ }
104
+ };
94
105
  worker.onmessage = (e) => {
106
+ if (settled) return;
95
107
  const msg = e.data;
96
108
  if (msg.type === "ready") {
97
109
  const { width, height, scale, fill, outputFill } = msg;
@@ -130,19 +142,23 @@ function runWorker(source, cfg, callbacks = {}) {
130
142
  progress
131
143
  });
132
144
  } else if (msg.type === "complete") {
133
- if (isBlob) URL.revokeObjectURL(url);
134
- callbacks.onComplete?.(msg.serialized);
145
+ finalize(() => {
146
+ callbacks.onComplete?.(msg.serialized);
147
+ });
135
148
  } else if (msg.type === "stopped") {
136
- if (isBlob) URL.revokeObjectURL(url);
137
- callbacks.onStop?.(msg.serialized);
149
+ finalize(() => {
150
+ callbacks.onStop?.(msg.serialized);
151
+ });
138
152
  } else if (msg.type === "error") {
139
- if (isBlob) URL.revokeObjectURL(url);
140
- callbacks.onError?.(msg.message);
153
+ finalize(() => {
154
+ callbacks.onError?.(msg.message);
155
+ });
141
156
  }
142
157
  };
143
158
  worker.onerror = (e) => {
144
- if (isBlob) URL.revokeObjectURL(url);
145
- callbacks.onError?.(e.message ?? "Unknown worker error");
159
+ finalize(() => {
160
+ callbacks.onError?.(e.message ?? "Unknown worker error");
161
+ });
146
162
  };
147
163
  const { shapeTypes, pixelRatio: _pr, width: _w, height: _h, scale: _s, ...rest } = cfg;
148
164
  const workerCfg = {
@@ -154,12 +170,18 @@ function runWorker(source, cfg, callbacks = {}) {
154
170
  worker.postMessage({ type: "start", url, cfg: workerCfg });
155
171
  return {
156
172
  stop: () => {
157
- if (stopped) return;
173
+ if (stopped || settled) return;
158
174
  stopped = true;
159
175
  worker.postMessage({ type: "stop" });
160
176
  },
161
- pause: () => worker.postMessage({ type: "pause" }),
162
- resume: () => worker.postMessage({ type: "resume" })
177
+ pause: () => {
178
+ if (settled) return;
179
+ worker.postMessage({ type: "pause" });
180
+ },
181
+ resume: () => {
182
+ if (settled) return;
183
+ worker.postMessage({ type: "resume" });
184
+ }
163
185
  };
164
186
  }
165
187
 
@@ -225,7 +247,8 @@ async function copyRasterFromVector(svgString, width, height) {
225
247
  return copyRaster(canvas);
226
248
  }
227
249
  async function shareFromVector(svgString, width, height, options) {
228
- const canvas = await svgToCanvas(svgString, width, height, resolveBackground(options?.background, "png"));
250
+ const format = options?.format ?? "png";
251
+ const canvas = await svgToCanvas(svgString, width, height, resolveBackground(options?.background, format));
229
252
  return share(canvas, options);
230
253
  }
231
254
  function saveVector(svgString, options) {
@@ -250,8 +273,12 @@ async function copyRaster(canvas) {
250
273
  await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]);
251
274
  }
252
275
  async function share(canvas, options) {
253
- const blob = await new Promise((resolve, reject) => canvas.toBlob((b) => b ? resolve(b) : reject(new Error("toBlob failed"))));
254
- const file = new File([blob], buildFilename("png", options), { type: "image/png" });
276
+ const format = options?.format ?? "png";
277
+ const quality = options?.quality ?? 0.92;
278
+ const mimeType = format === "jpeg" ? "image/jpeg" : "image/png";
279
+ const ext = format === "jpeg" ? "jpg" : "png";
280
+ const blob = await new Promise((resolve, reject) => canvas.toBlob((b) => b ? resolve(b) : reject(new Error("toBlob failed")), mimeType, quality));
281
+ const file = new File([blob], buildFilename(ext, options), { type: mimeType });
255
282
  if (navigator.canShare({ files: [file] })) {
256
283
  await navigator.share({ files: [file] });
257
284
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slithy/prim-interface",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Browser-facing API for primitive-based image reconstruction.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -26,8 +26,8 @@
26
26
  "tsup": "^8",
27
27
  "typescript": "^5",
28
28
  "vitest": "^4.1.2",
29
- "@slithy/eslint-config": "0.0.0",
30
- "@slithy/tsconfig": "0.0.0"
29
+ "@slithy/tsconfig": "0.0.0",
30
+ "@slithy/eslint-config": "0.0.0"
31
31
  },
32
32
  "author": {
33
33
  "name": "Matthew Campagna",