@slithy/prim-lib 0.5.0 → 0.6.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,7 @@ interface ShapeImageData {
23
23
  */
24
24
  interface ShapeInterface {
25
25
  bbox: Bbox;
26
+ tonalRange?: [number, number];
26
27
  /** cfg is rarely used by implementations; accepts Partial to keep call sites flexible. */
27
28
  mutate(cfg?: Partial<Cfg>): ShapeInterface;
28
29
  toSVG(): SVGElement | undefined;
@@ -43,6 +44,7 @@ interface Cfg {
43
44
  mutations: number;
44
45
  steps: number;
45
46
  fill: 'auto' | string;
47
+ outputFill?: string;
46
48
  computeSize: number;
47
49
  viewSize: number;
48
50
  allowUpscale?: boolean;
@@ -193,6 +195,7 @@ declare class Optimizer {
193
195
  _paused: boolean;
194
196
  _stepPlan: ShapeCtor[];
195
197
  _schedule: (fn: () => void) => void;
198
+ _rejectionStreak: number;
196
199
  constructor(original: Canvas, cfg: Cfg, schedule?: (fn: () => void) => void);
197
200
  start(): void;
198
201
  stop(): void;
@@ -303,6 +306,7 @@ interface NGonOptions {
303
306
  noise?: number;
304
307
  sizeRange?: [number, number];
305
308
  mutationScale?: number;
309
+ tonalRange?: [number, number];
306
310
  }
307
311
  declare function makeNGon(opts: NGonOptions): new (w: number, h: number) => ShapeInterface;
308
312
  interface RectOptions {
@@ -311,8 +315,31 @@ interface RectOptions {
311
315
  aspectRatio?: number;
312
316
  rotatable?: boolean;
313
317
  mutationScale?: number;
318
+ tonalRange?: [number, number];
314
319
  }
315
320
  declare function makeRect(opts?: Partial<RectOptions>): new (w: number, h: number) => ShapeInterface;
321
+ interface CircleOptions {
322
+ sizeRange?: [number, number];
323
+ mutationScale?: number;
324
+ tonalRange?: [number, number];
325
+ }
326
+ declare function makeCircle(opts?: Partial<CircleOptions>): new (w: number, h: number) => ShapeInterface;
327
+ interface EllipseOptions {
328
+ rxRange?: [number, number];
329
+ ryRange?: [number, number];
330
+ aspectRatio?: number;
331
+ mutationScale?: number;
332
+ tonalRange?: [number, number];
333
+ }
334
+ declare function makeEllipse(opts?: Partial<EllipseOptions>): new (w: number, h: number) => ShapeInterface;
335
+ interface GlyphOptions {
336
+ char?: string;
337
+ fontFamily?: string;
338
+ sizeRange?: [number, number];
339
+ mutationScale?: number;
340
+ tonalRange?: [number, number];
341
+ }
342
+ declare function makeGlyph(opts?: Partial<GlyphOptions>): new (w: number, h: number) => ShapeInterface;
316
343
  declare class Debug extends Shape {
317
344
  constructor(w: number, h: number);
318
345
  render(ctx: CanvasRenderingContext2D): void;
@@ -341,4 +368,4 @@ interface ReplayResult {
341
368
  }
342
369
  declare function replayOutput(data: SerializedOutput): ReplayResult;
343
370
 
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 };
371
+ 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, stepDataToSVGElement, stepPerf };
package/dist/index.js CHANGED
@@ -169,7 +169,7 @@ var Canvas = class _Canvas {
169
169
  }
170
170
  static empty(cfg, svg) {
171
171
  if (svg) {
172
- return this.svgRoot(cfg.width, cfg.height, cfg.fill);
172
+ return this.svgRoot(cfg.width, cfg.height, cfg.outputFill ?? cfg.fill);
173
173
  } else {
174
174
  return new this(cfg.width, cfg.height).fill(cfg.fill);
175
175
  }
@@ -195,7 +195,11 @@ var Canvas = class _Canvas {
195
195
  const fullCfg = { ...cfg, width: cfg.width, height: cfg.height };
196
196
  const canvas = this.empty(fullCfg);
197
197
  canvas.ctx.drawImage(img, 0, 0, fullCfg.width, fullCfg.height);
198
- if (cfg.fill == "auto") {
198
+ if (cfg.fill === "transparent") {
199
+ cfg.outputFill = "transparent";
200
+ cfg.fill = "auto";
201
+ }
202
+ if (cfg.fill === "auto") {
199
203
  cfg.fill = getFill(canvas.getImageData());
200
204
  }
201
205
  resolve(canvas);
@@ -217,6 +221,10 @@ var Canvas = class _Canvas {
217
221
  const fullCfg = { ...cfg, width: cfg.width, height: cfg.height };
218
222
  const canvas = this.empty(fullCfg);
219
223
  canvas.ctx.drawImage(bitmap, 0, 0, fullCfg.width, fullCfg.height);
224
+ if (cfg.fill === "transparent") {
225
+ cfg.outputFill = "transparent";
226
+ cfg.fill = "auto";
227
+ }
220
228
  if (cfg.fill === "auto") {
221
229
  cfg.fill = getFill(canvas.getImageData());
222
230
  }
@@ -355,6 +363,32 @@ var Step = class _Step {
355
363
  compute(state) {
356
364
  const pixels = state.canvas.node.width * state.canvas.node.height;
357
365
  const offset = this.shape.bbox;
366
+ const tonalRange = this.shape.tonalRange;
367
+ if (tonalRange) {
368
+ const { left, top, width, height } = offset;
369
+ const targetData = state.target.getImageData();
370
+ const fw = targetData.width;
371
+ const fh = targetData.height;
372
+ let sum = 0, count = 0;
373
+ for (let sy = 0; sy < height; sy++) {
374
+ const fy = top + sy;
375
+ if (fy < 0 || fy >= fh) continue;
376
+ for (let sx = 0; sx < width; sx++) {
377
+ const fx = left + sx;
378
+ if (fx < 0 || fx >= fw) continue;
379
+ const i = 4 * (fx + fy * fw);
380
+ sum += 0.299 * targetData.data[i] + 0.587 * targetData.data[i + 1] + 0.114 * targetData.data[i + 2];
381
+ count++;
382
+ }
383
+ }
384
+ if (count > 0) {
385
+ const luma = sum / count;
386
+ if (luma < tonalRange[0] || luma > tonalRange[1]) {
387
+ this.distance = Infinity;
388
+ return Promise.resolve(this);
389
+ }
390
+ }
391
+ }
358
392
  const t0 = performance.now();
359
393
  const shapeImageData = this.shape.rasterize(this.alpha).getImageData();
360
394
  const t1 = performance.now();
@@ -955,7 +989,9 @@ function makeNGon(opts) {
955
989
  static {
956
990
  __name(this, "NGonRegular");
957
991
  }
958
- static _ngonOpts = { sides, regular: true, rotatable, noise, sizeRange, mutationScale, startAngle };
992
+ static _ngonOpts = { sides, regular: true, rotatable, noise, sizeRange, mutationScale, startAngle, tonalRange: opts.tonalRange };
993
+ static _shapeSpec = { f: "ngon", o: NGonRegular._ngonOpts };
994
+ tonalRange;
959
995
  center;
960
996
  r;
961
997
  angle;
@@ -963,6 +999,7 @@ function makeNGon(opts) {
963
999
  _cachedPoints;
964
1000
  constructor(w, h) {
965
1001
  super(w, h);
1002
+ this.tonalRange = opts.tonalRange;
966
1003
  this.center = Shape.randomPoint(w, h);
967
1004
  this.r = Math.max(1, sizeRange[0] + ~~(Math.random() * (sizeRange[1] - sizeRange[0])));
968
1005
  this.angle = rotatable ? Math.random() * (2 * Math.PI / sides) : startAngle;
@@ -1058,10 +1095,13 @@ function makeNGon(opts) {
1058
1095
  static {
1059
1096
  __name(this, "NGonIrregular");
1060
1097
  }
1061
- static _ngonOpts = { sides, regular: false, convex, sizeRange, mutationScale };
1098
+ static _ngonOpts = { sides, regular: false, convex, sizeRange, mutationScale, tonalRange: opts.tonalRange };
1099
+ static _shapeSpec = { f: "ngon", o: NGonIrregular._ngonOpts };
1100
+ tonalRange;
1062
1101
  points;
1063
1102
  constructor(w, h) {
1064
1103
  super(w, h);
1104
+ this.tonalRange = opts.tonalRange;
1065
1105
  const first = Shape.randomPoint(w, h);
1066
1106
  this.points = [first];
1067
1107
  for (let i = 1; i < sides; i++) {
@@ -1133,14 +1173,16 @@ function makeRect(opts) {
1133
1173
  static {
1134
1174
  __name(this, "Rect");
1135
1175
  }
1136
- static _rectOpts = { widthRange, heightRange, aspectRatio, rotatable, mutationScale };
1137
- static _shapeSpec = { f: "rect" };
1176
+ static _rectOpts = { widthRange, heightRange, aspectRatio, rotatable, mutationScale, tonalRange: opts?.tonalRange };
1177
+ static _shapeSpec = { f: "rect", o: Rect._rectOpts };
1178
+ tonalRange;
1138
1179
  center;
1139
1180
  hw;
1140
1181
  hh;
1141
1182
  angle;
1142
1183
  constructor(w, h) {
1143
1184
  super(w, h);
1185
+ this.tonalRange = opts?.tonalRange;
1144
1186
  this.center = Shape.randomPoint(w, h);
1145
1187
  this.hw = widthRange[0] + ~~(Math.random() * (widthRange[1] - widthRange[0]));
1146
1188
  if (aspectRatio !== void 0) {
@@ -1246,6 +1288,229 @@ function makeRect(opts) {
1246
1288
  return Rect;
1247
1289
  }
1248
1290
  __name(makeRect, "makeRect");
1291
+ function makeCircle(opts) {
1292
+ const sizeRange = opts?.sizeRange ?? [1, 20];
1293
+ const mutationScale = opts?.mutationScale ?? 20;
1294
+ class MadeCircle extends Shape {
1295
+ static {
1296
+ __name(this, "MadeCircle");
1297
+ }
1298
+ static _circleOpts = { sizeRange, mutationScale, tonalRange: opts?.tonalRange };
1299
+ static _shapeSpec = { f: "circle", o: MadeCircle._circleOpts };
1300
+ tonalRange;
1301
+ center;
1302
+ r;
1303
+ constructor(w, h) {
1304
+ super(w, h);
1305
+ this.tonalRange = opts?.tonalRange;
1306
+ this.center = Shape.randomPoint(w, h);
1307
+ this.r = Math.max(1, sizeRange[0] + ~~(Math.random() * (sizeRange[1] - sizeRange[0])));
1308
+ this.computeBbox();
1309
+ }
1310
+ computeBbox() {
1311
+ this.bbox = {
1312
+ left: this.center[0] - this.r,
1313
+ top: this.center[1] - this.r,
1314
+ width: 2 * this.r || 1,
1315
+ height: 2 * this.r || 1
1316
+ };
1317
+ return this;
1318
+ }
1319
+ render(ctx) {
1320
+ ctx.beginPath();
1321
+ ctx.arc(this.center[0], this.center[1], this.r, 0, 2 * Math.PI);
1322
+ ctx.fill();
1323
+ }
1324
+ toSVG() {
1325
+ const node = document.createElementNS(SVGNS, "circle");
1326
+ node.setAttribute("cx", String(this.center[0]));
1327
+ node.setAttribute("cy", String(this.center[1]));
1328
+ node.setAttribute("r", String(this.r));
1329
+ return node;
1330
+ }
1331
+ toData(a, c) {
1332
+ return { t: "c", a, c, cx: this.center[0], cy: this.center[1], r: this.r };
1333
+ }
1334
+ mutate(_cfg) {
1335
+ const clone = new MadeCircle(0, 0);
1336
+ clone.center = [this.center[0], this.center[1]];
1337
+ clone.r = this.r;
1338
+ switch (Math.floor(Math.random() * 2)) {
1339
+ case 0: {
1340
+ const angle = Math.random() * 2 * Math.PI;
1341
+ const d = Math.random() * mutationScale;
1342
+ clone.center[0] += ~~(d * Math.cos(angle));
1343
+ clone.center[1] += ~~(d * Math.sin(angle));
1344
+ break;
1345
+ }
1346
+ case 1:
1347
+ clone.r += (Math.random() - 0.5) * mutationScale;
1348
+ clone.r = Math.max(1, ~~clone.r);
1349
+ break;
1350
+ }
1351
+ return clone.computeBbox();
1352
+ }
1353
+ }
1354
+ return MadeCircle;
1355
+ }
1356
+ __name(makeCircle, "makeCircle");
1357
+ function makeEllipse(opts) {
1358
+ const rxRange = opts?.rxRange ?? [1, 20];
1359
+ const ryRange = opts?.ryRange ?? [1, 20];
1360
+ const aspectRatio = opts?.aspectRatio;
1361
+ const mutationScale = opts?.mutationScale ?? 20;
1362
+ class MadeEllipse extends Shape {
1363
+ static {
1364
+ __name(this, "MadeEllipse");
1365
+ }
1366
+ static _ellipseOpts = { rxRange, ryRange, aspectRatio, mutationScale, tonalRange: opts?.tonalRange };
1367
+ static _shapeSpec = { f: "ellipse", o: MadeEllipse._ellipseOpts };
1368
+ tonalRange;
1369
+ center;
1370
+ rx;
1371
+ ry;
1372
+ constructor(w, h) {
1373
+ super(w, h);
1374
+ this.tonalRange = opts?.tonalRange;
1375
+ this.center = Shape.randomPoint(w, h);
1376
+ this.rx = Math.max(1, rxRange[0] + ~~(Math.random() * (rxRange[1] - rxRange[0])));
1377
+ this.ry = aspectRatio !== void 0 ? Math.max(1, Math.round(this.rx / aspectRatio)) : Math.max(1, ryRange[0] + ~~(Math.random() * (ryRange[1] - ryRange[0])));
1378
+ this.computeBbox();
1379
+ }
1380
+ computeBbox() {
1381
+ this.bbox = {
1382
+ left: this.center[0] - this.rx,
1383
+ top: this.center[1] - this.ry,
1384
+ width: 2 * this.rx || 1,
1385
+ height: 2 * this.ry || 1
1386
+ };
1387
+ return this;
1388
+ }
1389
+ render(ctx) {
1390
+ ctx.beginPath();
1391
+ ctx.ellipse(this.center[0], this.center[1], this.rx, this.ry, 0, 0, 2 * Math.PI, false);
1392
+ ctx.fill();
1393
+ }
1394
+ toSVG() {
1395
+ const node = document.createElementNS(SVGNS, "ellipse");
1396
+ node.setAttribute("cx", String(this.center[0]));
1397
+ node.setAttribute("cy", String(this.center[1]));
1398
+ node.setAttribute("rx", String(this.rx));
1399
+ node.setAttribute("ry", String(this.ry));
1400
+ return node;
1401
+ }
1402
+ toData(a, c) {
1403
+ return { t: "e", a, c, cx: this.center[0], cy: this.center[1], rx: this.rx, ry: this.ry };
1404
+ }
1405
+ mutate(_cfg) {
1406
+ const clone = new MadeEllipse(0, 0);
1407
+ clone.center = [this.center[0], this.center[1]];
1408
+ clone.rx = this.rx;
1409
+ clone.ry = this.ry;
1410
+ const mutCount = aspectRatio === void 0 ? 3 : 2;
1411
+ switch (Math.floor(Math.random() * mutCount)) {
1412
+ case 0: {
1413
+ const angle = Math.random() * 2 * Math.PI;
1414
+ const d = Math.random() * mutationScale;
1415
+ clone.center[0] += ~~(d * Math.cos(angle));
1416
+ clone.center[1] += ~~(d * Math.sin(angle));
1417
+ break;
1418
+ }
1419
+ case 1:
1420
+ clone.rx += (Math.random() - 0.5) * mutationScale;
1421
+ clone.rx = Math.max(1, ~~clone.rx);
1422
+ if (aspectRatio !== void 0) {
1423
+ clone.ry = Math.max(1, Math.round(clone.rx / aspectRatio));
1424
+ }
1425
+ break;
1426
+ case 2:
1427
+ clone.ry += (Math.random() - 0.5) * mutationScale;
1428
+ clone.ry = Math.max(1, ~~clone.ry);
1429
+ break;
1430
+ }
1431
+ return clone.computeBbox();
1432
+ }
1433
+ }
1434
+ return MadeEllipse;
1435
+ }
1436
+ __name(makeEllipse, "makeEllipse");
1437
+ function makeGlyph(opts) {
1438
+ const char = opts?.char ?? "\u263A";
1439
+ const fontFamily = opts?.fontFamily ?? "sans-serif";
1440
+ const sizeRange = opts?.sizeRange ?? [10, 30];
1441
+ const mutationScale = opts?.mutationScale ?? 20;
1442
+ class MadeGlyph extends Shape {
1443
+ static {
1444
+ __name(this, "MadeGlyph");
1445
+ }
1446
+ static _glyphOpts = { char, fontFamily, sizeRange, mutationScale, tonalRange: opts?.tonalRange };
1447
+ static _shapeSpec = { f: "glyph", o: MadeGlyph._glyphOpts };
1448
+ tonalRange;
1449
+ center;
1450
+ fontSize;
1451
+ constructor(w, h) {
1452
+ super(w, h);
1453
+ this.tonalRange = opts?.tonalRange;
1454
+ this.center = Shape.randomPoint(w, h);
1455
+ this.fontSize = Math.max(sizeRange[0], sizeRange[0] + ~~(Math.random() * (sizeRange[1] - sizeRange[0])));
1456
+ this.computeBbox();
1457
+ }
1458
+ computeBbox() {
1459
+ const tmp = new Canvas(1, 1);
1460
+ tmp.ctx.font = `${this.fontSize}px ${fontFamily}`;
1461
+ const w = ~~tmp.ctx.measureText(char).width;
1462
+ this.bbox = {
1463
+ left: ~~(this.center[0] - w / 2),
1464
+ top: ~~(this.center[1] - this.fontSize / 2),
1465
+ width: w || 1,
1466
+ height: this.fontSize
1467
+ };
1468
+ return this;
1469
+ }
1470
+ render(ctx) {
1471
+ ctx.textAlign = "center";
1472
+ ctx.textBaseline = "middle";
1473
+ ctx.font = `${this.fontSize}px ${fontFamily}`;
1474
+ ctx.fillText(char, this.center[0], this.center[1]);
1475
+ }
1476
+ toSVG() {
1477
+ const text = document.createElementNS(SVGNS, "text");
1478
+ text.appendChild(document.createTextNode(char));
1479
+ text.setAttribute("text-anchor", "middle");
1480
+ text.setAttribute("dominant-baseline", "central");
1481
+ text.setAttribute("font-size", String(this.fontSize));
1482
+ text.setAttribute("font-family", fontFamily);
1483
+ text.setAttribute("x", String(this.center[0]));
1484
+ text.setAttribute("y", String(this.center[1]));
1485
+ return text;
1486
+ }
1487
+ toData(a, c) {
1488
+ return { t: "sm", a, c, cx: this.center[0], cy: this.center[1], fs: this.fontSize, text: char };
1489
+ }
1490
+ mutate(_cfg) {
1491
+ const clone = new MadeGlyph(0, 0);
1492
+ clone.center = [this.center[0], this.center[1]];
1493
+ clone.fontSize = this.fontSize;
1494
+ switch (Math.floor(Math.random() * 2)) {
1495
+ case 0: {
1496
+ const angle = Math.random() * 2 * Math.PI;
1497
+ const d = Math.random() * mutationScale;
1498
+ clone.center[0] += ~~(d * Math.cos(angle));
1499
+ clone.center[1] += ~~(d * Math.sin(angle));
1500
+ break;
1501
+ }
1502
+ case 1: {
1503
+ const delta = Math.round((Math.random() - 0.5) * (mutationScale * 0.2));
1504
+ clone.fontSize = Math.max(sizeRange[0], Math.min(sizeRange[1], clone.fontSize + delta));
1505
+ break;
1506
+ }
1507
+ }
1508
+ return clone.computeBbox();
1509
+ }
1510
+ }
1511
+ return MadeGlyph;
1512
+ }
1513
+ __name(makeGlyph, "makeGlyph");
1249
1514
  var Debug = class extends Shape {
1250
1515
  static {
1251
1516
  __name(this, "Debug");
@@ -1289,12 +1554,14 @@ var Optimizer = class {
1289
1554
  _paused;
1290
1555
  _stepPlan;
1291
1556
  _schedule;
1557
+ _rejectionStreak;
1292
1558
  constructor(original, cfg, schedule = (fn) => requestAnimationFrame(fn)) {
1293
1559
  this.cfg = cfg;
1294
1560
  this.state = new State(original, Canvas.empty(cfg));
1295
1561
  this._steps = 0;
1296
1562
  this._stopped = false;
1297
1563
  this._paused = false;
1564
+ this._rejectionStreak = 0;
1298
1565
  this.onStep = () => {
1299
1566
  };
1300
1567
  this._stepPlan = buildStepPlan(cfg);
@@ -1318,7 +1585,13 @@ var Optimizer = class {
1318
1585
  }
1319
1586
  }
1320
1587
  _addShape() {
1321
- this._findBestStep().then((step) => step ? this._optimizeStep(step) : null).then((step) => {
1588
+ this._findBestStep().then((step) => step && step.distance < Infinity ? this._optimizeStep(step) : step).then((step) => {
1589
+ if (step && step.distance === Infinity && this._rejectionStreak < this.cfg.shapes) {
1590
+ this._rejectionStreak++;
1591
+ this._continue();
1592
+ return;
1593
+ }
1594
+ this._rejectionStreak = 0;
1322
1595
  this._steps++;
1323
1596
  if (step && step.distance < this.state.distance) {
1324
1597
  this.state = step.apply(this.state);
@@ -1564,6 +1837,9 @@ export {
1564
1837
  differenceToDistance,
1565
1838
  distanceToDifference,
1566
1839
  getFill,
1840
+ makeCircle,
1841
+ makeEllipse,
1842
+ makeGlyph,
1567
1843
  makeNGon,
1568
1844
  makeRect,
1569
1845
  parseColor,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slithy/prim-lib",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Core engine for primitive-based image reconstruction.",
5
5
  "type": "module",
6
6
  "exports": {