@slithy/prim-lib 0.4.1 → 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
@@ -915,6 +915,337 @@ var Hexagon = class _Hexagon extends Shape {
915
915
  return clone.computeBbox();
916
916
  }
917
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");
918
1249
  var Debug = class extends Shape {
919
1250
  static {
920
1251
  __name(this, "Debug");
@@ -1064,7 +1395,8 @@ function renderStepToCtx(data, ctx) {
1064
1395
  ctx.fillStyle = rgbString(data.c);
1065
1396
  switch (data.t) {
1066
1397
  case "t":
1067
- case "r": {
1398
+ case "r":
1399
+ case "p": {
1068
1400
  ctx.beginPath();
1069
1401
  data.pts.forEach(([x, y], i) => i ? ctx.lineTo(x, y) : ctx.moveTo(x, y));
1070
1402
  ctx.closePath();
@@ -1102,6 +1434,19 @@ function renderStepToCtx(data, ctx) {
1102
1434
  ctx.fillText(data.text, data.cx, data.cy);
1103
1435
  break;
1104
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
+ }
1105
1450
  }
1106
1451
  }
1107
1452
  __name(renderStepToCtx, "renderStepToCtx");
@@ -1111,7 +1456,8 @@ function stepDataToSVGElement(data) {
1111
1456
  let node;
1112
1457
  switch (data.t) {
1113
1458
  case "t":
1114
- case "r": {
1459
+ case "r":
1460
+ case "p": {
1115
1461
  node = document.createElementNS(SVGNS, "path");
1116
1462
  const d = data.pts.map(([x, y], i) => `${i ? "L" : "M"}${x},${y}`).join("") + "Z";
1117
1463
  node.setAttribute("d", d);
@@ -1157,6 +1503,21 @@ function stepDataToSVGElement(data) {
1157
1503
  node.setAttribute("y", String(data.cy));
1158
1504
  break;
1159
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
+ }
1160
1521
  }
1161
1522
  node.setAttribute("fill", color);
1162
1523
  node.setAttribute("fill-opacity", opacity);
@@ -1203,6 +1564,8 @@ export {
1203
1564
  differenceToDistance,
1204
1565
  distanceToDifference,
1205
1566
  getFill,
1567
+ makeNGon,
1568
+ makeRect,
1206
1569
  parseColor,
1207
1570
  renderStepToCtx,
1208
1571
  replayOutput,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slithy/prim-lib",
3
- "version": "0.4.1",
3
+ "version": "0.5.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.1.2",
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",