@slithy/prim-lib 0.6.0 → 0.8.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.
Files changed (3) hide show
  1. package/dist/index.d.ts +38 -1
  2. package/dist/index.js +129 -14
  3. package/package.json +15 -14
package/dist/index.d.ts CHANGED
@@ -24,6 +24,12 @@ interface ShapeImageData {
24
24
  interface ShapeInterface {
25
25
  bbox: Bbox;
26
26
  tonalRange?: [number, number];
27
+ invertTonal?: boolean;
28
+ saturationRange?: [number, number];
29
+ invertSaturation?: boolean;
30
+ hueCenter?: number;
31
+ hueTolerance?: number;
32
+ invertHue?: boolean;
27
33
  /** cfg is rarely used by implementations; accepts Partial to keep call sites flexible. */
28
34
  mutate(cfg?: Partial<Cfg>): ShapeInterface;
29
35
  toSVG(): SVGElement | undefined;
@@ -307,6 +313,12 @@ interface NGonOptions {
307
313
  sizeRange?: [number, number];
308
314
  mutationScale?: number;
309
315
  tonalRange?: [number, number];
316
+ invertTonal?: boolean;
317
+ saturationRange?: [number, number];
318
+ invertSaturation?: boolean;
319
+ hueCenter?: number;
320
+ hueTolerance?: number;
321
+ invertHue?: boolean;
310
322
  }
311
323
  declare function makeNGon(opts: NGonOptions): new (w: number, h: number) => ShapeInterface;
312
324
  interface RectOptions {
@@ -316,12 +328,24 @@ interface RectOptions {
316
328
  rotatable?: boolean;
317
329
  mutationScale?: number;
318
330
  tonalRange?: [number, number];
331
+ invertTonal?: boolean;
332
+ saturationRange?: [number, number];
333
+ invertSaturation?: boolean;
334
+ hueCenter?: number;
335
+ hueTolerance?: number;
336
+ invertHue?: boolean;
319
337
  }
320
338
  declare function makeRect(opts?: Partial<RectOptions>): new (w: number, h: number) => ShapeInterface;
321
339
  interface CircleOptions {
322
340
  sizeRange?: [number, number];
323
341
  mutationScale?: number;
324
342
  tonalRange?: [number, number];
343
+ invertTonal?: boolean;
344
+ saturationRange?: [number, number];
345
+ invertSaturation?: boolean;
346
+ hueCenter?: number;
347
+ hueTolerance?: number;
348
+ invertHue?: boolean;
325
349
  }
326
350
  declare function makeCircle(opts?: Partial<CircleOptions>): new (w: number, h: number) => ShapeInterface;
327
351
  interface EllipseOptions {
@@ -330,6 +354,12 @@ interface EllipseOptions {
330
354
  aspectRatio?: number;
331
355
  mutationScale?: number;
332
356
  tonalRange?: [number, number];
357
+ invertTonal?: boolean;
358
+ saturationRange?: [number, number];
359
+ invertSaturation?: boolean;
360
+ hueCenter?: number;
361
+ hueTolerance?: number;
362
+ invertHue?: boolean;
333
363
  }
334
364
  declare function makeEllipse(opts?: Partial<EllipseOptions>): new (w: number, h: number) => ShapeInterface;
335
365
  interface GlyphOptions {
@@ -338,6 +368,12 @@ interface GlyphOptions {
338
368
  sizeRange?: [number, number];
339
369
  mutationScale?: number;
340
370
  tonalRange?: [number, number];
371
+ invertTonal?: boolean;
372
+ saturationRange?: [number, number];
373
+ invertSaturation?: boolean;
374
+ hueCenter?: number;
375
+ hueTolerance?: number;
376
+ invertHue?: boolean;
341
377
  }
342
378
  declare function makeGlyph(opts?: Partial<GlyphOptions>): new (w: number, h: number) => ShapeInterface;
343
379
  declare class Debug extends Shape {
@@ -346,6 +382,7 @@ declare class Debug extends Shape {
346
382
  }
347
383
 
348
384
  declare const SVGNS = "http://www.w3.org/2000/svg";
385
+ declare function rgbToHsl(r: number, g: number, b: number): [number, number, number];
349
386
  declare function parseColor(color: string): [number, number, number];
350
387
 
351
388
  declare function clamp(x: number, min: number, max: number): number;
@@ -368,4 +405,4 @@ interface ReplayResult {
368
405
  }
369
406
  declare function replayOutput(data: SerializedOutput): ReplayResult;
370
407
 
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 };
408
+ 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}`);
@@ -363,13 +382,13 @@ var Step = class _Step {
363
382
  compute(state) {
364
383
  const pixels = state.canvas.node.width * state.canvas.node.height;
365
384
  const offset = this.shape.bbox;
366
- const tonalRange = this.shape.tonalRange;
367
- if (tonalRange) {
385
+ const { tonalRange, invertTonal, saturationRange, invertSaturation, hueCenter, hueTolerance, invertHue } = this.shape;
386
+ if (tonalRange || saturationRange || hueCenter !== void 0 && hueTolerance !== void 0) {
368
387
  const { left, top, width, height } = offset;
369
388
  const targetData = state.target.getImageData();
370
389
  const fw = targetData.width;
371
390
  const fh = targetData.height;
372
- let sum = 0, count = 0;
391
+ let rSum = 0, gSum = 0, bSum = 0, count = 0;
373
392
  for (let sy = 0; sy < height; sy++) {
374
393
  const fy = top + sy;
375
394
  if (fy < 0 || fy >= fh) continue;
@@ -377,15 +396,38 @@ var Step = class _Step {
377
396
  const fx = left + sx;
378
397
  if (fx < 0 || fx >= fw) continue;
379
398
  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];
399
+ rSum += targetData.data[i];
400
+ gSum += targetData.data[i + 1];
401
+ bSum += targetData.data[i + 2];
381
402
  count++;
382
403
  }
383
404
  }
384
405
  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);
406
+ if (tonalRange) {
407
+ const luma = (0.299 * rSum + 0.587 * gSum + 0.114 * bSum) / count;
408
+ const inRange = luma >= tonalRange[0] && luma <= tonalRange[1];
409
+ if (invertTonal ? inRange : !inRange) {
410
+ this.distance = Infinity;
411
+ return Promise.resolve(this);
412
+ }
413
+ }
414
+ if (saturationRange || hueCenter !== void 0 && hueTolerance !== void 0) {
415
+ const [h, s] = rgbToHsl(rSum / count, gSum / count, bSum / count);
416
+ if (saturationRange) {
417
+ const inRange = s >= saturationRange[0] && s <= saturationRange[1];
418
+ if (invertSaturation ? inRange : !inRange) {
419
+ this.distance = Infinity;
420
+ return Promise.resolve(this);
421
+ }
422
+ }
423
+ if (hueCenter !== void 0 && hueTolerance !== void 0) {
424
+ const diff = Math.abs((h - hueCenter + 180 + 360) % 360 - 180);
425
+ const inRange = diff <= hueTolerance;
426
+ if (invertHue ? inRange : !inRange) {
427
+ this.distance = Infinity;
428
+ return Promise.resolve(this);
429
+ }
430
+ }
389
431
  }
390
432
  }
391
433
  }
@@ -989,9 +1031,15 @@ function makeNGon(opts) {
989
1031
  static {
990
1032
  __name(this, "NGonRegular");
991
1033
  }
992
- static _ngonOpts = { sides, regular: true, rotatable, noise, sizeRange, mutationScale, startAngle, tonalRange: opts.tonalRange };
1034
+ 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 };
993
1035
  static _shapeSpec = { f: "ngon", o: NGonRegular._ngonOpts };
994
1036
  tonalRange;
1037
+ invertTonal;
1038
+ saturationRange;
1039
+ invertSaturation;
1040
+ hueCenter;
1041
+ hueTolerance;
1042
+ invertHue;
995
1043
  center;
996
1044
  r;
997
1045
  angle;
@@ -1000,6 +1048,12 @@ function makeNGon(opts) {
1000
1048
  constructor(w, h) {
1001
1049
  super(w, h);
1002
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;
1003
1057
  this.center = Shape.randomPoint(w, h);
1004
1058
  this.r = Math.max(1, sizeRange[0] + ~~(Math.random() * (sizeRange[1] - sizeRange[0])));
1005
1059
  this.angle = rotatable ? Math.random() * (2 * Math.PI / sides) : startAngle;
@@ -1095,13 +1149,25 @@ function makeNGon(opts) {
1095
1149
  static {
1096
1150
  __name(this, "NGonIrregular");
1097
1151
  }
1098
- static _ngonOpts = { sides, regular: false, convex, sizeRange, mutationScale, tonalRange: opts.tonalRange };
1152
+ 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 };
1099
1153
  static _shapeSpec = { f: "ngon", o: NGonIrregular._ngonOpts };
1100
1154
  tonalRange;
1155
+ invertTonal;
1156
+ saturationRange;
1157
+ invertSaturation;
1158
+ hueCenter;
1159
+ hueTolerance;
1160
+ invertHue;
1101
1161
  points;
1102
1162
  constructor(w, h) {
1103
1163
  super(w, h);
1104
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;
1105
1171
  const first = Shape.randomPoint(w, h);
1106
1172
  this.points = [first];
1107
1173
  for (let i = 1; i < sides; i++) {
@@ -1173,9 +1239,15 @@ function makeRect(opts) {
1173
1239
  static {
1174
1240
  __name(this, "Rect");
1175
1241
  }
1176
- static _rectOpts = { widthRange, heightRange, aspectRatio, rotatable, mutationScale, tonalRange: opts?.tonalRange };
1242
+ 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 };
1177
1243
  static _shapeSpec = { f: "rect", o: Rect._rectOpts };
1178
1244
  tonalRange;
1245
+ invertTonal;
1246
+ saturationRange;
1247
+ invertSaturation;
1248
+ hueCenter;
1249
+ hueTolerance;
1250
+ invertHue;
1179
1251
  center;
1180
1252
  hw;
1181
1253
  hh;
@@ -1183,6 +1255,12 @@ function makeRect(opts) {
1183
1255
  constructor(w, h) {
1184
1256
  super(w, h);
1185
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;
1186
1264
  this.center = Shape.randomPoint(w, h);
1187
1265
  this.hw = widthRange[0] + ~~(Math.random() * (widthRange[1] - widthRange[0]));
1188
1266
  if (aspectRatio !== void 0) {
@@ -1295,14 +1373,26 @@ function makeCircle(opts) {
1295
1373
  static {
1296
1374
  __name(this, "MadeCircle");
1297
1375
  }
1298
- static _circleOpts = { sizeRange, mutationScale, tonalRange: opts?.tonalRange };
1376
+ 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 };
1299
1377
  static _shapeSpec = { f: "circle", o: MadeCircle._circleOpts };
1300
1378
  tonalRange;
1379
+ invertTonal;
1380
+ saturationRange;
1381
+ invertSaturation;
1382
+ hueCenter;
1383
+ hueTolerance;
1384
+ invertHue;
1301
1385
  center;
1302
1386
  r;
1303
1387
  constructor(w, h) {
1304
1388
  super(w, h);
1305
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;
1306
1396
  this.center = Shape.randomPoint(w, h);
1307
1397
  this.r = Math.max(1, sizeRange[0] + ~~(Math.random() * (sizeRange[1] - sizeRange[0])));
1308
1398
  this.computeBbox();
@@ -1363,15 +1453,27 @@ function makeEllipse(opts) {
1363
1453
  static {
1364
1454
  __name(this, "MadeEllipse");
1365
1455
  }
1366
- static _ellipseOpts = { rxRange, ryRange, aspectRatio, mutationScale, tonalRange: opts?.tonalRange };
1456
+ 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 };
1367
1457
  static _shapeSpec = { f: "ellipse", o: MadeEllipse._ellipseOpts };
1368
1458
  tonalRange;
1459
+ invertTonal;
1460
+ saturationRange;
1461
+ invertSaturation;
1462
+ hueCenter;
1463
+ hueTolerance;
1464
+ invertHue;
1369
1465
  center;
1370
1466
  rx;
1371
1467
  ry;
1372
1468
  constructor(w, h) {
1373
1469
  super(w, h);
1374
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;
1375
1477
  this.center = Shape.randomPoint(w, h);
1376
1478
  this.rx = Math.max(1, rxRange[0] + ~~(Math.random() * (rxRange[1] - rxRange[0])));
1377
1479
  this.ry = aspectRatio !== void 0 ? Math.max(1, Math.round(this.rx / aspectRatio)) : Math.max(1, ryRange[0] + ~~(Math.random() * (ryRange[1] - ryRange[0])));
@@ -1443,14 +1545,26 @@ function makeGlyph(opts) {
1443
1545
  static {
1444
1546
  __name(this, "MadeGlyph");
1445
1547
  }
1446
- static _glyphOpts = { char, fontFamily, sizeRange, mutationScale, tonalRange: opts?.tonalRange };
1548
+ 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 };
1447
1549
  static _shapeSpec = { f: "glyph", o: MadeGlyph._glyphOpts };
1448
1550
  tonalRange;
1551
+ invertTonal;
1552
+ saturationRange;
1553
+ invertSaturation;
1554
+ hueCenter;
1555
+ hueTolerance;
1556
+ invertHue;
1449
1557
  center;
1450
1558
  fontSize;
1451
1559
  constructor(w, h) {
1452
1560
  super(w, h);
1453
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;
1454
1568
  this.center = Shape.randomPoint(w, h);
1455
1569
  this.fontSize = Math.max(sizeRange[0], sizeRange[0] + ~~(Math.random() * (sizeRange[1] - sizeRange[0])));
1456
1570
  this.computeBbox();
@@ -1845,6 +1959,7 @@ export {
1845
1959
  parseColor,
1846
1960
  renderStepToCtx,
1847
1961
  replayOutput,
1962
+ rgbToHsl,
1848
1963
  stepDataToSVGElement,
1849
1964
  stepPerf
1850
1965
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slithy/prim-lib",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Core engine for primitive-based image reconstruction.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -13,14 +13,24 @@
13
13
  "dist"
14
14
  ],
15
15
  "sideEffects": false,
16
+ "scripts": {
17
+ "clean": "rm -rf dist",
18
+ "prepack": "rm -rf dist && tsup src/index.ts --format esm --dts --keep-names",
19
+ "build": "rm -rf dist && tsup src/index.ts --format esm --dts --keep-names",
20
+ "dev": "tsup src/index.ts --format esm --watch --keep-names",
21
+ "typecheck": "tsc --noEmit",
22
+ "lint": "eslint .",
23
+ "test": "vitest run",
24
+ "test:watch": "vitest"
25
+ },
16
26
  "devDependencies": {
27
+ "@slithy/eslint-config": "workspace:*",
28
+ "@slithy/tsconfig": "workspace:*",
17
29
  "@vitest/coverage-v8": "^4.1.2",
18
30
  "jsdom": "^29.0.1",
19
31
  "tsup": "^8",
20
32
  "typescript": "^5",
21
- "vitest": "^4.1.2",
22
- "@slithy/eslint-config": "0.0.0",
23
- "@slithy/tsconfig": "0.0.0"
33
+ "vitest": "^4.1.2"
24
34
  },
25
35
  "author": {
26
36
  "name": "Matthew Campagna",
@@ -41,14 +51,5 @@
41
51
  },
42
52
  "publishConfig": {
43
53
  "access": "public"
44
- },
45
- "scripts": {
46
- "clean": "rm -rf dist",
47
- "build": "rm -rf dist && tsup src/index.ts --format esm --dts --keep-names",
48
- "dev": "tsup src/index.ts --format esm --watch --keep-names",
49
- "typecheck": "tsc --noEmit",
50
- "lint": "eslint .",
51
- "test": "vitest run",
52
- "test:watch": "vitest"
53
54
  }
54
- }
55
+ }