@pixerate/editor 0.187.0 → 0.189.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 (29) hide show
  1. package/dist/{chunk-X2KQQZBT.cjs → chunk-ED4L4UD6.cjs} +389 -180
  2. package/dist/{chunk-FW7N64XK.js → chunk-ZEHTUR7Z.js} +229 -20
  3. package/dist/image-editor/controller.d.cts +20 -3
  4. package/dist/image-editor/controller.d.ts +20 -3
  5. package/dist/image-editor/index.cjs +1618 -1467
  6. package/dist/image-editor/index.js +623 -472
  7. package/dist/image-editor/layers/paint-ui.d.cts +2 -1
  8. package/dist/image-editor/layers/paint-ui.d.ts +2 -1
  9. package/dist/image-editor/layers/panel.d.cts +3 -2
  10. package/dist/image-editor/layers/panel.d.ts +3 -2
  11. package/dist/image-editor/renderer.d.cts +6 -0
  12. package/dist/image-editor/renderer.d.ts +6 -0
  13. package/dist/image-editor/selection/gesture.d.cts +10 -0
  14. package/dist/image-editor/selection/gesture.d.ts +10 -0
  15. package/dist/image-editor/selection/layer-mask.d.cts +6 -0
  16. package/dist/image-editor/selection/layer-mask.d.ts +6 -0
  17. package/dist/image-editor/selection/mask.d.cts +20 -1
  18. package/dist/image-editor/selection/mask.d.ts +20 -1
  19. package/dist/image-editor/selection/shapes.d.cts +8 -0
  20. package/dist/image-editor/selection/shapes.d.ts +8 -0
  21. package/dist/image-editor/selection/tool-options.d.cts +1 -1
  22. package/dist/image-editor/selection/tool-options.d.ts +1 -1
  23. package/dist/image-editor/types.d.cts +8 -0
  24. package/dist/image-editor/types.d.ts +8 -0
  25. package/dist/paint-editor/index.cjs +6 -6
  26. package/dist/paint-editor/index.js +1 -1
  27. package/package.json +1 -1
  28. package/skills/pixerate-image-editor/SKILL.md +1 -0
  29. package/skills/pixerate-image-selection-masking/SKILL.md +4 -1
@@ -114,15 +114,19 @@ import {
114
114
  parseDataUrl,
115
115
  pngDataUrl,
116
116
  readDeepRegion,
117
+ readPixel,
117
118
  readRegion,
118
119
  setLum,
119
120
  surfaceDepth,
120
121
  surfaceFromDeepPixels,
121
122
  surfaceFromPixels,
123
+ tileCoord,
122
124
  tileKey,
123
125
  tilesInRect,
124
126
  translateSurface,
125
- writeDeepRegion
127
+ uniformTile,
128
+ writeDeepRegion,
129
+ writeRegion
126
130
  } from "./chunk-H367YTXI.js";
