@slithy/prim-lib 0.5.1 → 0.7.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.d.ts CHANGED
@@ -23,6 +23,10 @@ interface ShapeImageData {
23
23
  */
24
24
  interface ShapeInterface {
25
25
  bbox: Bbox;
26
+ tonalRange?: [number, number];
27
+ saturationRange?: [number, number];
28
+ hueCenter?: number;
29
+ hueTolerance?: number;
26
30
  /** cfg is rarely used by implementations; accepts Partial to keep call sites flexible. */
27
31
  mutate(cfg?: Partial<Cfg>): ShapeInterface;
28
32
  toSVG(): SVGElement | undefined;
@@ -43,6 +47,7 @@ interface Cfg {
43
47
  mutations: number;
44
48
  steps: number;
45
49
  fill: 'auto' | string;
50
+ outputFill?: string;
46
51
  computeSize: number;
47
52
  viewSize: number;
48
53
  allowUpscale?: boolean;
@@ -193,6 +198,7 @@ declare class Optimizer {
193
198
  _paused: boolean;
194
199
  _stepPlan: ShapeCtor[];
195
200
  _schedule: (fn: () => void) => void;
201
+ _rejectionStreak: number;
196
202
  constructor(original: Canvas, cfg: Cfg, schedule?: (fn: () => void) => void);
197
203
  start(): void;
198
204
  stop(): void;
@@ -303,6 +309,10 @@ interface NGonOptions {
303
309
  noise?: number;
304
310
  sizeRange?: [number, number];
305
311
  mutationScale?: number;
312
+ tonalRange?: [number, number];
313
+ saturationRange?: [number, number];
314
+ hueCenter?: number;
315
+ hueTolerance?: number;
306
316
  }
307
317
  declare function makeNGon(opts: NGonOptions): new (w: number, h: number) => ShapeInterface;
308
318
  interface RectOptions {
@@ -311,14 +321,50 @@ interface RectOptions {
311
321
  aspectRatio?: number;
312
322
  rotatable?: boolean;
313
323
  mutationScale?: number;
324
+ tonalRange?: [number, number];
325
+ saturationRange?: [number, number];
326
+ hueCenter?: number;
327
+ hueTolerance?: number;
314
328
  }
315
329
  declare function makeRect(opts?: Partial<RectOptions>): new (w: number, h: number) => ShapeInterface;
330
+ interface CircleOptions {
331
+ sizeRange?: [number, number];
332
+ mutationScale?: number;
333
+ tonalRange?: [number, number];
334
+ saturationRange?: [number, number];
335
+ hueCenter?: number;
336
+ hueTolerance?: number;
337
+ }
338
+ declare function makeCircle(opts?: Partial<CircleOptions>): new (w: number, h: number) => ShapeInterface;
339
+ interface EllipseOptions {
340
+ rxRange?: [number, number];
341
+ ryRange?: [number, number];
342
+ aspectRatio?: number;
343
+ mutationScale?: number;
344
+ tonalRange?: [number, number];
345
+ saturationRange?: [number, number];
346
+ hueCenter?: number;
347
+ hueTolerance?: number;
348
+ }
349
+ declare function makeEllipse(opts?: Partial<EllipseOptions>): new (w: number, h: number) => ShapeInterface;
350
+ interface GlyphOptions {
351
+ char?: string;
352
+ fontFamily?: string;
353
+ sizeRange?: [number, number];
354
+ mutationScale?: number;
355
+ tonalRange?: [number, number];
356
+ saturationRange?: [number, number];
357
+ hueCenter?: number;
358
+ hueTolerance?: number;
359
+ }
360
+ declare function makeGlyph(opts?: Partial<GlyphOptions>): new (w: number, h: number) => ShapeInterface;
316
361
  declare class Debug extends Shape {
317
362
  constructor(w: number, h: number);
318
363
  render(ctx: CanvasRenderingContext2D): void;
319
364
  }
320
365
 
321
366
  declare const SVGNS = "http://www.w3.org/2000/svg";
367
+ declare function rgbToHsl(r: number, g: number, b: number): [number, number, number];
322
368
  declare function parseColor(color: string): [number, number, number];
323
369
 
324
370
  declare function clamp(x: number, min: number, max: number): number;
@@ -341,4 +387,4 @@ interface ReplayResult {
341
387
  }
342
388
  declare function replayOutput(data: SerializedOutput): ReplayResult;
343
389
 
344
- export { type Bbox, Canvas, type Cfg, Circle, type Ctx2D, Debug, Ellipse, Glyph, Hexagon, type ImageDataLike, type NGonOptions, Optimizer, type Point, type PreCfg, type RGB, type RectOptions, Rectangle, type ReplayResult, SVGNS, type SerializedOutput, Shape, type ShapeImageData, type ShapeInterface, Square, State, Step, type StepData, Triangle, clamp, clampColor, computeColorAndDifferenceChange, difference, differenceToDistance, distanceToDifference, getFill, makeNGon, makeRect, parseColor, renderStepToCtx, replayOutput, stepDataToSVGElement, stepPerf };
390
+ export { type Bbox, Canvas, type Cfg, Circle, type CircleOptions, type Ctx2D, Debug, Ellipse, type EllipseOptions, Glyph, type GlyphOptions, Hexagon, type ImageDataLike, type NGonOptions, Optimizer, type Point, type PreCfg, type RGB, type RectOptions, Rectangle, type ReplayResult, SVGNS, type SerializedOutput, Shape, type ShapeImageData, type ShapeInterface, Square, State, Step, type StepData, Triangle, clamp, clampColor, computeColorAndDifferenceChange, difference, differenceToDistance, distanceToDifference, getFill, makeCircle, makeEllipse, makeGlyph, makeNGon, makeRect, parseColor, renderStepToCtx, replayOutput, rgbToHsl, stepDataToSVGElement, stepPerf };
package/dist/index.js CHANGED
@@ -3,6 +3,25 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
3
3
 
4
4
  // src/util.ts
5
5
  var SVGNS = "http://www.w3.org/2000/svg";
6
+ function rgbToHsl(r, g, b) {
7
+ const rn = r / 255, gn = g / 255, bn = b / 255;
8
+ const max = Math.max(rn, gn, bn);
9
+ const min = Math.min(rn, gn, bn);
10
+ const l = (max + min) / 2;
11
+ if (max === min) return [0, 0, l * 100];
12
+ const d = max - min;
13
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
14
+ let h;
15
+ if (max === rn) {
16
+ h = (gn - bn) / d + (gn < bn ? 6 : 0);
17
+ } else if (max === gn) {
18
+ h = (bn - rn) / d + 2;
19
+ } else {
20
+ h = (rn - gn) / d + 4;
21
+ }
22
+ return [h * 60, s * 100, l * 100];
23
+ }
24
+ __name(rgbToHsl, "rgbToHsl");
6
25
  function parseColor(color) {
7
26
  const m = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
8
27
  if (!m) throw new Error(`Cannot parse color: ${color}`);
@@ -169,7 +188,7 @@ var Canvas = class _Canvas {
169
188
  }
170
189
  static empty(cfg, svg) {
171
190
  if (svg) {
172
- return this.svgRoot(cfg.width, cfg.height, cfg.fill);
191
+ return this.svgRoot(cfg.width, cfg.height, cfg.outputFill ?? cfg.fill);
173
192
  } else {
174
193
  return new this(cfg.width, cfg.height).fill(cfg.fill);
175
194
  }
@@ -195,7 +214,11 @@ var Canvas = class _Canvas {
195
214
  const fullCfg = { ...cfg, width: cfg.width, height: cfg.height };
196
215
  const canvas = this.empty(fullCfg);
197
216
  canvas.ctx.drawImage(img, 0, 0, fullCfg.width, fullCfg.height);
198
- if (cfg.fill == "auto") {
217
+ if (cfg.fill === "transparent") {
218
+ cfg.outputFill = "transparent";
219
+ cfg.fill = "auto";
220
+ }
221
+ if (cfg.fill === "auto") {
199
222
  cfg.fill = getFill(canvas.getImageData());
200
223
  }
201
224
  resolve(canvas);
@@ -217,6 +240,10 @@ var Canvas = class _Canvas {
217
240
  const fullCfg = { ...cfg, width: cfg.width, height: cfg.height };
218
241
  const canvas = this.empty(fullCfg);
219
242
  canvas.ctx.drawImage(bitmap, 0, 0, fullCfg.width, fullCfg.height);
243
+ if (cfg.fill === "transparent") {
244
+ cfg.outputFill = "transparent";
245
+ cfg.fill = "auto";
246
+ }
220
247
  if (cfg.fill === "auto") {
221
248
  cfg.fill = getFill(canvas.getImageData());
222
249
  }
@@ -355,6 +382,50 @@ var Step = class _Step {
355
382
  compute(state) {
356
383
  const pixels = state.canvas.node.width * state.canvas.node.height;
357
384
  const offset = this.shape.bbox;
385
+ const { tonalRange, saturationRange, hueCenter, hueTolerance } = this.shape;
386
+ if (tonalRange || saturationRange || hueCenter !== void 0 && hueTolerance !== void 0) {
387
+ const { left, top, width, height } = offset;
388
+ const targetData = state.target.getImageData();
389
+ const fw = targetData.width;
390
+ const fh = targetData.height;
391
+ let rSum = 0, gSum = 0, bSum = 0, count = 0;
392
+ for (let sy = 0; sy < height; sy++) {
393
+ const fy = top + sy;
394
+ if (fy < 0 || fy >= fh) continue;
395
+ for (let sx = 0; sx < width; sx++) {
396
+ const fx = left + sx;
397
+ if (fx < 0 || fx >= fw) continue;
398
+ const i = 4 * (fx + fy * fw);
399
+ rSum += targetData.data[i];
400
+ gSum += targetData.data[i + 1];
401
+ bSum += targetData.data[i + 2];
402
+ count++;
403
+ }
404
+ }
405
+ if (count > 0) {
406
+ if (tonalRange) {
407
+ const luma = (0.299 * rSum + 0.587 * gSum + 0.114 * bSum) / count;
408
+ if (luma < tonalRange[0] || luma > tonalRange[1]) {
409
+ this.distance = Infinity;
410
+ return Promise.resolve(this);
411
+ }
412
+ }
413
+ if (saturationRange || hueCenter !== void 0 && hueTolerance !== void 0) {
414
+ const [h, s] = rgbToHsl(rSum / count, gSum / count, bSum / count);
415
+ if (saturationRange && (s < saturationRange[0] || s > saturationRange[1])) {
416
+ this.distance = Infinity;
417
+ return Promise.resolve(this);
418
+ }
419
+ if (hueCenter !== void 0 && hueTolerance !== void 0) {
420
+ const diff = Math.abs((h - hueCenter + 180 + 360) % 360 - 180);
421
+ if (diff > hueTolerance) {
422
+ this.distance = Infinity;
423
+ return Promise.resolve(this);
424
+ }
425
+ }
426
+ }
427
+ }
428
+ }
358
429
  const t0 = performance.now();
359
430
  const shapeImageData = this.shape.rasterize(this.alpha).getImageData();
360
431
  const t1 = performance.now();
@@ -955,8 +1026,12 @@ function makeNGon(opts) {
955
1026
  static {
956
1027
  __name(this, "NGonRegular");
957
1028
  }
958
- static _ngonOpts = { sides, regular: true, rotatable, noise, sizeRange, mutationScale, startAngle };
1029
+ static _ngonOpts = { sides, regular: true, rotatable, noise, sizeRange, mutationScale, startAngle, tonalRange: opts.tonalRange, saturationRange: opts.saturationRange, hueCenter: opts.hueCenter, hueTolerance: opts.hueTolerance };
959
1030
  static _shapeSpec = { f: "ngon", o: NGonRegular._ngonOpts };
1031
+ tonalRange;
1032
+ saturationRange;
1033
+ hueCenter;
1034
+ hueTolerance;
960
1035
  center;
961
1036
  r;
962
1037
  angle;
@@ -964,6 +1039,10 @@ function makeNGon(opts) {
964
1039
  _cachedPoints;
965
1040
  constructor(w, h) {
966
1041
  super(w, h);
1042
+ this.tonalRange = opts.tonalRange;
1043
+ this.saturationRange = opts.saturationRange;
1044
+ this.hueCenter = opts.hueCenter;
1045
+ this.hueTolerance = opts.hueTolerance;
967
1046
  this.center = Shape.randomPoint(w, h);
968
1047
  this.r = Math.max(1, sizeRange[0] + ~~(Math.random() * (sizeRange[1] - sizeRange[0])));
969
1048
  this.angle = rotatable ? Math.random() * (2 * Math.PI / sides) : startAngle;
@@ -1059,11 +1138,19 @@ function makeNGon(opts) {
1059
1138
  static {
1060
1139
  __name(this, "NGonIrregular");
1061
1140
  }
1062
- static _ngonOpts = { sides, regular: false, convex, sizeRange, mutationScale };
1141
+ static _ngonOpts = { sides, regular: false, convex, sizeRange, mutationScale, tonalRange: opts.tonalRange, saturationRange: opts.saturationRange, hueCenter: opts.hueCenter, hueTolerance: opts.hueTolerance };
1063
1142
  static _shapeSpec = { f: "ngon", o: NGonIrregular._ngonOpts };
1143
+ tonalRange;
1144
+ saturationRange;
1145
+ hueCenter;
1146
+ hueTolerance;
1064
1147
  points;
1065
1148
  constructor(w, h) {
1066
1149
  super(w, h);
1150
+ this.tonalRange = opts.tonalRange;
1151
+ this.saturationRange = opts.saturationRange;
1152
+ this.hueCenter = opts.hueCenter;
1153
+ this.hueTolerance = opts.hueTolerance;
1067
1154
  const first = Shape.randomPoint(w, h);
1068
1155
  this.points = [first];
1069
1156
  for (let i = 1; i < sides; i++) {
@@ -1135,14 +1222,22 @@ function makeRect(opts) {
1135
1222
  static {
1136
1223
  __name(this, "Rect");
1137
1224
  }
1138
- static _rectOpts = { widthRange, heightRange, aspectRatio, rotatable, mutationScale };
1225
+ static _rectOpts = { widthRange, heightRange, aspectRatio, rotatable, mutationScale, tonalRange: opts?.tonalRange, saturationRange: opts?.saturationRange, hueCenter: opts?.hueCenter, hueTolerance: opts?.hueTolerance };
1139
1226
  static _shapeSpec = { f: "rect", o: Rect._rectOpts };
1227
+ tonalRange;
1228
+ saturationRange;
1229
+ hueCenter;
1230
+ hueTolerance;
1140
1231
  center;
1141
1232
  hw;
1142
1233
  hh;
1143
1234
  angle;
1144
1235
  constructor(w, h) {
1145
1236
  super(w, h);
1237
+ this.tonalRange = opts?.tonalRange;
1238
+ this.saturationRange = opts?.saturationRange;
1239
+ this.hueCenter = opts?.hueCenter;
1240
+ this.hueTolerance = opts?.hueTolerance;
1146
1241
  this.center = Shape.randomPoint(w, h);
1147
1242
  this.hw = widthRange[0] + ~~(Math.random() * (widthRange[1] - widthRange[0]));
1148
1243
  if (aspectRatio !== void 0) {
@@ -1248,6 +1343,247 @@ function makeRect(opts) {
1248
1343
  return Rect;
1249
1344
  }
1250
1345
  __name(makeRect, "makeRect");
1346
+ function makeCircle(opts) {
1347
+ const sizeRange = opts?.sizeRange ?? [1, 20];
1348
+ const mutationScale = opts?.mutationScale ?? 20;
1349
+ class MadeCircle extends Shape {
1350
+ static {
1351
+ __name(this, "MadeCircle");
1352
+ }
1353
+ static _circleOpts = { sizeRange, mutationScale, tonalRange: opts?.tonalRange, saturationRange: opts?.saturationRange, hueCenter: opts?.hueCenter, hueTolerance: opts?.hueTolerance };
1354
+ static _shapeSpec = { f: "circle", o: MadeCircle._circleOpts };
1355
+ tonalRange;
1356
+ saturationRange;
1357
+ hueCenter;
1358
+ hueTolerance;
1359
+ center;
1360
+ r;
1361
+ constructor(w, h) {
1362
+ super(w, h);
1363
+ this.tonalRange = opts?.tonalRange;
1364
+ this.saturationRange = opts?.saturationRange;
1365
+ this.hueCenter = opts?.hueCenter;
1366
+ this.hueTolerance = opts?.hueTolerance;
1367
+ this.center = Shape.randomPoint(w, h);
1368
+ this.r = Math.max(1, sizeRange[0] + ~~(Math.random() * (sizeRange[1] - sizeRange[0])));
1369
+ this.computeBbox();
1370
+ }
1371
+ computeBbox() {
1372
+ this.bbox = {
1373
+ left: this.center[0] - this.r,
1374
+ top: this.center[1] - this.r,
1375
+ width: 2 * this.r || 1,
1376
+ height: 2 * this.r || 1
1377
+ };
1378
+ return this;
1379
+ }
1380
+ render(ctx) {
1381
+ ctx.beginPath();
1382
+ ctx.arc(this.center[0], this.center[1], this.r, 0, 2 * Math.PI);
1383
+ ctx.fill();
1384
+ }
1385
+ toSVG() {
1386
+ const node = document.createElementNS(SVGNS, "circle");
1387
+ node.setAttribute("cx", String(this.center[0]));
1388
+ node.setAttribute("cy", String(this.center[1]));
1389
+ node.setAttribute("r", String(this.r));
1390
+ return node;
1391
+ }
1392
+ toData(a, c) {
1393
+ return { t: "c", a, c, cx: this.center[0], cy: this.center[1], r: this.r };
1394
+ }
1395
+ mutate(_cfg) {
1396
+ const clone = new MadeCircle(0, 0);
1397
+ clone.center = [this.center[0], this.center[1]];
1398
+ clone.r = this.r;
1399
+ switch (Math.floor(Math.random() * 2)) {
1400
+ case 0: {
1401
+ const angle = Math.random() * 2 * Math.PI;
1402
+ const d = Math.random() * mutationScale;
1403
+ clone.center[0] += ~~(d * Math.cos(angle));
1404
+ clone.center[1] += ~~(d * Math.sin(angle));
1405
+ break;
1406
+ }
1407
+ case 1:
1408
+ clone.r += (Math.random() - 0.5) * mutationScale;
1409
+ clone.r = Math.max(1, ~~clone.r);
1410
+ break;
1411
+ }
1412
+ return clone.computeBbox();
1413
+ }
1414
+ }
1415
+ return MadeCircle;
1416
+ }
1417
+ __name(makeCircle, "makeCircle");
1418
+ function makeEllipse(opts) {
1419
+ const rxRange = opts?.rxRange ?? [1, 20];
1420
+ const ryRange = opts?.ryRange ?? [1, 20];
1421
+ const aspectRatio = opts?.aspectRatio;
1422
+ const mutationScale = opts?.mutationScale ?? 20;
1423
+ class MadeEllipse extends Shape {
1424
+ static {
1425
+ __name(this, "MadeEllipse");
1426
+ }
1427
+ static _ellipseOpts = { rxRange, ryRange, aspectRatio, mutationScale, tonalRange: opts?.tonalRange, saturationRange: opts?.saturationRange, hueCenter: opts?.hueCenter, hueTolerance: opts?.hueTolerance };
1428
+ static _shapeSpec = { f: "ellipse", o: MadeEllipse._ellipseOpts };
1429
+ tonalRange;
1430
+ saturationRange;
1431
+ hueCenter;
1432
+ hueTolerance;
1433
+ center;
1434
+ rx;
1435
+ ry;
1436
+ constructor(w, h) {
1437
+ super(w, h);
1438
+ this.tonalRange = opts?.tonalRange;
1439
+ this.saturationRange = opts?.saturationRange;
1440
+ this.hueCenter = opts?.hueCenter;
1441
+ this.hueTolerance = opts?.hueTolerance;
1442
+ this.center = Shape.randomPoint(w, h);
1443
+ this.rx = Math.max(1, rxRange[0] + ~~(Math.random() * (rxRange[1] - rxRange[0])));
1444
+ this.ry = aspectRatio !== void 0 ? Math.max(1, Math.round(this.rx / aspectRatio)) : Math.max(1, ryRange[0] + ~~(Math.random() * (ryRange[1] - ryRange[0])));
1445
+ this.computeBbox();
1446
+ }
1447
+ computeBbox() {
1448
+ this.bbox = {
1449
+ left: this.center[0] - this.rx,
1450
+ top: this.center[1] - this.ry,
1451
+ width: 2 * this.rx || 1,
1452
+ height: 2 * this.ry || 1
1453
+ };
1454
+ return this;
1455
+ }
1456
+ render(ctx) {
1457
+ ctx.beginPath();
1458
+ ctx.ellipse(this.center[0], this.center[1], this.rx, this.ry, 0, 0, 2 * Math.PI, false);
1459
+ ctx.fill();
1460
+ }
1461
+ toSVG() {
1462
+ const node = document.createElementNS(SVGNS, "ellipse");
1463
+ node.setAttribute("cx", String(this.center[0]));
1464
+ node.setAttribute("cy", String(this.center[1]));
1465
+ node.setAttribute("rx", String(this.rx));
1466
+ node.setAttribute("ry", String(this.ry));
1467
+ return node;
1468
+ }
1469
+ toData(a, c) {
1470
+ return { t: "e", a, c, cx: this.center[0], cy: this.center[1], rx: this.rx, ry: this.ry };
1471
+ }
1472
+ mutate(_cfg) {
1473
+ const clone = new MadeEllipse(0, 0);
1474
+ clone.center = [this.center[0], this.center[1]];
1475
+ clone.rx = this.rx;
1476
+ clone.ry = this.ry;
1477
+ const mutCount = aspectRatio === void 0 ? 3 : 2;
1478
+ switch (Math.floor(Math.random() * mutCount)) {
1479
+ case 0: {
1480
+ const angle = Math.random() * 2 * Math.PI;
1481
+ const d = Math.random() * mutationScale;
1482
+ clone.center[0] += ~~(d * Math.cos(angle));
1483
+ clone.center[1] += ~~(d * Math.sin(angle));
1484
+ break;
1485
+ }
1486
+ case 1:
1487
+ clone.rx += (Math.random() - 0.5) * mutationScale;
1488
+ clone.rx = Math.max(1, ~~clone.rx);
1489
+ if (aspectRatio !== void 0) {
1490
+ clone.ry = Math.max(1, Math.round(clone.rx / aspectRatio));
1491
+ }
1492
+ break;
1493
+ case 2:
1494
+ clone.ry += (Math.random() - 0.5) * mutationScale;
1495
+ clone.ry = Math.max(1, ~~clone.ry);
1496
+ break;
1497
+ }
1498
+ return clone.computeBbox();
1499
+ }
1500
+ }
1501
+ return MadeEllipse;
1502
+ }
1503
+ __name(makeEllipse, "makeEllipse");
1504
+ function makeGlyph(opts) {
1505
+ const char = opts?.char ?? "\u263A";
1506
+ const fontFamily = opts?.fontFamily ?? "sans-serif";
1507
+ const sizeRange = opts?.sizeRange ?? [10, 30];
1508
+ const mutationScale = opts?.mutationScale ?? 20;
1509
+ class MadeGlyph extends Shape {
1510
+ static {
1511
+ __name(this, "MadeGlyph");
1512
+ }
1513
+ static _glyphOpts = { char, fontFamily, sizeRange, mutationScale, tonalRange: opts?.tonalRange, saturationRange: opts?.saturationRange, hueCenter: opts?.hueCenter, hueTolerance: opts?.hueTolerance };
1514
+ static _shapeSpec = { f: "glyph", o: MadeGlyph._glyphOpts };
1515
+ tonalRange;
1516
+ saturationRange;
1517
+ hueCenter;
1518
+ hueTolerance;
1519
+ center;
1520
+ fontSize;
1521
+ constructor(w, h) {
1522
+ super(w, h);
1523
+ this.tonalRange = opts?.tonalRange;
1524
+ this.saturationRange = opts?.saturationRange;
1525
+ this.hueCenter = opts?.hueCenter;
1526
+ this.hueTolerance = opts?.hueTolerance;
1527
+ this.center = Shape.randomPoint(w, h);
1528
+ this.fontSize = Math.max(sizeRange[0], sizeRange[0] + ~~(Math.random() * (sizeRange[1] - sizeRange[0])));
1529
+ this.computeBbox();
1530
+ }
1531
+ computeBbox() {
1532
+ const tmp = new Canvas(1, 1);
1533
+ tmp.ctx.font = `${this.fontSize}px ${fontFamily}`;
1534
+ const w = ~~tmp.ctx.measureText(char).width;
1535
+ this.bbox = {
1536
+ left: ~~(this.center[0] - w / 2),
1537
+ top: ~~(this.center[1] - this.fontSize / 2),
1538
+ width: w || 1,
1539
+ height: this.fontSize
1540
+ };
1541
+ return this;
1542
+ }
1543
+ render(ctx) {
1544
+ ctx.textAlign = "center";
1545
+ ctx.textBaseline = "middle";
1546
+ ctx.font = `${this.fontSize}px ${fontFamily}`;
1547
+ ctx.fillText(char, this.center[0], this.center[1]);
1548
+ }
1549
+ toSVG() {
1550
+ const text = document.createElementNS(SVGNS, "text");
1551
+ text.appendChild(document.createTextNode(char));
1552
+ text.setAttribute("text-anchor", "middle");
1553
+ text.setAttribute("dominant-baseline", "central");
1554
+ text.setAttribute("font-size", String(this.fontSize));
1555
+ text.setAttribute("font-family", fontFamily);
1556
+ text.setAttribute("x", String(this.center[0]));
1557
+ text.setAttribute("y", String(this.center[1]));
1558
+ return text;
1559
+ }
1560
+ toData(a, c) {
1561
+ return { t: "sm", a, c, cx: this.center[0], cy: this.center[1], fs: this.fontSize, text: char };
1562
+ }
1563
+ mutate(_cfg) {
1564
+ const clone = new MadeGlyph(0, 0);
1565
+ clone.center = [this.center[0], this.center[1]];
1566
+ clone.fontSize = this.fontSize;
1567
+ switch (Math.floor(Math.random() * 2)) {
1568
+ case 0: {
1569
+ const angle = Math.random() * 2 * Math.PI;
1570
+ const d = Math.random() * mutationScale;
1571
+ clone.center[0] += ~~(d * Math.cos(angle));
1572
+ clone.center[1] += ~~(d * Math.sin(angle));
1573
+ break;
1574
+ }
1575
+ case 1: {
1576
+ const delta = Math.round((Math.random() - 0.5) * (mutationScale * 0.2));
1577
+ clone.fontSize = Math.max(sizeRange[0], Math.min(sizeRange[1], clone.fontSize + delta));
1578
+ break;
1579
+ }
1580
+ }
1581
+ return clone.computeBbox();
1582
+ }
1583
+ }
1584
+ return MadeGlyph;
1585
+ }
1586
+ __name(makeGlyph, "makeGlyph");
1251
1587
  var Debug = class extends Shape {
1252
1588
  static {
1253
1589
  __name(this, "Debug");
@@ -1291,12 +1627,14 @@ var Optimizer = class {
1291
1627
  _paused;
1292
1628
  _stepPlan;
1293
1629
  _schedule;
1630
+ _rejectionStreak;
1294
1631
  constructor(original, cfg, schedule = (fn) => requestAnimationFrame(fn)) {
1295
1632
  this.cfg = cfg;
1296
1633
  this.state = new State(original, Canvas.empty(cfg));
1297
1634
  this._steps = 0;
1298
1635
  this._stopped = false;
1299
1636
  this._paused = false;
1637
+ this._rejectionStreak = 0;
1300
1638
  this.onStep = () => {
1301
1639
  };
1302
1640
  this._stepPlan = buildStepPlan(cfg);
@@ -1320,7 +1658,13 @@ var Optimizer = class {
1320
1658
  }
1321
1659
  }
1322
1660
  _addShape() {
1323
- this._findBestStep().then((step) => step ? this._optimizeStep(step) : null).then((step) => {
1661
+ this._findBestStep().then((step) => step && step.distance < Infinity ? this._optimizeStep(step) : step).then((step) => {
1662
+ if (step && step.distance === Infinity && this._rejectionStreak < this.cfg.shapes) {
1663
+ this._rejectionStreak++;
1664
+ this._continue();
1665
+ return;
1666
+ }
1667
+ this._rejectionStreak = 0;
1324
1668
  this._steps++;
1325
1669
  if (step && step.distance < this.state.distance) {
1326
1670
  this.state = step.apply(this.state);
@@ -1566,11 +1910,15 @@ export {
1566
1910
  differenceToDistance,
1567
1911
  distanceToDifference,
1568
1912
  getFill,
1913
+ makeCircle,
1914
+ makeEllipse,
1915
+ makeGlyph,
1569
1916
  makeNGon,
1570
1917
  makeRect,
1571
1918
  parseColor,
1572
1919
  renderStepToCtx,
1573
1920
  replayOutput,
1921
+ rgbToHsl,
1574
1922
  stepDataToSVGElement,
1575
1923
  stepPerf
1576
1924
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slithy/prim-lib",
3
- "version": "0.5.1",
3
+ "version": "0.7.0",
4
4
  "description": "Core engine for primitive-based image reconstruction.",
5
5
  "type": "module",
6
6
  "exports": {