@slithy/prim-interface 1.0.6 → 1.1.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.
@@ -0,0 +1,23 @@
1
+ Commercial License for @slithy/prim-interface
2
+
3
+ Copyright Matthew Campagna
4
+
5
+ 1. Grant
6
+
7
+ The individual copyright owner of `@slithy/prim-interface`, currently Matthew Campagna, is granted a personal, non-transferable, non-sublicensable license to use, reproduce, modify, distribute, and commercially exploit `@slithy/prim-interface` as part of software products and related internal development activities.
8
+
9
+ 2. Scope
10
+
11
+ This commercial license applies only to the natural person who is the individual copyright owner of `@slithy/prim-interface` at the time of use, in that person's personal capacity. It does not extend to any employer, client, contractor, affiliate, assignee, or other third party.
12
+
13
+ 3. Public License Unchanged
14
+
15
+ This commercial license is separate from the public license distributed with the package. All other recipients use the software, if at all, under the terms of the public license or another separately granted written license.
16
+
17
+ 4. Third-Party Attributions
18
+
19
+ This package includes or is derived from third-party material identified in the package `LICENSE` file. Those third-party attribution and notice requirements remain in effect.
20
+
21
+ 5. No Warranty
22
+
23
+ To the maximum extent permitted by law, the software is provided "as is", without warranty of any kind, express or implied.
@@ -0,0 +1,16 @@
1
+ // src/shared.ts
2
+ import { parseColor } from "@slithy/prim-lib";
3
+ function deriveDisplayConfigs(computeCfg, pixelRatio) {
4
+ const viewCfg = { ...computeCfg, width: computeCfg.scale * computeCfg.width, height: computeCfg.scale * computeCfg.height };
5
+ const displayFill = computeCfg.outputFill ?? computeCfg.fill;
6
+ const deviceViewCfg = { ...viewCfg, width: viewCfg.width * pixelRatio, height: viewCfg.height * pixelRatio, fill: displayFill };
7
+ return { viewCfg, deviceViewCfg };
8
+ }
9
+ function serializeOutput(cc, steps) {
10
+ return { v: 1, w: cc.width, h: cc.height, scale: cc.scale, fill: parseColor(cc.fill), steps };
11
+ }
12
+
13
+ export {
14
+ deriveDisplayConfigs,
15
+ serializeOutput
16
+ };
package/dist/index.d.ts CHANGED
@@ -1,23 +1,9 @@
1
- import { ShapeInterface, StepData, SerializedOutput } from '@slithy/prim-lib';
1
+ import { PreCfg, StepData, SerializedOutput } from '@slithy/prim-lib';
2
2
  export { Bbox, Circle, CircleOptions, Ellipse, EllipseOptions, Glyph, GlyphOptions, Hexagon, NGonOptions, RGB, RectOptions, Rectangle, ReplayResult, SerializedOutput, ShapeInterface, Square, StepData, Triangle, makeCircle, makeEllipse, makeGlyph, makeNGon, makeRect, replayOutput, stepPerf } from '@slithy/prim-lib';
3
3
 