127
131
  import {
128
132
  ZlibError,
@@ -750,15 +754,159 @@ function computeSelectionBounds(width, height, data) {
750
754
  if (y0 < 0) return null;
751
755
  return { x: x0, y: y0, width: x1 - x0 + 1, height: y1 - y0 + 1 };
752
756
  }
757
+ var TILE_PIXELS = TILE * TILE;
758
+ var MATERIALIZED_LIMIT = 4;
759
+ var materialized = /* @__PURE__ */ new Map();
760
+ var lastRead = null;
761
+ var lastData = null;
762
+ var TiledSelectionMask = class {
763
+ constructor(width, height, surface, bounds, data) {
764
+ this.width = width;
765
+ this.height = height;
766
+ this.surface = surface;
767
+ this.cachedBounds = bounds;
768
+ if (data) this.remember(data);
769
+ }
770
+ width;
771
+ height;
772
+ surface;
773
+ cachedBounds;
774
+ weak = null;
775
+ get bounds() {
776
+ if (this.cachedBounds === void 0) this.cachedBounds = contentBounds(this.surface);
777
+ return this.cachedBounds;
778
+ }
779
+ get data() {
780
+ if (lastRead === this) return lastData;
781
+ let d = materialized.get(this);
782
+ if (d) {
783
+ materialized.delete(this);
784
+ materialized.set(this, d);
785
+ lastRead = this;
786
+ lastData = d;
787
+ return d;
788
+ }
789
+ d = this.weak?.deref();
790
+ if (!d) {
791
+ const all = readRegion(this.surface, { x: 0, y: 0, width: this.width, height: this.height });
792
+ d = new Uint8Array(all.buffer, all.byteOffset, all.length);
793
+ }
794
+ this.remember(d);
795
+ return d;
796
+ }
797
+ remember(d) {
798
+ lastRead = this;
799
+ lastData = d;
800
+ this.weak = typeof WeakRef === "function" ? new WeakRef(d) : null;
801
+ materialized.set(this, d);
802
+ while (materialized.size > MATERIALIZED_LIMIT) materialized.delete(materialized.keys().next().value);
803
+ }
804
+ };
805
+ function tileGrid(width, height) {
806
+ return { cols: Math.ceil(width / TILE), rows: Math.ceil(height / TILE) };
807
+ }
808
+ function insideTile(tx, ty, width, height) {
809
+ return (tx + 1) * TILE <= width && (ty + 1) * TILE <= height;
810
+ }
811
+ function surfaceFromCoverage(width, height, data) {
812
+ const tiles = /* @__PURE__ */ new Map();
813
+ const { cols, rows } = tileGrid(width, height);
814
+ for (let ty = 0; ty < rows; ty++) {
815
+ for (let tx = 0; tx < cols; tx++) {
816
+ const ox = tx * TILE;
817
+ const oy = ty * TILE;
818
+ const w = Math.min(TILE, width - ox);
819
+ const h = Math.min(TILE, height - oy);
820
+ let lo = 255;
821
+ let hi = 0;
822
+ for (let y = 0; y < h && !(hi > 0 && lo < hi); y++) {
823
+ const row = (oy + y) * width + ox;
824
+ for (let x = 0; x < w; x++) {
825
+ const v = data[row + x];
826
+ if (v < lo) lo = v;
827
+ if (v > hi) hi = v;
828
+ }
829
+ }
830
+ if (hi === 0) continue;
831
+ if (lo === hi && w === TILE && h === TILE) {
832
+ tiles.set(tileKey(tx, ty), uniformTile(1, hi));
833
+ continue;
834
+ }
835
+ const t = new Uint8ClampedArray(TILE_PIXELS);
836
+ for (let y = 0; y < h; y++) {
837
+ const row = (oy + y) * width + ox;
838
+ for (let x = 0; x < w; x++) t[y * TILE + x] = data[row + x];
839
+ }
840
+ tiles.set(tileKey(tx, ty), { data: t, uniform: null });
841
+ }
842
+ }
843
+ return new TiledSurface(1, tiles, 0);
844
+ }
845
+ function clipTile(tile, w, h) {
846
+ const d = tile.data;
847
+ let outside = false;
848
+ for (let y = 0; y < TILE && !outside; y++) {
849
+ for (let x = y < h ? w : 0; x < TILE; x++) {
850
+ if (d[y * TILE + x] !== 0) {
851
+ outside = true;
852
+ break;
853
+ }
854
+ }
855
+ }
856
+ if (!outside) return tile;
857
+ const t = new Uint8ClampedArray(TILE_PIXELS);
858
+ for (let y = 0; y < h; y++) t.set(d.subarray(y * TILE, y * TILE + w), y * TILE);
859
+ return finishTile(t, 1, 0);
860
+ }
861
+ function selectionFromTiles(width, height, surface) {
862
+ checkSize(width, height);
863
+ if (surface.channels !== 1) throw new Error("A selection needs a 1-channel surface");
864
+ if (surface.defaultValue !== 0) {
865
+ const all = readRegion(surface, { x: 0, y: 0, width, height });
866
+ return createSelectionMask(width, height, new Uint8Array(all.buffer, all.byteOffset, all.length));
867
+ }
868
+ const { cols, rows } = tileGrid(width, height);
869
+ let tiles = null;
870
+ for (const [key, tile] of surface.tiles) {
871
+ const { tx, ty } = tileCoord(key);
872
+ let next = tile;
873
+ if (tx < 0 || ty < 0 || tx >= cols || ty >= rows) next = null;
874
+ else if (!insideTile(tx, ty, width, height)) next = clipTile(tile, Math.min(TILE, width - tx * TILE), Math.min(TILE, height - ty * TILE));
875
+ if (next === tile) continue;
876
+ tiles ??= new Map(surface.tiles);
877
+ if (next) tiles.set(key, next);
878
+ else tiles.delete(key);
879
+ }
880
+ return new TiledSelectionMask(width, height, tiles ? new TiledSurface(1, tiles, 0) : surface);
881
+ }
753
882
  function createSelectionMask(width, height, data) {
754
883
  checkSize(width, height);
755
- const bytes = data ?? new Uint8Array(width * height);
756
- if (bytes.length !== width * height) throw new Error("Selection data does not match its size");
757
- return { width, height, data: bytes, bounds: data ? computeSelectionBounds(width, height, bytes) : null };
884
+ if (!data) return new TiledSelectionMask(width, height, new TiledSurface(1, /* @__PURE__ */ new Map(), 0), null);
885
+ if (data.length !== width * height) throw new Error("Selection data does not match its size");
886
+ return new TiledSelectionMask(width, height, surfaceFromCoverage(width, height, data), void 0, data);
887
+ }
888
+ function selectionFromRegion(width, height, rect, data) {
889
+ checkSize(width, height);
890
+ if (rect.x < 0 || rect.y < 0 || rect.x + rect.width > width || rect.y + rect.height > height) throw new Error("The region lies outside the selection");
891
+ return new TiledSelectionMask(width, height, writeRegion(new TiledSurface(1, /* @__PURE__ */ new Map(), 0), rect, data));
758
892
  }
759
893
  function fullSelection(width, height) {
760
894
  checkSize(width, height);
761
- return { width, height, data: new Uint8Array(width * height).fill(255), bounds: { x: 0, y: 0, width, height } };
895
+ const tiles = /* @__PURE__ */ new Map();
896
+ const { cols, rows } = tileGrid(width, height);
897
+ for (let ty = 0; ty < rows; ty++) {
898
+ for (let tx = 0; tx < cols; tx++) {
899
+ if (insideTile(tx, ty, width, height)) {
900
+ tiles.set(tileKey(tx, ty), uniformTile(1, 255));
901
+ continue;
902
+ }
903
+ const t = new Uint8ClampedArray(TILE_PIXELS);
904
+ const w = Math.min(TILE, width - tx * TILE);
905
+ for (let y = 0; y < Math.min(TILE, height - ty * TILE); y++) t.fill(255, y * TILE, y * TILE + w);
906
+ tiles.set(tileKey(tx, ty), { data: t, uniform: null });
907
+ }
908
+ }
909
+ return new TiledSelectionMask(width, height, new TiledSurface(1, tiles, 0), { x: 0, y: 0, width, height });
762
910
  }
763
911
  function isSelectionEmpty(mask) {
764
912
  return !mask || mask.bounds === null;
@@ -767,7 +915,7 @@ function selectionValueAt(mask, x, y) {
767
915
  const xi = Math.floor(x);
768
916
  const yi = Math.floor(y);
769
917
  if (xi < 0 || yi < 0 || xi >= mask.width || yi >= mask.height) return 0;
770
- return mask.data[yi * mask.width + xi] / 255;
918
+ return readPixel(mask.surface, xi, yi)[0] / 255;
771
919
  }
772
920
  function coverageToByte(v) {
773
921
  if (!(v > 0)) return 0;
@@ -788,10 +936,13 @@ function selectionToCoverage(mask) {
788
936
  function selectionArea(mask) {
789
937
  if (!mask || !mask.bounds) return 0;
790
938
  let sum = 0;
791
- const { x, y, width, height } = mask.bounds;
792
- for (let yy = y; yy < y + height; yy++) {
793
- const row = yy * mask.width;
794
- for (let xx = x; xx < x + width; xx++) sum += mask.data[row + xx];
939
+ for (const tile of mask.surface.tiles.values()) {
940
+ if (tile.uniform !== null) {
941
+ sum += tile.uniform * TILE_PIXELS;
942
+ continue;
943
+ }
944
+ const d = tile.data;
945
+ for (let i = 0; i < d.length; i++) sum += d[i];
795
946
  }
796
947
  return sum / 255;
797
948
  }
@@ -803,20 +954,68 @@ function combineSelections(prev, next, mode) {
803
954
  return null;
804
955
  } else {
805
956
  if (prev.width !== next.width || prev.height !== next.height) throw new Error("Selections differ in size");
806
- const a = prev.data;
807
- const b = next.data;
808
- const out = new Uint8Array(a.length);
809
- if (mode === "add") for (let i = 0; i < a.length; i++) out[i] = a[i] > b[i] ? a[i] : b[i];
810
- else if (mode === "subtract") for (let i = 0; i < a.length; i++) out[i] = a[i] > b[i] ? a[i] - b[i] : 0;
811
- else for (let i = 0; i < a.length; i++) out[i] = a[i] < b[i] ? a[i] : b[i];
812
- result = createSelectionMask(prev.width, prev.height, out);
957
+ result = new TiledSelectionMask(prev.width, prev.height, combineSurfaces(prev.surface, next.surface, mode));
813
958
  }
814
959
  return result.bounds ? result : null;
815
960
  }
961
+ var FULL = 255;
962
+ function combineTile(a, b, mode) {
963
+ if (mode === "add") {
964
+ if (!b || a?.uniform === FULL) return a;
965
+ if (!a || b.uniform === FULL) return b;
966
+ } else if (mode === "subtract") {
967
+ if (!a || !b) return a;
968
+ if (b.uniform === FULL) return void 0;
969
+ } else {
970
+ if (!a || !b) return void 0;
971
+ if (a.uniform === FULL) return b;
972
+ if (b.uniform === FULL) return a;
973
+ }
974
+ const x = a.data;
975
+ const y = b.data;
976
+ const out = new Uint8ClampedArray(TILE_PIXELS);
977
+ if (mode === "add") for (let i = 0; i < out.length; i++) out[i] = x[i] > y[i] ? x[i] : y[i];
978
+ else if (mode === "subtract") for (let i = 0; i < out.length; i++) out[i] = x[i] > y[i] ? x[i] - y[i] : 0;
979
+ else for (let i = 0; i < out.length; i++) out[i] = x[i] < y[i] ? x[i] : y[i];
980
+ return finishTile(out, 1, 0) ?? void 0;
981
+ }
982
+ function combineSurfaces(a, b, mode) {
983
+ const tiles = /* @__PURE__ */ new Map();
984
+ for (const key of /* @__PURE__ */ new Set([...a.tiles.keys(), ...b.tiles.keys()])) {
985
+ const tile = combineTile(a.tiles.get(key), b.tiles.get(key), mode);
986
+ if (tile) tiles.set(key, tile);
987
+ }
988
+ return new TiledSurface(1, tiles, 0);
989
+ }
816
990
  function invertSelection(mask) {
817
- const out = new Uint8Array(mask.data.length);
818
- for (let i = 0; i < out.length; i++) out[i] = 255 - mask.data[i];
819
- return createSelectionMask(mask.width, mask.height, out);
991
+ const { width, height } = mask;
992
+ const tiles = /* @__PURE__ */ new Map();
993
+ const { cols, rows } = tileGrid(width, height);
994
+ for (let ty = 0; ty < rows; ty++) {
995
+ for (let tx = 0; tx < cols; tx++) {
996
+ const src = mask.surface.getTile(tx, ty);
997
+ const key = tileKey(tx, ty);
998
+ if (insideTile(tx, ty, width, height)) {
999
+ if (!src) tiles.set(key, uniformTile(1, FULL));
1000
+ else if (src.uniform !== null) {
1001
+ if (src.uniform !== FULL) tiles.set(key, uniformTile(1, FULL - src.uniform));
1002
+ } else {
1003
+ const out2 = new Uint8ClampedArray(TILE_PIXELS);
1004
+ for (let i = 0; i < out2.length; i++) out2[i] = FULL - src.data[i];
1005
+ const t2 = finishTile(out2, 1, 0);
1006
+ if (t2) tiles.set(key, t2);
1007
+ }
1008
+ continue;
1009
+ }
1010
+ const w = Math.min(TILE, width - tx * TILE);
1011
+ const h = Math.min(TILE, height - ty * TILE);
1012
+ const out = new Uint8ClampedArray(TILE_PIXELS);
1013
+ for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) out[y * TILE + x] = FULL - (src ? src.data[y * TILE + x] : 0);
1014
+ const t = finishTile(out, 1, 0);
1015
+ if (t) tiles.set(key, t);
1016
+ }
1017
+ }
1018
+ return new TiledSelectionMask(width, height, new TiledSurface(1, tiles, 0));
820
1019
  }
821
1020
  function resizeSelection(mask, width, height) {
822
1021
  if (mask.width === width && mask.height === height) return mask;
@@ -1222,6 +1421,13 @@ function modifySelection(mask, op, radius) {
1222
1421
  }
1223
1422
 
1224
1423
  // src/image-editor/selection/layer-mask.ts
1424
+ function layerMaskSelection(layer, width, height) {
1425
+ if (!layer.mask || layer.mask.source) return null;
1426
+ const ox = layer.type === "pixel" ? layer.offset.x : 0;
1427
+ const oy = layer.type === "pixel" ? layer.offset.y : 0;
1428
+ const data = readRegion(layer.mask.surface, { x: -ox, y: -oy, width, height });
1429
+ return createSelectionMask(width, height, new Uint8Array(data.buffer, data.byteOffset, data.length));
1430
+ }
1225
1431
  var MAX_SIDE2 = 1 << 15;
1226
1432
  function normalizeLayerMask(raw) {
1227
1433
  if (!raw || typeof raw !== "object") return null;
@@ -11640,7 +11846,9 @@ export {
11640
11846
  isSelectionMode,
11641
11847
  parseSelectionMode,
11642
11848
  computeSelectionBounds,
11849
+ selectionFromTiles,
11643
11850
  createSelectionMask,
11851
+ selectionFromRegion,
11644
11852
  fullSelection,
11645
11853
  isSelectionEmpty,
11646
11854
  selectionValueAt,
@@ -11669,6 +11877,7 @@ export {
11669
11877
  shiftCoverage,
11670
11878
  SELECTION_MODIFY_OPS,
11671
11879
  modifySelection,
11880
+ layerMaskSelection,
11672
11881
  normalizeLayerMask,
11673
11882
  layerMaskFromSelection,
11674
11883
  updateLayerMaskProps,
@@ -113,6 +113,8 @@ export interface SelectionCommandOptions {
113
113
  }
114
114
  /** How the canvas shows the selection: marching ants, a Quick Mask tint, or not at all. */
115
115
  export type SelectionDisplay = 'ants' | 'quickMask' | 'none';
116
+ /** How a layer mask is shown on the canvas: alone in grey, or as a red tint over the image. */
117
+ export type MaskViewMode = 'gray' | 'overlay';
116
118
  /** What `exportMaskImage` writes: the selection, an alpha channel or a layer's painted mask. */
117
119
  export type MaskImageSource = 'selection' | {
118
120
  channelId: string;
@@ -724,8 +726,8 @@ export declare class ImageEditorController {
724
726
  private asOneStep;
725
727
  private recordHistory;
726
728
  /**
727
- * Bytes of pixel tiles held only by undo / redo steps (not by the current state). The oldest
728
- * undo steps are dropped while it exceeds `maxHistoryBytes`.
729
+ * Bytes of pixel, selection and channel tiles held only by undo / redo steps (not by the
730
+ * current state). The oldest undo steps are dropped while it exceeds `maxHistoryBytes`.
729
731
  */
730
732
  get historyBytes(): number;
731
733
  private trimHistoryBytes;
@@ -1506,6 +1508,21 @@ export declare class ImageEditorController {
1506
1508
  * it (the selection still applies). View state: no undo step, not saved.
1507
1509
  */
1508
1510
  setSelectionDisplay(display: SelectionDisplay): void;
1511
+ /** The layer mask shown on the canvas, or `null` (see `setMaskView`). */
1512
+ getMaskView(): {
1513
+ layerId: string;
1514
+ mode: MaskViewMode;
1515
+ } | null;
1516
+ /**
1517
+ * Shows a layer's mask on the canvas: `gray` alone in grey (Alt-click the mask thumbnail),
1518
+ * `overlay` as a red tint over what it hides (the \ key), `off` back to the image. The layer
1519
+ * defaults to the active one. View state: no undo step, not saved. Throws when the layer has
1520
+ * no painted mask.
1521
+ */
1522
+ setMaskView(mode: MaskViewMode | 'off', layerId?: string): {
1523
+ layerId: string;
1524
+ mode: MaskViewMode;
1525
+ } | null;
1509
1526
  /** The alpha channels (saved selections), in panel order. */
1510
1527
  getChannels(): readonly AlphaChannel[];
1511
1528
  getChannel(id: string): AlphaChannel | null;
@@ -1559,7 +1576,7 @@ export declare class ImageEditorController {
1559
1576
  setQuickMaskMode(on: boolean): void;
1560
1577
  /** The selection as a 1-channel surface to paint in Quick Mask mode. */
1561
1578
  private quickMaskSurface;
1562
- /** The selection a painted Quick Mask surface stands for (`rects` changed since `base`). */
1579
+ /** The selection a painted Quick Mask surface stands for (it shares the surface's tiles). */
1563
1580
  private selectionFromSurface;
1564
1581
  private maskFromSelection;
1565
1582
  /**
@@ -113,6 +113,8 @@ export interface SelectionCommandOptions {
113
113
  }
114
114
  /** How the canvas shows the selection: marching ants, a Quick Mask tint, or not at all. */
115
115
  export type SelectionDisplay = 'ants' | 'quickMask' | 'none';
116
+ /** How a layer mask is shown on the canvas: alone in grey, or as a red tint over the image. */
117
+ export type MaskViewMode = 'gray' | 'overlay';
116
118
  /** What `exportMaskImage` writes: the selection, an alpha channel or a layer's painted mask. */
117
119
  export type MaskImageSource = 'selection' | {
118
120
  channelId: string;
@@ -724,8 +726,8 @@ export declare class ImageEditorController {
724
726
  private asOneStep;
725
727
  private recordHistory;
726
728
  /**
727
- * Bytes of pixel tiles held only by undo / redo steps (not by the current state). The oldest
728
- * undo steps are dropped while it exceeds `maxHistoryBytes`.
729
+ * Bytes of pixel, selection and channel tiles held only by undo / redo steps (not by the
730
+ * current state). The oldest undo steps are dropped while it exceeds `maxHistoryBytes`.
729
731
  */
730
732
  get historyBytes(): number;
731
733
  private trimHistoryBytes;
@@ -1506,6 +1508,21 @@ export declare class ImageEditorController {
1506
1508
  * it (the selection still applies). View state: no undo step, not saved.
1507
1509
  */
1508
1510
  setSelectionDisplay(display: SelectionDisplay): void;
1511
+ /** The layer mask shown on the canvas, or `null` (see `setMaskView`). */
1512
+ getMaskView(): {
1513
+ layerId: string;
1514
+ mode: MaskViewMode;
1515
+ } | null;
1516
+ /**
1517
+ * Shows a layer's mask on the canvas: `gray` alone in grey (Alt-click the mask thumbnail),
1518
+ * `overlay` as a red tint over what it hides (the \ key), `off` back to the image. The layer
1519
+ * defaults to the active one. View state: no undo step, not saved. Throws when the layer has
1520
+ * no painted mask.
1521
+ */
1522
+ setMaskView(mode: MaskViewMode | 'off', layerId?: string): {
1523
+ layerId: string;
1524
+ mode: MaskViewMode;
1525
+ } | null;
1509
1526
  /** The alpha channels (saved selections), in panel order. */
1510
1527
  getChannels(): readonly AlphaChannel[];
1511
1528
  getChannel(id: string): AlphaChannel | null;
@@ -1559,7 +1576,7 @@ export declare class ImageEditorController {
1559
1576
  setQuickMaskMode(on: boolean): void;
1560
1577
  /** The selection as a 1-channel surface to paint in Quick Mask mode. */
1561
1578
  private quickMaskSurface;
1562
- /** The selection a painted Quick Mask surface stands for (`rects` changed since `base`). */
1579
+ /** The selection a painted Quick Mask surface stands for (it shares the surface's tiles). */
1563
1580
  private selectionFromSurface;
1564
1581
  private maskFromSelection;
1565
1582
  /**