@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 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
  }
@@ -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 Smiley = class _Smiley extends Shape {
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 _Smiley(0, 0, this.text);
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.1.1",
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/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",