@slithy/prim-lib 0.8.2 → 0.9.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/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
  }
@@ -178,7 +220,6 @@ var Canvas = class _Canvas {
178
220
  const cp = document.createElementNS(SVGNS, "clipPath");
179
221
  defs.appendChild(cp);
180
222
  cp.setAttribute("id", "clip");
181
- cp.setAttribute("clipPathUnits", "objectBoundingBox");
182
223
  let rect = svgRect(width, height);
183
224
  cp.appendChild(rect);
184
225
  rect = svgRect(width, height);
@@ -190,14 +231,14 @@ var Canvas = class _Canvas {
190
231
  if (svg) {
191
232
  return this.svgRoot(cfg.width, cfg.height, cfg.outputFill ?? cfg.fill);
192
233
  } else {
193
- return new this(cfg.width, cfg.height).fill(cfg.fill);
234
+ return new this(cfg.width, cfg.height, true).fill(cfg.fill);
194
235
  }
195
236
  }
196
237
  static original(url, cfg) {
197
238
  if (url == "test") {
198
239
  return Promise.resolve(this.test(cfg));
199
240
  }
200
- return new Promise((resolve) => {
241
+ return new Promise((resolve, reject) => {
201
242
  const img = new Image();
202
243
  if (!url.startsWith("blob:") && !url.startsWith("data:")) {
203
244
  img.crossOrigin = "anonymous";
@@ -207,12 +248,12 @@ var Canvas = class _Canvas {
207
248
  const w = img.naturalWidth;
208
249
  const h = img.naturalHeight;
209
250
  const computeScale = getScale(w, h, cfg.computeSize, cfg.allowUpscale);
210
- cfg.width = w / computeScale;
211
- cfg.height = h / computeScale;
251
+ cfg.width = Math.round(w / computeScale);
252
+ cfg.height = Math.round(h / computeScale);
212
253
  const viewScale = getScale(w, h, cfg.viewSize, cfg.allowUpscale);
213
254
  cfg.scale = computeScale / viewScale;
214
255
  const fullCfg = { ...cfg, width: cfg.width, height: cfg.height };
215
- const canvas = this.empty(fullCfg);
256
+ const canvas = new this(fullCfg.width, fullCfg.height, true);
216
257
  canvas.ctx.drawImage(img, 0, 0, fullCfg.width, fullCfg.height);
217
258
  if (cfg.fill === "transparent") {
218
259
  cfg.outputFill = "transparent";
@@ -221,11 +262,15 @@ var Canvas = class _Canvas {
221
262
  if (cfg.fill === "auto") {
222
263
  cfg.fill = getFill(canvas.getImageData());
223
264
  }
265
+ canvas.ctx.globalCompositeOperation = "destination-over";
266
+ canvas.ctx.fillStyle = cfg.fill;
267
+ canvas.ctx.fillRect(0, 0, fullCfg.width, fullCfg.height);
268
+ canvas.ctx.globalCompositeOperation = "source-over";
269
+ canvas._imageData = null;
224
270
  resolve(canvas);
225
271
  };
226
- img.onerror = (e) => {
227
- console.error(e);
228
- alert("The image URL cannot be loaded. Does the server support CORS?");
272
+ img.onerror = () => {
273
+ reject(new Error("The image URL cannot be loaded. Does the server support CORS?"));
229
274
  };
230
275
  });
231
276
  }
@@ -233,12 +278,12 @@ var Canvas = class _Canvas {
233
278
  const w = bitmap.width;
234
279
  const h = bitmap.height;
235
280
  const computeScale = getScale(w, h, cfg.computeSize, cfg.allowUpscale);
236
- cfg.width = w / computeScale;
237
- cfg.height = h / computeScale;
281
+ cfg.width = Math.round(w / computeScale);
282
+ cfg.height = Math.round(h / computeScale);
238
283
  const viewScale = getScale(w, h, cfg.viewSize, cfg.allowUpscale);
239
284
  cfg.scale = computeScale / viewScale;
240
285
  const fullCfg = { ...cfg, width: cfg.width, height: cfg.height };
241
- const canvas = this.empty(fullCfg);
286
+ const canvas = new this(fullCfg.width, fullCfg.height, true);
242
287
  canvas.ctx.drawImage(bitmap, 0, 0, fullCfg.width, fullCfg.height);
243
288
  if (cfg.fill === "transparent") {
244
289
  cfg.outputFill = "transparent";
@@ -247,6 +292,11 @@ var Canvas = class _Canvas {
247
292
  if (cfg.fill === "auto") {
248
293
  cfg.fill = getFill(canvas.getImageData());
249
294
  }
295
+ canvas.ctx.globalCompositeOperation = "destination-over";
296
+ canvas.ctx.fillStyle = cfg.fill;
297
+ canvas.ctx.fillRect(0, 0, fullCfg.width, fullCfg.height);
298
+ canvas.ctx.globalCompositeOperation = "source-over";
299
+ canvas._imageData = null;
250
300
  return canvas;
251
301
  }
252
302
  static test(cfg) {
@@ -280,22 +330,27 @@ var Canvas = class _Canvas {
280
330
  el.width = width;
281
331
  el.height = height;
282
332
  this.node = el;
283
- this.ctx = el.getContext("2d", { willReadFrequently });
333
+ const ctx = el.getContext("2d", { willReadFrequently });
334
+ if (!ctx) throw new Error("Failed to acquire 2d rendering context");
335
+ this.ctx = ctx;
284
336
  } else {
285
337
  const el = new OffscreenCanvas(width, height);
286
338
  this.node = el;
287
- this.ctx = el.getContext("2d", { willReadFrequently });
339
+ const ctx = el.getContext("2d", { willReadFrequently });
340
+ if (!ctx) throw new Error("Failed to acquire 2d rendering context");
341
+ this.ctx = ctx;
288
342
  }
289
343
  this._imageData = null;
290
344
  }
291
345
  clone() {
292
- const other = new _Canvas(this.node.width, this.node.height);
346
+ const other = new _Canvas(this.node.width, this.node.height, true);
293
347
  other.ctx.drawImage(this.node, 0, 0);
294
348
  return other;
295
349
  }
296
350
  fill(color) {
297
351
  this.ctx.fillStyle = color;
298
352
  this.ctx.fillRect(0, 0, this.node.width, this.node.height);
353
+ this._imageData = null;
299
354
  return this;
300
355
  }
301
356
  getImageData() {
@@ -313,7 +368,39 @@ var Canvas = class _Canvas {
313
368
  const difference2 = this.difference(otherCanvas);
314
369
  return differenceToDistance(difference2, this.node.width * this.node.height);
315
370
  }
371
+ patchImageData(offset, shapeData, color) {
372
+ if (!this._imageData) return;
373
+ const [cr, cg, cb] = color;
374
+ const dst = this._imageData.data;
375
+ const src = shapeData.data;
376
+ const sw = shapeData.width, sh = shapeData.height;
377
+ const fw = this._imageData.width, fh = this._imageData.height;
378
+ for (let sy = 0; sy < sh; sy++) {
379
+ const fy = sy + offset.top;
380
+ if (fy < 0 || fy >= fh) continue;
381
+ for (let sx = 0; sx < sw; sx++) {
382
+ const fx = sx + offset.left;
383
+ if (fx < 0 || fx >= fw) continue;
384
+ const si = 4 * (sx + sy * sw);
385
+ const a = src[si + 3];
386
+ if (a === 0) continue;
387
+ const fi = 4 * (fx + fy * fw);
388
+ const alpha = a / 255, blend = 1 - alpha;
389
+ dst[fi] = cr * alpha + dst[fi] * blend;
390
+ dst[fi + 1] = cg * alpha + dst[fi + 1] * blend;
391
+ dst[fi + 2] = cb * alpha + dst[fi + 2] * blend;
392
+ }
393
+ }
394
+ }
316
395
  drawStep(step) {
396
+ if (this._imageData) {
397
+ try {
398
+ const shapeData = step.shape.rasterize(step.alpha).getImageData();
399
+ this.patchImageData(step.shape.bbox, shapeData, parseColor(step.color));
400
+ } catch {
401
+ this._imageData = null;
402
+ }
403
+ }
317
404
  this.ctx.globalAlpha = step.alpha;
318
405
  this.ctx.fillStyle = step.color;
319
406
  step.shape.render(this.ctx);
@@ -375,8 +462,8 @@ var Step = class _Step {
375
462
  }
376
463
  /* apply this step to a state to get a new state. call only after .compute */
377
464
  apply(state) {
378
- const newCanvas = state.canvas.clone().drawStep(this);
379
- return new State(state.target, newCanvas, this.distance);
465
+ state.canvas.drawStep(this);
466
+ return new State(state.target, state.canvas, this.distance);
380
467
  }
381
468
  /* find optimal color and compute the resulting distance */
382
469
  compute(state) {
@@ -451,7 +538,7 @@ var Step = class _Step {
451
538
  }
452
539
  /* return a slightly mutated step */
453
540
  mutate() {
454
- const newShape = this.shape.mutate(this.cfg);
541
+ const newShape = this.shape.mutate();
455
542
  const mutated = new _Step(newShape, this.cfg);
456
543
  if (this.cfg.mutateAlpha) {
457
544
  const mutatedAlpha = this.alpha + (Math.random() - 0.5) * 0.08;
@@ -463,6 +550,15 @@ var Step = class _Step {
463
550
 
464
551
  // src/shape.ts
465
552
  var _rasterCanvas = null;
553
+ var _glyphMeasureCanvas = null;
554
+ function getGlyphMeasureCanvas() {
555
+ if (!_glyphMeasureCanvas) {
556
+ _glyphMeasureCanvas = new Canvas(1, 1);
557
+ }
558
+ return _glyphMeasureCanvas;
559
+ }
560
+ __name(getGlyphMeasureCanvas, "getGlyphMeasureCanvas");
561
+ var BBOX_PAD = 1;
466
562
  var Shape = class {
467
563
  static {
468
564
  __name(this, "Shape");
@@ -490,7 +586,7 @@ var Shape = class {
490
586
  constructor(_w, _h) {
491
587
  this.bbox = { left: 0, top: 0, width: 0, height: 0 };
492
588
  }
493
- mutate(_cfg) {
589
+ mutate() {
494
590
  return this;
495
591
  }
496
592
  toSVG() {
@@ -520,9 +616,41 @@ var Shape = class {
520
616
  const data = ctx.getImageData(0, 0, w, h);
521
617
  return { getImageData: /* @__PURE__ */ __name(() => data, "getImageData") };
522
618
  }
619
+ /* Grow a tight bbox by BBOX_PAD on all sides and snap to integer bounds.
620
+ * Integer left/top/width/height are required: computeColorAndDifferenceChange
621
+ * and rasterize index/translate a flat pixel array directly from these. */
622
+ setBbox(tight) {
623
+ const left = Math.floor(tight.left) - BBOX_PAD;
624
+ const top = Math.floor(tight.top) - BBOX_PAD;
625
+ const right = Math.ceil(tight.left + tight.width) + BBOX_PAD;
626
+ const bottom = Math.ceil(tight.top + tight.height) + BBOX_PAD;
627
+ this.bbox = { left, top, width: Math.max(1, right - left), height: Math.max(1, bottom - top) };
628
+ return this;
629
+ }
523
630
  render(_ctx) {
524
631
  }
525
632
  };
633
+ var ConstrainedShape = class extends Shape {
634
+ static {
635
+ __name(this, "ConstrainedShape");
636
+ }
637
+ tonalRange;
638
+ invertTonal;
639
+ saturationRange;
640
+ invertSaturation;
641
+ hueCenter;
642
+ hueTolerance;
643
+ invertHue;
644
+ applyConstraints(opts) {
645
+ this.tonalRange = opts?.tonalRange;
646
+ this.invertTonal = opts?.invertTonal;
647
+ this.saturationRange = opts?.saturationRange;
648
+ this.invertSaturation = opts?.invertSaturation;
649
+ this.hueCenter = opts?.hueCenter;
650
+ this.hueTolerance = opts?.hueTolerance;
651
+ this.invertHue = opts?.invertHue;
652
+ }
653
+ };
526
654
  var Polygon = class _Polygon extends Shape {
527
655
  static {
528
656
  __name(this, "Polygon");
@@ -557,33 +685,18 @@ var Polygon = class _Polygon extends Shape {
557
685
  path.setAttribute("d", `${d}Z`);
558
686
  return path;
559
687
  }
560
- mutate(_cfg) {
688
+ mutate() {
561
689
  const clone = this._cloneEmpty();
562
690
  clone.points = this.points.map(([x, y]) => [x, y]);
563
691
  const index = Math.floor(Math.random() * this.points.length);
564
692
  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));
693
+ const [dx, dy] = randomPolarOffset(20);
694
+ point[0] += dx;
695
+ point[1] += dy;
569
696
  return clone.computeBbox();
570
697
  }
571
698
  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;
699
+ return this.setBbox(bboxOfPoints(this.points));
587
700
  }
588
701
  _createPoints(w, h, count) {
589
702
  const first = Shape.randomPoint(w, h);
@@ -626,7 +739,7 @@ var Rectangle = class _Rectangle extends Polygon {
626
739
  toData(a, c) {
627
740
  return { t: "r", a, c, pts: this.points.map(([x, y]) => [x, y]) };
628
741
  }
629
- mutate(_cfg) {
742
+ mutate() {
630
743
  const clone = this._cloneEmpty();
631
744
  clone.points = this.points.map(([x, y]) => [x, y]);
632
745
  const amount = ~~((Math.random() - 0.5) * 20);
@@ -695,17 +808,16 @@ var Ellipse = class _Ellipse extends Shape {
695
808
  toData(a, c) {
696
809
  return { t: "e", a, c, cx: this.center[0], cy: this.center[1], rx: this.rx, ry: this.ry };
697
810
  }
698
- mutate(_cfg) {
811
+ mutate() {
699
812
  const clone = new _Ellipse(0, 0);
700
813
  clone.center = [this.center[0], this.center[1]];
701
814
  clone.rx = this.rx;
702
815
  clone.ry = this.ry;
703
816
  switch (Math.floor(Math.random() * 3)) {
704
817
  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));
818
+ const [dx, dy] = randomPolarOffset(20);
819
+ clone.center[0] += dx;
820
+ clone.center[1] += dy;
709
821
  break;
710
822
  }
711
823
  case 1:
@@ -720,13 +832,12 @@ var Ellipse = class _Ellipse extends Shape {
720
832
  return clone.computeBbox();
721
833
  }
722
834
  computeBbox() {
723
- this.bbox = {
835
+ return this.setBbox({
724
836
  left: this.center[0] - this.rx,
725
837
  top: this.center[1] - this.ry,
726
838
  width: 2 * this.rx,
727
839
  height: 2 * this.ry
728
- };
729
- return this;
840
+ });
730
841
  }
731
842
  };
732
843
  var Circle = class _Circle extends Shape {
@@ -742,13 +853,12 @@ var Circle = class _Circle extends Shape {
742
853
  this.computeBbox();
743
854
  }
744
855
  computeBbox() {
745
- this.bbox = {
856
+ return this.setBbox({
746
857
  left: this.center[0] - this.r,
747
858
  top: this.center[1] - this.r,
748
859
  width: 2 * this.r || 1,
749
860
  height: 2 * this.r || 1
750
- };
751
- return this;
861
+ });
752
862
  }
753
863
  render(ctx) {
754
864
  ctx.beginPath();
@@ -765,16 +875,15 @@ var Circle = class _Circle extends Shape {
765
875
  toData(a, c) {
766
876
  return { t: "c", a, c, cx: this.center[0], cy: this.center[1], r: this.r };
767
877
  }
768
- mutate(_cfg) {
878
+ mutate() {
769
879
  const clone = new _Circle(0, 0);
770
880
  clone.center = [this.center[0], this.center[1]];
771
881
  clone.r = this.r;
772
882
  switch (Math.floor(Math.random() * 2)) {
773
883
  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));
884
+ const [dx, dy] = randomPolarOffset(20);
885
+ clone.center[0] += dx;
886
+ clone.center[1] += dy;
778
887
  break;
779
888
  }
780
889
  case 1:
@@ -800,16 +909,15 @@ var Glyph = class _Glyph extends Shape {
800
909
  this.computeBbox();
801
910
  }
802
911
  computeBbox() {
803
- const tmp = new Canvas(1, 1);
912
+ const tmp = getGlyphMeasureCanvas();
804
913
  tmp.ctx.font = `${this.fontSize}px sans-serif`;
805
914
  const w = ~~tmp.ctx.measureText(this.text).width;
806
- this.bbox = {
915
+ return this.setBbox({
807
916
  left: ~~(this.center[0] - w / 2),
808
917
  top: ~~(this.center[1] - this.fontSize / 2),
809
918
  width: w,
810
919
  height: this.fontSize
811
- };
812
- return this;
920
+ });
813
921
  }
814
922
  render(ctx) {
815
923
  ctx.textAlign = "center";
@@ -817,16 +925,15 @@ var Glyph = class _Glyph extends Shape {
817
925
  ctx.font = `${this.fontSize}px sans-serif`;
818
926
  ctx.fillText(this.text, this.center[0], this.center[1]);
819
927
  }
820
- mutate(_cfg) {
928
+ mutate() {
821
929
  const clone = new _Glyph(0, 0, this.text);
822
930
  clone.center = [this.center[0], this.center[1]];
823
931
  clone.fontSize = this.fontSize;
824
932
  switch (Math.floor(Math.random() * 2)) {
825
933
  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));
934
+ const [dx, dy] = randomPolarOffset(20);
935
+ clone.center[0] += dx;
936
+ clone.center[1] += dy;
830
937
  break;
831
938
  }
832
939
  case 1:
@@ -848,7 +955,7 @@ var Glyph = class _Glyph extends Shape {
848
955
  return text;
849
956
  }
850
957
  toData(a, c) {
851
- return { t: "sm", a, c, cx: this.center[0], cy: this.center[1], fs: this.fontSize, text: this.text };
958
+ return { t: "sm", a, c, cx: this.center[0], cy: this.center[1], fs: this.fontSize, text: this.text, fontFamily: "sans-serif" };
852
959
  }
853
960
  };
854
961
  var Square = class _Square extends Shape {
@@ -864,13 +971,12 @@ var Square = class _Square extends Shape {
864
971
  this.computeBbox();
865
972
  }
866
973
  computeBbox() {
867
- this.bbox = {
974
+ return this.setBbox({
868
975
  left: this.center[0] - this.r,
869
976
  top: this.center[1] - this.r,
870
977
  width: 2 * this.r || 1,
871
978
  height: 2 * this.r || 1
872
- };
873
- return this;
979
+ });
874
980
  }
875
981
  render(ctx) {
876
982
  ctx.fillRect(this.center[0] - this.r, this.center[1] - this.r, 2 * this.r, 2 * this.r);
@@ -886,16 +992,15 @@ var Square = class _Square extends Shape {
886
992
  toData(a, c) {
887
993
  return { t: "s", a, c, cx: this.center[0], cy: this.center[1], r: this.r };
888
994
  }
889
- mutate(_cfg) {
995
+ mutate() {
890
996
  const clone = new _Square(0, 0);
891
997
  clone.center = [this.center[0], this.center[1]];
892
998
  clone.r = this.r;
893
999
  switch (Math.floor(Math.random() * 2)) {
894
1000
  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));
1001
+ const [dx, dy] = randomPolarOffset(20);
1002
+ clone.center[0] += dx;
1003
+ clone.center[1] += dy;
899
1004
  break;
900
1005
  }
901
1006
  case 1:
@@ -924,33 +1029,14 @@ var Hexagon = class _Hexagon extends Shape {
924
1029
  }
925
1030
  _points() {
926
1031
  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
- });
1032
+ this._cachedPoints = regularPolygonPoints(this.center[0], this.center[1], 6, this.angle, this.r);
934
1033
  }
935
1034
  return this._cachedPoints;
936
1035
  }
937
1036
  computeBbox() {
938
1037
  this._cachedPoints = null;
939
1038
  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;
1039
+ return this.setBbox(bboxOfPoints(pts));
954
1040
  }
955
1041
  render(ctx) {
956
1042
  const pts = this._points();
@@ -967,17 +1053,16 @@ var Hexagon = class _Hexagon extends Shape {
967
1053
  toData(a, c) {
968
1054
  return { t: "h", a, c, cx: this.center[0], cy: this.center[1], r: this.r, angle: this.angle };
969
1055
  }
970
- mutate(_cfg) {
1056
+ mutate() {
971
1057
  const clone = new _Hexagon(0, 0);
972
1058
  clone.center = [this.center[0], this.center[1]];
973
1059
  clone.r = this.r;
974
1060
  clone.angle = this.angle;
975
1061
  switch (Math.floor(Math.random() * 3)) {
976
1062
  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));
1063
+ const [dx, dy] = randomPolarOffset(20);
1064
+ clone.center[0] += dx;
1065
+ clone.center[1] += dy;
981
1066
  break;
982
1067
  }
983
1068
  case 1:
@@ -1027,19 +1112,12 @@ function makeNGon(opts) {
1027
1112
  const startAngle = opts.startAngle ?? defaultAngle;
1028
1113
  if (sides < 3) throw new RangeError("makeNGon requires at least 3 sides");
1029
1114
  if (regular) {
1030
- class NGonRegular extends Shape {
1115
+ class NGonRegular extends ConstrainedShape {
1031
1116
  static {
1032
1117
  __name(this, "NGonRegular");
1033
1118
  }
1034
1119
  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
1120
  static _shapeSpec = { f: "ngon", o: NGonRegular._ngonOpts };
1036
- tonalRange;
1037
- invertTonal;
1038
- saturationRange;
1039
- invertSaturation;
1040
- hueCenter;
1041
- hueTolerance;
1042
- invertHue;
1043
1121
  center;
1044
1122
  r;
1045
1123
  angle;
@@ -1047,13 +1125,7 @@ function makeNGon(opts) {
1047
1125
  _cachedPoints;
1048
1126
  constructor(w, h) {
1049
1127
  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;
1128
+ this.applyConstraints(opts);
1057
1129
  this.center = Shape.randomPoint(w, h);
1058
1130
  this.r = Math.max(1, sizeRange[0] + ~~(Math.random() * (sizeRange[1] - sizeRange[0])));
1059
1131
  this.angle = rotatable ? Math.random() * (2 * Math.PI / sides) : startAngle;
@@ -1077,20 +1149,7 @@ function makeNGon(opts) {
1077
1149
  computeBbox() {
1078
1150
  this._cachedPoints = null;
1079
1151
  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;
1152
+ return this.setBbox(bboxOfPoints(pts));
1094
1153
  }
1095
1154
  render(ctx) {
1096
1155
  const pts = this._points();
@@ -1107,7 +1166,7 @@ function makeNGon(opts) {
1107
1166
  toData(a, c) {
1108
1167
  return { t: "p", a, c, pts: this._points().map(([x, y]) => [x, y]) };
1109
1168
  }
1110
- mutate(_cfg) {
1169
+ mutate() {
1111
1170
  const clone = new NGonRegular(0, 0);
1112
1171
  clone.center = [this.center[0], this.center[1]];
1113
1172
  clone.r = this.r;
@@ -1116,10 +1175,9 @@ function makeNGon(opts) {
1116
1175
  const mutCount = 2 + (rotatable ? 1 : 0) + (noise > 0 ? 1 : 0);
1117
1176
  switch (Math.floor(Math.random() * mutCount)) {
1118
1177
  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));
1178
+ const [dx, dy] = randomPolarOffset(mutationScale);
1179
+ clone.center[0] += dx;
1180
+ clone.center[1] += dy;
1123
1181
  break;
1124
1182
  }
1125
1183
  case 1:
@@ -1145,29 +1203,16 @@ function makeNGon(opts) {
1145
1203
  }
1146
1204
  return NGonRegular;
1147
1205
  } else {
1148
- class NGonIrregular extends Shape {
1206
+ class NGonIrregular extends ConstrainedShape {
1149
1207
  static {
1150
1208
  __name(this, "NGonIrregular");
1151
1209
  }
1152
1210
  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
1211
  static _shapeSpec = { f: "ngon", o: NGonIrregular._ngonOpts };
1154
- tonalRange;
1155
- invertTonal;
1156
- saturationRange;
1157
- invertSaturation;
1158
- hueCenter;
1159
- hueTolerance;
1160
- invertHue;
1161
1212
  points;
1162
1213
  constructor(w, h) {
1163
1214
  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;
1215
+ this.applyConstraints(opts);
1171
1216
  const first = Shape.randomPoint(w, h);
1172
1217
  this.points = [first];
1173
1218
  for (let i = 1; i < sides; i++) {
@@ -1182,20 +1227,7 @@ function makeNGon(opts) {
1182
1227
  this.computeBbox();
1183
1228
  }
1184
1229
  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;
1230
+ return this.setBbox(bboxOfPoints(this.points));
1199
1231
  }
1200
1232
  render(ctx) {
1201
1233
  ctx.beginPath();
@@ -1212,15 +1244,14 @@ function makeNGon(opts) {
1212
1244
  toData(a, c) {
1213
1245
  return { t: "p", a, c, pts: this.points.map(([x, y]) => [x, y]) };
1214
1246
  }
1215
- mutate(_cfg) {
1247
+ mutate() {
1216
1248
  const clone = new NGonIrregular(0, 0);
1217
1249
  clone.points = this.points.map(([x, y]) => [x, y]);
1218
1250
  const index = Math.floor(Math.random() * clone.points.length);
1219
1251
  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));
1252
+ const [dx, dy] = randomPolarOffset(mutationScale);
1253
+ point[0] += dx;
1254
+ point[1] += dy;
1224
1255
  if (convex) clone.points = convexHull(clone.points);
1225
1256
  return clone.computeBbox();
1226
1257
  }
@@ -1235,32 +1266,19 @@ function makeRect(opts) {
1235
1266
  const aspectRatio = opts?.aspectRatio;
1236
1267
  const rotatable = opts?.rotatable ?? false;
1237
1268
  const mutationScale = opts?.mutationScale ?? 20;
1238
- class Rect extends Shape {
1269
+ class Rect extends ConstrainedShape {
1239
1270
  static {
1240
1271
  __name(this, "Rect");
1241
1272
  }
1242
1273
  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
1274
  static _shapeSpec = { f: "rect", o: Rect._rectOpts };
1244
- tonalRange;
1245
- invertTonal;
1246
- saturationRange;
1247
- invertSaturation;
1248
- hueCenter;
1249
- hueTolerance;
1250
- invertHue;
1251
1275
  center;
1252
1276
  hw;
1253
1277
  hh;
1254
1278
  angle;
1255
1279
  constructor(w, h) {
1256
1280
  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;
1281
+ this.applyConstraints(opts);
1264
1282
  this.center = Shape.randomPoint(w, h);
1265
1283
  this.hw = widthRange[0] + ~~(Math.random() * (widthRange[1] - widthRange[0]));
1266
1284
  if (aspectRatio !== void 0) {
@@ -1272,41 +1290,18 @@ function makeRect(opts) {
1272
1290
  this.computeBbox();
1273
1291
  }
1274
1292
  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;
1293
+ const corners = rectCorners(this.center[0], this.center[1], this.hw, this.hh, this.angle);
1294
+ return this.setBbox(bboxOfPoints(corners));
1286
1295
  }
1287
1296
  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
- ];
1297
+ const corners = rectCorners(this.center[0], this.center[1], this.hw, this.hh, this.angle);
1296
1298
  ctx.beginPath();
1297
- corners.forEach(([x, y], i) => i ? ctx.lineTo(~~x, ~~y) : ctx.moveTo(~~x, ~~y));
1299
+ corners.forEach(([x, y], i) => i ? ctx.lineTo(x, y) : ctx.moveTo(x, y));
1298
1300
  ctx.closePath();
1299
1301
  ctx.fill();
1300
1302
  }
1301
1303
  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
- ];
1304
+ const corners = rectCorners(this.center[0], this.center[1], this.hw, this.hh, this.angle);
1310
1305
  const node = document.createElementNS(SVGNS, "polygon");
1311
1306
  node.setAttribute("points", corners.map((p) => p.join(",")).join(" "));
1312
1307
  return node;
@@ -1323,7 +1318,7 @@ function makeRect(opts) {
1323
1318
  angle: this.angle
1324
1319
  };
1325
1320
  }
1326
- mutate(_cfg) {
1321
+ mutate() {
1327
1322
  const clone = new Rect(0, 0);
1328
1323
  clone.center = [this.center[0], this.center[1]];
1329
1324
  clone.hw = this.hw;
@@ -1332,10 +1327,9 @@ function makeRect(opts) {
1332
1327
  const mutCount = 2 + (rotatable ? 1 : 0) + (aspectRatio === void 0 ? 1 : 0);
1333
1328
  switch (Math.floor(Math.random() * mutCount)) {
1334
1329
  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));
1330
+ const [dx, dy] = randomPolarOffset(mutationScale);
1331
+ clone.center[0] += dx;
1332
+ clone.center[1] += dy;
1339
1333
  break;
1340
1334
  }
1341
1335
  case 1:
@@ -1369,42 +1363,28 @@ __name(makeRect, "makeRect");
1369
1363
  function makeCircle(opts) {
1370
1364
  const sizeRange = opts?.sizeRange ?? [1, 20];
1371
1365
  const mutationScale = opts?.mutationScale ?? 20;
1372
- class MadeCircle extends Shape {
1366
+ class MadeCircle extends ConstrainedShape {
1373
1367
  static {
1374
1368
  __name(this, "MadeCircle");
1375
1369
  }
1376
1370
  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
1371
  static _shapeSpec = { f: "circle", o: MadeCircle._circleOpts };
1378
- tonalRange;
1379
- invertTonal;
1380
- saturationRange;
1381
- invertSaturation;
1382
- hueCenter;
1383
- hueTolerance;
1384
- invertHue;
1385
1372
  center;
1386
1373
  r;
1387
1374
  constructor(w, h) {
1388
1375
  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;
1376
+ this.applyConstraints(opts);
1396
1377
  this.center = Shape.randomPoint(w, h);
1397
1378
  this.r = Math.max(1, sizeRange[0] + ~~(Math.random() * (sizeRange[1] - sizeRange[0])));
1398
1379
  this.computeBbox();
1399
1380
  }
1400
1381
  computeBbox() {
1401
- this.bbox = {
1382
+ return this.setBbox({
1402
1383
  left: this.center[0] - this.r,
1403
1384
  top: this.center[1] - this.r,
1404
1385
  width: 2 * this.r || 1,
1405
1386
  height: 2 * this.r || 1
1406
- };
1407
- return this;
1387
+ });
1408
1388
  }
1409
1389
  render(ctx) {
1410
1390
  ctx.beginPath();
@@ -1421,16 +1401,15 @@ function makeCircle(opts) {
1421
1401
  toData(a, c) {
1422
1402
  return { t: "c", a, c, cx: this.center[0], cy: this.center[1], r: this.r };
1423
1403
  }
1424
- mutate(_cfg) {
1404
+ mutate() {
1425
1405
  const clone = new MadeCircle(0, 0);
1426
1406
  clone.center = [this.center[0], this.center[1]];
1427
1407
  clone.r = this.r;
1428
1408
  switch (Math.floor(Math.random() * 2)) {
1429
1409
  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));
1410
+ const [dx, dy] = randomPolarOffset(mutationScale);
1411
+ clone.center[0] += dx;
1412
+ clone.center[1] += dy;
1434
1413
  break;
1435
1414
  }
1436
1415
  case 1:
@@ -1449,44 +1428,30 @@ function makeEllipse(opts) {
1449
1428
  const ryRange = opts?.ryRange ?? [1, 20];
1450
1429
  const aspectRatio = opts?.aspectRatio;
1451
1430
  const mutationScale = opts?.mutationScale ?? 20;
1452
- class MadeEllipse extends Shape {
1431
+ class MadeEllipse extends ConstrainedShape {
1453
1432
  static {
1454
1433
  __name(this, "MadeEllipse");
1455
1434
  }
1456
1435
  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
1436
  static _shapeSpec = { f: "ellipse", o: MadeEllipse._ellipseOpts };
1458
- tonalRange;
1459
- invertTonal;
1460
- saturationRange;
1461
- invertSaturation;
1462
- hueCenter;
1463
- hueTolerance;
1464
- invertHue;
1465
1437
  center;
1466
1438
  rx;
1467
1439
  ry;
1468
1440
  constructor(w, h) {
1469
1441
  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;
1442
+ this.applyConstraints(opts);
1477
1443
  this.center = Shape.randomPoint(w, h);
1478
1444
  this.rx = Math.max(1, rxRange[0] + ~~(Math.random() * (rxRange[1] - rxRange[0])));
1479
1445
  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
1446
  this.computeBbox();
1481
1447
  }
1482
1448
  computeBbox() {
1483
- this.bbox = {
1449
+ return this.setBbox({
1484
1450
  left: this.center[0] - this.rx,
1485
1451
  top: this.center[1] - this.ry,
1486
1452
  width: 2 * this.rx || 1,
1487
1453
  height: 2 * this.ry || 1
1488
- };
1489
- return this;
1454
+ });
1490
1455
  }
1491
1456
  render(ctx) {
1492
1457
  ctx.beginPath();
@@ -1504,7 +1469,7 @@ function makeEllipse(opts) {
1504
1469
  toData(a, c) {
1505
1470
  return { t: "e", a, c, cx: this.center[0], cy: this.center[1], rx: this.rx, ry: this.ry };
1506
1471
  }
1507
- mutate(_cfg) {
1472
+ mutate() {
1508
1473
  const clone = new MadeEllipse(0, 0);
1509
1474
  clone.center = [this.center[0], this.center[1]];
1510
1475
  clone.rx = this.rx;
@@ -1512,10 +1477,9 @@ function makeEllipse(opts) {
1512
1477
  const mutCount = aspectRatio === void 0 ? 3 : 2;
1513
1478
  switch (Math.floor(Math.random() * mutCount)) {
1514
1479
  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));
1480
+ const [dx, dy] = randomPolarOffset(mutationScale);
1481
+ clone.center[0] += dx;
1482
+ clone.center[1] += dy;
1519
1483
  break;
1520
1484
  }
1521
1485
  case 1:
@@ -1541,45 +1505,31 @@ function makeGlyph(opts) {
1541
1505
  const fontFamily = opts?.fontFamily ?? "sans-serif";
1542
1506
  const sizeRange = opts?.sizeRange ?? [10, 30];
1543
1507
  const mutationScale = opts?.mutationScale ?? 20;
1544
- class MadeGlyph extends Shape {
1508
+ class MadeGlyph extends ConstrainedShape {
1545
1509
  static {
1546
1510
  __name(this, "MadeGlyph");
1547
1511
  }
1548
1512
  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
1513
  static _shapeSpec = { f: "glyph", o: MadeGlyph._glyphOpts };
1550
- tonalRange;
1551
- invertTonal;
1552
- saturationRange;
1553
- invertSaturation;
1554
- hueCenter;
1555
- hueTolerance;
1556
- invertHue;
1557
1514
  center;
1558
1515
  fontSize;
1559
1516
  constructor(w, h) {
1560
1517
  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;
1518
+ this.applyConstraints(opts);
1568
1519
  this.center = Shape.randomPoint(w, h);
1569
- this.fontSize = Math.max(sizeRange[0], sizeRange[0] + ~~(Math.random() * (sizeRange[1] - sizeRange[0])));
1520
+ this.fontSize = sizeRange[0] + ~~(Math.random() * (sizeRange[1] - sizeRange[0]));
1570
1521
  this.computeBbox();
1571
1522
  }
1572
1523
  computeBbox() {
1573
- const tmp = new Canvas(1, 1);
1524
+ const tmp = getGlyphMeasureCanvas();
1574
1525
  tmp.ctx.font = `${this.fontSize}px ${fontFamily}`;
1575
1526
  const w = ~~tmp.ctx.measureText(char).width;
1576
- this.bbox = {
1527
+ return this.setBbox({
1577
1528
  left: ~~(this.center[0] - w / 2),
1578
1529
  top: ~~(this.center[1] - this.fontSize / 2),
1579
1530
  width: w || 1,
1580
1531
  height: this.fontSize
1581
- };
1582
- return this;
1532
+ });
1583
1533
  }
1584
1534
  render(ctx) {
1585
1535
  ctx.textAlign = "center";
@@ -1599,18 +1549,17 @@ function makeGlyph(opts) {
1599
1549
  return text;
1600
1550
  }
1601
1551
  toData(a, c) {
1602
- return { t: "sm", a, c, cx: this.center[0], cy: this.center[1], fs: this.fontSize, text: char };
1552
+ return { t: "sm", a, c, cx: this.center[0], cy: this.center[1], fs: this.fontSize, text: char, fontFamily };
1603
1553
  }
1604
- mutate(_cfg) {
1554
+ mutate() {
1605
1555
  const clone = new MadeGlyph(0, 0);
1606
1556
  clone.center = [this.center[0], this.center[1]];
1607
1557
  clone.fontSize = this.fontSize;
1608
1558
  switch (Math.floor(Math.random() * 2)) {
1609
1559
  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));
1560
+ const [dx, dy] = randomPolarOffset(mutationScale);
1561
+ clone.center[0] += dx;
1562
+ clone.center[1] += dy;
1614
1563
  break;
1615
1564
  }
1616
1565
  case 1: {
@@ -1643,6 +1592,7 @@ function buildStepPlan(cfg) {
1643
1592
  const { shapeTypes, shapeWeights, steps } = cfg;
1644
1593
  if (!shapeWeights || shapeWeights.length !== shapeTypes.length) return [];
1645
1594
  const total = shapeWeights.reduce((sum, w) => sum + w, 0);
1595
+ if (total <= 0) return [];
1646
1596
  const floats = shapeWeights.map((w) => w / total * steps);
1647
1597
  const floors = floats.map(Math.floor);
1648
1598
  const remainder = steps - floors.reduce((s, n) => s + n, 0);
@@ -1663,6 +1613,7 @@ var Optimizer = class {
1663
1613
  cfg;
1664
1614
  state;
1665
1615
  onStep;
1616
+ onError;
1666
1617
  _steps;
1667
1618
  _stopped;
1668
1619
  _paused;
@@ -1678,6 +1629,8 @@ var Optimizer = class {
1678
1629
  this._rejectionStreak = 0;
1679
1630
  this.onStep = () => {
1680
1631
  };
1632
+ this.onError = () => {
1633
+ };
1681
1634
  this._stepPlan = buildStepPlan(cfg);
1682
1635
  this._schedule = schedule;
1683
1636
  }
@@ -1714,7 +1667,10 @@ var Optimizer = class {
1714
1667
  this.onStep(null);
1715
1668
  }
1716
1669
  this._continue();
1717
- }).catch(() => this._continue());
1670
+ }).catch((error) => {
1671
+ this.onError(error);
1672
+ this.stop();
1673
+ });
1718
1674
  }
1719
1675
  _continue() {
1720
1676
  if (this._stopped || this._paused) {
@@ -1740,28 +1696,20 @@ var Optimizer = class {
1740
1696
  }
1741
1697
  return Promise.all(promises).then(() => bestStep);
1742
1698
  }
1743
- _optimizeStep(step) {
1699
+ async _optimizeStep(step) {
1744
1700
  const LIMIT = this.cfg.mutations;
1745
1701
  let failedAttempts = 0;
1746
- let resolve;
1747
1702
  let bestStep = step;
1748
- const promise = new Promise((r) => resolve = r);
1749
- const tryMutation = /* @__PURE__ */ __name(() => {
1750
- if (failedAttempts >= LIMIT) {
1751
- return resolve(bestStep);
1703
+ while (!this._stopped && failedAttempts < LIMIT) {
1704
+ const mutatedStep = await bestStep.mutate().compute(this.state);
1705
+ if (mutatedStep.distance < bestStep.distance) {
1706
+ failedAttempts = 0;
1707
+ bestStep = mutatedStep;
1708
+ } else {
1709
+ failedAttempts++;
1752
1710
  }
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;
1711
+ }
1712
+ return bestStep;
1765
1713
  }
1766
1714
  };
1767
1715
 
@@ -1770,13 +1718,6 @@ function rgbString([r, g, b]) {
1770
1718
  return `rgb(${r}, ${g}, ${b})`;
1771
1719
  }
1772
1720
  __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
1721
  function renderStepToCtx(data, ctx) {
1781
1722
  ctx.globalAlpha = data.a;
1782
1723
  ctx.fillStyle = rgbString(data.c);
@@ -1807,7 +1748,7 @@ function renderStepToCtx(data, ctx) {
1807
1748
  break;
1808
1749
  }
1809
1750
  case "h": {
1810
- const pts = hexPoints(data.cx, data.cy, data.r, data.angle);
1751
+ const pts = regularPolygonPoints(data.cx, data.cy, 6, data.angle, data.r);
1811
1752
  ctx.beginPath();
1812
1753
  pts.forEach(([x, y], i) => i ? ctx.lineTo(x, y) : ctx.moveTo(x, y));
1813
1754
  ctx.closePath();
@@ -1817,23 +1758,20 @@ function renderStepToCtx(data, ctx) {
1817
1758
  case "sm": {
1818
1759
  ctx.textAlign = "center";
1819
1760
  ctx.textBaseline = "middle";
1820
- ctx.font = `${data.fs}px sans-serif`;
1761
+ ctx.font = `${data.fs}px ${data.fontFamily ?? "sans-serif"}`;
1821
1762
  ctx.fillText(data.text, data.cx, data.cy);
1822
1763
  break;
1823
1764
  }
1824
1765
  case "rc": {
1825
- const { cx, cy, hw, hh, angle } = data;
1826
- const cos = Math.cos(angle);
1827
- const sin = Math.sin(angle);
1766
+ const corners = rectCorners(data.cx, data.cy, data.hw, data.hh, data.angle);
1828
1767
  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);
1768
+ corners.forEach(([x, y], i) => i ? ctx.lineTo(x, y) : ctx.moveTo(x, y));
1833
1769
  ctx.closePath();
1834
1770
  ctx.fill();
1835
1771
  break;
1836
1772
  }
1773
+ default:
1774
+ throw new Error("renderStepToCtx: unknown step type");
1837
1775
  }
1838
1776
  }
1839
1777
  __name(renderStepToCtx, "renderStepToCtx");
@@ -1875,7 +1813,7 @@ function stepDataToSVGElement(data) {
1875
1813
  }
1876
1814
  case "h": {
1877
1815
  node = document.createElementNS(SVGNS, "polygon");
1878
- const pts = hexPoints(data.cx, data.cy, data.r, data.angle);
1816
+ const pts = regularPolygonPoints(data.cx, data.cy, 6, data.angle, data.r);
1879
1817
  node.setAttribute("points", pts.map((p) => p.join(",")).join(" "));
1880
1818
  break;
1881
1819
  }
@@ -1885,26 +1823,20 @@ function stepDataToSVGElement(data) {
1885
1823
  node.setAttribute("text-anchor", "middle");
1886
1824
  node.setAttribute("dominant-baseline", "central");
1887
1825
  node.setAttribute("font-size", String(data.fs));
1888
- node.setAttribute("font-family", "sans-serif");
1826
+ node.setAttribute("font-family", data.fontFamily ?? "sans-serif");
1889
1827
  node.setAttribute("x", String(data.cx));
1890
1828
  node.setAttribute("y", String(data.cy));
1891
1829
  break;
1892
1830
  }
1893
1831
  case "rc": {
1894
- const { cx, cy, hw, hh, angle } = data;
1895
- const cos = Math.cos(angle);
1896
- const sin = Math.sin(angle);
1897
1832
  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
- ];
1833
+ const pts = rectCorners(data.cx, data.cy, data.hw, data.hh, data.angle);
1904
1834
  node = document.createElementNS(SVGNS, "polygon");
1905
1835
  node.setAttribute("points", pts.map(([x, y]) => `${fmt(x)},${fmt(y)}`).join(" "));
1906
1836
  break;
1907
1837
  }
1838
+ default:
1839
+ throw new Error("stepDataToSVGElement: unknown step type");
1908
1840
  }
1909
1841
  node.setAttribute("fill", color);
1910
1842
  node.setAttribute("fill-opacity", opacity);
@@ -1912,6 +1844,9 @@ function stepDataToSVGElement(data) {
1912
1844
  }
1913
1845
  __name(stepDataToSVGElement, "stepDataToSVGElement");
1914
1846
  function replayOutput(data) {
1847
+ if (data.v !== 1) {
1848
+ throw new Error(`Unsupported serialized output version: ${data.v}`);
1849
+ }
1915
1850
  const fill = rgbString(data.fill);
1916
1851
  const vw = data.w * data.scale;
1917
1852
  const vh = data.h * data.scale;
@@ -1944,6 +1879,7 @@ export {
1944
1879
  State,
1945
1880
  Step,
1946
1881
  Triangle,
1882
+ bboxOfPoints,
1947
1883
  clamp,
1948
1884
  clampColor,
1949
1885
  computeColorAndDifferenceChange,
@@ -1957,6 +1893,9 @@ export {
1957
1893
  makeNGon,
1958
1894
  makeRect,
1959
1895
  parseColor,
1896
+ randomPolarOffset,
1897
+ rectCorners,
1898
+ regularPolygonPoints,
1960
1899
  renderStepToCtx,
1961
1900
  replayOutput,
1962
1901
  rgbToHsl,