@slithy/prim-lib 0.8.2 → 0.9.1

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/dist/index.js CHANGED
@@ -32,6 +32,42 @@ function clamp(x, min, max) {
32
32
  return Math.max(min, Math.min(max, x));
33
33
  }
34
34
  __name(clamp, "clamp");
35
+ function rectCorners(cx, cy, hw, hh, angle) {
36
+ const cos = Math.cos(angle);
37
+ const sin = Math.sin(angle);
38
+ return [
39
+ [cx - hw * cos + hh * sin, cy - hw * sin - hh * cos],
40
+ [cx + hw * cos + hh * sin, cy + hw * sin - hh * cos],
41
+ [cx + hw * cos - hh * sin, cy + hw * sin + hh * cos],
42
+ [cx - hw * cos - hh * sin, cy - hw * sin + hh * cos]
43
+ ];
44
+ }
45
+ __name(rectCorners, "rectCorners");
46
+ function regularPolygonPoints(cx, cy, sides, angle, radius) {
47
+ return Array.from({ length: sides }, (_, i) => {
48
+ const a = angle + i * 2 * Math.PI / sides;
49
+ const r = typeof radius === "function" ? radius(i) : radius;
50
+ return [cx + r * Math.cos(a), cy + r * Math.sin(a)];
51
+ });
52
+ }
53
+ __name(regularPolygonPoints, "regularPolygonPoints");
54
+ function bboxOfPoints(points) {
55
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
56
+ for (const [x, y] of points) {
57
+ if (x < minX) minX = x;
58
+ if (y < minY) minY = y;
59
+ if (x > maxX) maxX = x;
60
+ if (y > maxY) maxY = y;
61
+ }
62
+ return { left: minX, top: minY, width: maxX - minX || 1, height: maxY - minY || 1 };
63
+ }
64
+ __name(bboxOfPoints, "bboxOfPoints");
65
+ function randomPolarOffset(scale) {
66
+ const angle = Math.random() * 2 * Math.PI;
67
+ const radius = Math.random() * scale;
68
+ return [~~(radius * Math.cos(angle)), ~~(radius * Math.sin(angle))];
69
+ }
70
+ __name(randomPolarOffset, "randomPolarOffset");
35
71
  function clampColor(x) {
36
72
  return clamp(x, 0, 255);
37
73
  }
@@ -67,13 +103,19 @@ function getFill(data) {
67
103
  if (x > 0 && y > 0 && x < w - 1 && y < h - 1) {
68
104
  continue;
69
105
  }
70
- count++;
71
106
  i = 4 * (x + y * w);
107
+ if (d[i + 3] === 0) {
108
+ continue;
109
+ }
110
+ count++;
72
111
  rgb[0] += d[i];
73
112
  rgb[1] += d[i + 1];
74
113
  rgb[2] += d[i + 2];
75
114
  }
76
115
  }
116
+ if (count === 0) {
117
+ return "rgb(255, 255, 255)";
118
+ }
77
119
  rgb = rgb.map((x) => ~~(x / count)).map(clampColor);
78
120
  return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
79
121
  }
