@slithy/prim-lib 0.4.0 → 0.5.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
@@ -17,9 +17,52 @@ Reconstructs an image by iteratively placing geometric shapes. Each step evaluat
17
17
 
18
18
  ### Shapes
19
19
 
20
- - **`Shape`** abstract base
20
+ Fixed shapes (class constructors, pass directly in `shapeTypes`):
21
+
21
22
  - **`Triangle`**, **`Rectangle`**, **`Ellipse`**, **`Circle`**, **`Square`**, **`Hexagon`**, **`Glyph`**, **`Debug`**
22
23
 
24
+ Configurable shapes (factory functions that return a constructor):
25
+
26
+ - **`makeNGon(opts)`** — N-sided polygon; supports regular and irregular modes
27
+ - **`makeRect(opts)`** — axis-aligned or rotatable rectangle with independent width/height control
28
+
29
+ ```ts
30
+ shapeTypes: [Triangle, makeNGon({ sides: 6, regular: true }), makeRect({ aspectRatio: 1.78 })]
31
+ ```
32
+
33
+ ### Factory options
34
+
35
+ **`NGonOptions`**
36
+
37
+ ```ts
38
+ interface NGonOptions {
39
+ sides: number // number of vertices (≥ 3, required)
40
+ regular?: boolean // true = equidistant vertices (default: true)
41
+ rotatable?: boolean // regular only: random rotation each instance (default: true)
42
+ startAngle?: number // regular, non-rotatable only: fixed orientation in radians
43
+ // default: -π/2 (top vertex pointing up)
44
+ noise?: number // regular only: 0–1 vertex jitter (default: 0)
45
+ convex?: boolean // irregular only: apply convex hull (default: false)
46
+ sizeRange?: [number, number] // vertex radius range in compute-space px (default: [1, 20])
47
+ // regular: radius from center; irregular: scatter radius from first point
48
+ mutationScale?: number // max mutation step size in px (default: 20)
49
+ }
50
+ ```
51
+
52
+ **`RectOptions`**
53
+
54
+ ```ts
55
+ interface RectOptions {
56
+ widthRange?: [number, number] // half-width range in compute-space px (default: [5, 40])
57
+ heightRange?: [number, number] // half-height range in compute-space px (default: [5, 40])
58
+ // ignored when aspectRatio is set
59
+ aspectRatio?: number // width ÷ height; locks proportions, derives hh from hw
60
+ // e.g. 1.78 for 16:9, 1.33 for 4:3, 1.0 for square
61
+ rotatable?: boolean // random rotation per instance (default: false)
62
+ mutationScale?: number // max mutation step size in px (default: 20)
63
+ }
64
+ ```
65
+
23
66
  ### Types
24
67
 
25
68
  **`Cfg`** — runtime config (all fields required):
