@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 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`**, **`Smiley`**, **`Debug`**
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 // 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
- scale?: number // viewSize / computeSize ratio (set by Canvas.original)
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 Smiley extends Shape {
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>): Smiley;
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
- export { type Bbox, Canvas, type Cfg, Debug, Ellipse, Hexagon, type ImageDataLike, Optimizer, type Point, type PreCfg, Rectangle, SVGNS, Shape, type ShapeImageData, type ShapeInterface, Smiley, Square, State, Step, Triangle, clamp, clampColor, computeColorAndDifferenceChange, difference, differenceToDistance, distanceToDifference, stepPerf };
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
- return Math.max(width / limit, height / limit, 1);
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
- const node = document.createElementNS(SVGNS, "svg");
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
- img.crossOrigin = "anonymous";
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 Smiley = class _Smiley extends Shape {
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 _Smiley(0, 0, this.text);
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.1.1",
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/tsconfig": "0.0.0",
23
- "@slithy/eslint-config": "0.0.0"
22
+ "@slithy/eslint-config": "0.0.0",
23
+ "@slithy/tsconfig": "0.0.0"
24
24
  },
25
25
  "author": {
26
26
  "name": "Matthew Campagna",