@slithy/prim-lib 0.1.1 → 0.2.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/README.md +58 -12
- package/dist/index.d.ts +74 -3
- package/dist/index.js +199 -47
- 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
|
}
|
|
@@ -161,16 +169,16 @@ var Canvas = class _Canvas {
|
|
|
161
169
|
img.onload = () => {
|
|
162
170
|
const w = img.naturalWidth;
|
|
163
171
|
const h = img.naturalHeight;
|
|
164
|
-
const computeScale = getScale(w, h, cfg.computeSize);
|
|
172
|
+
const computeScale = getScale(w, h, cfg.computeSize, cfg.allowUpscale);
|
|
165
173
|
cfg.width = w / computeScale;
|
|
166
174
|
cfg.height = h / computeScale;
|
|
167
|
-
const viewScale = getScale(w, h, cfg.viewSize);
|
|
175
|
+
const viewScale = getScale(w, h, cfg.viewSize, cfg.allowUpscale);
|
|
168
176
|
cfg.scale = computeScale / viewScale;
|
|
169
177
|
const fullCfg = { ...cfg, width: cfg.width, height: cfg.height };
|
|
170
178
|
const canvas = this.empty(fullCfg);
|
|
171
179
|
canvas.ctx.drawImage(img, 0, 0, fullCfg.width, fullCfg.height);
|
|
172
180
|
if (cfg.fill == "auto") {
|
|
173
|
-
cfg.fill = getFill(canvas);
|
|
181
|
+
cfg.fill = getFill(canvas.getImageData());
|
|
174
182
|
}
|
|
175
183
|
resolve(canvas);
|
|
176
184
|
};
|
|
@@ -201,7 +209,7 @@ var Canvas = class _Canvas {
|
|
|
201
209
|
ctx.arc(w * 3 / 4, h / 2, w / 7, 0, 2 * Math.PI, true);
|
|
202
210
|
ctx.fill();
|
|
203
211
|
if (cfg.fill == "auto") {
|
|
204
|
-
cfg.fill = getFill(canvas);
|
|
212
|
+
cfg.fill = getFill(canvas.getImageData());
|
|
205
213
|
}
|
|
206
214
|
return canvas;
|
|
207
215
|
}
|
|
@@ -288,6 +296,9 @@ var Step = class _Step {
|
|
|
288
296
|
node.setAttribute("fill-opacity", this.alpha.toFixed(2));
|
|
289
297
|
return node;
|
|
290
298
|
}
|
|
299
|
+
serialize() {
|
|
300
|
+
return this.shape.toData(this.alpha, parseColor(this.color));
|
|
301
|
+
}
|
|
291
302
|
/* apply this step to a state to get a new state. call only after .compute */
|
|
292
303
|
apply(state) {
|
|
293
304
|
const newCanvas = state.canvas.clone().drawStep(this);
|
|
@@ -349,6 +360,9 @@ var Shape = class {
|
|
|
349
360
|
toSVG() {
|
|
350
361
|
return void 0;
|
|
351
362
|
}
|
|
363
|
+
toData(_alpha, _color) {
|
|
364
|
+
throw new Error("Shape does not support serialization");
|
|
365
|
+
}
|
|
352
366
|
/* get a new smaller canvas with this shape */
|
|
353
367
|
rasterize(alpha) {
|
|
354
368
|
const w = this.bbox.width;
|
|
@@ -453,6 +467,9 @@ var Triangle = class _Triangle extends Polygon {
|
|
|
453
467
|
_cloneEmpty() {
|
|
454
468
|
return new _Triangle(0, 0);
|
|
455
469
|
}
|
|
470
|
+
toData(a, c) {
|
|
471
|
+
return { t: "t", a, c, pts: this.points.map(([x, y]) => [x, y]) };
|
|
472
|
+
}
|
|
456
473
|
};
|
|
457
474
|
var Rectangle = class _Rectangle extends Polygon {
|
|
458
475
|
constructor(w, h) {
|
|
@@ -461,6 +478,9 @@ var Rectangle = class _Rectangle extends Polygon {
|
|
|
461
478
|
_cloneEmpty() {
|
|
462
479
|
return new _Rectangle(0, 0);
|
|
463
480
|
}
|
|
481
|
+
toData(a, c) {
|
|
482
|
+
return { t: "r", a, c, pts: this.points.map(([x, y]) => [x, y]) };
|
|
483
|
+
}
|
|
464
484
|
mutate(_cfg) {
|
|
465
485
|
const clone = this._cloneEmpty();
|
|
466
486
|
clone.points = this.points.map(([x, y]) => [x, y]);
|
|
@@ -524,6 +544,9 @@ var Ellipse = class _Ellipse extends Shape {
|
|
|
524
544
|
node.setAttribute("ry", String(this.ry));
|
|
525
545
|
return node;
|
|
526
546
|
}
|
|
547
|
+
toData(a, c) {
|
|
548
|
+
return { t: "e", a, c, cx: this.center[0], cy: this.center[1], rx: this.rx, ry: this.ry };
|
|
549
|
+
}
|
|
527
550
|
mutate(_cfg) {
|
|
528
551
|
const clone = new _Ellipse(0, 0);
|
|
529
552
|
clone.center = [this.center[0], this.center[1]];
|
|
@@ -558,7 +581,7 @@ var Ellipse = class _Ellipse extends Shape {
|
|
|
558
581
|
return this;
|
|
559
582
|
}
|
|
560
583
|
};
|
|
561
|
-
var
|
|
584
|
+
var Glyph = class _Glyph extends Shape {
|
|
562
585
|
center;
|
|
563
586
|
text;
|
|
564
587
|
fontSize;
|
|
@@ -588,7 +611,7 @@ var Smiley = class _Smiley extends Shape {
|
|
|
588
611
|
ctx.fillText(this.text, this.center[0], this.center[1]);
|
|
589
612
|
}
|
|
590
613
|
mutate(_cfg) {
|
|
591
|
-
const clone = new
|
|
614
|
+
const clone = new _Glyph(0, 0, this.text);
|
|
592
615
|
clone.center = [this.center[0], this.center[1]];
|
|
593
616
|
clone.fontSize = this.fontSize;
|
|
594
617
|
switch (Math.floor(Math.random() * 2)) {
|
|
@@ -617,6 +640,9 @@ var Smiley = class _Smiley extends Shape {
|
|
|
617
640
|
text.setAttribute("y", String(this.center[1]));
|
|
618
641
|
return text;
|
|
619
642
|
}
|
|
643
|
+
toData(a, c) {
|
|
644
|
+
return { t: "sm", a, c, cx: this.center[0], cy: this.center[1], fs: this.fontSize, text: this.text };
|
|
645
|
+
}
|
|
620
646
|
};
|
|
621
647
|
var Square = class _Square extends Shape {
|
|
622
648
|
center;
|
|
@@ -647,6 +673,9 @@ var Square = class _Square extends Shape {
|
|
|
647
673
|
node.setAttribute("height", String(2 * this.r));
|
|
648
674
|
return node;
|
|
649
675
|
}
|
|
676
|
+
toData(a, c) {
|
|
677
|
+
return { t: "s", a, c, cx: this.center[0], cy: this.center[1], r: this.r };
|
|
678
|
+
}
|
|
650
679
|
mutate(_cfg) {
|
|
651
680
|
const clone = new _Square(0, 0);
|
|
652
681
|
clone.center = [this.center[0], this.center[1]];
|
|
@@ -722,6 +751,9 @@ var Hexagon = class _Hexagon extends Shape {
|
|
|
722
751
|
node.setAttribute("points", this._points().map((p) => p.join(",")).join(" "));
|
|
723
752
|
return node;
|
|
724
753
|
}
|
|
754
|
+
toData(a, c) {
|
|
755
|
+
return { t: "h", a, c, cx: this.center[0], cy: this.center[1], r: this.r, angle: this.angle };
|
|
756
|
+
}
|
|
725
757
|
mutate(_cfg) {
|
|
726
758
|
const clone = new _Hexagon(0, 0);
|
|
727
759
|
clone.center = [this.center[0], this.center[1]];
|
|
@@ -849,16 +881,133 @@ var Optimizer = class {
|
|
|
849
881
|
return promise;
|
|
850
882
|
}
|
|
851
883
|
};
|
|
884
|
+
|
|
885
|
+
// src/replay.ts
|
|
886
|
+
function rgbString([r, g, b]) {
|
|
887
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
888
|
+
}
|
|
889
|
+
function hexPoints(cx, cy, r, angle) {
|
|
890
|
+
return Array.from({ length: 6 }, (_, i) => {
|
|
891
|
+
const a = angle + i * Math.PI / 3;
|
|
892
|
+
return [~~(cx + r * Math.cos(a)), ~~(cy + r * Math.sin(a))];
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
function renderStep(data, ctx) {
|
|
896
|
+
ctx.globalAlpha = data.a;
|
|
897
|
+
ctx.fillStyle = rgbString(data.c);
|
|
898
|
+
switch (data.t) {
|
|
899
|
+
case "t":
|
|
900
|
+
case "r": {
|
|
901
|
+
ctx.beginPath();
|
|
902
|
+
data.pts.forEach(([x, y], i) => i ? ctx.lineTo(x, y) : ctx.moveTo(x, y));
|
|
903
|
+
ctx.closePath();
|
|
904
|
+
ctx.fill();
|
|
905
|
+
break;
|
|
906
|
+
}
|
|
907
|
+
case "e": {
|
|
908
|
+
ctx.beginPath();
|
|
909
|
+
ctx.ellipse(data.cx, data.cy, data.rx, data.ry, 0, 0, 2 * Math.PI, false);
|
|
910
|
+
ctx.fill();
|
|
911
|
+
break;
|
|
912
|
+
}
|
|
913
|
+
case "s": {
|
|
914
|
+
ctx.fillRect(data.cx - data.r, data.cy - data.r, 2 * data.r, 2 * data.r);
|
|
915
|
+
break;
|
|
916
|
+
}
|
|
917
|
+
case "h": {
|
|
918
|
+
const pts = hexPoints(data.cx, data.cy, data.r, data.angle);
|
|
919
|
+
ctx.beginPath();
|
|
920
|
+
pts.forEach(([x, y], i) => i ? ctx.lineTo(x, y) : ctx.moveTo(x, y));
|
|
921
|
+
ctx.closePath();
|
|
922
|
+
ctx.fill();
|
|
923
|
+
break;
|
|
924
|
+
}
|
|
925
|
+
case "sm": {
|
|
926
|
+
ctx.textAlign = "center";
|
|
927
|
+
ctx.textBaseline = "middle";
|
|
928
|
+
ctx.font = `${data.fs}px sans-serif`;
|
|
929
|
+
ctx.fillText(data.text, data.cx, data.cy);
|
|
930
|
+
break;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
function stepToSVG(data) {
|
|
935
|
+
const color = rgbString(data.c);
|
|
936
|
+
const opacity = data.a.toFixed(2);
|
|
937
|
+
let node;
|
|
938
|
+
switch (data.t) {
|
|
939
|
+
case "t":
|
|
940
|
+
case "r": {
|
|
941
|
+
node = document.createElementNS(SVGNS, "path");
|
|
942
|
+
const d = data.pts.map(([x, y], i) => `${i ? "L" : "M"}${x},${y}`).join("") + "Z";
|
|
943
|
+
node.setAttribute("d", d);
|
|
944
|
+
break;
|
|
945
|
+
}
|
|
946
|
+
case "e": {
|
|
947
|
+
node = document.createElementNS(SVGNS, "ellipse");
|
|
948
|
+
node.setAttribute("cx", String(data.cx));
|
|
949
|
+
node.setAttribute("cy", String(data.cy));
|
|
950
|
+
node.setAttribute("rx", String(data.rx));
|
|
951
|
+
node.setAttribute("ry", String(data.ry));
|
|
952
|
+
break;
|
|
953
|
+
}
|
|
954
|
+
case "s": {
|
|
955
|
+
node = document.createElementNS(SVGNS, "rect");
|
|
956
|
+
node.setAttribute("x", String(data.cx - data.r));
|
|
957
|
+
node.setAttribute("y", String(data.cy - data.r));
|
|
958
|
+
node.setAttribute("width", String(2 * data.r));
|
|
959
|
+
node.setAttribute("height", String(2 * data.r));
|
|
960
|
+
break;
|
|
961
|
+
}
|
|
962
|
+
case "h": {
|
|
963
|
+
node = document.createElementNS(SVGNS, "polygon");
|
|
964
|
+
const pts = hexPoints(data.cx, data.cy, data.r, data.angle);
|
|
965
|
+
node.setAttribute("points", pts.map((p) => p.join(",")).join(" "));
|
|
966
|
+
break;
|
|
967
|
+
}
|
|
968
|
+
case "sm": {
|
|
969
|
+
node = document.createElementNS(SVGNS, "text");
|
|
970
|
+
node.appendChild(document.createTextNode(data.text));
|
|
971
|
+
node.setAttribute("text-anchor", "middle");
|
|
972
|
+
node.setAttribute("dominant-baseline", "central");
|
|
973
|
+
node.setAttribute("font-size", String(data.fs));
|
|
974
|
+
node.setAttribute("font-family", "sans-serif");
|
|
975
|
+
node.setAttribute("x", String(data.cx));
|
|
976
|
+
node.setAttribute("y", String(data.cy));
|
|
977
|
+
break;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
node.setAttribute("fill", color);
|
|
981
|
+
node.setAttribute("fill-opacity", opacity);
|
|
982
|
+
return node;
|
|
983
|
+
}
|
|
984
|
+
function replayOutput(data) {
|
|
985
|
+
const fill = rgbString(data.fill);
|
|
986
|
+
const vw = data.w * data.scale;
|
|
987
|
+
const vh = data.h * data.scale;
|
|
988
|
+
const raster = new Canvas(vw, vh);
|
|
989
|
+
raster.fill(fill);
|
|
990
|
+
raster.ctx.scale(data.scale, data.scale);
|
|
991
|
+
const svg = Canvas.svgRoot(data.w, data.h, fill);
|
|
992
|
+
svg.setAttribute("width", String(vw));
|
|
993
|
+
svg.setAttribute("height", String(vh));
|
|
994
|
+
for (const step of data.steps) {
|
|
995
|
+
renderStep(step, raster.ctx);
|
|
996
|
+
svg.appendChild(stepToSVG(step));
|
|
997
|
+
}
|
|
998
|
+
const svgString = new XMLSerializer().serializeToString(svg);
|
|
999
|
+
return { raster: raster.node, svg, svgString };
|
|
1000
|
+
}
|
|
852
1001
|
export {
|
|
853
1002
|
Canvas,
|
|
854
1003
|
Debug,
|
|
855
1004
|
Ellipse,
|
|
1005
|
+
Glyph,
|
|
856
1006
|
Hexagon,
|
|
857
1007
|
Optimizer,
|
|
858
1008
|
Rectangle,
|
|
859
1009
|
SVGNS,
|
|
860
1010
|
Shape,
|
|
861
|
-
Smiley,
|
|
862
1011
|
Square,
|
|
863
1012
|
State,
|
|
864
1013
|
Step,
|
|
@@ -869,5 +1018,8 @@ export {
|
|
|
869
1018
|
difference,
|
|
870
1019
|
differenceToDistance,
|
|
871
1020
|
distanceToDifference,
|
|
1021
|
+
getFill,
|
|
1022
|
+
parseColor,
|
|
1023
|
+
replayOutput,
|
|
872
1024
|
stepPerf
|
|
873
1025
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slithy/prim-lib",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|