@slithy/prim-lib 0.1.1 → 0.2.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 +58 -12
- package/dist/index.d.ts +74 -3
- package/dist/index.js +202 -48
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ Reconstructs an image by iteratively placing geometric shapes. Each step evaluat
|
|
|
18
18
|
### Shapes
|
|
19
19
|
|
|
20
20
|
- **`Shape`** — abstract base
|
|
21
|
-
- **`Triangle`**, **`Rectangle`**, **`Ellipse`**, **`Hexagon`**, **`
|
|
21
|
+
- **`Triangle`**, **`Rectangle`**, **`Ellipse`**, **`Square`**, **`Hexagon`**, **`Glyph`**, **`Debug`**
|
|
22
22
|
|
|
23
23
|
### Types
|
|
24
24
|
|
|
@@ -26,27 +26,71 @@ Reconstructs an image by iteratively placing geometric shapes. Each step evaluat
|
|
|
26
26
|
|
|
27
27
|
```ts
|
|
28
28
|
interface Cfg {
|
|
29
|
-
width: number
|
|
30
|
-
height: number
|
|
31
|
-
steps: number
|
|
32
|
-
shapes: number
|
|
33
|
-
mutations: number
|
|
34
|
-
alpha: number
|
|
35
|
-
mutateAlpha: boolean
|
|
36
|
-
computeSize: number
|
|
37
|
-
viewSize: number
|
|
38
|
-
|
|
29
|
+
width: number // image width for computation (set by Canvas.original)
|
|
30
|
+
height: number // image height for computation (set by Canvas.original)
|
|
31
|
+
steps: number // total shapes to place
|
|
32
|
+
shapes: number // candidate shapes evaluated per step
|
|
33
|
+
mutations: number // hill-climb attempts per candidate
|
|
34
|
+
alpha: number // starting shape opacity
|
|
35
|
+
mutateAlpha: boolean // whether opacity is mutated per candidate
|
|
36
|
+
computeSize: number // max image dimension for pixel distance calculations
|
|
37
|
+
viewSize: number // max image dimension for display canvas
|
|
38
|
+
allowUpscale?: boolean // allow output larger than source image (default: false)
|
|
39
|
+
scale?: number // viewSize / computeSize ratio (set by Canvas.original)
|
|
39
40
|
shapeTypes: Array<new (w: number, h: number) => ShapeInterface>
|
|
40
41
|
fill: 'auto' | string
|
|
41
42
|
}
|
|
42
43
|
```
|
|
43
44
|
|
|
44
45
|
- **`PreCfg`** — `Cfg` with optional `width`/`height`; used before image dimensions are known
|
|
45
|
-
- **`ShapeInterface`** — structural interface for shapes
|
|
46
|
+
- **`ShapeInterface`** — structural interface for shapes; includes `toData(alpha, color): StepData`
|
|
46
47
|
- **`Bbox`**, **`Point`**, **`ImageDataLike`**, **`ShapeImageData`**
|
|
47
48
|
|
|
49
|
+
**`RGB`** — `[number, number, number]` tuple
|
|
50
|
+
|
|
51
|
+
**`StepData`** — discriminated union of serialized shape data, keyed by `t`:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
type StepData =
|
|
55
|
+
| { t: 't'; a: number; c: RGB; pts: [number, number][] } // Triangle
|
|
56
|
+
| { t: 'r'; a: number; c: RGB; pts: [number, number][] } // Rectangle
|
|
57
|
+
| { t: 'e'; a: number; c: RGB; cx: number; cy: number; rx: number; ry: number } // Ellipse
|
|
58
|
+
| { t: 's'; a: number; c: RGB; cx: number; cy: number; r: number } // Square
|
|
59
|
+
| { t: 'h'; a: number; c: RGB; cx: number; cy: number; r: number; angle: number } // Hexagon
|
|
60
|
+
| { t: 'sm'; a: number; c: RGB; cx: number; cy: number; fs: number; text: string } // Glyph
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**`SerializedOutput`** — compact, storage-ready representation of a completed run:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
interface SerializedOutput {
|
|
67
|
+
v: 1 // schema version
|
|
68
|
+
w: number // compute width
|
|
69
|
+
h: number // compute height
|
|
70
|
+
scale: number // viewSize / computeSize ratio
|
|
71
|
+
fill: RGB // background fill color
|
|
72
|
+
steps: StepData[]
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**`ReplayResult`** — returned by `replayOutput`:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
interface ReplayResult {
|
|
80
|
+
raster: HTMLCanvasElement
|
|
81
|
+
svg: SVGSVGElement
|
|
82
|
+
svgString: string
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Functions
|
|
87
|
+
|
|
88
|
+
**`replayOutput(data: SerializedOutput): ReplayResult`** — reconstructs canvas and SVG natively from serialized output, without re-running the optimizer. Useful for restoring saved results from localStorage or a database.
|
|
89
|
+
|
|
48
90
|
### Utilities
|
|
49
91
|
|
|
92
|
+
- **`getFill(data: ImageDataLike): string`** — computes a fill color (average of corner pixels) from image data
|
|
93
|
+
- **`parseColor(str: string): RGB`** — parses `"rgb(r, g, b)"` to an `[r, g, b]` tuple
|
|
50
94
|
- **`stepPerf`** — accumulator for profiling rasterize vs pixel math time per step
|
|
51
95
|
|
|
52
96
|
## Differences from primitive.js
|
|
@@ -60,9 +104,11 @@ interface Cfg {
|
|
|
60
104
|
- **Workers removed** — the original included worker stubs (`importScripts()`-based, not ESM-compatible). Workers were investigated and benchmarked; after the canvas reuse optimization, a batched worker implementation matched single-threaded performance. Workers are not used
|
|
61
105
|
- **Architecture split** — the original is a single-layer library. Here, `prim-lib` is the pure algorithm (no knowledge of how images arrive or where results go), and `prim-interface` is the adapter that wires it to the browser
|
|
62
106
|
- **`PreCfg` / `Cfg` distinction** — `Canvas.original()` accepts `PreCfg` (optional `width`/`height`) and resolves to a fully-populated `Cfg`, making the config lifecycle explicit in the types
|
|
107
|
+
- **Serialization** — `StepData` / `SerializedOutput` allow completed runs to be stored compactly and replayed via `replayOutput()` without re-running the optimizer
|
|
63
108
|
|
|
64
109
|
## Architecture notes
|
|
65
110
|
|
|
66
111
|
- `Shape.rasterize()` reuses a single module-level canvas (`_rasterCanvas`) grown to the max shape size seen; avoids per-call `createElement('canvas')` which was the dominant cost (27x speedup)
|
|
67
112
|
- `Cfg.shapeTypes` holds shape constructors; shapes are chosen randomly each step
|
|
68
113
|
- `PreCfg` exists to bridge the gap between call time (dimensions unknown) and runtime (dimensions set by `Canvas.original()`)
|
|
114
|
+
- `Canvas.svgRoot()` is a static helper shared by `Canvas.empty()` and `replayOutput()` to create the SVG root with clip path and background fill
|
package/dist/index.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ interface ShapeInterface {
|
|
|
26
26
|
/** cfg is rarely used by implementations; accepts Partial to keep call sites flexible. */
|
|
27
27
|
mutate(cfg?: Partial<Cfg>): ShapeInterface;
|
|
28
28
|
toSVG(): SVGElement | undefined;
|
|
29
|
+
toData(alpha: number, color: RGB): StepData;
|
|
29
30
|
rasterize(alpha: number): {
|
|
30
31
|
getImageData(): ImageDataLike;
|
|
31
32
|
};
|
|
@@ -43,6 +44,7 @@ interface Cfg {
|
|
|
43
44
|
fill: 'auto' | string;
|
|
44
45
|
computeSize: number;
|
|
45
46
|
viewSize: number;
|
|
47
|
+
allowUpscale?: boolean;
|
|
46
48
|
scale?: number;
|
|
47
49
|
}
|
|
48
50
|
/**
|
|
@@ -53,6 +55,57 @@ type PreCfg = Omit<Cfg, 'width' | 'height'> & {
|
|
|
53
55
|
width?: number;
|
|
54
56
|
height?: number;
|
|
55
57
|
};
|
|
58
|
+
type RGB = [number, number, number];
|
|
59
|
+
type StepData = {
|
|
60
|
+
t: 't';
|
|
61
|
+
a: number;
|
|
62
|
+
c: RGB;
|
|
63
|
+
pts: [number, number][];
|
|
64
|
+
} | {
|
|
65
|
+
t: 'r';
|
|
66
|
+
a: number;
|
|
67
|
+
c: RGB;
|
|
68
|
+
pts: [number, number][];
|
|
69
|
+
} | {
|
|
70
|
+
t: 'e';
|
|
71
|
+
a: number;
|
|
72
|
+
c: RGB;
|
|
73
|
+
cx: number;
|
|
74
|
+
cy: number;
|
|
75
|
+
rx: number;
|
|
76
|
+
ry: number;
|
|
77
|
+
} | {
|
|
78
|
+
t: 's';
|
|
79
|
+
a: number;
|
|
80
|
+
c: RGB;
|
|
81
|
+
cx: number;
|
|
82
|
+
cy: number;
|
|
83
|
+
r: number;
|
|
84
|
+
} | {
|
|
85
|
+
t: 'h';
|
|
86
|
+
a: number;
|
|
87
|
+
c: RGB;
|
|
88
|
+
cx: number;
|
|
89
|
+
cy: number;
|
|
90
|
+
r: number;
|
|
91
|
+
angle: number;
|
|
92
|
+
} | {
|
|
93
|
+
t: 'sm';
|
|
94
|
+
a: number;
|
|
95
|
+
c: RGB;
|
|
96
|
+
cx: number;
|
|
97
|
+
cy: number;
|
|
98
|
+
fs: number;
|
|
99
|
+
text: string;
|
|
100
|
+
};
|
|
101
|
+
interface SerializedOutput {
|
|
102
|
+
v: 1;
|
|
103
|
+
w: number;
|
|
104
|
+
h: number;
|
|
105
|
+
scale: number;
|
|
106
|
+
fill: RGB;
|
|
107
|
+
steps: StepData[];
|
|
108
|
+
}
|
|
56
109
|
|
|
57
110
|
interface DrawableStep {
|
|
58
111
|
alpha: number;
|
|
@@ -65,6 +118,7 @@ declare class Canvas {
|
|
|
65
118
|
node: HTMLCanvasElement;
|
|
66
119
|
ctx: CanvasRenderingContext2D;
|
|
67
120
|
_imageData: ImageData | null;
|
|
121
|
+
static svgRoot(width: number, height: number, fill: string): SVGSVGElement;
|
|
68
122
|
static empty(cfg: Cfg, svg: true): SVGSVGElement;
|
|
69
123
|
static empty(cfg: Cfg, svg?: false): Canvas;
|
|
70
124
|
static original(url: string, cfg: PreCfg): Promise<Canvas>;
|
|
@@ -99,6 +153,7 @@ declare class Step {
|
|
|
99
153
|
distance: number;
|
|
100
154
|
constructor(shape: ShapeInterface, cfg: Cfg);
|
|
101
155
|
toSVG(): SVGElement;
|
|
156
|
+
serialize(): StepData;
|
|
102
157
|
apply(state: State): State;
|
|
103
158
|
compute(state: State): Promise<Step>;
|
|
104
159
|
mutate(): Step;
|
|
@@ -129,6 +184,7 @@ declare class Shape implements ShapeInterface {
|
|
|
129
184
|
constructor(_w: number, _h: number);
|
|
130
185
|
mutate(_cfg?: Partial<Cfg>): ShapeInterface;
|
|
131
186
|
toSVG(): SVGElement | undefined;
|
|
187
|
+
toData(_alpha: number, _color: RGB): StepData;
|
|
132
188
|
rasterize(alpha: number): {
|
|
133
189
|
getImageData(): ImageDataLike;
|
|
134
190
|
};
|
|
@@ -147,10 +203,12 @@ declare class Polygon extends Shape {
|
|
|
147
203
|
declare class Triangle extends Polygon {
|
|
148
204
|
constructor(w: number, h: number);
|
|
149
205
|
protected _cloneEmpty(): Triangle;
|
|
206
|
+
toData(a: number, c: RGB): StepData;
|
|
150
207
|
}
|
|
151
208
|
declare class Rectangle extends Polygon {
|
|
152
209
|
constructor(w: number, h: number);
|
|
153
210
|
protected _cloneEmpty(): Rectangle;
|
|
211
|
+
toData(a: number, c: RGB): StepData;
|
|
154
212
|
mutate(_cfg?: Partial<Cfg>): ShapeInterface;
|
|
155
213
|
_createPoints(w: number, h: number, _count: number): Point[];
|
|
156
214
|
}
|
|
@@ -161,18 +219,20 @@ declare class Ellipse extends Shape {
|
|
|
161
219
|
constructor(w: number, h: number);
|
|
162
220
|
render(ctx: CanvasRenderingContext2D): void;
|
|
163
221
|
toSVG(): SVGElement;
|
|
222
|
+
toData(a: number, c: RGB): StepData;
|
|
164
223
|
mutate(_cfg?: Partial<Cfg>): ShapeInterface;
|
|
165
224
|
computeBbox(): this;
|
|
166
225
|
}
|
|
167
|
-
declare class
|
|
226
|
+
declare class Glyph extends Shape {
|
|
168
227
|
center: Point;
|
|
169
228
|
text: string;
|
|
170
229
|
fontSize: number;
|
|
171
230
|
constructor(w: number, h: number, text?: string);
|
|
172
231
|
computeBbox(): this;
|
|
173
232
|
render(ctx: CanvasRenderingContext2D): void;
|
|
174
|
-
mutate(_cfg?: Partial<Cfg>):
|
|
233
|
+
mutate(_cfg?: Partial<Cfg>): Glyph;
|
|
175
234
|
toSVG(): SVGElement;
|
|
235
|
+
toData(a: number, c: RGB): StepData;
|
|
176
236
|
}
|
|
177
237
|
declare class Square extends Shape {
|
|
178
238
|
center: Point;
|
|
@@ -181,6 +241,7 @@ declare class Square extends Shape {
|
|
|
181
241
|
computeBbox(): this;
|
|
182
242
|
render(ctx: CanvasRenderingContext2D): void;
|
|
183
243
|
toSVG(): SVGElement;
|
|
244
|
+
toData(a: number, c: RGB): StepData;
|
|
184
245
|
mutate(_cfg?: Partial<Cfg>): ShapeInterface;
|
|
185
246
|
}
|
|
186
247
|
declare class Hexagon extends Shape {
|
|
@@ -193,6 +254,7 @@ declare class Hexagon extends Shape {
|
|
|
193
254
|
computeBbox(): this;
|
|
194
255
|
render(ctx: CanvasRenderingContext2D): void;
|
|
195
256
|
toSVG(): SVGElement;
|
|
257
|
+
toData(a: number, c: RGB): StepData;
|
|
196
258
|
mutate(_cfg?: Partial<Cfg>): ShapeInterface;
|
|
197
259
|
}
|
|
198
260
|
declare class Debug extends Shape {
|
|
@@ -201,15 +263,24 @@ declare class Debug extends Shape {
|
|
|
201
263
|
}
|
|
202
264
|
|
|
203
265
|
declare const SVGNS = "http://www.w3.org/2000/svg";
|
|
266
|
+
declare function parseColor(color: string): [number, number, number];
|
|
204
267
|
|
|
205
268
|
declare function clamp(x: number, min: number, max: number): number;
|
|
206
269
|
declare function clampColor(x: number): number;
|
|
207
270
|
declare function distanceToDifference(distance: number, pixels: number): number;
|
|
208
271
|
declare function differenceToDistance(difference: number, pixels: number): number;
|
|
209
272
|
declare function difference(data: ImageDataLike, dataOther: ImageDataLike): number;
|
|
273
|
+
declare function getFill(data: ImageDataLike): string;
|
|
210
274
|
declare function computeColorAndDifferenceChange(offset: Bbox, imageData: ShapeImageData, alpha: number): {
|
|
211
275
|
color: string;
|
|
212
276
|
differenceChange: number;
|
|
213
277
|
};
|
|
214
278
|
|
|
215
|
-
|
|
279
|
+
interface ReplayResult {
|
|
280
|
+
raster: HTMLCanvasElement;
|
|
281
|
+
svg: SVGSVGElement;
|
|
282
|
+
svgString: string;
|
|
283
|
+
}
|
|
284
|
+
declare function replayOutput(data: SerializedOutput): ReplayResult;
|
|
285
|
+
|
|
286
|
+
export { type Bbox, Canvas, type Cfg, Debug, Ellipse, Glyph, Hexagon, type ImageDataLike, Optimizer, type Point, type PreCfg, type RGB, Rectangle, type ReplayResult, SVGNS, type SerializedOutput, Shape, type ShapeImageData, type ShapeInterface, Square, State, Step, type StepData, Triangle, clamp, clampColor, computeColorAndDifferenceChange, difference, differenceToDistance, distanceToDifference, getFill, parseColor, replayOutput, stepPerf };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
// src/util.ts
|
|
2
2
|
var SVGNS = "http://www.w3.org/2000/svg";
|
|
3
|
+
function parseColor(color) {
|
|
4
|
+
const m = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
|
|
5
|
+
if (!m) throw new Error(`Cannot parse color: ${color}`);
|
|
6
|
+
return [parseInt(m[1]), parseInt(m[2]), parseInt(m[3])];
|
|
7
|
+
}
|
|
3
8
|
function clamp(x, min, max) {
|
|
4
9
|
return Math.max(min, Math.min(max, x));
|
|
5
10
|
}
|
|
@@ -22,6 +27,28 @@ function difference(data, dataOther) {
|
|
|
22
27
|
}
|
|
23
28
|
return sum;
|
|
24
29
|
}
|
|
30
|
+
function getFill(data) {
|
|
31
|
+
const w = data.width;
|
|
32
|
+
const h = data.height;
|
|
33
|
+
const d = data.data;
|
|
34
|
+
let rgb = [0, 0, 0];
|
|
35
|
+
let count = 0;
|
|
36
|
+
let i = 0;
|
|
37
|
+
for (let x = 0; x < w; x++) {
|
|
38
|
+
for (let y = 0; y < h; y++) {
|
|
39
|
+
if (x > 0 && y > 0 && x < w - 1 && y < h - 1) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
count++;
|
|
43
|
+
i = 4 * (x + y * w);
|
|
44
|
+
rgb[0] += d[i];
|
|
45
|
+
rgb[1] += d[i + 1];
|
|
46
|
+
rgb[2] += d[i + 2];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
rgb = rgb.map((x) => ~~(x / count)).map(clampColor);
|
|
50
|
+
return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
|
|
51
|
+
}
|
|
25
52
|
function computeColorAndDifferenceChange(offset, imageData, alpha) {
|
|
26
53
|
const { shape, current, target } = imageData;
|
|
27
54
|
const shapeData = shape.data;
|
|
@@ -91,31 +118,9 @@ function computeColorAndDifferenceChange(offset, imageData, alpha) {
|
|
|
91
118
|
}
|
|
92
119
|
|
|
93
120
|
// src/canvas.ts
|
|
94
|
-
function getScale(width, height, limit) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
function getFill(canvas) {
|
|
98
|
-
const data = canvas.getImageData();
|
|
99
|
-
const w = data.width;
|
|
100
|
-
const h = data.height;
|
|
101
|
-
const d = data.data;
|
|
102
|
-
let rgb = [0, 0, 0];
|
|
103
|
-
let count = 0;
|
|
104
|
-
let i = 0;
|
|
105
|
-
for (let x = 0; x < w; x++) {
|
|
106
|
-
for (let y = 0; y < h; y++) {
|
|
107
|
-
if (x > 0 && y > 0 && x < w - 1 && y < h - 1) {
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
count++;
|
|
111
|
-
i = 4 * (x + y * w);
|
|
112
|
-
rgb[0] += d[i];
|
|
113
|
-
rgb[1] += d[i + 1];
|
|
114
|
-
rgb[2] += d[i + 2];
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
rgb = rgb.map((x) => ~~(x / count)).map(clampColor);
|
|
118
|
-
return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
|
|
121
|
+
function getScale(width, height, limit, allowUpscale = false) {
|
|
122
|
+
const scale = Math.max(width / limit, height / limit);
|
|
123
|
+
return allowUpscale ? scale : Math.max(scale, 1);
|
|
119
124
|
}
|
|
120
125
|
function svgRect(w, h) {
|
|
121
126
|
const node = document.createElementNS(SVGNS, "rect");
|
|
@@ -129,23 +134,26 @@ var Canvas = class _Canvas {
|
|
|
129
134
|
node;
|
|
130
135
|
ctx;
|
|
131
136
|
_imageData;
|
|
137
|
+
static svgRoot(width, height, fill) {
|
|
138
|
+
const node = document.createElementNS(SVGNS, "svg");
|
|
139
|
+
node.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
|
140
|
+
node.setAttribute("clip-path", "url(#clip)");
|
|
141
|
+
const defs = document.createElementNS(SVGNS, "defs");
|
|
142
|
+
node.appendChild(defs);
|
|
143
|
+
const cp = document.createElementNS(SVGNS, "clipPath");
|
|
144
|
+
defs.appendChild(cp);
|
|
145
|
+
cp.setAttribute("id", "clip");
|
|
146
|
+
cp.setAttribute("clipPathUnits", "objectBoundingBox");
|
|
147
|
+
let rect = svgRect(width, height);
|
|
148
|
+
cp.appendChild(rect);
|
|
149
|
+
rect = svgRect(width, height);
|
|
150
|
+
rect.setAttribute("fill", fill);
|
|
151
|
+
node.appendChild(rect);
|
|
152
|
+
return node;
|
|
153
|
+
}
|
|
132
154
|
static empty(cfg, svg) {
|
|
133
155
|
if (svg) {
|
|
134
|
-
|
|
135
|
-
node.setAttribute("viewBox", `0 0 ${cfg.width} ${cfg.height}`);
|
|
136
|
-
node.setAttribute("clip-path", "url(#clip)");
|
|
137
|
-
const defs = document.createElementNS(SVGNS, "defs");
|
|
138
|
-
node.appendChild(defs);
|
|
139
|
-
const cp = document.createElementNS(SVGNS, "clipPath");
|
|
140
|
-
defs.appendChild(cp);
|
|
141
|
-
cp.setAttribute("id", "clip");
|
|
142
|
-
cp.setAttribute("clipPathUnits", "objectBoundingBox");
|
|
143
|
-
let rect = svgRect(cfg.width, cfg.height);
|
|
144
|
-
cp.appendChild(rect);
|
|
145
|
-
rect = svgRect(cfg.width, cfg.height);
|
|
146
|
-
rect.setAttribute("fill", cfg.fill);
|
|
147
|
-
node.appendChild(rect);
|
|
148
|
-
return node;
|
|
156
|
+
return this.svgRoot(cfg.width, cfg.height, cfg.fill);
|
|
149
157
|
} else {
|
|
150
158
|
return new this(cfg.width, cfg.height).fill(cfg.fill);
|
|
151
159
|
}
|
|
@@ -156,21 +164,23 @@ var Canvas = class _Canvas {
|
|
|
156
164
|
}
|
|
157
165
|
return new Promise((resolve) => {
|
|
158
166
|
const img = new Image();
|
|
159
|
-
|
|
167
|
+
if (!url.startsWith("blob:") && !url.startsWith("data:")) {
|
|
168
|
+
img.crossOrigin = "anonymous";
|
|
169
|
+
}
|
|
160
170
|
img.src = url;
|
|
161
171
|
img.onload = () => {
|
|
162
172
|
const w = img.naturalWidth;
|
|
163
173
|
const h = img.naturalHeight;
|
|
164
|
-
const computeScale = getScale(w, h, cfg.computeSize);
|
|
174
|
+
const computeScale = getScale(w, h, cfg.computeSize, cfg.allowUpscale);
|
|
165
175
|
cfg.width = w / computeScale;
|
|
166
176
|
cfg.height = h / computeScale;
|
|
167
|
-
const viewScale = getScale(w, h, cfg.viewSize);
|
|
177
|
+
const viewScale = getScale(w, h, cfg.viewSize, cfg.allowUpscale);
|
|
168
178
|
cfg.scale = computeScale / viewScale;
|
|
169
179
|
const fullCfg = { ...cfg, width: cfg.width, height: cfg.height };
|
|
170
180
|
const canvas = this.empty(fullCfg);
|
|
171
181
|
canvas.ctx.drawImage(img, 0, 0, fullCfg.width, fullCfg.height);
|
|
172
182
|
if (cfg.fill == "auto") {
|
|
173
|
-
cfg.fill = getFill(canvas);
|
|
183
|
+
cfg.fill = getFill(canvas.getImageData());
|
|
174
184
|
}
|
|
175
185
|
resolve(canvas);
|
|
176
186
|
};
|
|
@@ -201,7 +211,7 @@ var Canvas = class _Canvas {
|
|
|
201
211
|
ctx.arc(w * 3 / 4, h / 2, w / 7, 0, 2 * Math.PI, true);
|
|
202
212
|
ctx.fill();
|
|
203
213
|
if (cfg.fill == "auto") {
|
|
204
|
-
cfg.fill = getFill(canvas);
|
|
214
|
+
cfg.fill = getFill(canvas.getImageData());
|
|
205
215
|
}
|
|
206
216
|
return canvas;
|
|
207
217
|
}
|
|
@@ -288,6 +298,9 @@ var Step = class _Step {
|
|
|
288
298
|
node.setAttribute("fill-opacity", this.alpha.toFixed(2));
|
|
289
299
|
return node;
|
|
290
300
|
}
|
|
301
|
+
serialize() {
|
|
302
|
+
return this.shape.toData(this.alpha, parseColor(this.color));
|
|
303
|
+
}
|
|
291
304
|
/* apply this step to a state to get a new state. call only after .compute */
|
|
292
305
|
apply(state) {
|
|
293
306
|
const newCanvas = state.canvas.clone().drawStep(this);
|
|
@@ -349,6 +362,9 @@ var Shape = class {
|
|
|
349
362
|
toSVG() {
|
|
350
363
|
return void 0;
|
|
351
364
|
}
|
|
365
|
+
toData(_alpha, _color) {
|
|
366
|
+
throw new Error("Shape does not support serialization");
|
|
367
|
+
}
|
|
352
368
|
/* get a new smaller canvas with this shape */
|
|
353
369
|
rasterize(alpha) {
|
|
354
370
|
const w = this.bbox.width;
|
|
@@ -453,6 +469,9 @@ var Triangle = class _Triangle extends Polygon {
|
|
|
453
469
|
_cloneEmpty() {
|
|
454
470
|
return new _Triangle(0, 0);
|
|
455
471
|
}
|
|
472
|
+
toData(a, c) {
|
|
473
|
+
return { t: "t", a, c, pts: this.points.map(([x, y]) => [x, y]) };
|
|
474
|
+
}
|
|
456
475
|
};
|
|
457
476
|
var Rectangle = class _Rectangle extends Polygon {
|
|
458
477
|
constructor(w, h) {
|
|
@@ -461,6 +480,9 @@ var Rectangle = class _Rectangle extends Polygon {
|
|
|
461
480
|
_cloneEmpty() {
|
|
462
481
|
return new _Rectangle(0, 0);
|
|
463
482
|
}
|
|
483
|
+
toData(a, c) {
|
|
484
|
+
return { t: "r", a, c, pts: this.points.map(([x, y]) => [x, y]) };
|
|
485
|
+
}
|
|
464
486
|
mutate(_cfg) {
|
|
465
487
|
const clone = this._cloneEmpty();
|
|
466
488
|
clone.points = this.points.map(([x, y]) => [x, y]);
|
|
@@ -524,6 +546,9 @@ var Ellipse = class _Ellipse extends Shape {
|
|
|
524
546
|
node.setAttribute("ry", String(this.ry));
|
|
525
547
|
return node;
|
|
526
548
|
}
|
|
549
|
+
toData(a, c) {
|
|
550
|
+
return { t: "e", a, c, cx: this.center[0], cy: this.center[1], rx: this.rx, ry: this.ry };
|
|
551
|
+
}
|
|
527
552
|
mutate(_cfg) {
|
|
528
553
|
const clone = new _Ellipse(0, 0);
|
|
529
554
|
clone.center = [this.center[0], this.center[1]];
|
|
@@ -558,7 +583,7 @@ var Ellipse = class _Ellipse extends Shape {
|
|
|
558
583
|
return this;
|
|
559
584
|
}
|
|
560
585
|
};
|
|
561
|
-
var
|
|
586
|
+
var Glyph = class _Glyph extends Shape {
|
|
562
587
|
center;
|
|
563
588
|
text;
|
|
564
589
|
fontSize;
|
|
@@ -588,7 +613,7 @@ var Smiley = class _Smiley extends Shape {
|
|
|
588
613
|
ctx.fillText(this.text, this.center[0], this.center[1]);
|
|
589
614
|
}
|
|
590
615
|
mutate(_cfg) {
|
|
591
|
-
const clone = new
|
|
616
|
+
const clone = new _Glyph(0, 0, this.text);
|
|
592
617
|
clone.center = [this.center[0], this.center[1]];
|
|
593
618
|
clone.fontSize = this.fontSize;
|
|
594
619
|
switch (Math.floor(Math.random() * 2)) {
|
|
@@ -617,6 +642,9 @@ var Smiley = class _Smiley extends Shape {
|
|
|
617
642
|
text.setAttribute("y", String(this.center[1]));
|
|
618
643
|
return text;
|
|
619
644
|
}
|
|
645
|
+
toData(a, c) {
|
|
646
|
+
return { t: "sm", a, c, cx: this.center[0], cy: this.center[1], fs: this.fontSize, text: this.text };
|
|
647
|
+
}
|
|
620
648
|
};
|
|
621
649
|
var Square = class _Square extends Shape {
|
|
622
650
|
center;
|
|
@@ -647,6 +675,9 @@ var Square = class _Square extends Shape {
|
|
|
647
675
|
node.setAttribute("height", String(2 * this.r));
|
|
648
676
|
return node;
|
|
649
677
|
}
|
|
678
|
+
toData(a, c) {
|
|
679
|
+
return { t: "s", a, c, cx: this.center[0], cy: this.center[1], r: this.r };
|
|
680
|
+
}
|
|
650
681
|
mutate(_cfg) {
|
|
651
682
|
const clone = new _Square(0, 0);
|
|
652
683
|
clone.center = [this.center[0], this.center[1]];
|
|
@@ -722,6 +753,9 @@ var Hexagon = class _Hexagon extends Shape {
|
|
|
722
753
|
node.setAttribute("points", this._points().map((p) => p.join(",")).join(" "));
|
|
723
754
|
return node;
|
|
724
755
|
}
|
|
756
|
+
toData(a, c) {
|
|
757
|
+
return { t: "h", a, c, cx: this.center[0], cy: this.center[1], r: this.r, angle: this.angle };
|
|
758
|
+
}
|
|
725
759
|
mutate(_cfg) {
|
|
726
760
|
const clone = new _Hexagon(0, 0);
|
|
727
761
|
clone.center = [this.center[0], this.center[1]];
|
|
@@ -849,16 +883,133 @@ var Optimizer = class {
|
|
|
849
883
|
return promise;
|
|
850
884
|
}
|
|
851
885
|
};
|
|
886
|
+
|
|
887
|
+
// src/replay.ts
|
|
888
|
+
function rgbString([r, g, b]) {
|
|
889
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
890
|
+
}
|
|
891
|
+
function hexPoints(cx, cy, r, angle) {
|
|
892
|
+
return Array.from({ length: 6 }, (_, i) => {
|
|
893
|
+
const a = angle + i * Math.PI / 3;
|
|
894
|
+
return [~~(cx + r * Math.cos(a)), ~~(cy + r * Math.sin(a))];
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
function renderStep(data, ctx) {
|
|
898
|
+
ctx.globalAlpha = data.a;
|
|
899
|
+
ctx.fillStyle = rgbString(data.c);
|
|
900
|
+
switch (data.t) {
|
|
901
|
+
case "t":
|
|
902
|
+
case "r": {
|
|
903
|
+
ctx.beginPath();
|
|
904
|
+
data.pts.forEach(([x, y], i) => i ? ctx.lineTo(x, y) : ctx.moveTo(x, y));
|
|
905
|
+
ctx.closePath();
|
|
906
|
+
ctx.fill();
|
|
907
|
+
break;
|
|
908
|
+
}
|
|
909
|
+
case "e": {
|
|
910
|
+
ctx.beginPath();
|
|
911
|
+
ctx.ellipse(data.cx, data.cy, data.rx, data.ry, 0, 0, 2 * Math.PI, false);
|
|
912
|
+
ctx.fill();
|
|
913
|
+
break;
|
|
914
|
+
}
|
|
915
|
+
case "s": {
|
|
916
|
+
ctx.fillRect(data.cx - data.r, data.cy - data.r, 2 * data.r, 2 * data.r);
|
|
917
|
+
break;
|
|
918
|
+
}
|
|
919
|
+
case "h": {
|
|
920
|
+
const pts = hexPoints(data.cx, data.cy, data.r, data.angle);
|
|
921
|
+
ctx.beginPath();
|
|
922
|
+
pts.forEach(([x, y], i) => i ? ctx.lineTo(x, y) : ctx.moveTo(x, y));
|
|
923
|
+
ctx.closePath();
|
|
924
|
+
ctx.fill();
|
|
925
|
+
break;
|
|
926
|
+
}
|
|
927
|
+
case "sm": {
|
|
928
|
+
ctx.textAlign = "center";
|
|
929
|
+
ctx.textBaseline = "middle";
|
|
930
|
+
ctx.font = `${data.fs}px sans-serif`;
|
|
931
|
+
ctx.fillText(data.text, data.cx, data.cy);
|
|
932
|
+
break;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
function stepToSVG(data) {
|
|
937
|
+
const color = rgbString(data.c);
|
|
938
|
+
const opacity = data.a.toFixed(2);
|
|
939
|
+
let node;
|
|
940
|
+
switch (data.t) {
|
|
941
|
+
case "t":
|
|
942
|
+
case "r": {
|
|
943
|
+
node = document.createElementNS(SVGNS, "path");
|
|
944
|
+
const d = data.pts.map(([x, y], i) => `${i ? "L" : "M"}${x},${y}`).join("") + "Z";
|
|
945
|
+
node.setAttribute("d", d);
|
|
946
|
+
break;
|
|
947
|
+
}
|
|
948
|
+
case "e": {
|
|
949
|
+
node = document.createElementNS(SVGNS, "ellipse");
|
|
950
|
+
node.setAttribute("cx", String(data.cx));
|
|
951
|
+
node.setAttribute("cy", String(data.cy));
|
|
952
|
+
node.setAttribute("rx", String(data.rx));
|
|
953
|
+
node.setAttribute("ry", String(data.ry));
|
|
954
|
+
break;
|
|
955
|
+
}
|
|
956
|
+
case "s": {
|
|
957
|
+
node = document.createElementNS(SVGNS, "rect");
|
|
958
|
+
node.setAttribute("x", String(data.cx - data.r));
|
|
959
|
+
node.setAttribute("y", String(data.cy - data.r));
|
|
960
|
+
node.setAttribute("width", String(2 * data.r));
|
|
961
|
+
node.setAttribute("height", String(2 * data.r));
|
|
962
|
+
break;
|
|
963
|
+
}
|
|
964
|
+
case "h": {
|
|
965
|
+
node = document.createElementNS(SVGNS, "polygon");
|
|
966
|
+
const pts = hexPoints(data.cx, data.cy, data.r, data.angle);
|
|
967
|
+
node.setAttribute("points", pts.map((p) => p.join(",")).join(" "));
|
|
968
|
+
break;
|
|
969
|
+
}
|
|
970
|
+
case "sm": {
|
|
971
|
+
node = document.createElementNS(SVGNS, "text");
|
|
972
|
+
node.appendChild(document.createTextNode(data.text));
|
|
973
|
+
node.setAttribute("text-anchor", "middle");
|
|
974
|
+
node.setAttribute("dominant-baseline", "central");
|
|
975
|
+
node.setAttribute("font-size", String(data.fs));
|
|
976
|
+
node.setAttribute("font-family", "sans-serif");
|
|
977
|
+
node.setAttribute("x", String(data.cx));
|
|
978
|
+
node.setAttribute("y", String(data.cy));
|
|
979
|
+
break;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
node.setAttribute("fill", color);
|
|
983
|
+
node.setAttribute("fill-opacity", opacity);
|
|
984
|
+
return node;
|
|
985
|
+
}
|
|
986
|
+
function replayOutput(data) {
|
|
987
|
+
const fill = rgbString(data.fill);
|
|
988
|
+
const vw = data.w * data.scale;
|
|
989
|
+
const vh = data.h * data.scale;
|
|
990
|
+
const raster = new Canvas(vw, vh);
|
|
991
|
+
raster.fill(fill);
|
|
992
|
+
raster.ctx.scale(data.scale, data.scale);
|
|
993
|
+
const svg = Canvas.svgRoot(data.w, data.h, fill);
|
|
994
|
+
svg.setAttribute("width", String(vw));
|
|
995
|
+
svg.setAttribute("height", String(vh));
|
|
996
|
+
for (const step of data.steps) {
|
|
997
|
+
renderStep(step, raster.ctx);
|
|
998
|
+
svg.appendChild(stepToSVG(step));
|
|
999
|
+
}
|
|
1000
|
+
const svgString = new XMLSerializer().serializeToString(svg);
|
|
1001
|
+
return { raster: raster.node, svg, svgString };
|
|
1002
|
+
}
|
|
852
1003
|
export {
|
|
853
1004
|
Canvas,
|
|
854
1005
|
Debug,
|
|
855
1006
|
Ellipse,
|
|
1007
|
+
Glyph,
|
|
856
1008
|
Hexagon,
|
|
857
1009
|
Optimizer,
|
|
858
1010
|
Rectangle,
|
|
859
1011
|
SVGNS,
|
|
860
1012
|
Shape,
|
|
861
|
-
Smiley,
|
|
862
1013
|
Square,
|
|
863
1014
|
State,
|
|
864
1015
|
Step,
|
|
@@ -869,5 +1020,8 @@ export {
|
|
|
869
1020
|
difference,
|
|
870
1021
|
differenceToDistance,
|
|
871
1022
|
distanceToDifference,
|
|
1023
|
+
getFill,
|
|
1024
|
+
parseColor,
|
|
1025
|
+
replayOutput,
|
|
872
1026
|
stepPerf
|
|
873
1027
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slithy/prim-lib",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Core engine for primitive-based image reconstruction.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
"tsup": "^8",
|
|
20
20
|
"typescript": "^5",
|
|
21
21
|
"vitest": "^4",
|
|
22
|
-
"@slithy/
|
|
23
|
-
"@slithy/
|
|
22
|
+
"@slithy/eslint-config": "0.0.0",
|
|
23
|
+
"@slithy/tsconfig": "0.0.0"
|
|
24
24
|
},
|
|
25
25
|
"author": {
|
|
26
26
|
"name": "Matthew Campagna",
|