@pixerate/editor 0.109.0 → 0.110.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 (61) hide show
  1. package/dist/blending-DXl06NSx.d.ts +486 -0
  2. package/dist/blending-aOE6Y3UE.d.cts +486 -0
  3. package/dist/chunk-7VOHOPLW.cjs +1136 -0
  4. package/dist/chunk-7VOHOPLW.cjs.map +1 -0
  5. package/dist/{chunk-RE77QYUF.js → chunk-DMRSJBJF.js} +3 -3
  6. package/dist/chunk-FTBZPI77.cjs +1 -0
  7. package/dist/chunk-FTBZPI77.cjs.map +1 -0
  8. package/dist/{chunk-WMBIJCHA.js → chunk-HNS7UHCM.js} +5 -1
  9. package/dist/{chunk-WMBIJCHA.js.map → chunk-HNS7UHCM.js.map} +1 -1
  10. package/dist/{chunk-CFMWALBM.js → chunk-LQEEDHOP.js} +2 -2
  11. package/dist/{chunk-D37BX2ZA.cjs → chunk-MGL4U5WA.cjs} +199 -199
  12. package/dist/{chunk-D37BX2ZA.cjs.map → chunk-MGL4U5WA.cjs.map} +1 -1
  13. package/dist/{chunk-AZTZRLDI.cjs → chunk-POGBUYWE.cjs} +92 -92
  14. package/dist/{chunk-AZTZRLDI.cjs.map → chunk-POGBUYWE.cjs.map} +1 -1
  15. package/dist/chunk-VKK5VS2T.js +1 -0
  16. package/dist/{chunk-SZ6KMZNM.cjs → chunk-XIOMMGPD.cjs} +6 -2
  17. package/dist/chunk-XIOMMGPD.cjs.map +1 -0
  18. package/dist/{chunk-3PHA3UKI.js → chunk-Z4BC6M74.js} +523 -121
  19. package/dist/chunk-Z4BC6M74.js.map +1 -0
  20. package/dist/{export-BLPLaE6Z.d.ts → export-CO2ryu3I.d.ts} +3 -3
  21. package/dist/{export-DSUc4fQD.d.cts → export-DRiLzP_j.d.cts} +3 -3
  22. package/dist/image-editor/index.cjs +722 -696
  23. package/dist/image-editor/index.cjs.map +1 -1
  24. package/dist/image-editor/index.d.cts +9 -9
  25. package/dist/image-editor/index.d.ts +9 -9
  26. package/dist/image-editor/index.js +31 -5
  27. package/dist/image-editor/index.js.map +1 -1
  28. package/dist/paint/index.cjs +28 -4
  29. package/dist/paint/index.cjs.map +1 -1
  30. package/dist/paint/index.d.cts +3 -89
  31. package/dist/paint/index.d.ts +3 -89
  32. package/dist/paint/index.js +27 -3
  33. package/dist/paint-editor/index.cjs +1052 -286
  34. package/dist/paint-editor/index.cjs.map +1 -1
  35. package/dist/paint-editor/index.d.cts +301 -22
  36. package/dist/paint-editor/index.d.ts +301 -22
  37. package/dist/paint-editor/index.js +848 -82
  38. package/dist/paint-editor/index.js.map +1 -1
  39. package/dist/raster/index.cjs +5 -3
  40. package/dist/raster/index.cjs.map +1 -1
  41. package/dist/raster/index.d.cts +2 -2
  42. package/dist/raster/index.d.ts +2 -2
  43. package/dist/raster/index.js +4 -2
  44. package/dist/{render-C-fO_mes.d.cts → render-BEriFuqK.d.cts} +1 -1
  45. package/dist/{render-OLIMk_v0.d.ts → render-tjzKaik-.d.ts} +1 -1
  46. package/dist/{surface-CkKOHGPm.d.cts → surface-C8RRg_Bh.d.cts} +109 -1
  47. package/dist/{surface-CkKOHGPm.d.ts → surface-C8RRg_Bh.d.ts} +109 -1
  48. package/package.json +1 -1
  49. package/skills/pixerate-paint-editor/SKILL.md +49 -13
  50. package/dist/chunk-3PHA3UKI.js.map +0 -1
  51. package/dist/chunk-6BU5WU4R.cjs +0 -1
  52. package/dist/chunk-6BU5WU4R.cjs.map +0 -1
  53. package/dist/chunk-HUFQD4CB.cjs +0 -734
  54. package/dist/chunk-HUFQD4CB.cjs.map +0 -1
  55. package/dist/chunk-SZ6KMZNM.cjs.map +0 -1
  56. package/dist/chunk-XYBJKUAU.js +0 -1
  57. package/dist/mixer-5ziHU8OS.d.cts +0 -282
  58. package/dist/mixer-DNqiYXYQ.d.ts +0 -282
  59. /package/dist/{chunk-RE77QYUF.js.map → chunk-DMRSJBJF.js.map} +0 -0
  60. /package/dist/{chunk-CFMWALBM.js.map → chunk-LQEEDHOP.js.map} +0 -0
  61. /package/dist/{chunk-XYBJKUAU.js.map → chunk-VKK5VS2T.js.map} +0 -0
@@ -1,14 +1,16 @@
1
1
  import {
2
+ PIGMENT_BANDS,
2
3
  TILE,
3
4
  blendPixel,
4
5
  createPigment,
5
6
  lerpPigmentInto,
6
7
  mixPigment8,
7
8
  mixPigments,
9
+ pigment8,
8
10
  setPigmentColor,
9
11
  tileCoord,
10
12
  tileKey
11
- } from "./chunk-WMBIJCHA.js";
13
+ } from "./chunk-HNS7UHCM.js";
12
14
 
13
15
  // src/paint/brush.ts
