@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 +48 -2
- package/dist/index.d.ts +34 -1
- package/dist/index.js +365 -2
- package/package.json +3 -3
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
|
-
|
|
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.
|
|
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/
|
|
23
|
-
"@slithy/
|
|
22
|
+
"@slithy/eslint-config": "0.0.0",
|
|
23
|
+
"@slithy/tsconfig": "0.0.0"
|
|
24
24
|
},
|
|
25
25
|
"author": {
|
|
26
26
|
"name": "Matthew Campagna",
|