@pixerate/editor 0.258.0 → 0.260.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/agent/index.cjs +2 -2
- package/dist/agent/index.js +1 -1
- package/dist/animation-editor/index.cjs +61 -59
- package/dist/animation-editor/index.js +5 -3
- package/dist/brushes/contract.d.cts +24 -0
- package/dist/brushes/contract.d.ts +24 -0
- package/dist/brushes/index.cjs +146 -31
- package/dist/brushes/index.d.cts +1 -0
- package/dist/brushes/index.d.ts +1 -0
- package/dist/brushes/index.js +121 -6
- package/dist/{brushstrokeRender-JI4W6F2G.js → brushstrokeRender-25Q5BEY6.js} +17 -7
- package/dist/{brushstrokeRender-MQ6PSDM5.cjs → brushstrokeRender-N6UPZSQS.cjs} +31 -21
- package/dist/canvas/brushTypes.d.cts +5 -0
- package/dist/canvas/brushTypes.d.ts +5 -0
- package/dist/canvas/drawController.d.cts +15 -1
- package/dist/canvas/drawController.d.ts +15 -1
- package/dist/canvas/index.cjs +136 -102
- package/dist/canvas/index.js +35 -1
- package/dist/{chunk-3PPJRMIG.js → chunk-3O47TPGX.js} +197 -18
- package/dist/{chunk-5Q5MLXAD.cjs → chunk-3TQW5MPW.cjs} +23 -23
- package/dist/{chunk-JBBJE76O.cjs → chunk-3ZRNK3RO.cjs} +1138 -85
- package/dist/{chunk-YSGW7VV4.cjs → chunk-43NT242D.cjs} +1401 -228
- package/dist/{chunk-SMTUQWGY.js → chunk-5IW65VQ6.js} +1 -1
- package/dist/{chunk-YQ4PQTBB.js → chunk-6NZL24W4.js} +9 -1
- package/dist/{chunk-O3ZVZSNJ.cjs → chunk-7AUQ2UJ3.cjs} +137 -129
- package/dist/{chunk-4M4E3N6X.js → chunk-CQVTGBQI.js} +1 -1
- package/dist/{chunk-SSFMWJDK.js → chunk-D4TE5JNW.js} +1192 -19
- package/dist/{chunk-AORZ5M5C.js → chunk-EHALWBBD.js} +1 -1
- package/dist/{chunk-6WCQ35GI.cjs → chunk-NHA26O5X.cjs} +2 -2
- package/dist/{chunk-DPYAO5U6.cjs → chunk-PVJMTLPJ.cjs} +197 -18
- package/dist/{chunk-MQGQTKVW.cjs → chunk-PYTJBVW5.cjs} +29 -29
- package/dist/{chunk-IXQZHCOF.js → chunk-QAR2N7HZ.js} +1 -1
- package/dist/{chunk-5P4JEOIV.js → chunk-TMDDMJCR.js} +2 -2
- package/dist/{chunk-RSIKDWOP.cjs → chunk-TZOX7KWV.cjs} +15 -15
- package/dist/{chunk-IPUTIUGQ.cjs → chunk-UVZ7RQOE.cjs} +1 -1
- package/dist/{chunk-A7GJ6BXI.js → chunk-WJ4WQJCC.js} +1062 -9
- package/dist/{chunk-ICTPLVSR.js → chunk-X2EXYY4J.js} +1 -1
- package/dist/{chunk-WQCNC4DK.js → chunk-XC6DPRYI.js} +695 -4
- package/dist/{chunk-KBYB7OWB.cjs → chunk-YHDHFJUC.cjs} +776 -85
- package/dist/{chunk-P3PZ63M3.cjs → chunk-YLUIFXSL.cjs} +35 -35
- package/dist/commands/index.cjs +2 -2
- package/dist/commands/index.js +1 -1
- package/dist/image-editor/index.cjs +1385 -2484
- package/dist/image-editor/index.js +139 -1238
- package/dist/image-editor/selection-worker.cjs +3 -4
- package/dist/image-editor/selection-worker.js +1 -2
- package/dist/paint/index.cjs +3 -5
- package/dist/paint/index.js +40 -42
- package/dist/paint-editor/contract.d.cts +29 -0
- package/dist/paint-editor/contract.d.ts +29 -0
- package/dist/paint-editor/dab-stroke.d.cts +5 -0
- package/dist/paint-editor/dab-stroke.d.ts +5 -0
- package/dist/paint-editor/index.cjs +402 -258
- package/dist/paint-editor/index.d.cts +1 -0
- package/dist/paint-editor/index.d.ts +1 -0
- package/dist/paint-editor/index.js +197 -53
- package/dist/paint-editor/stamp-tip.d.cts +17 -0
- package/dist/paint-editor/stamp-tip.d.ts +17 -0
- package/dist/paint-editor/ui.d.cts +21 -1
- package/dist/paint-editor/ui.d.ts +21 -1
- package/dist/raster/index.cjs +2 -4
- package/dist/raster/index.js +6 -8
- package/dist/vector/brushstroke.d.cts +6 -0
- package/dist/vector/brushstroke.d.ts +6 -0
- package/dist/vector/controller.d.cts +11 -2
- package/dist/vector/controller.d.ts +11 -2
- package/dist/vector/index.cjs +990 -973
- package/dist/vector/index.js +28 -11
- package/package.json +1 -1
- package/skills/pixerate-brushes/SKILL.md +1 -1
- package/skills/pixerate-canvas/SKILL.md +3 -1
- package/skills/pixerate-vector/SKILL.md +2 -2
- package/dist/chunk-2VJAVXLI.js +0 -192
- package/dist/chunk-62Z5WLRA.js +0 -700
- package/dist/chunk-BOHDJJ2O.cjs +0 -192
- package/dist/chunk-NKGB64CW.cjs +0 -700
- package/dist/chunk-SS53NNZO.cjs +0 -1066
- package/dist/chunk-ZNA3IQZF.js +0 -1066
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
2
|
boxMean,
|
|
3
|
+
combineSelections,
|
|
3
4
|
createSelectionMask,
|
|
4
5
|
decodeSelectionRle,
|
|
5
6
|
decontaminateColors,
|
|
7
|
+
ellipsePoints,
|
|
6
8
|
encodeSelectionRle,
|
|
7
9
|
guidedFilterColor,
|
|
8
10
|
hashMaskContent,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from "./chunk-
|
|
11
|
+
normalizeRect,
|
|
12
|
+
resizeSelection,
|
|
13
|
+
selectionArea,
|
|
14
|
+
serializeMask
|
|
15
|
+
} from "./chunk-WJ4WQJCC.js";
|
|
14
16
|
import {
|
|
15
17
|
DEFAULT_GLOBAL_LIGHT,
|
|
16
18
|
DeepCompositor,
|
|
@@ -48,7 +50,7 @@ import {
|
|
|
48
50
|
validateEffectGradient,
|
|
49
51
|
validateLayerEffects,
|
|
50
52
|
warpSurface
|
|
51
|
-
} from "./chunk-
|
|
53
|
+
} from "./chunk-3O47TPGX.js";
|
|
52
54
|
import {
|
|
53
55
|
affineFromHomography,
|
|
54
56
|
applyHomography,
|
|
@@ -106,7 +108,7 @@ import {
|
|
|
106
108
|
transformTextRect,
|
|
107
109
|
translateMatrix,
|
|
108
110
|
warpPresetMesh
|
|
109
|
-
} from "./chunk-
|
|
111
|
+
} from "./chunk-6NZL24W4.js";
|
|
110
112
|
import {
|
|
111
113
|
bezierPathBounds,
|
|
112
114
|
outlineStroke,
|
|
@@ -115,6 +117,12 @@ import {
|
|
|
115
117
|
strokeOutlineRings,
|
|
116
118
|
transformBezierPath
|
|
117
119
|
} from "./chunk-WEIK3UYB.js";
|
|
120
|
+
import {
|
|
121
|
+
parseCssColor
|
|
122
|
+
} from "./chunk-3CI7FRMK.js";
|
|
123
|
+
import {
|
|
124
|
+
MAX_BRUSH_SIZE
|
|
125
|
+
} from "./chunk-GCVFEPJG.js";
|
|
118
126
|
import {
|
|
119
127
|
base64ToBytes,
|
|
120
128
|
bytesToBase64,
|
|
@@ -4547,6 +4555,312 @@ function isLegacyShape(stack) {
|
|
|
4547
4555
|
return true;
|
|
4548
4556
|
}
|
|
4549
4557
|
|
|
4558
|
+
// src/image-editor/selection/magnetic.ts
|
|
4559
|
+
var BLOCK = 64;
|
|
4560
|
+
var W_ZERO = 0.3;
|
|
4561
|
+
var W_GRADIENT = 0.4;
|
|
4562
|
+
var W_DIRECTION = 0.3;
|
|
4563
|
+
var EdgeMap = class {
|
|
4564
|
+
width;
|
|
4565
|
+
height;
|
|
4566
|
+
contrast;
|
|
4567
|
+
luma;
|
|
4568
|
+
blocks = /* @__PURE__ */ new Map();
|
|
4569
|
+
constructor(pixels, options = {}) {
|
|
4570
|
+
const { width, height, data } = pixels;
|
|
4571
|
+
if (!(width >= 1 && height >= 1) || data.length < width * height * 4) throw new Error("Invalid pixel buffer for edge detection.");
|
|
4572
|
+
this.width = width;
|
|
4573
|
+
this.height = height;
|
|
4574
|
+
this.contrast = Math.min(1, Math.max(0, options.contrast ?? 0.1));
|
|
4575
|
+
const luma = new Uint8Array(width * height);
|
|
4576
|
+
for (let i = 0, p = 0; i < luma.length; i++, p += 4) {
|
|
4577
|
+
const a = data[p + 3] / 255;
|
|
4578
|
+
const y = 0.299 * data[p] + 0.587 * data[p + 1] + 0.114 * data[p + 2];
|
|
4579
|
+
luma[i] = Math.round(y * a + 255 * (1 - a));
|
|
4580
|
+
}
|
|
4581
|
+
this.luma = luma;
|
|
4582
|
+
}
|
|
4583
|
+
/** Edge strength at (x, y) in whole pixels, 0..1 (0 outside the image or below the contrast). */
|
|
4584
|
+
strength(x, y) {
|
|
4585
|
+
if (x < 0 || y < 0 || x >= this.width || y >= this.height) return 0;
|
|
4586
|
+
const b = this.block(x, y);
|
|
4587
|
+
return b.strength[y % BLOCK * BLOCK + x % BLOCK];
|
|
4588
|
+
}
|
|
4589
|
+
block(x, y) {
|
|
4590
|
+
const bx = Math.floor(x / BLOCK);
|
|
4591
|
+
const by = Math.floor(y / BLOCK);
|
|
4592
|
+
const key = by * 65536 + bx;
|
|
4593
|
+
let b = this.blocks.get(key);
|
|
4594
|
+
if (!b) {
|
|
4595
|
+
b = this.computeBlock(bx * BLOCK, by * BLOCK);
|
|
4596
|
+
this.blocks.set(key, b);
|
|
4597
|
+
}
|
|
4598
|
+
return b;
|
|
4599
|
+
}
|
|
4600
|
+
computeBlock(x0, y0) {
|
|
4601
|
+
const { width: W, height: H, luma } = this;
|
|
4602
|
+
const M = 2;
|
|
4603
|
+
const S = BLOCK + 2 * M;
|
|
4604
|
+
const smooth = new Float32Array(S * S);
|
|
4605
|
+
const at = (x, y) => luma[Math.min(H - 1, Math.max(0, y)) * W + Math.min(W - 1, Math.max(0, x))];
|
|
4606
|
+
for (let j = 0; j < S; j++) {
|
|
4607
|
+
const y = y0 - M + j;
|
|
4608
|
+
for (let i = 0; i < S; i++) {
|
|
4609
|
+
const x = x0 - M + i;
|
|
4610
|
+
const v = at(x - 1, y - 1) + 2 * at(x, y - 1) + at(x + 1, y - 1) + 2 * at(x - 1, y) + 4 * at(x, y) + 2 * at(x + 1, y) + at(x - 1, y + 1) + 2 * at(x, y + 1) + at(x + 1, y + 1);
|
|
4611
|
+
smooth[j * S + i] = v / (16 * 255);
|
|
4612
|
+
}
|
|
4613
|
+
}
|
|
4614
|
+
const lap = new Float32Array(S * S);
|
|
4615
|
+
for (let j = 1; j < S - 1; j++) {
|
|
4616
|
+
for (let i = 1; i < S - 1; i++) {
|
|
4617
|
+
const k = j * S + i;
|
|
4618
|
+
lap[k] = smooth[k - 1] + smooth[k + 1] + smooth[k - S] + smooth[k + S] - 4 * smooth[k];
|
|
4619
|
+
}
|
|
4620
|
+
}
|
|
4621
|
+
const n = BLOCK * BLOCK;
|
|
4622
|
+
const strength = new Float32Array(n);
|
|
4623
|
+
const dirX = new Float32Array(n);
|
|
4624
|
+
const dirY = new Float32Array(n);
|
|
4625
|
+
const zero = new Uint8Array(n);
|
|
4626
|
+
const c = this.contrast;
|
|
4627
|
+
for (let j = 0; j < BLOCK; j++) {
|
|
4628
|
+
if (y0 + j >= H) break;
|
|
4629
|
+
for (let i = 0; i < BLOCK; i++) {
|
|
4630
|
+
if (x0 + i >= W) break;
|
|
4631
|
+
const k = (j + M) * S + (i + M);
|
|
4632
|
+
const gx = (smooth[k + 1] - smooth[k - 1]) / 2;
|
|
4633
|
+
const gy = (smooth[k + S] - smooth[k - S]) / 2;
|
|
4634
|
+
const g = Math.hypot(gx, gy);
|
|
4635
|
+
const e = Math.min(1, 2 * g);
|
|
4636
|
+
if (e < c || g === 0) continue;
|
|
4637
|
+
const o = j * BLOCK + i;
|
|
4638
|
+
strength[o] = e;
|
|
4639
|
+
dirX[o] = gy / g;
|
|
4640
|
+
dirY[o] = -gx / g;
|
|
4641
|
+
const l = lap[k];
|
|
4642
|
+
for (const d of [1, -1, S, -S]) {
|
|
4643
|
+
const m = lap[k + d];
|
|
4644
|
+
if (l > 0 && m < 0 || l < 0 && m > 0 || l === 0) {
|
|
4645
|
+
if (Math.abs(l) <= Math.abs(m)) {
|
|
4646
|
+
zero[o] = 1;
|
|
4647
|
+
break;
|
|
4648
|
+
}
|
|
4649
|
+
}
|
|
4650
|
+
}
|
|
4651
|
+
}
|
|
4652
|
+
}
|
|
4653
|
+
return { strength, dirX, dirY, zero };
|
|
4654
|
+
}
|
|
4655
|
+
/** Features of the pixel at (x, y), which must be inside the image. */
|
|
4656
|
+
features(x, y) {
|
|
4657
|
+
const b = this.block(x, y);
|
|
4658
|
+
const o = y % BLOCK * BLOCK + x % BLOCK;
|
|
4659
|
+
return { strength: b.strength[o], dirX: b.dirX[o], dirY: b.dirY[o], zero: b.zero[o] };
|
|
4660
|
+
}
|
|
4661
|
+
};
|
|
4662
|
+
var clampPixel = (v, max) => Math.min(max - 1, Math.max(0, Math.floor(v)));
|
|
4663
|
+
function snapToEdge(map, point2, width = 10) {
|
|
4664
|
+
const cx = clampPixel(point2.x, map.width);
|
|
4665
|
+
const cy = clampPixel(point2.y, map.height);
|
|
4666
|
+
const r = Math.max(0, Math.round(width));
|
|
4667
|
+
let best = 0;
|
|
4668
|
+
let bestD = Infinity;
|
|
4669
|
+
let bx = cx;
|
|
4670
|
+
let by = cy;
|
|
4671
|
+
for (let y = Math.max(0, cy - r); y <= Math.min(map.height - 1, cy + r); y++) {
|
|
4672
|
+
for (let x = Math.max(0, cx - r); x <= Math.min(map.width - 1, cx + r); x++) {
|
|
4673
|
+
const d = (x - cx) ** 2 + (y - cy) ** 2;
|
|
4674
|
+
if (d > r * r) continue;
|
|
4675
|
+
const s = map.strength(x, y);
|
|
4676
|
+
if (s > best + 0.02 || s > 0 && Math.abs(s - best) <= 0.02 && d < bestD) {
|
|
4677
|
+
if (s > best) best = s;
|
|
4678
|
+
bestD = d;
|
|
4679
|
+
bx = x;
|
|
4680
|
+
by = y;
|
|
4681
|
+
}
|
|
4682
|
+
}
|
|
4683
|
+
}
|
|
4684
|
+
return best > 0 ? { x: bx + 0.5, y: by + 0.5 } : point2;
|
|
4685
|
+
}
|
|
4686
|
+
var Heap = class {
|
|
4687
|
+
ids = new Int32Array(256);
|
|
4688
|
+
keys = new Float64Array(256);
|
|
4689
|
+
size = 0;
|
|
4690
|
+
push(id, key) {
|
|
4691
|
+
if (this.size === this.ids.length) {
|
|
4692
|
+
const ids = new Int32Array(this.size * 2);
|
|
4693
|
+
ids.set(this.ids);
|
|
4694
|
+
const keys = new Float64Array(this.size * 2);
|
|
4695
|
+
keys.set(this.keys);
|
|
4696
|
+
this.ids = ids;
|
|
4697
|
+
this.keys = keys;
|
|
4698
|
+
}
|
|
4699
|
+
let i = this.size++;
|
|
4700
|
+
while (i > 0) {
|
|
4701
|
+
const p = i - 1 >> 1;
|
|
4702
|
+
if (this.keys[p] <= key) break;
|
|
4703
|
+
this.ids[i] = this.ids[p];
|
|
4704
|
+
this.keys[i] = this.keys[p];
|
|
4705
|
+
i = p;
|
|
4706
|
+
}
|
|
4707
|
+
this.ids[i] = id;
|
|
4708
|
+
this.keys[i] = key;
|
|
4709
|
+
}
|
|
4710
|
+
/** Removes the cheapest entry; read it from `topId` / `topKey` first. */
|
|
4711
|
+
pop() {
|
|
4712
|
+
const n = --this.size;
|
|
4713
|
+
const id = this.ids[n];
|
|
4714
|
+
const key = this.keys[n];
|
|
4715
|
+
let i = 0;
|
|
4716
|
+
for (; ; ) {
|
|
4717
|
+
let c = 2 * i + 1;
|
|
4718
|
+
if (c >= n) break;
|
|
4719
|
+
if (c + 1 < n && this.keys[c + 1] < this.keys[c]) c++;
|
|
4720
|
+
if (this.keys[c] >= key) break;
|
|
4721
|
+
this.ids[i] = this.ids[c];
|
|
4722
|
+
this.keys[i] = this.keys[c];
|
|
4723
|
+
i = c;
|
|
4724
|
+
}
|
|
4725
|
+
this.ids[i] = id;
|
|
4726
|
+
this.keys[i] = key;
|
|
4727
|
+
}
|
|
4728
|
+
get topId() {
|
|
4729
|
+
return this.ids[0];
|
|
4730
|
+
}
|
|
4731
|
+
get topKey() {
|
|
4732
|
+
return this.keys[0];
|
|
4733
|
+
}
|
|
4734
|
+
};
|
|
4735
|
+
var NEIGHBOURS = [
|
|
4736
|
+
[1, 0],
|
|
4737
|
+
[-1, 0],
|
|
4738
|
+
[0, 1],
|
|
4739
|
+
[0, -1],
|
|
4740
|
+
[1, 1],
|
|
4741
|
+
[1, -1],
|
|
4742
|
+
[-1, 1],
|
|
4743
|
+
[-1, -1]
|
|
4744
|
+
];
|
|
4745
|
+
function liveWire(map, from, to, band) {
|
|
4746
|
+
const ax = clampPixel(from.x, map.width);
|
|
4747
|
+
const ay = clampPixel(from.y, map.height);
|
|
4748
|
+
const bx = clampPixel(to.x, map.width);
|
|
4749
|
+
const by = clampPixel(to.y, map.height);
|
|
4750
|
+
if (ax === bx && ay === by) return [{ x: ax + 0.5, y: ay + 0.5 }];
|
|
4751
|
+
const len = Math.hypot(bx - ax, by - ay);
|
|
4752
|
+
const half = Math.max(2, band ?? Math.max(16, len / 4));
|
|
4753
|
+
const rx0 = Math.max(0, Math.floor(Math.min(ax, bx) - half));
|
|
4754
|
+
const ry0 = Math.max(0, Math.floor(Math.min(ay, by) - half));
|
|
4755
|
+
const rx1 = Math.min(map.width - 1, Math.ceil(Math.max(ax, bx) + half));
|
|
4756
|
+
const ry1 = Math.min(map.height - 1, Math.ceil(Math.max(ay, by) + half));
|
|
4757
|
+
const w = rx1 - rx0 + 1;
|
|
4758
|
+
const h = ry1 - ry0 + 1;
|
|
4759
|
+
const n = w * h;
|
|
4760
|
+
const strength = new Float32Array(n);
|
|
4761
|
+
const dirX = new Float32Array(n);
|
|
4762
|
+
const dirY = new Float32Array(n);
|
|
4763
|
+
const zero = new Uint8Array(n);
|
|
4764
|
+
const inside = new Uint8Array(n);
|
|
4765
|
+
let maxStrength = 0;
|
|
4766
|
+
const ux = (bx - ax) / len;
|
|
4767
|
+
const uy = (by - ay) / len;
|
|
4768
|
+
for (let y = 0; y < h; y++) {
|
|
4769
|
+
for (let x = 0; x < w; x++) {
|
|
4770
|
+
const px2 = rx0 + x - ax;
|
|
4771
|
+
const py = ry0 + y - ay;
|
|
4772
|
+
const t = Math.min(len, Math.max(0, px2 * ux + py * uy));
|
|
4773
|
+
if (Math.hypot(px2 - t * ux, py - t * uy) > half) continue;
|
|
4774
|
+
const i = y * w + x;
|
|
4775
|
+
inside[i] = 1;
|
|
4776
|
+
const f = map.features(rx0 + x, ry0 + y);
|
|
4777
|
+
strength[i] = f.strength;
|
|
4778
|
+
dirX[i] = f.dirX;
|
|
4779
|
+
dirY[i] = f.dirY;
|
|
4780
|
+
zero[i] = f.zero;
|
|
4781
|
+
if (f.strength > maxStrength) maxStrength = f.strength;
|
|
4782
|
+
}
|
|
4783
|
+
}
|
|
4784
|
+
const scale = maxStrength > 0 ? 1 / maxStrength : 0;
|
|
4785
|
+
const dist = new Float64Array(n).fill(Infinity);
|
|
4786
|
+
const prev = new Int32Array(n).fill(-1);
|
|
4787
|
+
const done = new Uint8Array(n);
|
|
4788
|
+
const start = (ay - ry0) * w + (ax - rx0);
|
|
4789
|
+
const goal = (by - ry0) * w + (bx - rx0);
|
|
4790
|
+
dist[start] = 0;
|
|
4791
|
+
const heap = new Heap();
|
|
4792
|
+
heap.push(start, 0);
|
|
4793
|
+
while (heap.size) {
|
|
4794
|
+
const p = heap.topId;
|
|
4795
|
+
const d = heap.topKey;
|
|
4796
|
+
heap.pop();
|
|
4797
|
+
if (done[p] || d > dist[p]) continue;
|
|
4798
|
+
done[p] = 1;
|
|
4799
|
+
if (p === goal) break;
|
|
4800
|
+
const x = p % w;
|
|
4801
|
+
const y = (p - x) / w;
|
|
4802
|
+
for (const [dx, dy] of NEIGHBOURS) {
|
|
4803
|
+
const qx = x + dx;
|
|
4804
|
+
const qy = y + dy;
|
|
4805
|
+
if (qx < 0 || qy < 0 || qx >= w || qy >= h) continue;
|
|
4806
|
+
const q2 = qy * w + qx;
|
|
4807
|
+
if (!inside[q2] || done[q2]) continue;
|
|
4808
|
+
const step = dx && dy ? Math.SQRT2 : 1;
|
|
4809
|
+
const lx = dx / step;
|
|
4810
|
+
const ly = dy / step;
|
|
4811
|
+
const dp = Math.min(1, Math.abs(dirX[p] * lx + dirY[p] * ly));
|
|
4812
|
+
const dq = Math.min(1, Math.abs(dirX[q2] * lx + dirY[q2] * ly));
|
|
4813
|
+
const fD = (Math.acos(dp) + Math.acos(dq)) / Math.PI;
|
|
4814
|
+
const fG = 1 - strength[q2] * scale;
|
|
4815
|
+
const fZ = 1 - zero[q2];
|
|
4816
|
+
const cost = (W_ZERO * fZ + W_GRADIENT * fG + W_DIRECTION * fD + 0.01) * step;
|
|
4817
|
+
const nd = d + cost;
|
|
4818
|
+
if (nd < dist[q2]) {
|
|
4819
|
+
dist[q2] = nd;
|
|
4820
|
+
prev[q2] = p;
|
|
4821
|
+
heap.push(q2, nd);
|
|
4822
|
+
}
|
|
4823
|
+
}
|
|
4824
|
+
}
|
|
4825
|
+
if (prev[goal] < 0) return [{ x: ax + 0.5, y: ay + 0.5 }, { x: bx + 0.5, y: by + 0.5 }];
|
|
4826
|
+
const path = [];
|
|
4827
|
+
for (let p = goal; p >= 0; p = prev[p]) {
|
|
4828
|
+
const x = p % w;
|
|
4829
|
+
path.push({ x: rx0 + x + 0.5, y: ry0 + (p - x) / w + 0.5 });
|
|
4830
|
+
}
|
|
4831
|
+
return path.reverse();
|
|
4832
|
+
}
|
|
4833
|
+
function simplifyPixelPath(points) {
|
|
4834
|
+
if (points.length < 3) return points.slice();
|
|
4835
|
+
const out = [points[0]];
|
|
4836
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
4837
|
+
const a = out[out.length - 1];
|
|
4838
|
+
const b = points[i];
|
|
4839
|
+
const c = points[i + 1];
|
|
4840
|
+
const cross = (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
|
|
4841
|
+
if (Math.abs(cross) > 1e-9) out.push(b);
|
|
4842
|
+
}
|
|
4843
|
+
out.push(points[points.length - 1]);
|
|
4844
|
+
return out;
|
|
4845
|
+
}
|
|
4846
|
+
function snapPolygonToEdges(pixels, points, options = {}) {
|
|
4847
|
+
const map = pixels instanceof EdgeMap ? pixels : new EdgeMap(pixels, options);
|
|
4848
|
+
const width = options.width ?? 10;
|
|
4849
|
+
const snapped = points.map((p) => snapToEdge(map, p, width));
|
|
4850
|
+
const out = [];
|
|
4851
|
+
for (let i = 0; i < snapped.length; i++) {
|
|
4852
|
+
const a = snapped[i];
|
|
4853
|
+
const b = snapped[(i + 1) % snapped.length];
|
|
4854
|
+
const seg = liveWire(map, a, b, Math.max(2 * width, 16, Math.hypot(b.x - a.x, b.y - a.y) / 4));
|
|
4855
|
+
out.push(...seg.slice(0, -1));
|
|
4856
|
+
}
|
|
4857
|
+
return simplifyPixelPath(out);
|
|
4858
|
+
}
|
|
4859
|
+
function magneticAnchorSpacing(frequency = 57) {
|
|
4860
|
+
const f = Math.min(100, Math.max(0, frequency));
|
|
4861
|
+
return 8 + (100 - f);
|
|
4862
|
+
}
|
|
4863
|
+
|
|
4550
4864
|
// src/image-editor/transform.ts
|
|
4551
4865
|
var FREE_TRANSFORM_MODES = ["scale", "rotate", "skew", "distort", "perspective", "warp"];
|
|
4552
4866
|
var TRANSFORM_ANCHORS = ["top-left", "top", "top-right", "left", "center", "right", "bottom-left", "bottom", "bottom-right"];
|
|
@@ -5473,7 +5787,7 @@ function baseFields(layer, ctx) {
|
|
|
5473
5787
|
blend: layer.blend
|
|
5474
5788
|
};
|
|
5475
5789
|
if (layer.clipped) out.clipped = true;
|
|
5476
|
-
if (layer.mask) out.mask =
|
|
5790
|
+
if (layer.mask) out.mask = serializeMask2(layer.mask, `${layer.id}-mask`, ctx);
|
|
5477
5791
|
if (layer.vectorMask) out.vectorMask = serializeVectorMask(layer.vectorMask);
|
|
5478
5792
|
if (layer.locks && Object.values(layer.locks).some(Boolean)) out.locks = { ...layer.locks };
|
|
5479
5793
|
if (layer.effects) out.effects = serializeEffects(layer.effects);
|
|
@@ -5558,7 +5872,7 @@ function readVectorMask(r) {
|
|
|
5558
5872
|
}
|
|
5559
5873
|
return vm;
|
|
5560
5874
|
}
|
|
5561
|
-
function
|
|
5875
|
+
function serializeMask2(mask, hint, ctx) {
|
|
5562
5876
|
const out = { defaultValue: mask.defaultValue };
|
|
5563
5877
|
if (!mask.source) {
|
|
5564
5878
|
const enc = encodeSurface(mask.surface);
|
|
@@ -5757,7 +6071,7 @@ function serializeSmartFilters(stack, layerId, ctx) {
|
|
|
5757
6071
|
enabled: stack.enabled,
|
|
5758
6072
|
filters: stack.filters.map((f) => f.kind === "psd" ? { ...f, data: bytesToBase64(f.data) } : JSON.parse(JSON.stringify(f)))
|
|
5759
6073
|
};
|
|
5760
|
-
if (stack.mask) out.mask =
|
|
6074
|
+
if (stack.mask) out.mask = serializeMask2(stack.mask, `${layerId}-filter-mask`, ctx);
|
|
5761
6075
|
return out;
|
|
5762
6076
|
}
|
|
5763
6077
|
function serializeSmartObject(layer, base, ctx) {
|
|
@@ -6105,7 +6419,7 @@ function readSmartFilters(raw, ctx, what) {
|
|
|
6105
6419
|
const r = raw;
|
|
6106
6420
|
const filters = [];
|
|
6107
6421
|
const list2 = Array.isArray(r.filters) ? r.filters : [];
|
|
6108
|
-
const
|
|
6422
|
+
const plain2 = readFilters(list2.filter((f) => f && f.kind !== "psd")) ?? [];
|
|
6109
6423
|
let k = 0;
|
|
6110
6424
|
for (const f of list2) {
|
|
6111
6425
|
if (!f || typeof f !== "object") continue;
|
|
@@ -6118,7 +6432,7 @@ function readSmartFilters(raw, ctx, what) {
|
|
|
6118
6432
|
ctx.problems.push(`${what}: a Photoshop smart filter could not be read`);
|
|
6119
6433
|
}
|
|
6120
6434
|
} else if (f.kind === "develop" || f.kind === "blur") {
|
|
6121
|
-
const base =
|
|
6435
|
+
const base = plain2[k++];
|
|
6122
6436
|
if (base) filters.push({ ...base, blend, opacity });
|
|
6123
6437
|
} else if (f.kind === "effect") {
|
|
6124
6438
|
const ref = readEffectRef(f);
|
|
@@ -11332,12 +11646,12 @@ function prepareNestedLayers(layers) {
|
|
|
11332
11646
|
const b = l.surface.bounds;
|
|
11333
11647
|
if (b) {
|
|
11334
11648
|
const r = { x: b.x + l.offset.x - 64, y: b.y + l.offset.y - 64, width: b.width + 128, height: b.height + 128 };
|
|
11335
|
-
const
|
|
11336
|
-
delete
|
|
11337
|
-
delete
|
|
11338
|
-
delete
|
|
11339
|
-
delete
|
|
11340
|
-
const data = new LayerCompositor().composite([
|
|
11649
|
+
const plain2 = { ...l, visible: true, opacity: 1, blend: "normal" };
|
|
11650
|
+
delete plain2.mask;
|
|
11651
|
+
delete plain2.clipped;
|
|
11652
|
+
delete plain2.effects;
|
|
11653
|
+
delete plain2.fillOpacity;
|
|
11654
|
+
const data = new LayerCompositor().composite([plain2], r, { width: r.x + r.width, height: r.y + r.height });
|
|
11341
11655
|
p.surface = surfaceFromPixels({ width: r.width, height: r.height, data }, 4, r.x - l.offset.x, r.y - l.offset.y);
|
|
11342
11656
|
}
|
|
11343
11657
|
}
|
|
@@ -11728,6 +12042,95 @@ function exportPsd(input) {
|
|
|
11728
12042
|
return { bytes, psb, records: records.length, warnings };
|
|
11729
12043
|
}
|
|
11730
12044
|
|
|
12045
|
+
// src/image-editor/selection/tool-options.ts
|
|
12046
|
+
var SELECTION_TOOLS = [
|
|
12047
|
+
{ id: "wand", label: "Magic Wand" },
|
|
12048
|
+
{ id: "rect", label: "Rectangle" },
|
|
12049
|
+
{ id: "ellipse", label: "Ellipse" },
|
|
12050
|
+
{ id: "row", label: "Single Row" },
|
|
12051
|
+
{ id: "column", label: "Single Column" },
|
|
12052
|
+
{ id: "lasso", label: "Lasso" },
|
|
12053
|
+
{ id: "polygon", label: "Polygonal Lasso" },
|
|
12054
|
+
{ id: "magnetic", label: "Magnetic Lasso" },
|
|
12055
|
+
{ id: "quick", label: "Quick Selection" },
|
|
12056
|
+
{ id: "object", label: "Object Selection" },
|
|
12057
|
+
{ id: "range", label: "Color Range" },
|
|
12058
|
+
{ id: "type", label: "Type Mask" },
|
|
12059
|
+
{ id: "type-vertical", label: "Vertical Type Mask" }
|
|
12060
|
+
];
|
|
12061
|
+
var COLOR_RANGE_PRESET_LABELS = {
|
|
12062
|
+
reds: "Reds",
|
|
12063
|
+
yellows: "Yellows",
|
|
12064
|
+
greens: "Greens",
|
|
12065
|
+
cyans: "Cyans",
|
|
12066
|
+
blues: "Blues",
|
|
12067
|
+
magentas: "Magentas",
|
|
12068
|
+
highlights: "Highlights",
|
|
12069
|
+
midtones: "Midtones",
|
|
12070
|
+
shadows: "Shadows",
|
|
12071
|
+
skinTones: "Skin Tones"
|
|
12072
|
+
};
|
|
12073
|
+
var SELECTION_MODE_LABELS = {
|
|
12074
|
+
replace: "New",
|
|
12075
|
+
add: "Add",
|
|
12076
|
+
subtract: "Subtract",
|
|
12077
|
+
intersect: "Intersect"
|
|
12078
|
+
};
|
|
12079
|
+
var DEFAULT_SELECTION_TOOL_OPTIONS = {
|
|
12080
|
+
tool: "wand",
|
|
12081
|
+
mode: "replace",
|
|
12082
|
+
tolerance: 32,
|
|
12083
|
+
contiguous: true,
|
|
12084
|
+
antiAlias: true,
|
|
12085
|
+
sampleMerged: false,
|
|
12086
|
+
distance: "rgb",
|
|
12087
|
+
feather: 0,
|
|
12088
|
+
fuzziness: 40,
|
|
12089
|
+
localized: 0,
|
|
12090
|
+
rangeInvert: false,
|
|
12091
|
+
magneticWidth: 10,
|
|
12092
|
+
magneticContrast: 10,
|
|
12093
|
+
magneticFrequency: 57,
|
|
12094
|
+
quickSize: 30,
|
|
12095
|
+
quickSampleAll: true,
|
|
12096
|
+
quickEnhance: false,
|
|
12097
|
+
objectSampleAll: true,
|
|
12098
|
+
objectEnhance: false,
|
|
12099
|
+
focusRange: 50,
|
|
12100
|
+
focusNoise: 0,
|
|
12101
|
+
focusSoftness: 4,
|
|
12102
|
+
typeText: "Type",
|
|
12103
|
+
typeSize: 72,
|
|
12104
|
+
typeFamily: "sans-serif"
|
|
12105
|
+
};
|
|
12106
|
+
function selectionModeFromModifiers(modifiers, fallback) {
|
|
12107
|
+
if (modifiers.shiftKey && modifiers.altKey) return "intersect";
|
|
12108
|
+
if (modifiers.shiftKey) return "add";
|
|
12109
|
+
if (modifiers.altKey) return "subtract";
|
|
12110
|
+
return fallback;
|
|
12111
|
+
}
|
|
12112
|
+
var REFINE_EDGE_CONTROLS = [
|
|
12113
|
+
{ id: "radius", label: "Radius", min: 0, max: 64, step: 1, unit: "px" },
|
|
12114
|
+
{ id: "smooth", label: "Smooth", min: 0, max: 100, step: 1, unit: "" },
|
|
12115
|
+
{ id: "feather", label: "Feather", min: 0, max: 50, step: 0.5, unit: "px" },
|
|
12116
|
+
{ id: "contrast", label: "Contrast", min: 0, max: 100, step: 1, unit: "%" },
|
|
12117
|
+
{ id: "shiftEdge", label: "Shift Edge", min: -100, max: 100, step: 1, unit: "%" }
|
|
12118
|
+
];
|
|
12119
|
+
var REFINE_OUTPUTS = [
|
|
12120
|
+
{ id: "selection", label: "Selection" },
|
|
12121
|
+
{ id: "layer-mask", label: "Layer Mask" },
|
|
12122
|
+
{ id: "new-layer", label: "New Layer" },
|
|
12123
|
+
{ id: "new-layer-with-mask", label: "New Layer with Layer Mask" },
|
|
12124
|
+
{ id: "new-document", label: "New Document", needsDocuments: true },
|
|
12125
|
+
{ id: "new-document-with-mask", label: "New Document with Layer Mask", needsDocuments: true }
|
|
12126
|
+
];
|
|
12127
|
+
function describeSelection(mask) {
|
|
12128
|
+
if (!mask || !mask.bounds) return "No selection";
|
|
12129
|
+
const pct2 = selectionArea(mask) / (mask.width * mask.height) * 100;
|
|
12130
|
+
const shown = pct2 >= 10 ? Math.round(pct2) : Math.round(pct2 * 10) / 10;
|
|
12131
|
+
return `${mask.bounds.width} \xD7 ${mask.bounds.height} px selected (${shown}% of the image)`;
|
|
12132
|
+
}
|
|
12133
|
+
|
|
11731
12134
|
// src/image-editor/effect-tools.ts
|
|
11732
12135
|
var READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
11733
12136
|
var WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
@@ -12077,6 +12480,740 @@ function createLayerEffectTools(host, options) {
|
|
|
12077
12480
|
return host.setGlobalLight ? [list2, add, update, remove, setEnabled, light] : [list2, add, update, remove, setEnabled];
|
|
12078
12481
|
}
|
|
12079
12482
|
|
|
12483
|
+
// src/image-editor/selection/gesture.ts
|
|
12484
|
+
function snapTo45(from, point2) {
|
|
12485
|
+
const dx = point2.x - from.x;
|
|
12486
|
+
const dy = point2.y - from.y;
|
|
12487
|
+
const len = Math.hypot(dx, dy);
|
|
12488
|
+
if (len === 0) return point2;
|
|
12489
|
+
const a = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4);
|
|
12490
|
+
return { x: from.x + Math.cos(a) * len, y: from.y + Math.sin(a) * len };
|
|
12491
|
+
}
|
|
12492
|
+
var SelectionGesture = class {
|
|
12493
|
+
constructor(controller, getOptions) {
|
|
12494
|
+
this.controller = controller;
|
|
12495
|
+
this.getOptions = getOptions;
|
|
12496
|
+
}
|
|
12497
|
+
controller;
|
|
12498
|
+
getOptions;
|
|
12499
|
+
start = null;
|
|
12500
|
+
points = [];
|
|
12501
|
+
current = null;
|
|
12502
|
+
modifiers = {};
|
|
12503
|
+
/** Polygonal lasso corners while one is being drawn. */
|
|
12504
|
+
polygon = null;
|
|
12505
|
+
pressedAt = null;
|
|
12506
|
+
freehand = false;
|
|
12507
|
+
magnetic = null;
|
|
12508
|
+
/** A click this close to the first corner (natural pixels) closes a polygonal lasso. */
|
|
12509
|
+
closeRadius = 8;
|
|
12510
|
+
/** The last error (e.g. pixels unavailable for the wand), for the UI to show. */
|
|
12511
|
+
error = null;
|
|
12512
|
+
pending = Promise.resolve();
|
|
12513
|
+
/** Quick Selection while dragging: the selection before the stroke, how the stroke combines, the pixels. */
|
|
12514
|
+
quick = null;
|
|
12515
|
+
livePreviews = Promise.resolve();
|
|
12516
|
+
get active() {
|
|
12517
|
+
return this.start !== null || this.polygon !== null || this.magnetic !== null;
|
|
12518
|
+
}
|
|
12519
|
+
/** Resolves once a Quick Selection stroke or an Object Selection has been applied. */
|
|
12520
|
+
get settled() {
|
|
12521
|
+
return this.pending;
|
|
12522
|
+
}
|
|
12523
|
+
/** Resolves once no live Quick Selection preview is being computed. */
|
|
12524
|
+
get previewed() {
|
|
12525
|
+
return this.livePreviews;
|
|
12526
|
+
}
|
|
12527
|
+
/** Grows the Quick Selection preview to the stroke so far: one at a time, the newest points following. */
|
|
12528
|
+
growPreview() {
|
|
12529
|
+
const q2 = this.quick;
|
|
12530
|
+
if (!q2) return;
|
|
12531
|
+
if (q2.running) {
|
|
12532
|
+
q2.dirty = true;
|
|
12533
|
+
return;
|
|
12534
|
+
}
|
|
12535
|
+
q2.running = true;
|
|
12536
|
+
this.livePreviews = (async () => {
|
|
12537
|
+
do {
|
|
12538
|
+
q2.dirty = false;
|
|
12539
|
+
const stroke = this.points.slice();
|
|
12540
|
+
const options = this.getOptions();
|
|
12541
|
+
try {
|
|
12542
|
+
const pixels = await q2.pixels;
|
|
12543
|
+
if (!pixels || this.quick !== q2) break;
|
|
12544
|
+
const { mask } = await this.controller.runSelectionJob(
|
|
12545
|
+
{
|
|
12546
|
+
op: "quick",
|
|
12547
|
+
stroke,
|
|
12548
|
+
size: options.quickSize ?? 30,
|
|
12549
|
+
grow: q2.mode === "add",
|
|
12550
|
+
enhanceEdge: options.quickEnhance ?? false,
|
|
12551
|
+
selection: q2.mode === "add" && q2.base?.bounds ? serializeMask(q2.base) : null
|
|
12552
|
+
},
|
|
12553
|
+
pixels
|
|
12554
|
+
);
|
|
12555
|
+
if (this.quick === q2) this.controller.setSelectionPreview(combineSelections(q2.base, mask, q2.mode));
|
|
12556
|
+
} catch {
|
|
12557
|
+
break;
|
|
12558
|
+
}
|
|
12559
|
+
} while (q2.dirty && this.quick === q2);
|
|
12560
|
+
q2.running = false;
|
|
12561
|
+
})();
|
|
12562
|
+
}
|
|
12563
|
+
/** The anchors of a Magnetic Lasso being drawn (empty otherwise). */
|
|
12564
|
+
get magneticAnchors() {
|
|
12565
|
+
const m = this.magnetic;
|
|
12566
|
+
return m ? m.anchors.map((i) => m.fixed[i]) : [];
|
|
12567
|
+
}
|
|
12568
|
+
/** The outline of the shape being drawn, or `null`. */
|
|
12569
|
+
get draftPath() {
|
|
12570
|
+
if (this.magnetic) return [...this.magnetic.fixed, ...this.magnetic.live.slice(1)];
|
|
12571
|
+
if (this.polygon) return this.current ? [...this.polygon, this.current] : this.polygon.slice();
|
|
12572
|
+
if (!this.start || !this.current) return null;
|
|
12573
|
+
const { tool } = this.getOptions();
|
|
12574
|
+
if (tool === "quick") return null;
|
|
12575
|
+
if (tool === "lasso") return this.points.length > 1 ? this.points.slice() : null;
|
|
12576
|
+
const rect = normalizeRect({
|
|
12577
|
+
x: this.start.x,
|
|
12578
|
+
y: this.start.y,
|
|
12579
|
+
width: this.current.x - this.start.x,
|
|
12580
|
+
height: this.current.y - this.start.y
|
|
12581
|
+
});
|
|
12582
|
+
if (rect.width < 1 && rect.height < 1) return null;
|
|
12583
|
+
if (tool === "ellipse") return ellipsePoints(rect);
|
|
12584
|
+
return [
|
|
12585
|
+
{ x: rect.x, y: rect.y },
|
|
12586
|
+
{ x: rect.x + rect.width, y: rect.y },
|
|
12587
|
+
{ x: rect.x + rect.width, y: rect.y + rect.height },
|
|
12588
|
+
{ x: rect.x, y: rect.y + rect.height }
|
|
12589
|
+
];
|
|
12590
|
+
}
|
|
12591
|
+
/** Pointer down. Resolves once a wand click has been applied. */
|
|
12592
|
+
async begin(point2, modifiers = {}) {
|
|
12593
|
+
const options = this.getOptions();
|
|
12594
|
+
this.error = null;
|
|
12595
|
+
if (this.polygon && options.tool !== "polygon" || this.magnetic && options.tool !== "magnetic") this.cancel();
|
|
12596
|
+
if (options.tool === "magnetic") return this.magneticClick(point2, modifiers, options);
|
|
12597
|
+
if (options.tool === "polygon") {
|
|
12598
|
+
this.pressedAt = point2;
|
|
12599
|
+
this.freehand = false;
|
|
12600
|
+
if (!this.polygon) {
|
|
12601
|
+
this.modifiers = modifiers;
|
|
12602
|
+
this.polygon = [point2];
|
|
12603
|
+
this.current = point2;
|
|
12604
|
+
return;
|
|
12605
|
+
}
|
|
12606
|
+
const last = this.polygon[this.polygon.length - 1];
|
|
12607
|
+
const p = modifiers.shiftKey ? snapTo45(last, point2) : point2;
|
|
12608
|
+
const first = this.polygon[0];
|
|
12609
|
+
if (this.polygon.length >= 3 && Math.hypot(p.x - first.x, p.y - first.y) <= this.closeRadius) {
|
|
12610
|
+
this.close();
|
|
12611
|
+
return;
|
|
12612
|
+
}
|
|
12613
|
+
if (Math.hypot(p.x - last.x, p.y - last.y) >= 0.5) this.polygon.push(p);
|
|
12614
|
+
this.current = p;
|
|
12615
|
+
if ((modifiers.detail ?? 1) >= 2) this.close();
|
|
12616
|
+
return;
|
|
12617
|
+
}
|
|
12618
|
+
this.modifiers = modifiers;
|
|
12619
|
+
if (options.tool === "row" || options.tool === "column") {
|
|
12620
|
+
this.start = null;
|
|
12621
|
+
try {
|
|
12622
|
+
this.controller.selectLine(options.tool, options.tool === "row" ? point2.y : point2.x, {
|
|
12623
|
+
mode: selectionModeFromModifiers(modifiers, options.mode),
|
|
12624
|
+
feather: options.feather ?? 0
|
|
12625
|
+
});
|
|
12626
|
+
} catch (err) {
|
|
12627
|
+
this.error = err instanceof Error ? err.message : String(err);
|
|
12628
|
+
}
|
|
12629
|
+
return;
|
|
12630
|
+
}
|
|
12631
|
+
if (options.tool === "type" || options.tool === "type-vertical") {
|
|
12632
|
+
this.start = null;
|
|
12633
|
+
try {
|
|
12634
|
+
this.controller.selectTextShape({
|
|
12635
|
+
text: options.typeText || "Type",
|
|
12636
|
+
x: point2.x,
|
|
12637
|
+
y: point2.y,
|
|
12638
|
+
orientation: options.tool === "type-vertical" ? "vertical" : "horizontal",
|
|
12639
|
+
style: { fontSize: options.typeSize ?? 72, fontFamily: options.typeFamily || "sans-serif" },
|
|
12640
|
+
mode: selectionModeFromModifiers(modifiers, options.mode),
|
|
12641
|
+
feather: options.feather ?? 0,
|
|
12642
|
+
antiAlias: options.antiAlias
|
|
12643
|
+
});
|
|
12644
|
+
} catch (err) {
|
|
12645
|
+
this.error = err instanceof Error ? err.message : String(err);
|
|
12646
|
+
}
|
|
12647
|
+
return;
|
|
12648
|
+
}
|
|
12649
|
+
if (options.tool === "wand" || options.tool === "range") {
|
|
12650
|
+
this.start = null;
|
|
12651
|
+
const common2 = {
|
|
12652
|
+
sampleMerged: options.sampleMerged,
|
|
12653
|
+
sample: options.sample,
|
|
12654
|
+
feather: options.feather ?? 0,
|
|
12655
|
+
mode: selectionModeFromModifiers(modifiers, options.mode)
|
|
12656
|
+
};
|
|
12657
|
+
try {
|
|
12658
|
+
if (options.tool === "range") {
|
|
12659
|
+
const pixels = await this.controller.getSamplePixels(common2);
|
|
12660
|
+
if (!pixels) throw new Error("The image pixels are not available to sample.");
|
|
12661
|
+
const x = Math.min(pixels.width - 1, Math.max(0, Math.floor(point2.x)));
|
|
12662
|
+
const y = Math.min(pixels.height - 1, Math.max(0, Math.floor(point2.y)));
|
|
12663
|
+
const p = (y * pixels.width + x) * 4;
|
|
12664
|
+
const color = [pixels.data[p], pixels.data[p + 1], pixels.data[p + 2]];
|
|
12665
|
+
await this.controller.selectColorRange([{ color, at: point2 }], {
|
|
12666
|
+
...common2,
|
|
12667
|
+
fuzziness: options.fuzziness ?? 40,
|
|
12668
|
+
localized: options.localized ? options.localized : void 0,
|
|
12669
|
+
invert: options.rangeInvert ?? false
|
|
12670
|
+
});
|
|
12671
|
+
return;
|
|
12672
|
+
}
|
|
12673
|
+
await this.controller.magicWand(point2, {
|
|
12674
|
+
...common2,
|
|
12675
|
+
tolerance: options.tolerance,
|
|
12676
|
+
contiguous: options.contiguous,
|
|
12677
|
+
antiAlias: options.antiAlias,
|
|
12678
|
+
distance: options.distance
|
|
12679
|
+
});
|
|
12680
|
+
} catch (err) {
|
|
12681
|
+
this.error = err instanceof Error ? err.message : String(err);
|
|
12682
|
+
}
|
|
12683
|
+
return;
|
|
12684
|
+
}
|
|
12685
|
+
this.start = point2;
|
|
12686
|
+
this.current = point2;
|
|
12687
|
+
this.points = [point2];
|
|
12688
|
+
if (options.tool === "quick") {
|
|
12689
|
+
const picked = selectionModeFromModifiers(modifiers, options.mode);
|
|
12690
|
+
const base = this.controller.getSelection();
|
|
12691
|
+
this.quick = {
|
|
12692
|
+
base,
|
|
12693
|
+
mode: picked === "replace" && base ? "add" : picked,
|
|
12694
|
+
pixels: this.controller.getSamplePixels({ sample: options.quickSampleAll === false ? "active-layer" : "composite" }).catch(() => null),
|
|
12695
|
+
running: false,
|
|
12696
|
+
dirty: false
|
|
12697
|
+
};
|
|
12698
|
+
this.growPreview();
|
|
12699
|
+
}
|
|
12700
|
+
}
|
|
12701
|
+
/** Pointer move; returns true when the draft changed (re-render). */
|
|
12702
|
+
move(point2, modifiers = {}) {
|
|
12703
|
+
if (this.magnetic) {
|
|
12704
|
+
this.current = point2;
|
|
12705
|
+
this.followEdges(point2);
|
|
12706
|
+
return true;
|
|
12707
|
+
}
|
|
12708
|
+
if (this.polygon) {
|
|
12709
|
+
const last = this.polygon[this.polygon.length - 1];
|
|
12710
|
+
if (this.pressedAt && (this.freehand || Math.hypot(point2.x - this.pressedAt.x, point2.y - this.pressedAt.y) >= 3)) {
|
|
12711
|
+
this.freehand = true;
|
|
12712
|
+
if (Math.hypot(point2.x - last.x, point2.y - last.y) >= 0.5) this.polygon.push(point2);
|
|
12713
|
+
this.current = null;
|
|
12714
|
+
return true;
|
|
12715
|
+
}
|
|
12716
|
+
this.current = modifiers.shiftKey ? snapTo45(last, point2) : point2;
|
|
12717
|
+
return true;
|
|
12718
|
+
}
|
|
12719
|
+
if (!this.start) return false;
|
|
12720
|
+
this.current = point2;
|
|
12721
|
+
const drawn = this.getOptions().tool;
|
|
12722
|
+
if (drawn === "lasso" || drawn === "quick") {
|
|
12723
|
+
const last = this.points[this.points.length - 1];
|
|
12724
|
+
if (Math.hypot(point2.x - last.x, point2.y - last.y) < 0.5) return false;
|
|
12725
|
+
this.points.push(point2);
|
|
12726
|
+
if (drawn === "quick") this.growPreview();
|
|
12727
|
+
}
|
|
12728
|
+
return true;
|
|
12729
|
+
}
|
|
12730
|
+
/** Pointer up: commits the shape. Returns true when the selection changed. */
|
|
12731
|
+
end(point2, modifiers) {
|
|
12732
|
+
if (this.magnetic) return false;
|
|
12733
|
+
if (this.polygon) {
|
|
12734
|
+
if (point2 && this.freehand) this.move(point2);
|
|
12735
|
+
this.pressedAt = null;
|
|
12736
|
+
this.freehand = false;
|
|
12737
|
+
return false;
|
|
12738
|
+
}
|
|
12739
|
+
if (!this.start) return false;
|
|
12740
|
+
if (point2) this.move(point2);
|
|
12741
|
+
const options = this.getOptions();
|
|
12742
|
+
const mode = selectionModeFromModifiers(modifiers ?? this.modifiers, options.mode);
|
|
12743
|
+
if (options.tool === "object") {
|
|
12744
|
+
const rect2 = normalizeRect({ x: this.start.x, y: this.start.y, width: this.current.x - this.start.x, height: this.current.y - this.start.y });
|
|
12745
|
+
this.start = null;
|
|
12746
|
+
this.current = null;
|
|
12747
|
+
this.points = [];
|
|
12748
|
+
const target = rect2.width >= 4 && rect2.height >= 4 ? { rect: rect2 } : { point: { x: rect2.x + rect2.width / 2, y: rect2.y + rect2.height / 2 } };
|
|
12749
|
+
this.pending = this.controller.selectObject(target, { mode, sampleAllLayers: options.objectSampleAll ?? true, enhanceEdge: options.objectEnhance ?? false }).then(
|
|
12750
|
+
() => void 0,
|
|
12751
|
+
(err) => {
|
|
12752
|
+
this.error = err instanceof Error ? err.message : String(err);
|
|
12753
|
+
}
|
|
12754
|
+
);
|
|
12755
|
+
return false;
|
|
12756
|
+
}
|
|
12757
|
+
if (options.tool === "quick") {
|
|
12758
|
+
const stroke = this.points;
|
|
12759
|
+
this.start = null;
|
|
12760
|
+
this.current = null;
|
|
12761
|
+
this.points = [];
|
|
12762
|
+
const strokeMode = this.quick?.mode ?? (mode === "replace" && this.controller.getSelection() ? "add" : mode);
|
|
12763
|
+
this.quick = null;
|
|
12764
|
+
this.pending = this.controller.quickSelect([stroke], {
|
|
12765
|
+
size: options.quickSize ?? 30,
|
|
12766
|
+
mode: strokeMode,
|
|
12767
|
+
sampleAllLayers: options.quickSampleAll ?? true,
|
|
12768
|
+
enhanceEdge: options.quickEnhance ?? false
|
|
12769
|
+
}).then(
|
|
12770
|
+
() => void 0,
|
|
12771
|
+
(err) => {
|
|
12772
|
+
this.error = err instanceof Error ? err.message : String(err);
|
|
12773
|
+
}
|
|
12774
|
+
).finally(() => this.controller.setSelectionPreview(void 0));
|
|
12775
|
+
return false;
|
|
12776
|
+
}
|
|
12777
|
+
const path = this.draftPath;
|
|
12778
|
+
const rect = normalizeRect({
|
|
12779
|
+
x: this.start.x,
|
|
12780
|
+
y: this.start.y,
|
|
12781
|
+
width: this.current.x - this.start.x,
|
|
12782
|
+
height: this.current.y - this.start.y
|
|
12783
|
+
});
|
|
12784
|
+
this.start = null;
|
|
12785
|
+
this.current = null;
|
|
12786
|
+
this.points = [];
|
|
12787
|
+
if (!path) return mode === "replace" ? this.controller.deselect() : false;
|
|
12788
|
+
const opts = { mode, antiAlias: options.antiAlias, feather: options.feather ?? 0 };
|
|
12789
|
+
if (options.tool === "lasso") return path.length >= 3 ? this.controller.selectPolygon(path, opts) : false;
|
|
12790
|
+
return options.tool === "ellipse" ? this.controller.selectEllipse(rect, opts) : this.controller.selectRect(rect, opts);
|
|
12791
|
+
}
|
|
12792
|
+
/** Closes a polygonal lasso and selects it. Returns true when the selection changed. */
|
|
12793
|
+
close() {
|
|
12794
|
+
let polygon2 = this.polygon;
|
|
12795
|
+
const m = this.magnetic;
|
|
12796
|
+
if (m) {
|
|
12797
|
+
const tail = m.edges && m.fixed.length > 1 ? liveWire(m.edges, m.fixed[m.fixed.length - 1], m.fixed[0], this.corridor(m.fixed[m.fixed.length - 1], m.fixed[0])) : [];
|
|
12798
|
+
polygon2 = simplifyPixelPath([...m.fixed, ...tail.slice(1, -1)]);
|
|
12799
|
+
}
|
|
12800
|
+
if (!polygon2) return false;
|
|
12801
|
+
const options = this.getOptions();
|
|
12802
|
+
this.cancel();
|
|
12803
|
+
if (polygon2.length < 3) return false;
|
|
12804
|
+
return this.controller.selectPolygon(polygon2, {
|
|
12805
|
+
mode: selectionModeFromModifiers(this.modifiers, options.mode),
|
|
12806
|
+
antiAlias: options.antiAlias,
|
|
12807
|
+
feather: options.feather ?? 0
|
|
12808
|
+
});
|
|
12809
|
+
}
|
|
12810
|
+
/**
|
|
12811
|
+
* Keys for a polygonal lasso being drawn: Enter closes it, Escape cancels, Backspace / Delete
|
|
12812
|
+
* removes the last corner. Returns true when the key was used (the host prevents its default).
|
|
12813
|
+
*/
|
|
12814
|
+
key(key) {
|
|
12815
|
+
if (!this.polygon && !this.magnetic) return false;
|
|
12816
|
+
if (key === "Enter") this.close();
|
|
12817
|
+
else if (key === "Escape") this.cancel();
|
|
12818
|
+
else if (key === "Backspace" || key === "Delete") {
|
|
12819
|
+
const m = this.magnetic;
|
|
12820
|
+
if (m) {
|
|
12821
|
+
m.anchors.pop();
|
|
12822
|
+
if (!m.anchors.length) this.cancel();
|
|
12823
|
+
else {
|
|
12824
|
+
m.fixed.length = m.anchors[m.anchors.length - 1] + 1;
|
|
12825
|
+
m.live = [];
|
|
12826
|
+
if (this.current) this.followEdges(this.current, false);
|
|
12827
|
+
}
|
|
12828
|
+
} else {
|
|
12829
|
+
this.polygon.pop();
|
|
12830
|
+
if (!this.polygon.length) this.cancel();
|
|
12831
|
+
}
|
|
12832
|
+
} else return false;
|
|
12833
|
+
return true;
|
|
12834
|
+
}
|
|
12835
|
+
/** Corridor half-width for a live-wire between two points. */
|
|
12836
|
+
corridor(a, b) {
|
|
12837
|
+
return Math.max(2 * (this.getOptions().magneticWidth ?? 10), 16, Math.hypot(b.x - a.x, b.y - a.y) / 4);
|
|
12838
|
+
}
|
|
12839
|
+
async magneticClick(point2, modifiers, options) {
|
|
12840
|
+
const m = this.magnetic;
|
|
12841
|
+
if (!m) {
|
|
12842
|
+
this.modifiers = modifiers;
|
|
12843
|
+
const draft = { fixed: [point2], anchors: [0], live: [], edges: null };
|
|
12844
|
+
this.magnetic = draft;
|
|
12845
|
+
this.current = point2;
|
|
12846
|
+
try {
|
|
12847
|
+
const pixels = await this.controller.getSamplePixels({ sampleMerged: options.sampleMerged, sample: options.sample });
|
|
12848
|
+
if (!pixels) throw new Error("The image pixels are not available to find edges in.");
|
|
12849
|
+
if (this.magnetic !== draft) return;
|
|
12850
|
+
draft.edges = new EdgeMap(pixels, { contrast: (options.magneticContrast ?? 10) / 100 });
|
|
12851
|
+
draft.fixed = [snapToEdge(draft.edges, point2, options.magneticWidth ?? 10)];
|
|
12852
|
+
if (this.current) this.followEdges(this.current);
|
|
12853
|
+
} catch (err) {
|
|
12854
|
+
this.error = err instanceof Error ? err.message : String(err);
|
|
12855
|
+
if (this.magnetic === draft) this.cancel();
|
|
12856
|
+
}
|
|
12857
|
+
return;
|
|
12858
|
+
}
|
|
12859
|
+
const first = m.fixed[0];
|
|
12860
|
+
if (m.anchors.length >= 2 && Math.hypot(point2.x - first.x, point2.y - first.y) <= this.closeRadius) {
|
|
12861
|
+
this.close();
|
|
12862
|
+
return;
|
|
12863
|
+
}
|
|
12864
|
+
this.current = point2;
|
|
12865
|
+
this.followEdges(point2);
|
|
12866
|
+
if (m.live.length > 1) {
|
|
12867
|
+
m.fixed.push(...m.live.slice(1));
|
|
12868
|
+
m.anchors.push(m.fixed.length - 1);
|
|
12869
|
+
m.live = [];
|
|
12870
|
+
}
|
|
12871
|
+
if ((modifiers.detail ?? 1) >= 2) this.close();
|
|
12872
|
+
}
|
|
12873
|
+
/** Updates the live path to `point`, fixing anchors along it every `frequency` px (with `place`). */
|
|
12874
|
+
followEdges(point2, place = true) {
|
|
12875
|
+
const m = this.magnetic;
|
|
12876
|
+
const last = () => m.fixed[m.fixed.length - 1];
|
|
12877
|
+
if (!m.edges) {
|
|
12878
|
+
m.live = [last(), point2];
|
|
12879
|
+
return;
|
|
12880
|
+
}
|
|
12881
|
+
const options = this.getOptions();
|
|
12882
|
+
const target = snapToEdge(m.edges, point2, options.magneticWidth ?? 10);
|
|
12883
|
+
const spacing = magneticAnchorSpacing(options.magneticFrequency ?? 57);
|
|
12884
|
+
for (let guard2 = 0; guard2 < 64; guard2++) {
|
|
12885
|
+
const path = liveWire(m.edges, last(), target, this.corridor(last(), target));
|
|
12886
|
+
let length = 0;
|
|
12887
|
+
let k = 0;
|
|
12888
|
+
for (k = 1; k < path.length; k++) {
|
|
12889
|
+
length += Math.hypot(path[k].x - path[k - 1].x, path[k].y - path[k - 1].y);
|
|
12890
|
+
if (length >= spacing) break;
|
|
12891
|
+
}
|
|
12892
|
+
if (!place || k >= path.length - 1) {
|
|
12893
|
+
m.live = path;
|
|
12894
|
+
return;
|
|
12895
|
+
}
|
|
12896
|
+
m.fixed.push(...path.slice(1, k + 1));
|
|
12897
|
+
m.anchors.push(m.fixed.length - 1);
|
|
12898
|
+
}
|
|
12899
|
+
m.live = [last(), target];
|
|
12900
|
+
}
|
|
12901
|
+
/** Abandons a drag (or a polygonal lasso) without changing the selection. */
|
|
12902
|
+
cancel() {
|
|
12903
|
+
if (this.quick) {
|
|
12904
|
+
this.quick = null;
|
|
12905
|
+
this.controller.setSelectionPreview(void 0);
|
|
12906
|
+
}
|
|
12907
|
+
this.start = null;
|
|
12908
|
+
this.current = null;
|
|
12909
|
+
this.points = [];
|
|
12910
|
+
this.polygon = null;
|
|
12911
|
+
this.magnetic = null;
|
|
12912
|
+
this.pressedAt = null;
|
|
12913
|
+
this.freehand = false;
|
|
12914
|
+
}
|
|
12915
|
+
};
|
|
12916
|
+
function handleSelectionShortcut(controller, e, onError) {
|
|
12917
|
+
if (!(e.ctrlKey || e.metaKey)) return false;
|
|
12918
|
+
const key = e.key.toLowerCase();
|
|
12919
|
+
const run = (fn) => {
|
|
12920
|
+
try {
|
|
12921
|
+
fn();
|
|
12922
|
+
} catch (err) {
|
|
12923
|
+
onError?.(err instanceof Error ? err.message : String(err));
|
|
12924
|
+
}
|
|
12925
|
+
return true;
|
|
12926
|
+
};
|
|
12927
|
+
if (key === "c") return run(() => controller.copyPixels({ merged: Boolean(e.shiftKey) }));
|
|
12928
|
+
if (key === "x" && !e.shiftKey) return run(() => controller.cutPixels());
|
|
12929
|
+
if (key === "v") return run(() => controller.pastePixels(e.shiftKey && e.altKey ? "into" : e.shiftKey ? "in-place" : "new-layer"));
|
|
12930
|
+
if (key === "d") return run(() => e.shiftKey ? controller.reselect() : controller.deselect());
|
|
12931
|
+
if (key === "i" && e.shiftKey) return run(() => controller.invertSelection());
|
|
12932
|
+
return false;
|
|
12933
|
+
}
|
|
12934
|
+
function handleMaskViewKey(controller, e) {
|
|
12935
|
+
if (e.ctrlKey || e.metaKey || e.altKey || e.key !== "\\") return false;
|
|
12936
|
+
const id = controller.getState().activeLayerId;
|
|
12937
|
+
const layer = id ? controller.getLayer(id) : null;
|
|
12938
|
+
if (!layer?.mask || layer.mask.source) return false;
|
|
12939
|
+
const view = controller.getMaskView();
|
|
12940
|
+
controller.setMaskView(view?.layerId === layer.id && view.mode === "overlay" ? "off" : "overlay", layer.id);
|
|
12941
|
+
return true;
|
|
12942
|
+
}
|
|
12943
|
+
function handleQuickMaskKey(controller, e) {
|
|
12944
|
+
if (e.ctrlKey || e.metaKey || e.altKey || e.key.toLowerCase() !== "q") return false;
|
|
12945
|
+
controller.setQuickMaskMode(!controller.quickMaskEditing);
|
|
12946
|
+
return true;
|
|
12947
|
+
}
|
|
12948
|
+
|
|
12949
|
+
// src/image-editor/layers/paint-ui.ts
|
|
12950
|
+
var plain = { kind: "plain" };
|
|
12951
|
+
var BRUSH_OPTION_CONTROLS = [
|
|
12952
|
+
{ id: "size", label: "Size", section: "layer", min: 1, max: 500, step: 1, default: 20, decimals: 0, track: plain, unit: "px" },
|
|
12953
|
+
{ id: "hardness", label: "Hardness", section: "layer", min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain, displayScale: 100, unit: "%" },
|
|
12954
|
+
{ id: "opacity", label: "Opacity", section: "layer", min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain, displayScale: 100, unit: "%" },
|
|
12955
|
+
{ id: "flow", label: "Flow", section: "layer", min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain, displayScale: 100, unit: "%" },
|
|
12956
|
+
{ id: "smoothing", label: "Smoothing", section: "layer", min: 0, max: 0.95, step: 0.01, default: 0, decimals: 0, track: plain, displayScale: 100, unit: "%" }
|
|
12957
|
+
];
|
|
12958
|
+
var MIXER_OPTION_CONTROLS = [
|
|
12959
|
+
{ id: "wet", label: "Wet", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain, displayScale: 100, unit: "%" },
|
|
12960
|
+
{ id: "load", label: "Load", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain, displayScale: 100, unit: "%" },
|
|
12961
|
+
{ id: "mix", label: "Mix", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain, displayScale: 100, unit: "%" }
|
|
12962
|
+
];
|
|
12963
|
+
var MIXER_MIXING_OPTIONS = [
|
|
12964
|
+
{ id: "rgb", label: "RGB", title: "Mix colours as RGB averages" },
|
|
12965
|
+
{ id: "pigment", label: "Pigment", title: "Mix colours like paint: blue and yellow make green" }
|
|
12966
|
+
];
|
|
12967
|
+
var PAINT_TOOLS = [
|
|
12968
|
+
{ id: "move", label: "Move", shortcut: "V" },
|
|
12969
|
+
{ id: "brush", label: "Brush", shortcut: "B" },
|
|
12970
|
+
{ id: "mixer", label: "Mixer", shortcut: "Shift+B" },
|
|
12971
|
+
{ id: "eraser", label: "Eraser", shortcut: "E" },
|
|
12972
|
+
{ id: "clone", label: "Clone", shortcut: "S" },
|
|
12973
|
+
{ id: "heal", label: "Heal", shortcut: "Shift+J" },
|
|
12974
|
+
{ id: "spot-heal", label: "Spot Heal", shortcut: "J" },
|
|
12975
|
+
{ id: "smudge", label: "Smudge", shortcut: "" },
|
|
12976
|
+
{ id: "dodge", label: "Dodge", shortcut: "O" },
|
|
12977
|
+
{ id: "burn", label: "Burn", shortcut: "Shift+O" },
|
|
12978
|
+
{ id: "sponge", label: "Sponge", shortcut: "Shift+O" },
|
|
12979
|
+
{ id: "blur", label: "Blur", shortcut: "" },
|
|
12980
|
+
{ id: "sharpen", label: "Sharpen", shortcut: "" },
|
|
12981
|
+
{ id: "history", label: "History", shortcut: "Y" }
|
|
12982
|
+
];
|
|
12983
|
+
var isPaintTool = (tool) => PAINT_TOOLS.some((t) => t.id === tool);
|
|
12984
|
+
var isRetouchPaintTool = (tool) => tool === "clone" || tool === "heal" || tool === "spot-heal" || tool === "smudge" || tool === "dodge" || tool === "burn" || tool === "sponge" || tool === "blur" || tool === "sharpen" || tool === "history";
|
|
12985
|
+
var RETOUCH_OPTION_CONTROLS = [
|
|
12986
|
+
{ id: "exposure", label: "Exposure", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain, displayScale: 100, unit: "%", tools: ["dodge", "burn"] },
|
|
12987
|
+
{ id: "strength", label: "Strength", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain, displayScale: 100, unit: "%", tools: ["smudge", "blur", "sharpen"] },
|
|
12988
|
+
{ id: "historySteps", label: "Steps back", section: "layer", min: 0, max: 50, step: 1, default: 0, decimals: 0, track: plain, unit: "", tools: ["history"] }
|
|
12989
|
+
];
|
|
12990
|
+
var CLONE_TRANSFORM_CONTROLS = [
|
|
12991
|
+
{ id: "width", label: "Source W", section: "layer", min: 1, max: 400, step: 1, default: 100, decimals: 0, track: plain, unit: "%" },
|
|
12992
|
+
{ id: "height", label: "Source H", section: "layer", min: 1, max: 400, step: 1, default: 100, decimals: 0, track: plain, unit: "%" },
|
|
12993
|
+
{ id: "angle", label: "Source angle", section: "layer", min: -180, max: 180, step: 1, default: 0, decimals: 0, track: plain, unit: "\xB0" }
|
|
12994
|
+
];
|
|
12995
|
+
function cloneTransformView(controller) {
|
|
12996
|
+
const t = controller.getState().paint.cloneTransform;
|
|
12997
|
+
return {
|
|
12998
|
+
width: Math.round(Math.abs(t.scaleX) * 100),
|
|
12999
|
+
height: Math.round(Math.abs(t.scaleY) * 100),
|
|
13000
|
+
angle: t.rotate,
|
|
13001
|
+
flipH: t.scaleX < 0,
|
|
13002
|
+
flipV: t.scaleY < 0,
|
|
13003
|
+
identity: t.scaleX === 1 && t.scaleY === 1 && t.rotate === 0
|
|
13004
|
+
};
|
|
13005
|
+
}
|
|
13006
|
+
function setCloneTransformValue(controller, id, value) {
|
|
13007
|
+
const t = controller.getState().paint.cloneTransform;
|
|
13008
|
+
if (id === "width") controller.setCloneTransform({ scaleX: Math.sign(t.scaleX) * (value ?? 100) / 100 });
|
|
13009
|
+
else if (id === "height") controller.setCloneTransform({ scaleY: Math.sign(t.scaleY) * (value ?? 100) / 100 });
|
|
13010
|
+
else if (id === "angle") controller.setCloneTransform({ rotate: value ?? 0 });
|
|
13011
|
+
else if (id === "flipH") controller.setCloneTransform({ scaleX: -t.scaleX });
|
|
13012
|
+
else controller.setCloneTransform({ scaleY: -t.scaleY });
|
|
13013
|
+
}
|
|
13014
|
+
var RETOUCH_CHOICES = [
|
|
13015
|
+
{
|
|
13016
|
+
id: "sample",
|
|
13017
|
+
label: "Sample",
|
|
13018
|
+
tools: ["clone", "heal", "spot-heal"],
|
|
13019
|
+
options: [
|
|
13020
|
+
{ id: "current", label: "Current", title: "Sample the layer being retouched" },
|
|
13021
|
+
{ id: "current-below", label: "Current & Below", title: "Sample this layer and the layers under it" },
|
|
13022
|
+
{ id: "all", label: "All", title: "Sample every layer (retouch onto an empty layer)" }
|
|
13023
|
+
]
|
|
13024
|
+
},
|
|
13025
|
+
{
|
|
13026
|
+
id: "spotType",
|
|
13027
|
+
label: "Type",
|
|
13028
|
+
tools: ["spot-heal"],
|
|
13029
|
+
options: [
|
|
13030
|
+
{ id: "proximity-match", label: "Proximity Match", title: "Copy the best matching patch nearby" },
|
|
13031
|
+
{ id: "content-aware", label: "Content-Aware", title: "Synthesise texture and edges from patches around the spot" },
|
|
13032
|
+
{ id: "smooth", label: "Smooth", title: "Fill smoothly from the edges" }
|
|
13033
|
+
]
|
|
13034
|
+
},
|
|
13035
|
+
{
|
|
13036
|
+
id: "range",
|
|
13037
|
+
label: "Range",
|
|
13038
|
+
tools: ["dodge", "burn"],
|
|
13039
|
+
options: [
|
|
13040
|
+
{ id: "shadows", label: "Shadows", title: "Change the dark tones most" },
|
|
13041
|
+
{ id: "midtones", label: "Midtones", title: "Change the middle tones most" },
|
|
13042
|
+
{ id: "highlights", label: "Highlights", title: "Change the light tones most" }
|
|
13043
|
+
]
|
|
13044
|
+
},
|
|
13045
|
+
{
|
|
13046
|
+
id: "spongeMode",
|
|
13047
|
+
label: "Mode",
|
|
13048
|
+
tools: ["sponge"],
|
|
13049
|
+
options: [
|
|
13050
|
+
{ id: "desaturate", label: "Desaturate", title: "Take colour out" },
|
|
13051
|
+
{ id: "saturate", label: "Saturate", title: "Add colour" }
|
|
13052
|
+
]
|
|
13053
|
+
}
|
|
13054
|
+
];
|
|
13055
|
+
var RETOUCH_TOGGLES = [
|
|
13056
|
+
{ id: "aligned", label: "Aligned", tools: ["clone", "heal"] },
|
|
13057
|
+
{ id: "protectTones", label: "Protect tones", tools: ["dodge", "burn"] },
|
|
13058
|
+
{ id: "vibrance", label: "Vibrance", tools: ["sponge"] }
|
|
13059
|
+
];
|
|
13060
|
+
function setRetouchChoice(controller, id, value) {
|
|
13061
|
+
controller.setPaintOptions({ retouch: { [id]: value } });
|
|
13062
|
+
}
|
|
13063
|
+
function cloneSourceLabel(controller) {
|
|
13064
|
+
const cs = controller.getState().paint.cloneSource;
|
|
13065
|
+
return cs ? `Source ${cs.x}, ${cs.y}${cs.offset ? ` (offset ${cs.offset.x}, ${cs.offset.y})` : ""}` : "Alt-click to set the source";
|
|
13066
|
+
}
|
|
13067
|
+
function rgbUnitToHex(c) {
|
|
13068
|
+
return `#${c.map((v) => Math.round(Math.min(1, Math.max(0, v)) * 255).toString(16).padStart(2, "0")).join("")}`;
|
|
13069
|
+
}
|
|
13070
|
+
function sizeStep(size) {
|
|
13071
|
+
return size < 10 ? 1 : size < 50 ? 5 : size < 100 ? 10 : size < 200 ? 25 : size < 500 ? 50 : 100;
|
|
13072
|
+
}
|
|
13073
|
+
var isTyping = (t) => {
|
|
13074
|
+
const el = t;
|
|
13075
|
+
return Boolean(el && (el.isContentEditable || el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.tagName === "SELECT"));
|
|
13076
|
+
};
|
|
13077
|
+
function handlePaintShortcut(controller, e) {
|
|
13078
|
+
if (e.ctrlKey || e.metaKey || e.altKey || isTyping(e.target)) return false;
|
|
13079
|
+
const brush = controller.getState().paint.brush;
|
|
13080
|
+
const set = (b) => controller.setPaintOptions({ brush: b });
|
|
13081
|
+
const key = e.key;
|
|
13082
|
+
if (handleMaskViewKey(controller, e)) return true;
|
|
13083
|
+
switch (key.toLowerCase()) {
|
|
13084
|
+
case "q":
|
|
13085
|
+
controller.setQuickMaskMode(!controller.quickMaskEditing);
|
|
13086
|
+
return true;
|
|
13087
|
+
case "b":
|
|
13088
|
+
controller.setTool(e.shiftKey || key === "B" ? "mixer" : "brush");
|
|
13089
|
+
return true;
|
|
13090
|
+
case "e":
|
|
13091
|
+
controller.setTool("eraser");
|
|
13092
|
+
return true;
|
|
13093
|
+
case "v":
|
|
13094
|
+
controller.setTool("move");
|
|
13095
|
+
return true;
|
|
13096
|
+
case "s":
|
|
13097
|
+
controller.setTool("clone");
|
|
13098
|
+
return true;
|
|
13099
|
+
case "j":
|
|
13100
|
+
controller.setTool(e.shiftKey || key === "J" ? controller.getState().activeTool === "heal" ? "spot-heal" : "heal" : "spot-heal");
|
|
13101
|
+
return true;
|
|
13102
|
+
case "o": {
|
|
13103
|
+
const cur = controller.getState().activeTool;
|
|
13104
|
+
const next = e.shiftKey || key === "O" ? cur === "dodge" ? "burn" : cur === "burn" ? "sponge" : "dodge" : "dodge";
|
|
13105
|
+
controller.setTool(next);
|
|
13106
|
+
return true;
|
|
13107
|
+
}
|
|
13108
|
+
case "y":
|
|
13109
|
+
controller.setTool("history");
|
|
13110
|
+
return true;
|
|
13111
|
+
case "d":
|
|
13112
|
+
controller.resetPaintColors();
|
|
13113
|
+
return true;
|
|
13114
|
+
case "x":
|
|
13115
|
+
controller.swapPaintColors();
|
|
13116
|
+
return true;
|
|
13117
|
+
}
|
|
13118
|
+
if (key === "[" || key === "{") {
|
|
13119
|
+
if (e.shiftKey || key === "{") set({ hardness: Math.max(0, Math.round((brush.hardness - 0.25) * 4) / 4) });
|
|
13120
|
+
else set({ size: Math.max(1, brush.size - sizeStep(brush.size - 1)) });
|
|
13121
|
+
return true;
|
|
13122
|
+
}
|
|
13123
|
+
if (key === "]" || key === "}") {
|
|
13124
|
+
if (e.shiftKey || key === "}") set({ hardness: Math.min(1, Math.round((brush.hardness + 0.25) * 4) / 4) });
|
|
13125
|
+
else set({ size: Math.min(MAX_BRUSH_SIZE, brush.size + sizeStep(brush.size)) });
|
|
13126
|
+
return true;
|
|
13127
|
+
}
|
|
13128
|
+
if (/^[0-9]$/.test(key) && isPaintTool(controller.getState().activeTool)) {
|
|
13129
|
+
set({ opacity: key === "0" ? 1 : Number(key) / 10 });
|
|
13130
|
+
return true;
|
|
13131
|
+
}
|
|
13132
|
+
return false;
|
|
13133
|
+
}
|
|
13134
|
+
var PaintGesture = class {
|
|
13135
|
+
constructor(controller) {
|
|
13136
|
+
this.controller = controller;
|
|
13137
|
+
}
|
|
13138
|
+
controller;
|
|
13139
|
+
stroke = null;
|
|
13140
|
+
move = null;
|
|
13141
|
+
/** The last refusal (e.g. "layer is locked"), for the editor to show. */
|
|
13142
|
+
error = null;
|
|
13143
|
+
get active() {
|
|
13144
|
+
return Boolean(this.stroke || this.move);
|
|
13145
|
+
}
|
|
13146
|
+
down(sample) {
|
|
13147
|
+
this.error = null;
|
|
13148
|
+
const tool = this.controller.getState().activeTool;
|
|
13149
|
+
try {
|
|
13150
|
+
if (tool === "move") {
|
|
13151
|
+
const layer = this.controller.getActiveLayer();
|
|
13152
|
+
if (!layer) throw new Error("Select a layer to move in the Layers panel.");
|
|
13153
|
+
if (layer.type !== "pixel" && layer.type !== "shape" && layer.type !== "smart") throw new Error(`Only pixel, shape and smart object layers move; "${layer.name}" is a ${layer.type} layer.`);
|
|
13154
|
+
if (layer.locks?.all || layer.locks?.position) throw new Error(`Layer "${layer.name}" has its position locked.`);
|
|
13155
|
+
const origin = layer.type === "pixel" ? { ...layer.offset } : layer.type === "smart" ? { ...layer.placement.corners[0] } : (() => {
|
|
13156
|
+
const b = shapeGeometryBounds(layer);
|
|
13157
|
+
return b ? { x: b.x, y: b.y } : { x: 0, y: 0 };
|
|
13158
|
+
})();
|
|
13159
|
+
this.move = { id: layer.id, start: { x: sample.x, y: sample.y }, origin, moved: false };
|
|
13160
|
+
return null;
|
|
13161
|
+
}
|
|
13162
|
+
if ((tool === "clone" || tool === "heal") && sample.altKey) {
|
|
13163
|
+
this.controller.setCloneSource({ x: sample.x, y: sample.y });
|
|
13164
|
+
return null;
|
|
13165
|
+
}
|
|
13166
|
+
if (isPaintTool(tool)) {
|
|
13167
|
+
this.stroke = this.controller.beginStroke({ tool });
|
|
13168
|
+
this.stroke.push([{ x: sample.x, y: sample.y, pressure: sample.pressure }]);
|
|
13169
|
+
return null;
|
|
13170
|
+
}
|
|
13171
|
+
} catch (err) {
|
|
13172
|
+
this.error = userErrorMessage(err);
|
|
13173
|
+
this.stroke = null;
|
|
13174
|
+
this.move = null;
|
|
13175
|
+
}
|
|
13176
|
+
return this.error;
|
|
13177
|
+
}
|
|
13178
|
+
/** Feeds pointer samples (all coalesced events of one move, in order). */
|
|
13179
|
+
moveTo(samples) {
|
|
13180
|
+
if (!samples.length) return;
|
|
13181
|
+
if (this.stroke) {
|
|
13182
|
+
this.stroke.push(samples);
|
|
13183
|
+
return;
|
|
13184
|
+
}
|
|
13185
|
+
const m = this.move;
|
|
13186
|
+
if (m) {
|
|
13187
|
+
const last = samples[samples.length - 1];
|
|
13188
|
+
const x = m.origin.x + Math.round(last.x - m.start.x);
|
|
13189
|
+
const y = m.origin.y + Math.round(last.y - m.start.y);
|
|
13190
|
+
try {
|
|
13191
|
+
this.controller.offsetLayer(m.id, { x, y }, !m.moved);
|
|
13192
|
+
m.moved = true;
|
|
13193
|
+
} catch (err) {
|
|
13194
|
+
this.error = userErrorMessage(err);
|
|
13195
|
+
this.move = null;
|
|
13196
|
+
}
|
|
13197
|
+
}
|
|
13198
|
+
}
|
|
13199
|
+
up() {
|
|
13200
|
+
if (this.stroke) {
|
|
13201
|
+
try {
|
|
13202
|
+
this.stroke.end();
|
|
13203
|
+
} catch (err) {
|
|
13204
|
+
this.error = userErrorMessage(err);
|
|
13205
|
+
}
|
|
13206
|
+
}
|
|
13207
|
+
this.stroke = null;
|
|
13208
|
+
this.move = null;
|
|
13209
|
+
}
|
|
13210
|
+
cancel() {
|
|
13211
|
+
this.stroke?.cancel();
|
|
13212
|
+
this.stroke = null;
|
|
13213
|
+
this.move = null;
|
|
13214
|
+
}
|
|
13215
|
+
};
|
|
13216
|
+
|
|
12080
13217
|
export {
|
|
12081
13218
|
LEGACY_PROCESS_VERSION,
|
|
12082
13219
|
LATEST_PROCESS_VERSION,
|
|
@@ -12334,6 +13471,12 @@ export {
|
|
|
12334
13471
|
developTarget,
|
|
12335
13472
|
legacyView,
|
|
12336
13473
|
isLegacyShape,
|
|
13474
|
+
EdgeMap,
|
|
13475
|
+
snapToEdge,
|
|
13476
|
+
liveWire,
|
|
13477
|
+
simplifyPixelPath,
|
|
13478
|
+
snapPolygonToEdges,
|
|
13479
|
+
magneticAnchorSpacing,
|
|
12337
13480
|
FREE_TRANSFORM_MODES,
|
|
12338
13481
|
TRANSFORM_ANCHORS,
|
|
12339
13482
|
ANCHOR_FRACTION,
|
|
@@ -12439,7 +13582,37 @@ export {
|
|
|
12439
13582
|
importPsd,
|
|
12440
13583
|
smartContentsFile,
|
|
12441
13584
|
exportPsd,
|
|
13585
|
+
SELECTION_TOOLS,
|
|
13586
|
+
COLOR_RANGE_PRESET_LABELS,
|
|
13587
|
+
SELECTION_MODE_LABELS,
|
|
13588
|
+
DEFAULT_SELECTION_TOOL_OPTIONS,
|
|
13589
|
+
selectionModeFromModifiers,
|
|
13590
|
+
REFINE_EDGE_CONTROLS,
|
|
13591
|
+
REFINE_OUTPUTS,
|
|
13592
|
+
describeSelection,
|
|
12442
13593
|
describeLayerEffects,
|
|
12443
13594
|
createImageEffectTools,
|
|
12444
|
-
createLayerEffectTools
|
|
13595
|
+
createLayerEffectTools,
|
|
13596
|
+
snapTo45,
|
|
13597
|
+
SelectionGesture,
|
|
13598
|
+
handleSelectionShortcut,
|
|
13599
|
+
handleMaskViewKey,
|
|
13600
|
+
handleQuickMaskKey,
|
|
13601
|
+
BRUSH_OPTION_CONTROLS,
|
|
13602
|
+
MIXER_OPTION_CONTROLS,
|
|
13603
|
+
MIXER_MIXING_OPTIONS,
|
|
13604
|
+
PAINT_TOOLS,
|
|
13605
|
+
isPaintTool,
|
|
13606
|
+
isRetouchPaintTool,
|
|
13607
|
+
RETOUCH_OPTION_CONTROLS,
|
|
13608
|
+
CLONE_TRANSFORM_CONTROLS,
|
|
13609
|
+
cloneTransformView,
|
|
13610
|
+
setCloneTransformValue,
|
|
13611
|
+
RETOUCH_CHOICES,
|
|
13612
|
+
RETOUCH_TOGGLES,
|
|
13613
|
+
setRetouchChoice,
|
|
13614
|
+
cloneSourceLabel,
|
|
13615
|
+
rgbUnitToHex,
|
|
13616
|
+
handlePaintShortcut,
|
|
13617
|
+
PaintGesture
|
|
12445
13618
|
};
|