14
16
  var DEFAULT_BRUSH = {
@@ -412,83 +414,51 @@ function coverageTileCoord(key) {
412
414
  return tileCoord(key);
413
415
  }
414
416
 
415
- // src/paint/mixer.ts
417
+ // src/paint/exchange.ts
416
418
  var MIXING_MODELS = ["rgb", "pigment"];
417
- var DEFAULT_MIXER = {
418
- wet: 0.5,
419
- load: 0.5,
420
- mix: 0.5,
421
- loadEachStroke: true,
422
- cleanEachStroke: true,
423
- mixing: "rgb"
424
- };
419
+ var EMPTY_MIXER_LOAD = { reservoir: null, amount: 0, pickup: null };
425
420
  var MIXER_PICKUP_RATE = 0.25;
426
421
  var MIXER_DRAIN = 0.08;
427
- function normalizeMixer(raw = {}) {
428
- const unit = (v, d) => typeof v === "number" && Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : d;
429
- const bool = (v, d) => typeof v === "boolean" ? v : d;
430
- return {
431
- wet: unit(raw.wet, DEFAULT_MIXER.wet),
432
- load: unit(raw.load, DEFAULT_MIXER.load),
433
- mix: unit(raw.mix, DEFAULT_MIXER.mix),
434
- loadEachStroke: bool(raw.loadEachStroke, DEFAULT_MIXER.loadEachStroke),
435
- cleanEachStroke: bool(raw.cleanEachStroke, DEFAULT_MIXER.cleanEachStroke),
436
- mixing: raw.mixing === "pigment" ? "pigment" : "rgb"
437
- };
438
- }
439
- var EMPTY_MIXER_LOAD = { reservoir: null, amount: 0, pickup: null };
440
- function loadMixer(load, color) {
441
- const c = (v) => Math.min(1, Math.max(0, Number.isFinite(v) ? v : 0));
442
- return { reservoir: [c(color[0]), c(color[1]), c(color[2])], amount: 1, pickup: load.pickup };
422
+ function exchangeRates(pickup, mix, load, mixing) {
423
+ return { pickup: MIXER_PICKUP_RATE * pickup, mix: mix * pickup, drain: MIXER_DRAIN * (1 - load), mixing };
443
424
  }
444
- function cleanMixer(load) {
445
- return { reservoir: load.reservoir, amount: load.amount, pickup: null };
446
- }
447
- var MixerStroke = class {
448
- constructor(brush, settings, load, target) {
449
- this.brush = brush;
450
- this.target = target;
451
- this.settings = normalizeMixer(settings);
452
- this.walker = new DabWalker(brush);
425
+ var PaintExchange = class {
426
+ constructor(rates, load) {
427
+ this.rates = rates;
453
428
  this.reservoir = load.reservoir ? [load.reservoir[0], load.reservoir[1], load.reservoir[2]] : null;
454
429
  this.amount = this.reservoir ? Math.min(1, Math.max(0, load.amount)) : 0;
455
- this.pickup = load.pickup ? [load.pickup[0], load.pickup[1], load.pickup[2]] : null;
456
- this.pig = this.settings.mixing === "pigment" ? { res: createPigment(this.reservoir ?? [0, 0, 0]), pick: createPigment(this.pickup ?? [0, 0, 0]), paint: createPigment([0, 0, 0]), canvas: createPigment([0, 0, 0]) } : null;
430
+ this.well = load.pickup ? [load.pickup[0], load.pickup[1], load.pickup[2]] : null;
431
+ this.pig = rates.mixing === "pigment" ? { res: createPigment(this.reservoir ?? [0, 0, 0]), well: createPigment(this.well ?? [0, 0, 0]), paint: createPigment([0, 0, 0]), canvas: createPigment([0, 0, 0]) } : null;
457
432
  }
458
- brush;
459
- target;
460
- dabCount = 0;
461
- bounds = null;
462
- walker;
463
- smoothPos = null;
464
- buf = new Float32Array(0);
465
- settings;
433
+ rates;
466
434
  reservoir;
467
435
  amount;
468
- pickup;
436
+ well;
469
437
  pig;
470
438
  out = [0, 0, 0];
471
- /** The brush load after the dabs so far. */
439
+ /** What the brush carries now. */
472
440
  get load() {
473
- return { reservoir: this.reservoir ? [...this.reservoir] : null, amount: this.amount, pickup: this.pickup ? [...this.pickup] : null };
441
+ return { reservoir: this.reservoir ? [...this.reservoir] : null, amount: this.amount, pickup: this.well ? [...this.well] : null };
474
442
  }
475
- push(points) {
476
- const dabs = [];
477
- for (const p of points) this.walker.push(this.smooth(p), dabs);
478
- return this.paint(dabs);
443
+ /** The colour picked up so far, or `null`. */
444
+ get pickup() {
445
+ return this.well;
479
446
  }
480
- finish(last) {
481
- const dabs = [];
482
- if (last && this.smoothPos && (this.smoothPos.x !== last.x || this.smoothPos.y !== last.y)) this.walker.push(last, dabs);
483
- this.walker.finish(dabs);
484
- return this.paint(dabs);
447
+ /** The prepared pigment of the last `paint` colour (pigment mixing only), for `deposit`. */
448
+ get paintPigment() {
449
+ return this.pig?.paint ?? null;
485
450
  }
486
- smooth(p) {
487
- const a = 1 - Math.min(0.95, Math.max(0, this.brush.smoothing));
488
- const cur = this.smoothPos;
489
- const n = cur && a < 1 ? { ...p, x: cur.x + (p.x - cur.x) * a, y: cur.y + (p.y - cur.y) * a } : p;
490
- this.smoothPos = n;
491
- return n;
451
+ /** Recolours the reservoir without refilling it (a multi-coloured brush, per dab). */
452
+ setReservoirColor(r, g, b) {
453
+ if (!this.reservoir) {
454
+ this.reservoir = [r, g, b];
455
+ this.amount = 1;
456
+ } else {
457
+ this.reservoir[0] = r;
458
+ this.reservoir[1] = g;
459
+ this.reservoir[2] = b;
460
+ }
461
+ if (this.pig) setPigmentColor(this.pig.res, r, g, b);
492
462
  }
493
463
  /** `a` mixed `t` of the way to `b` into `this.out`, by the mixing model. */
494
464
  mix(a, pa, b, pb, t) {
@@ -497,27 +467,84 @@ var MixerStroke = class {
497
467
  else for (let c = 0; c < 3; c++) o[c] = a[c] + (b[c] - a[c]) * t;
498
468
  return o;
499
469
  }
500
- paint(dabs) {
501
- const rects = [];
502
- for (const d of dabs) {
503
- const rect = dabRect(d, this.brush.hardness);
504
- const n = rect.width * rect.height;
505
- if (this.buf.length < n) this.buf = new Float32Array(n);
506
- const cov = this.buf.subarray(0, n);
507
- rasterizeDab({ ...d, alpha: 1 }, this.brush.hardness, rect, cov);
508
- this.dab(d, rect, cov);
509
- rects.push(rect);
510
- this.bounds = union(this.bounds, rect);
511
- this.dabCount++;
470
+ /** `a` mixed `t` of the way to `b` (sRGB 0..1) by the mixing model, into `out`. */
471
+ mixColors(a, b, t, out) {
472
+ if (this.pig) {
473
+ setPigmentColor(this.pig.canvas, a[0], a[1], a[2]);
474
+ setPigmentColor(this.pig.paint, b[0], b[1], b[2]);
475
+ lerpPigmentInto(this.pig.canvas, this.pig.paint, t, out, 0);
476
+ } else for (let c = 0; c < 3; c++) out[c] = a[c] + (b[c] - a[c]) * t;
477
+ return out;
478
+ }
479
+ /** 1. Pick-up: the well moves towards the sampled colour (or takes it, when clean). */
480
+ pickUp(sample) {
481
+ if (!sample || !(this.rates.pickup > 0)) return;
482
+ const pig = this.pig;
483
+ if (!this.well) this.well = [sample.rgb[0], sample.rgb[1], sample.rgb[2]];
484
+ else {
485
+ if (pig) setPigmentColor(pig.canvas, sample.rgb[0], sample.rgb[1], sample.rgb[2]);
486
+ const o = this.mix(this.well, pig?.well ?? null, sample.rgb, pig?.canvas ?? null, this.rates.pickup * sample.contact);
487
+ this.well = [o[0], o[1], o[2]];
512
488
  }
513
- return rects;
489
+ if (pig) setPigmentColor(pig.well, this.well[0], this.well[1], this.well[2]);
514
490
  }
491
+ /**
492
+ * 2. The dab's paint: the reservoir mixed with the well, written to `out`. Returns the total
493
+ * weight (≤ 0: nothing to lay down); deposit `min(1, weight)` of it. With pigment mixing,
494
+ * `paintPigment` is the result's pigment afterwards.
495
+ */
496
+ paint(out) {
497
+ const res = this.reservoir;
498
+ const wr = res ? this.amount * (1 - this.rates.mix) : 0;
499
+ const wp = this.well ? this.rates.mix : 0;
500
+ const total = wr + wp;
501
+ if (!(total > 1e-6)) return 0;
502
+ const pig = this.pig;
503
+ if (res && this.well) {
504
+ const m = this.mix(res, pig?.res ?? null, this.well, pig?.well ?? null, wp / total);
505
+ out[0] = m[0];
506
+ out[1] = m[1];
507
+ out[2] = m[2];
508
+ } else {
509
+ const c = res ?? this.well;
510
+ out[0] = c[0];
511
+ out[1] = c[1];
512
+ out[2] = c[2];
513
+ }
514
+ if (pig) setPigmentColor(pig.paint, out[0], out[1], out[2]);
515
+ return total;
516
+ }
517
+ /** 3. The reservoir drains. */
518
+ drain() {
519
+ this.amount *= 1 - this.rates.drain;
520
+ }
521
+ };
522
+ var TileMedium = class {
523
+ constructor(target, options = {}) {
524
+ this.target = target;
525
+ this.options = options;
526
+ }
527
+ target;
528
+ options;
515
529
  // The tile last looked up by `locate` (and whether it came from `writeTile`).
516
530
  tx = NaN;
517
531
  ty = NaN;
518
532
  tile = null;
519
533
  writable = false;
520
- /** Index of document pixel (dx, dy) in `this.tile`, or -1 when reading an empty tile. */
534
+ canvasPig = createPigment([0, 0, 0]);
535
+ paintPig = createPigment([0, 0, 0]);
536
+ paintKey = -1;
537
+ cb = [0, 0, 0, 0];
538
+ src = [0, 0, 0, 1];
539
+ o = [0, 0, 0];
540
+ /** Forgets the cached tile (call when another writer may have replaced tiles). */
541
+ reset() {
542
+ this.tx = NaN;
543
+ }
544
+ /**
545
+ * The tile holding document pixel (dx, dy) (`pixels`) and the pixel's byte index in it, or -1
546
+ * when reading an empty tile.
547
+ */
521
548
  locate(dx, dy, write) {
522
549
  const t = this.target;
523
550
  const lx = dx - t.offset.x;
@@ -532,9 +559,18 @@ var MixerStroke = class {
532
559
  }
533
560
  return this.tile ? ((ly - ty * TILE) * TILE + (lx - tx * TILE)) * 4 : -1;
534
561
  }
535
- dab(d, rect, cov) {
536
- const { wet, mix, load } = this.settings;
537
- const pig = this.pig;
562
+ /** The tile `locate` last found. */
563
+ get pixels() {
564
+ return this.tile;
565
+ }
566
+ /** Selection coverage at a document pixel, 0..1 (1 without a selection). */
567
+ selectionAt(dx, dy) {
568
+ const sel = this.target.selection;
569
+ if (!sel) return 1;
570
+ if (dx < 0 || dy < 0 || dx >= sel.width || dy >= sel.height) return 0;
571
+ return sel.data[dy * sel.width + dx] / 255;
572
+ }
573
+ sample(rect, cov) {
538
574
  const w = rect.width;
539
575
  this.tx = NaN;
540
576
  let ar = 0;
@@ -559,40 +595,24 @@ var MixerStroke = class {
559
595
  asum += ka;
560
596
  }
561
597
  }
562
- if (asum > 1e-6 && wet > 0) {
563
- const canvas = [ar / asum, ag / asum, ab / asum];
564
- if (!this.pickup) this.pickup = canvas;
565
- else {
566
- if (pig) setPigmentColor(pig.canvas, canvas[0], canvas[1], canvas[2]);
567
- const o = this.mix(this.pickup, pig?.pick ?? null, canvas, pig?.canvas ?? null, MIXER_PICKUP_RATE * wet * (asum / Math.max(1e-6, wsum)));
568
- this.pickup = [o[0], o[1], o[2]];
569
- }
570
- if (pig) setPigmentColor(pig.pick, this.pickup[0], this.pickup[1], this.pickup[2]);
571
- }
572
- const res = this.reservoir;
573
- const wr = res ? this.amount * (1 - mix * wet) : 0;
574
- const wp = this.pickup ? mix * wet : 0;
575
- const total = wr + wp;
576
- if (total > 1e-6) {
577
- const paint = res && this.pickup ? [...this.mix(res, pig?.res ?? null, this.pickup, pig?.pick ?? null, wp / total)] : [...res ?? this.pickup];
578
- if (pig) setPigmentColor(pig.paint, paint[0], paint[1], paint[2]);
579
- const dep = Math.min(1, total) * this.brush.flow * this.brush.opacity * d.opacity;
580
- if (dep > 0) this.deposit(rect, cov, paint, dep);
581
- }
582
- this.amount *= 1 - MIXER_DRAIN * (1 - load);
598
+ if (!(asum > 1e-6)) return null;
599
+ return { rgb: [ar / asum, ag / asum, ab / asum], contact: asum / Math.max(1e-6, wsum) };
583
600
  }
584
- /** Lays `paint` over the pixels under the dab at `dep` × coverage (× selection). */
585
- deposit(rect, cov, paint, dep) {
601
+ deposit(rect, cov, dep, paint, paintPigment, field = null) {
586
602
  const t = this.target;
587
603
  const sel = t.selection;
588
- const pig = this.pig;
589
604
  const w = rect.width;
590
- const o = this.out;
605
+ const o = this.o;
606
+ const blend = this.options.blend;
607
+ const pigment = blend === void 0 ? paintPigment !== null : blend === "pigment";
608
+ const general = blend !== void 0 && blend !== "normal" && blend !== "pigment";
591
609
  let lastKey = -1;
610
+ this.tx = NaN;
592
611
  for (let yy = 0; yy < rect.height; yy++) {
593
612
  const dy = rect.y + yy;
594
613
  for (let xx = 0; xx < w; xx++) {
595
- let k = cov[yy * w + xx] * dep;
614
+ const p = yy * w + xx;
615
+ let k = cov[p] * dep;
596
616
  if (k <= 0) continue;
597
617
  const dx = rect.x + xx;
598
618
  if (sel) {
@@ -607,22 +627,52 @@ var MixerStroke = class {
607
627
  }
608
628
  const i = this.locate(dx, dy, true);
609
629
  const px = this.tile;
630
+ const pr = field ? field[p * 3] : paint[0];
631
+ const pg = field ? field[p * 3 + 1] : paint[1];
632
+ const pb = field ? field[p * 3 + 2] : paint[2];
633
+ if (general) {
634
+ const cb = this.cb;
635
+ for (let c = 0; c < 4; c++) cb[c] = px[i + c] / 255;
636
+ const src = this.src;
637
+ src[0] = pr;
638
+ src[1] = pg;
639
+ src[2] = pb;
640
+ const r = blendPixel(blend, cb, src, k);
641
+ px[i] = Math.round(r[0] * 255);
642
+ px[i + 1] = Math.round(r[1] * 255);
643
+ px[i + 2] = Math.round(r[2] * 255);
644
+ if (!t.lockTransparency) px[i + 3] = Math.round(r[3] * 255);
645
+ continue;
646
+ }
610
647
  const a = px[i + 3] / 255;
611
648
  const na = t.lockTransparency ? a : a + (1 - a) * k;
612
649
  const share = t.lockTransparency ? k : na > 0 ? k / na : 1;
613
650
  if (a <= 0 || share >= 1) {
614
- o[0] = paint[0];
615
- o[1] = paint[1];
616
- o[2] = paint[2];
617
- } else if (pig) {
651
+ o[0] = pr;
652
+ o[1] = pg;
653
+ o[2] = pb;
654
+ } else if (pigment && field) {
655
+ mixPigment8(px[i], px[i + 1], px[i + 2], Math.round(pr * 255), Math.round(pg * 255), Math.round(pb * 255), share, o);
656
+ } else if (pigment) {
618
657
  const key = px[i] << 16 | px[i + 1] << 8 | px[i + 2];
619
658
  if (key !== lastKey) {
620
- setPigmentColor(pig.canvas, px[i] / 255, px[i + 1] / 255, px[i + 2] / 255);
659
+ setPigmentColor(this.canvasPig, px[i] / 255, px[i + 1] / 255, px[i + 2] / 255);
621
660
  lastKey = key;
622
661
  }
623
- lerpPigmentInto(pig.canvas, pig.paint, share, o, 0);
662
+ let pp = paintPigment;
663
+ if (!pp) {
664
+ const pkey = Math.round(pr * 255) << 16 | Math.round(pg * 255) << 8 | Math.round(pb * 255);
665
+ if (pkey !== this.paintKey) {
666
+ setPigmentColor(this.paintPig, pr, pg, pb);
667
+ this.paintKey = pkey;
668
+ }
669
+ pp = this.paintPig;
670
+ }
671
+ lerpPigmentInto(this.canvasPig, pp, share, o, 0);
624
672
  } else {
625
- for (let c = 0; c < 3; c++) o[c] = px[i + c] / 255 + (paint[c] - px[i + c] / 255) * share;
673
+ o[0] = px[i] / 255 + (pr - px[i] / 255) * share;
674
+ o[1] = px[i + 1] / 255 + (pg - px[i + 1] / 255) * share;
675
+ o[2] = px[i + 2] / 255 + (pb - px[i + 2] / 255) * share;
626
676
  }
627
677
  px[i] = Math.round(o[0] * 255);
628
678
  px[i + 1] = Math.round(o[1] * 255);
@@ -632,6 +682,102 @@ var MixerStroke = class {
632
682
  }
633
683
  }
634
684
  };
685
+
686
+ // src/paint/mixer.ts
687
+ var DEFAULT_MIXER = {
688
+ wet: 0.5,
689
+ load: 0.5,
690
+ mix: 0.5,
691
+ loadEachStroke: true,
692
+ cleanEachStroke: true,
693
+ mixing: "rgb"
694
+ };
695
+ function normalizeMixer(raw = {}) {
696
+ const unit2 = (v, d) => typeof v === "number" && Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : d;
697
+ const bool = (v, d) => typeof v === "boolean" ? v : d;
698
+ return {
699
+ wet: unit2(raw.wet, DEFAULT_MIXER.wet),
700
+ load: unit2(raw.load, DEFAULT_MIXER.load),
701
+ mix: unit2(raw.mix, DEFAULT_MIXER.mix),
702
+ loadEachStroke: bool(raw.loadEachStroke, DEFAULT_MIXER.loadEachStroke),
703
+ cleanEachStroke: bool(raw.cleanEachStroke, DEFAULT_MIXER.cleanEachStroke),
704
+ mixing: raw.mixing === "pigment" ? "pigment" : "rgb"
705
+ };
706
+ }
707
+ function mixerRates(settings) {
708
+ return exchangeRates(settings.wet, settings.mix, settings.load, settings.mixing);
709
+ }
710
+ function loadMixer(load, color) {
711
+ const c = (v) => Math.min(1, Math.max(0, Number.isFinite(v) ? v : 0));
712
+ return { reservoir: [c(color[0]), c(color[1]), c(color[2])], amount: 1, pickup: load.pickup };
713
+ }
714
+ function cleanMixer(load) {
715
+ return { reservoir: load.reservoir, amount: load.amount, pickup: null };
716
+ }
717
+ var MixerStroke = class {
718
+ constructor(brush, settings, load, target) {
719
+ this.brush = brush;
720
+ this.walker = new DabWalker(brush);
721
+ this.exchange = new PaintExchange(mixerRates(normalizeMixer(settings)), load);
722
+ this.medium = new TileMedium(target);
723
+ }
724
+ brush;
725
+ dabCount = 0;
726
+ bounds = null;
727
+ walker;
728
+ smoothPos = null;
729
+ buf = new Float32Array(0);
730
+ exchange;
731
+ medium;
732
+ paintColor = [0, 0, 0];
733
+ /** The brush load after the dabs so far. */
734
+ get load() {
735
+ return this.exchange.load;
736
+ }
737
+ push(points) {
738
+ const dabs = [];
739
+ for (const p of points) this.walker.push(this.smooth(p), dabs);
740
+ return this.paint(dabs);
741
+ }
742
+ finish(last) {
743
+ const dabs = [];
744
+ if (last && this.smoothPos && (this.smoothPos.x !== last.x || this.smoothPos.y !== last.y)) this.walker.push(last, dabs);
745
+ this.walker.finish(dabs);
746
+ return this.paint(dabs);
747
+ }
748
+ smooth(p) {
749
+ const a = 1 - Math.min(0.95, Math.max(0, this.brush.smoothing));
750
+ const cur = this.smoothPos;
751
+ const n = cur && a < 1 ? { ...p, x: cur.x + (p.x - cur.x) * a, y: cur.y + (p.y - cur.y) * a } : p;
752
+ this.smoothPos = n;
753
+ return n;
754
+ }
755
+ paint(dabs) {
756
+ const rects = [];
757
+ for (const d of dabs) {
758
+ const rect = dabRect(d, this.brush.hardness);
759
+ const n = rect.width * rect.height;
760
+ if (this.buf.length < n) this.buf = new Float32Array(n);
761
+ const cov = this.buf.subarray(0, n);
762
+ rasterizeDab({ ...d, alpha: 1 }, this.brush.hardness, rect, cov);
763
+ this.dab(d, rect, cov);
764
+ rects.push(rect);
765
+ this.bounds = union(this.bounds, rect);
766
+ this.dabCount++;
767
+ }
768
+ return rects;
769
+ }
770
+ dab(d, rect, cov) {
771
+ const ex = this.exchange;
772
+ ex.pickUp(this.medium.sample(rect, cov));
773
+ const total = ex.paint(this.paintColor);
774
+ if (total > 0) {
775
+ const dep = Math.min(1, total) * this.brush.flow * this.brush.opacity * d.opacity;
776
+ if (dep > 0) this.medium.deposit(rect, cov, dep, this.paintColor, ex.paintPigment);
777
+ }
778
+ ex.drain();
779
+ }
780
+ };
635
781
  function union(a, b) {
636
782
  if (!a) return { ...b };
637
783
  const x = Math.min(a.x, b.x);
@@ -639,6 +785,243 @@ function union(a, b) {
639
785
  return { x, y, width: Math.max(a.x + a.width, b.x + b.width) - x, height: Math.max(a.y + a.height, b.y + b.height) - y };
640
786
  }
641
787
 
788
+ // src/paint/blending.ts
789
+ var DEFAULT_BLENDER = { strength: 0.5, distance: 0.35, mixing: "pigment" };
790
+ var DEFAULT_SMUDGE = { strength: 0.7, length: 0.85, mixing: "rgb" };
791
+ var unit = (v, d) => typeof v === "number" && Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : d;
792
+ function normalizeBlender(raw = {}, base = DEFAULT_BLENDER) {
793
+ return { strength: unit(raw.strength, base.strength), distance: unit(raw.distance, base.distance), mixing: raw.mixing === "rgb" || raw.mixing === "pigment" ? raw.mixing : base.mixing };
794
+ }
795
+ function normalizeSmudge(raw = {}, base = DEFAULT_SMUDGE) {
796
+ return { strength: unit(raw.strength, base.strength), length: unit(raw.length, base.length), mixing: raw.mixing === "rgb" || raw.mixing === "pigment" ? raw.mixing : base.mixing };
797
+ }
798
+ function blendRadius(settings, radius) {
799
+ return Math.max(1, Math.round(settings.distance * 2 * radius));
800
+ }
801
+ function createBlenderScratch() {
802
+ return { sums: new Float64Array(0), mix: { rgb: [0, 0, 0], strength: 1, ks: new Float32Array(PIGMENT_BANDS), scatter: 1 }, out: [0, 0, 0] };
803
+ }
804
+ var N = PIGMENT_BANDS;
805
+ function blenderDab(medium, rect, cov, opacity, radius, settings, scratch) {
806
+ const R = blendRadius(settings, radius);
807
+ const W = rect.width + 2 * R;
808
+ const H = rect.height + 2 * R;
809
+ const pigment = settings.mixing === "pigment";
810
+ const C = pigment ? N + 2 : 4;
811
+ const stride = (W + 1) * C;
812
+ const size = stride * (H + 1);
813
+ if (scratch.sums.length < size) scratch.sums = new Float64Array(size);
814
+ const I = scratch.sums;
815
+ I.fill(0, 0, size);
816
+ medium.reset();
817
+ const row = new Float64Array(C);
818
+ for (let y = 0; y < H; y++) {
819
+ row.fill(0);
820
+ const dy = rect.y - R + y;
821
+ for (let x = 0; x < W; x++) {
822
+ const dx = rect.x - R + x;
823
+ const i = medium.locate(dx, dy, false);
824
+ if (i >= 0) {
825
+ const px = medium.pixels;
826
+ const a = px[i + 3] / 255;
827
+ if (a > 0) {
828
+ if (pigment) {
829
+ const p = pigment8(px[i], px[i + 1], px[i + 2]);
830
+ const sa = p.scatter * p.strength * a;
831
+ for (let k = 0; k < N; k++) row[k] += sa * p.ks[k];
832
+ row[N] += sa;
833
+ row[N + 1] += a;
834
+ } else {
835
+ row[0] += px[i] / 255 * a;
836
+ row[1] += px[i + 1] / 255 * a;
837
+ row[2] += px[i + 2] / 255 * a;
838
+ row[3] += a;
839
+ }
840
+ }
841
+ }
842
+ const o = (y + 1) * stride + (x + 1) * C;
843
+ const up = y * stride + (x + 1) * C;
844
+ for (let c = 0; c < C; c++) I[o + c] = I[up + c] + row[c];
845
+ }
846
+ }
847
+ const area = (2 * R + 1) * (2 * R + 1);
848
+ const strength = settings.strength * opacity;
849
+ const lock = medium.target.lockTransparency;
850
+ const mixPig = scratch.mix;
851
+ const out = scratch.out;
852
+ const sum = (c, x1, y1, x2, y2) => I[y2 * stride + x2 * C + c] - I[y1 * stride + x2 * C + c] - I[y2 * stride + x1 * C + c] + I[y1 * stride + x1 * C + c];
853
+ const w = rect.width;
854
+ medium.reset();
855
+ for (let yy = 0; yy < rect.height; yy++) {
856
+ for (let xx = 0; xx < w; xx++) {
857
+ const k = cov[yy * w + xx];
858
+ if (k <= 0) continue;
859
+ const dx = rect.x + xx;
860
+ const dy = rect.y + yy;
861
+ let t = strength * k * medium.selectionAt(dx, dy);
862
+ if (t <= 0) continue;
863
+ if (t > 1) t = 1;
864
+ const x1 = xx;
865
+ const y1 = yy;
866
+ const x2 = xx + 2 * R + 1;
867
+ const y2 = yy + 2 * R + 1;
868
+ const asum = sum(pigment ? N + 1 : 3, x1, y1, x2, y2);
869
+ const ri = medium.locate(dx, dy, false);
870
+ const a = ri < 0 ? 0 : medium.pixels[ri + 3] / 255;
871
+ if (lock && a <= 0) continue;
872
+ const avgA = asum / area;
873
+ const na = lock ? a : (1 - t) * a + t * avgA;
874
+ if (asum <= 1e-9) {
875
+ if (lock || a <= 0) continue;
876
+ const i2 = medium.locate(dx, dy, true);
877
+ medium.pixels[i2 + 3] = Math.round(na * 255);
878
+ continue;
879
+ }
880
+ const share = lock ? t : na > 0 ? t * avgA / na : 1;
881
+ const i = medium.locate(dx, dy, true);
882
+ const px = medium.pixels;
883
+ if (pigment) {
884
+ const sa = sum(N, x1, y1, x2, y2);
885
+ for (let c = 0; c < N; c++) mixPig.ks[c] = sum(c, x1, y1, x2, y2) / sa;
886
+ mixPig.scatter = sa / asum;
887
+ if (a <= 0 || share >= 1) lerpPigmentInto(mixPig, mixPig, 0, out, 0);
888
+ else lerpPigmentInto(pigment8(px[i], px[i + 1], px[i + 2]), mixPig, share, out, 0);
889
+ } else {
890
+ for (let c = 0; c < 3; c++) {
891
+ const m = sum(c, x1, y1, x2, y2) / asum;
892
+ out[c] = a <= 0 || share >= 1 ? m : px[i + c] / 255 + (m - px[i + c] / 255) * share;
893
+ }
894
+ }
895
+ px[i] = Math.round(out[0] * 255);
896
+ px[i + 1] = Math.round(out[1] * 255);
897
+ px[i + 2] = Math.round(out[2] * 255);
898
+ if (!lock) px[i + 3] = Math.round(na * 255);
899
+ }
900
+ }
901
+ }
902
+ var SmudgeTip = class {
903
+ constructor(settings, diameter) {
904
+ this.settings = settings;
905
+ this.size = Math.max(2, Math.min(512, Math.ceil(diameter)));
906
+ this.patch = new Float32Array(this.size * this.size * 4);
907
+ }
908
+ settings;
909
+ size;
910
+ patch;
911
+ loaded = false;
912
+ out = [0, 0, 0];
913
+ cell(d, px, py) {
914
+ const s = this.size;
915
+ const r = Math.max(0.5, d.radius);
916
+ const bx = Math.min(s - 1, Math.max(0, Math.floor(((px - d.x) / r + 1) / 2 * s)));
917
+ const by = Math.min(s - 1, Math.max(0, Math.floor(((py - d.y) / r + 1) / 2 * s)));
918
+ return (by * s + bx) * 4;
919
+ }
920
+ /** Fills the patch from the pixels under the dab. */
921
+ load(medium, d) {
922
+ const s = this.size;
923
+ const r = Math.max(0.5, d.radius);
924
+ medium.reset();
925
+ for (let by = 0; by < s; by++) {
926
+ for (let bx = 0; bx < s; bx++) {
927
+ const dx = Math.floor(d.x + ((bx + 0.5) / s * 2 - 1) * r);
928
+ const dy = Math.floor(d.y + ((by + 0.5) / s * 2 - 1) * r);
929
+ const i = medium.locate(dx, dy, false);
930
+ const c = (by * s + bx) * 4;
931
+ if (i < 0) {
932
+ this.patch.fill(0, c, c + 4);
933
+ continue;
934
+ }
935
+ const px = medium.pixels;
936
+ this.patch[c] = px[i] / 255;
937
+ this.patch[c + 1] = px[i + 1] / 255;
938
+ this.patch[c + 2] = px[i + 2] / 255;
939
+ this.patch[c + 3] = px[i + 3] / 255;
940
+ }
941
+ }
942
+ this.loaded = true;
943
+ }
944
+ /** `a` mixed `t` of the way to `b` (both 0..1) into `out`. */
945
+ mix(a0, a1, a2, b0, b1, b2, t) {
946
+ const o = this.out;
947
+ if (this.settings.mixing === "pigment") {
948
+ mixPigment8(Math.round(a0 * 255), Math.round(a1 * 255), Math.round(a2 * 255), Math.round(b0 * 255), Math.round(b1 * 255), Math.round(b2 * 255), t, o);
949
+ } else {
950
+ o[0] = a0 + (b0 - a0) * t;
951
+ o[1] = a1 + (b1 - a1) * t;
952
+ o[2] = a2 + (b2 - a2) * t;
953
+ }
954
+ return o;
955
+ }
956
+ /** One dab: lays the carried patch over the tip's footprint (`cov` over `rect`), then picks up. */
957
+ dab(medium, d, rect, cov) {
958
+ if (!this.loaded) {
959
+ this.load(medium, d);
960
+ return;
961
+ }
962
+ const { strength, length } = this.settings;
963
+ const lock = medium.target.lockTransparency;
964
+ const patch = this.patch;
965
+ const w = rect.width;
966
+ medium.reset();
967
+ for (let yy = 0; yy < rect.height; yy++) {
968
+ for (let xx = 0; xx < w; xx++) {
969
+ const cv = cov[yy * w + xx];
970
+ if (cv <= 0) continue;
971
+ const dx = rect.x + xx;
972
+ const dy = rect.y + yy;
973
+ let k = strength * cv * d.opacity * medium.selectionAt(dx, dy);
974
+ if (k <= 0) continue;
975
+ if (k > 1) k = 1;
976
+ const c = this.cell(d, dx + 0.5, dy + 0.5);
977
+ const ba = patch[c + 3];
978
+ const ri = medium.locate(dx, dy, false);
979
+ const a = ri < 0 ? 0 : medium.pixels[ri + 3] / 255;
980
+ if (lock && a <= 0) continue;
981
+ const px0 = ri >= 0 ? medium.pixels : null;
982
+ const o0 = px0 ? px0[ri] / 255 : 0;
983
+ const o1 = px0 ? px0[ri + 1] / 255 : 0;
984
+ const o2 = px0 ? px0[ri + 2] / 255 : 0;
985
+ if (a > 0 || ba > 0) {
986
+ const na = lock ? a : a + (ba - a) * k;
987
+ const share = lock ? k * ba : na > 0 ? k * ba / na : 0;
988
+ let r = o0;
989
+ let g = o1;
990
+ let b = o2;
991
+ if (a <= 0 || share >= 1) {
992
+ r = patch[c];
993
+ g = patch[c + 1];
994
+ b = patch[c + 2];
995
+ } else if (share > 0) {
996
+ const m = this.mix(o0, o1, o2, patch[c], patch[c + 1], patch[c + 2], share);
997
+ r = m[0];
998
+ g = m[1];
999
+ b = m[2];
1000
+ }
1001
+ const i = medium.locate(dx, dy, true);
1002
+ const out = medium.pixels;
1003
+ out[i] = Math.round(r * 255);
1004
+ out[i + 1] = Math.round(g * 255);
1005
+ out[i + 2] = Math.round(b * 255);
1006
+ out[i + 3] = Math.round(na * 255);
1007
+ }
1008
+ const pick = (1 - length) * Math.min(1, cv * d.opacity);
1009
+ if (pick > 0) {
1010
+ const pa = ba + (a - ba) * pick;
1011
+ const s = pa > 0 ? a * pick / pa : 0;
1012
+ if (s > 0) {
1013
+ const m = ba <= 0 || s >= 1 ? [o0, o1, o2] : this.mix(patch[c], patch[c + 1], patch[c + 2], o0, o1, o2, s);
1014
+ patch[c] = m[0];
1015
+ patch[c + 1] = m[1];
1016
+ patch[c + 2] = m[2];
1017
+ }
1018
+ patch[c + 3] = pa;
1019
+ }
1020
+ }
1021
+ }
1022
+ }
1023
+ };
1024
+
642
1025
  // src/paint/pigment-tools.ts
643
1026
  var HEX = /^#?([0-9a-f]{6})$/i;
644
1027
  function parseHex(value, index) {
@@ -654,7 +1037,7 @@ function createPigmentTools() {
654
1037
  const mixColors = {
655
1038
  name: "mix_colors",
656
1039
  title: "Mix colours like paint",
657
- description: "Mixes colours as paint would (Kubelka-Munk pigment mixing over reflectance spectra), so blue and yellow make green and white lightens a colour into a tint instead of greying it. Changes nothing. Returns the mixed colour as #rrggbb, the plain RGB average (rgbLerp) for comparison, and the normalised weights.",
1040
+ description: "Mixes colours as paint would (model pigment, the default: Kubelka-Munk mixing over reflectance spectra, so blue and yellow make green and white lightens a colour into a tint instead of greying it), or as light on screen (model rgb: the weighted RGB average). Changes nothing. Returns mixed (#rrggbb, by the chosen model), the other model's result for comparison (rgbLerp and pigment), the normalised weights and the model.",
658
1041
  inputSchema: {
659
1042
  type: "object",
660
1043
  properties: {
@@ -678,6 +1061,11 @@ function createPigmentTools() {
678
1061
  required: ["color", "weight"],
679
1062
  additionalProperties: false
680
1063
  }
1064
+ },
1065
+ model: {
1066
+ type: "string",
1067
+ enum: ["pigment", "rgb"],
1068
+ description: "pigment (default): mix like paint (Kubelka-Munk), as the pigment blend mode, Mixer Brush and mixing pad do; rgb: average the RGB values, as normal blending and rgb mixing brushes do (strength is ignored)."
681
1069
  }
682
1070
  },
683
1071
  required: ["colors"],
@@ -689,14 +1077,16 @@ function createPigmentTools() {
689
1077
  const rgbs = items.map((item, i) => parseHex(item.color, i));
690
1078
  const total = items.reduce((sum, item) => sum + item.weight, 0);
691
1079
  if (!(total > 0)) throw new Error("Every weight is 0; give at least one colour a weight above 0.");
692
- const pigments = rgbs.map((rgb, i) => createPigment(rgb, { strength: items[i].strength }));
1080
+ const pigments = rgbs.map((rgb2, i) => createPigment(rgb2, { strength: items[i].strength }));
693
1081
  const mixed = mixPigments(pigments, items.map((item) => item.weight));
694
- const lerp = [0, 1, 2].map((c) => rgbs.reduce((sum, rgb, i) => sum + rgb[c] * items[i].weight, 0) / total);
1082
+ const lerp = [0, 1, 2].map((c) => rgbs.reduce((sum, rgb2, i) => sum + rgb2[c] * items[i].weight, 0) / total);
1083
+ const rgb = args.model === "rgb";
695
1084
  return {
696
- mixed: toHex(mixed),
1085
+ mixed: toHex(rgb ? lerp : mixed),
697
1086
  rgbLerp: toHex(lerp),
1087
+ pigment: toHex(mixed),
698
1088
  weights: items.map((item) => item.weight / total),
699
- model: "kubelka-munk"
1089
+ model: rgb ? "rgb" : "kubelka-munk"
700
1090
  };
701
1091
  }
702
1092
  };
@@ -721,14 +1111,26 @@ export {
721
1111
  applyStrokeToTile,
722
1112
  coverageTileCoord,
723
1113
  MIXING_MODELS,
724
- DEFAULT_MIXER,
1114
+ EMPTY_MIXER_LOAD,
725
1115
  MIXER_PICKUP_RATE,
726
1116
  MIXER_DRAIN,
1117
+ exchangeRates,
1118
+ PaintExchange,
1119
+ TileMedium,
1120
+ DEFAULT_MIXER,
727
1121
  normalizeMixer,
728
- EMPTY_MIXER_LOAD,
1122
+ mixerRates,
729
1123
  loadMixer,
730
1124
  cleanMixer,
731
1125
  MixerStroke,
1126
+ DEFAULT_BLENDER,
1127
+ DEFAULT_SMUDGE,
1128
+ normalizeBlender,
1129
+ normalizeSmudge,
1130
+ blendRadius,
1131
+ createBlenderScratch,
1132
+ blenderDab,
1133
+ SmudgeTip,
732
1134
  createPigmentTools
733
1135
  };
734
- //# sourceMappingURL=chunk-3PHA3UKI.js.map
1136
+ //# sourceMappingURL=chunk-Z4BC6M74.js.map