@slithy/prim-lib 0.5.1 → 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,8 +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 };
959
993
  static _shapeSpec = { f: "ngon", o: NGonRegular._ngonOpts };
994
+ tonalRange;
960
995
  center;
961
996
  r;
962
997
  angle;
@@ -964,6 +999,7 @@ function makeNGon(opts) {
964
999
  _cachedPoints;
965
1000
  constructor(w, h) {
966
1001
  super(w, h);
1002
+ this.tonalRange = opts.tonalRange;
967
1003
  this.center = Shape.randomPoint(w, h);
968
1004
  this.r = Math.max(1, sizeRange[0] + ~~(Math.random() * (sizeRange[1] - sizeRange[0])));
969
1005
  this.angle = rotatable ? Math.random() * (2 * Math.PI / sides) : startAngle;
@@ -1059,11 +1095,13 @@ function makeNGon(opts) {
1059
1095
  static {
1060
1096
  __name(this, "NGonIrregular");
1061
1097
  }
1062
- static _ngonOpts = { sides, regular: false, convex, sizeRange, mutationScale };
1098
+ static _ngonOpts = { sides, regular: false, convex, sizeRange, mutationScale, tonalRange: opts.tonalRange };
1063
1099
  static _shapeSpec = { f: "ngon", o: NGonIrregular._ngonOpts };
1100
+ tonalRange;
1064
1101
  points;
1065
1102
  constructor(w, h) {
1066
1103
  super(w, h);
1104
+ this.tonalRange = opts.tonalRange;
1067
1105
  const first = Shape.randomPoint(w, h);
1068
1106
  this.points = [first];
1069
1107
  for (let i = 1; i < sides; i++) {
@@ -1135,14 +1173,16 @@ function makeRect(opts) {
1135
1173
  static {
1136
1174
  __name(this, "Rect");
1137
1175
  }
1138
- static _rectOpts = { widthRange, heightRange, aspectRatio, rotatable, mutationScale };
1176
+ static _rectOpts = { widthRange, heightRange, aspectRatio, rotatable, mutationScale, tonalRange: opts?.tonalRange };
1139
1177
  static _shapeSpec = { f: "rect", o: Rect._rectOpts };
1178
+ tonalRange;
1140
1179
  center;
1141
1180
  hw;
1142
1181
  hh;
1143
1182
  angle;
1144
1183
  constructor(w, h) {
1145
1184
  super(w, h);
1185
+ this.tonalRange = opts?.tonalRange;
1146
1186
  this.center = Shape.randomPoint(w, h);
1147
1187
  this.hw = widthRange[0] + ~~(Math.random() * (widthRange[1] - widthRange[0]));
1148
1188
  if (aspectRatio !== void 0) {
@@ -1248,6 +1288,229 @@ function makeRect(opts) {
1248
1288
  return Rect;
1249
1289
  }
1250
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");
1251
1514
  var Debug = class extends Shape {
1252
1515
  static {
1253
1516
  __name(this, "Debug");
@@ -1291,12 +1554,14 @@ var Optimizer = class {
1291
1554
  _paused;
1292
1555
  _stepPlan;
1293
1556
  _schedule;
1557
+ _rejectionStreak;
1294
1558
  constructor(original, cfg, schedule = (fn) => requestAnimationFrame(fn)) {
1295
1559
  this.cfg = cfg;
1296
1560
  this.state = new State(original, Canvas.empty(cfg));
1297
1561
  this._steps = 0;
1298
1562
  this._stopped = false;
1299
1563
  this._paused = false;
1564
+ this._rejectionStreak = 0;
1300
1565
  this.onStep = () => {
1301
1566
  };
1302
1567
  this._stepPlan = buildStepPlan(cfg);
@@ -1320,7 +1585,13 @@ var Optimizer = class {
1320
1585
  }
1321
1586
  }
1322
1587
  _addShape() {
1323
- 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;
1324
1595
  this._steps++;
1325
1596
  if (step && step.distance < this.state.distance) {
1326
1597
  this.state = step.apply(this.state);
@@ -1566,6 +1837,9 @@ export {
1566
1837
  differenceToDistance,
1567
1838
  distanceToDifference,
1568
1839
  getFill,
1840
+ makeCircle,
1841
+ makeEllipse,
1842
+ makeGlyph,
1569
1843
  makeNGon,
1570
1844
  makeRect,
1571
1845
  parseColor,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slithy/prim-lib",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Core engine for primitive-based image reconstruction.",
5
5
  "type": "module",
6
6
  "exports": {