@@ -172,16 +214,7 @@ var Canvas = class _Canvas {
172
214
  static svgRoot(width, height, fill) {
173
215
  const node = document.createElementNS(SVGNS, "svg");
174
216
  node.setAttribute("viewBox", `0 0 ${width} ${height}`);
175
- node.setAttribute("clip-path", "url(#clip)");
176
- const defs = document.createElementNS(SVGNS, "defs");
177
- node.appendChild(defs);
178
- const cp = document.createElementNS(SVGNS, "clipPath");
179
- defs.appendChild(cp);
180
- cp.setAttribute("id", "clip");
181
- cp.setAttribute("clipPathUnits", "objectBoundingBox");
182
- let rect = svgRect(width, height);
183
- cp.appendChild(rect);
184
- rect = svgRect(width, height);
217
+ const rect = svgRect(width, height);
185
218
  rect.setAttribute("fill", fill);
186
219
  node.appendChild(rect);
187
220
  return node;
@@ -190,14 +223,14 @@ var Canvas = class _Canvas {
190
223
  if (svg) {
191
224
  return this.svgRoot(cfg.width, cfg.height, cfg.outputFill ?? cfg.fill);
192
225
  } else {
193
- return new this(cfg.width, cfg.height).fill(cfg.fill);
226
+ return new this(cfg.width, cfg.height, true).fill(cfg.fill);
194
227
  }
195
228
  }
196
229
  static original(url, cfg) {
197
230
  if (url == "test") {
198
231
  return Promise.resolve(this.test(cfg));
199
232
  }
200
- return new Promise((resolve) => {
233
+ return new Promise((resolve, reject) => {
201
234
  const img = new Image();
202
235
  if (!url.startsWith("blob:") && !url.startsWith("data:")) {
203
236
  img.crossOrigin = "anonymous";
@@ -207,12 +240,12 @@ var Canvas = class _Canvas {
207
240
  const w = img.naturalWidth;
208
241
  const h = img.naturalHeight;
209
242
  const computeScale = getScale(w, h, cfg.computeSize, cfg.allowUpscale);
210
- cfg.width = w / computeScale;
211
- cfg.height = h / computeScale;
243
+ cfg.width = Math.round(w / computeScale);
244
+ cfg.height = Math.round(h / computeScale);
212
245
  const viewScale = getScale(w, h, cfg.viewSize, cfg.allowUpscale);
213
246
  cfg.scale = computeScale / viewScale;
214
247
  const fullCfg = { ...cfg, width: cfg.width, height: cfg.height };
215
- const canvas = this.empty(fullCfg);
248
+ const canvas = new this(fullCfg.width, fullCfg.height, true);
216
249
  canvas.ctx.drawImage(img, 0, 0, fullCfg.width, fullCfg.height);
217
250
  if (cfg.fill === "transparent") {
218
251
  cfg.outputFill = "transparent";
@@ -221,11 +254,15 @@ var Canvas = class _Canvas {
221
254
  if (cfg.fill === "auto") {
222
255
  cfg.fill = getFill(canvas.getImageData());
223
256
  }
257
+ canvas.ctx.globalCompositeOperation = "destination-over";
258
+ canvas.ctx.fillStyle = cfg.fill;
259
+ canvas.ctx.fillRect(0, 0, fullCfg.width, fullCfg.height);
260
+ canvas.ctx.globalCompositeOperation = "source-over";
261
+ canvas._imageData = null;
224
262
  resolve(canvas);
225
263
  };
226
- img.onerror = (e) => {
227
- console.error(e);
228
- alert("The image URL cannot be loaded. Does the server support CORS?");
264
+ img.onerror = () => {
265
+ reject(new Error("The image URL cannot be loaded. Does the server support CORS?"));
229
266
  };
230
267
  });
231
268
  }
@@ -233,12 +270,12 @@ var Canvas = class _Canvas {
233
270
  const w = bitmap.width;
234
271
  const h = bitmap.height;
235
272
  const computeScale = getScale(w, h, cfg.computeSize, cfg.allowUpscale);
236
- cfg.width = w / computeScale;
237
- cfg.height = h / computeScale;
273
+ cfg.width = Math.round(w / computeScale);
274
+ cfg.height = Math.round(h / computeScale);
238
275
  const viewScale = getScale(w, h, cfg.viewSize, cfg.allowUpscale);
239
276
  cfg.scale = computeScale / viewScale;
240
277
  const fullCfg = { ...cfg, width: cfg.width, height: cfg.height };
241
- const canvas = this.empty(fullCfg);
278
+ const canvas = new this(fullCfg.width, fullCfg.height, true);
242
279
  canvas.ctx.drawImage(bitmap, 0, 0, fullCfg.width, fullCfg.height);
243
280
  if (cfg.fill === "transparent") {
244
281
  cfg.outputFill = "transparent";
@@ -247,6 +284,11 @@ var Canvas = class _Canvas {
247
284
  if (cfg.fill === "auto") {
248
285
  cfg.fill = getFill(canvas.getImageData());
249
286
  }
287
+ canvas.ctx.globalCompositeOperation = "destination-over";
288
+ canvas.ctx.fillStyle = cfg.fill;
289
+ canvas.ctx.fillRect(0, 0, fullCfg.width, fullCfg.height);
290
+ canvas.ctx.globalCompositeOperation = "source-over";
291
+ canvas._imageData = null;
250
292
  return canvas;
251
293
  }
252
294
  static test(cfg) {
@@ -280,22 +322,27 @@ var Canvas = class _Canvas {
280
322
  el.width = width;
281
323
  el.height = height;
282
324
  this.node = el;
283
- this.ctx = el.getContext("2d", { willReadFrequently });
325
+ const ctx = el.getContext("2d", { willReadFrequently });
326
+ if (!ctx) throw new Error("Failed to acquire 2d rendering context");
327
+ this.ctx = ctx;
284
328
  } else {
285
329
  const el = new OffscreenCanvas(width, height);
286
330
  this.node = el;
287
- this.ctx = el.getContext("2d", { willReadFrequently });
331
+ const ctx = el.getContext("2d", { willReadFrequently });
332
+ if (!ctx) throw new Error("Failed to acquire 2d rendering context");
333
+ this.ctx = ctx;
288
334
  }
289
335
  this._imageData = null;
290
336
  }
291
337
  clone() {
292
- const other = new _Canvas(this.node.width, this.node.height);
338
+ const other = new _Canvas(this.node.width, this.node.height, true);
293
339
  other.ctx.drawImage(this.node, 0, 0);
294
340
  return other;
295
341
  }
296
342
  fill(color) {
297
343
  this.ctx.fillStyle = color;
298
344
  this.ctx.fillRect(0, 0, this.node.width, this.node.height);
345
+ this._imageData = null;
299
346
  return this;
300
347
  }
301
348
  getImageData() {
@@ -313,7 +360,39 @@ var Canvas = class _Canvas {
313
360
  const difference2 = this.difference(otherCanvas);
314
361
  return differenceToDistance(difference2, this.node.width * this.node.height);
315
362
  }
363
+ patchImageData(offset, shapeData, color) {
364
+ if (!this._imageData) return;
365
+ const [cr, cg, cb] = color;
366
+ const dst = this._imageData.data;
367
+ const src = shapeData.data;
368
+ const sw = shapeData.width, sh = shapeData.height;
369
+ const fw = this._imageData.width, fh = this._imageData.height;
370
+ for (let sy = 0; sy < sh; sy++) {
371
+ const fy = sy + offset.top;
372
+ if (fy < 0 || fy >= fh) continue;
373
+ for (let sx = 0; sx < sw; sx++) {
374
+ const fx = sx + offset.left;
375
+ if (fx < 0 || fx >= fw) continue;
376
+ const si = 4 * (sx + sy * sw);
377
+ const a = src[si + 3];
378
+ if (a === 0) continue;
379
+ const fi = 4 * (fx + fy * fw);
380
+ const alpha = a / 255, blend = 1 - alpha;
381
+ dst[fi] = cr * alpha + dst[fi] * blend;
382
+ dst[fi + 1] = cg * alpha + dst[fi + 1] * blend;
383
+ dst[fi + 2] = cb * alpha + dst[fi + 2] * blend;
384
+ }
385
+ }
386
+ }
316
387
  drawStep(step) {
388
+ if (this._imageData) {
389
+ try {
390
+ const shapeData = step.shape.rasterize(step.alpha).getImageData();
391
+ this.patchImageData(step.shape.bbox, shapeData, parseColor(step.color));
392
+ } catch {
393
+ this._imageData = null;
394
+ }
395
+ }
317
396
  this.ctx.globalAlpha = step.alpha;
318
397
  this.ctx.fillStyle = step.color;
319
398
  step.shape.render(this.ctx);
@@ -375,8 +454,8 @@ var Step = class _Step {
375
454
  }
376
455
  /* apply this step to a state to get a new state. call only after .compute */
377
456
  apply(state) {
378
- const newCanvas = state.canvas.clone().drawStep(this);
379
- return new State(state.target, newCanvas, this.distance);
457
+ state.canvas.drawStep(this);
458
+ return new State(state.target, state.canvas, this.distance);
380
459
  }
381
460
  /* find optimal color and compute the resulting distance */
382
461
  compute(state) {
@@ -451,7 +530,7 @@ var Step = class _Step {
451
530
  }
452
531
  /* return a slightly mutated step */
453
532
  mutate() {
454
- const newShape = this.shape.mutate(this.cfg);
533
+ const newShape = this.shape.mutate();
455
534
  const mutated = new _Step(newShape, this.cfg);
456
535
  if (this.cfg.mutateAlpha) {
457
536
  const mutatedAlpha = this.alpha + (Math.random() - 0.5) * 0.08;
@@ -463,6 +542,15 @@ var Step = class _Step {
463
542
 
464
543
  // src/shape.ts
465
544
  var _rasterCanvas = null;
545
+ var _glyphMeasureCanvas = null;
546
+ function getGlyphMeasureCanvas() {
547
+ if (!_glyphMeasureCanvas) {
548
+ _glyphMeasureCanvas = new Canvas(1, 1);
549
+ }
550
+ return _glyphMeasureCanvas;
551
+ }
552
+ __name(getGlyphMeasureCanvas, "getGlyphMeasureCanvas");
553
+ var BBOX_PAD = 1;
466
554
  var Shape = class {
467
555
  static {
468
556
  __name(this, "Shape");
@@ -490,7 +578,7 @@ var Shape = class {
490
578
  constructor(_w, _h) {
491
579
  this.bbox = { left: 0, top: 0, width: 0, height: 0 };
492
580
  }
493
- mutate(_cfg) {
581
+ mutate() {
494
582
  return this;
495
583
  }
496
584
  toSVG() {
@@ -520,9 +608,41 @@ var Shape = class {
520
608
  const data = ctx.getImageData(0, 0, w, h);
521
609
  return { getImageData: /* @__PURE__ */ __name(() => data, "getImageData") };
522
610
  }
611
+ /* Grow a tight bbox by BBOX_PAD on all sides and snap to integer bounds.
612
+ * Integer left/top/width/height are required: computeColorAndDifferenceChange
613
+ * and rasterize index/translate a flat pixel array directly from these. */
614
+ setBbox(tight) {
615
+ const left = Math.floor(tight.left) - BBOX_PAD;
616
+ const top = Math.floor(tight.top) - BBOX_PAD;
617
+ const right = Math.ceil(tight.left + tight.width) + BBOX_PAD;
618
+ const bottom = Math.ceil(tight.top + tight.height) + BBOX_PAD;
619
+ this.bbox = { left, top, width: Math.max(1, right - left), height: Math.max(1, bottom - top) };
620
+ return this;
621
+ }
523
622
  render(_ctx) {
524
623
  }
525
624
  };
625
+ var ConstrainedShape = class extends Shape {
626
+ static {
627
+ __name(this, "ConstrainedShape");
628
+ }
629
+ tonalRange;
630
+ invertTonal;
631
+ saturationRange;
632
+ invertSaturation;
633
+ hueCenter;
634
+ hueTolerance;
635
+ invertHue;
636
+ applyConstraints(opts) {
637
+ this.tonalRange = opts?.tonalRange;
638
+ this.invertTonal = opts?.invertTonal;
639
+ this.saturationRange = opts?.saturationRange;
640
+ this.invertSaturation = opts?.invertSaturation;
641
+ this.hueCenter = opts?.hueCenter;
642
+ this.hueTolerance = opts?.hueTolerance;
643
+ this.invertHue = opts?.invertHue;
644
+ }
645
+ };
526
646
  var Polygon = class _Polygon extends Shape {
527
647
  static {
528
648
  __name(this, "Polygon");
@@ -557,33 +677,18 @@ var Polygon = class _Polygon extends Shape {
557
677
  path.setAttribute("d", `${d}Z`);
558
678
  return path;
559
679
  }
560
- mutate(_cfg) {
680
+ mutate() {
561
681
  const clone = this._cloneEmpty();
562
682
  clone.points = this.points.map(([x, y]) => [x, y]);
563
683
  const index = Math.floor(Math.random() * this.points.length);
564
684
  const point = clone.points[index];
565
- const angle = Math.random() * 2 * Math.PI;
566
- const radius = Math.random() * 20;
567
- point[0] += ~~(radius * Math.cos(angle));
568
- point[1] += ~~(radius * Math.sin(angle));
685
+ const [dx, dy] = randomPolarOffset(20);
686
+ point[0] += dx;
687
+ point[1] += dy;
569
688
  return clone.computeBbox();
570
689
  }
571
690
  computeBbox() {
572
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
573
- for (const [x, y] of this.points) {
574
- if (x < minX) minX = x;
575
- if (y < minY) minY = y;
576
- if (x > maxX) maxX = x;
577
- if (y > maxY) maxY = y;
578
- }
579
- this.bbox = {
580
- left: minX,
581
- top: minY,
582
- width: maxX - minX || 1,
583
- /* fallback for deformed shapes */
584
- height: maxY - minY || 1
585
- };
586
- return this;
691
+ return this.setBbox(bboxOfPoints(this.points));
587
692
  }
588
693
  _createPoints(w, h, count) {
589
694
  const first = Shape.randomPoint(w, h);
@@ -626,7 +731,7 @@ var Rectangle = class _Rectangle extends Polygon {
626
731
  toData(a, c) {
627
732
  return { t: "r", a, c, pts: this.points.map(([x, y]) => [x, y]) };
628
733
  }
629
- mutate(_cfg) {
734
+ mutate() {
630
735
  const clone = this._cloneEmpty();
631
736
  clone.points = this.points.map(([x, y]) => [x, y]);
632
737
  const amount = ~~((Math.random() - 0.5) * 20);
@@ -695,17 +800,16 @@ var Ellipse = class _Ellipse extends Shape {
695
800
  toData(a, c) {
696
801
  return { t: "e", a, c, cx: this.center[0], cy: this.center[1], rx: this.rx, ry: this.ry };
697
802
  }
698
- mutate(_cfg) {
803
+ mutate() {
699
804
  const clone = new _Ellipse(0, 0);
700
805
  clone.center = [this.center[0], this.center[1]];
701
806
  clone.rx = this.rx;
702
807
  clone.ry = this.ry;
703
808
  switch (Math.floor(Math.random() * 3)) {
704
809
  case 0: {
705
- const angle = Math.random() * 2 * Math.PI;
706
- const radius = Math.random() * 20;
707
- clone.center[0] += ~~(radius * Math.cos(angle));
708
- clone.center[1] += ~~(radius * Math.sin(angle));
810
+ const [dx, dy] = randomPolarOffset(20);
811
+ clone.center[0] += dx;
812
+ clone.center[1] += dy;
709
813
  break;
710
814
  }
711
815
  case 1:
@@ -720,13 +824,12 @@ var Ellipse = class _Ellipse extends Shape {
720
824
  return clone.computeBbox();
721
825
  }
722
826
  computeBbox() {
723
- this.bbox = {
827
+ return this.setBbox({
724
828
  left: this.center[0] - this.rx,
725
829
  top: this.center[1] - this.ry,
726
830
  width: 2 * this.rx,
727
831
  height: 2 * this.ry
728
- };
729
- return this;
832
+ });
730
833
  }
731
834
  };
732
835
  var Circle = class _Circle extends Shape {
@@ -742,13 +845,12 @@ var Circle = class _Circle extends Shape {
742
845
  this.computeBbox();
743
846
  }
744
847
  computeBbox() {
745
- this.bbox = {
848
+ return this.setBbox({
746
849
  left: this.center[0] - this.r,
747
850
  top: this.center[1] - this.r,
748
851
  width: 2 * this.r || 1,
749
852
  height: 2 * this.r || 1
750
- };
751
- return this;
853
+ });
752
854
  }
753
855
  render(ctx) {
754
856
  ctx.beginPath();
@@ -765,16 +867,15 @@ var Circle = class _Circle extends Shape {
765
867
  toData(a, c) {
766
868
  return { t: "c", a, c, cx: this.center[0], cy: this.center[1], r: this.r };
767
869
  }
768
- mutate(_cfg) {
870
+ mutate() {
769
871
  const clone = new _Circle(0, 0);
770
872
  clone.center = [this.center[0], this.center[1]];
771
873
  clone.r = this.r;
772
874
  switch (Math.floor(Math.random() * 2)) {
773
875
  case 0: {
774
- const angle = Math.random() * 2 * Math.PI;
775
- const radius = Math.random() * 20;
776
- clone.center[0] += ~~(radius * Math.cos(angle));
777
- clone.center[1] += ~~(radius * Math.sin(angle));
876
+ const [dx, dy] = randomPolarOffset(20);
877
+ clone.center[0] += dx;
878
+ clone.center[1] += dy;
778
879
  break;
779
880
  }
780
881
  case 1:
@@ -800,16 +901,15 @@ var Glyph = class _Glyph extends Shape {
800
901
  this.computeBbox();
801
902
  }
802
903
  computeBbox() {
803
- const tmp = new Canvas(1, 1);
904
+ const tmp = getGlyphMeasureCanvas();
804
905
  tmp.ctx.font = `${this.fontSize}px sans-serif`;
805
906
  const w = ~~tmp.ctx.measureText(this.text).width;
806
- this.bbox = {
907
+ return this.setBbox({
807
908
  left: ~~(this.center[0] - w / 2),
808
909
  top: ~~(this.center[1] - this.fontSize / 2),
809
910
  width: w,
810
911
  height: this.fontSize
811
- };
812
- return this;
912
+ });
813
913
  }
814
914
  render(ctx) {
815
915
  ctx.textAlign = "center";
@@ -817,16 +917,15 @@ var Glyph = class _Glyph extends Shape {
817
917
  ctx.font = `${this.fontSize}px sans-serif`;
818
918
  ctx.fillText(this.text, this.center[0], this.center[1]);
819
919
  }
820
- mutate(_cfg) {
920
+ mutate() {
821
921
  const clone = new _Glyph(0, 0, this.text);
822
922
  clone.center = [this.center[0], this.center[1]];
823
923
  clone.fontSize = this.fontSize;
824
924
  switch (Math.floor(Math.random() * 2)) {
825
925
  case 0: {
826
- const angle = Math.random() * 2 * Math.PI;
827
- const radius = Math.random() * 20;
828
- clone.center[0] += ~~(radius * Math.cos(angle));
829
- clone.center[1] += ~~(radius * Math.sin(angle));
926
+ const [dx, dy] = randomPolarOffset(20);
927
+ clone.center[0] += dx;
928
+ clone.center[1] += dy;
830
929
  break;
831
930
  }
832
931
  case 1:
@@ -848,7 +947,7 @@ var Glyph = class _Glyph extends Shape {
848
947
  return text;
849
948
  }
850
949
  toData(a, c) {
851
- return { t: "sm", a, c, cx: this.center[0], cy: this.center[1], fs: this.fontSize, text: this.text };
950
+ return { t: "sm", a, c, cx: this.center[0], cy: this.center[1], fs: this.fontSize, text: this.text, fontFamily: "sans-serif" };
852
951
  }
853
952
  };
854
953
  var Square = class _Square extends Shape {
@@ -864,13 +963,12 @@ var Square = class _Square extends Shape {
864
963
  this.computeBbox();
865
964
  }
866
965
  computeBbox() {
867
- this.bbox = {
966
+ return this.setBbox({
868
967
  left: this.center[0] - this.r,
869
968
  top: this.center[1] - this.r,
870
969
  width: 2 * this.r || 1,
871
970
  height: 2 * this.r || 1
872
- };
873
- return this;
971
+ });
874
972
  }
875
973
  render(ctx) {
876
974
  ctx.fillRect(this.center[0] - this.r, this.center[1] - this.r, 2 * this.r, 2 * this.r);
@@ -886,16 +984,15 @@ var Square = class _Square extends Shape {
886
984
  toData(a, c) {
887
985
  return { t: "s", a, c, cx: this.center[0], cy: this.center[1], r: this.r };
888
986
  }
889
- mutate(_cfg) {
987
+ mutate() {
890
988
  const clone = new _Square(0, 0);
891
989
  clone.center = [this.center[0], this.center[1]];
892
990
  clone.r = this.r;
893
991
  switch (Math.floor(Math.random() * 2)) {
894
992
  case 0: {
895
- const angle = Math.random() * 2 * Math.PI;
896
- const radius = Math.random() * 20;
897
- clone.center[0] += ~~(radius * Math.cos(angle));
898
- clone.center[1] += ~~(radius * Math.sin(angle));
993
+ const [dx, dy] = randomPolarOffset(20);
994
+ clone.center[0] += dx;
995
+ clone.center[1] += dy;
899
996
  break;
900
997
  }
901
998
  case 1:
@@ -924,33 +1021,14 @@ var Hexagon = class _Hexagon extends Shape {
924
1021
  }
925
1022
  _points() {
926
1023
  if (!this._cachedPoints) {
927
- this._cachedPoints = Array.from({ length: 6 }, (_, i) => {
928
- const a = this.angle + i * Math.PI / 3;
929
- return [
930
- ~~(this.center[0] + this.r * Math.cos(a)),
931
- ~~(this.center[1] + this.r * Math.sin(a))
932
- ];
933
- });
1024
+ this._cachedPoints = regularPolygonPoints(this.center[0], this.center[1], 6, this.angle, this.r);
934
1025
  }
935
1026
  return this._cachedPoints;
936
1027
  }
937
1028
  computeBbox() {
938
1029
  this._cachedPoints = null;
939
1030
  const pts = this._points();
940
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
941
- for (const [x, y] of pts) {
942
- if (x < minX) minX = x;
943
- if (y < minY) minY = y;
944
- if (x > maxX) maxX = x;
945
- if (y > maxY) maxY = y;
946
- }
947
- this.bbox = {
948
- left: minX,
949
- top: minY,
950
- width: maxX - minX || 1,
951
- height: maxY - minY || 1
952
- };
953
- return this;
1031
+ return this.setBbox(bboxOfPoints(pts));
954
1032
  }
955
1033
  render(ctx) {
956
1034
  const pts = this._points();
@@ -967,17 +1045,16 @@ var Hexagon = class _Hexagon extends Shape {
967
1045
  toData(a, c) {
968
1046
  return { t: "h", a, c, cx: this.center[0], cy: this.center[1], r: this.r, angle: this.angle };
969
1047
  }
970
- mutate(_cfg) {
1048
+ mutate() {
971
1049
  const clone = new _Hexagon(0, 0);
972
1050
  clone.center = [this.center[0], this.center[1]];
973
1051
  clone.r = this.r;
974
1052
  clone.angle = this.angle;
975
1053
  switch (Math.floor(Math.random() * 3)) {
976
1054
  case 0: {
977
- const a = Math.random() * 2 * Math.PI;
978
- const d = Math.random() * 20;
979
- clone.center[0] += ~~(d * Math.cos(a));
980
- clone.center[1] += ~~(d * Math.sin(a));
1055
+ const [dx, dy] = randomPolarOffset(20);
1056
+ clone.center[0] += dx;
1057
+ clone.center[1] += dy;
981
1058
  break;
982
1059
  }
983
1060
  case 1:
@@ -1027,19 +1104,12 @@ function makeNGon(opts) {
1027
1104
  const startAngle = opts.startAngle ?? defaultAngle;
1028
1105
  if (sides < 3) throw new RangeError("makeNGon requires at least 3 sides");
1029
1106
  if (regular) {
1030
- class NGonRegular extends Shape {
1107
+ class NGonRegular extends ConstrainedShape {
1031
1108
  static {
1032
1109
  __name(this, "NGonRegular");
1033
1110
  }
1034
1111
  static _ngonOpts = { sides, regular: true, rotatable, noise, sizeRange, mutationScale, startAngle, tonalRange: opts.tonalRange, invertTonal: opts.invertTonal, saturationRange: opts.saturationRange, invertSaturation: opts.invertSaturation, hueCenter: opts.hueCenter, hueTolerance: opts.hueTolerance, invertHue: opts.invertHue };
1035
1112
  static _shapeSpec = { f: "ngon", o: NGonRegular._ngonOpts };
1036
- tonalRange;
1037
- invertTonal;
1038
- saturationRange;
1039
- invertSaturation;
1040
- hueCenter;
1041
- hueTolerance;
1042
- invertHue;
1043
1113
  center;
1044
1114
  r;
1045
1115
  angle;
@@ -1047,13 +1117,7 @@ function makeNGon(opts) {
1047
1117
  _cachedPoints;
1048
1118
  constructor(w, h) {
1049
1119
  super(w, h);
1050
- this.tonalRange = opts.tonalRange;
1051
- this.invertTonal = opts.invertTonal;
1052
- this.saturationRange = opts.saturationRange;
1053
- this.invertSaturation = opts.invertSaturation;
1054
- this.hueCenter = opts.hueCenter;
1055
- this.hueTolerance = opts.hueTolerance;
1056
- this.invertHue = opts.invertHue;
1120
+ this.applyConstraints(opts);
1057
1121
  this.center = Shape.randomPoint(w, h);
1058
1122
  this.r = Math.max(1, sizeRange[0] + ~~(Math.random() * (sizeRange[1] - sizeRange[0])));
1059
1123
  this.angle = rotatable ? Math.random() * (2 * Math.PI / sides) : startAngle;
@@ -1077,20 +1141,7 @@ function makeNGon(opts) {
1077
1141
  computeBbox() {
1078
1142
  this._cachedPoints = null;
1079
1143
  const pts = this._points();
1080
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1081
- for (const [x, y] of pts) {
1082
- if (x < minX) minX = x;
1083
- if (y < minY) minY = y;
1084
- if (x > maxX) maxX = x;
1085
- if (y > maxY) maxY = y;
1086
- }
1087
- this.bbox = {
1088
- left: minX,
1089
- top: minY,
1090
- width: maxX - minX || 1,
1091
- height: maxY - minY || 1
1092
- };
1093
- return this;
1144
+ return this.setBbox(bboxOfPoints(pts));
1094
1145
  }
1095
1146
  render(ctx) {
1096
1147
  const pts = this._points();
@@ -1107,7 +1158,7 @@ function makeNGon(opts) {
1107
1158
  toData(a, c) {
1108
1159
  return { t: "p", a, c, pts: this._points().map(([x, y]) => [x, y]) };
1109
1160
  }
1110
- mutate(_cfg) {
1161
+ mutate() {
1111
1162
  const clone = new NGonRegular(0, 0);
1112
1163
  clone.center = [this.center[0], this.center[1]];
1113
1164
  clone.r = this.r;
@@ -1116,10 +1167,9 @@ function makeNGon(opts) {
1116
1167
  const mutCount = 2 + (rotatable ? 1 : 0) + (noise > 0 ? 1 : 0);
1117
1168
  switch (Math.floor(Math.random() * mutCount)) {
1118
1169
  case 0: {
1119
- const a = Math.random() * 2 * Math.PI;
1120
- const d = Math.random() * mutationScale;
1121
- clone.center[0] += ~~(d * Math.cos(a));
1122
- clone.center[1] += ~~(d * Math.sin(a));
1170
+ const [dx, dy] = randomPolarOffset(mutationScale);
1171
+ clone.center[0] += dx;
1172
+ clone.center[1] += dy;
1123
1173
  break;
1124
1174
  }
1125
1175
  case 1:
@@ -1145,29 +1195,16 @@ function makeNGon(opts) {
1145
1195
  }
1146
1196
  return NGonRegular;
1147
1197
  } else {
1148
- class NGonIrregular extends Shape {
1198
+ class NGonIrregular extends ConstrainedShape {
1149
1199
  static {
1150
1200
  __name(this, "NGonIrregular");
1151
1201
  }
1152
1202
  static _ngonOpts = { sides, regular: false, convex, sizeRange, mutationScale, tonalRange: opts.tonalRange, invertTonal: opts.invertTonal, saturationRange: opts.saturationRange, invertSaturation: opts.invertSaturation, hueCenter: opts.hueCenter, hueTolerance: opts.hueTolerance, invertHue: opts.invertHue };
1153
1203
  static _shapeSpec = { f: "ngon", o: NGonIrregular._ngonOpts };
1154
- tonalRange;
1155
- invertTonal;
1156
- saturationRange;
1157
- invertSaturation;
1158
- hueCenter;
1159
- hueTolerance;
1160
- invertHue;
1161
1204
  points;
1162
1205
  constructor(w, h) {
1163
1206
  super(w, h);
1164
- this.tonalRange = opts.tonalRange;
1165
- this.invertTonal = opts.invertTonal;
1166
- this.saturationRange = opts.saturationRange;
1167
- this.invertSaturation = opts.invertSaturation;
1168
- this.hueCenter = opts.hueCenter;
1169
- this.hueTolerance = opts.hueTolerance;
1170
- this.invertHue = opts.invertHue;
1207
+ this.applyConstraints(opts);
1171
1208
  const first = Shape.randomPoint(w, h);
1172
1209
  this.points = [first];
1173
1210
  for (let i = 1; i < sides; i++) {
@@ -1182,20 +1219,7 @@ function makeNGon(opts) {
1182
1219
  this.computeBbox();
1183
1220
  }
1184
1221
  computeBbox() {
1185
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1186
- for (const [x, y] of this.points) {
1187
- if (x < minX) minX = x;
1188
- if (y < minY) minY = y;
1189
- if (x > maxX) maxX = x;
1190
- if (y > maxY) maxY = y;
1191
- }
1192
- this.bbox = {
1193
- left: minX,
1194
- top: minY,
1195
- width: maxX - minX || 1,
1196
- height: maxY - minY || 1
1197
- };
1198
- return this;
1222
+ return this.setBbox(bboxOfPoints(this.points));
1199
1223
  }
1200
1224
  render(ctx) {
1201
1225
  ctx.beginPath();
@@ -1212,15 +1236,14 @@ function makeNGon(opts) {
1212
1236
  toData(a, c) {
1213
1237
  return { t: "p", a, c, pts: this.points.map(([x, y]) => [x, y]) };
1214
1238
  }
1215
- mutate(_cfg) {
1239
+ mutate() {
1216
1240
  const clone = new NGonIrregular(0, 0);
1217
1241
  clone.points = this.points.map(([x, y]) => [x, y]);
1218
1242
  const index = Math.floor(Math.random() * clone.points.length);
1219
1243
  const point = clone.points[index];
1220
- const angle = Math.random() * 2 * Math.PI;
1221
- const radius = Math.random() * mutationScale;
1222
- point[0] += ~~(radius * Math.cos(angle));
1223
- point[1] += ~~(radius * Math.sin(angle));
1244
+ const [dx, dy] = randomPolarOffset(mutationScale);
1245
+ point[0] += dx;
1246
+ point[1] += dy;
1224
1247
  if (convex) clone.points = convexHull(clone.points);
1225
1248
  return clone.computeBbox();
1226
1249
  }
@@ -1235,32 +1258,19 @@ function makeRect(opts) {
1235
1258
  const aspectRatio = opts?.aspectRatio;
1236
1259
  const rotatable = opts?.rotatable ?? false;
1237
1260
  const mutationScale = opts?.mutationScale ?? 20;
1238
- class Rect extends Shape {
1261
+ class Rect extends ConstrainedShape {
1239
1262
  static {
1240
1263
  __name(this, "Rect");
1241
1264
  }
1242
1265
  static _rectOpts = { widthRange, heightRange, aspectRatio, rotatable, mutationScale, tonalRange: opts?.tonalRange, invertTonal: opts?.invertTonal, saturationRange: opts?.saturationRange, invertSaturation: opts?.invertSaturation, hueCenter: opts?.hueCenter, hueTolerance: opts?.hueTolerance, invertHue: opts?.invertHue };
1243
1266
  static _shapeSpec = { f: "rect", o: Rect._rectOpts };
1244
- tonalRange;
1245
- invertTonal;
1246
- saturationRange;
1247
- invertSaturation;
1248
- hueCenter;
1249
- hueTolerance;
1250
- invertHue;
1251
1267
  center;
1252
1268
  hw;
1253
1269
  hh;
1254
1270
  angle;
1255
1271
  constructor(w, h) {
1256
1272
  super(w, h);
1257
- this.tonalRange = opts?.tonalRange;
1258
- this.invertTonal = opts?.invertTonal;
1259
- this.saturationRange = opts?.saturationRange;
1260
- this.invertSaturation = opts?.invertSaturation;
1261
- this.hueCenter = opts?.hueCenter;
1262
- this.hueTolerance = opts?.hueTolerance;
1263
- this.invertHue = opts?.invertHue;
1273
+ this.applyConstraints(opts);
1264
1274
  this.center = Shape.randomPoint(w, h);
1265
1275
  this.hw = widthRange[0] + ~~(Math.random() * (widthRange[1] - widthRange[0]));
1266
1276
  if (aspectRatio !== void 0) {
@@ -1272,41 +1282,18 @@ function makeRect(opts) {
1272
1282
  this.computeBbox();
1273
1283
  }
1274
1284
  computeBbox() {
1275
- const cos = Math.abs(Math.cos(this.angle));
1276
- const sin = Math.abs(Math.sin(this.angle));
1277
- const w = ~~(this.hw * cos + this.hh * sin);
1278
- const h = ~~(this.hw * sin + this.hh * cos);
1279
- this.bbox = {
1280
- left: this.center[0] - w,
1281
- top: this.center[1] - h,
1282
- width: 2 * w || 1,
1283
- height: 2 * h || 1
1284
- };
1285
- return this;
1285
+ const corners = rectCorners(this.center[0], this.center[1], this.hw, this.hh, this.angle);
1286
+ return this.setBbox(bboxOfPoints(corners));
1286
1287
  }
1287
1288
  render(ctx) {
1288
- const cos = Math.cos(this.angle);
1289
- const sin = Math.sin(this.angle);
1290
- const corners = [
1291
- [this.center[0] - this.hw * cos + this.hh * sin, this.center[1] - this.hw * sin - this.hh * cos],
1292
- [this.center[0] + this.hw * cos + this.hh * sin, this.center[1] + this.hw * sin - this.hh * cos],
1293
- [this.center[0] + this.hw * cos - this.hh * sin, this.center[1] + this.hw * sin + this.hh * cos],
1294
- [this.center[0] - this.hw * cos - this.hh * sin, this.center[1] - this.hw * sin + this.hh * cos]
1295
- ];
1289
+ const corners = rectCorners(this.center[0], this.center[1], this.hw, this.hh, this.angle);
1296
1290
  ctx.beginPath();
1297
- corners.forEach(([x, y], i) => i ? ctx.lineTo(~~x, ~~y) : ctx.moveTo(~~x, ~~y));
1291
+ corners.forEach(([x, y], i) => i ? ctx.lineTo(x, y) : ctx.moveTo(x, y));
1298
1292
  ctx.closePath();
1299
1293
  ctx.fill();
1300
1294
  }
1301
1295
  toSVG() {
1302
- const cos = Math.cos(this.angle);
1303
- const sin = Math.sin(this.angle);
1304
- const corners = [
1305
- [this.center[0] - this.hw * cos + this.hh * sin, this.center[1] - this.hw * sin - this.hh * cos],
1306
- [this.center[0] + this.hw * cos + this.hh * sin, this.center[1] + this.hw * sin - this.hh * cos],
1307
- [this.center[0] + this.hw * cos - this.hh * sin, this.center[1] + this.hw * sin + this.hh * cos],
1308
- [this.center[0] - this.hw * cos - this.hh * sin, this.center[1] - this.hw * sin + this.hh * cos]
1309
- ];
1296
+ const corners = rectCorners(this.center[0], this.center[1], this.hw, this.hh, this.angle);
1310
1297
  const node = document.createElementNS(SVGNS, "polygon");
1311
1298
  node.setAttribute("points", corners.map((p) => p.join(",")).join(" "));
1312
1299
  return node;
@@ -1323,7 +1310,7 @@ function makeRect(opts) {
1323
1310
  angle: this.angle
1324
1311
  };
1325
1312
  }
1326
- mutate(_cfg) {
1313
+ mutate() {
1327
1314
  const clone = new Rect(0, 0);
1328
1315
  clone.center = [this.center[0], this.center[1]];
1329
1316
  clone.hw = this.hw;
@@ -1332,10 +1319,9 @@ function makeRect(opts) {
1332
1319
  const mutCount = 2 + (rotatable ? 1 : 0) + (aspectRatio === void 0 ? 1 : 0);
1333
1320
  switch (Math.floor(Math.random() * mutCount)) {
1334
1321
  case 0: {
1335
- const a = Math.random() * 2 * Math.PI;
1336
- const d = Math.random() * mutationScale;
1337
- clone.center[0] += ~~(d * Math.cos(a));
1338
- clone.center[1] += ~~(d * Math.sin(a));
1322
+ const [dx, dy] = randomPolarOffset(mutationScale);
1323
+ clone.center[0] += dx;
1324
+ clone.center[1] += dy;
1339
1325
  break;
1340
1326
  }
1341
1327
  case 1:
@@ -1369,42 +1355,28 @@ __name(makeRect, "makeRect");
1369
1355
  function makeCircle(opts) {
1370
1356
  const sizeRange = opts?.sizeRange ?? [1, 20];
1371
1357
  const mutationScale = opts?.mutationScale ?? 20;
1372
- class MadeCircle extends Shape {
1358
+ class MadeCircle extends ConstrainedShape {
1373
1359
  static {
1374
1360
  __name(this, "MadeCircle");
1375
1361
  }
1376
1362
  static _circleOpts = { sizeRange, mutationScale, tonalRange: opts?.tonalRange, invertTonal: opts?.invertTonal, saturationRange: opts?.saturationRange, invertSaturation: opts?.invertSaturation, hueCenter: opts?.hueCenter, hueTolerance: opts?.hueTolerance, invertHue: opts?.invertHue };
1377
1363
  static _shapeSpec = { f: "circle", o: MadeCircle._circleOpts };
1378
- tonalRange;
1379
- invertTonal;
1380
- saturationRange;
1381
- invertSaturation;
1382
- hueCenter;
1383
- hueTolerance;
1384
- invertHue;
1385
1364
  center;
1386
1365
  r;
1387
1366
  constructor(w, h) {
1388
1367
  super(w, h);
1389
- this.tonalRange = opts?.tonalRange;
1390
- this.invertTonal = opts?.invertTonal;
1391
- this.saturationRange = opts?.saturationRange;
1392
- this.invertSaturation = opts?.invertSaturation;
1393
- this.hueCenter = opts?.hueCenter;
1394
- this.hueTolerance = opts?.hueTolerance;
1395
- this.invertHue = opts?.invertHue;
1368
+ this.applyConstraints(opts);
1396
1369
  this.center = Shape.randomPoint(w, h);
1397
1370
  this.r = Math.max(1, sizeRange[0] + ~~(Math.random() * (sizeRange[1] - sizeRange[0])));
1398
1371
  this.computeBbox();
1399
1372
  }
1400
1373
  computeBbox() {
1401
- this.bbox = {
1374
+ return this.setBbox({
1402
1375
  left: this.center[0] - this.r,
1403
1376
  top: this.center[1] - this.r,
1404
1377
  width: 2 * this.r || 1,
1405
1378
  height: 2 * this.r || 1
1406
- };
1407
- return this;
1379
+ });
1408
1380
  }
1409
1381
  render(ctx) {
1410
1382
  ctx.beginPath();
@@ -1421,16 +1393,15 @@ function makeCircle(opts) {
1421
1393
  toData(a, c) {
1422
1394
  return { t: "c", a, c, cx: this.center[0], cy: this.center[1], r: this.r };
1423
1395
  }
1424
- mutate(_cfg) {
1396
+ mutate() {
1425
1397
  const clone = new MadeCircle(0, 0);
1426
1398
  clone.center = [this.center[0], this.center[1]];
1427
1399
  clone.r = this.r;
1428
1400
  switch (Math.floor(Math.random() * 2)) {
1429
1401
  case 0: {
1430
- const angle = Math.random() * 2 * Math.PI;
1431
- const d = Math.random() * mutationScale;
1432
- clone.center[0] += ~~(d * Math.cos(angle));
1433
- clone.center[1] += ~~(d * Math.sin(angle));
1402
+ const [dx, dy] = randomPolarOffset(mutationScale);
1403
+ clone.center[0] += dx;
1404
+ clone.center[1] += dy;
1434
1405
  break;
1435
1406
  }
1436
1407
  case 1:
@@ -1449,44 +1420,30 @@ function makeEllipse(opts) {
1449
1420
  const ryRange = opts?.ryRange ?? [1, 20];
1450
1421
  const aspectRatio = opts?.aspectRatio;
1451
1422
  const mutationScale = opts?.mutationScale ?? 20;
1452
- class MadeEllipse extends Shape {
1423
+ class MadeEllipse extends ConstrainedShape {
1453
1424
  static {
1454
1425
  __name(this, "MadeEllipse");
1455
1426
  }
1456
1427
  static _ellipseOpts = { rxRange, ryRange, aspectRatio, mutationScale, tonalRange: opts?.tonalRange, invertTonal: opts?.invertTonal, saturationRange: opts?.saturationRange, invertSaturation: opts?.invertSaturation, hueCenter: opts?.hueCenter, hueTolerance: opts?.hueTolerance, invertHue: opts?.invertHue };
1457
1428
  static _shapeSpec = { f: "ellipse", o: MadeEllipse._ellipseOpts };
1458
- tonalRange;
1459
- invertTonal;
1460
- saturationRange;
1461
- invertSaturation;
1462
- hueCenter;
1463
- hueTolerance;
1464
- invertHue;
1465
1429
  center;
1466
1430
  rx;
1467
1431
  ry;
1468
1432
  constructor(w, h) {
1469
1433
  super(w, h);
1470
- this.tonalRange = opts?.tonalRange;
1471
- this.invertTonal = opts?.invertTonal;
1472
- this.saturationRange = opts?.saturationRange;
1473
- this.invertSaturation = opts?.invertSaturation;
1474
- this.hueCenter = opts?.hueCenter;
1475
- this.hueTolerance = opts?.hueTolerance;
1476
- this.invertHue = opts?.invertHue;
1434
+ this.applyConstraints(opts);
1477
1435
  this.center = Shape.randomPoint(w, h);
1478
1436
  this.rx = Math.max(1, rxRange[0] + ~~(Math.random() * (rxRange[1] - rxRange[0])));
1479
1437
  this.ry = aspectRatio !== void 0 ? Math.max(1, Math.round(this.rx / aspectRatio)) : Math.max(1, ryRange[0] + ~~(Math.random() * (ryRange[1] - ryRange[0])));
1480
1438
  this.computeBbox();
1481
1439
  }
1482
1440
  computeBbox() {
1483
- this.bbox = {
1441
+ return this.setBbox({
1484
1442
  left: this.center[0] - this.rx,
1485
1443
  top: this.center[1] - this.ry,
1486
1444
  width: 2 * this.rx || 1,
1487
1445
  height: 2 * this.ry || 1
1488
- };
1489
- return this;
1446
+ });
1490
1447
  }
1491
1448
  render(ctx) {
1492
1449
  ctx.beginPath();
@@ -1504,7 +1461,7 @@ function makeEllipse(opts) {
1504
1461
  toData(a, c) {
1505
1462
  return { t: "e", a, c, cx: this.center[0], cy: this.center[1], rx: this.rx, ry: this.ry };
1506
1463
  }
1507
- mutate(_cfg) {
1464
+ mutate() {
1508
1465
  const clone = new MadeEllipse(0, 0);
1509
1466
  clone.center = [this.center[0], this.center[1]];
1510
1467
  clone.rx = this.rx;
@@ -1512,10 +1469,9 @@ function makeEllipse(opts) {
1512
1469
  const mutCount = aspectRatio === void 0 ? 3 : 2;
1513
1470
  switch (Math.floor(Math.random() * mutCount)) {
1514
1471
  case 0: {
1515
- const angle = Math.random() * 2 * Math.PI;
1516
- const d = Math.random() * mutationScale;
1517
- clone.center[0] += ~~(d * Math.cos(angle));
1518
- clone.center[1] += ~~(d * Math.sin(angle));
1472
+ const [dx, dy] = randomPolarOffset(mutationScale);
1473
+ clone.center[0] += dx;
1474
+ clone.center[1] += dy;
1519
1475
  break;
1520
1476
  }
1521
1477
  case 1:
@@ -1541,45 +1497,31 @@ function makeGlyph(opts) {
1541
1497
  const fontFamily = opts?.fontFamily ?? "sans-serif";
1542
1498
  const sizeRange = opts?.sizeRange ?? [10, 30];
1543
1499
  const mutationScale = opts?.mutationScale ?? 20;
1544
- class MadeGlyph extends Shape {
1500
+ class MadeGlyph extends ConstrainedShape {
1545
1501
  static {
1546
1502
  __name(this, "MadeGlyph");
1547
1503
  }
1548
1504
  static _glyphOpts = { char, fontFamily, sizeRange, mutationScale, tonalRange: opts?.tonalRange, invertTonal: opts?.invertTonal, saturationRange: opts?.saturationRange, invertSaturation: opts?.invertSaturation, hueCenter: opts?.hueCenter, hueTolerance: opts?.hueTolerance, invertHue: opts?.invertHue };
1549
1505
  static _shapeSpec = { f: "glyph", o: MadeGlyph._glyphOpts };
1550
- tonalRange;
1551
- invertTonal;
1552
- saturationRange;
1553
- invertSaturation;
1554
- hueCenter;
1555
- hueTolerance;
1556
- invertHue;
1557
1506
  center;
1558
1507
  fontSize;
1559
1508
  constructor(w, h) {
1560
1509
  super(w, h);
1561
- this.tonalRange = opts?.tonalRange;
1562
- this.invertTonal = opts?.invertTonal;
1563
- this.saturationRange = opts?.saturationRange;
1564
- this.invertSaturation = opts?.invertSaturation;
1565
- this.hueCenter = opts?.hueCenter;
1566
- this.hueTolerance = opts?.hueTolerance;
1567
- this.invertHue = opts?.invertHue;
1510
+ this.applyConstraints(opts);
1568
1511
  this.center = Shape.randomPoint(w, h);
1569
- this.fontSize = Math.max(sizeRange[0], sizeRange[0] + ~~(Math.random() * (sizeRange[1] - sizeRange[0])));
1512
+ this.fontSize = sizeRange[0] + ~~(Math.random() * (sizeRange[1] - sizeRange[0]));
1570
1513
  this.computeBbox();
1571
1514
  }
1572
1515
  computeBbox() {
1573
- const tmp = new Canvas(1, 1);
1516
+ const tmp = getGlyphMeasureCanvas();
1574
1517
  tmp.ctx.font = `${this.fontSize}px ${fontFamily}`;
1575
1518
  const w = ~~tmp.ctx.measureText(char).width;
1576
- this.bbox = {
1519
+ return this.setBbox({
1577
1520
  left: ~~(this.center[0] - w / 2),
1578
1521
  top: ~~(this.center[1] - this.fontSize / 2),
1579
1522
  width: w || 1,
1580
1523
  height: this.fontSize
1581
- };
1582
- return this;
1524
+ });
1583
1525
  }
1584
1526
  render(ctx) {
1585
1527
  ctx.textAlign = "center";
@@ -1599,18 +1541,17 @@ function makeGlyph(opts) {
1599
1541
  return text;
1600
1542
  }
1601
1543
  toData(a, c) {
1602
- return { t: "sm", a, c, cx: this.center[0], cy: this.center[1], fs: this.fontSize, text: char };
1544
+ return { t: "sm", a, c, cx: this.center[0], cy: this.center[1], fs: this.fontSize, text: char, fontFamily };
1603
1545
  }
1604
- mutate(_cfg) {
1546
+ mutate() {
1605
1547
  const clone = new MadeGlyph(0, 0);
1606
1548
  clone.center = [this.center[0], this.center[1]];
1607
1549
  clone.fontSize = this.fontSize;
1608
1550
  switch (Math.floor(Math.random() * 2)) {
1609
1551
  case 0: {
1610
- const angle = Math.random() * 2 * Math.PI;
1611
- const d = Math.random() * mutationScale;
1612
- clone.center[0] += ~~(d * Math.cos(angle));
1613
- clone.center[1] += ~~(d * Math.sin(angle));
1552
+ const [dx, dy] = randomPolarOffset(mutationScale);
1553
+ clone.center[0] += dx;
1554
+ clone.center[1] += dy;
1614
1555
  break;
1615
1556
  }
1616
1557
  case 1: {
@@ -1643,6 +1584,7 @@ function buildStepPlan(cfg) {
1643
1584
  const { shapeTypes, shapeWeights, steps } = cfg;
1644
1585
  if (!shapeWeights || shapeWeights.length !== shapeTypes.length) return [];
1645
1586
  const total = shapeWeights.reduce((sum, w) => sum + w, 0);
1587
+ if (total <= 0) return [];
1646
1588
  const floats = shapeWeights.map((w) => w / total * steps);
1647
1589
  const floors = floats.map(Math.floor);
1648
1590
  const remainder = steps - floors.reduce((s, n) => s + n, 0);
@@ -1663,6 +1605,7 @@ var Optimizer = class {
1663
1605
  cfg;
1664
1606
  state;
1665
1607
  onStep;
1608
+ onError;
1666
1609
  _steps;
1667
1610
  _stopped;
1668
1611
  _paused;
@@ -1678,6 +1621,8 @@ var Optimizer = class {
1678
1621
  this._rejectionStreak = 0;
1679
1622
  this.onStep = () => {
1680
1623
  };
1624
+ this.onError = () => {
1625
+ };
1681
1626
  this._stepPlan = buildStepPlan(cfg);
1682
1627
  this._schedule = schedule;
1683
1628
  }
@@ -1714,7 +1659,10 @@ var Optimizer = class {
1714
1659
  this.onStep(null);
1715
1660
  }
1716
1661
  this._continue();
1717
- }).catch(() => this._continue());
1662
+ }).catch((error) => {
1663
+ this.onError(error);
1664
+ this.stop();
1665
+ });
1718
1666
  }
1719
1667
  _continue() {
1720
1668
  if (this._stopped || this._paused) {
@@ -1740,28 +1688,20 @@ var Optimizer = class {
1740
1688
  }
1741
1689
  return Promise.all(promises).then(() => bestStep);
1742
1690
  }
1743
- _optimizeStep(step) {
1691
+ async _optimizeStep(step) {
1744
1692
  const LIMIT = this.cfg.mutations;
1745
1693
  let failedAttempts = 0;
1746
- let resolve;
1747
1694
  let bestStep = step;
1748
- const promise = new Promise((r) => resolve = r);
1749
- const tryMutation = /* @__PURE__ */ __name(() => {
1750
- if (failedAttempts >= LIMIT) {
1751
- return resolve(bestStep);
1695
+ while (!this._stopped && failedAttempts < LIMIT) {
1696
+ const mutatedStep = await bestStep.mutate().compute(this.state);
1697
+ if (mutatedStep.distance < bestStep.distance) {
1698
+ failedAttempts = 0;
1699
+ bestStep = mutatedStep;
1700
+ } else {
1701
+ failedAttempts++;
1752
1702
  }
1753
- bestStep.mutate().compute(this.state).then((mutatedStep) => {
1754
- if (mutatedStep.distance < bestStep.distance) {
1755
- failedAttempts = 0;
1756
- bestStep = mutatedStep;
1757
- } else {
1758
- failedAttempts++;
1759
- }
1760
- tryMutation();
1761
- });
1762
- }, "tryMutation");
1763
- tryMutation();
1764
- return promise;
1703
+ }
1704
+ return bestStep;
1765
1705
  }
1766
1706
  };
1767
1707
 
@@ -1770,13 +1710,6 @@ function rgbString([r, g, b]) {
1770
1710
  return `rgb(${r}, ${g}, ${b})`;
1771
1711
  }
1772
1712
  __name(rgbString, "rgbString");
1773
- function hexPoints(cx, cy, r, angle) {
1774
- return Array.from({ length: 6 }, (_, i) => {
1775
- const a = angle + i * Math.PI / 3;
1776
- return [~~(cx + r * Math.cos(a)), ~~(cy + r * Math.sin(a))];
1777
- });
1778
- }
1779
- __name(hexPoints, "hexPoints");
1780
1713
  function renderStepToCtx(data, ctx) {
1781
1714
  ctx.globalAlpha = data.a;
1782
1715
  ctx.fillStyle = rgbString(data.c);
@@ -1807,7 +1740,7 @@ function renderStepToCtx(data, ctx) {
1807
1740
  break;
1808
1741
  }
1809
1742
  case "h": {
1810
- const pts = hexPoints(data.cx, data.cy, data.r, data.angle);
1743
+ const pts = regularPolygonPoints(data.cx, data.cy, 6, data.angle, data.r);
1811
1744
  ctx.beginPath();
1812
1745
  pts.forEach(([x, y], i) => i ? ctx.lineTo(x, y) : ctx.moveTo(x, y));
1813
1746
  ctx.closePath();
@@ -1817,23 +1750,20 @@ function renderStepToCtx(data, ctx) {
1817
1750
  case "sm": {
1818
1751
  ctx.textAlign = "center";
1819
1752
  ctx.textBaseline = "middle";
1820
- ctx.font = `${data.fs}px sans-serif`;
1753
+ ctx.font = `${data.fs}px ${data.fontFamily ?? "sans-serif"}`;
1821
1754
  ctx.fillText(data.text, data.cx, data.cy);
1822
1755
  break;
1823
1756
  }
1824
1757
  case "rc": {
1825
- const { cx, cy, hw, hh, angle } = data;
1826
- const cos = Math.cos(angle);
1827
- const sin = Math.sin(angle);
1758
+ const corners = rectCorners(data.cx, data.cy, data.hw, data.hh, data.angle);
1828
1759
  ctx.beginPath();
1829
- ctx.moveTo(cx - hw * cos + hh * sin, cy - hw * sin - hh * cos);
1830
- ctx.lineTo(cx + hw * cos + hh * sin, cy + hw * sin - hh * cos);
1831
- ctx.lineTo(cx + hw * cos - hh * sin, cy + hw * sin + hh * cos);
1832
- ctx.lineTo(cx - hw * cos - hh * sin, cy - hw * sin + hh * cos);
1760
+ corners.forEach(([x, y], i) => i ? ctx.lineTo(x, y) : ctx.moveTo(x, y));
1833
1761
  ctx.closePath();
1834
1762
  ctx.fill();
1835
1763
  break;
1836
1764
  }
1765
+ default:
1766
+ throw new Error("renderStepToCtx: unknown step type");
1837
1767
  }
1838
1768
  }
1839
1769
  __name(renderStepToCtx, "renderStepToCtx");
@@ -1875,7 +1805,7 @@ function stepDataToSVGElement(data) {
1875
1805
  }
1876
1806
  case "h": {
1877
1807
  node = document.createElementNS(SVGNS, "polygon");
1878
- const pts = hexPoints(data.cx, data.cy, data.r, data.angle);
1808
+ const pts = regularPolygonPoints(data.cx, data.cy, 6, data.angle, data.r);
1879
1809
  node.setAttribute("points", pts.map((p) => p.join(",")).join(" "));
1880
1810
  break;
1881
1811
  }
@@ -1885,26 +1815,20 @@ function stepDataToSVGElement(data) {
1885
1815
  node.setAttribute("text-anchor", "middle");
1886
1816
  node.setAttribute("dominant-baseline", "central");
1887
1817
  node.setAttribute("font-size", String(data.fs));
1888
- node.setAttribute("font-family", "sans-serif");
1818
+ node.setAttribute("font-family", data.fontFamily ?? "sans-serif");
1889
1819
  node.setAttribute("x", String(data.cx));
1890
1820
  node.setAttribute("y", String(data.cy));
1891
1821
  break;
1892
1822
  }
1893
1823
  case "rc": {
1894
- const { cx, cy, hw, hh, angle } = data;
1895
- const cos = Math.cos(angle);
1896
- const sin = Math.sin(angle);
1897
1824
  const fmt = /* @__PURE__ */ __name((n) => n.toFixed(2), "fmt");
1898
- const pts = [
1899
- [cx - hw * cos + hh * sin, cy - hw * sin - hh * cos],
1900
- [cx + hw * cos + hh * sin, cy + hw * sin - hh * cos],
1901
- [cx + hw * cos - hh * sin, cy + hw * sin + hh * cos],
1902
- [cx - hw * cos - hh * sin, cy - hw * sin + hh * cos]
1903
- ];
1825
+ const pts = rectCorners(data.cx, data.cy, data.hw, data.hh, data.angle);
1904
1826
  node = document.createElementNS(SVGNS, "polygon");
1905
1827
  node.setAttribute("points", pts.map(([x, y]) => `${fmt(x)},${fmt(y)}`).join(" "));
1906
1828
  break;
1907
1829
  }
1830
+ default:
1831
+ throw new Error("stepDataToSVGElement: unknown step type");
1908
1832
  }
1909
1833
  node.setAttribute("fill", color);
1910
1834
  node.setAttribute("fill-opacity", opacity);
@@ -1912,6 +1836,9 @@ function stepDataToSVGElement(data) {
1912
1836
  }
1913
1837
  __name(stepDataToSVGElement, "stepDataToSVGElement");
1914
1838
  function replayOutput(data) {
1839
+ if (data.v !== 1) {
1840
+ throw new Error(`Unsupported serialized output version: ${data.v}`);
1841
+ }
1915
1842
  const fill = rgbString(data.fill);
1916
1843
  const vw = data.w * data.scale;
1917
1844
  const vh = data.h * data.scale;
@@ -1944,6 +1871,7 @@ export {
1944
1871
  State,
1945
1872
  Step,
1946
1873
  Triangle,
1874
+ bboxOfPoints,
1947
1875
  clamp,
1948
1876
  clampColor,
1949
1877
  computeColorAndDifferenceChange,
@@ -1957,6 +1885,9 @@ export {
1957
1885
  makeNGon,
1958
1886
  makeRect,
1959
1887
  parseColor,
1888
+ randomPolarOffset,
1889
+ rectCorners,
1890
+ regularPolygonPoints,
1960
1891
  renderStepToCtx,
1961
1892
  replayOutput,
1962
1893
  rgbToHsl,