@pixerate/editor 0.96.0 → 0.98.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/THIRD_PARTY_NOTICES.md +4 -2
- package/dist/animation-editor/index.cjs +55 -53
- package/dist/animation-editor/index.cjs.map +1 -1
- package/dist/animation-editor/index.d.cts +3 -2
- package/dist/animation-editor/index.d.ts +3 -2
- package/dist/animation-editor/index.js +11 -9
- package/dist/animation-editor/index.js.map +1 -1
- package/dist/chunk-DWGZUSES.cjs +694 -0
- package/dist/chunk-DWGZUSES.cjs.map +1 -0
- package/dist/chunk-GX332W2C.js +181 -0
- package/dist/chunk-GX332W2C.js.map +1 -0
- package/dist/{chunk-7MD7C6XQ.cjs → chunk-JF5PXN7W.cjs} +96 -744
- package/dist/chunk-JF5PXN7W.cjs.map +1 -0
- package/dist/chunk-KKREERP2.js +694 -0
- package/dist/chunk-KKREERP2.js.map +1 -0
- package/dist/chunk-MUUXNRU4.cjs +181 -0
- package/dist/chunk-MUUXNRU4.cjs.map +1 -0
- package/dist/{chunk-KFTQ2PKP.js → chunk-WIR377F6.js} +1001 -61
- package/dist/chunk-WIR377F6.js.map +1 -0
- package/dist/{chunk-T422YCHR.cjs → chunk-WT6ZAWJP.cjs} +1023 -83
- package/dist/chunk-WT6ZAWJP.cjs.map +1 -0
- package/dist/{chunk-4JLB2USO.js → chunk-YGZNGDIT.js} +45 -693
- package/dist/chunk-YGZNGDIT.js.map +1 -0
- package/dist/image-editor/index.cjs +1519 -360
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +378 -5
- package/dist/image-editor/index.d.ts +378 -5
- package/dist/image-editor/index.js +1244 -85
- package/dist/image-editor/index.js.map +1 -1
- package/dist/raster/index.cjs +25 -2
- package/dist/raster/index.cjs.map +1 -1
- package/dist/raster/index.d.cts +143 -4
- package/dist/raster/index.d.ts +143 -4
- package/dist/raster/index.js +25 -2
- package/dist/{svg-ymShkNtu.d.cts → svg-C9r284x8.d.cts} +6 -9
- package/dist/{svg-Dh17jCyA.d.ts → svg-MVKBqwD4.d.ts} +6 -9
- package/dist/types-CwMPoRMU.d.cts +40 -0
- package/dist/types-CwMPoRMU.d.ts +40 -0
- package/dist/vector/index.cjs +490 -554
- package/dist/vector/index.cjs.map +1 -1
- package/dist/vector/index.d.cts +154 -292
- package/dist/vector/index.d.ts +154 -292
- package/dist/vector/index.js +91 -155
- package/dist/vector/index.js.map +1 -1
- package/dist/warp-B8rAguzs.d.ts +165 -0
- package/dist/warp-nYs2DPrx.d.cts +165 -0
- package/package.json +1 -1
- package/skills/pixerate-image-editor/SKILL.md +49 -3
- package/dist/chunk-4JLB2USO.js.map +0 -1
- package/dist/chunk-7MD7C6XQ.cjs.map +0 -1
- package/dist/chunk-KFTQ2PKP.js.map +0 -1
- package/dist/chunk-T422YCHR.cjs.map +0 -1
|
@@ -7,12 +7,15 @@ import {
|
|
|
7
7
|
import {
|
|
8
8
|
DEFAULT_GLOBAL_LIGHT,
|
|
9
9
|
DIRECT_GAUSSIAN_MAX_SIGMA,
|
|
10
|
+
INTERPOLATIONS,
|
|
10
11
|
LAYER_EFFECT_KINDS,
|
|
11
12
|
LayerEffectError,
|
|
12
13
|
MAX_EFFECT_REACH,
|
|
14
|
+
MAX_TRANSFORM_PIXELS,
|
|
13
15
|
PatternPlacement,
|
|
14
16
|
PatternTile,
|
|
15
17
|
RasterCompositor,
|
|
18
|
+
TransformTooLargeError,
|
|
16
19
|
ZlibError,
|
|
17
20
|
adler32,
|
|
18
21
|
base64ToBytes,
|
|
@@ -21,6 +24,7 @@ import {
|
|
|
21
24
|
builtinPatternIds,
|
|
22
25
|
bytesToBase64,
|
|
23
26
|
changedTiles,
|
|
27
|
+
compositeSurfaceOver,
|
|
24
28
|
curveLut,
|
|
25
29
|
decodePng,
|
|
26
30
|
defaultEffectGradient,
|
|
@@ -41,9 +45,11 @@ import {
|
|
|
41
45
|
inflate,
|
|
42
46
|
inflateRaw,
|
|
43
47
|
insertLayer,
|
|
48
|
+
isInterpolation,
|
|
44
49
|
isLayerEffectKind,
|
|
45
50
|
isLayerLocked,
|
|
46
51
|
isPng,
|
|
52
|
+
keysWeight,
|
|
47
53
|
layerEffectFields,
|
|
48
54
|
layerEffectLabel,
|
|
49
55
|
mapLayer,
|
|
@@ -53,12 +59,27 @@ import {
|
|
|
53
59
|
parseDataUrl,
|
|
54
60
|
parseEffectColor,
|
|
55
61
|
pngDataUrl,
|
|
62
|
+
reduceSurface,
|
|
56
63
|
renderLayerEffects,
|
|
57
64
|
scaleLayerEffect,
|
|
65
|
+
transformSurface,
|
|
66
|
+
transformedBounds,
|
|
58
67
|
validateEffectGradient,
|
|
59
68
|
validateLayerEffect,
|
|
60
|
-
validateLayerEffects
|
|
61
|
-
|
|
69
|
+
validateLayerEffects,
|
|
70
|
+
warpSurface
|
|
71
|
+
} from "../chunk-WIR377F6.js";
|
|
72
|
+
import {
|
|
73
|
+
affineFromHomography,
|
|
74
|
+
applyHomography,
|
|
75
|
+
boxQuad,
|
|
76
|
+
homographyFromAffine,
|
|
77
|
+
homographyFromPoints,
|
|
78
|
+
homographyKeepsQuad,
|
|
79
|
+
invertHomography,
|
|
80
|
+
multiplyHomographies,
|
|
81
|
+
rectToQuad
|
|
82
|
+
} from "../chunk-GX332W2C.js";
|
|
62
83
|
import {
|
|
63
84
|
CoverageMap,
|
|
64
85
|
DEFAULT_BRUSH,
|
|
@@ -136,6 +157,28 @@ import {
|
|
|
136
157
|
uniformTile,
|
|
137
158
|
writeRegion
|
|
138
159
|
} from "../chunk-HEW6KDNI.js";
|
|
160
|
+
import {
|
|
161
|
+
WARP_STYLES,
|
|
162
|
+
WARP_STYLE_LABELS,
|
|
163
|
+
applyMatrix,
|
|
164
|
+
dragWarpMeshPoint,
|
|
165
|
+
evalWarpMesh,
|
|
166
|
+
identityWarpMesh,
|
|
167
|
+
invertMatrix,
|
|
168
|
+
isValidWarpMesh,
|
|
169
|
+
matrixAbout,
|
|
170
|
+
multiplyMatrices,
|
|
171
|
+
pullWarpMesh,
|
|
172
|
+
resizeWarpMesh,
|
|
173
|
+
rotateMatrix,
|
|
174
|
+
translateMatrix,
|
|
175
|
+
warpMeshColumns,
|
|
176
|
+
warpMeshEditPoints,
|
|
177
|
+
warpMeshGridCurves,
|
|
178
|
+
warpMeshParamAt,
|
|
179
|
+
warpMeshRows,
|
|
180
|
+
warpPresetMesh
|
|
181
|
+
} from "../chunk-KKREERP2.js";
|
|
139
182
|
|
|
140
183
|
// src/image-editor/color.ts
|
|
141
184
|
var LEGACY_PROCESS_VERSION = 1;
|
|
@@ -557,20 +600,20 @@ function hitTestCrop(canvasPt, state, vpWidth, vpHeight, handleThreshold = 14, c
|
|
|
557
600
|
}
|
|
558
601
|
function hitTestCropRect(canvasPt, rect, handleThreshold) {
|
|
559
602
|
const { x, y, width: w, height: h } = rect;
|
|
560
|
-
const
|
|
603
|
+
const px2 = canvasPt.x;
|
|
561
604
|
const py = canvasPt.y;
|
|
562
605
|
const dist = (x1, y1, x2, y2) => Math.hypot(x1 - x2, y1 - y2);
|
|
563
|
-
if (dist(
|
|
564
|
-
if (dist(
|
|
565
|
-
if (dist(
|
|
566
|
-
if (dist(
|
|
567
|
-
const nearX =
|
|
606
|
+
if (dist(px2, py, x, y) <= handleThreshold) return "nw";
|
|
607
|
+
if (dist(px2, py, x + w, y) <= handleThreshold) return "ne";
|
|
608
|
+
if (dist(px2, py, x + w, y + h) <= handleThreshold) return "se";
|
|
609
|
+
if (dist(px2, py, x, y + h) <= handleThreshold) return "sw";
|
|
610
|
+
const nearX = px2 >= x - handleThreshold && px2 <= x + w + handleThreshold;
|
|
568
611
|
const nearY = py >= y - handleThreshold && py <= y + h + handleThreshold;
|
|
569
612
|
if (nearX && Math.abs(py - y) <= handleThreshold) return "n";
|
|
570
613
|
if (nearX && Math.abs(py - (y + h)) <= handleThreshold) return "s";
|
|
571
|
-
if (nearY && Math.abs(
|
|
572
|
-
if (nearY && Math.abs(
|
|
573
|
-
if (
|
|
614
|
+
if (nearY && Math.abs(px2 - x) <= handleThreshold) return "w";
|
|
615
|
+
if (nearY && Math.abs(px2 - (x + w)) <= handleThreshold) return "e";
|
|
616
|
+
if (px2 >= x && px2 <= x + w && py >= y && py <= y + h) {
|
|
574
617
|
return "inside";
|
|
575
618
|
}
|
|
576
619
|
return null;
|
|
@@ -1598,13 +1641,13 @@ function hslToRgb(h, s, l) {
|
|
|
1598
1641
|
};
|
|
1599
1642
|
return [f(h + 1 / 3), f(h), f(h - 1 / 3)];
|
|
1600
1643
|
}
|
|
1601
|
-
function hueRangeWeight(range,
|
|
1644
|
+
function hueRangeWeight(range, deg2) {
|
|
1602
1645
|
const [a, b0, c0, d0] = range.bounds;
|
|
1603
1646
|
const rel = (x) => Number.isFinite(x) && Number.isFinite(a) ? remEuclid(x - a, 360) : 0;
|
|
1604
1647
|
const b = rel(b0);
|
|
1605
1648
|
const c = Math.max(rel(c0), b);
|
|
1606
1649
|
const d = Math.max(rel(d0), c);
|
|
1607
|
-
const h = rel(
|
|
1650
|
+
const h = rel(deg2);
|
|
1608
1651
|
if (h <= b) return b <= 0 ? 1 : h / b;
|
|
1609
1652
|
if (h <= c) return 1;
|
|
1610
1653
|
if (h <= d) return d <= c ? 1 : (d - h) / (d - c);
|
|
@@ -1616,9 +1659,9 @@ function hueRangeTables(ranges) {
|
|
|
1616
1659
|
const t = [new Float32Array(n), new Float32Array(n), new Float32Array(n)];
|
|
1617
1660
|
const active = ranges.filter((r) => !isNeutralRange(r));
|
|
1618
1661
|
for (let k = 0; k < n; k++) {
|
|
1619
|
-
const
|
|
1662
|
+
const deg2 = k / (n - 1) * 360;
|
|
1620
1663
|
for (const r of active) {
|
|
1621
|
-
const w = hueRangeWeight(r,
|
|
1664
|
+
const w = hueRangeWeight(r, deg2);
|
|
1622
1665
|
t[0][k] += w * r.hue;
|
|
1623
1666
|
t[1][k] += w * r.saturation / 100;
|
|
1624
1667
|
t[2][k] += w * r.lightness / 100;
|
|
@@ -2181,8 +2224,8 @@ function applyFastBoxBlur(imageData, radius) {
|
|
|
2181
2224
|
let r = 0, g = 0, b = 0, a = 0;
|
|
2182
2225
|
let count = 0;
|
|
2183
2226
|
for (let k = -radius; k <= radius; k++) {
|
|
2184
|
-
const
|
|
2185
|
-
const idx = (y * w +
|
|
2227
|
+
const px2 = Math.min(w - 1, Math.max(0, x + k));
|
|
2228
|
+
const idx = (y * w + px2) * 4;
|
|
2186
2229
|
r += src[idx];
|
|
2187
2230
|
g += src[idx + 1];
|
|
2188
2231
|
b += src[idx + 2];
|
|
@@ -3137,13 +3180,13 @@ var ImageEditorRenderer = class {
|
|
|
3137
3180
|
ctx.save();
|
|
3138
3181
|
try {
|
|
3139
3182
|
applyImageProjectionToContext(ctx, projection);
|
|
3140
|
-
const
|
|
3183
|
+
const px2 = 1 / (projection.scale || 1);
|
|
3141
3184
|
ctx.beginPath();
|
|
3142
|
-
ctx.arc(b.x, b.y, Math.max(
|
|
3143
|
-
ctx.lineWidth = 2 *
|
|
3185
|
+
ctx.arc(b.x, b.y, Math.max(px2, b.diameter / 2), 0, Math.PI * 2);
|
|
3186
|
+
ctx.lineWidth = 2 * px2;
|
|
3144
3187
|
ctx.strokeStyle = "rgba(0,0,0,0.6)";
|
|
3145
3188
|
ctx.stroke();
|
|
3146
|
-
ctx.lineWidth =
|
|
3189
|
+
ctx.lineWidth = px2;
|
|
3147
3190
|
ctx.strokeStyle = "#ffffff";
|
|
3148
3191
|
ctx.stroke();
|
|
3149
3192
|
} catch {
|
|
@@ -3426,32 +3469,32 @@ var ImageEditorRenderer = class {
|
|
|
3426
3469
|
this.outlineCache.set(selection, segs);
|
|
3427
3470
|
}
|
|
3428
3471
|
if (!segs.length) return;
|
|
3429
|
-
const
|
|
3472
|
+
const px2 = 1 / (scale || 1);
|
|
3430
3473
|
ctx.beginPath();
|
|
3431
3474
|
for (let i = 0; i < segs.length; i += 4) {
|
|
3432
3475
|
ctx.moveTo(segs[i], segs[i + 1]);
|
|
3433
3476
|
ctx.lineTo(segs[i + 2], segs[i + 3]);
|
|
3434
3477
|
}
|
|
3435
|
-
ctx.lineWidth =
|
|
3478
|
+
ctx.lineWidth = px2;
|
|
3436
3479
|
ctx.strokeStyle = "#ffffff";
|
|
3437
3480
|
ctx.stroke();
|
|
3438
3481
|
if (typeof ctx.setLineDash === "function") {
|
|
3439
|
-
ctx.setLineDash([4 *
|
|
3440
|
-
ctx.lineDashOffset = -offset *
|
|
3482
|
+
ctx.setLineDash([4 * px2, 4 * px2]);
|
|
3483
|
+
ctx.lineDashOffset = -offset * px2;
|
|
3441
3484
|
}
|
|
3442
3485
|
ctx.strokeStyle = "#000000";
|
|
3443
3486
|
ctx.stroke();
|
|
3444
3487
|
if (typeof ctx.setLineDash === "function") ctx.setLineDash([]);
|
|
3445
3488
|
}
|
|
3446
3489
|
drawDraftPath(ctx, points, scale) {
|
|
3447
|
-
const
|
|
3490
|
+
const px2 = 1 / (scale || 1);
|
|
3448
3491
|
ctx.beginPath();
|
|
3449
3492
|
points.forEach((p, i) => i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y));
|
|
3450
3493
|
ctx.closePath();
|
|
3451
|
-
ctx.lineWidth =
|
|
3494
|
+
ctx.lineWidth = px2;
|
|
3452
3495
|
ctx.strokeStyle = "#ffffff";
|
|
3453
3496
|
ctx.stroke();
|
|
3454
|
-
if (typeof ctx.setLineDash === "function") ctx.setLineDash([4 *
|
|
3497
|
+
if (typeof ctx.setLineDash === "function") ctx.setLineDash([4 * px2, 4 * px2]);
|
|
3455
3498
|
ctx.strokeStyle = "#000000";
|
|
3456
3499
|
ctx.stroke();
|
|
3457
3500
|
if (typeof ctx.setLineDash === "function") ctx.setLineDash([]);
|
|
@@ -4297,8 +4340,8 @@ var gfRadius = (p) => Math.min(64, Math.max(1, Math.ceil(p.radius)));
|
|
|
4297
4340
|
var smoothSigma = (p) => p.smooth / 100 * 4;
|
|
4298
4341
|
var featherSigma = (p) => p.feather / 2;
|
|
4299
4342
|
function shiftAmount(p) {
|
|
4300
|
-
const
|
|
4301
|
-
return Number.isFinite(
|
|
4343
|
+
const px2 = p.shiftEdge / 100 * Math.max(p.radius, 4);
|
|
4344
|
+
return Number.isFinite(px2) ? Math.min(64, Math.max(-64, px2)) : 0;
|
|
4302
4345
|
}
|
|
4303
4346
|
function refineEdgeHalo(params = {}) {
|
|
4304
4347
|
const p = normalizeRefineEdgeParams(params);
|
|
@@ -4584,6 +4627,7 @@ function serializeMask(mask, hint, ctx) {
|
|
|
4584
4627
|
if (mask.density !== void 0 && mask.density !== 1) out.density = mask.density;
|
|
4585
4628
|
if (mask.feather) out.feather = mask.feather;
|
|
4586
4629
|
if (mask.enabled === false) out.enabled = false;
|
|
4630
|
+
if (mask.linked === false) out.linked = false;
|
|
4587
4631
|
return out;
|
|
4588
4632
|
}
|
|
4589
4633
|
function serializeLayer(layer, ctx) {
|
|
@@ -4702,8 +4746,8 @@ function readSurface(ref, x, y, channels, defaultValue, ctx, what) {
|
|
|
4702
4746
|
return emptySurface(channels, defaultValue);
|
|
4703
4747
|
}
|
|
4704
4748
|
try {
|
|
4705
|
-
const
|
|
4706
|
-
return surfaceFromPixels(
|
|
4749
|
+
const px2 = (ctx.decodeAsset ?? decodePngDataUrl)(url, channels);
|
|
4750
|
+
return surfaceFromPixels(px2, channels, Math.round(num(x, -1e7, 1e7, 0)), Math.round(num(y, -1e7, 1e7, 0)), defaultValue);
|
|
4707
4751
|
} catch (err) {
|
|
4708
4752
|
ctx.problems.push(`${what}: asset "${ref}" could not be decoded (${err.message})`);
|
|
4709
4753
|
return emptySurface(channels, defaultValue);
|
|
@@ -4720,6 +4764,7 @@ function readMask(raw, ctx, what) {
|
|
|
4720
4764
|
if (typeof r.density === "number" && r.density !== 1) mask.density = num(r.density, 0, 1, 1);
|
|
4721
4765
|
if (typeof r.feather === "number" && r.feather > 0) mask.feather = num(r.feather, 0, 1e3, 0);
|
|
4722
4766
|
if (r.enabled === false) mask.enabled = false;
|
|
4767
|
+
if (r.linked === false) mask.linked = false;
|
|
4723
4768
|
if (r.source && typeof r.source === "object") {
|
|
4724
4769
|
const s = { ...DEFAULT_DEPTH_MASK_VALUES, ...r.source };
|
|
4725
4770
|
mask.source = { ...s, depthRange: [...s.depthRange] };
|
|
@@ -4989,7 +5034,12 @@ function offsetLayer(layers, id, offset) {
|
|
|
4989
5034
|
requireUnlocked(loc.layer, "position", "move it");
|
|
4990
5035
|
if (!Number.isFinite(offset.x) || !Number.isFinite(offset.y)) fail("x and y must be numbers.");
|
|
4991
5036
|
const layer = loc.layer;
|
|
4992
|
-
|
|
5037
|
+
const next = { ...layer, offset: { x: Math.round(offset.x), y: Math.round(offset.y) } };
|
|
5038
|
+
const mask = layer.mask;
|
|
5039
|
+
if (mask && mask.linked === false && !mask.source) {
|
|
5040
|
+
next.mask = { ...mask, surface: translateSurface(mask.surface, layer.offset.x - next.offset.x, layer.offset.y - next.offset.y) };
|
|
5041
|
+
}
|
|
5042
|
+
return mapLayer(layers, id, () => next);
|
|
4993
5043
|
}
|
|
4994
5044
|
function setLayerMask(layers, id, mask) {
|
|
4995
5045
|
const loc = requireLayer(layers, id);
|
|
@@ -6175,6 +6225,7 @@ var CHANNEL_USER_MASK = -2;
|
|
|
6175
6225
|
var CHANNEL_REAL_USER_MASK = -3;
|
|
6176
6226
|
var LAYER_FLAG_TRANSPARENCY_PROTECTED = 1;
|
|
6177
6227
|
var LAYER_FLAG_HIDDEN = 2;
|
|
6228
|
+
var MASK_FLAG_UNLINKED = 1;
|
|
6178
6229
|
var MASK_FLAG_DISABLED = 2;
|
|
6179
6230
|
var MASK_FLAG_PARAMETERS = 16;
|
|
6180
6231
|
var SECTION_OTHER = 0;
|
|
@@ -7604,6 +7655,7 @@ function recordMask(ctx, rec, name) {
|
|
|
7604
7655
|
if (m.userDensity !== void 0 && m.userDensity < 255) mask.density = percent(m.userDensity);
|
|
7605
7656
|
if (m.userFeather !== void 0 && m.userFeather > 0) mask.feather = Math.min(1e3, m.userFeather);
|
|
7606
7657
|
if (flags & MASK_FLAG_DISABLED) mask.enabled = false;
|
|
7658
|
+
if (flags & MASK_FLAG_UNLINKED) mask.linked = false;
|
|
7607
7659
|
return mask;
|
|
7608
7660
|
}
|
|
7609
7661
|
function baseProps(ctx, rec, name, notes) {
|
|
@@ -7890,7 +7942,8 @@ function maskChannel(mask, ox, oy, psb) {
|
|
|
7890
7942
|
if (mask.source) throw new PsdError("A parametric mask must be baked before export.");
|
|
7891
7943
|
const bounds = contentBounds(mask.surface);
|
|
7892
7944
|
const def = mask.defaultValue;
|
|
7893
|
-
const
|
|
7945
|
+
const flags = (mask.enabled === false ? MASK_FLAG_DISABLED : 0) | (mask.linked === false ? MASK_FLAG_UNLINKED : 0);
|
|
7946
|
+
const m = { rect: EMPTY_RECT, defaultColor: def, flags };
|
|
7894
7947
|
if ((mask.density ?? 1) < 1) m.userDensity = Math.round(Math.max(0, mask.density ?? 1) * 255);
|
|
7895
7948
|
if ((mask.feather ?? 0) > 0) m.userFeather = mask.feather;
|
|
7896
7949
|
if (!bounds) return { channel: emptyChannel(CHANNEL_USER_MASK), mask: m };
|
|
@@ -8087,6 +8140,499 @@ function exportPsd(input) {
|
|
|
8087
8140
|
return { bytes, psb, records: records.length, warnings };
|
|
8088
8141
|
}
|
|
8089
8142
|
|
|
8143
|
+
// src/image-editor/transform.ts
|
|
8144
|
+
var FREE_TRANSFORM_MODES = ["scale", "rotate", "skew", "distort", "perspective", "warp"];
|
|
8145
|
+
var TRANSFORM_ANCHORS = ["top-left", "top", "top-right", "left", "center", "right", "bottom-left", "bottom", "bottom-right"];
|
|
8146
|
+
var ANCHOR_FRACTION = {
|
|
8147
|
+
"top-left": [0, 0],
|
|
8148
|
+
top: [0.5, 0],
|
|
8149
|
+
"top-right": [1, 0],
|
|
8150
|
+
left: [0, 0.5],
|
|
8151
|
+
center: [0.5, 0.5],
|
|
8152
|
+
right: [1, 0.5],
|
|
8153
|
+
"bottom-left": [0, 1],
|
|
8154
|
+
bottom: [0.5, 1],
|
|
8155
|
+
"bottom-right": [1, 1]
|
|
8156
|
+
};
|
|
8157
|
+
function isTransformAnchor(value) {
|
|
8158
|
+
return typeof value === "string" && value in ANCHOR_FRACTION;
|
|
8159
|
+
}
|
|
8160
|
+
function identityTransform(box) {
|
|
8161
|
+
return { box: { ...box }, corners: boxQuad(box) };
|
|
8162
|
+
}
|
|
8163
|
+
function cornersFromMatrix(box, m) {
|
|
8164
|
+
const q2 = boxQuad(box);
|
|
8165
|
+
return [applyMatrix(m, q2[0]), applyMatrix(m, q2[1]), applyMatrix(m, q2[2]), applyMatrix(m, q2[3])];
|
|
8166
|
+
}
|
|
8167
|
+
function matrixFromCorners(box, corners, eps = 1e-6) {
|
|
8168
|
+
const [tl, tr, br, bl] = corners;
|
|
8169
|
+
const size = 1 + Math.max(box.width, box.height);
|
|
8170
|
+
if (Math.abs(tl.x + br.x - tr.x - bl.x) > eps * size || Math.abs(tl.y + br.y - tr.y - bl.y) > eps * size) return null;
|
|
8171
|
+
const h = rectToQuad(box, corners);
|
|
8172
|
+
return h ? affineFromHomography(h, 1e-9) : null;
|
|
8173
|
+
}
|
|
8174
|
+
function transformHomography(geometry) {
|
|
8175
|
+
const h = rectToQuad(geometry.box, geometry.corners);
|
|
8176
|
+
if (!h) return null;
|
|
8177
|
+
return homographyKeepsQuad(h, boxQuad(geometry.box)) ? h : null;
|
|
8178
|
+
}
|
|
8179
|
+
function hasWarp(geometry) {
|
|
8180
|
+
const w = geometry.warp;
|
|
8181
|
+
if (!w) return false;
|
|
8182
|
+
const id = identityWarpMesh(geometry.box, w.us.length - 1, w.vs.length - 1);
|
|
8183
|
+
if (id.points.length !== w.points.length || id.us.some((u, i) => Math.abs(u - w.us[i]) > 1e-12) || id.vs.some((v, i) => Math.abs(v - w.vs[i]) > 1e-12)) return true;
|
|
8184
|
+
return w.points.some((p, i) => Math.abs(p.x - id.points[i].x) > 1e-6 || Math.abs(p.y - id.points[i].y) > 1e-6);
|
|
8185
|
+
}
|
|
8186
|
+
function isIdentityTransform(geometry) {
|
|
8187
|
+
if (hasWarp(geometry)) return false;
|
|
8188
|
+
const q2 = boxQuad(geometry.box);
|
|
8189
|
+
return geometry.corners.every((c, i) => Math.abs(c.x - q2[i].x) < 1e-6 && Math.abs(c.y - q2[i].y) < 1e-6);
|
|
8190
|
+
}
|
|
8191
|
+
function transformMap(geometry) {
|
|
8192
|
+
const h = transformHomography(geometry);
|
|
8193
|
+
if (!h) return null;
|
|
8194
|
+
const { box } = geometry;
|
|
8195
|
+
const warp = hasWarp(geometry) ? geometry.warp : null;
|
|
8196
|
+
if (!warp) return (p) => applyHomography(h, p);
|
|
8197
|
+
return (p) => applyHomography(h, evalWarpMesh(warp, (p.x - box.x) / box.width, (p.y - box.y) / box.height));
|
|
8198
|
+
}
|
|
8199
|
+
function transformedPoint(geometry, fx, fy) {
|
|
8200
|
+
const f = transformMap(geometry);
|
|
8201
|
+
const { box } = geometry;
|
|
8202
|
+
return f ? f({ x: box.x + fx * box.width, y: box.y + fy * box.height }) : null;
|
|
8203
|
+
}
|
|
8204
|
+
function anchorPoint(geometry, anchor) {
|
|
8205
|
+
const [fx, fy] = ANCHOR_FRACTION[anchor];
|
|
8206
|
+
const h = rectToQuad(geometry.box, geometry.corners);
|
|
8207
|
+
const p = { x: geometry.box.x + fx * geometry.box.width, y: geometry.box.y + fy * geometry.box.height };
|
|
8208
|
+
return (h && applyHomography(h, p)) ?? p;
|
|
8209
|
+
}
|
|
8210
|
+
function transformedBox(geometry) {
|
|
8211
|
+
const f = transformMap(geometry);
|
|
8212
|
+
if (!f) return null;
|
|
8213
|
+
const { box } = geometry;
|
|
8214
|
+
const pts = [];
|
|
8215
|
+
const n = hasWarp(geometry) ? 16 : 1;
|
|
8216
|
+
for (let i = 0; i <= n; i++) {
|
|
8217
|
+
const t = i / n;
|
|
8218
|
+
for (const [fx, fy] of [[t, 0], [1, t], [1 - t, 1], [0, 1 - t]]) {
|
|
8219
|
+
const p = f({ x: box.x + fx * box.width, y: box.y + fy * box.height });
|
|
8220
|
+
if (!p) return null;
|
|
8221
|
+
pts.push(p);
|
|
8222
|
+
}
|
|
8223
|
+
}
|
|
8224
|
+
const xs = pts.map((p) => p.x);
|
|
8225
|
+
const ys = pts.map((p) => p.y);
|
|
8226
|
+
const x = Math.min(...xs);
|
|
8227
|
+
const y = Math.min(...ys);
|
|
8228
|
+
return { x, y, width: Math.max(...xs) - x, height: Math.max(...ys) - y };
|
|
8229
|
+
}
|
|
8230
|
+
function referencePoint(box, reference) {
|
|
8231
|
+
if (reference && typeof reference === "object") return { x: reference.x, y: reference.y };
|
|
8232
|
+
const [fx, fy] = ANCHOR_FRACTION[reference ?? "center"];
|
|
8233
|
+
return { x: box.x + fx * box.width, y: box.y + fy * box.height };
|
|
8234
|
+
}
|
|
8235
|
+
function matrixFromParams(box, params) {
|
|
8236
|
+
const sx = (params.scaleX ?? 100) / 100;
|
|
8237
|
+
const sy = (params.scaleY ?? 100) / 100;
|
|
8238
|
+
const tx = Math.tan((params.skewX ?? 0) * Math.PI / 180);
|
|
8239
|
+
const ty = Math.tan((params.skewY ?? 0) * Math.PI / 180);
|
|
8240
|
+
const ref = referencePoint(box, params.reference);
|
|
8241
|
+
const linear = multiplyMatrices(multiplyMatrices([1, ty, tx, 1, 0, 0], [sx, 0, 0, sy, 0, 0]), rotateMatrix(params.rotate ?? 0));
|
|
8242
|
+
return multiplyMatrices(matrixAbout(linear, ref), translateMatrix(params.dx ?? 0, params.dy ?? 0));
|
|
8243
|
+
}
|
|
8244
|
+
function describeTransform(geometry, reference = "center") {
|
|
8245
|
+
const m = hasWarp(geometry) ? null : matrixFromCorners(geometry.box, geometry.corners);
|
|
8246
|
+
const ref = referencePoint(geometry.box, reference);
|
|
8247
|
+
const bounds = transformedBox(geometry);
|
|
8248
|
+
if (!m) {
|
|
8249
|
+
const h = rectToQuad(geometry.box, geometry.corners);
|
|
8250
|
+
const at2 = (h && applyHomography(h, ref)) ?? ref;
|
|
8251
|
+
return { affine: false, scaleX: 100, scaleY: 100, rotate: 0, skewX: 0, x: at2.x, y: at2.y, bounds, corners: geometry.corners };
|
|
8252
|
+
}
|
|
8253
|
+
const [a, b, c, d] = m;
|
|
8254
|
+
const sxAbs = Math.hypot(a, b);
|
|
8255
|
+
const rot = Math.atan2(b, a) * 180 / Math.PI;
|
|
8256
|
+
const shear = sxAbs ? (a * c + b * d) / sxAbs : 0;
|
|
8257
|
+
const sy = sxAbs ? (a * d - b * c) / sxAbs : 0;
|
|
8258
|
+
const skew = sxAbs ? Math.atan(shear / sxAbs) * 180 / Math.PI : 0;
|
|
8259
|
+
const at = applyMatrix(m, ref);
|
|
8260
|
+
const clean = (v) => Math.abs(v) < 1e-9 ? 0 : v;
|
|
8261
|
+
return {
|
|
8262
|
+
affine: true,
|
|
8263
|
+
scaleX: clean(sxAbs * 100),
|
|
8264
|
+
scaleY: clean(sy * 100),
|
|
8265
|
+
rotate: clean(rot === -180 ? 180 : rot),
|
|
8266
|
+
skewX: clean(skew),
|
|
8267
|
+
x: at.x,
|
|
8268
|
+
y: at.y,
|
|
8269
|
+
bounds,
|
|
8270
|
+
corners: geometry.corners
|
|
8271
|
+
};
|
|
8272
|
+
}
|
|
8273
|
+
function applyParams(geometry, params) {
|
|
8274
|
+
const ref = params.reference && typeof params.reference === "object" ? params.reference : anchorPoint(geometry, params.reference ?? "center");
|
|
8275
|
+
const m = matrixFromParams({ x: ref.x, y: ref.y, width: 0, height: 0 }, { ...params, reference: ref });
|
|
8276
|
+
const c = geometry.corners;
|
|
8277
|
+
return [applyMatrix(m, c[0]), applyMatrix(m, c[1]), applyMatrix(m, c[2]), applyMatrix(m, c[3])];
|
|
8278
|
+
}
|
|
8279
|
+
function flipCorners(geometry, axis) {
|
|
8280
|
+
const c = anchorPoint(geometry, "center");
|
|
8281
|
+
const m = axis === "horizontal" ? [-1, 0, 0, 1, 2 * c.x, 0] : [1, 0, 0, -1, 0, 2 * c.y];
|
|
8282
|
+
const q2 = geometry.corners;
|
|
8283
|
+
return [applyMatrix(m, q2[0]), applyMatrix(m, q2[1]), applyMatrix(m, q2[2]), applyMatrix(m, q2[3])];
|
|
8284
|
+
}
|
|
8285
|
+
function boxPointOf(geometry, p) {
|
|
8286
|
+
const m = matrixFromCorners(geometry.box, geometry.corners);
|
|
8287
|
+
if (m) return applyMatrix(invertMatrix(m), p);
|
|
8288
|
+
return null;
|
|
8289
|
+
}
|
|
8290
|
+
function presetWarp(box, preset) {
|
|
8291
|
+
return warpPresetMesh(preset, box);
|
|
8292
|
+
}
|
|
8293
|
+
function isValidWarp(mesh) {
|
|
8294
|
+
return isValidWarpMesh(mesh);
|
|
8295
|
+
}
|
|
8296
|
+
function geometryHomography(geometry) {
|
|
8297
|
+
const m = matrixFromCorners(geometry.box, geometry.corners);
|
|
8298
|
+
return m ? homographyFromAffine(m) : transformHomography(geometry);
|
|
8299
|
+
}
|
|
8300
|
+
|
|
8301
|
+
// src/image-editor/free-transform.ts
|
|
8302
|
+
var FREE_TRANSFORM_CONTENTS = ["auto", "layer", "selection", "selection-copy", "selection-outline"];
|
|
8303
|
+
var fail2 = (message) => {
|
|
8304
|
+
throw new LayerOperationError(message);
|
|
8305
|
+
};
|
|
8306
|
+
var T = (x, y) => [1, 0, x, 0, 1, y, 0, 0, 1];
|
|
8307
|
+
function previewStep(scale) {
|
|
8308
|
+
if (!scale || !(scale > 0) || scale >= 1) return 1;
|
|
8309
|
+
return Math.min(8, 2 ** Math.floor(Math.log2(1 / scale)));
|
|
8310
|
+
}
|
|
8311
|
+
function toDoc(r, o) {
|
|
8312
|
+
return r ? { x: r.x + o.x, y: r.y + o.y, width: r.width, height: r.height } : null;
|
|
8313
|
+
}
|
|
8314
|
+
function selectionSurface(sel) {
|
|
8315
|
+
return surfaceFromPixels({ width: sel.width, height: sel.height, data: sel.data }, 1, 0, 0, 0);
|
|
8316
|
+
}
|
|
8317
|
+
function liftPixels(surface, sel, o) {
|
|
8318
|
+
const sb = sel.bounds;
|
|
8319
|
+
const area = { x: sb.x - o.x, y: sb.y - o.y, width: sb.width, height: sb.height };
|
|
8320
|
+
const px2 = readRegion(surface, area);
|
|
8321
|
+
const kept = new Uint8ClampedArray(px2);
|
|
8322
|
+
for (let y = 0; y < area.height; y++) {
|
|
8323
|
+
for (let x = 0; x < area.width; x++) {
|
|
8324
|
+
const k = sel.data[(sb.y + y) * sel.width + sb.x + x] / 255;
|
|
8325
|
+
const i = (y * area.width + x) * 4;
|
|
8326
|
+
if (k <= 0) {
|
|
8327
|
+
px2[i] = px2[i + 1] = px2[i + 2] = px2[i + 3] = 0;
|
|
8328
|
+
continue;
|
|
8329
|
+
}
|
|
8330
|
+
px2[i + 3] = px2[i + 3] * k;
|
|
8331
|
+
if (!px2[i + 3]) px2[i] = px2[i + 1] = px2[i + 2] = 0;
|
|
8332
|
+
kept[i + 3] = kept[i + 3] * (1 - k);
|
|
8333
|
+
if (!kept[i + 3]) kept[i] = kept[i + 1] = kept[i + 2] = 0;
|
|
8334
|
+
}
|
|
8335
|
+
}
|
|
8336
|
+
return { lifted: surfaceFromPixels({ width: area.width, height: area.height, data: px2 }, 4, area.x, area.y), rest: writeRegion(surface, area, kept) };
|
|
8337
|
+
}
|
|
8338
|
+
function liftMask(mask, sel, o) {
|
|
8339
|
+
const sb = sel.bounds;
|
|
8340
|
+
const area = { x: sb.x - o.x, y: sb.y - o.y, width: sb.width, height: sb.height };
|
|
8341
|
+
const v = readRegion(mask, area);
|
|
8342
|
+
const def = mask.defaultValue;
|
|
8343
|
+
const lifted = new Uint8ClampedArray(area.width * area.height * 4);
|
|
8344
|
+
const rest = new Uint8ClampedArray(v.length);
|
|
8345
|
+
for (let y = 0; y < area.height; y++) {
|
|
8346
|
+
for (let x = 0; x < area.width; x++) {
|
|
8347
|
+
const i = y * area.width + x;
|
|
8348
|
+
const k = sel.data[(sb.y + y) * sel.width + sb.x + x] / 255;
|
|
8349
|
+
const g = v[i];
|
|
8350
|
+
if (k > 0) {
|
|
8351
|
+
lifted[i * 4] = lifted[i * 4 + 1] = lifted[i * 4 + 2] = g;
|
|
8352
|
+
lifted[i * 4 + 3] = k * 255;
|
|
8353
|
+
}
|
|
8354
|
+
rest[i] = g * (1 - k) + def * k;
|
|
8355
|
+
}
|
|
8356
|
+
}
|
|
8357
|
+
return { lifted: surfaceFromPixels({ width: area.width, height: area.height, data: lifted }, 4, area.x, area.y), rest: writeRegion(mask, area, rest) };
|
|
8358
|
+
}
|
|
8359
|
+
var FreeTransformSession = class {
|
|
8360
|
+
constructor(host, init = {}) {
|
|
8361
|
+
this.host = host;
|
|
8362
|
+
if (init.interpolation !== void 0 && !isInterpolation(init.interpolation)) fail2(`Unknown interpolation "${String(init.interpolation)}"; use nearest, bilinear or bicubic.`);
|
|
8363
|
+
this.interp = init.interpolation ?? "bicubic";
|
|
8364
|
+
const sel = host.selection();
|
|
8365
|
+
const selected = Boolean(sel?.bounds);
|
|
8366
|
+
let content = init.content ?? "auto";
|
|
8367
|
+
if (!["auto", "layer", "selection", "selection-copy", "selection-outline"].includes(content)) {
|
|
8368
|
+
fail2(`Unknown content "${String(content)}"; use auto, layer, selection, selection-copy or selection-outline.`);
|
|
8369
|
+
}
|
|
8370
|
+
if (content === "auto") content = selected ? "selection" : "layer";
|
|
8371
|
+
this.content = content;
|
|
8372
|
+
if (content !== "layer" && !selected) fail2(`Nothing is selected; ${content} needs a selection. Select first, or use content layer.`);
|
|
8373
|
+
const layers = host.layers();
|
|
8374
|
+
this.startLayers = layers;
|
|
8375
|
+
this.startSelection = sel;
|
|
8376
|
+
this.startActive = host.activeLayerId();
|
|
8377
|
+
this.before = host.capture();
|
|
8378
|
+
const moves = [];
|
|
8379
|
+
let box = null;
|
|
8380
|
+
if (content === "selection-outline") {
|
|
8381
|
+
this.layerId = null;
|
|
8382
|
+
this.parts.push({ what: "selection", offset: { x: 0, y: 0 }, moving: selectionSurface(sel), base: null, reduced: /* @__PURE__ */ new Map() });
|
|
8383
|
+
moves.push("selection");
|
|
8384
|
+
box = { ...sel.bounds };
|
|
8385
|
+
} else {
|
|
8386
|
+
const id = init.layerId ?? host.activeLayerId();
|
|
8387
|
+
if (!id) fail2("No layer given and none is active; pass layerId (see list_layers).");
|
|
8388
|
+
const layer = requireLayer(layers, id).layer;
|
|
8389
|
+
this.layerId = layer.id;
|
|
8390
|
+
const mask = layer.mask && !layer.mask.source ? layer.mask : void 0;
|
|
8391
|
+
const channel = init.channel ?? (layer.id === host.activeLayerId() ? host.activeTarget() : "pixels");
|
|
8392
|
+
if (channel !== "pixels" && channel !== "mask") fail2(`Unknown channel "${String(channel)}"; use pixels or mask.`);
|
|
8393
|
+
const linked = mask?.linked !== false;
|
|
8394
|
+
const maskAlone = channel === "mask" && (!linked || layer.type !== "pixel");
|
|
8395
|
+
if (channel === "mask" && !mask) fail2(layer.mask?.source ? `Layer "${layer.name || id}" has a depth mask, which is generated, not transformed.` : `Layer "${layer.name || id}" has no mask to transform; add one with add_layer_mask, or transform its pixels.`);
|
|
8396
|
+
if (!maskAlone && layer.type !== "pixel") fail2(`Layer "${layer.name || id}" is a ${layer.type} layer; Free Transform works on pixel layers, layer masks (channel mask) and selections.`);
|
|
8397
|
+
if (!layer.visible && content !== "selection-copy") fail2(`Layer "${layer.name || id}" is hidden; show it before you transform it.`);
|
|
8398
|
+
if (content !== "selection-copy") {
|
|
8399
|
+
requireUnlocked(layer, "position", "transform it");
|
|
8400
|
+
if (!maskAlone) requireUnlocked(layer, "pixels", "transform it");
|
|
8401
|
+
}
|
|
8402
|
+
const o = layer.type === "pixel" ? layer.offset : { x: 0, y: 0 };
|
|
8403
|
+
const pixels = layer.type === "pixel" ? host.pixelsOf(layer) : null;
|
|
8404
|
+
if (maskAlone) {
|
|
8405
|
+
if (content === "selection-copy") fail2("Layer via Copy copies pixels; transform a mask with content layer or selection.");
|
|
8406
|
+
if (content === "selection") {
|
|
8407
|
+
const { lifted, rest } = liftMask(mask.surface, sel, o);
|
|
8408
|
+
this.parts.push({ what: "mask", offset: o, moving: lifted, base: rest, reduced: /* @__PURE__ */ new Map() });
|
|
8409
|
+
box = toDoc(contentBounds(lifted), o);
|
|
8410
|
+
} else {
|
|
8411
|
+
this.parts.push({ what: "mask", offset: o, moving: mask.surface, base: null, reduced: /* @__PURE__ */ new Map() });
|
|
8412
|
+
box = toDoc(contentBounds(mask.surface), o);
|
|
8413
|
+
}
|
|
8414
|
+
moves.push("mask");
|
|
8415
|
+
} else if (content === "layer") {
|
|
8416
|
+
this.parts.push({ what: "pixels", offset: o, moving: pixels, base: null, reduced: /* @__PURE__ */ new Map() });
|
|
8417
|
+
moves.push("pixels");
|
|
8418
|
+
box = toDoc(contentBounds(pixels), o);
|
|
8419
|
+
if (mask && linked) {
|
|
8420
|
+
this.parts.push({ what: "mask", offset: o, moving: mask.surface, base: null, reduced: /* @__PURE__ */ new Map() });
|
|
8421
|
+
moves.push("mask");
|
|
8422
|
+
if (!box) box = toDoc(contentBounds(mask.surface), o);
|
|
8423
|
+
}
|
|
8424
|
+
} else if (content === "selection") {
|
|
8425
|
+
const { lifted, rest } = liftPixels(pixels, sel, o);
|
|
8426
|
+
this.parts.push({ what: "pixels", offset: o, moving: lifted, base: rest, reduced: /* @__PURE__ */ new Map() });
|
|
8427
|
+
moves.push("pixels");
|
|
8428
|
+
box = toDoc(contentBounds(lifted), o);
|
|
8429
|
+
if (mask && linked) {
|
|
8430
|
+
const m = liftMask(mask.surface, sel, o);
|
|
8431
|
+
this.parts.push({ what: "mask", offset: o, moving: m.lifted, base: m.rest, reduced: /* @__PURE__ */ new Map() });
|
|
8432
|
+
moves.push("mask");
|
|
8433
|
+
}
|
|
8434
|
+
} else {
|
|
8435
|
+
const { lifted } = liftPixels(pixels, sel, o);
|
|
8436
|
+
box = toDoc(contentBounds(lifted), o);
|
|
8437
|
+
if (box) {
|
|
8438
|
+
const copy = {
|
|
8439
|
+
type: "pixel",
|
|
8440
|
+
id: newLayerId(),
|
|
8441
|
+
name: `${layer.name || "Layer"} copy`,
|
|
8442
|
+
visible: true,
|
|
8443
|
+
opacity: 1,
|
|
8444
|
+
blend: "normal",
|
|
8445
|
+
surface: lifted,
|
|
8446
|
+
offset: { ...o }
|
|
8447
|
+
};
|
|
8448
|
+
this.copyId = copy.id;
|
|
8449
|
+
const loc = findLayer(layers, layer.id);
|
|
8450
|
+
this.startLayers = insertLayer(layers, copy, loc.parent?.id ?? null, loc.index + 1);
|
|
8451
|
+
this.parts.push({ what: "pixels", offset: o, moving: lifted, base: null, reduced: /* @__PURE__ */ new Map() });
|
|
8452
|
+
moves.push("pixels");
|
|
8453
|
+
}
|
|
8454
|
+
}
|
|
8455
|
+
if (content !== "layer") {
|
|
8456
|
+
this.parts.push({ what: "selection", offset: { x: 0, y: 0 }, moving: selectionSurface(sel), base: null, reduced: /* @__PURE__ */ new Map() });
|
|
8457
|
+
moves.push("selection");
|
|
8458
|
+
}
|
|
8459
|
+
}
|
|
8460
|
+
if (!box && moves[0] === "mask") fail2("The mask holds nothing but its default value, so there is nothing to transform; paint it first, or transform the pixels (channel pixels).");
|
|
8461
|
+
if (!box) fail2(content === "layer" ? "The layer has no pixels to transform." : "The selection holds no pixels of this layer; select some, or transform the outline (content selection-outline).");
|
|
8462
|
+
this.box = box;
|
|
8463
|
+
this.moves = moves;
|
|
8464
|
+
this.geometryValue = identityTransform(box);
|
|
8465
|
+
this.shown = this.startLayers;
|
|
8466
|
+
if (this.copyId) host.show(this.startLayers, sel, this.copyId);
|
|
8467
|
+
this.shown = host.layers();
|
|
8468
|
+
}
|
|
8469
|
+
host;
|
|
8470
|
+
content;
|
|
8471
|
+
/** The layer being transformed (`null` for `selection-outline`). */
|
|
8472
|
+
layerId;
|
|
8473
|
+
/** The new layer of a `selection-copy`. */
|
|
8474
|
+
copyId = null;
|
|
8475
|
+
/** The frame (document pixels). */
|
|
8476
|
+
box;
|
|
8477
|
+
/** What moves. */
|
|
8478
|
+
moves;
|
|
8479
|
+
geometryValue;
|
|
8480
|
+
interp;
|
|
8481
|
+
parts = [];
|
|
8482
|
+
before;
|
|
8483
|
+
startLayers;
|
|
8484
|
+
startSelection;
|
|
8485
|
+
startActive;
|
|
8486
|
+
/** The stack this session last showed: anything else means the document changed under it. */
|
|
8487
|
+
shown;
|
|
8488
|
+
closed = false;
|
|
8489
|
+
/** The current geometry. */
|
|
8490
|
+
get geometry() {
|
|
8491
|
+
return this.geometryValue;
|
|
8492
|
+
}
|
|
8493
|
+
get interpolation() {
|
|
8494
|
+
return this.interp;
|
|
8495
|
+
}
|
|
8496
|
+
get isOpen() {
|
|
8497
|
+
return !this.closed;
|
|
8498
|
+
}
|
|
8499
|
+
check() {
|
|
8500
|
+
if (this.closed) fail2("This transform has already been committed or cancelled; begin a new one.");
|
|
8501
|
+
if (this.host.layers() !== this.shown) {
|
|
8502
|
+
this.closed = true;
|
|
8503
|
+
fail2("The document changed while the transform was open, so it was abandoned; begin a new one.");
|
|
8504
|
+
}
|
|
8505
|
+
}
|
|
8506
|
+
/**
|
|
8507
|
+
* Changes the geometry (`corners`, `warp`) or the interpolation and, unless `preview` is `false`,
|
|
8508
|
+
* shows the result live (no history). Returns the geometry.
|
|
8509
|
+
*/
|
|
8510
|
+
update(changes, preview = {}) {
|
|
8511
|
+
this.check();
|
|
8512
|
+
if (changes.interpolation !== void 0) {
|
|
8513
|
+
if (!isInterpolation(changes.interpolation)) fail2(`Unknown interpolation "${String(changes.interpolation)}"; use nearest, bilinear or bicubic.`);
|
|
8514
|
+
this.interp = changes.interpolation;
|
|
8515
|
+
}
|
|
8516
|
+
const next = {
|
|
8517
|
+
box: this.box,
|
|
8518
|
+
corners: (changes.corners ?? this.geometryValue.corners).map((p) => ({ x: p.x, y: p.y })),
|
|
8519
|
+
warp: changes.warp === void 0 ? this.geometryValue.warp ?? null : changes.warp
|
|
8520
|
+
};
|
|
8521
|
+
if (next.corners.length !== 4 || !next.corners.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))) fail2("corners must be four finite points: top-left, top-right, bottom-right, bottom-left.");
|
|
8522
|
+
if (!transformMap(next)) fail2("These corners fold the frame over itself or through the horizon; keep the quad convex.");
|
|
8523
|
+
this.geometryValue = next;
|
|
8524
|
+
if (preview !== false) this.render(preview);
|
|
8525
|
+
return next;
|
|
8526
|
+
}
|
|
8527
|
+
/** Resamples every part for the current geometry and shows it (or returns it, for commit). */
|
|
8528
|
+
resample(final, preview = {}) {
|
|
8529
|
+
const g = this.geometryValue;
|
|
8530
|
+
const identity = isIdentityTransform(g);
|
|
8531
|
+
const warp = hasWarp(g);
|
|
8532
|
+
const h = warp ? null : geometryHomography(g);
|
|
8533
|
+
const f = warp ? transformMap(g) : null;
|
|
8534
|
+
const step = final ? 1 : previewStep(preview.scale);
|
|
8535
|
+
const interpolation = final ? this.interp : this.interp === "nearest" ? "nearest" : "bilinear";
|
|
8536
|
+
let layers = this.startLayers;
|
|
8537
|
+
let selection = this.startSelection;
|
|
8538
|
+
const targetId = this.copyId ?? this.layerId;
|
|
8539
|
+
for (const part of this.parts) {
|
|
8540
|
+
const o = part.offset;
|
|
8541
|
+
let moved;
|
|
8542
|
+
if (identity) moved = part.moving;
|
|
8543
|
+
else {
|
|
8544
|
+
let src = part.moving;
|
|
8545
|
+
let k = 1;
|
|
8546
|
+
if (step > 1) {
|
|
8547
|
+
k = step;
|
|
8548
|
+
let r = part.reduced.get(k);
|
|
8549
|
+
if (!r) {
|
|
8550
|
+
r = reduceSurface(part.moving, k);
|
|
8551
|
+
part.reduced.set(k, r);
|
|
8552
|
+
}
|
|
8553
|
+
src = r;
|
|
8554
|
+
}
|
|
8555
|
+
const opts = { interpolation, step: step > 1 ? step : void 0 };
|
|
8556
|
+
if (preview.clip && !final) opts.clip = { x: Math.floor(preview.clip.x - o.x), y: Math.floor(preview.clip.y - o.y), width: Math.ceil(preview.clip.width) + 1, height: Math.ceil(preview.clip.height) + 1 };
|
|
8557
|
+
try {
|
|
8558
|
+
if (f) {
|
|
8559
|
+
moved = warpSurface(src, (p) => {
|
|
8560
|
+
const q2 = f({ x: p.x * k + o.x, y: p.y * k + o.y });
|
|
8561
|
+
return q2 && { x: q2.x - o.x, y: q2.y - o.y };
|
|
8562
|
+
}, opts);
|
|
8563
|
+
} else {
|
|
8564
|
+
const hl = multiplyHomographies(multiplyHomographies(multiplyHomographies([k, 0, 0, 0, k, 0, 0, 0, 1], T(o.x, o.y)), h), T(-o.x, -o.y));
|
|
8565
|
+
moved = transformSurface(src, hl, opts);
|
|
8566
|
+
}
|
|
8567
|
+
} catch (e) {
|
|
8568
|
+
if (e instanceof TransformTooLargeError) fail2(e.message);
|
|
8569
|
+
throw e;
|
|
8570
|
+
}
|
|
8571
|
+
}
|
|
8572
|
+
const result = part.base ? compositeSurfaceOver(part.base, moved) : moved;
|
|
8573
|
+
if (part.what === "selection") {
|
|
8574
|
+
const { width, height } = this.host.canvas();
|
|
8575
|
+
const data = new Uint8Array(readRegion(result, { x: 0, y: 0, width, height }).buffer);
|
|
8576
|
+
const next = createSelectionMask(width, height, data);
|
|
8577
|
+
selection = next.bounds ? next : null;
|
|
8578
|
+
} else if (part.what === "pixels") {
|
|
8579
|
+
layers = mapLayer(layers, targetId, (l) => {
|
|
8580
|
+
const next = { ...l, surface: result };
|
|
8581
|
+
delete next.source;
|
|
8582
|
+
return next;
|
|
8583
|
+
});
|
|
8584
|
+
} else {
|
|
8585
|
+
layers = mapLayer(layers, targetId, (l) => ({ ...l, mask: { ...l.mask, surface: result } }));
|
|
8586
|
+
}
|
|
8587
|
+
}
|
|
8588
|
+
return { layers, selection };
|
|
8589
|
+
}
|
|
8590
|
+
render(preview) {
|
|
8591
|
+
const { layers, selection } = this.resample(false, preview);
|
|
8592
|
+
this.host.show(layers, selection, this.copyId ?? this.startActive);
|
|
8593
|
+
this.shown = this.host.layers();
|
|
8594
|
+
}
|
|
8595
|
+
/** Finishes the transform with full-quality resampling, as one undo step. */
|
|
8596
|
+
commit() {
|
|
8597
|
+
this.check();
|
|
8598
|
+
const g = this.geometryValue;
|
|
8599
|
+
const changed = !isIdentityTransform(g) || Boolean(this.copyId);
|
|
8600
|
+
if (!changed) {
|
|
8601
|
+
this.closed = true;
|
|
8602
|
+
this.host.restore(this.before);
|
|
8603
|
+
return this.result(false, this.startLayers, this.startSelection);
|
|
8604
|
+
}
|
|
8605
|
+
const { layers, selection } = this.resample(true);
|
|
8606
|
+
this.closed = true;
|
|
8607
|
+
this.host.show(layers, selection, this.copyId ?? this.startActive);
|
|
8608
|
+
this.host.record(this.before);
|
|
8609
|
+
return this.result(true, layers, selection);
|
|
8610
|
+
}
|
|
8611
|
+
/** Abandons the transform and puts everything back (no history). */
|
|
8612
|
+
cancel() {
|
|
8613
|
+
if (this.closed) return;
|
|
8614
|
+
this.closed = true;
|
|
8615
|
+
this.host.restore(this.before);
|
|
8616
|
+
}
|
|
8617
|
+
result(changed, layers, selection) {
|
|
8618
|
+
let bounds = null;
|
|
8619
|
+
const id = this.copyId ?? this.layerId;
|
|
8620
|
+
const layer = id ? findLayer(layers, id)?.layer : void 0;
|
|
8621
|
+
if (this.moves.includes("pixels") && layer?.type === "pixel") bounds = toDoc(contentBounds(this.host.pixelsOf(layer)), layer.offset);
|
|
8622
|
+
else if (this.moves.includes("mask") && layer?.mask) bounds = toDoc(contentBounds(layer.mask.surface), layer.type === "pixel" ? layer.offset : { x: 0, y: 0 });
|
|
8623
|
+
else if (this.moves.includes("selection")) bounds = selection?.bounds ? { ...selection.bounds } : null;
|
|
8624
|
+
return {
|
|
8625
|
+
layerId: this.copyId ?? this.layerId,
|
|
8626
|
+
content: this.content,
|
|
8627
|
+
moved: [...this.moves],
|
|
8628
|
+
box: { ...this.box },
|
|
8629
|
+
corners: this.geometryValue.corners,
|
|
8630
|
+
bounds,
|
|
8631
|
+
changed
|
|
8632
|
+
};
|
|
8633
|
+
}
|
|
8634
|
+
};
|
|
8635
|
+
|
|
8090
8636
|
// src/image-editor/controller.ts
|
|
8091
8637
|
var IMAGE_EDITOR_SCHEMA_VERSION = LAYERED_SCHEMA_VERSION;
|
|
8092
8638
|
function paintCoverageInto(base, coverage, apply, rects) {
|
|
@@ -8763,17 +9309,17 @@ var ImageEditorController = class {
|
|
|
8763
9309
|
*/
|
|
8764
9310
|
sourceSurface() {
|
|
8765
9311
|
const { width, height } = this.naturalSize;
|
|
8766
|
-
const fromPixels = (
|
|
8767
|
-
let s = this.sourceSurfaces.get(
|
|
9312
|
+
const fromPixels = (px3) => {
|
|
9313
|
+
let s = this.sourceSurfaces.get(px3);
|
|
8768
9314
|
if (!s) {
|
|
8769
|
-
s = surfaceFromPixels(
|
|
8770
|
-
this.sourceSurfaces.set(
|
|
9315
|
+
s = surfaceFromPixels(px3, 4);
|
|
9316
|
+
this.sourceSurfaces.set(px3, s);
|
|
8771
9317
|
}
|
|
8772
9318
|
return s;
|
|
8773
9319
|
};
|
|
8774
|
-
const
|
|
8775
|
-
const fits = Boolean(
|
|
8776
|
-
if (this.samplePixelsOverride && fits) return fromPixels(
|
|
9320
|
+
const px2 = this.samplePixels;
|
|
9321
|
+
const fits = Boolean(px2 && px2.width === width && px2.height === height);
|
|
9322
|
+
if (this.samplePixelsOverride && fits) return fromPixels(px2);
|
|
8777
9323
|
const el = this.imageElement;
|
|
8778
9324
|
if (el) {
|
|
8779
9325
|
const cached = this.sourceSurfaces.get(el);
|
|
@@ -8787,7 +9333,7 @@ var ImageEditorController = class {
|
|
|
8787
9333
|
return s;
|
|
8788
9334
|
}
|
|
8789
9335
|
}
|
|
8790
|
-
return fits ? fromPixels(
|
|
9336
|
+
return fits ? fromPixels(px2) : null;
|
|
8791
9337
|
}
|
|
8792
9338
|
opaqueCache = /* @__PURE__ */ new WeakMap();
|
|
8793
9339
|
/** Whether every source pixel is opaque (some legacy renders depend on it). */
|
|
@@ -8846,11 +9392,11 @@ var ImageEditorController = class {
|
|
|
8846
9392
|
const { width, height } = this.naturalSize;
|
|
8847
9393
|
let surface = emptySurface(4);
|
|
8848
9394
|
if (init.pixels) {
|
|
8849
|
-
const
|
|
8850
|
-
if (!(
|
|
9395
|
+
const px2 = init.pixels;
|
|
9396
|
+
if (!(px2.width > 0 && px2.height > 0) || px2.data.length < px2.width * px2.height * 4) {
|
|
8851
9397
|
throw new LayerOperationError("pixels must be RGBA with width \xD7 height \xD7 4 bytes.");
|
|
8852
9398
|
}
|
|
8853
|
-
surface = surfaceFromPixels(
|
|
9399
|
+
surface = surfaceFromPixels(px2, 4);
|
|
8854
9400
|
} else if (init.color !== void 0) {
|
|
8855
9401
|
const c = parseColor(init.color);
|
|
8856
9402
|
surface = solidSurface(4, { x: 0, y: 0, width, height }, packRgba(c[0], c[1], c[2], c[3]));
|
|
@@ -9038,7 +9584,7 @@ var ImageEditorController = class {
|
|
|
9038
9584
|
if (this.state.activeLayerId === id) this.state.activeTarget = "pixels";
|
|
9039
9585
|
});
|
|
9040
9586
|
}
|
|
9041
|
-
/** Changes any layer mask's density, feather or
|
|
9587
|
+
/** Changes any layer mask's density, feather, enabled or linked flag (pixels kept). Returns the mask. */
|
|
9042
9588
|
setLayerMaskProps(id, props, commitToHistory = true) {
|
|
9043
9589
|
const layer = requireLayer(this.state.layers, id).layer;
|
|
9044
9590
|
if (!layer.mask) throw new LayerOperationError(`Layer "${layer.name || id}" has no mask; add one with add_layer_mask.`);
|
|
@@ -9098,14 +9644,14 @@ var ImageEditorController = class {
|
|
|
9098
9644
|
const surface = this.surfaceOf(layer);
|
|
9099
9645
|
const sb = sel.bounds;
|
|
9100
9646
|
const src = { x: sb.x - layer.offset.x, y: sb.y - layer.offset.y, width: sb.width, height: sb.height };
|
|
9101
|
-
const
|
|
9102
|
-
const kept = options.cut ? new Uint8ClampedArray(
|
|
9647
|
+
const px2 = readRegion(surface, src);
|
|
9648
|
+
const kept = options.cut ? new Uint8ClampedArray(px2) : null;
|
|
9103
9649
|
for (let y = 0; y < sb.height; y++) {
|
|
9104
9650
|
for (let x = 0; x < sb.width; x++) {
|
|
9105
9651
|
const c = sel.data[(sb.y + y) * sel.width + sb.x + x];
|
|
9106
9652
|
const i = (y * sb.width + x) * 4 + 3;
|
|
9107
9653
|
if (kept) kept[i] = Math.round(kept[i] * (1 - c / 255));
|
|
9108
|
-
|
|
9654
|
+
px2[i] = Math.round(px2[i] * (c / 255));
|
|
9109
9655
|
}
|
|
9110
9656
|
}
|
|
9111
9657
|
const copy = {
|
|
@@ -9115,7 +9661,7 @@ var ImageEditorController = class {
|
|
|
9115
9661
|
visible: true,
|
|
9116
9662
|
opacity: 1,
|
|
9117
9663
|
blend: "normal",
|
|
9118
|
-
surface: surfaceFromPixels({ width: src.width, height: src.height, data:
|
|
9664
|
+
surface: surfaceFromPixels({ width: src.width, height: src.height, data: px2 }, 4, src.x, src.y),
|
|
9119
9665
|
offset: { ...layer.offset }
|
|
9120
9666
|
};
|
|
9121
9667
|
let next = insertLayer(this.state.layers, copy, loc.parent?.id ?? null, loc.index + 1);
|
|
@@ -9130,6 +9676,134 @@ var ImageEditorController = class {
|
|
|
9130
9676
|
});
|
|
9131
9677
|
return copy.id;
|
|
9132
9678
|
}
|
|
9679
|
+
// --- Free Transform ---
|
|
9680
|
+
openTransform = null;
|
|
9681
|
+
/** The Free Transform in progress (see `beginFreeTransform`), or `null`. */
|
|
9682
|
+
getFreeTransform() {
|
|
9683
|
+
if (this.openTransform && !this.openTransform.isOpen) this.openTransform = null;
|
|
9684
|
+
return this.openTransform;
|
|
9685
|
+
}
|
|
9686
|
+
transformHost() {
|
|
9687
|
+
return {
|
|
9688
|
+
layers: () => this.state.layers,
|
|
9689
|
+
activeLayerId: () => this.state.activeLayerId,
|
|
9690
|
+
activeTarget: () => this.state.activeTarget,
|
|
9691
|
+
selection: () => this.state.selection?.bounds ? this.state.selection : null,
|
|
9692
|
+
canvas: () => this.naturalSize,
|
|
9693
|
+
pixelsOf: (layer) => this.surfaceOf(layer),
|
|
9694
|
+
capture: () => this.captureHistoryEntry(),
|
|
9695
|
+
show: (layers, selection, active) => {
|
|
9696
|
+
this.state.layers = layers;
|
|
9697
|
+
this.state.selection = selection;
|
|
9698
|
+
if (active !== this.state.activeLayerId) {
|
|
9699
|
+
this.state.activeLayerId = active;
|
|
9700
|
+
this.state.activeTarget = "pixels";
|
|
9701
|
+
}
|
|
9702
|
+
this.notify();
|
|
9703
|
+
},
|
|
9704
|
+
record: (before) => {
|
|
9705
|
+
if (this.isApplyingHistory) return;
|
|
9706
|
+
this.undoStack.push(before);
|
|
9707
|
+
if (this.undoStack.length > this.maxHistoryDepth) this.undoStack.shift();
|
|
9708
|
+
this.redoStack = [];
|
|
9709
|
+
this.trimHistoryBytes();
|
|
9710
|
+
this.notify();
|
|
9711
|
+
},
|
|
9712
|
+
restore: (before) => {
|
|
9713
|
+
const e = before;
|
|
9714
|
+
this.state.layers = e.layers;
|
|
9715
|
+
this.state.selection = e.selection;
|
|
9716
|
+
this.state.activeLayerId = e.activeLayerId;
|
|
9717
|
+
this.state.activeTarget = e.activeTarget;
|
|
9718
|
+
this.notify();
|
|
9719
|
+
}
|
|
9720
|
+
};
|
|
9721
|
+
}
|
|
9722
|
+
/**
|
|
9723
|
+
* Starts a Free Transform (Edit › Free Transform, Transform Selection, or a transform of a Layer
|
|
9724
|
+
* via Copy): of the layer's pixels with its linked mask, an unlinked mask alone, the selected
|
|
9725
|
+
* pixels or just the selection outline (see `FreeTransformInit`). Change the geometry with
|
|
9726
|
+
* `session.update` (live preview, no history), then `commit()` (one undo step) or `cancel()`.
|
|
9727
|
+
* Refused while another transform is open.
|
|
9728
|
+
*/
|
|
9729
|
+
beginFreeTransform(init = {}) {
|
|
9730
|
+
if (this.getFreeTransform()) throw new LayerOperationError("A Free Transform is already open; commit or cancel it first.");
|
|
9731
|
+
const session = new FreeTransformSession(this.transformHost(), init);
|
|
9732
|
+
this.openTransform = session;
|
|
9733
|
+
this.notify();
|
|
9734
|
+
return session;
|
|
9735
|
+
}
|
|
9736
|
+
/**
|
|
9737
|
+
* Transforms in one step (agents, scripts): begins a Free Transform for `spec`, sets its corners
|
|
9738
|
+
* from exactly one of `matrix` (document-space affine), `corners` (where the frame's corners go:
|
|
9739
|
+
* distort / perspective) or `params` (scale, rotate, skew and move about a reference point), and
|
|
9740
|
+
* commits it as one undo step. Returns what changed and the new bounds.
|
|
9741
|
+
*/
|
|
9742
|
+
transformLayer(spec) {
|
|
9743
|
+
const given = [spec.matrix, spec.corners, spec.params].filter((v) => v !== void 0).length;
|
|
9744
|
+
if (given > 1) throw new LayerOperationError("Give only one of matrix, corners and params.");
|
|
9745
|
+
if (!given && spec.warp === void 0) throw new LayerOperationError("Give matrix, corners or params (or a warp).");
|
|
9746
|
+
if (spec.matrix && !(Array.isArray(spec.matrix) && spec.matrix.length === 6 && spec.matrix.every(Number.isFinite))) {
|
|
9747
|
+
throw new LayerOperationError("matrix must be six finite numbers [a, b, c, d, e, f] (x' = a\xB7x + c\xB7y + e, y' = b\xB7x + d\xB7y + f).");
|
|
9748
|
+
}
|
|
9749
|
+
if (spec.matrix) {
|
|
9750
|
+
const [a, b, c, d] = spec.matrix;
|
|
9751
|
+
if (Math.abs(a * d - b * c) < 1e-9) throw new LayerOperationError("matrix is singular (it collapses the layer to a line); its determinant a\xB7d \u2212 b\xB7c must not be 0.");
|
|
9752
|
+
}
|
|
9753
|
+
const session = this.beginFreeTransform(spec);
|
|
9754
|
+
try {
|
|
9755
|
+
const g = identityTransform(session.box);
|
|
9756
|
+
const corners = spec.matrix ? cornersFromMatrix(session.box, spec.matrix) : spec.corners ? spec.corners : spec.params ? applyParams(g, spec.params) : g.corners;
|
|
9757
|
+
let warp = null;
|
|
9758
|
+
if (spec.warp) {
|
|
9759
|
+
if ("style" in spec.warp) {
|
|
9760
|
+
if (!WARP_STYLES.includes(spec.warp.style)) throw new LayerOperationError(`Unknown warp style "${String(spec.warp.style)}". Styles: ${WARP_STYLES.join(", ")}.`);
|
|
9761
|
+
warp = presetWarp(session.box, spec.warp);
|
|
9762
|
+
} else {
|
|
9763
|
+
if (!isValidWarp(spec.warp)) throw new LayerOperationError("mesh must have us and vs (knots from 0 to 1, increasing) and (3\xB7columns + 1) \xD7 (3\xB7rows + 1) finite points, row by row.");
|
|
9764
|
+
warp = spec.warp;
|
|
9765
|
+
}
|
|
9766
|
+
}
|
|
9767
|
+
session.update({ corners, warp }, false);
|
|
9768
|
+
return session.commit();
|
|
9769
|
+
} catch (e) {
|
|
9770
|
+
session.cancel();
|
|
9771
|
+
throw e;
|
|
9772
|
+
}
|
|
9773
|
+
}
|
|
9774
|
+
/** Flips the layer (or its selected pixels, or the outline) about the frame's centre, exactly (one undo step). */
|
|
9775
|
+
flipLayer(axis, init = {}) {
|
|
9776
|
+
if (axis !== "horizontal" && axis !== "vertical") throw new LayerOperationError(`axis must be horizontal or vertical, got "${String(axis)}".`);
|
|
9777
|
+
const session = this.beginFreeTransform({ interpolation: "nearest", ...init });
|
|
9778
|
+
try {
|
|
9779
|
+
session.update({ corners: flipCorners(identityTransform(session.box), axis) }, false);
|
|
9780
|
+
return session.commit();
|
|
9781
|
+
} catch (e) {
|
|
9782
|
+
session.cancel();
|
|
9783
|
+
throw e;
|
|
9784
|
+
}
|
|
9785
|
+
}
|
|
9786
|
+
/**
|
|
9787
|
+
* Rotates by a quarter turn (90 clockwise, −90 counter-clockwise) or 180 about the frame's
|
|
9788
|
+
* centre, snapped so pixels land on whole pixels: exact, no resampling (one undo step).
|
|
9789
|
+
*/
|
|
9790
|
+
rotateLayer90(turn, init = {}) {
|
|
9791
|
+
if (turn !== 90 && turn !== -90 && turn !== 180) throw new LayerOperationError(`turn must be 90, -90 or 180, got ${String(turn)}.`);
|
|
9792
|
+
const session = this.beginFreeTransform({ interpolation: "nearest", ...init });
|
|
9793
|
+
try {
|
|
9794
|
+
const b = session.box;
|
|
9795
|
+
const cx = b.x + b.width / 2;
|
|
9796
|
+
const cy = b.y + b.height / 2;
|
|
9797
|
+
const ox = turn === 180 ? 0 : Math.round(cx - b.height / 2) - (cx - b.height / 2);
|
|
9798
|
+
const oy = turn === 180 ? 0 : Math.round(cy - b.width / 2) - (cy - b.width / 2);
|
|
9799
|
+
const m = turn === 90 ? [0, 1, -1, 0, cx + cy + ox, cy - cx + oy] : turn === -90 ? [0, -1, 1, 0, cx - cy + ox, cy + cx + oy] : [-1, 0, 0, -1, 2 * cx, 2 * cy];
|
|
9800
|
+
session.update({ corners: cornersFromMatrix(b, m) }, false);
|
|
9801
|
+
return session.commit();
|
|
9802
|
+
} catch (e) {
|
|
9803
|
+
session.cancel();
|
|
9804
|
+
throw e;
|
|
9805
|
+
}
|
|
9806
|
+
}
|
|
9133
9807
|
/** Adds a `develop` or `blur` filter to a pixel layer (one undo step). Returns the layer's filters. */
|
|
9134
9808
|
addLayerFilter(id, filter) {
|
|
9135
9809
|
const f = normalizeFilter(filter);
|
|
@@ -10377,9 +11051,9 @@ var ImageEditorController = class {
|
|
|
10377
11051
|
return;
|
|
10378
11052
|
}
|
|
10379
11053
|
const { width, height } = this.naturalSize;
|
|
10380
|
-
let
|
|
10381
|
-
if (
|
|
10382
|
-
const data = new Uint8ClampedArray(
|
|
11054
|
+
let px2 = await this.decodeImage(newDataUrl);
|
|
11055
|
+
if (px2.width !== width || px2.height !== height) px2 = resizePixels(px2, width, height);
|
|
11056
|
+
const data = new Uint8ClampedArray(px2.data);
|
|
10383
11057
|
const sel = this.state.selection?.bounds ? this.state.selection : null;
|
|
10384
11058
|
if (sel) for (let p = 0, i = 3; p < width * height; p++, i += 4) data[i] = Math.round(data[i] * sel.data[p] / 255);
|
|
10385
11059
|
return this.addPixelLayer({ name: "Inpaint", pixels: { width, height, data } });
|
|
@@ -10400,10 +11074,10 @@ var ImageEditorController = class {
|
|
|
10400
11074
|
if (!target) throw new LayerOperationError("There is no pixel layer to mask.");
|
|
10401
11075
|
requireUnlocked(target, "all", "mask it");
|
|
10402
11076
|
const { width, height } = this.naturalSize;
|
|
10403
|
-
let
|
|
10404
|
-
if (
|
|
11077
|
+
let px2 = await this.decodeImage(newDataUrl);
|
|
11078
|
+
if (px2.width !== width || px2.height !== height) px2 = resizePixels(px2, width, height);
|
|
10405
11079
|
const alpha = new Uint8ClampedArray(width * height);
|
|
10406
|
-
for (let p = 0; p < alpha.length; p++) alpha[p] =
|
|
11080
|
+
for (let p = 0; p < alpha.length; p++) alpha[p] = px2.data[p * 4 + 3];
|
|
10407
11081
|
let surface = surfaceFromPixels({ width, height, data: alpha }, 1, 0, 0, 0);
|
|
10408
11082
|
if (target.offset.x || target.offset.y) surface = translateSurface(surface, -target.offset.x, -target.offset.y);
|
|
10409
11083
|
this.commitLayers(setLayerMask(this.state.layers, target.id, { surface, defaultValue: 0 }));
|
|
@@ -10868,6 +11542,10 @@ function withMaskProps(mask, props) {
|
|
|
10868
11542
|
if (props.enabled) delete next.enabled;
|
|
10869
11543
|
else next.enabled = false;
|
|
10870
11544
|
}
|
|
11545
|
+
if (props.linked !== void 0) {
|
|
11546
|
+
if (props.linked) delete next.linked;
|
|
11547
|
+
else next.linked = false;
|
|
11548
|
+
}
|
|
10871
11549
|
return next;
|
|
10872
11550
|
}
|
|
10873
11551
|
function resizeLegacyMasks(layers, from, to) {
|
|
@@ -10913,24 +11591,24 @@ function pixelsOnly(layers) {
|
|
|
10913
11591
|
}
|
|
10914
11592
|
return out;
|
|
10915
11593
|
}
|
|
10916
|
-
function resizePixels(
|
|
10917
|
-
const src =
|
|
11594
|
+
function resizePixels(px2, width, height) {
|
|
11595
|
+
const src = px2.data;
|
|
10918
11596
|
const out = new Uint8ClampedArray(width * height * 4);
|
|
10919
|
-
const kx =
|
|
10920
|
-
const ky =
|
|
11597
|
+
const kx = px2.width / width;
|
|
11598
|
+
const ky = px2.height / height;
|
|
10921
11599
|
for (let y = 0; y < height; y++) {
|
|
10922
|
-
const fy = Math.min(
|
|
11600
|
+
const fy = Math.min(px2.height - 1, Math.max(0, (y + 0.5) * ky - 0.5));
|
|
10923
11601
|
const y0 = Math.floor(fy);
|
|
10924
|
-
const y1 = Math.min(
|
|
11602
|
+
const y1 = Math.min(px2.height - 1, y0 + 1);
|
|
10925
11603
|
const ty = fy - y0;
|
|
10926
11604
|
for (let x = 0; x < width; x++) {
|
|
10927
|
-
const fx = Math.min(
|
|
11605
|
+
const fx = Math.min(px2.width - 1, Math.max(0, (x + 0.5) * kx - 0.5));
|
|
10928
11606
|
const x0 = Math.floor(fx);
|
|
10929
|
-
const x1 = Math.min(
|
|
11607
|
+
const x1 = Math.min(px2.width - 1, x0 + 1);
|
|
10930
11608
|
const tx = fx - x0;
|
|
10931
11609
|
for (let c = 0; c < 4; c++) {
|
|
10932
|
-
const a = src[(y0 *
|
|
10933
|
-
const b = src[(y1 *
|
|
11610
|
+
const a = src[(y0 * px2.width + x0) * 4 + c] * (1 - tx) + src[(y0 * px2.width + x1) * 4 + c] * tx;
|
|
11611
|
+
const b = src[(y1 * px2.width + x0) * 4 + c] * (1 - tx) + src[(y1 * px2.width + x1) * 4 + c] * tx;
|
|
10934
11612
|
out[(y * width + x) * 4 + c] = a * (1 - ty) + b * ty;
|
|
10935
11613
|
}
|
|
10936
11614
|
}
|
|
@@ -11425,8 +12103,8 @@ function describeSelection(mask) {
|
|
|
11425
12103
|
if (!mask || !mask.bounds) return "No selection";
|
|
11426
12104
|
let sum = 0;
|
|
11427
12105
|
for (let i = 0; i < mask.data.length; i++) sum += mask.data[i];
|
|
11428
|
-
const
|
|
11429
|
-
const shown =
|
|
12106
|
+
const pct3 = sum / 255 / mask.data.length * 100;
|
|
12107
|
+
const shown = pct3 >= 10 ? Math.round(pct3) : Math.round(pct3 * 10) / 10;
|
|
11430
12108
|
return `${mask.bounds.width} \xD7 ${mask.bounds.height} px selected (${shown}% of the image)`;
|
|
11431
12109
|
}
|
|
11432
12110
|
|
|
@@ -11746,14 +12424,14 @@ function rebin(bins, count) {
|
|
|
11746
12424
|
return out;
|
|
11747
12425
|
}
|
|
11748
12426
|
function histogramSummary(h, bins) {
|
|
11749
|
-
const
|
|
12427
|
+
const pct3 = (n) => h.samples ? round(n / h.samples * 100) : 0;
|
|
11750
12428
|
const channels = { red: h.red, green: h.green, blue: h.blue, luminance: h.luminance };
|
|
11751
12429
|
return {
|
|
11752
12430
|
samples: h.samples,
|
|
11753
12431
|
transparent: h.transparent,
|
|
11754
12432
|
clipping: {
|
|
11755
|
-
shadowsPercent: { red:
|
|
11756
|
-
highlightsPercent: { red:
|
|
12433
|
+
shadowsPercent: { red: pct3(h.shadows[0]), green: pct3(h.shadows[1]), blue: pct3(h.shadows[2]) },
|
|
12434
|
+
highlightsPercent: { red: pct3(h.highlights[0]), green: pct3(h.highlights[1]), blue: pct3(h.highlights[2]) }
|
|
11757
12435
|
},
|
|
11758
12436
|
stats: Object.fromEntries(Object.entries(channels).map(([k, v]) => [k, binStats(v, h.samples)])),
|
|
11759
12437
|
...bins ? { bins: Object.fromEntries(Object.entries(channels).map(([k, v]) => [k, rebin(v, bins)])) } : {}
|
|
@@ -11985,6 +12663,7 @@ function maskSummary(mask) {
|
|
|
11985
12663
|
const out = {
|
|
11986
12664
|
kind: mask.source ? "depth" : "raster",
|
|
11987
12665
|
enabled: mask.enabled !== false,
|
|
12666
|
+
linked: mask.linked !== false,
|
|
11988
12667
|
density: mask.density ?? 1,
|
|
11989
12668
|
feather: mask.feather ?? 0
|
|
11990
12669
|
};
|
|
@@ -12361,23 +13040,29 @@ function createImageLayerTools(controller) {
|
|
|
12361
13040
|
const maskProps = {
|
|
12362
13041
|
name: "set_layer_mask_properties",
|
|
12363
13042
|
title: "Set layer mask properties",
|
|
12364
|
-
description: "Changes a layer mask's density (0..1), feather (px) or
|
|
13043
|
+
description: "Changes a layer mask's density (0..1), feather (px), enabled flag or linked flag; its pixels are kept. An unlinked mask (linked false) stays in place when the layer is moved, transformed or warped; transform it alone with transform_layer target mask. One undo step. Returns the mask summary.",
|
|
12365
13044
|
inputSchema: {
|
|
12366
13045
|
type: "object",
|
|
12367
13046
|
properties: {
|
|
12368
13047
|
layerId: LAYER_ID2,
|
|
12369
13048
|
density: { type: "number", minimum: 0, maximum: 1, description: "Density 0..1." },
|
|
12370
13049
|
feather: { type: "number", minimum: 0, maximum: 1e3, description: "Feather \u03C3 in px." },
|
|
12371
|
-
enabled: { type: "boolean", description: "false ignores the mask but keeps it." }
|
|
13050
|
+
enabled: { type: "boolean", description: "false ignores the mask but keeps it." },
|
|
13051
|
+
linked: { type: "boolean", description: "false unlinks the mask from the layer pixels (it stays put when they move or transform); true links it again." }
|
|
12372
13052
|
},
|
|
12373
13053
|
required: ["layerId"],
|
|
12374
13054
|
additionalProperties: false
|
|
12375
13055
|
},
|
|
12376
13056
|
annotations: { title: "Set layer mask properties", ...WRITE_IDEMPOTENT2 },
|
|
12377
|
-
handler: ({ layerId, density, feather, enabled }) => {
|
|
13057
|
+
handler: ({ layerId, density, feather, enabled, linked }) => {
|
|
12378
13058
|
const id = exists(layerId);
|
|
12379
|
-
if ([density, feather, enabled].every((v) => v === void 0)) throw new LayerOperationError("Give density, feather or
|
|
12380
|
-
const mask = controller.setLayerMaskProps(id, {
|
|
13059
|
+
if ([density, feather, enabled, linked].every((v) => v === void 0)) throw new LayerOperationError("Give density, feather, enabled or linked.");
|
|
13060
|
+
const mask = controller.setLayerMaskProps(id, {
|
|
13061
|
+
density,
|
|
13062
|
+
feather,
|
|
13063
|
+
enabled,
|
|
13064
|
+
linked
|
|
13065
|
+
});
|
|
12381
13066
|
return { layerId: id, mask: maskSummary(mask) };
|
|
12382
13067
|
}
|
|
12383
13068
|
};
|
|
@@ -12620,7 +13305,7 @@ var DEFS = {
|
|
|
12620
13305
|
};
|
|
12621
13306
|
var nullable = (schema, description) => ({ anyOf: [schema, { type: "null" }], description });
|
|
12622
13307
|
function settingsSchema(forUpdate) {
|
|
12623
|
-
const
|
|
13308
|
+
const opt2 = (schema, description) => forUpdate ? nullable(schema, `${description} null removes it.`) : { ...schema, description };
|
|
12624
13309
|
return {
|
|
12625
13310
|
type: "object",
|
|
12626
13311
|
description: forUpdate ? "Settings to change (only these; nested objects are replaced whole). Which settings a kind takes: see add_layer_effect." : "Settings to change from Photoshop's defaults (all optional). Unknown settings for the kind are refused with the list of its settings.",
|
|
@@ -12635,7 +13320,7 @@ function settingsSchema(forUpdate) {
|
|
|
12635
13320
|
spread: { type: "number", minimum: 0, maximum: 1, description: "drop-shadow, outer-glow: 0..1 of the size made solid (default 0)." },
|
|
12636
13321
|
choke: { type: "number", minimum: 0, maximum: 1, description: "inner-shadow, inner-glow: 0..1 of the size made solid (default 0)." },
|
|
12637
13322
|
size: { type: "number", minimum: 0, maximum: 250, description: "Shadows, glows, satin, bevel-emboss: blur / size in px (default 5; satin 14); stroke: width in px (default 3)." },
|
|
12638
|
-
contour:
|
|
13323
|
+
contour: opt2({ $ref: "#/$defs/contour" }, "Shadows, glows, satin: coverage contour (default linear)."),
|
|
12639
13324
|
antiAlias: { type: "boolean", description: "Shadows, glows, satin: smooth the contour (default false; satin true)." },
|
|
12640
13325
|
noise: { type: "number", minimum: 0, maximum: 1, description: "Shadows, glows: speckle 0..1 (default 0)." },
|
|
12641
13326
|
knocksOut: { type: "boolean", description: "drop-shadow: the layer hides the shadow beneath its see-through fill (default true)." },
|
|
@@ -12649,14 +13334,14 @@ function settingsSchema(forUpdate) {
|
|
|
12649
13334
|
direction: { type: "string", enum: ["up", "down"], description: "bevel-emboss: raised (up, default) or sunk (down)." },
|
|
12650
13335
|
soften: { type: "number", minimum: 0, maximum: 16, description: "bevel-emboss: blur of the height in px (default 0)." },
|
|
12651
13336
|
altitude: { type: "number", minimum: 0, maximum: 90, description: "bevel-emboss: light altitude in degrees (default 30)." },
|
|
12652
|
-
glossContour:
|
|
13337
|
+
glossContour: opt2({ $ref: "#/$defs/contour" }, "bevel-emboss: gloss contour over the shading (default linear)."),
|
|
12653
13338
|
highlightColor: { $ref: "#/$defs/color", description: "bevel-emboss: highlight colour (default #ffffff)." },
|
|
12654
13339
|
highlightBlend: { type: "string", enum: BLEND_IDS, description: "bevel-emboss: highlight mode (default screen)." },
|
|
12655
13340
|
highlightOpacity: { type: "number", minimum: 0, maximum: 1, description: "bevel-emboss: highlight opacity (default 0.75)." },
|
|
12656
13341
|
shadowColor: { $ref: "#/$defs/color", description: "bevel-emboss: shadow colour (default #000000)." },
|
|
12657
13342
|
shadowBlend: { type: "string", enum: BLEND_IDS, description: "bevel-emboss: shadow mode (default multiply)." },
|
|
12658
13343
|
shadowOpacity: { type: "number", minimum: 0, maximum: 1, description: "bevel-emboss: shadow opacity (default 0.75)." },
|
|
12659
|
-
contourElement:
|
|
13344
|
+
contourElement: opt2(
|
|
12660
13345
|
{
|
|
12661
13346
|
type: "object",
|
|
12662
13347
|
properties: {
|
|
@@ -12669,7 +13354,7 @@ function settingsSchema(forUpdate) {
|
|
|
12669
13354
|
},
|
|
12670
13355
|
"bevel-emboss: the Contour element, shaping the bevel's height profile (default off)."
|
|
12671
13356
|
),
|
|
12672
|
-
texture:
|
|
13357
|
+
texture: opt2(
|
|
12673
13358
|
{
|
|
12674
13359
|
type: "object",
|
|
12675
13360
|
properties: {
|
|
@@ -13238,6 +13923,248 @@ function createImagePsdTools(controller) {
|
|
|
13238
13923
|
return [inspect, open, save];
|
|
13239
13924
|
}
|
|
13240
13925
|
|
|
13926
|
+
// src/image-editor/transform-tools.ts
|
|
13927
|
+
var READ6 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
13928
|
+
var WRITE5 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
13929
|
+
var round4 = (n, digits = 3) => Math.round(n * 10 ** digits) / 10 ** digits;
|
|
13930
|
+
var roundPoint = (p) => ({ x: round4(p.x), y: round4(p.y) });
|
|
13931
|
+
var POINT = {
|
|
13932
|
+
type: "object",
|
|
13933
|
+
properties: { x: { type: "number", description: "Document pixels from the left." }, y: { type: "number", description: "Document pixels from the top." } },
|
|
13934
|
+
required: ["x", "y"],
|
|
13935
|
+
additionalProperties: false
|
|
13936
|
+
};
|
|
13937
|
+
var TARGET2 = {
|
|
13938
|
+
layerId: { type: "string", description: "Layer to transform (from list_layers; default the active layer)." },
|
|
13939
|
+
content: {
|
|
13940
|
+
type: "string",
|
|
13941
|
+
enum: FREE_TRANSFORM_CONTENTS.filter((c) => c !== "selection-outline"),
|
|
13942
|
+
description: "What moves: layer (all its pixels with a linked mask), selection (only the selected pixels, committed back into the layer; the outline moves too), selection-copy (Layer via Copy: the selected pixels on a new layer above, then transformed; the original stays) or auto (default: selection when something is selected, else layer)."
|
|
13943
|
+
},
|
|
13944
|
+
channel: {
|
|
13945
|
+
type: "string",
|
|
13946
|
+
enum: ["pixels", "mask"],
|
|
13947
|
+
description: "pixels (default) moves the pixels and a linked mask; mask moves an unlinked mask alone (a linked mask always moves with its pixels)."
|
|
13948
|
+
},
|
|
13949
|
+
interpolation: { type: "string", enum: INTERPOLATIONS, description: "Resampling: nearest (hard pixels), bilinear or bicubic (default, Photoshop\u2019s Bicubic)." }
|
|
13950
|
+
};
|
|
13951
|
+
var GEOMETRY = {
|
|
13952
|
+
scaleX: { type: "number", description: "Width in percent of the frame (default 100; negative flips horizontally)." },
|
|
13953
|
+
scaleY: { type: "number", description: "Height in percent of the frame (default 100; negative flips vertically)." },
|
|
13954
|
+
rotate: { type: "number", description: "Rotation in degrees, clockwise (default 0)." },
|
|
13955
|
+
skewX: { type: "number", minimum: -89, maximum: 89, description: "Horizontal skew in degrees: vertical edges lean right for positive angles (default 0)." },
|
|
13956
|
+
skewY: { type: "number", minimum: -89, maximum: 89, description: "Vertical skew in degrees: horizontal edges lean down for positive angles (default 0)." },
|
|
13957
|
+
dx: { type: "number", description: "Move right after scaling and rotating, in document pixels (default 0)." },
|
|
13958
|
+
dy: { type: "number", description: "Move down after scaling and rotating, in document pixels (default 0)." },
|
|
13959
|
+
reference: {
|
|
13960
|
+
description: "Fixed point of scale, skew and rotate: an anchor of the frame (top-left, top, top-right, left, center, right, bottom-left, bottom, bottom-right; default center) or a document point {x, y}.",
|
|
13961
|
+
anyOf: [{ type: "string", enum: TRANSFORM_ANCHORS }, POINT]
|
|
13962
|
+
},
|
|
13963
|
+
matrix: {
|
|
13964
|
+
type: "array",
|
|
13965
|
+
items: { type: "number" },
|
|
13966
|
+
minItems: 6,
|
|
13967
|
+
maxItems: 6,
|
|
13968
|
+
description: "Instead of the numeric fields: an affine matrix [a, b, c, d, e, f] in document pixels (x' = a\xB7x + c\xB7y + e, y' = b\xB7x + d\xB7y + f)."
|
|
13969
|
+
},
|
|
13970
|
+
corners: {
|
|
13971
|
+
type: "array",
|
|
13972
|
+
items: POINT,
|
|
13973
|
+
minItems: 4,
|
|
13974
|
+
maxItems: 4,
|
|
13975
|
+
description: "Instead of the numeric fields: where the frame\u2019s corners go, [top-left, top-right, bottom-right, bottom-left] in document pixels (Distort; for Perspective move a pair of corners symmetrically). The quad must stay convex. Get the frame and its corners from get_transform_frame."
|
|
13976
|
+
}
|
|
13977
|
+
};
|
|
13978
|
+
var NUMERIC = ["scaleX", "scaleY", "rotate", "skewX", "skewY", "dx", "dy", "reference"];
|
|
13979
|
+
function geometryFrom(args) {
|
|
13980
|
+
const numeric = NUMERIC.filter((k) => args[k] !== void 0);
|
|
13981
|
+
const ways = [numeric.length > 0, args.matrix !== void 0, args.corners !== void 0].filter(Boolean).length;
|
|
13982
|
+
if (ways === 0) throw new LayerOperationError("Give the transform one way: numeric fields (scaleX, scaleY, rotate, skewX, skewY, dx, dy, reference), matrix, or corners.");
|
|
13983
|
+
if (ways > 1) throw new LayerOperationError("Give the transform only one way: numeric fields, matrix or corners, not a mix.");
|
|
13984
|
+
if (args.matrix !== void 0) return { matrix: args.matrix };
|
|
13985
|
+
if (args.corners !== void 0) return { corners: args.corners };
|
|
13986
|
+
const params = {};
|
|
13987
|
+
for (const k of NUMERIC) if (args[k] !== void 0) params[k] = args[k];
|
|
13988
|
+
if (params.scaleX === 0 || params.scaleY === 0) throw new LayerOperationError("scaleX and scaleY must not be 0 (that collapses the layer to a line).");
|
|
13989
|
+
if (typeof params.reference === "string" && !isTransformAnchor(params.reference)) throw new LayerOperationError(`Unknown reference "${params.reference}". Use ${TRANSFORM_ANCHORS.join(", ")} or {x, y}.`);
|
|
13990
|
+
return { params };
|
|
13991
|
+
}
|
|
13992
|
+
function targetFrom(args) {
|
|
13993
|
+
const init = {};
|
|
13994
|
+
if (typeof args.layerId === "string") init.layerId = args.layerId;
|
|
13995
|
+
if (args.content !== void 0) init.content = args.content;
|
|
13996
|
+
init.channel = args.channel ?? "pixels";
|
|
13997
|
+
if (args.interpolation !== void 0) init.interpolation = args.interpolation;
|
|
13998
|
+
return init;
|
|
13999
|
+
}
|
|
14000
|
+
function summary2(res) {
|
|
14001
|
+
return {
|
|
14002
|
+
layerId: res.layerId,
|
|
14003
|
+
content: res.content,
|
|
14004
|
+
moved: res.moved,
|
|
14005
|
+
changed: res.changed,
|
|
14006
|
+
frame: { ...res.box },
|
|
14007
|
+
corners: res.corners.map(roundPoint),
|
|
14008
|
+
bounds: res.bounds
|
|
14009
|
+
};
|
|
14010
|
+
}
|
|
14011
|
+
function createImageTransformTools(controller) {
|
|
14012
|
+
const getFrame = {
|
|
14013
|
+
name: "get_transform_frame",
|
|
14014
|
+
title: "Get the Free Transform frame",
|
|
14015
|
+
description: "Returns what a transform would move and its frame: content (layer, selection, selection-copy or selection-outline), moved (pixels, mask, selection), the frame (document bounds of the content), its corners [top-left, top-right, bottom-right, bottom-left] and centre. Pass meshColumns / meshRows to also get the identity warp mesh over the frame to edit for warp_layer. Changes nothing.",
|
|
14016
|
+
inputSchema: {
|
|
14017
|
+
type: "object",
|
|
14018
|
+
properties: {
|
|
14019
|
+
layerId: TARGET2.layerId,
|
|
14020
|
+
content: { type: "string", enum: FREE_TRANSFORM_CONTENTS, description: `${TARGET2.content.description} selection-outline frames the selection outline alone.` },
|
|
14021
|
+
channel: TARGET2.channel,
|
|
14022
|
+
meshColumns: { type: "integer", minimum: 1, maximum: 16, description: "Patch columns of an identity warp mesh to return (omit for none)." },
|
|
14023
|
+
meshRows: { type: "integer", minimum: 1, maximum: 16, description: "Patch rows of an identity warp mesh to return (default meshColumns)." }
|
|
14024
|
+
},
|
|
14025
|
+
additionalProperties: false
|
|
14026
|
+
},
|
|
14027
|
+
annotations: { title: "Get the Free Transform frame", ...READ6 },
|
|
14028
|
+
handler: (args) => {
|
|
14029
|
+
const init = targetFrom(args);
|
|
14030
|
+
if (init.content === "selection-copy") init.content = "selection";
|
|
14031
|
+
const s = controller.beginFreeTransform(init);
|
|
14032
|
+
const out = {
|
|
14033
|
+
layerId: s.layerId,
|
|
14034
|
+
content: args.content === "selection-copy" ? "selection-copy" : s.content,
|
|
14035
|
+
moved: [...s.moves],
|
|
14036
|
+
frame: { ...s.box },
|
|
14037
|
+
corners: identityTransform(s.box).corners.map(roundPoint),
|
|
14038
|
+
center: roundPoint({ x: s.box.x + s.box.width / 2, y: s.box.y + s.box.height / 2 })
|
|
14039
|
+
};
|
|
14040
|
+
s.cancel();
|
|
14041
|
+
if (args.meshColumns !== void 0) {
|
|
14042
|
+
const cols = args.meshColumns;
|
|
14043
|
+
const rows = args.meshRows ?? cols;
|
|
14044
|
+
const mesh = identityWarpMesh(s.box, cols, rows);
|
|
14045
|
+
out.mesh = { columns: cols, rows, pointsPerRow: warpMeshColumns(mesh), pointRows: warpMeshRows(mesh), points: mesh.points.map(roundPoint) };
|
|
14046
|
+
}
|
|
14047
|
+
return out;
|
|
14048
|
+
}
|
|
14049
|
+
};
|
|
14050
|
+
const transformLayer = {
|
|
14051
|
+
name: "transform_layer",
|
|
14052
|
+
title: "Free Transform a layer",
|
|
14053
|
+
description: "Edit \u203A Free Transform: scales, rotates, skews, moves, distorts or puts in perspective a pixel layer (with its linked mask), the selected pixels, a copy of them on a new layer, or an unlinked mask. Give the transform one way: the numeric fields (as Photoshop\u2019s options bar, about a reference point), a matrix, or the frame\u2019s new corners (distort / perspective; read the frame with get_transform_frame). Resamples once, tile by tile; one undo step. Returns layerId (the new layer for selection-copy), what moved, the frame, the corners and the new document bounds.",
|
|
14054
|
+
inputSchema: {
|
|
14055
|
+
type: "object",
|
|
14056
|
+
properties: { ...TARGET2, ...GEOMETRY },
|
|
14057
|
+
additionalProperties: false
|
|
14058
|
+
},
|
|
14059
|
+
annotations: { title: "Free Transform a layer", ...WRITE5 },
|
|
14060
|
+
handler: (args) => summary2(controller.transformLayer({ ...targetFrom(args), ...geometryFrom(args) }))
|
|
14061
|
+
};
|
|
14062
|
+
const warpLayer = {
|
|
14063
|
+
name: "warp_layer",
|
|
14064
|
+
title: "Warp a layer",
|
|
14065
|
+
description: "Edit \u203A Transform \u203A Warp: bends a pixel layer (with its linked mask), the selected pixels, a copy of them, or an unlinked mask through a preset (style with bend and distortions in percent) or a B\xE9zier mesh over the frame (get the frame and an identity mesh to edit from get_transform_frame with meshColumns). Give style or mesh, not both. One undo step. Returns what moved and the new document bounds.",
|
|
14066
|
+
inputSchema: {
|
|
14067
|
+
type: "object",
|
|
14068
|
+
properties: {
|
|
14069
|
+
...TARGET2,
|
|
14070
|
+
style: { type: "string", enum: WARP_STYLES, description: "Preset warp (Photoshop\u2019s menu): arc, arcLower, arcUpper, arch, bulge, shellLower, shellUpper, flag, wave, fish, rise, fisheye, inflate, squeeze, twist." },
|
|
14071
|
+
bend: { type: "number", minimum: -100, maximum: 100, description: "Preset bend in percent, \u2212100..100 (default 50)." },
|
|
14072
|
+
horizontal: { type: "number", minimum: -100, maximum: 100, description: "Preset horizontal distortion in percent, \u2212100..100 (default 0)." },
|
|
14073
|
+
vertical: { type: "number", minimum: -100, maximum: 100, description: "Preset vertical distortion in percent, \u2212100..100 (default 0)." },
|
|
14074
|
+
orientation: { type: "string", enum: ["horizontal", "vertical"], description: "Bend along the horizontal (default) or vertical axis." },
|
|
14075
|
+
mesh: {
|
|
14076
|
+
type: "object",
|
|
14077
|
+
description: "A custom warp: patch columns and rows, and the (3\xB7columns + 1) \xD7 (3\xB7rows + 1) control points row by row in document pixels (anchors where both indices are multiples of 3, B\xE9zier handles between). Start from get_transform_frame\u2019s identity mesh.",
|
|
14078
|
+
properties: {
|
|
14079
|
+
columns: { type: "integer", minimum: 1, maximum: 16, description: "Patch columns." },
|
|
14080
|
+
rows: { type: "integer", minimum: 1, maximum: 16, description: "Patch rows." },
|
|
14081
|
+
points: { type: "array", items: POINT, minItems: 16, description: "Control points, row by row, document pixels." }
|
|
14082
|
+
},
|
|
14083
|
+
required: ["columns", "rows", "points"],
|
|
14084
|
+
additionalProperties: false
|
|
14085
|
+
}
|
|
14086
|
+
},
|
|
14087
|
+
additionalProperties: false
|
|
14088
|
+
},
|
|
14089
|
+
annotations: { title: "Warp a layer", ...WRITE5 },
|
|
14090
|
+
handler: (args) => {
|
|
14091
|
+
const hasPreset = ["style", "bend", "horizontal", "vertical", "orientation"].some((k) => args[k] !== void 0);
|
|
14092
|
+
if (hasPreset && args.mesh !== void 0) throw new LayerOperationError("Give a preset (style, bend, horizontal, vertical, orientation) or a mesh, not both.");
|
|
14093
|
+
if (!hasPreset && args.mesh === void 0) throw new LayerOperationError("Give a preset style or a mesh.");
|
|
14094
|
+
let warp;
|
|
14095
|
+
if (hasPreset) {
|
|
14096
|
+
if (args.style === void 0) throw new LayerOperationError(`style is required with a preset. Styles: ${WARP_STYLES.join(", ")}.`);
|
|
14097
|
+
warp = {
|
|
14098
|
+
style: args.style,
|
|
14099
|
+
bend: args.bend ?? 50,
|
|
14100
|
+
horizontal: args.horizontal,
|
|
14101
|
+
vertical: args.vertical,
|
|
14102
|
+
orientation: args.orientation
|
|
14103
|
+
};
|
|
14104
|
+
} else {
|
|
14105
|
+
const m = args.mesh;
|
|
14106
|
+
const want = (3 * m.columns + 1) * (3 * m.rows + 1);
|
|
14107
|
+
if (m.points.length !== want) throw new LayerOperationError(`A ${m.columns} \xD7 ${m.rows} mesh needs (3\xB7${m.columns} + 1) \xD7 (3\xB7${m.rows} + 1) = ${want} points, got ${m.points.length}.`);
|
|
14108
|
+
const knots = (n) => Array.from({ length: n + 1 }, (_, i) => i / n);
|
|
14109
|
+
warp = { us: knots(m.columns), vs: knots(m.rows), points: m.points.map((p) => ({ x: p.x, y: p.y })) };
|
|
14110
|
+
}
|
|
14111
|
+
return summary2(controller.transformLayer({ ...targetFrom(args), warp }));
|
|
14112
|
+
}
|
|
14113
|
+
};
|
|
14114
|
+
const transformSelection = {
|
|
14115
|
+
name: "transform_selection",
|
|
14116
|
+
title: "Transform the selection outline",
|
|
14117
|
+
description: "Select \u203A Transform Selection: scales, rotates, skews, moves or distorts only the selection outline; no pixels change. Give the transform one way: the numeric fields (about a reference point of the selection\u2019s bounds), a matrix, or the new corners of its bounds. Needs a selection. One undo step. Returns the new selection bounds.",
|
|
14118
|
+
inputSchema: {
|
|
14119
|
+
type: "object",
|
|
14120
|
+
properties: { interpolation: { ...TARGET2.interpolation, description: "Resampling of soft edges: nearest, bilinear or bicubic (default)." }, ...GEOMETRY },
|
|
14121
|
+
additionalProperties: false
|
|
14122
|
+
},
|
|
14123
|
+
annotations: { title: "Transform the selection outline", ...WRITE5 },
|
|
14124
|
+
handler: (args) => {
|
|
14125
|
+
const res = controller.transformLayer({ content: "selection-outline", interpolation: args.interpolation, ...geometryFrom(args) });
|
|
14126
|
+
return { changed: res.changed, frame: { ...res.box }, corners: res.corners.map(roundPoint), bounds: res.bounds };
|
|
14127
|
+
}
|
|
14128
|
+
};
|
|
14129
|
+
const flipLayer = {
|
|
14130
|
+
name: "flip_layer",
|
|
14131
|
+
title: "Flip a layer",
|
|
14132
|
+
description: "Edit \u203A Transform \u203A Flip Horizontal or Vertical: mirrors a pixel layer (with its linked mask), the selected pixels or a copy of them about the centre of their frame, exactly (no resampling). To flip the whole image use flip_image. One undo step. Returns the new bounds.",
|
|
14133
|
+
inputSchema: {
|
|
14134
|
+
type: "object",
|
|
14135
|
+
properties: {
|
|
14136
|
+
axis: { type: "string", enum: ["horizontal", "vertical"], description: "horizontal mirrors left \u2194 right, vertical top \u2194 bottom." },
|
|
14137
|
+
layerId: TARGET2.layerId,
|
|
14138
|
+
content: TARGET2.content,
|
|
14139
|
+
channel: TARGET2.channel
|
|
14140
|
+
},
|
|
14141
|
+
required: ["axis"],
|
|
14142
|
+
additionalProperties: false
|
|
14143
|
+
},
|
|
14144
|
+
annotations: { title: "Flip a layer", ...WRITE5 },
|
|
14145
|
+
handler: (args) => summary2(controller.flipLayer(args.axis, targetFrom(args)))
|
|
14146
|
+
};
|
|
14147
|
+
const rotate90 = {
|
|
14148
|
+
name: "rotate_layer_90",
|
|
14149
|
+
title: "Rotate a layer by quarter turns",
|
|
14150
|
+
description: "Edit \u203A Transform \u203A Rotate 90\xB0 / 180\xB0: turns a pixel layer (with its linked mask), the selected pixels or a copy of them about the centre of their frame, exactly (pixels land on whole pixels; no resampling). To rotate the whole image use set_rotation. One undo step. Returns the new bounds.",
|
|
14151
|
+
inputSchema: {
|
|
14152
|
+
type: "object",
|
|
14153
|
+
properties: {
|
|
14154
|
+
turn: { type: "integer", enum: [90, -90, 180], description: "Degrees: 90 clockwise, -90 counter-clockwise, or 180." },
|
|
14155
|
+
layerId: TARGET2.layerId,
|
|
14156
|
+
content: TARGET2.content,
|
|
14157
|
+
channel: TARGET2.channel
|
|
14158
|
+
},
|
|
14159
|
+
required: ["turn"],
|
|
14160
|
+
additionalProperties: false
|
|
14161
|
+
},
|
|
14162
|
+
annotations: { title: "Rotate a layer by quarter turns", ...WRITE5 },
|
|
14163
|
+
handler: (args) => summary2(controller.rotateLayer90(args.turn, targetFrom(args)))
|
|
14164
|
+
};
|
|
14165
|
+
return [getFrame, transformLayer, warpLayer, transformSelection, flipLayer, rotate90];
|
|
14166
|
+
}
|
|
14167
|
+
|
|
13241
14168
|
// src/image-editor/tools.ts
|
|
13242
14169
|
var LAYER_PATH = /^layers\.([^.]+)\.(.+)$/;
|
|
13243
14170
|
function adjustmentsSummary(adjustments) {
|
|
@@ -13475,6 +14402,7 @@ function createImageEditorToolProvider(controller, options = {}) {
|
|
|
13475
14402
|
...createImageSelectionTools(controller),
|
|
13476
14403
|
...createImageEditTools(controller),
|
|
13477
14404
|
...createImageLayerTools(controller),
|
|
14405
|
+
...createImageTransformTools(controller),
|
|
13478
14406
|
...createImageEffectTools(controller),
|
|
13479
14407
|
...createImagePaintTools(controller),
|
|
13480
14408
|
...createPigmentTools(),
|
|
@@ -13686,8 +14614,8 @@ function applyRegionMosaic(imageData, regions, blockSize = 12) {
|
|
|
13686
14614
|
const count = bw * bh;
|
|
13687
14615
|
for (let py = by; py < by + bh; py++) {
|
|
13688
14616
|
const rowOffset = py * width;
|
|
13689
|
-
for (let
|
|
13690
|
-
const idx = (rowOffset +
|
|
14617
|
+
for (let px2 = bx; px2 < bx + bw; px2++) {
|
|
14618
|
+
const idx = (rowOffset + px2) * 4;
|
|
13691
14619
|
rSum += data[idx];
|
|
13692
14620
|
gSum += data[idx + 1];
|
|
13693
14621
|
bSum += data[idx + 2];
|
|
@@ -13700,8 +14628,8 @@ function applyRegionMosaic(imageData, regions, blockSize = 12) {
|
|
|
13700
14628
|
const aAvg = Math.round(aSum / count);
|
|
13701
14629
|
for (let py = by; py < by + bh; py++) {
|
|
13702
14630
|
const rowOffset = py * width;
|
|
13703
|
-
for (let
|
|
13704
|
-
const idx = (rowOffset +
|
|
14631
|
+
for (let px2 = bx; px2 < bx + bw; px2++) {
|
|
14632
|
+
const idx = (rowOffset + px2) * 4;
|
|
13705
14633
|
data[idx] = rAvg;
|
|
13706
14634
|
data[idx + 1] = gAvg;
|
|
13707
14635
|
data[idx + 2] = bAvg;
|
|
@@ -13919,6 +14847,179 @@ async function runLayerMenuAction(controller, action) {
|
|
|
13919
14847
|
}
|
|
13920
14848
|
}
|
|
13921
14849
|
|
|
14850
|
+
// src/image-editor/layers/effects-ui.ts
|
|
14851
|
+
function layerEffectsPanel(layer) {
|
|
14852
|
+
const empty = { readOnly: true, enabled: true, scale: 1, rows: [] };
|
|
14853
|
+
if (!layer) return { ...empty, available: false, reason: "Select a layer to add effects." };
|
|
14854
|
+
if (layer.type === "adjustment") return { ...empty, available: false, reason: "Adjustment layers cannot have effects." };
|
|
14855
|
+
const fx = layer.effects;
|
|
14856
|
+
const readOnly = Boolean(layer.locks?.all);
|
|
14857
|
+
return {
|
|
14858
|
+
available: true,
|
|
14859
|
+
readOnly,
|
|
14860
|
+
reason: readOnly ? "The layer is locked; unlock it to change its effects." : void 0,
|
|
14861
|
+
enabled: fx?.enabled ?? true,
|
|
14862
|
+
scale: fx?.scale ?? 1,
|
|
14863
|
+
rows: (fx?.items ?? []).map((e) => ({ id: e.id, kind: e.kind, label: layerEffectLabel(e.kind), enabled: e.enabled }))
|
|
14864
|
+
};
|
|
14865
|
+
}
|
|
14866
|
+
function layerEffectsBadge(layer) {
|
|
14867
|
+
if (!layer.effects?.items.length) return null;
|
|
14868
|
+
return hasActiveEffects(layer.effects) ? "on" : "off";
|
|
14869
|
+
}
|
|
14870
|
+
var LAYER_EFFECT_MENU = LAYER_EFFECT_KINDS.map((kind) => ({ kind, label: layerEffectLabel(kind) }));
|
|
14871
|
+
var BLENDS = BLEND_MODES.filter((m) => m.id !== "pigment").map((m) => ({ value: m.id, label: m.label }));
|
|
14872
|
+
var opt = (...pairs) => pairs.map(([value, label]) => ({ value, label }));
|
|
14873
|
+
function patternOptions(patterns) {
|
|
14874
|
+
return [...patterns.map((p) => ({ value: p.id, label: p.name || p.id })), ...builtinPatternIds().map((id) => ({ value: id, label: builtinPattern(id).name }))];
|
|
14875
|
+
}
|
|
14876
|
+
function getPath(o, path) {
|
|
14877
|
+
let v = o;
|
|
14878
|
+
for (const p of path.split(".")) {
|
|
14879
|
+
if (v === null || v === void 0) return void 0;
|
|
14880
|
+
if (Array.isArray(v)) v = p === "last" ? v[v.length - 1] : v[Number(p)];
|
|
14881
|
+
else v = v[p];
|
|
14882
|
+
}
|
|
14883
|
+
return v;
|
|
14884
|
+
}
|
|
14885
|
+
function setPath(o, parts, value) {
|
|
14886
|
+
if (!parts.length) return value;
|
|
14887
|
+
const [p, ...rest] = parts;
|
|
14888
|
+
if (Array.isArray(o)) {
|
|
14889
|
+
const i = p === "last" ? o.length - 1 : Number(p);
|
|
14890
|
+
const out = [...o];
|
|
14891
|
+
out[i] = setPath(o[i], rest, value);
|
|
14892
|
+
return out;
|
|
14893
|
+
}
|
|
14894
|
+
const r = o ?? {};
|
|
14895
|
+
return { ...r, [p]: setPath(r[p], rest, value) };
|
|
14896
|
+
}
|
|
14897
|
+
function fillControls(f, allowPattern, patterns) {
|
|
14898
|
+
const types = allowPattern ? opt(["color", "Color"], ["gradient", "Gradient"], ["pattern", "Pattern"]) : opt(["color", "Color"], ["gradient", "Gradient"]);
|
|
14899
|
+
const out = [{ key: "fill.type", label: "Fill", type: "select", value: f.type, options: types }];
|
|
14900
|
+
if (f.type === "color") out.push({ key: "fill.color", label: "Color", type: "color", value: f.color });
|
|
14901
|
+
else if (f.type === "gradient") out.push(...gradientControls("fill.gradient", f.gradient));
|
|
14902
|
+
else {
|
|
14903
|
+
out.push(
|
|
14904
|
+
{ key: "fill.patternId", label: "Pattern", type: "select", value: f.patternId, options: patternOptions(patterns) },
|
|
14905
|
+
{ key: "fill.scale", label: "Scale", type: "range", min: 0.01, max: 10, step: 0.01, value: f.scale, percent: true, unit: "%" }
|
|
14906
|
+
);
|
|
14907
|
+
}
|
|
14908
|
+
return out;
|
|
14909
|
+
}
|
|
14910
|
+
function gradientControls(prefix, g) {
|
|
14911
|
+
return [
|
|
14912
|
+
{ key: `${prefix}.stops.0.color`, label: "Start colour", type: "color", value: g.stops[0]?.color ?? "#000000" },
|
|
14913
|
+
{ key: `${prefix}.stops.last.color`, label: "End colour", type: "color", value: g.stops[g.stops.length - 1]?.color ?? "#ffffff" },
|
|
14914
|
+
{ key: `${prefix}.style`, label: "Style", type: "select", value: g.style, options: opt(["linear", "Linear"], ["radial", "Radial"], ["angle", "Angle"], ["reflected", "Reflected"], ["diamond", "Diamond"]) },
|
|
14915
|
+
{ key: `${prefix}.angle`, label: "Angle", type: "range", min: -180, max: 180, step: 1, value: g.angle, unit: "\xB0" },
|
|
14916
|
+
{ key: `${prefix}.scale`, label: "Scale", type: "range", min: 0.1, max: 1.5, step: 0.01, value: g.scale, percent: true, unit: "%" },
|
|
14917
|
+
{ key: `${prefix}.reverse`, label: "Reverse", type: "checkbox", value: g.reverse },
|
|
14918
|
+
{ key: `${prefix}.align`, label: "Align with layer", type: "checkbox", value: g.align }
|
|
14919
|
+
];
|
|
14920
|
+
}
|
|
14921
|
+
var px = (key, label, value, max = 250) => ({ key, label, type: "range", min: 0, max, step: 1, value, unit: "px" });
|
|
14922
|
+
var pct2 = (key, label, value, min = 0, max = 1) => ({ key, label, type: "range", min, max, step: 0.01, value, percent: true, unit: "%" });
|
|
14923
|
+
var deg = (key, label, value, min = -180, max = 180) => ({ key, label, type: "range", min, max, step: 1, value, unit: "\xB0" });
|
|
14924
|
+
function layerEffectControls(e, patterns = []) {
|
|
14925
|
+
const blended = e.kind === "bevel-emboss" ? [] : [
|
|
14926
|
+
{ key: "blend", label: "Blend mode", type: "select", value: e.blend, options: BLENDS },
|
|
14927
|
+
pct2("opacity", "Opacity", e.opacity)
|
|
14928
|
+
];
|
|
14929
|
+
switch (e.kind) {
|
|
14930
|
+
case "drop-shadow":
|
|
14931
|
+
case "inner-shadow":
|
|
14932
|
+
return [
|
|
14933
|
+
...blended,
|
|
14934
|
+
{ key: "color", label: "Color", type: "color", value: e.color },
|
|
14935
|
+
{ key: "useGlobalLight", label: "Use global light", type: "checkbox", value: e.useGlobalLight },
|
|
14936
|
+
deg("angle", "Angle", e.angle),
|
|
14937
|
+
px("distance", "Distance", e.distance, 500),
|
|
14938
|
+
e.kind === "drop-shadow" ? pct2("spread", "Spread", e.spread) : pct2("choke", "Choke", e.choke),
|
|
14939
|
+
px("size", "Size", e.size),
|
|
14940
|
+
pct2("noise", "Noise", e.noise),
|
|
14941
|
+
...e.kind === "drop-shadow" ? [{ key: "knocksOut", label: "Layer knocks out shadow", type: "checkbox", value: e.knocksOut }] : []
|
|
14942
|
+
];
|
|
14943
|
+
case "outer-glow":
|
|
14944
|
+
case "inner-glow":
|
|
14945
|
+
return [
|
|
14946
|
+
...blended,
|
|
14947
|
+
...fillControls(e.fill, false, patterns),
|
|
14948
|
+
{ key: "technique", label: "Technique", type: "select", value: e.technique, options: opt(["softer", "Softer"], ["precise", "Precise"]) },
|
|
14949
|
+
...e.kind === "inner-glow" ? [{ key: "source", label: "Source", type: "select", value: e.source, options: opt(["edge", "Edge"], ["center", "Center"]) }] : [],
|
|
14950
|
+
e.kind === "outer-glow" ? pct2("spread", "Spread", e.spread) : pct2("choke", "Choke", e.choke),
|
|
14951
|
+
px("size", "Size", e.size),
|
|
14952
|
+
pct2("range", "Range", e.range, 0.01, 1),
|
|
14953
|
+
pct2("noise", "Noise", e.noise)
|
|
14954
|
+
];
|
|
14955
|
+
case "bevel-emboss":
|
|
14956
|
+
return [
|
|
14957
|
+
{ key: "style", label: "Style", type: "select", value: e.style, options: opt(["inner-bevel", "Inner Bevel"], ["outer-bevel", "Outer Bevel"], ["emboss", "Emboss"], ["pillow-emboss", "Pillow Emboss"], ["stroke-emboss", "Stroke Emboss"]) },
|
|
14958
|
+
{ key: "technique", label: "Technique", type: "select", value: e.technique, options: opt(["smooth", "Smooth"], ["chisel-hard", "Chisel Hard"], ["chisel-soft", "Chisel Soft"]) },
|
|
14959
|
+
pct2("depth", "Depth", e.depth, 0, 10),
|
|
14960
|
+
{ key: "direction", label: "Direction", type: "select", value: e.direction, options: opt(["up", "Up"], ["down", "Down"]) },
|
|
14961
|
+
px("size", "Size", e.size),
|
|
14962
|
+
px("soften", "Soften", e.soften, 16),
|
|
14963
|
+
{ key: "useGlobalLight", label: "Use global light", type: "checkbox", value: e.useGlobalLight },
|
|
14964
|
+
deg("angle", "Angle", e.angle),
|
|
14965
|
+
deg("altitude", "Altitude", e.altitude, 0, 90),
|
|
14966
|
+
{ key: "highlightColor", label: "Highlight", type: "color", value: e.highlightColor },
|
|
14967
|
+
{ key: "highlightBlend", label: "Highlight mode", type: "select", value: e.highlightBlend, options: BLENDS },
|
|
14968
|
+
pct2("highlightOpacity", "Highlight opacity", e.highlightOpacity),
|
|
14969
|
+
{ key: "shadowColor", label: "Shadow", type: "color", value: e.shadowColor },
|
|
14970
|
+
{ key: "shadowBlend", label: "Shadow mode", type: "select", value: e.shadowBlend, options: BLENDS },
|
|
14971
|
+
pct2("shadowOpacity", "Shadow opacity", e.shadowOpacity)
|
|
14972
|
+
];
|
|
14973
|
+
case "satin":
|
|
14974
|
+
return [
|
|
14975
|
+
...blended,
|
|
14976
|
+
{ key: "color", label: "Color", type: "color", value: e.color },
|
|
14977
|
+
deg("angle", "Angle", e.angle),
|
|
14978
|
+
px("distance", "Distance", e.distance),
|
|
14979
|
+
px("size", "Size", e.size),
|
|
14980
|
+
{ key: "invert", label: "Invert", type: "checkbox", value: e.invert }
|
|
14981
|
+
];
|
|
14982
|
+
case "color-overlay":
|
|
14983
|
+
return [...blended, { key: "color", label: "Color", type: "color", value: e.color }];
|
|
14984
|
+
case "gradient-overlay":
|
|
14985
|
+
return [...blended, ...gradientControls("gradient", e.gradient)];
|
|
14986
|
+
case "pattern-overlay":
|
|
14987
|
+
return [
|
|
14988
|
+
...blended,
|
|
14989
|
+
{ key: "patternId", label: "Pattern", type: "select", value: e.patternId, options: patternOptions(patterns) },
|
|
14990
|
+
pct2("scale", "Scale", e.scale, 0.01, 10),
|
|
14991
|
+
deg("angle", "Angle", e.angle ?? 0),
|
|
14992
|
+
{ key: "link", label: "Link with layer", type: "checkbox", value: e.link ?? true }
|
|
14993
|
+
];
|
|
14994
|
+
case "stroke":
|
|
14995
|
+
return [
|
|
14996
|
+
...blended,
|
|
14997
|
+
px("size", "Size", e.size),
|
|
14998
|
+
{ key: "position", label: "Position", type: "select", value: e.position, options: opt(["outside", "Outside"], ["inside", "Inside"], ["center", "Center"]) },
|
|
14999
|
+
...fillControls(e.fill, true, patterns)
|
|
15000
|
+
];
|
|
15001
|
+
}
|
|
15002
|
+
}
|
|
15003
|
+
function fillOfType(current, type, patterns) {
|
|
15004
|
+
if (type === "gradient") return { type: "gradient", gradient: defaultEffectGradient() };
|
|
15005
|
+
if (type === "pattern") return { type: "pattern", patternId: patterns[0]?.id ?? "checker", scale: 1 };
|
|
15006
|
+
return { type: "color", color: current.type === "color" ? current.color : "#000000" };
|
|
15007
|
+
}
|
|
15008
|
+
function effectControlChange(effect, key, value, patterns = []) {
|
|
15009
|
+
const [top, ...rest] = key.split(".");
|
|
15010
|
+
if (top === "fill" && rest[0] === "type") return { fill: fillOfType(effect.fill, String(value), patterns) };
|
|
15011
|
+
if (top === "patternId" && !rest.length) {
|
|
15012
|
+
const name = patternOptions(patterns).find((o) => o.value === value)?.label;
|
|
15013
|
+
return name ? { patternId: value, name } : { patternId: value };
|
|
15014
|
+
}
|
|
15015
|
+
if (!rest.length) return { [top]: value };
|
|
15016
|
+
if (top === "fill" && rest[0] === "patternId") {
|
|
15017
|
+
const name = patternOptions(patterns).find((o) => o.value === value)?.label;
|
|
15018
|
+
return { fill: { ...effect.fill, patternId: value, ...name ? { name } : {} } };
|
|
15019
|
+
}
|
|
15020
|
+
return { [top]: setPath(getPath(effect, top), rest, value) };
|
|
15021
|
+
}
|
|
15022
|
+
|
|
13922
15023
|
// src/image-editor/psd/ui.ts
|
|
13923
15024
|
var PSD_FILE_ACCEPT = ".psd,.psb,image/vnd.adobe.photoshop,application/x-photoshop";
|
|
13924
15025
|
function blobBytes(file) {
|
|
@@ -13979,6 +15080,9 @@ export {
|
|
|
13979
15080
|
DabWalker,
|
|
13980
15081
|
EMPTY_DISTANCE,
|
|
13981
15082
|
EMPTY_MIXER_LOAD,
|
|
15083
|
+
FREE_TRANSFORM_CONTENTS,
|
|
15084
|
+
FREE_TRANSFORM_MODES,
|
|
15085
|
+
FreeTransformSession,
|
|
13982
15086
|
GUIDED_FILTER_EPS,
|
|
13983
15087
|
HISTOGRAM_BINS,
|
|
13984
15088
|
HUE_RANGE_LABELS,
|
|
@@ -13986,11 +15090,13 @@ export {
|
|
|
13986
15090
|
IMAGE_ADJUSTMENT_CONTROLS,
|
|
13987
15091
|
IMAGE_EDITOR_SCHEMA_VERSION,
|
|
13988
15092
|
IMAGE_EDITOR_STATE_VERSION,
|
|
15093
|
+
INTERPOLATIONS,
|
|
13989
15094
|
ImageEditorController,
|
|
13990
15095
|
ImageEditorRenderer,
|
|
13991
15096
|
LATEST_PROCESS_VERSION,
|
|
13992
15097
|
LAYERED_SCHEMA_VERSION,
|
|
13993
15098
|
LAYER_EFFECT_KINDS,
|
|
15099
|
+
LAYER_EFFECT_MENU,
|
|
13994
15100
|
LEGACY_PROCESS_VERSION,
|
|
13995
15101
|
LUMA_709,
|
|
13996
15102
|
LayerCompositor,
|
|
@@ -13999,6 +15105,7 @@ export {
|
|
|
13999
15105
|
MAX_BRUSH_SIZE,
|
|
14000
15106
|
MAX_EFFECT_REACH,
|
|
14001
15107
|
MAX_KELVIN,
|
|
15108
|
+
MAX_TRANSFORM_PIXELS,
|
|
14002
15109
|
MIN_KELVIN,
|
|
14003
15110
|
MIRED_PER_TEMPERATURE_UNIT,
|
|
14004
15111
|
MIXER_DRAIN,
|
|
@@ -14034,8 +15141,12 @@ export {
|
|
|
14034
15141
|
TINT_SCALE,
|
|
14035
15142
|
TONE_LUT_SIZE,
|
|
14036
15143
|
TONE_RANGE_PRESETS,
|
|
15144
|
+
TRANSFORM_ANCHORS,
|
|
14037
15145
|
TiledSurface,
|
|
15146
|
+
TransformTooLargeError,
|
|
14038
15147
|
VIBRANCE_GREY,
|
|
15148
|
+
WARP_STYLES,
|
|
15149
|
+
WARP_STYLE_LABELS,
|
|
14039
15150
|
XYZ_D50_TO_SRGB,
|
|
14040
15151
|
ZlibError,
|
|
14041
15152
|
activeAdjustmentLayers,
|
|
@@ -14047,16 +15158,19 @@ export {
|
|
|
14047
15158
|
adjustmentNodeAsLayer,
|
|
14048
15159
|
adjustmentNodeFromLegacy,
|
|
14049
15160
|
adler32,
|
|
15161
|
+
anchorPoint,
|
|
14050
15162
|
annotationLayerFor,
|
|
14051
15163
|
annotationLayerName,
|
|
14052
15164
|
applyAdjustmentLayers,
|
|
14053
15165
|
applyAdjustments,
|
|
14054
15166
|
applyBoxBlur,
|
|
14055
15167
|
applyDepthMask,
|
|
15168
|
+
applyHomography,
|
|
14056
15169
|
applyImageProjectionToContext,
|
|
14057
15170
|
applyLegacyAdjustments,
|
|
14058
15171
|
applyMatrix3,
|
|
14059
15172
|
applyOutputStage,
|
|
15173
|
+
applyParams,
|
|
14060
15174
|
applyProcess2Adjustments,
|
|
14061
15175
|
applyRegionBlur,
|
|
14062
15176
|
applyRegionMosaic,
|
|
@@ -14074,6 +15188,8 @@ export {
|
|
|
14074
15188
|
blurFilter,
|
|
14075
15189
|
borderCoverage,
|
|
14076
15190
|
boxMean,
|
|
15191
|
+
boxPointOf,
|
|
15192
|
+
boxQuad,
|
|
14077
15193
|
boxToCurvePoint,
|
|
14078
15194
|
bradfordAdaptation,
|
|
14079
15195
|
buildLayerMaskPlanes,
|
|
@@ -14093,12 +15209,14 @@ export {
|
|
|
14093
15209
|
colorRange,
|
|
14094
15210
|
combineSelections,
|
|
14095
15211
|
compileAdjustmentLayer,
|
|
15212
|
+
compositeSurfaceOver,
|
|
14096
15213
|
computeHistogram,
|
|
14097
15214
|
computeSelectionBounds,
|
|
14098
15215
|
contentBounds,
|
|
14099
15216
|
contractCoverage,
|
|
14100
15217
|
controlPosition,
|
|
14101
15218
|
controlTrackBackground,
|
|
15219
|
+
cornersFromMatrix,
|
|
14102
15220
|
coverageTileCoord,
|
|
14103
15221
|
coverageToByte,
|
|
14104
15222
|
coveringRect,
|
|
@@ -14111,6 +15229,7 @@ export {
|
|
|
14111
15229
|
createImagePaintTools,
|
|
14112
15230
|
createImagePsdTools,
|
|
14113
15231
|
createImageSelectionTools,
|
|
15232
|
+
createImageTransformTools,
|
|
14114
15233
|
createPigment,
|
|
14115
15234
|
createPigmentTools,
|
|
14116
15235
|
createSelectionMask,
|
|
@@ -14137,6 +15256,7 @@ export {
|
|
|
14137
15256
|
describeLayer,
|
|
14138
15257
|
describeLayerEffects,
|
|
14139
15258
|
describeSelection,
|
|
15259
|
+
describeTransform,
|
|
14140
15260
|
deserializeEffects,
|
|
14141
15261
|
deserializeLayer,
|
|
14142
15262
|
deserializeLayers,
|
|
@@ -14147,11 +15267,13 @@ export {
|
|
|
14147
15267
|
developTarget,
|
|
14148
15268
|
distanceTransform,
|
|
14149
15269
|
downloadPsd,
|
|
15270
|
+
dragWarpMeshPoint,
|
|
14150
15271
|
dropPlacement,
|
|
14151
15272
|
dropZone,
|
|
14152
15273
|
duplicateLayer,
|
|
14153
15274
|
edgeWidth,
|
|
14154
15275
|
effectColorHex,
|
|
15276
|
+
effectControlChange,
|
|
14155
15277
|
effectReach,
|
|
14156
15278
|
effectsFingerprint,
|
|
14157
15279
|
effectsMargin,
|
|
@@ -14164,6 +15286,7 @@ export {
|
|
|
14164
15286
|
encodeRle,
|
|
14165
15287
|
encodeSelectionRle,
|
|
14166
15288
|
encodeSurface,
|
|
15289
|
+
evalWarpMesh,
|
|
14167
15290
|
evaluateAdjustmentLayers,
|
|
14168
15291
|
expandCoverage,
|
|
14169
15292
|
exportPsd,
|
|
@@ -14174,9 +15297,11 @@ export {
|
|
|
14174
15297
|
findLayerControl,
|
|
14175
15298
|
finishTile,
|
|
14176
15299
|
flattenLayers,
|
|
15300
|
+
flipCorners,
|
|
14177
15301
|
formatControlValue,
|
|
14178
15302
|
fullSelection,
|
|
14179
15303
|
gaussianBlur,
|
|
15304
|
+
geometryHomography,
|
|
14180
15305
|
getAnnotationBounds,
|
|
14181
15306
|
getControlPath,
|
|
14182
15307
|
getCropCursor,
|
|
@@ -14191,6 +15316,7 @@ export {
|
|
|
14191
15316
|
handlePaintShortcut,
|
|
14192
15317
|
hasActiveEffects,
|
|
14193
15318
|
hasResampledLegacyMask,
|
|
15319
|
+
hasWarp,
|
|
14194
15320
|
hashMaskContent,
|
|
14195
15321
|
hashNoise,
|
|
14196
15322
|
hexToRgb,
|
|
@@ -14198,6 +15324,8 @@ export {
|
|
|
14198
15324
|
hitTestAnnotation,
|
|
14199
15325
|
hitTestCrop,
|
|
14200
15326
|
hitTestCurvePoint,
|
|
15327
|
+
homographyFromAffine,
|
|
15328
|
+
homographyFromPoints,
|
|
14201
15329
|
hslToRgb,
|
|
14202
15330
|
hueRange,
|
|
14203
15331
|
hueRangeTables,
|
|
@@ -14205,6 +15333,8 @@ export {
|
|
|
14205
15333
|
hueSaturation,
|
|
14206
15334
|
identityCurve,
|
|
14207
15335
|
identityLevels,
|
|
15336
|
+
identityTransform,
|
|
15337
|
+
identityWarpMesh,
|
|
14208
15338
|
imageEditorStateMigrations,
|
|
14209
15339
|
imageToCanvasPoint,
|
|
14210
15340
|
importPsd,
|
|
@@ -14212,12 +15342,15 @@ export {
|
|
|
14212
15342
|
inflateRaw,
|
|
14213
15343
|
insertCurvePoint,
|
|
14214
15344
|
insertLayer,
|
|
15345
|
+
invertHomography,
|
|
14215
15346
|
invertMatrix3,
|
|
14216
15347
|
invertSelection,
|
|
14217
15348
|
isActiveFilter,
|
|
14218
15349
|
isAdjustmentKind,
|
|
14219
15350
|
isBlendMode,
|
|
14220
15351
|
isDefaultDepthMask,
|
|
15352
|
+
isIdentityTransform,
|
|
15353
|
+
isInterpolation,
|
|
14221
15354
|
isLayerEffectKind,
|
|
14222
15355
|
isLayerLocked,
|
|
14223
15356
|
isLegacyShape,
|
|
@@ -14229,11 +15362,17 @@ export {
|
|
|
14229
15362
|
isSelectionMode,
|
|
14230
15363
|
isSeparableBlendMode,
|
|
14231
15364
|
isSpatialFilter,
|
|
15365
|
+
isTransformAnchor,
|
|
15366
|
+
isValidWarp,
|
|
14232
15367
|
kelvinToTemperature,
|
|
15368
|
+
keysWeight,
|
|
14233
15369
|
layerBadge,
|
|
15370
|
+
layerEffectControls,
|
|
14234
15371
|
layerEffectFields,
|
|
14235
15372
|
layerEffectLabel,
|
|
15373
|
+
layerEffectsBadge,
|
|
14236
15374
|
layerEffectsOf,
|
|
15375
|
+
layerEffectsPanel,
|
|
14237
15376
|
layerLabel,
|
|
14238
15377
|
layerMaskCoverage,
|
|
14239
15378
|
layerMaskFromSelection,
|
|
@@ -14258,6 +15397,8 @@ export {
|
|
|
14258
15397
|
mapCropHandle,
|
|
14259
15398
|
mapLayer,
|
|
14260
15399
|
maskSummary,
|
|
15400
|
+
matrixFromCorners,
|
|
15401
|
+
matrixFromParams,
|
|
14261
15402
|
mergeLayerEffect,
|
|
14262
15403
|
migrateSerializedImageEditorState,
|
|
14263
15404
|
migrateToLayeredState,
|
|
@@ -14308,6 +15449,8 @@ export {
|
|
|
14308
15449
|
polygonCoverage,
|
|
14309
15450
|
polygonSelection,
|
|
14310
15451
|
posterizeValue,
|
|
15452
|
+
presetWarp,
|
|
15453
|
+
previewStep,
|
|
14311
15454
|
projectImagePoint,
|
|
14312
15455
|
projectImageRect,
|
|
14313
15456
|
projectMaskCoverage,
|
|
@@ -14316,6 +15459,7 @@ export {
|
|
|
14316
15459
|
psdFileName,
|
|
14317
15460
|
psdPatternToEffectPattern,
|
|
14318
15461
|
psdToLayers,
|
|
15462
|
+
pullWarpMesh,
|
|
14319
15463
|
quickMaskPixels,
|
|
14320
15464
|
rangeLightness,
|
|
14321
15465
|
rasterMaskFromLegacy,
|
|
@@ -14325,6 +15469,9 @@ export {
|
|
|
14325
15469
|
readPsd,
|
|
14326
15470
|
readRegion,
|
|
14327
15471
|
rectSelection,
|
|
15472
|
+
rectToQuad,
|
|
15473
|
+
reduceSurface,
|
|
15474
|
+
referencePoint,
|
|
14328
15475
|
refineCoverage,
|
|
14329
15476
|
refineEdgeHalo,
|
|
14330
15477
|
refineSelection,
|
|
@@ -14340,6 +15487,7 @@ export {
|
|
|
14340
15487
|
requireUnlocked,
|
|
14341
15488
|
resizePixels,
|
|
14342
15489
|
resizeSelection,
|
|
15490
|
+
resizeWarpMesh,
|
|
14343
15491
|
resolveExternalAssets,
|
|
14344
15492
|
resolveMosaicRegionBounds,
|
|
14345
15493
|
rgbToGray,
|
|
@@ -14395,6 +15543,12 @@ export {
|
|
|
14395
15543
|
toneCurve,
|
|
14396
15544
|
toneLuts,
|
|
14397
15545
|
toneRange,
|
|
15546
|
+
transformHomography,
|
|
15547
|
+
transformMap,
|
|
15548
|
+
transformSurface,
|
|
15549
|
+
transformedBounds,
|
|
15550
|
+
transformedBox,
|
|
15551
|
+
transformedPoint,
|
|
14398
15552
|
translateSurface,
|
|
14399
15553
|
ungroupLayer,
|
|
14400
15554
|
uniformTile,
|
|
@@ -14411,6 +15565,11 @@ export {
|
|
|
14411
15565
|
vibranceLinear,
|
|
14412
15566
|
vibrancePixel,
|
|
14413
15567
|
walkDabs,
|
|
15568
|
+
warpMeshEditPoints,
|
|
15569
|
+
warpMeshGridCurves,
|
|
15570
|
+
warpMeshParamAt,
|
|
15571
|
+
warpPresetMesh,
|
|
15572
|
+
warpSurface,
|
|
14414
15573
|
wbMatrix,
|
|
14415
15574
|
whiteBalanceFromSample,
|
|
14416
15575
|
whiteBalanceMatrix,
|