@slithy/prim-lib 0.6.0 → 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
@@ -24,6 +24,9 @@ interface ShapeImageData {
24
24
  interface ShapeInterface {
25
25
  bbox: Bbox;
26
26
  tonalRange?: [number, number];
27
+ saturationRange?: [number, number];
28
+ hueCenter?: number;
29
+ hueTolerance?: number;
27
30
  /** cfg is rarely used by implementations; accepts Partial to keep call sites flexible. */
28
31
  mutate(cfg?: Partial<Cfg>): ShapeInterface;
29
32
  toSVG(): SVGElement | undefined;
@@ -307,6 +310,9 @@ interface NGonOptions {
307
310
  sizeRange?: [number, number];
308
311
  mutationScale?: number;
309
312
  tonalRange?: [number, number];
313
+ saturationRange?: [number, number];
314
+ hueCenter?: number;
315
+ hueTolerance?: number;
310
316
  }
311
317
  declare function makeNGon(opts: NGonOptions): new (w: number, h: number) => ShapeInterface;
312
318
  interface RectOptions {
@@ -316,12 +322,18 @@ interface RectOptions {
316
322
  rotatable?: boolean;
317
323
  mutationScale?: number;
318
324
  tonalRange?: [number, number];
325
+ saturationRange?: [number, number];
326
+ hueCenter?: number;
327
+ hueTolerance?: number;
319
328
  }
320
329
  declare function makeRect(opts?: Partial<RectOptions>): new (w: number, h: number) => ShapeInterface;
321
330
  interface CircleOptions {
322
331
  sizeRange?: [number, number];
323
332
  mutationScale?: number;
324
333
  tonalRange?: [number, number];
334
+ saturationRange?: [number, number];
335
+ hueCenter?: number;
336
+ hueTolerance?: number;
325
337
  }
326
338
  declare function makeCircle(opts?: Partial<CircleOptions>): new (w: number, h: number) => ShapeInterface;
327
339
  interface EllipseOptions {
@@ -330,6 +342,9 @@ interface EllipseOptions {
330
342
  aspectRatio?: number;
331
343
  mutationScale?: number;
332
344
  tonalRange?: [number, number];
345
+ saturationRange?: [number, number];
346
+ hueCenter?: number;
347
+ hueTolerance?: number;
333
348
  }
334
349
  declare function makeEllipse(opts?: Partial<EllipseOptions>): new (w: number, h: number) => ShapeInterface;
335
350
  interface GlyphOptions {
@@ -338,6 +353,9 @@ interface GlyphOptions {
338
353
  sizeRange?: [number, number];
339
354
  mutationScale?: number;
340
355
  tonalRange?: [number, number];
356
+ saturationRange?: [number, number];
357
+ hueCenter?: number;
358
+ hueTolerance?: number;
341
359
  }
342
360
  declare function makeGlyph(opts?: Partial<GlyphOptions>): new (w: number, h: number) => ShapeInterface;
343
361
  declare class Debug extends Shape {
@@ -346,6 +364,7 @@ declare class Debug extends Shape {
346
364
  }
347
365
 
348
366
  declare const SVGNS = "http://www.w3.org/2000/svg";
367
+ declare function rgbToHsl(r: number, g: number, b: number): [number, number, number];
349
368
  declare function parseColor(color: string): [number, number, number];
350
369
 
351
370
  declare function clamp(x: number, min: number, max: number): number;
@@ -368,4 +387,4 @@ interface ReplayResult {
368
387
  }
369
388
  declare function replayOutput(data: SerializedOutput): ReplayResult;
370
389
 
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 };
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}`);
@@ -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, saturationRange, hueCenter, hueTolerance } = 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,33 @@ 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
+ 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
+ }
389
426
  }
390
427
  }
391
428
  }
@@ -989,9 +1026,12 @@ function makeNGon(opts) {
989
1026
  static {
990
1027
  __name(this, "NGonRegular");
991
1028
  }
992
- static _ngonOpts = { sides, regular: true, rotatable, noise, sizeRange, mutationScale, startAngle, tonalRange: opts.tonalRange };
1029
+ static _ngonOpts = { sides, regular: true, rotatable, noise, sizeRange, mutationScale, startAngle, tonalRange: opts.tonalRange, saturationRange: opts.saturationRange, hueCenter: opts.hueCenter, hueTolerance: opts.hueTolerance };
993
1030
  static _shapeSpec = { f: "ngon", o: NGonRegular._ngonOpts };
994
1031
  tonalRange;
1032
+ saturationRange;
1033
+ hueCenter;
1034
+ hueTolerance;
995
1035
  center;
996
1036
  r;
997
1037
  angle;
@@ -1000,6 +1040,9 @@ function makeNGon(opts) {
1000
1040
  constructor(w, h) {
1001
1041
  super(w, h);
1002
1042
  this.tonalRange = opts.tonalRange;
1043
+ this.saturationRange = opts.saturationRange;
1044
+ this.hueCenter = opts.hueCenter;
1045
+ this.hueTolerance = opts.hueTolerance;
1003
1046
  this.center = Shape.randomPoint(w, h);
1004
1047
  this.r = Math.max(1, sizeRange[0] + ~~(Math.random() * (sizeRange[1] - sizeRange[0])));
1005
1048
  this.angle = rotatable ? Math.random() * (2 * Math.PI / sides) : startAngle;
@@ -1095,13 +1138,19 @@ function makeNGon(opts) {
1095
1138
  static {
1096
1139
  __name(this, "NGonIrregular");
1097
1140
  }
1098
- static _ngonOpts = { sides, regular: false, convex, sizeRange, mutationScale, tonalRange: opts.tonalRange };
1141
+ static _ngonOpts = { sides, regular: false, convex, sizeRange, mutationScale, tonalRange: opts.tonalRange, saturationRange: opts.saturationRange, hueCenter: opts.hueCenter, hueTolerance: opts.hueTolerance };
1099
1142
  static _shapeSpec = { f: "ngon", o: NGonIrregular._ngonOpts };
1100
1143
  tonalRange;
1144
+ saturationRange;
1145
+ hueCenter;
1146
+ hueTolerance;
1101
1147
  points;
1102
1148
  constructor(w, h) {
1103
1149
  super(w, h);
1104
1150
  this.tonalRange = opts.tonalRange;
1151
+ this.saturationRange = opts.saturationRange;
1152
+ this.hueCenter = opts.hueCenter;
1153
+ this.hueTolerance = opts.hueTolerance;
1105
1154
  const first = Shape.randomPoint(w, h);
1106
1155
  this.points = [first];
1107
1156
  for (let i = 1; i < sides; i++) {
@@ -1173,9 +1222,12 @@ function makeRect(opts) {
1173
1222
  static {
1174
1223
  __name(this, "Rect");
1175
1224
  }
1176
- static _rectOpts = { widthRange, heightRange, aspectRatio, rotatable, mutationScale, tonalRange: opts?.tonalRange };
1225
+ static _rectOpts = { widthRange, heightRange, aspectRatio, rotatable, mutationScale, tonalRange: opts?.tonalRange, saturationRange: opts?.saturationRange, hueCenter: opts?.hueCenter, hueTolerance: opts?.hueTolerance };
1177
1226
  static _shapeSpec = { f: "rect", o: Rect._rectOpts };
1178
1227
  tonalRange;
1228
+ saturationRange;
1229
+ hueCenter;
1230
+ hueTolerance;
1179
1231
  center;
1180
1232
  hw;
1181
1233
  hh;
@@ -1183,6 +1235,9 @@ function makeRect(opts) {
1183
1235
  constructor(w, h) {
1184
1236
  super(w, h);
1185
1237
  this.tonalRange = opts?.tonalRange;
1238
+ this.saturationRange = opts?.saturationRange;
1239
+ this.hueCenter = opts?.hueCenter;
1240
+ this.hueTolerance = opts?.hueTolerance;
1186
1241
  this.center = Shape.randomPoint(w, h);
1187
1242
  this.hw = widthRange[0] + ~~(Math.random() * (widthRange[1] - widthRange[0]));
1188
1243
  if (aspectRatio !== void 0) {
@@ -1295,14 +1350,20 @@ function makeCircle(opts) {
1295
1350
  static {
1296
1351
  __name(this, "MadeCircle");
1297
1352
  }
1298
- static _circleOpts = { sizeRange, mutationScale, tonalRange: opts?.tonalRange };
1353
+ static _circleOpts = { sizeRange, mutationScale, tonalRange: opts?.tonalRange, saturationRange: opts?.saturationRange, hueCenter: opts?.hueCenter, hueTolerance: opts?.hueTolerance };
1299
1354
  static _shapeSpec = { f: "circle", o: MadeCircle._circleOpts };
1300
1355
  tonalRange;
1356
+ saturationRange;
1357
+ hueCenter;
1358
+ hueTolerance;
1301
1359
  center;
1302
1360
  r;
1303
1361
  constructor(w, h) {
1304
1362
  super(w, h);
1305
1363
  this.tonalRange = opts?.tonalRange;
1364
+ this.saturationRange = opts?.saturationRange;
1365
+ this.hueCenter = opts?.hueCenter;
1366
+ this.hueTolerance = opts?.hueTolerance;
1306
1367
  this.center = Shape.randomPoint(w, h);
1307
1368
  this.r = Math.max(1, sizeRange[0] + ~~(Math.random() * (sizeRange[1] - sizeRange[0])));
1308
1369
  this.computeBbox();
@@ -1363,15 +1424,21 @@ function makeEllipse(opts) {
1363
1424
  static {
1364
1425
  __name(this, "MadeEllipse");
1365
1426
  }
1366
- static _ellipseOpts = { rxRange, ryRange, aspectRatio, mutationScale, tonalRange: opts?.tonalRange };
1427
+ static _ellipseOpts = { rxRange, ryRange, aspectRatio, mutationScale, tonalRange: opts?.tonalRange, saturationRange: opts?.saturationRange, hueCenter: opts?.hueCenter, hueTolerance: opts?.hueTolerance };
1367
1428
  static _shapeSpec = { f: "ellipse", o: MadeEllipse._ellipseOpts };
1368
1429
  tonalRange;
1430
+ saturationRange;
1431
+ hueCenter;
1432
+ hueTolerance;
1369
1433
  center;
1370
1434
  rx;
1371
1435
  ry;
1372
1436
  constructor(w, h) {
1373
1437
  super(w, h);
1374
1438
  this.tonalRange = opts?.tonalRange;
1439
+ this.saturationRange = opts?.saturationRange;
1440
+ this.hueCenter = opts?.hueCenter;
1441
+ this.hueTolerance = opts?.hueTolerance;
1375
1442
  this.center = Shape.randomPoint(w, h);
1376
1443
  this.rx = Math.max(1, rxRange[0] + ~~(Math.random() * (rxRange[1] - rxRange[0])));
1377
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])));
@@ -1443,14 +1510,20 @@ function makeGlyph(opts) {
1443
1510
  static {
1444
1511
  __name(this, "MadeGlyph");
1445
1512
  }
1446
- static _glyphOpts = { char, fontFamily, sizeRange, mutationScale, tonalRange: opts?.tonalRange };
1513
+ static _glyphOpts = { char, fontFamily, sizeRange, mutationScale, tonalRange: opts?.tonalRange, saturationRange: opts?.saturationRange, hueCenter: opts?.hueCenter, hueTolerance: opts?.hueTolerance };
1447
1514
  static _shapeSpec = { f: "glyph", o: MadeGlyph._glyphOpts };
1448
1515
  tonalRange;
1516
+ saturationRange;
1517
+ hueCenter;
1518
+ hueTolerance;
1449
1519
  center;
1450
1520
  fontSize;
1451
1521
  constructor(w, h) {
1452
1522
  super(w, h);
1453
1523
  this.tonalRange = opts?.tonalRange;
1524
+ this.saturationRange = opts?.saturationRange;
1525
+ this.hueCenter = opts?.hueCenter;
1526
+ this.hueTolerance = opts?.hueTolerance;
1454
1527
  this.center = Shape.randomPoint(w, h);
1455
1528
  this.fontSize = Math.max(sizeRange[0], sizeRange[0] + ~~(Math.random() * (sizeRange[1] - sizeRange[0])));
1456
1529
  this.computeBbox();
@@ -1845,6 +1918,7 @@ export {
1845
1918
  parseColor,
1846
1919
  renderStepToCtx,
1847
1920
  replayOutput,
1921
+ rgbToHsl,
1848
1922
  stepDataToSVGElement,
1849
1923
  stepPerf
1850
1924
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slithy/prim-lib",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Core engine for primitive-based image reconstruction.",
5
5
  "type": "module",
6
6
  "exports": {