@slithy/prim-interface 1.0.7 → 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.
- package/LICENSE-COMMERCIAL.md +2 -2
- package/dist/chunk-4GHCEQDS.js +16 -0
- package/dist/index.d.ts +3 -17
- package/dist/index.js +41 -53
- package/dist/worker-entry.js +15 -18
- package/package.json +13 -11
package/LICENSE-COMMERCIAL.md
CHANGED
|
@@ -4,11 +4,11 @@ Copyright Matthew Campagna
|
|
|
4
4
|
|
|
5
5
|
1. Grant
|
|
6
6
|
|
|
7
|
-
|
|
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
8
|
|
|
9
9
|
2. Scope
|
|
10
10
|
|
|
11
|
-
This commercial license applies only to
|
|
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
12
|
|
|
13
13
|
3. Public License Unchanged
|
|
14
14
|
|
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
128
|
+
svg.appendChild(stepDataToSVGElement(stepData));
|
|
137
129
|
callbacks.onStep?.({
|
|
138
|
-
raster:
|
|
139
|
-
svg
|
|
140
|
-
svgString
|
|
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
|
|
237
|
-
|
|
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 =
|
|
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
|
}
|
package/dist/worker-entry.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
serializeOutput
|
|
3
|
+
} from "./chunk-4GHCEQDS.js";
|
|
4
|
+
|
|
1
5
|
// src/worker-entry.ts
|
|
2
|
-
import { Canvas, Optimizer,
|
|
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) =>
|
|
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,34 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slithy/prim-interface",
|
|
3
|
-
"version": "1.0
|
|
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
|
-
"
|
|
9
|
-
"
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.js"
|
|
10
10
|
},
|
|
11
11
|
"./worker-entry": {
|
|
12
|
-
"
|
|
13
|
-
"
|
|
12
|
+
"types": "./dist/worker-entry.d.ts",
|
|
13
|
+
"import": "./dist/worker-entry.js"
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"dist",
|
|
18
18
|
"LICENSE-COMMERCIAL.md"
|
|
19
19
|
],
|
|
20
|
-
"sideEffects":
|
|
20
|
+
"sideEffects": [
|
|
21
|
+
"./dist/worker-entry.js"
|
|
22
|
+
],
|
|
21
23
|
"dependencies": {
|
|
22
|
-
"@slithy/prim-lib": "0.
|
|
24
|
+
"@slithy/prim-lib": "0.9.0"
|
|
23
25
|
},
|
|
24
26
|
"devDependencies": {
|
|
25
|
-
"@vitest/coverage-v8": "^4.1.
|
|
27
|
+
"@vitest/coverage-v8": "^4.1.9",
|
|
26
28
|
"jsdom": "^29.1.1",
|
|
27
29
|
"tsup": "^8",
|
|
28
30
|
"typescript": "^5",
|
|
29
|
-
"vitest": "^4.1.
|
|
30
|
-
"@slithy/
|
|
31
|
-
"@slithy/
|
|
31
|
+
"vitest": "^4.1.9",
|
|
32
|
+
"@slithy/tsconfig": "0.0.0",
|
|
33
|
+
"@slithy/eslint-config": "0.0.0"
|
|
32
34
|
},
|
|
33
35
|
"author": {
|
|
34
36
|
"name": "Matthew Campagna",
|