@slithy/prim-lib 0.1.0 → 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 +83 -3
- package/dist/index.js +249 -47
- package/package.json +1 -1
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,30 @@ 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;
|
|
234
|
+
toSVG(): SVGElement;
|
|
235
|
+
toData(a: number, c: RGB): StepData;
|
|
236
|
+
}
|
|
237
|
+
declare class Square extends Shape {
|
|
238
|
+
center: Point;
|
|
239
|
+
r: number;
|
|
240
|
+
constructor(w: number, h: number);
|
|
241
|
+
computeBbox(): this;
|
|
242
|
+
render(ctx: CanvasRenderingContext2D): void;
|
|
175
243
|
toSVG(): SVGElement;
|
|
244
|
+
toData(a: number, c: RGB): StepData;
|
|
245
|
+
mutate(_cfg?: Partial<Cfg>): ShapeInterface;
|
|
176
246
|
}
|
|
177
247
|
declare class Hexagon extends Shape {
|
|
178
248
|
center: Point;
|
|
@@ -184,6 +254,7 @@ declare class Hexagon extends Shape {
|
|
|
184
254
|
computeBbox(): this;
|
|
185
255
|
render(ctx: CanvasRenderingContext2D): void;
|
|
186
256
|
toSVG(): SVGElement;
|
|
257
|
+
toData(a: number, c: RGB): StepData;
|
|
187
258
|
mutate(_cfg?: Partial<Cfg>): ShapeInterface;
|
|
188
259
|
}
|
|
189
260
|
declare class Debug extends Shape {
|
|
@@ -192,15 +263,24 @@ declare class Debug extends Shape {
|
|
|
192
263
|
}
|
|
193
264
|
|
|
194
265
|
declare const SVGNS = "http://www.w3.org/2000/svg";
|
|
266
|
+
declare function parseColor(color: string): [number, number, number];
|
|
195
267
|
|
|
196
268
|
declare function clamp(x: number, min: number, max: number): number;
|
|
197
269
|
declare function clampColor(x: number): number;
|
|
198
270
|
declare function distanceToDifference(distance: number, pixels: number): number;
|
|
199
271
|
declare function differenceToDistance(difference: number, pixels: number): number;
|
|
200
272
|
declare function difference(data: ImageDataLike, dataOther: ImageDataLike): number;
|
|
273
|
+
declare function getFill(data: ImageDataLike): string;
|
|
201
274
|
declare function computeColorAndDifferenceChange(offset: Bbox, imageData: ShapeImageData, alpha: number): {
|
|
202
275
|
color: string;
|
|
203
276
|
differenceChange: number;
|
|
204
277
|
};
|
|
205
278
|
|
|
206
|
-
|
|
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,61 @@ 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
|
+
}
|
|
646
|
+
};
|
|
647
|
+
var Square = class _Square extends Shape {
|
|
648
|
+
center;
|
|
649
|
+
r;
|
|
650
|
+
constructor(w, h) {
|
|
651
|
+
super(w, h);
|
|
652
|
+
this.center = Shape.randomPoint(w, h);
|
|
653
|
+
this.r = 1 + ~~(Math.random() * 20);
|
|
654
|
+
this.computeBbox();
|
|
655
|
+
}
|
|
656
|
+
computeBbox() {
|
|
657
|
+
this.bbox = {
|
|
658
|
+
left: this.center[0] - this.r,
|
|
659
|
+
top: this.center[1] - this.r,
|
|
660
|
+
width: 2 * this.r || 1,
|
|
661
|
+
height: 2 * this.r || 1
|
|
662
|
+
};
|
|
663
|
+
return this;
|
|
664
|
+
}
|
|
665
|
+
render(ctx) {
|
|
666
|
+
ctx.fillRect(this.center[0] - this.r, this.center[1] - this.r, 2 * this.r, 2 * this.r);
|
|
667
|
+
}
|
|
668
|
+
toSVG() {
|
|
669
|
+
const node = document.createElementNS(SVGNS, "rect");
|
|
670
|
+
node.setAttribute("x", String(this.center[0] - this.r));
|
|
671
|
+
node.setAttribute("y", String(this.center[1] - this.r));
|
|
672
|
+
node.setAttribute("width", String(2 * this.r));
|
|
673
|
+
node.setAttribute("height", String(2 * this.r));
|
|
674
|
+
return node;
|
|
675
|
+
}
|
|
676
|
+
toData(a, c) {
|
|
677
|
+
return { t: "s", a, c, cx: this.center[0], cy: this.center[1], r: this.r };
|
|
678
|
+
}
|
|
679
|
+
mutate(_cfg) {
|
|
680
|
+
const clone = new _Square(0, 0);
|
|
681
|
+
clone.center = [this.center[0], this.center[1]];
|
|
682
|
+
clone.r = this.r;
|
|
683
|
+
switch (Math.floor(Math.random() * 2)) {
|
|
684
|
+
case 0: {
|
|
685
|
+
const angle = Math.random() * 2 * Math.PI;
|
|
686
|
+
const radius = Math.random() * 20;
|
|
687
|
+
clone.center[0] += ~~(radius * Math.cos(angle));
|
|
688
|
+
clone.center[1] += ~~(radius * Math.sin(angle));
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
case 1:
|
|
692
|
+
clone.r += (Math.random() - 0.5) * 20;
|
|
693
|
+
clone.r = Math.max(1, ~~clone.r);
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
696
|
+
return clone.computeBbox();
|
|
697
|
+
}
|
|
620
698
|
};
|
|
621
699
|
var Hexagon = class _Hexagon extends Shape {
|
|
622
700
|
center;
|
|
@@ -673,6 +751,9 @@ var Hexagon = class _Hexagon extends Shape {
|
|
|
673
751
|
node.setAttribute("points", this._points().map((p) => p.join(",")).join(" "));
|
|
674
752
|
return node;
|
|
675
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
|
+
}
|
|
676
757
|
mutate(_cfg) {
|
|
677
758
|
const clone = new _Hexagon(0, 0);
|
|
678
759
|
clone.center = [this.center[0], this.center[1]];
|
|
@@ -800,16 +881,134 @@ var Optimizer = class {
|
|
|
800
881
|
return promise;
|
|
801
882
|
}
|
|
802
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
|
+
}
|
|
803
1001
|
export {
|
|
804
1002
|
Canvas,
|
|
805
1003
|
Debug,
|
|
806
1004
|
Ellipse,
|
|
1005
|
+
Glyph,
|
|
807
1006
|
Hexagon,
|
|
808
1007
|
Optimizer,
|
|
809
1008
|
Rectangle,
|
|
810
1009
|
SVGNS,
|
|
811
1010
|
Shape,
|
|
812
|
-
|
|
1011
|
+
Square,
|
|
813
1012
|
State,
|
|
814
1013
|
Step,
|
|
815
1014
|
Triangle,
|
|
@@ -819,5 +1018,8 @@ export {
|
|
|
819
1018
|
difference,
|
|
820
1019
|
differenceToDistance,
|
|
821
1020
|
distanceToDifference,
|
|
1021
|
+
getFill,
|
|
1022
|
+
parseColor,
|
|
1023
|
+
replayOutput,
|
|
822
1024
|
stepPerf
|
|
823
1025
|
};
|