@@ -56,11 +99,13 @@ interface Cfg {
56
99
  type StepData =
57
100
  | { t: 't'; a: number; c: RGB; pts: [number, number][] } // Triangle
58
101
  | { t: 'r'; a: number; c: RGB; pts: [number, number][] } // Rectangle
102
+ | { t: 'p'; a: number; c: RGB; pts: [number, number][] } // makeNGon (regular and irregular)
59
103
  | { t: 'e'; a: number; c: RGB; cx: number; cy: number; rx: number; ry: number } // Ellipse
60
104
  | { t: 'c'; a: number; c: RGB; cx: number; cy: number; r: number } // Circle
61
105
  | { t: 's'; a: number; c: RGB; cx: number; cy: number; r: number } // Square
62
106
  | { t: 'h'; a: number; c: RGB; cx: number; cy: number; r: number; angle: number } // Hexagon
63
107
  | { t: 'sm'; a: number; c: RGB; cx: number; cy: number; fs: number; text: string } // Glyph
108
+ | { t: 'rc'; a: number; c: RGB; cx: number; cy: number; hw: number; hh: number; angle: number } // makeRect
64
109
  ```
65
110
 
66
111
  **`SerializedOutput`** — compact, storage-ready representation of a completed run:
@@ -112,7 +157,7 @@ interface ReplayResult {
112
157
  - **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
113
158
  - **`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
114
159
  - **Serialization** — `StepData` / `SerializedOutput` allow completed runs to be stored compactly and replayed via `replayOutput()` without re-running the optimizer
115
- - **Additional shapes** — `Circle`, `Square`, `Hexagon`, and `Glyph` are not in the original; `Circle` is a uniform-radius variant of `Ellipse`
160
+ - **Additional shapes** — `Circle`, `Square`, `Hexagon`, and `Glyph` are not in the original; `Circle` is a uniform-radius variant of `Ellipse`; `makeNGon` and `makeRect` are configurable factory shapes also not in the original
116
161
  - **Shape weighting** — `shapeWeights` allows biased shape selection with a guaranteed distribution: the optimizer pre-allocates exact per-shape step counts (largest-remainder rounding) and shuffles them, so the final mix always matches the requested weights
117
162
 
118
163
  ## Architecture notes
@@ -122,3 +167,4 @@ interface ReplayResult {
122
167
  - When `shapeWeights` is provided (same length as `shapeTypes`), `Optimizer` builds a step plan at construction time: each shape type is allocated an exact number of slots proportional to its weight (using largest-remainder rounding), then the plan is Fisher-Yates shuffled. This guarantees the final shape distribution matches the weights, rather than just biasing random selection
123
168
  - `PreCfg` exists to bridge the gap between call time (dimensions unknown) and runtime (dimensions set by `Canvas.original()` or `Canvas.fromBitmap()`)
124
169
  - `Canvas.svgRoot()` is a static helper shared by `Canvas.empty()` and `replayOutput()` to create the SVG root with clip path and background fill; it uses DOM APIs and is main-thread only
170
+ - Factory shapes (`makeNGon`, `makeRect`) capture their options in a closure and return an inner class. The class carries a static `_shapeSpec` property (`{ f: string, o: opts }`) used by `runWorker` to serialize and reconstruct the factory call inside the worker
package/dist/index.d.ts CHANGED
@@ -68,6 +68,11 @@ type StepData = {
68
68
  a: number;
69
69
  c: RGB;
70
70
  pts: [number, number][];
71
+ } | {
72
+ t: 'p';
73
+ a: number;
74
+ c: RGB;
75
+ pts: [number, number][];
71
76
  } | {
72
77
  t: 'e';
73
78
  a: number;
@@ -106,6 +111,15 @@ type StepData = {
106
111
  cy: number;
107
112
  fs: number;
108
113
  text: string;
114
+ } | {
115
+ t: 'rc';
116
+ a: number;
117
+ c: RGB;
118
+ cx: number;
119
+ cy: number;
120
+ hw: number;
121
+ hh: number;
122
+ angle: number;
109
123
  };
110
124
  interface SerializedOutput {
111
125
  v: 1;
@@ -280,6 +294,25 @@ declare class Hexagon extends Shape {
280
294
  toData(a: number, c: RGB): StepData;
281
295
  mutate(_cfg?: Partial<Cfg>): ShapeInterface;
282
296
  }
297
+ interface NGonOptions {
298
+ sides: number;
299
+ regular?: boolean;
300
+ rotatable?: boolean;
301
+ startAngle?: number;
302
+ convex?: boolean;
303
+ noise?: number;
304
+ sizeRange?: [number, number];
305
+ mutationScale?: number;
306
+ }
307
+ declare function makeNGon(opts: NGonOptions): new (w: number, h: number) => ShapeInterface;
308
+ interface RectOptions {
309
+ widthRange?: [number, number];
310
+ heightRange?: [number, number];
311
+ aspectRatio?: number;
312
+ rotatable?: boolean;
313
+ mutationScale?: number;
314
+ }
315
+ declare function makeRect(opts?: Partial<RectOptions>): new (w: number, h: number) => ShapeInterface;
283
316
  declare class Debug extends Shape {
284
317
  constructor(w: number, h: number);
285
318
  render(ctx: CanvasRenderingContext2D): void;
@@ -308,4 +341,4 @@ interface ReplayResult {
308
341
  }
309
342
  declare function replayOutput(data: SerializedOutput): ReplayResult;
310
343
 
311
- export { type Bbox, Canvas, type Cfg, Circle, type Ctx2D, 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, renderStepToCtx, replayOutput, stepDataToSVGElement, stepPerf };
344
+ export { type Bbox, Canvas, type Cfg, Circle, type Ctx2D, Debug, Ellipse, Glyph, Hexagon, type ImageDataLike, type NGonOptions, Optimizer, type Point, type PreCfg, type RGB, type RectOptions, Rectangle, type ReplayResult, SVGNS, type SerializedOutput, Shape, type ShapeImageData, type ShapeInterface, Square, State, Step, type StepData, Triangle, clamp, clampColor, computeColorAndDifferenceChange, difference, differenceToDistance, distanceToDifference, getFill, makeNGon, makeRect, parseColor, renderStepToCtx, replayOutput, stepDataToSVGElement, stepPerf };
package/dist/index.js CHANGED
@@ -1,3 +1,6 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
1
4
  // src/util.ts
2
5
  var SVGNS = "http://www.w3.org/2000/svg";
3
6
  function parseColor(color) {
@@ -5,18 +8,23 @@ function parseColor(color) {
5
8
  if (!m) throw new Error(`Cannot parse color: ${color}`);
6
9
  return [parseInt(m[1]), parseInt(m[2]), parseInt(m[3])];
7
10
  }
11
+ __name(parseColor, "parseColor");
8
12
  function clamp(x, min, max) {
9
13
  return Math.max(min, Math.min(max, x));
10
14
  }
15
+ __name(clamp, "clamp");
11
16
  function clampColor(x) {
12
17
  return clamp(x, 0, 255);
13
18
  }
19
+ __name(clampColor, "clampColor");
14
20
  function distanceToDifference(distance, pixels) {
15
21
  return Math.pow(distance * 255, 2) * (3 * pixels);
16
22
  }
23
+ __name(distanceToDifference, "distanceToDifference");
17
24
  function differenceToDistance(difference2, pixels) {
18
25
  return Math.sqrt(difference2 / (3 * pixels)) / 255;
19
26
  }
27
+ __name(differenceToDistance, "differenceToDistance");
20
28
  function difference(data, dataOther) {
21
29
  let sum = 0, dr = 0, dg = 0, db = 0;
22
30
  for (let i = 0; i < data.data.length; i += 4) {
@@ -27,6 +35,7 @@ function difference(data, dataOther) {
27
35
  }
28
36
  return sum;
29
37
  }
38
+ __name(difference, "difference");
30
39
  function getFill(data) {
31
40
  const w = data.width;
32
41
  const h = data.height;
@@ -49,6 +58,7 @@ function getFill(data) {
49
58
  rgb = rgb.map((x) => ~~(x / count)).map(clampColor);
50
59
  return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
51
60
  }
61
+ __name(getFill, "getFill");
52
62
  function computeColorAndDifferenceChange(offset, imageData, alpha) {
53
63
  const { shape, current, target } = imageData;
54
64
  const shapeData = shape.data;
@@ -116,12 +126,14 @@ function computeColorAndDifferenceChange(offset, imageData, alpha) {
116
126
  }
117
127
  return { color: `rgb(${cr}, ${cg}, ${cb})`, differenceChange: sum };
118
128
  }
129
+ __name(computeColorAndDifferenceChange, "computeColorAndDifferenceChange");
119
130
 
120
131
  // src/canvas.ts
121
132
  function getScale(width, height, limit, allowUpscale = false) {
122
133
  const scale = Math.max(width / limit, height / limit);
123
134
  return allowUpscale ? scale : Math.max(scale, 1);
124
135
  }
136
+ __name(getScale, "getScale");
125
137
  function svgRect(w, h) {
126
138
  const node = document.createElementNS(SVGNS, "rect");
127
139
  node.setAttribute("x", "0");
@@ -130,7 +142,11 @@ function svgRect(w, h) {
130
142
  node.setAttribute("height", String(h));
131
143
  return node;
132
144
  }
145
+ __name(svgRect, "svgRect");
133
146
  var Canvas = class _Canvas {
147
+ static {
148
+ __name(this, "Canvas");
149
+ }
134
150
  node;
135
151
  ctx;
136
152
  _imageData;
@@ -280,6 +296,9 @@ var Canvas = class _Canvas {
280
296
 
281
297
  // src/state.ts
282
298
  var State = class {
299
+ static {
300
+ __name(this, "State");
301
+ }
283
302
  target;
284
303
  canvas;
285
304
  distance;
@@ -302,6 +321,9 @@ var stepPerf = {
302
321
  }
303
322
  };
304
323
  var Step = class _Step {
324
+ static {
325
+ __name(this, "Step");
326
+ }
305
327
  shape;
306
328
  cfg;
307
329
  alpha;
@@ -366,6 +388,9 @@ var Step = class _Step {
366
388
  // src/shape.ts
367
389
  var _rasterCanvas = null;
368
390
  var Shape = class {
391
+ static {
392
+ __name(this, "Shape");
393
+ }
369
394
  bbox;
370
395
  static randomPoint(width, height) {
371
396
  return [~~(Math.random() * width), ~~(Math.random() * height)];
@@ -417,12 +442,15 @@ var Shape = class {
417
442
  ctx.translate(-this.bbox.left, -this.bbox.top);
418
443
  this.render(ctx);
419
444
  const data = ctx.getImageData(0, 0, w, h);
420
- return { getImageData: () => data };
445
+ return { getImageData: /* @__PURE__ */ __name(() => data, "getImageData") };
421
446
  }
422
447
  render(_ctx) {
423
448
  }
424
449
  };
425
450
  var Polygon = class _Polygon extends Shape {
451
+ static {
452
+ __name(this, "Polygon");
453
+ }
426
454
  points;
427
455
  constructor(w, h, count) {
428
456
  super(w, h);
@@ -496,6 +524,9 @@ var Polygon = class _Polygon extends Shape {
496
524
  }
497
525
  };
498
526
  var Triangle = class _Triangle extends Polygon {
527
+ static {
528
+ __name(this, "Triangle");
529
+ }
499
530
  constructor(w, h) {
500
531
  super(w, h, 3);
501
532
  }
@@ -507,6 +538,9 @@ var Triangle = class _Triangle extends Polygon {
507
538
  }
508
539
  };
509
540
  var Rectangle = class _Rectangle extends Polygon {
541
+ static {
542
+ __name(this, "Rectangle");
543
+ }
510
544
  constructor(w, h) {
511
545
  super(w, h, 4);
512
546
  }
@@ -556,6 +590,9 @@ var Rectangle = class _Rectangle extends Polygon {
556
590
  }
557
591
  };
558
592
  var Ellipse = class _Ellipse extends Shape {
593
+ static {
594
+ __name(this, "Ellipse");
595
+ }
559
596
  center;
560
597
  rx;
561
598
  ry;
@@ -617,6 +654,9 @@ var Ellipse = class _Ellipse extends Shape {
617
654
  }
618
655
  };
619
656
  var Circle = class _Circle extends Shape {
657
+ static {
658
+ __name(this, "Circle");
659
+ }
620
660
  center;
621
661
  r;
622
662
  constructor(w, h) {
@@ -670,6 +710,9 @@ var Circle = class _Circle extends Shape {
670
710
  }
671
711
  };
672
712
  var Glyph = class _Glyph extends Shape {
713
+ static {
714
+ __name(this, "Glyph");
715
+ }
673
716
  center;
674
717
  text;
675
718
  fontSize;
@@ -733,6 +776,9 @@ var Glyph = class _Glyph extends Shape {
733
776
  }
734
777
  };
735
778
  var Square = class _Square extends Shape {
779
+ static {
780
+ __name(this, "Square");
781
+ }
736
782
  center;
737
783
  r;
738
784
  constructor(w, h) {
@@ -785,6 +831,9 @@ var Square = class _Square extends Shape {
785
831
  }
786
832
  };
787
833
  var Hexagon = class _Hexagon extends Shape {
834
+ static {
835
+ __name(this, "Hexagon");
836
+ }
788
837
  center;
789
838
  r;
790
839
  angle;
@@ -866,7 +915,341 @@ var Hexagon = class _Hexagon extends Shape {
866
915
  return clone.computeBbox();
867
916
  }
868
917
  };
918
+ function convexHull(pts) {
919
+ if (pts.length <= 3) return [...pts];
920
+ const sorted = [...pts].sort((a, b) => a[0] !== b[0] ? a[0] - b[0] : a[1] - b[1]);
921
+ function cross(O, A, B) {
922
+ return (A[0] - O[0]) * (B[1] - O[1]) - (A[1] - O[1]) * (B[0] - O[0]);
923
+ }
924
+ __name(cross, "cross");
925
+ const lower = [];
926
+ for (const p of sorted) {
927
+ while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0) lower.pop();
928
+ lower.push(p);
929
+ }
930
+ const upper = [];
931
+ for (let i = sorted.length - 1; i >= 0; i--) {
932
+ const p = sorted[i];
933
+ while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0) upper.pop();
934
+ upper.push(p);
935
+ }
936
+ lower.pop();
937
+ upper.pop();
938
+ const hull = [...lower, ...upper];
939
+ return hull.length >= 3 ? hull : [...pts];
940
+ }
941
+ __name(convexHull, "convexHull");
942
+ function makeNGon(opts) {
943
+ const sides = opts.sides;
944
+ const regular = opts.regular ?? true;
945
+ const rotatable = opts.rotatable ?? true;
946
+ const noise = opts.noise ?? 0;
947
+ const convex = opts.convex ?? false;
948
+ const sizeRange = opts.sizeRange ?? [1, 20];
949
+ const mutationScale = opts.mutationScale ?? 20;
950
+ const defaultAngle = rotatable ? 0 : -(Math.PI / 2);
951
+ const startAngle = opts.startAngle ?? defaultAngle;
952
+ if (sides < 3) throw new RangeError("makeNGon requires at least 3 sides");
953
+ if (regular) {
954
+ class NGonRegular extends Shape {
955
+ static {
956
+ __name(this, "NGonRegular");
957
+ }
958
+ static _ngonOpts = { sides, regular: true, rotatable, noise, sizeRange, mutationScale, startAngle };
959
+ center;
960
+ r;
961
+ angle;
962
+ noises;
963
+ _cachedPoints;
964
+ constructor(w, h) {
965
+ super(w, h);
966
+ this.center = Shape.randomPoint(w, h);
967
+ this.r = Math.max(1, sizeRange[0] + ~~(Math.random() * (sizeRange[1] - sizeRange[0])));
968
+ this.angle = rotatable ? Math.random() * (2 * Math.PI / sides) : startAngle;
969
+ this.noises = Array.from({ length: sides }, () => Math.random());
970
+ this._cachedPoints = null;
971
+ this.computeBbox();
972
+ }
973
+ _points() {
974
+ if (!this._cachedPoints) {
975
+ this._cachedPoints = Array.from({ length: sides }, (_, i) => {
976
+ const a = this.angle + i * 2 * Math.PI / sides;
977
+ const ri = Math.max(1, ~~(this.r * (1 + (this.noises[i] - 0.5) * noise)));
978
+ return [
979
+ ~~(this.center[0] + ri * Math.cos(a)),
980
+ ~~(this.center[1] + ri * Math.sin(a))
981
+ ];
982
+ });
983
+ }
984
+ return this._cachedPoints;
985
+ }
986
+ computeBbox() {
987
+ this._cachedPoints = null;
988
+ const pts = this._points();
989
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
990
+ for (const [x, y] of pts) {
991
+ if (x < minX) minX = x;
992
+ if (y < minY) minY = y;
993
+ if (x > maxX) maxX = x;
994
+ if (y > maxY) maxY = y;
995
+ }
996
+ this.bbox = {
997
+ left: minX,
998
+ top: minY,
999
+ width: maxX - minX || 1,
1000
+ height: maxY - minY || 1
1001
+ };
1002
+ return this;
1003
+ }
1004
+ render(ctx) {
1005
+ const pts = this._points();
1006
+ ctx.beginPath();
1007
+ pts.forEach(([x, y], i) => i ? ctx.lineTo(x, y) : ctx.moveTo(x, y));
1008
+ ctx.closePath();
1009
+ ctx.fill();
1010
+ }
1011
+ toSVG() {
1012
+ const node = document.createElementNS(SVGNS, "polygon");
1013
+ node.setAttribute("points", this._points().map((p) => p.join(",")).join(" "));
1014
+ return node;
1015
+ }
1016
+ toData(a, c) {
1017
+ return { t: "p", a, c, pts: this._points().map(([x, y]) => [x, y]) };
1018
+ }
1019
+ mutate(_cfg) {
1020
+ const clone = new NGonRegular(0, 0);
1021
+ clone.center = [this.center[0], this.center[1]];
1022
+ clone.r = this.r;
1023
+ clone.angle = this.angle;
1024
+ clone.noises = [...this.noises];
1025
+ const mutCount = 2 + (rotatable ? 1 : 0) + (noise > 0 ? 1 : 0);
1026
+ switch (Math.floor(Math.random() * mutCount)) {
1027
+ case 0: {
1028
+ const a = Math.random() * 2 * Math.PI;
1029
+ const d = Math.random() * mutationScale;
1030
+ clone.center[0] += ~~(d * Math.cos(a));
1031
+ clone.center[1] += ~~(d * Math.sin(a));
1032
+ break;
1033
+ }
1034
+ case 1:
1035
+ clone.r += (Math.random() - 0.5) * mutationScale;
1036
+ clone.r = Math.max(1, ~~clone.r);
1037
+ break;
1038
+ case 2:
1039
+ if (rotatable) {
1040
+ clone.angle += (Math.random() - 0.5) * (2 * Math.PI / sides);
1041
+ } else {
1042
+ const ni = Math.floor(Math.random() * sides);
1043
+ clone.noises[ni] = Math.random();
1044
+ }
1045
+ break;
1046
+ case 3: {
1047
+ const ni = Math.floor(Math.random() * sides);
1048
+ clone.noises[ni] = Math.random();
1049
+ break;
1050
+ }
1051
+ }
1052
+ return clone.computeBbox();
1053
+ }
1054
+ }
1055
+ return NGonRegular;
1056
+ } else {
1057
+ class NGonIrregular extends Shape {
1058
+ static {
1059
+ __name(this, "NGonIrregular");
1060
+ }
1061
+ static _ngonOpts = { sides, regular: false, convex, sizeRange, mutationScale };
1062
+ points;
1063
+ constructor(w, h) {
1064
+ super(w, h);
1065
+ const first = Shape.randomPoint(w, h);
1066
+ this.points = [first];
1067
+ for (let i = 1; i < sides; i++) {
1068
+ const angle = Math.random() * 2 * Math.PI;
1069
+ const radius = sizeRange[0] + Math.random() * (sizeRange[1] - sizeRange[0]);
1070
+ this.points.push([
1071
+ first[0] + ~~(radius * Math.cos(angle)),
1072
+ first[1] + ~~(radius * Math.sin(angle))
1073
+ ]);
1074
+ }
1075
+ if (convex) this.points = convexHull(this.points);
1076
+ this.computeBbox();
1077
+ }
1078
+ computeBbox() {
1079
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1080
+ for (const [x, y] of this.points) {
1081
+ if (x < minX) minX = x;
1082
+ if (y < minY) minY = y;
1083
+ if (x > maxX) maxX = x;
1084
+ if (y > maxY) maxY = y;
1085
+ }
1086
+ this.bbox = {
1087
+ left: minX,
1088
+ top: minY,
1089
+ width: maxX - minX || 1,
1090
+ height: maxY - minY || 1
1091
+ };
1092
+ return this;
1093
+ }
1094
+ render(ctx) {
1095
+ ctx.beginPath();
1096
+ this.points.forEach(([x, y], i) => i ? ctx.lineTo(x, y) : ctx.moveTo(x, y));
1097
+ ctx.closePath();
1098
+ ctx.fill();
1099
+ }
1100
+ toSVG() {
1101
+ const path = document.createElementNS(SVGNS, "path");
1102
+ const d = this.points.map(([x, y], i) => `${i ? "L" : "M"}${x},${y}`).join("") + "Z";
1103
+ path.setAttribute("d", d);
1104
+ return path;
1105
+ }
1106
+ toData(a, c) {
1107
+ return { t: "p", a, c, pts: this.points.map(([x, y]) => [x, y]) };
1108
+ }
1109
+ mutate(_cfg) {
1110
+ const clone = new NGonIrregular(0, 0);
1111
+ clone.points = this.points.map(([x, y]) => [x, y]);
1112
+ const index = Math.floor(Math.random() * clone.points.length);
1113
+ const point = clone.points[index];
1114
+ const angle = Math.random() * 2 * Math.PI;
1115
+ const radius = Math.random() * mutationScale;
1116
+ point[0] += ~~(radius * Math.cos(angle));
1117
+ point[1] += ~~(radius * Math.sin(angle));
1118
+ if (convex) clone.points = convexHull(clone.points);
1119
+ return clone.computeBbox();
1120
+ }
1121
+ }
1122
+ return NGonIrregular;
1123
+ }
1124
+ }
1125
+ __name(makeNGon, "makeNGon");
1126
+ function makeRect(opts) {
1127
+ const widthRange = opts?.widthRange ?? [5, 40];
1128
+ const heightRange = opts?.heightRange ?? [5, 40];
1129
+ const aspectRatio = opts?.aspectRatio;
1130
+ const rotatable = opts?.rotatable ?? false;
1131
+ const mutationScale = opts?.mutationScale ?? 20;
1132
+ class Rect extends Shape {
1133
+ static {
1134
+ __name(this, "Rect");
1135
+ }
1136
+ static _rectOpts = { widthRange, heightRange, aspectRatio, rotatable, mutationScale };
1137
+ static _shapeSpec = { f: "rect" };
1138
+ center;
1139
+ hw;
1140
+ hh;
1141
+ angle;
1142
+ constructor(w, h) {
1143
+ super(w, h);
1144
+ this.center = Shape.randomPoint(w, h);
1145
+ this.hw = widthRange[0] + ~~(Math.random() * (widthRange[1] - widthRange[0]));
1146
+ if (aspectRatio !== void 0) {
1147
+ this.hh = Math.max(1, Math.round(this.hw / aspectRatio));
1148
+ } else {
1149
+ this.hh = heightRange[0] + ~~(Math.random() * (heightRange[1] - heightRange[0]));
1150
+ }
1151
+ this.angle = rotatable ? Math.random() * Math.PI : 0;
1152
+ this.computeBbox();
1153
+ }
1154
+ computeBbox() {
1155
+ const cos = Math.abs(Math.cos(this.angle));
1156
+ const sin = Math.abs(Math.sin(this.angle));
1157
+ const w = ~~(this.hw * cos + this.hh * sin);
1158
+ const h = ~~(this.hw * sin + this.hh * cos);
1159
+ this.bbox = {
1160
+ left: this.center[0] - w,
1161
+ top: this.center[1] - h,
1162
+ width: 2 * w || 1,
1163
+ height: 2 * h || 1
1164
+ };
1165
+ return this;
1166
+ }
1167
+ render(ctx) {
1168
+ const cos = Math.cos(this.angle);
1169
+ const sin = Math.sin(this.angle);
1170
+ const corners = [
1171
+ [this.center[0] - this.hw * cos + this.hh * sin, this.center[1] - this.hw * sin - this.hh * cos],
1172
+ [this.center[0] + this.hw * cos + this.hh * sin, this.center[1] + this.hw * sin - this.hh * cos],
1173
+ [this.center[0] + this.hw * cos - this.hh * sin, this.center[1] + this.hw * sin + this.hh * cos],
1174
+ [this.center[0] - this.hw * cos - this.hh * sin, this.center[1] - this.hw * sin + this.hh * cos]
1175
+ ];
1176
+ ctx.beginPath();
1177
+ corners.forEach(([x, y], i) => i ? ctx.lineTo(~~x, ~~y) : ctx.moveTo(~~x, ~~y));
1178
+ ctx.closePath();
1179
+ ctx.fill();
1180
+ }
1181
+ toSVG() {
1182
+ const cos = Math.cos(this.angle);
1183
+ const sin = Math.sin(this.angle);
1184
+ const corners = [
1185
+ [this.center[0] - this.hw * cos + this.hh * sin, this.center[1] - this.hw * sin - this.hh * cos],
1186
+ [this.center[0] + this.hw * cos + this.hh * sin, this.center[1] + this.hw * sin - this.hh * cos],
1187
+ [this.center[0] + this.hw * cos - this.hh * sin, this.center[1] + this.hw * sin + this.hh * cos],
1188
+ [this.center[0] - this.hw * cos - this.hh * sin, this.center[1] - this.hw * sin + this.hh * cos]
1189
+ ];
1190
+ const node = document.createElementNS(SVGNS, "polygon");
1191
+ node.setAttribute("points", corners.map((p) => p.join(",")).join(" "));
1192
+ return node;
1193
+ }
1194
+ toData(a, c) {
1195
+ return {
1196
+ t: "rc",
1197
+ a,
1198
+ c,
1199
+ cx: this.center[0],
1200
+ cy: this.center[1],
1201
+ hw: this.hw,
1202
+ hh: this.hh,
1203
+ angle: this.angle
1204
+ };
1205
+ }
1206
+ mutate(_cfg) {
1207
+ const clone = new Rect(0, 0);
1208
+ clone.center = [this.center[0], this.center[1]];
1209
+ clone.hw = this.hw;
1210
+ clone.hh = this.hh;
1211
+ clone.angle = this.angle;
1212
+ const mutCount = 2 + (rotatable ? 1 : 0) + (aspectRatio === void 0 ? 1 : 0);
1213
+ switch (Math.floor(Math.random() * mutCount)) {
1214
+ case 0: {
1215
+ const a = Math.random() * 2 * Math.PI;
1216
+ const d = Math.random() * mutationScale;
1217
+ clone.center[0] += ~~(d * Math.cos(a));
1218
+ clone.center[1] += ~~(d * Math.sin(a));
1219
+ break;
1220
+ }
1221
+ case 1:
1222
+ clone.hw += (Math.random() - 0.5) * mutationScale;
1223
+ clone.hw = Math.max(1, ~~clone.hw);
1224
+ if (aspectRatio !== void 0) {
1225
+ clone.hh = Math.max(1, Math.round(clone.hw / aspectRatio));
1226
+ }
1227
+ break;
1228
+ case 2:
1229
+ if (rotatable) {
1230
+ clone.angle += (Math.random() - 0.5) * Math.PI / 4;
1231
+ } else if (aspectRatio === void 0) {
1232
+ clone.hh += (Math.random() - 0.5) * mutationScale;
1233
+ clone.hh = Math.max(1, ~~clone.hh);
1234
+ }
1235
+ break;
1236
+ case 3:
1237
+ if (aspectRatio === void 0) {
1238
+ clone.hh += (Math.random() - 0.5) * mutationScale;
1239
+ clone.hh = Math.max(1, ~~clone.hh);
1240
+ }
1241
+ break;
1242
+ }
1243
+ return clone.computeBbox();
1244
+ }
1245
+ }
1246
+ return Rect;
1247
+ }
1248
+ __name(makeRect, "makeRect");
869
1249
  var Debug = class extends Shape {
1250
+ static {
1251
+ __name(this, "Debug");
1252
+ }
870
1253
  constructor(w, h) {
871
1254
  super(w, h);
872
1255
  this.bbox = { left: 0, top: 0, width: w, height: h };
@@ -893,7 +1276,11 @@ function buildStepPlan(cfg) {
893
1276
  }
894
1277
  return plan;
895
1278
  }
1279
+ __name(buildStepPlan, "buildStepPlan");
896
1280
  var Optimizer = class {
1281
+ static {
1282
+ __name(this, "Optimizer");
1283
+ }
897
1284
  cfg;
898
1285
  state;
899
1286
  onStep;
@@ -972,7 +1359,7 @@ var Optimizer = class {
972
1359
  let resolve;
973
1360
  let bestStep = step;
974
1361
  const promise = new Promise((r) => resolve = r);
975
- const tryMutation = () => {
1362
+ const tryMutation = /* @__PURE__ */ __name(() => {
976
1363
  if (failedAttempts >= LIMIT) {
977
1364
  return resolve(bestStep);
978
1365
  }
@@ -985,7 +1372,7 @@ var Optimizer = class {
985
1372
  }
986
1373
  tryMutation();
987
1374
  });
988
- };
1375
+ }, "tryMutation");
989
1376
  tryMutation();
990
1377
  return promise;
991
1378
  }
@@ -995,18 +1382,21 @@ var Optimizer = class {
995
1382
  function rgbString([r, g, b]) {
996
1383
  return `rgb(${r}, ${g}, ${b})`;
997
1384
  }
1385
+ __name(rgbString, "rgbString");
998
1386
  function hexPoints(cx, cy, r, angle) {
999
1387
  return Array.from({ length: 6 }, (_, i) => {
1000
1388
  const a = angle + i * Math.PI / 3;
1001
1389
  return [~~(cx + r * Math.cos(a)), ~~(cy + r * Math.sin(a))];
1002
1390
  });
1003
1391
  }
1392
+ __name(hexPoints, "hexPoints");
1004
1393
  function renderStepToCtx(data, ctx) {
1005
1394
  ctx.globalAlpha = data.a;
1006
1395
  ctx.fillStyle = rgbString(data.c);
1007
1396
  switch (data.t) {
1008
1397
  case "t":
1009
- case "r": {
1398
+ case "r":
1399
+ case "p": {
1010
1400
  ctx.beginPath();
1011
1401
  data.pts.forEach(([x, y], i) => i ? ctx.lineTo(x, y) : ctx.moveTo(x, y));
1012
1402
  ctx.closePath();
@@ -1044,15 +1434,30 @@ function renderStepToCtx(data, ctx) {
1044
1434
  ctx.fillText(data.text, data.cx, data.cy);
1045
1435
  break;
1046
1436
  }
1437
+ case "rc": {
1438
+ const { cx, cy, hw, hh, angle } = data;
1439
+ const cos = Math.cos(angle);
1440
+ const sin = Math.sin(angle);
1441
+ ctx.beginPath();
1442
+ ctx.moveTo(cx - hw * cos + hh * sin, cy - hw * sin - hh * cos);
1443
+ ctx.lineTo(cx + hw * cos + hh * sin, cy + hw * sin - hh * cos);
1444
+ ctx.lineTo(cx + hw * cos - hh * sin, cy + hw * sin + hh * cos);
1445
+ ctx.lineTo(cx - hw * cos - hh * sin, cy - hw * sin + hh * cos);
1446
+ ctx.closePath();
1447
+ ctx.fill();
1448
+ break;
1449
+ }
1047
1450
  }
1048
1451
  }
1452
+ __name(renderStepToCtx, "renderStepToCtx");
1049
1453
  function stepDataToSVGElement(data) {
1050
1454
  const color = rgbString(data.c);
1051
1455
  const opacity = data.a.toFixed(2);
1052
1456
  let node;
1053
1457
  switch (data.t) {
1054
1458
  case "t":
1055
- case "r": {
1459
+ case "r":
1460
+ case "p": {
1056
1461
  node = document.createElementNS(SVGNS, "path");
1057
1462
  const d = data.pts.map(([x, y], i) => `${i ? "L" : "M"}${x},${y}`).join("") + "Z";
1058
1463
  node.setAttribute("d", d);
@@ -1098,11 +1503,27 @@ function stepDataToSVGElement(data) {
1098
1503
  node.setAttribute("y", String(data.cy));
1099
1504
  break;
1100
1505
  }
1506
+ case "rc": {
1507
+ const { cx, cy, hw, hh, angle } = data;
1508
+ const cos = Math.cos(angle);
1509
+ const sin = Math.sin(angle);
1510
+ const fmt = /* @__PURE__ */ __name((n) => n.toFixed(2), "fmt");
1511
+ const pts = [
1512
+ [cx - hw * cos + hh * sin, cy - hw * sin - hh * cos],
1513
+ [cx + hw * cos + hh * sin, cy + hw * sin - hh * cos],
1514
+ [cx + hw * cos - hh * sin, cy + hw * sin + hh * cos],
1515
+ [cx - hw * cos - hh * sin, cy - hw * sin + hh * cos]
1516
+ ];
1517
+ node = document.createElementNS(SVGNS, "polygon");
1518
+ node.setAttribute("points", pts.map(([x, y]) => `${fmt(x)},${fmt(y)}`).join(" "));
1519
+ break;
1520
+ }
1101
1521
  }
1102
1522
  node.setAttribute("fill", color);
1103
1523
  node.setAttribute("fill-opacity", opacity);
1104
1524
  return node;
1105
1525
  }
1526
+ __name(stepDataToSVGElement, "stepDataToSVGElement");
1106
1527
  function replayOutput(data) {
1107
1528
  const fill = rgbString(data.fill);
1108
1529
  const vw = data.w * data.scale;
@@ -1120,6 +1541,7 @@ function replayOutput(data) {
1120
1541
  const svgString = new XMLSerializer().serializeToString(svg);
1121
1542
  return { raster: raster.node, svg, svgString };
1122
1543
  }
1544
+ __name(replayOutput, "replayOutput");
1123
1545
  export {
1124
1546
  Canvas,
1125
1547
  Circle,
@@ -1142,6 +1564,8 @@ export {
1142
1564
  differenceToDistance,
1143
1565
  distanceToDifference,
1144
1566
  getFill,
1567
+ makeNGon,
1568
+ makeRect,
1145
1569
  parseColor,
1146
1570
  renderStepToCtx,
1147
1571
  replayOutput,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slithy/prim-lib",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Core engine for primitive-based image reconstruction.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -44,8 +44,8 @@
44
44
  },
45
45
  "scripts": {
46
46
  "clean": "rm -rf dist",
47
- "build": "rm -rf dist && tsup src/index.ts --format esm --dts",
48
- "dev": "tsup src/index.ts --format esm --watch",
47
+ "build": "rm -rf dist && tsup src/index.ts --format esm --dts --keep-names",
48
+ "dev": "tsup src/index.ts --format esm --watch --keep-names",
49
49
  "typecheck": "tsc --noEmit",
50
50
  "lint": "eslint .",
51
51
  "test": "vitest run",