4
- interface Config {
5
- steps: number;
6
- shapes: number;
7
- mutations: number;
8
- alpha: number;
9
- mutateAlpha: boolean;
10
- computeSize: number;
11
- viewSize: number;
12
- allowUpscale?: boolean;
4
+ interface Config extends PreCfg {
13
5
  pixelRatio?: number;
14
6
  glyphChar?: string;
15
- shapeTypes: Array<new (w: number, h: number) => ShapeInterface>;
16
- shapeWeights?: number[];
17
- fill: 'auto' | string;
18
- width?: number;
19
- height?: number;
20
- scale?: number;
21
7
  }
22
8
  interface StepResult {
23
9
  raster: HTMLCanvasElement;
@@ -66,7 +52,7 @@ interface ShareFromVectorOptions extends SaveOptions {
66
52
  quality?: number;
67
53
  }
68
54
  type Background = string | [png: string, jpeg: string];
69
- declare function saveRaster(canvas: HTMLCanvasElement, options?: SaveRasterOptions): void;
55
+ declare function saveRaster(canvas: HTMLCanvasElement, options?: SaveRasterOptions): Promise<void>;
70
56
  declare function saveRasterFromVector(svgString: string, width: number, height: number, options?: SaveRasterFromVectorOptions): Promise<void>;
71
57
  declare function copyRasterFromVector(svgString: string, width: number, height: number): Promise<void>;
72
58
  declare function shareFromVector(svgString: string, width: number, height: number, options?: ShareFromVectorOptions): Promise<void>;
package/dist/index.js CHANGED
@@ -1,9 +1,15 @@
1
+ import {
2
+ deriveDisplayConfigs,
3
+ serializeOutput
4
+ } from "./chunk-4GHCEQDS.js";
5
+
1
6
  // src/run.ts
2
- import { Canvas, Optimizer, parseColor } from "@slithy/prim-lib";
7
+ import { Canvas, Optimizer } from "@slithy/prim-lib";
3
8
  function run(source, cfg, callbacks = {}) {
4
9
  const url = source instanceof File ? URL.createObjectURL(source) : source;
5
10
  let optimizer = null;
6
11
  let stopped = false;
12
+ let settled = false;
7
13
  let computeCfg = null;
8
14
  const accumulatedSteps = [];
9
15
  Canvas.original(url, cfg).then((original) => {
@@ -14,21 +20,24 @@ function run(source, cfg, callbacks = {}) {
14
20
  if (source instanceof File) URL.revokeObjectURL(url);
15
21
  computeCfg = { ...cfg, width: cfg.width, height: cfg.height };
16
22
  const pixelRatio = cfg.pixelRatio ?? 1;
17
- const viewCfg = { ...computeCfg, width: computeCfg.scale * computeCfg.width, height: computeCfg.scale * computeCfg.height };
18
- const displayFill = computeCfg.outputFill ?? computeCfg.fill;
19
- const deviceViewCfg = { ...viewCfg, width: viewCfg.width * pixelRatio, height: viewCfg.height * pixelRatio, fill: displayFill };
23
+ const { viewCfg, deviceViewCfg } = deriveDisplayConfigs(computeCfg, pixelRatio);
20
24
  const result = Canvas.empty(deviceViewCfg);
21
25
  result.ctx.scale(computeCfg.scale * pixelRatio, computeCfg.scale * pixelRatio);
22
26
  if (!(result.node instanceof HTMLCanvasElement)) throw new Error("[prim] Expected HTMLCanvasElement");
23
27
  const rasterNode = result.node;
28
+ rasterNode.style.width = `${viewCfg.width}px`;
24
29
  const svg = Canvas.empty(computeCfg, true);
25
30
  svg.setAttribute("width", String(viewCfg.width));
26
31
  svg.setAttribute("height", String(viewCfg.height));
27
32
  callbacks.onStart?.(rasterNode, svg);
28
33
  const serializer = new XMLSerializer();
29
34
  optimizer = new Optimizer(original, computeCfg);
35
+ optimizer.onError = (err) => {
36
+ callbacks.onError?.(err instanceof Error ? err.message : String(err));
37
+ };
30
38
  let stepCount = 0;
31
39
  optimizer.onStep = (step) => {
40
+ if (settled) return;
32
41
  stepCount++;
33
42
  if (step) {
34
43
  result.drawStep(step);
@@ -38,7 +47,9 @@ function run(source, cfg, callbacks = {}) {
38
47
  callbacks.onStep?.({
39
48
  raster: rasterNode,
40
49
  svg,
41
- svgString: serializer.serializeToString(svg),
50
+ get svgString() {
51
+ return serializer.serializeToString(svg);
52
+ },
42
53
  stepData,
43
54
  progress: {
44
55
  current: stepCount,
@@ -48,14 +59,8 @@ function run(source, cfg, callbacks = {}) {
48
59
  });
49
60
  }
50
61
  if (stepCount >= cfg.steps) {
51
- callbacks.onComplete?.({
52
- v: 1,
53
- w: computeCfg.width,
54
- h: computeCfg.height,
55
- scale: computeCfg.scale,
56
- fill: parseColor(cfg.fill),
57
- steps: accumulatedSteps
58
- });
62
+ settled = true;
63
+ callbacks.onComplete?.(serializeOutput(computeCfg, accumulatedSteps));
59
64
  }
60
65
  };
61
66
  optimizer.start();
@@ -66,15 +71,10 @@ function run(source, cfg, callbacks = {}) {
66
71
  return {
67
72
  stop: () => {
68
73
  stopped = true;
74
+ if (settled) return;
75
+ settled = true;
69
76
  optimizer?.stop();
70
- callbacks.onStop?.(computeCfg ? {
71
- v: 1,
72
- w: computeCfg.width,
73
- h: computeCfg.height,
74
- scale: computeCfg.scale,
75
- fill: parseColor(cfg.fill),
76
- steps: accumulatedSteps
77
- } : null);
77
+ callbacks.onStop?.(computeCfg ? serializeOutput(computeCfg, accumulatedSteps) : null);
78
78
  },
79
79
  pause: () => optimizer?.pause(),
80
80
  resume: () => optimizer?.resume()
@@ -89,6 +89,7 @@ function runWorker(source, cfg, callbacks = {}) {
89
89
  let stopped = false;
90
90
  let settled = false;
91
91
  let result = null;
92
+ let displayCanvas = null;
92
93
  let svgEl = null;
93
94
  const serializer = new XMLSerializer();
94
95
  const worker = new Worker(new URL("./worker-entry.js", import.meta.url), { type: "module" });
@@ -109,20 +110,10 @@ function runWorker(source, cfg, callbacks = {}) {
109
110
  const { width, height, scale, fill, outputFill } = msg;
110
111
  const pixelRatio = cfg.pixelRatio ?? 1;
111
112
  const computeCfg = { ...cfg, width, height, scale, fill, outputFill };
112
- const viewCfg = {
113
- ...computeCfg,
114
- width: scale * width,
115
- height: scale * height
116
- };
117
- const displayFill = outputFill ?? fill;
118
- const deviceViewCfg = {
119
- ...viewCfg,
120
- width: viewCfg.width * pixelRatio,
121
- height: viewCfg.height * pixelRatio,
122
- fill: displayFill
123
- };
113
+ const { viewCfg, deviceViewCfg } = deriveDisplayConfigs(computeCfg, pixelRatio);
124
114
  result = Canvas2.empty(deviceViewCfg);
125
- const displayCanvas = result.node;
115
+ if (!(result.node instanceof HTMLCanvasElement)) throw new Error("[prim] Expected HTMLCanvasElement");
116
+ displayCanvas = result.node;
126
117
  displayCanvas.style.width = `${viewCfg.width}px`;
127
118
  result.ctx.scale(scale * pixelRatio, scale * pixelRatio);
128
119
  svgEl = Canvas2.empty(computeCfg, true);
@@ -130,14 +121,17 @@ function runWorker(source, cfg, callbacks = {}) {
130
121
  svgEl.setAttribute("height", String(viewCfg.height));
131
122
  callbacks.onStart?.(displayCanvas, svgEl);
132
123
  } else if (msg.type === "step") {
133
- if (!result || !svgEl) return;
124
+ if (!result || !svgEl || !displayCanvas) return;
125
+ const svg = svgEl;
134
126
  const { stepData, progress } = msg;
135
127
  renderStepToCtx(stepData, result.ctx);
136
- svgEl.appendChild(stepDataToSVGElement(stepData));
128
+ svg.appendChild(stepDataToSVGElement(stepData));
137
129
  callbacks.onStep?.({
138
- raster: result.node,
139
- svg: svgEl,
140
- svgString: serializer.serializeToString(svgEl),
130
+ raster: displayCanvas,
131
+ svg,
132
+ get svgString() {
133
+ return serializer.serializeToString(svg);
134
+ },
141
135
  stepData,
142
136
  progress
143
137
  });
@@ -228,19 +222,21 @@ function svgToCanvas(svgString, width, height, background) {
228
222
  img.src = url;
229
223
  });
230
224
  }
231
- function saveRaster(canvas, options) {
225
+ async function saveRaster(canvas, options) {
232
226
  const format = options?.format ?? "png";
233
227
  const quality = options?.quality ?? 0.92;
234
228
  const mimeType = format === "jpeg" ? "image/jpeg" : "image/png";
235
229
  const ext = format === "jpeg" ? "jpg" : "png";
236
- const dataUrl = canvas.toDataURL(mimeType, quality);
237
- triggerDownload(dataUrl, buildFilename(ext, options));
230
+ const blob = await new Promise((resolve, reject) => canvas.toBlob((b) => b ? resolve(b) : reject(new Error("toBlob failed")), mimeType, quality));
231
+ const url = URL.createObjectURL(blob);
232
+ triggerDownload(url, buildFilename(ext, options));
233
+ URL.revokeObjectURL(url);
238
234
  }
239
235
  async function saveRasterFromVector(svgString, width, height, options) {
240
236
  const format = options?.format ?? "png";
241
237
  const quality = options?.quality ?? 0.92;
242
238
  const canvas = await svgToCanvas(svgString, width, height, resolveBackground(options?.background, format));
243
- saveRaster(canvas, { format, quality, suffix: options?.suffix, filename: options?.filename });
239
+ await saveRaster(canvas, { format, quality, suffix: options?.suffix, filename: options?.filename });
244
240
  }
245
241
  async function copyRasterFromVector(svgString, width, height) {
246
242
  const canvas = await svgToCanvas(svgString, width, height);
@@ -260,16 +256,8 @@ function saveVector(svgString, options) {
260
256
  async function copyVector(svgString) {
261
257
  await navigator.clipboard.writeText(svgString);
262
258
  }
263
- function dataUrlToBlob(dataUrl) {
264
- const [header, data] = dataUrl.split(",");
265
- const mime = header.match(/:(.*?);/)[1];
266
- const bytes = atob(data);
267
- const arr = new Uint8Array(bytes.length);
268
- for (let i = 0; i < bytes.length; i++) arr[i] = bytes.charCodeAt(i);
269
- return new Blob([arr], { type: mime });
270
- }
271
259
  async function copyRaster(canvas) {
272
- const blob = dataUrlToBlob(canvas.toDataURL("image/png"));
260
+ const blob = await new Promise((resolve, reject) => canvas.toBlob((b) => b ? resolve(b) : reject(new Error("toBlob failed")), "image/png"));
273
261
  await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]);
274
262
  }
275
263
  async function share(canvas, options) {
@@ -279,7 +267,7 @@ async function share(canvas, options) {
279
267
  const ext = format === "jpeg" ? "jpg" : "png";
280
268
  const blob = await new Promise((resolve, reject) => canvas.toBlob((b) => b ? resolve(b) : reject(new Error("toBlob failed")), mimeType, quality));
281
269
  const file = new File([blob], buildFilename(ext, options), { type: mimeType });
282
- if (navigator.canShare({ files: [file] })) {
270
+ if (navigator.canShare?.({ files: [file] })) {
283
271
  await navigator.share({ files: [file] });
284
272
  }
285
273
  }
@@ -1,5 +1,9 @@
1
+ import {
2
+ serializeOutput
3
+ } from "./chunk-4GHCEQDS.js";
4
+
1
5
  // src/worker-entry.ts
2
- import { Canvas, Optimizer, parseColor, makeNGon, makeRect, makeCircle, makeEllipse, makeGlyph } from "@slithy/prim-lib";
6
+ import { Canvas, Optimizer, makeNGon, makeRect, makeCircle, makeEllipse, makeGlyph } from "@slithy/prim-lib";
3
7
  import { Triangle, Rectangle, Ellipse, Circle, Square, Hexagon, Glyph } from "@slithy/prim-lib";
4
8
  var SHAPES = {
5
9
  Triangle,
@@ -17,6 +21,7 @@ self.onmessage = async (e) => {
17
21
  const msg = e.data;
18
22
  switch (msg.type) {
19
23
  case "start": {
24
+ optimizer?.stop();
20
25
  const { url, cfg: workerCfg } = msg;
21
26
  const { shapeTypeNames, glyphChar, ...rest } = workerCfg;
22
27
  const shapeTypes = shapeTypeNames.map((spec) => {
@@ -50,7 +55,10 @@ self.onmessage = async (e) => {
50
55
  let blob;
51
56
  let bitmap;
52
57
  try {
53
- blob = await fetch(url).then((r) => r.blob());
58
+ blob = await fetch(url).then((r) => {
59
+ if (!r.ok) throw new Error(`Failed to fetch image: ${r.status} ${r.statusText}`);
60
+ return r.blob();
61
+ });
54
62
  bitmap = await createImageBitmap(blob);
55
63
  } catch (err) {
56
64
  self.postMessage({ type: "error", message: err instanceof Error ? err.message : String(err) });
@@ -71,6 +79,9 @@ self.onmessage = async (e) => {
71
79
  let stepCount = 0;
72
80
  const cc = computeCfg;
73
81
  optimizer = new Optimizer(original, cc, (fn) => setTimeout(fn, 0));
82
+ optimizer.onError = (err) => {
83
+ self.postMessage({ type: "error", message: err instanceof Error ? err.message : String(err) });
84
+ };
74
85
  optimizer.onStep = (step) => {
75
86
  stepCount++;
76
87
  if (step) {
@@ -88,14 +99,7 @@ self.onmessage = async (e) => {
88
99
  self.postMessage(outbound);
89
100
  }
90
101
  if (stepCount >= cc.steps) {
91
- const serialized = {
92
- v: 1,
93
- w: cc.width,
94
- h: cc.height,
95
- scale: cc.scale,
96
- fill: parseColor(cc.fill),
97
- steps: accumulatedSteps
98
- };
102
+ const serialized = serializeOutput(cc, accumulatedSteps);
99
103
  self.postMessage({ type: "complete", serialized });
100
104
  }
101
105
  };
@@ -110,14 +114,7 @@ self.onmessage = async (e) => {
110
114
  break;
111
115
  case "stop":
112
116
  optimizer?.stop();
113
- self.postMessage({ type: "stopped", serialized: computeCfg ? {
114
- v: 1,
115
- w: computeCfg.width,
116
- h: computeCfg.height,
117
- scale: computeCfg.scale,
118
- fill: parseColor(computeCfg.fill),
119
- steps: accumulatedSteps
120
- } : null });
117
+ self.postMessage({ type: "stopped", serialized: computeCfg ? serializeOutput(computeCfg, accumulatedSteps) : null });
121
118
  break;
122
119
  }
123
120
  };
package/package.json CHANGED
@@ -1,39 +1,43 @@
1
1
  {
2
2
  "name": "@slithy/prim-interface",
3
- "version": "1.0.6",
3
+ "version": "1.1.0",
4
4
  "description": "Browser-facing API for primitive-based image reconstruction.",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {
8
- "import": "./dist/index.js",
9
- "types": "./dist/index.d.ts"
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.js"
10
10
  },
11
11
  "./worker-entry": {
12
- "import": "./dist/worker-entry.js",
13
- "types": "./dist/worker-entry.d.ts"
12
+ "types": "./dist/worker-entry.d.ts",
13
+ "import": "./dist/worker-entry.js"
14
14
  }
15
15
  },
16
16
  "files": [
17
- "dist"
17
+ "dist",
18
+ "LICENSE-COMMERCIAL.md"
19
+ ],
20
+ "sideEffects": [
21
+ "./dist/worker-entry.js"
18
22
  ],
19
- "sideEffects": false,
20
23
  "dependencies": {
21
- "@slithy/prim-lib": "0.8.1"
24
+ "@slithy/prim-lib": "0.9.0"
22
25
  },
23
26
  "devDependencies": {
24
- "@vitest/coverage-v8": "^4.1.7",
27
+ "@vitest/coverage-v8": "^4.1.9",
25
28
  "jsdom": "^29.1.1",
26
29
  "tsup": "^8",
27
30
  "typescript": "^5",
28
- "vitest": "^4.1.7",
29
- "@slithy/eslint-config": "0.0.0",
30
- "@slithy/tsconfig": "0.0.0"
31
+ "vitest": "^4.1.9",
32
+ "@slithy/tsconfig": "0.0.0",
33
+ "@slithy/eslint-config": "0.0.0"
31
34
  },
32
35
  "author": {
33
36
  "name": "Matthew Campagna",
34
37
  "url": "https://github.com/mjcampagna"
35
38
  },
36
39
  "license": "PolyForm-Noncommercial-1.0.0",
40
+ "commercialLicense": "SEE LICENSE-COMMERCIAL.md",
37
41
  "keywords": [
38
42
  "image",
39
43
  "reconstruction",