@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 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,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 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;
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
- export { type Bbox, Canvas, type Cfg, Debug, Ellipse, Hexagon, type ImageDataLike, Optimizer, type Point, type PreCfg, Rectangle, SVGNS, Shape, type ShapeImageData, type ShapeInterface, Smiley, 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,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
- Smiley,
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slithy/prim-lib",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Core engine for primitive-based image reconstruction.",
5
5
  "type": "module",
6
6
  "exports": {