@pixerate/editor 0.97.0 → 0.99.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-OEVSVA6V.js → chunk-WIR377F6.js} +788 -22
- package/dist/chunk-WIR377F6.js.map +1 -0
- package/dist/{chunk-4AB5BHYK.cjs → chunk-WT6ZAWJP.cjs} +788 -22
- 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 +1798 -333
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +482 -6
- package/dist/image-editor/index.d.ts +482 -6
- package/dist/image-editor/index.js +1488 -23
- package/dist/image-editor/index.js.map +1 -1
- package/dist/raster/index.cjs +23 -2
- package/dist/raster/index.cjs.map +1 -1
- package/dist/raster/index.d.cts +98 -2
- package/dist/raster/index.d.ts +98 -2
- package/dist/raster/index.js +23 -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-4AB5BHYK.cjs.map +0 -1
- package/dist/chunk-4JLB2USO.js.map +0 -1
- package/dist/chunk-7MD7C6XQ.cjs.map +0 -1
- package/dist/chunk-OEVSVA6V.js.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;
|
|
@@ -3150,6 +3193,44 @@ var ImageEditorRenderer = class {
|
|
|
3150
3193
|
}
|
|
3151
3194
|
ctx.restore();
|
|
3152
3195
|
}
|
|
3196
|
+
if (options.freeTransform) {
|
|
3197
|
+
const t = options.freeTransform;
|
|
3198
|
+
ctx.save();
|
|
3199
|
+
try {
|
|
3200
|
+
const scr = (p) => projectImagePoint(p, projection);
|
|
3201
|
+
const outline = t.outline.map(scr);
|
|
3202
|
+
ctx.beginPath();
|
|
3203
|
+
outline.forEach((p, i) => i ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y));
|
|
3204
|
+
ctx.closePath();
|
|
3205
|
+
ctx.lineWidth = 3;
|
|
3206
|
+
ctx.strokeStyle = "rgba(0,0,0,0.5)";
|
|
3207
|
+
ctx.stroke();
|
|
3208
|
+
ctx.lineWidth = 1;
|
|
3209
|
+
ctx.strokeStyle = "#ffffff";
|
|
3210
|
+
ctx.stroke();
|
|
3211
|
+
for (const h of t.handles.map(scr)) {
|
|
3212
|
+
ctx.fillStyle = "#ffffff";
|
|
3213
|
+
ctx.strokeStyle = "#1e293b";
|
|
3214
|
+
ctx.fillRect(h.x - 4, h.y - 4, 8, 8);
|
|
3215
|
+
ctx.strokeRect(h.x - 4, h.y - 4, 8, 8);
|
|
3216
|
+
}
|
|
3217
|
+
const r = scr(t.reference);
|
|
3218
|
+
ctx.beginPath();
|
|
3219
|
+
ctx.arc(r.x, r.y, 5, 0, Math.PI * 2);
|
|
3220
|
+
ctx.moveTo(r.x - 8, r.y);
|
|
3221
|
+
ctx.lineTo(r.x + 8, r.y);
|
|
3222
|
+
ctx.moveTo(r.x, r.y - 8);
|
|
3223
|
+
ctx.lineTo(r.x, r.y + 8);
|
|
3224
|
+
ctx.lineWidth = 3;
|
|
3225
|
+
ctx.strokeStyle = "rgba(0,0,0,0.5)";
|
|
3226
|
+
ctx.stroke();
|
|
3227
|
+
ctx.lineWidth = 1;
|
|
3228
|
+
ctx.strokeStyle = "#ffffff";
|
|
3229
|
+
ctx.stroke();
|
|
3230
|
+
} catch {
|
|
3231
|
+
}
|
|
3232
|
+
ctx.restore();
|
|
3233
|
+
}
|
|
3153
3234
|
if (isActivelyCropping) {
|
|
3154
3235
|
const activeCrop = options.draftCrop !== void 0 && options.draftCrop !== null ? options.draftCrop : crop ?? { x: 0, y: 0, width: naturalWidth, height: naturalHeight };
|
|
3155
3236
|
this.drawCropOverlay(ctx, getViewportMetrics(geoState, vpWidth, vpHeight, true, activeCrop));
|
|
@@ -3958,7 +4039,7 @@ function removeAnnotationLayers(layers) {
|
|
|
3958
4039
|
function polygonCoverage(points, width, height, antiAlias = true) {
|
|
3959
4040
|
const m = new Float32Array(width * height);
|
|
3960
4041
|
if (points.length < 3 || width <= 0 || height <= 0) return m;
|
|
3961
|
-
const
|
|
4042
|
+
const sub2 = antiAlias ? 4 : 1;
|
|
3962
4043
|
const edges = [];
|
|
3963
4044
|
for (let i = 0; i < points.length; i++) {
|
|
3964
4045
|
const a = points[i];
|
|
@@ -3973,8 +4054,8 @@ function polygonCoverage(points, width, height, antiAlias = true) {
|
|
|
3973
4054
|
const xs = [];
|
|
3974
4055
|
for (let py = 0; py < height; py++) {
|
|
3975
4056
|
const row = py * width;
|
|
3976
|
-
for (let s = 0; s <
|
|
3977
|
-
const y = py + (s + 0.5) /
|
|
4057
|
+
for (let s = 0; s < sub2; s++) {
|
|
4058
|
+
const y = py + (s + 0.5) / sub2;
|
|
3978
4059
|
while (next < edges.length && edges[next].top <= y) active.push(edges[next++]);
|
|
3979
4060
|
if (active.length) active = active.filter((e) => e.bottom > y);
|
|
3980
4061
|
if (!active.length) continue;
|
|
@@ -3996,7 +4077,7 @@ function polygonCoverage(points, width, height, antiAlias = true) {
|
|
|
3996
4077
|
const c = cx + 0.5;
|
|
3997
4078
|
cov = c >= x0 && c < x1 ? 1 : 0;
|
|
3998
4079
|
}
|
|
3999
|
-
if (cov > 0) m[row + cx] += cov /
|
|
4080
|
+
if (cov > 0) m[row + cx] += cov / sub2;
|
|
4000
4081
|
}
|
|
4001
4082
|
}
|
|
4002
4083
|
}
|
|
@@ -4584,6 +4665,7 @@ function serializeMask(mask, hint, ctx) {
|
|
|
4584
4665
|
if (mask.density !== void 0 && mask.density !== 1) out.density = mask.density;
|
|
4585
4666
|
if (mask.feather) out.feather = mask.feather;
|
|
4586
4667
|
if (mask.enabled === false) out.enabled = false;
|
|
4668
|
+
if (mask.linked === false) out.linked = false;
|
|
4587
4669
|
return out;
|
|
4588
4670
|
}
|
|
4589
4671
|
function serializeLayer(layer, ctx) {
|
|
@@ -4720,6 +4802,7 @@ function readMask(raw, ctx, what) {
|
|
|
4720
4802
|
if (typeof r.density === "number" && r.density !== 1) mask.density = num(r.density, 0, 1, 1);
|
|
4721
4803
|
if (typeof r.feather === "number" && r.feather > 0) mask.feather = num(r.feather, 0, 1e3, 0);
|
|
4722
4804
|
if (r.enabled === false) mask.enabled = false;
|
|
4805
|
+
if (r.linked === false) mask.linked = false;
|
|
4723
4806
|
if (r.source && typeof r.source === "object") {
|
|
4724
4807
|
const s = { ...DEFAULT_DEPTH_MASK_VALUES, ...r.source };
|
|
4725
4808
|
mask.source = { ...s, depthRange: [...s.depthRange] };
|
|
@@ -4989,7 +5072,12 @@ function offsetLayer(layers, id, offset) {
|
|
|
4989
5072
|
requireUnlocked(loc.layer, "position", "move it");
|
|
4990
5073
|
if (!Number.isFinite(offset.x) || !Number.isFinite(offset.y)) fail("x and y must be numbers.");
|
|
4991
5074
|
const layer = loc.layer;
|
|
4992
|
-
|
|
5075
|
+
const next = { ...layer, offset: { x: Math.round(offset.x), y: Math.round(offset.y) } };
|
|
5076
|
+
const mask = layer.mask;
|
|
5077
|
+
if (mask && mask.linked === false && !mask.source) {
|
|
5078
|
+
next.mask = { ...mask, surface: translateSurface(mask.surface, layer.offset.x - next.offset.x, layer.offset.y - next.offset.y) };
|
|
5079
|
+
}
|
|
5080
|
+
return mapLayer(layers, id, () => next);
|
|
4993
5081
|
}
|
|
4994
5082
|
function setLayerMask(layers, id, mask) {
|
|
4995
5083
|
const loc = requireLayer(layers, id);
|
|
@@ -6175,6 +6263,7 @@ var CHANNEL_USER_MASK = -2;
|
|
|
6175
6263
|
var CHANNEL_REAL_USER_MASK = -3;
|
|
6176
6264
|
var LAYER_FLAG_TRANSPARENCY_PROTECTED = 1;
|
|
6177
6265
|
var LAYER_FLAG_HIDDEN = 2;
|
|
6266
|
+
var MASK_FLAG_UNLINKED = 1;
|
|
6178
6267
|
var MASK_FLAG_DISABLED = 2;
|
|
6179
6268
|
var MASK_FLAG_PARAMETERS = 16;
|
|
6180
6269
|
var SECTION_OTHER = 0;
|
|
@@ -7604,6 +7693,7 @@ function recordMask(ctx, rec, name) {
|
|
|
7604
7693
|
if (m.userDensity !== void 0 && m.userDensity < 255) mask.density = percent(m.userDensity);
|
|
7605
7694
|
if (m.userFeather !== void 0 && m.userFeather > 0) mask.feather = Math.min(1e3, m.userFeather);
|
|
7606
7695
|
if (flags & MASK_FLAG_DISABLED) mask.enabled = false;
|
|
7696
|
+
if (flags & MASK_FLAG_UNLINKED) mask.linked = false;
|
|
7607
7697
|
return mask;
|
|
7608
7698
|
}
|
|
7609
7699
|
function baseProps(ctx, rec, name, notes) {
|
|
@@ -7890,7 +7980,8 @@ function maskChannel(mask, ox, oy, psb) {
|
|
|
7890
7980
|
if (mask.source) throw new PsdError("A parametric mask must be baked before export.");
|
|
7891
7981
|
const bounds = contentBounds(mask.surface);
|
|
7892
7982
|
const def = mask.defaultValue;
|
|
7893
|
-
const
|
|
7983
|
+
const flags = (mask.enabled === false ? MASK_FLAG_DISABLED : 0) | (mask.linked === false ? MASK_FLAG_UNLINKED : 0);
|
|
7984
|
+
const m = { rect: EMPTY_RECT, defaultColor: def, flags };
|
|
7894
7985
|
if ((mask.density ?? 1) < 1) m.userDensity = Math.round(Math.max(0, mask.density ?? 1) * 255);
|
|
7895
7986
|
if ((mask.feather ?? 0) > 0) m.userFeather = mask.feather;
|
|
7896
7987
|
if (!bounds) return { channel: emptyChannel(CHANNEL_USER_MASK), mask: m };
|
|
@@ -8087,6 +8178,499 @@ function exportPsd(input) {
|
|
|
8087
8178
|
return { bytes, psb, records: records.length, warnings };
|
|
8088
8179
|
}
|
|
8089
8180
|
|
|
8181
|
+
// src/image-editor/transform.ts
|
|
8182
|
+
var FREE_TRANSFORM_MODES = ["scale", "rotate", "skew", "distort", "perspective", "warp"];
|
|
8183
|
+
var TRANSFORM_ANCHORS = ["top-left", "top", "top-right", "left", "center", "right", "bottom-left", "bottom", "bottom-right"];
|
|
8184
|
+
var ANCHOR_FRACTION = {
|
|
8185
|
+
"top-left": [0, 0],
|
|
8186
|
+
top: [0.5, 0],
|
|
8187
|
+
"top-right": [1, 0],
|
|
8188
|
+
left: [0, 0.5],
|
|
8189
|
+
center: [0.5, 0.5],
|
|
8190
|
+
right: [1, 0.5],
|
|
8191
|
+
"bottom-left": [0, 1],
|
|
8192
|
+
bottom: [0.5, 1],
|
|
8193
|
+
"bottom-right": [1, 1]
|
|
8194
|
+
};
|
|
8195
|
+
function isTransformAnchor(value) {
|
|
8196
|
+
return typeof value === "string" && value in ANCHOR_FRACTION;
|
|
8197
|
+
}
|
|
8198
|
+
function identityTransform(box) {
|
|
8199
|
+
return { box: { ...box }, corners: boxQuad(box) };
|
|
8200
|
+
}
|
|
8201
|
+
function cornersFromMatrix(box, m) {
|
|
8202
|
+
const q2 = boxQuad(box);
|
|
8203
|
+
return [applyMatrix(m, q2[0]), applyMatrix(m, q2[1]), applyMatrix(m, q2[2]), applyMatrix(m, q2[3])];
|
|
8204
|
+
}
|
|
8205
|
+
function matrixFromCorners(box, corners, eps = 1e-6) {
|
|
8206
|
+
const [tl, tr, br, bl] = corners;
|
|
8207
|
+
const size = 1 + Math.max(box.width, box.height);
|
|
8208
|
+
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;
|
|
8209
|
+
const h = rectToQuad(box, corners);
|
|
8210
|
+
return h ? affineFromHomography(h, 1e-9) : null;
|
|
8211
|
+
}
|
|
8212
|
+
function transformHomography(geometry) {
|
|
8213
|
+
const h = rectToQuad(geometry.box, geometry.corners);
|
|
8214
|
+
if (!h) return null;
|
|
8215
|
+
return homographyKeepsQuad(h, boxQuad(geometry.box)) ? h : null;
|
|
8216
|
+
}
|
|
8217
|
+
function hasWarp(geometry) {
|
|
8218
|
+
const w = geometry.warp;
|
|
8219
|
+
if (!w) return false;
|
|
8220
|
+
const id = identityWarpMesh(geometry.box, w.us.length - 1, w.vs.length - 1);
|
|
8221
|
+
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;
|
|
8222
|
+
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);
|
|
8223
|
+
}
|
|
8224
|
+
function isIdentityTransform(geometry) {
|
|
8225
|
+
if (hasWarp(geometry)) return false;
|
|
8226
|
+
const q2 = boxQuad(geometry.box);
|
|
8227
|
+
return geometry.corners.every((c, i) => Math.abs(c.x - q2[i].x) < 1e-6 && Math.abs(c.y - q2[i].y) < 1e-6);
|
|
8228
|
+
}
|
|
8229
|
+
function transformMap(geometry) {
|
|
8230
|
+
const h = transformHomography(geometry);
|
|
8231
|
+
if (!h) return null;
|
|
8232
|
+
const { box } = geometry;
|
|
8233
|
+
const warp = hasWarp(geometry) ? geometry.warp : null;
|
|
8234
|
+
if (!warp) return (p) => applyHomography(h, p);
|
|
8235
|
+
return (p) => applyHomography(h, evalWarpMesh(warp, (p.x - box.x) / box.width, (p.y - box.y) / box.height));
|
|
8236
|
+
}
|
|
8237
|
+
function transformedPoint(geometry, fx, fy) {
|
|
8238
|
+
const f = transformMap(geometry);
|
|
8239
|
+
const { box } = geometry;
|
|
8240
|
+
return f ? f({ x: box.x + fx * box.width, y: box.y + fy * box.height }) : null;
|
|
8241
|
+
}
|
|
8242
|
+
function anchorPoint(geometry, anchor) {
|
|
8243
|
+
const [fx, fy] = ANCHOR_FRACTION[anchor];
|
|
8244
|
+
const h = rectToQuad(geometry.box, geometry.corners);
|
|
8245
|
+
const p = { x: geometry.box.x + fx * geometry.box.width, y: geometry.box.y + fy * geometry.box.height };
|
|
8246
|
+
return (h && applyHomography(h, p)) ?? p;
|
|
8247
|
+
}
|
|
8248
|
+
function transformedBox(geometry) {
|
|
8249
|
+
const f = transformMap(geometry);
|
|
8250
|
+
if (!f) return null;
|
|
8251
|
+
const { box } = geometry;
|
|
8252
|
+
const pts = [];
|
|
8253
|
+
const n = hasWarp(geometry) ? 16 : 1;
|
|
8254
|
+
for (let i = 0; i <= n; i++) {
|
|
8255
|
+
const t = i / n;
|
|
8256
|
+
for (const [fx, fy] of [[t, 0], [1, t], [1 - t, 1], [0, 1 - t]]) {
|
|
8257
|
+
const p = f({ x: box.x + fx * box.width, y: box.y + fy * box.height });
|
|
8258
|
+
if (!p) return null;
|
|
8259
|
+
pts.push(p);
|
|
8260
|
+
}
|
|
8261
|
+
}
|
|
8262
|
+
const xs = pts.map((p) => p.x);
|
|
8263
|
+
const ys = pts.map((p) => p.y);
|
|
8264
|
+
const x = Math.min(...xs);
|
|
8265
|
+
const y = Math.min(...ys);
|
|
8266
|
+
return { x, y, width: Math.max(...xs) - x, height: Math.max(...ys) - y };
|
|
8267
|
+
}
|
|
8268
|
+
function referencePoint(box, reference) {
|
|
8269
|
+
if (reference && typeof reference === "object") return { x: reference.x, y: reference.y };
|
|
8270
|
+
const [fx, fy] = ANCHOR_FRACTION[reference ?? "center"];
|
|
8271
|
+
return { x: box.x + fx * box.width, y: box.y + fy * box.height };
|
|
8272
|
+
}
|
|
8273
|
+
function matrixFromParams(box, params) {
|
|
8274
|
+
const sx = (params.scaleX ?? 100) / 100;
|
|
8275
|
+
const sy = (params.scaleY ?? 100) / 100;
|
|
8276
|
+
const tx = Math.tan((params.skewX ?? 0) * Math.PI / 180);
|
|
8277
|
+
const ty = Math.tan((params.skewY ?? 0) * Math.PI / 180);
|
|
8278
|
+
const ref = referencePoint(box, params.reference);
|
|
8279
|
+
const linear = multiplyMatrices(multiplyMatrices([1, ty, tx, 1, 0, 0], [sx, 0, 0, sy, 0, 0]), rotateMatrix(params.rotate ?? 0));
|
|
8280
|
+
return multiplyMatrices(matrixAbout(linear, ref), translateMatrix(params.dx ?? 0, params.dy ?? 0));
|
|
8281
|
+
}
|
|
8282
|
+
function describeTransform(geometry, reference = "center") {
|
|
8283
|
+
const m = hasWarp(geometry) ? null : matrixFromCorners(geometry.box, geometry.corners);
|
|
8284
|
+
const ref = referencePoint(geometry.box, reference);
|
|
8285
|
+
const bounds = transformedBox(geometry);
|
|
8286
|
+
if (!m) {
|
|
8287
|
+
const h = rectToQuad(geometry.box, geometry.corners);
|
|
8288
|
+
const at2 = (h && applyHomography(h, ref)) ?? ref;
|
|
8289
|
+
return { affine: false, scaleX: 100, scaleY: 100, rotate: 0, skewX: 0, x: at2.x, y: at2.y, bounds, corners: geometry.corners };
|
|
8290
|
+
}
|
|
8291
|
+
const [a, b, c, d] = m;
|
|
8292
|
+
const sxAbs = Math.hypot(a, b);
|
|
8293
|
+
const rot = Math.atan2(b, a) * 180 / Math.PI;
|
|
8294
|
+
const shear = sxAbs ? (a * c + b * d) / sxAbs : 0;
|
|
8295
|
+
const sy = sxAbs ? (a * d - b * c) / sxAbs : 0;
|
|
8296
|
+
const skew = sxAbs ? Math.atan(shear / sxAbs) * 180 / Math.PI : 0;
|
|
8297
|
+
const at = applyMatrix(m, ref);
|
|
8298
|
+
const clean = (v) => Math.abs(v) < 1e-9 ? 0 : v;
|
|
8299
|
+
return {
|
|
8300
|
+
affine: true,
|
|
8301
|
+
scaleX: clean(sxAbs * 100),
|
|
8302
|
+
scaleY: clean(sy * 100),
|
|
8303
|
+
rotate: clean(rot === -180 ? 180 : rot),
|
|
8304
|
+
skewX: clean(skew),
|
|
8305
|
+
x: at.x,
|
|
8306
|
+
y: at.y,
|
|
8307
|
+
bounds,
|
|
8308
|
+
corners: geometry.corners
|
|
8309
|
+
};
|
|
8310
|
+
}
|
|
8311
|
+
function applyParams(geometry, params) {
|
|
8312
|
+
const ref = params.reference && typeof params.reference === "object" ? params.reference : anchorPoint(geometry, params.reference ?? "center");
|
|
8313
|
+
const m = matrixFromParams({ x: ref.x, y: ref.y, width: 0, height: 0 }, { ...params, reference: ref });
|
|
8314
|
+
const c = geometry.corners;
|
|
8315
|
+
return [applyMatrix(m, c[0]), applyMatrix(m, c[1]), applyMatrix(m, c[2]), applyMatrix(m, c[3])];
|
|
8316
|
+
}
|
|
8317
|
+
function flipCorners(geometry, axis) {
|
|
8318
|
+
const c = anchorPoint(geometry, "center");
|
|
8319
|
+
const m = axis === "horizontal" ? [-1, 0, 0, 1, 2 * c.x, 0] : [1, 0, 0, -1, 0, 2 * c.y];
|
|
8320
|
+
const q2 = geometry.corners;
|
|
8321
|
+
return [applyMatrix(m, q2[0]), applyMatrix(m, q2[1]), applyMatrix(m, q2[2]), applyMatrix(m, q2[3])];
|
|
8322
|
+
}
|
|
8323
|
+
function boxPointOf(geometry, p) {
|
|
8324
|
+
const m = matrixFromCorners(geometry.box, geometry.corners);
|
|
8325
|
+
if (m) return applyMatrix(invertMatrix(m), p);
|
|
8326
|
+
return null;
|
|
8327
|
+
}
|
|
8328
|
+
function presetWarp(box, preset) {
|
|
8329
|
+
return warpPresetMesh(preset, box);
|
|
8330
|
+
}
|
|
8331
|
+
function isValidWarp(mesh) {
|
|
8332
|
+
return isValidWarpMesh(mesh);
|
|
8333
|
+
}
|
|
8334
|
+
function geometryHomography(geometry) {
|
|
8335
|
+
const m = matrixFromCorners(geometry.box, geometry.corners);
|
|
8336
|
+
return m ? homographyFromAffine(m) : transformHomography(geometry);
|
|
8337
|
+
}
|
|
8338
|
+
|
|
8339
|
+
// src/image-editor/free-transform.ts
|
|
8340
|
+
var FREE_TRANSFORM_CONTENTS = ["auto", "layer", "selection", "selection-copy", "selection-outline"];
|
|
8341
|
+
var fail2 = (message) => {
|
|
8342
|
+
throw new LayerOperationError(message);
|
|
8343
|
+
};
|
|
8344
|
+
var T = (x, y) => [1, 0, x, 0, 1, y, 0, 0, 1];
|
|
8345
|
+
function previewStep(scale) {
|
|
8346
|
+
if (!scale || !(scale > 0) || scale >= 1) return 1;
|
|
8347
|
+
return Math.min(8, 2 ** Math.floor(Math.log2(1 / scale)));
|
|
8348
|
+
}
|
|
8349
|
+
function toDoc(r, o) {
|
|
8350
|
+
return r ? { x: r.x + o.x, y: r.y + o.y, width: r.width, height: r.height } : null;
|
|
8351
|
+
}
|
|
8352
|
+
function selectionSurface(sel) {
|
|
8353
|
+
return surfaceFromPixels({ width: sel.width, height: sel.height, data: sel.data }, 1, 0, 0, 0);
|
|
8354
|
+
}
|
|
8355
|
+
function liftPixels(surface, sel, o) {
|
|
8356
|
+
const sb = sel.bounds;
|
|
8357
|
+
const area = { x: sb.x - o.x, y: sb.y - o.y, width: sb.width, height: sb.height };
|
|
8358
|
+
const px2 = readRegion(surface, area);
|
|
8359
|
+
const kept = new Uint8ClampedArray(px2);
|
|
8360
|
+
for (let y = 0; y < area.height; y++) {
|
|
8361
|
+
for (let x = 0; x < area.width; x++) {
|
|
8362
|
+
const k = sel.data[(sb.y + y) * sel.width + sb.x + x] / 255;
|
|
8363
|
+
const i = (y * area.width + x) * 4;
|
|
8364
|
+
if (k <= 0) {
|
|
8365
|
+
px2[i] = px2[i + 1] = px2[i + 2] = px2[i + 3] = 0;
|
|
8366
|
+
continue;
|
|
8367
|
+
}
|
|
8368
|
+
px2[i + 3] = px2[i + 3] * k;
|
|
8369
|
+
if (!px2[i + 3]) px2[i] = px2[i + 1] = px2[i + 2] = 0;
|
|
8370
|
+
kept[i + 3] = kept[i + 3] * (1 - k);
|
|
8371
|
+
if (!kept[i + 3]) kept[i] = kept[i + 1] = kept[i + 2] = 0;
|
|
8372
|
+
}
|
|
8373
|
+
}
|
|
8374
|
+
return { lifted: surfaceFromPixels({ width: area.width, height: area.height, data: px2 }, 4, area.x, area.y), rest: writeRegion(surface, area, kept) };
|
|
8375
|
+
}
|
|
8376
|
+
function liftMask(mask, sel, o) {
|
|
8377
|
+
const sb = sel.bounds;
|
|
8378
|
+
const area = { x: sb.x - o.x, y: sb.y - o.y, width: sb.width, height: sb.height };
|
|
8379
|
+
const v = readRegion(mask, area);
|
|
8380
|
+
const def = mask.defaultValue;
|
|
8381
|
+
const lifted = new Uint8ClampedArray(area.width * area.height * 4);
|
|
8382
|
+
const rest = new Uint8ClampedArray(v.length);
|
|
8383
|
+
for (let y = 0; y < area.height; y++) {
|
|
8384
|
+
for (let x = 0; x < area.width; x++) {
|
|
8385
|
+
const i = y * area.width + x;
|
|
8386
|
+
const k = sel.data[(sb.y + y) * sel.width + sb.x + x] / 255;
|
|
8387
|
+
const g = v[i];
|
|
8388
|
+
if (k > 0) {
|
|
8389
|
+
lifted[i * 4] = lifted[i * 4 + 1] = lifted[i * 4 + 2] = g;
|
|
8390
|
+
lifted[i * 4 + 3] = k * 255;
|
|
8391
|
+
}
|
|
8392
|
+
rest[i] = g * (1 - k) + def * k;
|
|
8393
|
+
}
|
|
8394
|
+
}
|
|
8395
|
+
return { lifted: surfaceFromPixels({ width: area.width, height: area.height, data: lifted }, 4, area.x, area.y), rest: writeRegion(mask, area, rest) };
|
|
8396
|
+
}
|
|
8397
|
+
var FreeTransformSession = class {
|
|
8398
|
+
constructor(host, init = {}) {
|
|
8399
|
+
this.host = host;
|
|
8400
|
+
if (init.interpolation !== void 0 && !isInterpolation(init.interpolation)) fail2(`Unknown interpolation "${String(init.interpolation)}"; use nearest, bilinear or bicubic.`);
|
|
8401
|
+
this.interp = init.interpolation ?? "bicubic";
|
|
8402
|
+
const sel = host.selection();
|
|
8403
|
+
const selected = Boolean(sel?.bounds);
|
|
8404
|
+
let content = init.content ?? "auto";
|
|
8405
|
+
if (!["auto", "layer", "selection", "selection-copy", "selection-outline"].includes(content)) {
|
|
8406
|
+
fail2(`Unknown content "${String(content)}"; use auto, layer, selection, selection-copy or selection-outline.`);
|
|
8407
|
+
}
|
|
8408
|
+
if (content === "auto") content = selected ? "selection" : "layer";
|
|
8409
|
+
this.content = content;
|
|
8410
|
+
if (content !== "layer" && !selected) fail2(`Nothing is selected; ${content} needs a selection. Select first, or use content layer.`);
|
|
8411
|
+
const layers = host.layers();
|
|
8412
|
+
this.startLayers = layers;
|
|
8413
|
+
this.startSelection = sel;
|
|
8414
|
+
this.startActive = host.activeLayerId();
|
|
8415
|
+
this.before = host.capture();
|
|
8416
|
+
const moves = [];
|
|
8417
|
+
let box = null;
|
|
8418
|
+
if (content === "selection-outline") {
|
|
8419
|
+
this.layerId = null;
|
|
8420
|
+
this.parts.push({ what: "selection", offset: { x: 0, y: 0 }, moving: selectionSurface(sel), base: null, reduced: /* @__PURE__ */ new Map() });
|
|
8421
|
+
moves.push("selection");
|
|
8422
|
+
box = { ...sel.bounds };
|
|
8423
|
+
} else {
|
|
8424
|
+
const id = init.layerId ?? host.activeLayerId();
|
|
8425
|
+
if (!id) fail2("No layer given and none is active; pass layerId (see list_layers).");
|
|
8426
|
+
const layer = requireLayer(layers, id).layer;
|
|
8427
|
+
this.layerId = layer.id;
|
|
8428
|
+
const mask = layer.mask && !layer.mask.source ? layer.mask : void 0;
|
|
8429
|
+
const channel = init.channel ?? (layer.id === host.activeLayerId() ? host.activeTarget() : "pixels");
|
|
8430
|
+
if (channel !== "pixels" && channel !== "mask") fail2(`Unknown channel "${String(channel)}"; use pixels or mask.`);
|
|
8431
|
+
const linked = mask?.linked !== false;
|
|
8432
|
+
const maskAlone = channel === "mask" && (!linked || layer.type !== "pixel");
|
|
8433
|
+
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.`);
|
|
8434
|
+
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.`);
|
|
8435
|
+
if (!layer.visible && content !== "selection-copy") fail2(`Layer "${layer.name || id}" is hidden; show it before you transform it.`);
|
|
8436
|
+
if (content !== "selection-copy") {
|
|
8437
|
+
requireUnlocked(layer, "position", "transform it");
|
|
8438
|
+
if (!maskAlone) requireUnlocked(layer, "pixels", "transform it");
|
|
8439
|
+
}
|
|
8440
|
+
const o = layer.type === "pixel" ? layer.offset : { x: 0, y: 0 };
|
|
8441
|
+
const pixels = layer.type === "pixel" ? host.pixelsOf(layer) : null;
|
|
8442
|
+
if (maskAlone) {
|
|
8443
|
+
if (content === "selection-copy") fail2("Layer via Copy copies pixels; transform a mask with content layer or selection.");
|
|
8444
|
+
if (content === "selection") {
|
|
8445
|
+
const { lifted, rest } = liftMask(mask.surface, sel, o);
|
|
8446
|
+
this.parts.push({ what: "mask", offset: o, moving: lifted, base: rest, reduced: /* @__PURE__ */ new Map() });
|
|
8447
|
+
box = toDoc(contentBounds(lifted), o);
|
|
8448
|
+
} else {
|
|
8449
|
+
this.parts.push({ what: "mask", offset: o, moving: mask.surface, base: null, reduced: /* @__PURE__ */ new Map() });
|
|
8450
|
+
box = toDoc(contentBounds(mask.surface), o);
|
|
8451
|
+
}
|
|
8452
|
+
moves.push("mask");
|
|
8453
|
+
} else if (content === "layer") {
|
|
8454
|
+
this.parts.push({ what: "pixels", offset: o, moving: pixels, base: null, reduced: /* @__PURE__ */ new Map() });
|
|
8455
|
+
moves.push("pixels");
|
|
8456
|
+
box = toDoc(contentBounds(pixels), o);
|
|
8457
|
+
if (mask && linked) {
|
|
8458
|
+
this.parts.push({ what: "mask", offset: o, moving: mask.surface, base: null, reduced: /* @__PURE__ */ new Map() });
|
|
8459
|
+
moves.push("mask");
|
|
8460
|
+
if (!box) box = toDoc(contentBounds(mask.surface), o);
|
|
8461
|
+
}
|
|
8462
|
+
} else if (content === "selection") {
|
|
8463
|
+
const { lifted, rest } = liftPixels(pixels, sel, o);
|
|
8464
|
+
this.parts.push({ what: "pixels", offset: o, moving: lifted, base: rest, reduced: /* @__PURE__ */ new Map() });
|
|
8465
|
+
moves.push("pixels");
|
|
8466
|
+
box = toDoc(contentBounds(lifted), o);
|
|
8467
|
+
if (mask && linked) {
|
|
8468
|
+
const m = liftMask(mask.surface, sel, o);
|
|
8469
|
+
this.parts.push({ what: "mask", offset: o, moving: m.lifted, base: m.rest, reduced: /* @__PURE__ */ new Map() });
|
|
8470
|
+
moves.push("mask");
|
|
8471
|
+
}
|
|
8472
|
+
} else {
|
|
8473
|
+
const { lifted } = liftPixels(pixels, sel, o);
|
|
8474
|
+
box = toDoc(contentBounds(lifted), o);
|
|
8475
|
+
if (box) {
|
|
8476
|
+
const copy = {
|
|
8477
|
+
type: "pixel",
|
|
8478
|
+
id: newLayerId(),
|
|
8479
|
+
name: `${layer.name || "Layer"} copy`,
|
|
8480
|
+
visible: true,
|
|
8481
|
+
opacity: 1,
|
|
8482
|
+
blend: "normal",
|
|
8483
|
+
surface: lifted,
|
|
8484
|
+
offset: { ...o }
|
|
8485
|
+
};
|
|
8486
|
+
this.copyId = copy.id;
|
|
8487
|
+
const loc = findLayer(layers, layer.id);
|
|
8488
|
+
this.startLayers = insertLayer(layers, copy, loc.parent?.id ?? null, loc.index + 1);
|
|
8489
|
+
this.parts.push({ what: "pixels", offset: o, moving: lifted, base: null, reduced: /* @__PURE__ */ new Map() });
|
|
8490
|
+
moves.push("pixels");
|
|
8491
|
+
}
|
|
8492
|
+
}
|
|
8493
|
+
if (content !== "layer") {
|
|
8494
|
+
this.parts.push({ what: "selection", offset: { x: 0, y: 0 }, moving: selectionSurface(sel), base: null, reduced: /* @__PURE__ */ new Map() });
|
|
8495
|
+
moves.push("selection");
|
|
8496
|
+
}
|
|
8497
|
+
}
|
|
8498
|
+
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).");
|
|
8499
|
+
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).");
|
|
8500
|
+
this.box = box;
|
|
8501
|
+
this.moves = moves;
|
|
8502
|
+
this.geometryValue = identityTransform(box);
|
|
8503
|
+
this.shown = this.startLayers;
|
|
8504
|
+
if (this.copyId) host.show(this.startLayers, sel, this.copyId);
|
|
8505
|
+
this.shown = host.layers();
|
|
8506
|
+
}
|
|
8507
|
+
host;
|
|
8508
|
+
content;
|
|
8509
|
+
/** The layer being transformed (`null` for `selection-outline`). */
|
|
8510
|
+
layerId;
|
|
8511
|
+
/** The new layer of a `selection-copy`. */
|
|
8512
|
+
copyId = null;
|
|
8513
|
+
/** The frame (document pixels). */
|
|
8514
|
+
box;
|
|
8515
|
+
/** What moves. */
|
|
8516
|
+
moves;
|
|
8517
|
+
geometryValue;
|
|
8518
|
+
interp;
|
|
8519
|
+
parts = [];
|
|
8520
|
+
before;
|
|
8521
|
+
startLayers;
|
|
8522
|
+
startSelection;
|
|
8523
|
+
startActive;
|
|
8524
|
+
/** The stack this session last showed: anything else means the document changed under it. */
|
|
8525
|
+
shown;
|
|
8526
|
+
closed = false;
|
|
8527
|
+
/** The current geometry. */
|
|
8528
|
+
get geometry() {
|
|
8529
|
+
return this.geometryValue;
|
|
8530
|
+
}
|
|
8531
|
+
get interpolation() {
|
|
8532
|
+
return this.interp;
|
|
8533
|
+
}
|
|
8534
|
+
get isOpen() {
|
|
8535
|
+
return !this.closed;
|
|
8536
|
+
}
|
|
8537
|
+
check() {
|
|
8538
|
+
if (this.closed) fail2("This transform has already been committed or cancelled; begin a new one.");
|
|
8539
|
+
if (this.host.layers() !== this.shown) {
|
|
8540
|
+
this.closed = true;
|
|
8541
|
+
fail2("The document changed while the transform was open, so it was abandoned; begin a new one.");
|
|
8542
|
+
}
|
|
8543
|
+
}
|
|
8544
|
+
/**
|
|
8545
|
+
* Changes the geometry (`corners`, `warp`) or the interpolation and, unless `preview` is `false`,
|
|
8546
|
+
* shows the result live (no history). Returns the geometry.
|
|
8547
|
+
*/
|
|
8548
|
+
update(changes, preview = {}) {
|
|
8549
|
+
this.check();
|
|
8550
|
+
if (changes.interpolation !== void 0) {
|
|
8551
|
+
if (!isInterpolation(changes.interpolation)) fail2(`Unknown interpolation "${String(changes.interpolation)}"; use nearest, bilinear or bicubic.`);
|
|
8552
|
+
this.interp = changes.interpolation;
|
|
8553
|
+
}
|
|
8554
|
+
const next = {
|
|
8555
|
+
box: this.box,
|
|
8556
|
+
corners: (changes.corners ?? this.geometryValue.corners).map((p) => ({ x: p.x, y: p.y })),
|
|
8557
|
+
warp: changes.warp === void 0 ? this.geometryValue.warp ?? null : changes.warp
|
|
8558
|
+
};
|
|
8559
|
+
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.");
|
|
8560
|
+
if (!transformMap(next)) fail2("These corners fold the frame over itself or through the horizon; keep the quad convex.");
|
|
8561
|
+
this.geometryValue = next;
|
|
8562
|
+
if (preview !== false) this.render(preview);
|
|
8563
|
+
return next;
|
|
8564
|
+
}
|
|
8565
|
+
/** Resamples every part for the current geometry and shows it (or returns it, for commit). */
|
|
8566
|
+
resample(final, preview = {}) {
|
|
8567
|
+
const g = this.geometryValue;
|
|
8568
|
+
const identity = isIdentityTransform(g);
|
|
8569
|
+
const warp = hasWarp(g);
|
|
8570
|
+
const h = warp ? null : geometryHomography(g);
|
|
8571
|
+
const f = warp ? transformMap(g) : null;
|
|
8572
|
+
const step = final ? 1 : previewStep(preview.scale);
|
|
8573
|
+
const interpolation = final ? this.interp : this.interp === "nearest" ? "nearest" : "bilinear";
|
|
8574
|
+
let layers = this.startLayers;
|
|
8575
|
+
let selection = this.startSelection;
|
|
8576
|
+
const targetId = this.copyId ?? this.layerId;
|
|
8577
|
+
for (const part of this.parts) {
|
|
8578
|
+
const o = part.offset;
|
|
8579
|
+
let moved;
|
|
8580
|
+
if (identity) moved = part.moving;
|
|
8581
|
+
else {
|
|
8582
|
+
let src = part.moving;
|
|
8583
|
+
let k = 1;
|
|
8584
|
+
if (step > 1) {
|
|
8585
|
+
k = step;
|
|
8586
|
+
let r = part.reduced.get(k);
|
|
8587
|
+
if (!r) {
|
|
8588
|
+
r = reduceSurface(part.moving, k);
|
|
8589
|
+
part.reduced.set(k, r);
|
|
8590
|
+
}
|
|
8591
|
+
src = r;
|
|
8592
|
+
}
|
|
8593
|
+
const opts = { interpolation, step: step > 1 ? step : void 0 };
|
|
8594
|
+
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 };
|
|
8595
|
+
try {
|
|
8596
|
+
if (f) {
|
|
8597
|
+
moved = warpSurface(src, (p) => {
|
|
8598
|
+
const q2 = f({ x: p.x * k + o.x, y: p.y * k + o.y });
|
|
8599
|
+
return q2 && { x: q2.x - o.x, y: q2.y - o.y };
|
|
8600
|
+
}, opts);
|
|
8601
|
+
} else {
|
|
8602
|
+
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));
|
|
8603
|
+
moved = transformSurface(src, hl, opts);
|
|
8604
|
+
}
|
|
8605
|
+
} catch (e) {
|
|
8606
|
+
if (e instanceof TransformTooLargeError) fail2(e.message);
|
|
8607
|
+
throw e;
|
|
8608
|
+
}
|
|
8609
|
+
}
|
|
8610
|
+
const result = part.base ? compositeSurfaceOver(part.base, moved) : moved;
|
|
8611
|
+
if (part.what === "selection") {
|
|
8612
|
+
const { width, height } = this.host.canvas();
|
|
8613
|
+
const data = new Uint8Array(readRegion(result, { x: 0, y: 0, width, height }).buffer);
|
|
8614
|
+
const next = createSelectionMask(width, height, data);
|
|
8615
|
+
selection = next.bounds ? next : null;
|
|
8616
|
+
} else if (part.what === "pixels") {
|
|
8617
|
+
layers = mapLayer(layers, targetId, (l) => {
|
|
8618
|
+
const next = { ...l, surface: result };
|
|
8619
|
+
delete next.source;
|
|
8620
|
+
return next;
|
|
8621
|
+
});
|
|
8622
|
+
} else {
|
|
8623
|
+
layers = mapLayer(layers, targetId, (l) => ({ ...l, mask: { ...l.mask, surface: result } }));
|
|
8624
|
+
}
|
|
8625
|
+
}
|
|
8626
|
+
return { layers, selection };
|
|
8627
|
+
}
|
|
8628
|
+
render(preview) {
|
|
8629
|
+
const { layers, selection } = this.resample(false, preview);
|
|
8630
|
+
this.host.show(layers, selection, this.copyId ?? this.startActive);
|
|
8631
|
+
this.shown = this.host.layers();
|
|
8632
|
+
}
|
|
8633
|
+
/** Finishes the transform with full-quality resampling, as one undo step. */
|
|
8634
|
+
commit() {
|
|
8635
|
+
this.check();
|
|
8636
|
+
const g = this.geometryValue;
|
|
8637
|
+
const changed = !isIdentityTransform(g) || Boolean(this.copyId);
|
|
8638
|
+
if (!changed) {
|
|
8639
|
+
this.closed = true;
|
|
8640
|
+
this.host.restore(this.before);
|
|
8641
|
+
return this.result(false, this.startLayers, this.startSelection);
|
|
8642
|
+
}
|
|
8643
|
+
const { layers, selection } = this.resample(true);
|
|
8644
|
+
this.closed = true;
|
|
8645
|
+
this.host.show(layers, selection, this.copyId ?? this.startActive);
|
|
8646
|
+
this.host.record(this.before);
|
|
8647
|
+
return this.result(true, layers, selection);
|
|
8648
|
+
}
|
|
8649
|
+
/** Abandons the transform and puts everything back (no history). */
|
|
8650
|
+
cancel() {
|
|
8651
|
+
if (this.closed) return;
|
|
8652
|
+
this.closed = true;
|
|
8653
|
+
this.host.restore(this.before);
|
|
8654
|
+
}
|
|
8655
|
+
result(changed, layers, selection) {
|
|
8656
|
+
let bounds = null;
|
|
8657
|
+
const id = this.copyId ?? this.layerId;
|
|
8658
|
+
const layer = id ? findLayer(layers, id)?.layer : void 0;
|
|
8659
|
+
if (this.moves.includes("pixels") && layer?.type === "pixel") bounds = toDoc(contentBounds(this.host.pixelsOf(layer)), layer.offset);
|
|
8660
|
+
else if (this.moves.includes("mask") && layer?.mask) bounds = toDoc(contentBounds(layer.mask.surface), layer.type === "pixel" ? layer.offset : { x: 0, y: 0 });
|
|
8661
|
+
else if (this.moves.includes("selection")) bounds = selection?.bounds ? { ...selection.bounds } : null;
|
|
8662
|
+
return {
|
|
8663
|
+
layerId: this.copyId ?? this.layerId,
|
|
8664
|
+
content: this.content,
|
|
8665
|
+
moved: [...this.moves],
|
|
8666
|
+
box: { ...this.box },
|
|
8667
|
+
corners: this.geometryValue.corners,
|
|
8668
|
+
bounds,
|
|
8669
|
+
changed
|
|
8670
|
+
};
|
|
8671
|
+
}
|
|
8672
|
+
};
|
|
8673
|
+
|
|
8090
8674
|
// src/image-editor/controller.ts
|
|
8091
8675
|
var IMAGE_EDITOR_SCHEMA_VERSION = LAYERED_SCHEMA_VERSION;
|
|
8092
8676
|
function paintCoverageInto(base, coverage, apply, rects) {
|
|
@@ -9038,7 +9622,7 @@ var ImageEditorController = class {
|
|
|
9038
9622
|
if (this.state.activeLayerId === id) this.state.activeTarget = "pixels";
|
|
9039
9623
|
});
|
|
9040
9624
|
}
|
|
9041
|
-
/** Changes any layer mask's density, feather or
|
|
9625
|
+
/** Changes any layer mask's density, feather, enabled or linked flag (pixels kept). Returns the mask. */
|
|
9042
9626
|
setLayerMaskProps(id, props, commitToHistory = true) {
|
|
9043
9627
|
const layer = requireLayer(this.state.layers, id).layer;
|
|
9044
9628
|
if (!layer.mask) throw new LayerOperationError(`Layer "${layer.name || id}" has no mask; add one with add_layer_mask.`);
|
|
@@ -9130,6 +9714,134 @@ var ImageEditorController = class {
|
|
|
9130
9714
|
});
|
|
9131
9715
|
return copy.id;
|
|
9132
9716
|
}
|
|
9717
|
+
// --- Free Transform ---
|
|
9718
|
+
openTransform = null;
|
|
9719
|
+
/** The Free Transform in progress (see `beginFreeTransform`), or `null`. */
|
|
9720
|
+
getFreeTransform() {
|
|
9721
|
+
if (this.openTransform && !this.openTransform.isOpen) this.openTransform = null;
|
|
9722
|
+
return this.openTransform;
|
|
9723
|
+
}
|
|
9724
|
+
transformHost() {
|
|
9725
|
+
return {
|
|
9726
|
+
layers: () => this.state.layers,
|
|
9727
|
+
activeLayerId: () => this.state.activeLayerId,
|
|
9728
|
+
activeTarget: () => this.state.activeTarget,
|
|
9729
|
+
selection: () => this.state.selection?.bounds ? this.state.selection : null,
|
|
9730
|
+
canvas: () => this.naturalSize,
|
|
9731
|
+
pixelsOf: (layer) => this.surfaceOf(layer),
|
|
9732
|
+
capture: () => this.captureHistoryEntry(),
|
|
9733
|
+
show: (layers, selection, active) => {
|
|
9734
|
+
this.state.layers = layers;
|
|
9735
|
+
this.state.selection = selection;
|
|
9736
|
+
if (active !== this.state.activeLayerId) {
|
|
9737
|
+
this.state.activeLayerId = active;
|
|
9738
|
+
this.state.activeTarget = "pixels";
|
|
9739
|
+
}
|
|
9740
|
+
this.notify();
|
|
9741
|
+
},
|
|
9742
|
+
record: (before) => {
|
|
9743
|
+
if (this.isApplyingHistory) return;
|
|
9744
|
+
this.undoStack.push(before);
|
|
9745
|
+
if (this.undoStack.length > this.maxHistoryDepth) this.undoStack.shift();
|
|
9746
|
+
this.redoStack = [];
|
|
9747
|
+
this.trimHistoryBytes();
|
|
9748
|
+
this.notify();
|
|
9749
|
+
},
|
|
9750
|
+
restore: (before) => {
|
|
9751
|
+
const e = before;
|
|
9752
|
+
this.state.layers = e.layers;
|
|
9753
|
+
this.state.selection = e.selection;
|
|
9754
|
+
this.state.activeLayerId = e.activeLayerId;
|
|
9755
|
+
this.state.activeTarget = e.activeTarget;
|
|
9756
|
+
this.notify();
|
|
9757
|
+
}
|
|
9758
|
+
};
|
|
9759
|
+
}
|
|
9760
|
+
/**
|
|
9761
|
+
* Starts a Free Transform (Edit › Free Transform, Transform Selection, or a transform of a Layer
|
|
9762
|
+
* via Copy): of the layer's pixels with its linked mask, an unlinked mask alone, the selected
|
|
9763
|
+
* pixels or just the selection outline (see `FreeTransformInit`). Change the geometry with
|
|
9764
|
+
* `session.update` (live preview, no history), then `commit()` (one undo step) or `cancel()`.
|
|
9765
|
+
* Refused while another transform is open.
|
|
9766
|
+
*/
|
|
9767
|
+
beginFreeTransform(init = {}) {
|
|
9768
|
+
if (this.getFreeTransform()) throw new LayerOperationError("A Free Transform is already open; commit or cancel it first.");
|
|
9769
|
+
const session = new FreeTransformSession(this.transformHost(), init);
|
|
9770
|
+
this.openTransform = session;
|
|
9771
|
+
this.notify();
|
|
9772
|
+
return session;
|
|
9773
|
+
}
|
|
9774
|
+
/**
|
|
9775
|
+
* Transforms in one step (agents, scripts): begins a Free Transform for `spec`, sets its corners
|
|
9776
|
+
* from exactly one of `matrix` (document-space affine), `corners` (where the frame's corners go:
|
|
9777
|
+
* distort / perspective) or `params` (scale, rotate, skew and move about a reference point), and
|
|
9778
|
+
* commits it as one undo step. Returns what changed and the new bounds.
|
|
9779
|
+
*/
|
|
9780
|
+
transformLayer(spec) {
|
|
9781
|
+
const given = [spec.matrix, spec.corners, spec.params].filter((v) => v !== void 0).length;
|
|
9782
|
+
if (given > 1) throw new LayerOperationError("Give only one of matrix, corners and params.");
|
|
9783
|
+
if (!given && spec.warp === void 0) throw new LayerOperationError("Give matrix, corners or params (or a warp).");
|
|
9784
|
+
if (spec.matrix && !(Array.isArray(spec.matrix) && spec.matrix.length === 6 && spec.matrix.every(Number.isFinite))) {
|
|
9785
|
+
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).");
|
|
9786
|
+
}
|
|
9787
|
+
if (spec.matrix) {
|
|
9788
|
+
const [a, b, c, d] = spec.matrix;
|
|
9789
|
+
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.");
|
|
9790
|
+
}
|
|
9791
|
+
const session = this.beginFreeTransform(spec);
|
|
9792
|
+
try {
|
|
9793
|
+
const g = identityTransform(session.box);
|
|
9794
|
+
const corners = spec.matrix ? cornersFromMatrix(session.box, spec.matrix) : spec.corners ? spec.corners : spec.params ? applyParams(g, spec.params) : g.corners;
|
|
9795
|
+
let warp = null;
|
|
9796
|
+
if (spec.warp) {
|
|
9797
|
+
if ("style" in spec.warp) {
|
|
9798
|
+
if (!WARP_STYLES.includes(spec.warp.style)) throw new LayerOperationError(`Unknown warp style "${String(spec.warp.style)}". Styles: ${WARP_STYLES.join(", ")}.`);
|
|
9799
|
+
warp = presetWarp(session.box, spec.warp);
|
|
9800
|
+
} else {
|
|
9801
|
+
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.");
|
|
9802
|
+
warp = spec.warp;
|
|
9803
|
+
}
|
|
9804
|
+
}
|
|
9805
|
+
session.update({ corners, warp }, false);
|
|
9806
|
+
return session.commit();
|
|
9807
|
+
} catch (e) {
|
|
9808
|
+
session.cancel();
|
|
9809
|
+
throw e;
|
|
9810
|
+
}
|
|
9811
|
+
}
|
|
9812
|
+
/** Flips the layer (or its selected pixels, or the outline) about the frame's centre, exactly (one undo step). */
|
|
9813
|
+
flipLayer(axis, init = {}) {
|
|
9814
|
+
if (axis !== "horizontal" && axis !== "vertical") throw new LayerOperationError(`axis must be horizontal or vertical, got "${String(axis)}".`);
|
|
9815
|
+
const session = this.beginFreeTransform({ interpolation: "nearest", ...init });
|
|
9816
|
+
try {
|
|
9817
|
+
session.update({ corners: flipCorners(identityTransform(session.box), axis) }, false);
|
|
9818
|
+
return session.commit();
|
|
9819
|
+
} catch (e) {
|
|
9820
|
+
session.cancel();
|
|
9821
|
+
throw e;
|
|
9822
|
+
}
|
|
9823
|
+
}
|
|
9824
|
+
/**
|
|
9825
|
+
* Rotates by a quarter turn (90 clockwise, −90 counter-clockwise) or 180 about the frame's
|
|
9826
|
+
* centre, snapped so pixels land on whole pixels: exact, no resampling (one undo step).
|
|
9827
|
+
*/
|
|
9828
|
+
rotateLayer90(turn, init = {}) {
|
|
9829
|
+
if (turn !== 90 && turn !== -90 && turn !== 180) throw new LayerOperationError(`turn must be 90, -90 or 180, got ${String(turn)}.`);
|
|
9830
|
+
const session = this.beginFreeTransform({ interpolation: "nearest", ...init });
|
|
9831
|
+
try {
|
|
9832
|
+
const b = session.box;
|
|
9833
|
+
const cx = b.x + b.width / 2;
|
|
9834
|
+
const cy = b.y + b.height / 2;
|
|
9835
|
+
const ox = turn === 180 ? 0 : Math.round(cx - b.height / 2) - (cx - b.height / 2);
|
|
9836
|
+
const oy = turn === 180 ? 0 : Math.round(cy - b.width / 2) - (cy - b.width / 2);
|
|
9837
|
+
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];
|
|
9838
|
+
session.update({ corners: cornersFromMatrix(b, m) }, false);
|
|
9839
|
+
return session.commit();
|
|
9840
|
+
} catch (e) {
|
|
9841
|
+
session.cancel();
|
|
9842
|
+
throw e;
|
|
9843
|
+
}
|
|
9844
|
+
}
|
|
9133
9845
|
/** Adds a `develop` or `blur` filter to a pixel layer (one undo step). Returns the layer's filters. */
|
|
9134
9846
|
addLayerFilter(id, filter) {
|
|
9135
9847
|
const f = normalizeFilter(filter);
|
|
@@ -10506,7 +11218,7 @@ var ImageEditorController = class {
|
|
|
10506
11218
|
const canvas = { x: 0, y: 0, width, height };
|
|
10507
11219
|
const warnings = [];
|
|
10508
11220
|
const note = /* @__PURE__ */ new Map();
|
|
10509
|
-
const
|
|
11221
|
+
const add2 = (msg, name) => note.set(msg, [...note.get(msg) ?? [], name]);
|
|
10510
11222
|
const canRasterize = this.renderer.canRasterize();
|
|
10511
11223
|
await this.renderer.prepareLayerAssets(this.state.layers, width, height);
|
|
10512
11224
|
const sources = this.renderer.compositorSources(width, height);
|
|
@@ -10514,7 +11226,7 @@ var ImageEditorController = class {
|
|
|
10514
11226
|
const bakeMask = (layer) => {
|
|
10515
11227
|
const mask = layer.mask;
|
|
10516
11228
|
if (!mask?.source) return mask;
|
|
10517
|
-
|
|
11229
|
+
add2("Parametric (depth) masks were baked into pixel masks", layer.name);
|
|
10518
11230
|
const plain3 = { ...layer, mask: { ...mask, density: 1, feather: 0, enabled: true } };
|
|
10519
11231
|
const ox = layer.type === "pixel" ? layer.offset.x : 0;
|
|
10520
11232
|
const oy = layer.type === "pixel" ? layer.offset.y : 0;
|
|
@@ -10530,7 +11242,7 @@ var ImageEditorController = class {
|
|
|
10530
11242
|
};
|
|
10531
11243
|
const prepare = async (layer) => {
|
|
10532
11244
|
const p = await prepareLayer(layer);
|
|
10533
|
-
if (p?.blend === "pigment")
|
|
11245
|
+
if (p?.blend === "pigment") add2("The Pigment blend mode has no PSD equivalent and was written as Normal", layer.name);
|
|
10534
11246
|
return p;
|
|
10535
11247
|
};
|
|
10536
11248
|
const prepareLayer = async (layer) => {
|
|
@@ -10545,10 +11257,10 @@ var ImageEditorController = class {
|
|
|
10545
11257
|
if (layer.type === "adjustment") return { ...layer, mask: bakeMask(layer) };
|
|
10546
11258
|
if (layer.type === "annotation") {
|
|
10547
11259
|
if (!canRasterize) {
|
|
10548
|
-
|
|
11260
|
+
add2("Annotation layers need a canvas (a browser) to rasterize and were left out", layer.name);
|
|
10549
11261
|
return null;
|
|
10550
11262
|
}
|
|
10551
|
-
|
|
11263
|
+
add2("Annotation layers were written as pixel layers", layer.name);
|
|
10552
11264
|
const blend = layer.blend === "normal" && layer.annotation.blend ? layer.annotation.blend : layer.blend;
|
|
10553
11265
|
const content = { ...layer, visible: true, opacity: 1, blend: "normal", annotation: { ...layer.annotation, blend: void 0 } };
|
|
10554
11266
|
delete content.mask;
|
|
@@ -10579,7 +11291,7 @@ var ImageEditorController = class {
|
|
|
10579
11291
|
}
|
|
10580
11292
|
const active = layer.filters?.filter(isActiveFilter) ?? [];
|
|
10581
11293
|
if (active.length) {
|
|
10582
|
-
|
|
11294
|
+
add2("Layer filters (develop, blur) were baked into the pixels", layer.name);
|
|
10583
11295
|
const content = { ...out, visible: true, opacity: 1, blend: "normal" };
|
|
10584
11296
|
delete content.mask;
|
|
10585
11297
|
delete content.clipped;
|
|
@@ -10588,7 +11300,7 @@ var ImageEditorController = class {
|
|
|
10588
11300
|
const data = compositor.composite([content], canvas, sources);
|
|
10589
11301
|
out = { ...out, surface: surfaceFromPixels({ width, height, data }, 4, -layer.offset.x, -layer.offset.y) };
|
|
10590
11302
|
}
|
|
10591
|
-
if (layer.filters?.some((f) => !f.enabled))
|
|
11303
|
+
if (layer.filters?.some((f) => !f.enabled)) add2("Disabled layer filters were left out", layer.name);
|
|
10592
11304
|
delete out.filters;
|
|
10593
11305
|
const mask = bakeMask(layer);
|
|
10594
11306
|
if (mask) out.mask = mask;
|
|
@@ -10868,6 +11580,10 @@ function withMaskProps(mask, props) {
|
|
|
10868
11580
|
if (props.enabled) delete next.enabled;
|
|
10869
11581
|
else next.enabled = false;
|
|
10870
11582
|
}
|
|
11583
|
+
if (props.linked !== void 0) {
|
|
11584
|
+
if (props.linked) delete next.linked;
|
|
11585
|
+
else next.linked = false;
|
|
11586
|
+
}
|
|
10871
11587
|
return next;
|
|
10872
11588
|
}
|
|
10873
11589
|
function resizeLegacyMasks(layers, from, to) {
|
|
@@ -11985,6 +12701,7 @@ function maskSummary(mask) {
|
|
|
11985
12701
|
const out = {
|
|
11986
12702
|
kind: mask.source ? "depth" : "raster",
|
|
11987
12703
|
enabled: mask.enabled !== false,
|
|
12704
|
+
linked: mask.linked !== false,
|
|
11988
12705
|
density: mask.density ?? 1,
|
|
11989
12706
|
feather: mask.feather ?? 0
|
|
11990
12707
|
};
|
|
@@ -12361,23 +13078,29 @@ function createImageLayerTools(controller) {
|
|
|
12361
13078
|
const maskProps = {
|
|
12362
13079
|
name: "set_layer_mask_properties",
|
|
12363
13080
|
title: "Set layer mask properties",
|
|
12364
|
-
description: "Changes a layer mask's density (0..1), feather (px) or
|
|
13081
|
+
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
13082
|
inputSchema: {
|
|
12366
13083
|
type: "object",
|
|
12367
13084
|
properties: {
|
|
12368
13085
|
layerId: LAYER_ID2,
|
|
12369
13086
|
density: { type: "number", minimum: 0, maximum: 1, description: "Density 0..1." },
|
|
12370
13087
|
feather: { type: "number", minimum: 0, maximum: 1e3, description: "Feather \u03C3 in px." },
|
|
12371
|
-
enabled: { type: "boolean", description: "false ignores the mask but keeps it." }
|
|
13088
|
+
enabled: { type: "boolean", description: "false ignores the mask but keeps it." },
|
|
13089
|
+
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
13090
|
},
|
|
12373
13091
|
required: ["layerId"],
|
|
12374
13092
|
additionalProperties: false
|
|
12375
13093
|
},
|
|
12376
13094
|
annotations: { title: "Set layer mask properties", ...WRITE_IDEMPOTENT2 },
|
|
12377
|
-
handler: ({ layerId, density, feather, enabled }) => {
|
|
13095
|
+
handler: ({ layerId, density, feather, enabled, linked }) => {
|
|
12378
13096
|
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, {
|
|
13097
|
+
if ([density, feather, enabled, linked].every((v) => v === void 0)) throw new LayerOperationError("Give density, feather, enabled or linked.");
|
|
13098
|
+
const mask = controller.setLayerMaskProps(id, {
|
|
13099
|
+
density,
|
|
13100
|
+
feather,
|
|
13101
|
+
enabled,
|
|
13102
|
+
linked
|
|
13103
|
+
});
|
|
12381
13104
|
return { layerId: id, mask: maskSummary(mask) };
|
|
12382
13105
|
}
|
|
12383
13106
|
};
|
|
@@ -12731,7 +13454,7 @@ function createImageEffectTools(controller) {
|
|
|
12731
13454
|
annotations: { title: "List layer effects", ...READ3 },
|
|
12732
13455
|
handler: ({ layerId }) => result(layerArg(layerId), { globalLight: controller.getGlobalLight(), patterns: patternList(controller), kinds: [...LAYER_EFFECT_KINDS] })
|
|
12733
13456
|
};
|
|
12734
|
-
const
|
|
13457
|
+
const add2 = {
|
|
12735
13458
|
name: "add_layer_effect",
|
|
12736
13459
|
title: "Add a layer effect",
|
|
12737
13460
|
description: "Adds a live effect to a pixel, annotation or group layer with Photoshop's defaults, changed by settings: drop-shadow, inner-shadow, outer-glow, inner-glow, bevel-emboss, satin, color-overlay, gradient-overlay, pattern-overlay or stroke. Several of one kind are allowed (the first listed is on top). One undo step. Returns the new effect's id and the layer's effects.",
|
|
@@ -12829,7 +13552,7 @@ function createImageEffectTools(controller) {
|
|
|
12829
13552
|
return { globalLight: controller.setGlobalLight(next) };
|
|
12830
13553
|
}
|
|
12831
13554
|
};
|
|
12832
|
-
return [list2,
|
|
13555
|
+
return [list2, add2, update, remove, setEnabled, light];
|
|
12833
13556
|
}
|
|
12834
13557
|
|
|
12835
13558
|
// src/image-editor/layers/paint-ui.ts
|
|
@@ -13238,6 +13961,248 @@ function createImagePsdTools(controller) {
|
|
|
13238
13961
|
return [inspect, open, save];
|
|
13239
13962
|
}
|
|
13240
13963
|
|
|
13964
|
+
// src/image-editor/transform-tools.ts
|
|
13965
|
+
var READ6 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
13966
|
+
var WRITE5 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
13967
|
+
var round4 = (n, digits = 3) => Math.round(n * 10 ** digits) / 10 ** digits;
|
|
13968
|
+
var roundPoint = (p) => ({ x: round4(p.x), y: round4(p.y) });
|
|
13969
|
+
var POINT = {
|
|
13970
|
+
type: "object",
|
|
13971
|
+
properties: { x: { type: "number", description: "Document pixels from the left." }, y: { type: "number", description: "Document pixels from the top." } },
|
|
13972
|
+
required: ["x", "y"],
|
|
13973
|
+
additionalProperties: false
|
|
13974
|
+
};
|
|
13975
|
+
var TARGET2 = {
|
|
13976
|
+
layerId: { type: "string", description: "Layer to transform (from list_layers; default the active layer)." },
|
|
13977
|
+
content: {
|
|
13978
|
+
type: "string",
|
|
13979
|
+
enum: FREE_TRANSFORM_CONTENTS.filter((c) => c !== "selection-outline"),
|
|
13980
|
+
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)."
|
|
13981
|
+
},
|
|
13982
|
+
channel: {
|
|
13983
|
+
type: "string",
|
|
13984
|
+
enum: ["pixels", "mask"],
|
|
13985
|
+
description: "pixels (default) moves the pixels and a linked mask; mask moves an unlinked mask alone (a linked mask always moves with its pixels)."
|
|
13986
|
+
},
|
|
13987
|
+
interpolation: { type: "string", enum: INTERPOLATIONS, description: "Resampling: nearest (hard pixels), bilinear or bicubic (default, Photoshop\u2019s Bicubic)." }
|
|
13988
|
+
};
|
|
13989
|
+
var GEOMETRY = {
|
|
13990
|
+
scaleX: { type: "number", description: "Width in percent of the frame (default 100; negative flips horizontally)." },
|
|
13991
|
+
scaleY: { type: "number", description: "Height in percent of the frame (default 100; negative flips vertically)." },
|
|
13992
|
+
rotate: { type: "number", description: "Rotation in degrees, clockwise (default 0)." },
|
|
13993
|
+
skewX: { type: "number", minimum: -89, maximum: 89, description: "Horizontal skew in degrees: vertical edges lean right for positive angles (default 0)." },
|
|
13994
|
+
skewY: { type: "number", minimum: -89, maximum: 89, description: "Vertical skew in degrees: horizontal edges lean down for positive angles (default 0)." },
|
|
13995
|
+
dx: { type: "number", description: "Move right after scaling and rotating, in document pixels (default 0)." },
|
|
13996
|
+
dy: { type: "number", description: "Move down after scaling and rotating, in document pixels (default 0)." },
|
|
13997
|
+
reference: {
|
|
13998
|
+
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}.",
|
|
13999
|
+
anyOf: [{ type: "string", enum: TRANSFORM_ANCHORS }, POINT]
|
|
14000
|
+
},
|
|
14001
|
+
matrix: {
|
|
14002
|
+
type: "array",
|
|
14003
|
+
items: { type: "number" },
|
|
14004
|
+
minItems: 6,
|
|
14005
|
+
maxItems: 6,
|
|
14006
|
+
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)."
|
|
14007
|
+
},
|
|
14008
|
+
corners: {
|
|
14009
|
+
type: "array",
|
|
14010
|
+
items: POINT,
|
|
14011
|
+
minItems: 4,
|
|
14012
|
+
maxItems: 4,
|
|
14013
|
+
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."
|
|
14014
|
+
}
|
|
14015
|
+
};
|
|
14016
|
+
var NUMERIC = ["scaleX", "scaleY", "rotate", "skewX", "skewY", "dx", "dy", "reference"];
|
|
14017
|
+
function geometryFrom(args) {
|
|
14018
|
+
const numeric = NUMERIC.filter((k) => args[k] !== void 0);
|
|
14019
|
+
const ways = [numeric.length > 0, args.matrix !== void 0, args.corners !== void 0].filter(Boolean).length;
|
|
14020
|
+
if (ways === 0) throw new LayerOperationError("Give the transform one way: numeric fields (scaleX, scaleY, rotate, skewX, skewY, dx, dy, reference), matrix, or corners.");
|
|
14021
|
+
if (ways > 1) throw new LayerOperationError("Give the transform only one way: numeric fields, matrix or corners, not a mix.");
|
|
14022
|
+
if (args.matrix !== void 0) return { matrix: args.matrix };
|
|
14023
|
+
if (args.corners !== void 0) return { corners: args.corners };
|
|
14024
|
+
const params = {};
|
|
14025
|
+
for (const k of NUMERIC) if (args[k] !== void 0) params[k] = args[k];
|
|
14026
|
+
if (params.scaleX === 0 || params.scaleY === 0) throw new LayerOperationError("scaleX and scaleY must not be 0 (that collapses the layer to a line).");
|
|
14027
|
+
if (typeof params.reference === "string" && !isTransformAnchor(params.reference)) throw new LayerOperationError(`Unknown reference "${params.reference}". Use ${TRANSFORM_ANCHORS.join(", ")} or {x, y}.`);
|
|
14028
|
+
return { params };
|
|
14029
|
+
}
|
|
14030
|
+
function targetFrom(args) {
|
|
14031
|
+
const init = {};
|
|
14032
|
+
if (typeof args.layerId === "string") init.layerId = args.layerId;
|
|
14033
|
+
if (args.content !== void 0) init.content = args.content;
|
|
14034
|
+
init.channel = args.channel ?? "pixels";
|
|
14035
|
+
if (args.interpolation !== void 0) init.interpolation = args.interpolation;
|
|
14036
|
+
return init;
|
|
14037
|
+
}
|
|
14038
|
+
function summary2(res) {
|
|
14039
|
+
return {
|
|
14040
|
+
layerId: res.layerId,
|
|
14041
|
+
content: res.content,
|
|
14042
|
+
moved: res.moved,
|
|
14043
|
+
changed: res.changed,
|
|
14044
|
+
frame: { ...res.box },
|
|
14045
|
+
corners: res.corners.map(roundPoint),
|
|
14046
|
+
bounds: res.bounds
|
|
14047
|
+
};
|
|
14048
|
+
}
|
|
14049
|
+
function createImageTransformTools(controller) {
|
|
14050
|
+
const getFrame = {
|
|
14051
|
+
name: "get_transform_frame",
|
|
14052
|
+
title: "Get the Free Transform frame",
|
|
14053
|
+
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.",
|
|
14054
|
+
inputSchema: {
|
|
14055
|
+
type: "object",
|
|
14056
|
+
properties: {
|
|
14057
|
+
layerId: TARGET2.layerId,
|
|
14058
|
+
content: { type: "string", enum: FREE_TRANSFORM_CONTENTS, description: `${TARGET2.content.description} selection-outline frames the selection outline alone.` },
|
|
14059
|
+
channel: TARGET2.channel,
|
|
14060
|
+
meshColumns: { type: "integer", minimum: 1, maximum: 16, description: "Patch columns of an identity warp mesh to return (omit for none)." },
|
|
14061
|
+
meshRows: { type: "integer", minimum: 1, maximum: 16, description: "Patch rows of an identity warp mesh to return (default meshColumns)." }
|
|
14062
|
+
},
|
|
14063
|
+
additionalProperties: false
|
|
14064
|
+
},
|
|
14065
|
+
annotations: { title: "Get the Free Transform frame", ...READ6 },
|
|
14066
|
+
handler: (args) => {
|
|
14067
|
+
const init = targetFrom(args);
|
|
14068
|
+
if (init.content === "selection-copy") init.content = "selection";
|
|
14069
|
+
const s = controller.beginFreeTransform(init);
|
|
14070
|
+
const out = {
|
|
14071
|
+
layerId: s.layerId,
|
|
14072
|
+
content: args.content === "selection-copy" ? "selection-copy" : s.content,
|
|
14073
|
+
moved: [...s.moves],
|
|
14074
|
+
frame: { ...s.box },
|
|
14075
|
+
corners: identityTransform(s.box).corners.map(roundPoint),
|
|
14076
|
+
center: roundPoint({ x: s.box.x + s.box.width / 2, y: s.box.y + s.box.height / 2 })
|
|
14077
|
+
};
|
|
14078
|
+
s.cancel();
|
|
14079
|
+
if (args.meshColumns !== void 0) {
|
|
14080
|
+
const cols = args.meshColumns;
|
|
14081
|
+
const rows = args.meshRows ?? cols;
|
|
14082
|
+
const mesh = identityWarpMesh(s.box, cols, rows);
|
|
14083
|
+
out.mesh = { columns: cols, rows, pointsPerRow: warpMeshColumns(mesh), pointRows: warpMeshRows(mesh), points: mesh.points.map(roundPoint) };
|
|
14084
|
+
}
|
|
14085
|
+
return out;
|
|
14086
|
+
}
|
|
14087
|
+
};
|
|
14088
|
+
const transformLayer = {
|
|
14089
|
+
name: "transform_layer",
|
|
14090
|
+
title: "Free Transform a layer",
|
|
14091
|
+
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.",
|
|
14092
|
+
inputSchema: {
|
|
14093
|
+
type: "object",
|
|
14094
|
+
properties: { ...TARGET2, ...GEOMETRY },
|
|
14095
|
+
additionalProperties: false
|
|
14096
|
+
},
|
|
14097
|
+
annotations: { title: "Free Transform a layer", ...WRITE5 },
|
|
14098
|
+
handler: (args) => summary2(controller.transformLayer({ ...targetFrom(args), ...geometryFrom(args) }))
|
|
14099
|
+
};
|
|
14100
|
+
const warpLayer = {
|
|
14101
|
+
name: "warp_layer",
|
|
14102
|
+
title: "Warp a layer",
|
|
14103
|
+
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.",
|
|
14104
|
+
inputSchema: {
|
|
14105
|
+
type: "object",
|
|
14106
|
+
properties: {
|
|
14107
|
+
...TARGET2,
|
|
14108
|
+
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." },
|
|
14109
|
+
bend: { type: "number", minimum: -100, maximum: 100, description: "Preset bend in percent, \u2212100..100 (default 50)." },
|
|
14110
|
+
horizontal: { type: "number", minimum: -100, maximum: 100, description: "Preset horizontal distortion in percent, \u2212100..100 (default 0)." },
|
|
14111
|
+
vertical: { type: "number", minimum: -100, maximum: 100, description: "Preset vertical distortion in percent, \u2212100..100 (default 0)." },
|
|
14112
|
+
orientation: { type: "string", enum: ["horizontal", "vertical"], description: "Bend along the horizontal (default) or vertical axis." },
|
|
14113
|
+
mesh: {
|
|
14114
|
+
type: "object",
|
|
14115
|
+
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.",
|
|
14116
|
+
properties: {
|
|
14117
|
+
columns: { type: "integer", minimum: 1, maximum: 16, description: "Patch columns." },
|
|
14118
|
+
rows: { type: "integer", minimum: 1, maximum: 16, description: "Patch rows." },
|
|
14119
|
+
points: { type: "array", items: POINT, minItems: 16, description: "Control points, row by row, document pixels." }
|
|
14120
|
+
},
|
|
14121
|
+
required: ["columns", "rows", "points"],
|
|
14122
|
+
additionalProperties: false
|
|
14123
|
+
}
|
|
14124
|
+
},
|
|
14125
|
+
additionalProperties: false
|
|
14126
|
+
},
|
|
14127
|
+
annotations: { title: "Warp a layer", ...WRITE5 },
|
|
14128
|
+
handler: (args) => {
|
|
14129
|
+
const hasPreset = ["style", "bend", "horizontal", "vertical", "orientation"].some((k) => args[k] !== void 0);
|
|
14130
|
+
if (hasPreset && args.mesh !== void 0) throw new LayerOperationError("Give a preset (style, bend, horizontal, vertical, orientation) or a mesh, not both.");
|
|
14131
|
+
if (!hasPreset && args.mesh === void 0) throw new LayerOperationError("Give a preset style or a mesh.");
|
|
14132
|
+
let warp;
|
|
14133
|
+
if (hasPreset) {
|
|
14134
|
+
if (args.style === void 0) throw new LayerOperationError(`style is required with a preset. Styles: ${WARP_STYLES.join(", ")}.`);
|
|
14135
|
+
warp = {
|
|
14136
|
+
style: args.style,
|
|
14137
|
+
bend: args.bend ?? 50,
|
|
14138
|
+
horizontal: args.horizontal,
|
|
14139
|
+
vertical: args.vertical,
|
|
14140
|
+
orientation: args.orientation
|
|
14141
|
+
};
|
|
14142
|
+
} else {
|
|
14143
|
+
const m = args.mesh;
|
|
14144
|
+
const want = (3 * m.columns + 1) * (3 * m.rows + 1);
|
|
14145
|
+
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}.`);
|
|
14146
|
+
const knots = (n) => Array.from({ length: n + 1 }, (_, i) => i / n);
|
|
14147
|
+
warp = { us: knots(m.columns), vs: knots(m.rows), points: m.points.map((p) => ({ x: p.x, y: p.y })) };
|
|
14148
|
+
}
|
|
14149
|
+
return summary2(controller.transformLayer({ ...targetFrom(args), warp }));
|
|
14150
|
+
}
|
|
14151
|
+
};
|
|
14152
|
+
const transformSelection = {
|
|
14153
|
+
name: "transform_selection",
|
|
14154
|
+
title: "Transform the selection outline",
|
|
14155
|
+
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.",
|
|
14156
|
+
inputSchema: {
|
|
14157
|
+
type: "object",
|
|
14158
|
+
properties: { interpolation: { ...TARGET2.interpolation, description: "Resampling of soft edges: nearest, bilinear or bicubic (default)." }, ...GEOMETRY },
|
|
14159
|
+
additionalProperties: false
|
|
14160
|
+
},
|
|
14161
|
+
annotations: { title: "Transform the selection outline", ...WRITE5 },
|
|
14162
|
+
handler: (args) => {
|
|
14163
|
+
const res = controller.transformLayer({ content: "selection-outline", interpolation: args.interpolation, ...geometryFrom(args) });
|
|
14164
|
+
return { changed: res.changed, frame: { ...res.box }, corners: res.corners.map(roundPoint), bounds: res.bounds };
|
|
14165
|
+
}
|
|
14166
|
+
};
|
|
14167
|
+
const flipLayer = {
|
|
14168
|
+
name: "flip_layer",
|
|
14169
|
+
title: "Flip a layer",
|
|
14170
|
+
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.",
|
|
14171
|
+
inputSchema: {
|
|
14172
|
+
type: "object",
|
|
14173
|
+
properties: {
|
|
14174
|
+
axis: { type: "string", enum: ["horizontal", "vertical"], description: "horizontal mirrors left \u2194 right, vertical top \u2194 bottom." },
|
|
14175
|
+
layerId: TARGET2.layerId,
|
|
14176
|
+
content: TARGET2.content,
|
|
14177
|
+
channel: TARGET2.channel
|
|
14178
|
+
},
|
|
14179
|
+
required: ["axis"],
|
|
14180
|
+
additionalProperties: false
|
|
14181
|
+
},
|
|
14182
|
+
annotations: { title: "Flip a layer", ...WRITE5 },
|
|
14183
|
+
handler: (args) => summary2(controller.flipLayer(args.axis, targetFrom(args)))
|
|
14184
|
+
};
|
|
14185
|
+
const rotate90 = {
|
|
14186
|
+
name: "rotate_layer_90",
|
|
14187
|
+
title: "Rotate a layer by quarter turns",
|
|
14188
|
+
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.",
|
|
14189
|
+
inputSchema: {
|
|
14190
|
+
type: "object",
|
|
14191
|
+
properties: {
|
|
14192
|
+
turn: { type: "integer", enum: [90, -90, 180], description: "Degrees: 90 clockwise, -90 counter-clockwise, or 180." },
|
|
14193
|
+
layerId: TARGET2.layerId,
|
|
14194
|
+
content: TARGET2.content,
|
|
14195
|
+
channel: TARGET2.channel
|
|
14196
|
+
},
|
|
14197
|
+
required: ["turn"],
|
|
14198
|
+
additionalProperties: false
|
|
14199
|
+
},
|
|
14200
|
+
annotations: { title: "Rotate a layer by quarter turns", ...WRITE5 },
|
|
14201
|
+
handler: (args) => summary2(controller.rotateLayer90(args.turn, targetFrom(args)))
|
|
14202
|
+
};
|
|
14203
|
+
return [getFrame, transformLayer, warpLayer, transformSelection, flipLayer, rotate90];
|
|
14204
|
+
}
|
|
14205
|
+
|
|
13241
14206
|
// src/image-editor/tools.ts
|
|
13242
14207
|
var LAYER_PATH = /^layers\.([^.]+)\.(.+)$/;
|
|
13243
14208
|
function adjustmentsSummary(adjustments) {
|
|
@@ -13475,6 +14440,7 @@ function createImageEditorToolProvider(controller, options = {}) {
|
|
|
13475
14440
|
...createImageSelectionTools(controller),
|
|
13476
14441
|
...createImageEditTools(controller),
|
|
13477
14442
|
...createImageLayerTools(controller),
|
|
14443
|
+
...createImageTransformTools(controller),
|
|
13478
14444
|
...createImageEffectTools(controller),
|
|
13479
14445
|
...createImagePaintTools(controller),
|
|
13480
14446
|
...createPigmentTools(),
|
|
@@ -13851,6 +14817,7 @@ function layerMenu(controller) {
|
|
|
13851
14817
|
{ action: "apply-mask", label: "Apply Layer Mask", enabled: isPixel && Boolean(active?.mask) },
|
|
13852
14818
|
{ action: "delete-mask", label: "Delete Layer Mask", enabled: Boolean(active?.mask) },
|
|
13853
14819
|
{ action: "toggle-mask", label: active?.mask?.enabled === false ? "Enable Layer Mask" : "Disable Layer Mask", enabled: Boolean(active?.mask) },
|
|
14820
|
+
{ action: "toggle-mask-link", label: active?.mask?.linked === false ? "Link Layer Mask" : "Unlink Layer Mask", enabled: Boolean(active?.mask && !active.mask.source) },
|
|
13854
14821
|
{ action: "rasterize", label: "Rasterize Layer", enabled: active?.type === "annotation" || isPixel && Boolean(active?.type === "pixel" && active.filters?.length) },
|
|
13855
14822
|
{ action: "toggle-clip", label: active?.clipped ? "Release Clipping Mask" : "Create Clipping Mask", enabled: Boolean(loc && loc.index > 0) }
|
|
13856
14823
|
];
|
|
@@ -13906,6 +14873,9 @@ async function runLayerMenuAction(controller, action) {
|
|
|
13906
14873
|
case "toggle-mask":
|
|
13907
14874
|
if (id && active?.mask) controller.setLayerMaskProps(id, { enabled: active.mask.enabled === false });
|
|
13908
14875
|
break;
|
|
14876
|
+
case "toggle-mask-link":
|
|
14877
|
+
if (id && active?.mask) controller.setLayerMaskProps(id, { linked: active.mask.linked === false });
|
|
14878
|
+
break;
|
|
13909
14879
|
case "rasterize":
|
|
13910
14880
|
if (id) await controller.rasterizeLayer(id);
|
|
13911
14881
|
break;
|
|
@@ -14124,6 +15094,445 @@ async function downloadPsd(controller, name = "image") {
|
|
|
14124
15094
|
}
|
|
14125
15095
|
return { fileName, warnings: res.warnings };
|
|
14126
15096
|
}
|
|
15097
|
+
|
|
15098
|
+
// src/image-editor/layers/transform-ui.ts
|
|
15099
|
+
var CORNERS = ["nw", "ne", "se", "sw"];
|
|
15100
|
+
var SIDES = ["n", "e", "s", "w"];
|
|
15101
|
+
var CORNER_INDEX = { nw: 0, ne: 1, se: 2, sw: 3 };
|
|
15102
|
+
var SIDE_CORNERS = { n: [0, 1], e: [1, 2], s: [2, 3], w: [3, 0] };
|
|
15103
|
+
var HANDLE_FRACTION = { nw: [0, 0], n: [0.5, 0], ne: [1, 0], e: [1, 0.5], se: [1, 1], s: [0.5, 1], sw: [0, 1], w: [0, 0.5] };
|
|
15104
|
+
var ANCHOR_FRACTION2 = {
|
|
15105
|
+
"top-left": [0, 0],
|
|
15106
|
+
top: [0.5, 0],
|
|
15107
|
+
"top-right": [1, 0],
|
|
15108
|
+
left: [0, 0.5],
|
|
15109
|
+
center: [0.5, 0.5],
|
|
15110
|
+
right: [1, 0.5],
|
|
15111
|
+
"bottom-left": [0, 1],
|
|
15112
|
+
bottom: [0.5, 1],
|
|
15113
|
+
"bottom-right": [1, 1]
|
|
15114
|
+
};
|
|
15115
|
+
var FREE_TRANSFORM_MODE_OPTIONS = [
|
|
15116
|
+
{ id: "scale", label: "Free", title: "Free Transform: corners scale, outside rotates, Ctrl/Cmd distorts" },
|
|
15117
|
+
{ id: "rotate", label: "Rotate", title: "Every handle rotates about the reference point" },
|
|
15118
|
+
{ id: "skew", label: "Skew", title: "Sides slide along themselves" },
|
|
15119
|
+
{ id: "distort", label: "Distort", title: "Each corner moves freely" },
|
|
15120
|
+
{ id: "perspective", label: "Perspective", title: "A corner and its partner move symmetrically" }
|
|
15121
|
+
];
|
|
15122
|
+
var FREE_TRANSFORM_FIELDS = [
|
|
15123
|
+
{ id: "x", label: "X", unit: "px", title: "Reference point X (document pixels)" },
|
|
15124
|
+
{ id: "y", label: "Y", unit: "px", title: "Reference point Y (document pixels)" },
|
|
15125
|
+
{ id: "scaleX", label: "W", unit: "%", title: "Width in percent of the original" },
|
|
15126
|
+
{ id: "scaleY", label: "H", unit: "%", title: "Height in percent of the original" },
|
|
15127
|
+
{ id: "rotate", label: "\u2220", unit: "\xB0", title: "Rotation in degrees, clockwise" },
|
|
15128
|
+
{ id: "skewX", label: "H skew", unit: "\xB0", title: "Horizontal skew in degrees" },
|
|
15129
|
+
{ id: "skewY", label: "V skew", unit: "\xB0", title: "Vertical skew in degrees" }
|
|
15130
|
+
];
|
|
15131
|
+
var CURSOR = {
|
|
15132
|
+
nw: "nwse-resize",
|
|
15133
|
+
se: "nwse-resize",
|
|
15134
|
+
ne: "nesw-resize",
|
|
15135
|
+
sw: "nesw-resize",
|
|
15136
|
+
n: "ns-resize",
|
|
15137
|
+
s: "ns-resize",
|
|
15138
|
+
e: "ew-resize",
|
|
15139
|
+
w: "ew-resize",
|
|
15140
|
+
reference: "move",
|
|
15141
|
+
inside: "move",
|
|
15142
|
+
outside: "alias"
|
|
15143
|
+
};
|
|
15144
|
+
var mod = (m) => Boolean(m.ctrlKey || m.metaKey);
|
|
15145
|
+
var add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
|
|
15146
|
+
var sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
|
|
15147
|
+
function rotateAbout(p, c, rad) {
|
|
15148
|
+
const cos = Math.cos(rad);
|
|
15149
|
+
const sin = Math.sin(rad);
|
|
15150
|
+
const d = sub(p, c);
|
|
15151
|
+
return { x: c.x + d.x * cos - d.y * sin, y: c.y + d.x * sin + d.y * cos };
|
|
15152
|
+
}
|
|
15153
|
+
function insideQuad(q2, p) {
|
|
15154
|
+
let sign = 0;
|
|
15155
|
+
for (let i = 0; i < 4; i++) {
|
|
15156
|
+
const a = q2[i];
|
|
15157
|
+
const b = q2[(i + 1) % 4];
|
|
15158
|
+
const c = (b.x - a.x) * (p.y - a.y) - (b.y - a.y) * (p.x - a.x);
|
|
15159
|
+
if (Math.abs(c) < 1e-12) continue;
|
|
15160
|
+
const s = Math.sign(c);
|
|
15161
|
+
if (sign && s !== sign) return false;
|
|
15162
|
+
sign = s;
|
|
15163
|
+
}
|
|
15164
|
+
return true;
|
|
15165
|
+
}
|
|
15166
|
+
var FreeTransformTool = class {
|
|
15167
|
+
constructor(controller, options = {}) {
|
|
15168
|
+
this.controller = controller;
|
|
15169
|
+
this.options = options;
|
|
15170
|
+
}
|
|
15171
|
+
controller;
|
|
15172
|
+
options;
|
|
15173
|
+
sessionRef = null;
|
|
15174
|
+
modeValue = "scale";
|
|
15175
|
+
/** The reference point in the untransformed frame's space (follows the transform). */
|
|
15176
|
+
refBox = { x: 0, y: 0 };
|
|
15177
|
+
corners = null;
|
|
15178
|
+
drag = null;
|
|
15179
|
+
pending = false;
|
|
15180
|
+
viewScale = 1;
|
|
15181
|
+
viewClip;
|
|
15182
|
+
/** Values typed into the options bar since the last drag (V skew is not recovered from the shape). */
|
|
15183
|
+
typed = {};
|
|
15184
|
+
/** Width and height scale together when typed (the chain icon). */
|
|
15185
|
+
proportional = true;
|
|
15186
|
+
error = null;
|
|
15187
|
+
lastResult = null;
|
|
15188
|
+
get active() {
|
|
15189
|
+
return Boolean(this.sessionRef?.isOpen);
|
|
15190
|
+
}
|
|
15191
|
+
get session() {
|
|
15192
|
+
return this.active ? this.sessionRef : null;
|
|
15193
|
+
}
|
|
15194
|
+
get mode() {
|
|
15195
|
+
return this.modeValue;
|
|
15196
|
+
}
|
|
15197
|
+
get interpolation() {
|
|
15198
|
+
return this.sessionRef?.interpolation ?? "bicubic";
|
|
15199
|
+
}
|
|
15200
|
+
listeners = /* @__PURE__ */ new Set();
|
|
15201
|
+
/** Calls `listener` whenever the overlay, readout, mode or error changes. Returns the unsubscribe. */
|
|
15202
|
+
subscribe(listener) {
|
|
15203
|
+
this.listeners.add(listener);
|
|
15204
|
+
return () => {
|
|
15205
|
+
this.listeners.delete(listener);
|
|
15206
|
+
};
|
|
15207
|
+
}
|
|
15208
|
+
changed() {
|
|
15209
|
+
this.options.onChange?.();
|
|
15210
|
+
for (const l of this.listeners) l();
|
|
15211
|
+
}
|
|
15212
|
+
/** Opens a Free Transform (default: the selected pixels of the active layer, or the whole layer). Returns an error message or `null`. */
|
|
15213
|
+
begin(init = {}) {
|
|
15214
|
+
if (this.active) return null;
|
|
15215
|
+
try {
|
|
15216
|
+
if (!init.layerId && init.content !== "selection-outline" && !this.controller.getState().activeLayerId) {
|
|
15217
|
+
const top = [...flattenLayers(this.controller.getState().layers)].reverse().find((l) => l.type === "pixel" && l.visible);
|
|
15218
|
+
if (top) this.controller.setActiveLayer(top.id);
|
|
15219
|
+
}
|
|
15220
|
+
const s = this.controller.beginFreeTransform(init);
|
|
15221
|
+
this.sessionRef = s;
|
|
15222
|
+
this.corners = s.geometry.corners;
|
|
15223
|
+
this.refBox = { x: s.box.x + s.box.width / 2, y: s.box.y + s.box.height / 2 };
|
|
15224
|
+
this.modeValue = "scale";
|
|
15225
|
+
this.typed = {};
|
|
15226
|
+
this.error = null;
|
|
15227
|
+
} catch (e) {
|
|
15228
|
+
this.error = e instanceof Error ? e.message : String(e);
|
|
15229
|
+
}
|
|
15230
|
+
this.changed();
|
|
15231
|
+
return this.error;
|
|
15232
|
+
}
|
|
15233
|
+
setMode(mode) {
|
|
15234
|
+
this.modeValue = mode;
|
|
15235
|
+
this.changed();
|
|
15236
|
+
}
|
|
15237
|
+
/** Sets the reference point to an anchor of the frame (the options bar's locator). */
|
|
15238
|
+
setReference(anchor) {
|
|
15239
|
+
const s = this.session;
|
|
15240
|
+
if (!s) return;
|
|
15241
|
+
const [fx, fy] = ANCHOR_FRACTION2[anchor];
|
|
15242
|
+
this.refBox = { x: s.box.x + fx * s.box.width, y: s.box.y + fy * s.box.height };
|
|
15243
|
+
this.changed();
|
|
15244
|
+
}
|
|
15245
|
+
setInterpolation(interpolation) {
|
|
15246
|
+
const s = this.session;
|
|
15247
|
+
if (!s) return;
|
|
15248
|
+
this.run(() => s.update({ interpolation }, false));
|
|
15249
|
+
}
|
|
15250
|
+
/** Screen pixels per document pixel and the visible document area, for previews. */
|
|
15251
|
+
setView(scale, clip) {
|
|
15252
|
+
this.viewScale = scale > 0 ? scale : 1;
|
|
15253
|
+
this.viewClip = clip;
|
|
15254
|
+
}
|
|
15255
|
+
homography() {
|
|
15256
|
+
const s = this.session;
|
|
15257
|
+
return s && this.corners ? rectToQuad(s.box, this.corners) : null;
|
|
15258
|
+
}
|
|
15259
|
+
/** The reference point in document pixels. */
|
|
15260
|
+
referencePoint() {
|
|
15261
|
+
const h = this.homography();
|
|
15262
|
+
return h ? applyHomography(h, this.refBox) : null;
|
|
15263
|
+
}
|
|
15264
|
+
/** The outline, handles and reference point to draw, or `null` when no transform is open. */
|
|
15265
|
+
overlay() {
|
|
15266
|
+
const s = this.session;
|
|
15267
|
+
const h = this.homography();
|
|
15268
|
+
if (!s || !h || !this.corners) return null;
|
|
15269
|
+
const handles = [...CORNERS, ...SIDES].map((id) => {
|
|
15270
|
+
const [fx, fy] = HANDLE_FRACTION[id];
|
|
15271
|
+
const p = applyHomography(h, { x: s.box.x + fx * s.box.width, y: s.box.y + fy * s.box.height }) ?? { x: 0, y: 0 };
|
|
15272
|
+
return { id, x: p.x, y: p.y };
|
|
15273
|
+
});
|
|
15274
|
+
return { outline: this.corners.map((p) => ({ x: p.x, y: p.y })), handles, reference: this.referencePoint() ?? { x: 0, y: 0 } };
|
|
15275
|
+
}
|
|
15276
|
+
/** What a document point is over (`tolerance` in document pixels: a few screen pixels / scale). */
|
|
15277
|
+
hit(p, tolerance) {
|
|
15278
|
+
const o = this.overlay();
|
|
15279
|
+
if (!o) return null;
|
|
15280
|
+
let best = null;
|
|
15281
|
+
let bestD = tolerance;
|
|
15282
|
+
for (const h of o.handles) {
|
|
15283
|
+
const d = Math.hypot(h.x - p.x, h.y - p.y);
|
|
15284
|
+
if (d <= bestD) {
|
|
15285
|
+
best = h.id;
|
|
15286
|
+
bestD = d;
|
|
15287
|
+
}
|
|
15288
|
+
}
|
|
15289
|
+
if (Math.hypot(o.reference.x - p.x, o.reference.y - p.y) < bestD) best = "reference";
|
|
15290
|
+
if (best) return best;
|
|
15291
|
+
return insideQuad(o.outline, p) ? "inside" : "outside";
|
|
15292
|
+
}
|
|
15293
|
+
/** The CSS cursor for a handle. */
|
|
15294
|
+
cursor(handle) {
|
|
15295
|
+
if (!handle) return "default";
|
|
15296
|
+
if (this.modeValue === "rotate" && handle !== "inside" && handle !== "reference") return CURSOR.outside;
|
|
15297
|
+
return CURSOR[handle];
|
|
15298
|
+
}
|
|
15299
|
+
/** Starts a drag on what `p` is over. */
|
|
15300
|
+
down(p, tolerance) {
|
|
15301
|
+
if (!this.session || !this.corners) return null;
|
|
15302
|
+
const handle = this.hit(p, tolerance);
|
|
15303
|
+
if (!handle) return null;
|
|
15304
|
+
this.drag = { handle, start: { x: p.x, y: p.y }, corners: this.corners, refBox: { ...this.refBox }, ref: this.referencePoint() ?? p };
|
|
15305
|
+
return handle;
|
|
15306
|
+
}
|
|
15307
|
+
/** Continues the drag to `p` with the modifiers held now. */
|
|
15308
|
+
move(p, m = {}) {
|
|
15309
|
+
const d = this.drag;
|
|
15310
|
+
const s = this.session;
|
|
15311
|
+
if (!d || !s) return;
|
|
15312
|
+
if (d.handle === "reference") {
|
|
15313
|
+
const h = rectToQuad(s.box, d.corners);
|
|
15314
|
+
const inv = h && invertHomography(h);
|
|
15315
|
+
const q2 = inv && applyHomography(inv, p);
|
|
15316
|
+
if (q2) this.refBox = q2;
|
|
15317
|
+
this.changed();
|
|
15318
|
+
return;
|
|
15319
|
+
}
|
|
15320
|
+
const corners = this.dragCorners(d, p, m);
|
|
15321
|
+
if (corners) this.setCorners(corners);
|
|
15322
|
+
}
|
|
15323
|
+
/** Ends the drag (the transform stays open). */
|
|
15324
|
+
up() {
|
|
15325
|
+
this.drag = null;
|
|
15326
|
+
}
|
|
15327
|
+
dragCorners(d, p, m) {
|
|
15328
|
+
const s = this.session;
|
|
15329
|
+
const c0 = d.corners;
|
|
15330
|
+
let delta = sub(p, d.start);
|
|
15331
|
+
const map = (f) => c0.map(f);
|
|
15332
|
+
const handle = d.handle;
|
|
15333
|
+
const isCorner = handle in CORNER_INDEX;
|
|
15334
|
+
const isSide = handle in SIDE_CORNERS;
|
|
15335
|
+
const rotating = handle === "outside" || this.modeValue === "rotate" && (isCorner || isSide);
|
|
15336
|
+
if (handle === "inside") {
|
|
15337
|
+
if (m.shiftKey) delta = Math.abs(delta.x) >= Math.abs(delta.y) ? { x: delta.x, y: 0 } : { x: 0, y: delta.y };
|
|
15338
|
+
return map((q3) => add(q3, delta));
|
|
15339
|
+
}
|
|
15340
|
+
if (rotating) {
|
|
15341
|
+
const r = d.ref;
|
|
15342
|
+
let a = Math.atan2(p.y - r.y, p.x - r.x) - Math.atan2(d.start.y - r.y, d.start.x - r.x);
|
|
15343
|
+
if (m.shiftKey) a = Math.round(a / (Math.PI / 12)) * (Math.PI / 12);
|
|
15344
|
+
return map((q3) => rotateAbout(q3, r, a));
|
|
15345
|
+
}
|
|
15346
|
+
const perspective = this.modeValue === "perspective" || mod(m) && m.altKey && m.shiftKey;
|
|
15347
|
+
const distort = this.modeValue === "distort" || mod(m) && !perspective;
|
|
15348
|
+
const skew = this.modeValue === "skew" || mod(m) && isSide;
|
|
15349
|
+
if (isSide && (skew || perspective)) {
|
|
15350
|
+
const [i, j] = SIDE_CORNERS[handle];
|
|
15351
|
+
const dir = sub(c0[j], c0[i]);
|
|
15352
|
+
const len = Math.hypot(dir.x, dir.y) || 1;
|
|
15353
|
+
const t = (delta.x * dir.x + delta.y * dir.y) / len;
|
|
15354
|
+
const v = { x: dir.x / len * t, y: dir.y / len * t };
|
|
15355
|
+
const [oi, oj] = SIDE_CORNERS[{ n: "s", s: "n", e: "w", w: "e" }[handle]];
|
|
15356
|
+
return map((q3, k) => k === i || k === j ? add(q3, v) : m.altKey && (k === oi || k === oj) ? sub(q3, v) : q3);
|
|
15357
|
+
}
|
|
15358
|
+
if (isCorner && perspective) {
|
|
15359
|
+
const k = CORNER_INDEX[handle];
|
|
15360
|
+
if (Math.abs(delta.x) >= Math.abs(delta.y)) {
|
|
15361
|
+
const partner2 = [1, 0, 3, 2][k];
|
|
15362
|
+
return map((q3, i) => i === k ? { x: q3.x + delta.x, y: q3.y } : i === partner2 ? { x: q3.x - delta.x, y: q3.y } : q3);
|
|
15363
|
+
}
|
|
15364
|
+
const partner = [3, 2, 1, 0][k];
|
|
15365
|
+
return map((q3, i) => i === k ? { x: q3.x, y: q3.y + delta.y } : i === partner ? { x: q3.x, y: q3.y - delta.y } : q3);
|
|
15366
|
+
}
|
|
15367
|
+
if (distort || isCorner && this.modeValue === "skew") {
|
|
15368
|
+
if (isCorner) return map((q3, i2) => i2 === CORNER_INDEX[handle] ? add(q3, delta) : q3);
|
|
15369
|
+
const [i, j] = SIDE_CORNERS[handle];
|
|
15370
|
+
return map((q3, k) => k === i || k === j ? add(q3, delta) : q3);
|
|
15371
|
+
}
|
|
15372
|
+
const h = rectToQuad(s.box, c0);
|
|
15373
|
+
const inv = h && invertHomography(h);
|
|
15374
|
+
if (!h || !inv) return null;
|
|
15375
|
+
const q2 = applyHomography(inv, p);
|
|
15376
|
+
const q0 = applyHomography(inv, d.start);
|
|
15377
|
+
if (!q2 || !q0) return null;
|
|
15378
|
+
const b = s.box;
|
|
15379
|
+
const [fx, fy] = HANDLE_FRACTION[handle];
|
|
15380
|
+
const grab = { x: b.x + fx * b.width, y: b.y + fy * b.height };
|
|
15381
|
+
const anchor = m.altKey ? d.refBox : { x: b.x + (1 - fx) * b.width, y: b.y + (1 - fy) * b.height };
|
|
15382
|
+
const target = add(grab, sub(q2, q0));
|
|
15383
|
+
const ax = grab.x - anchor.x;
|
|
15384
|
+
const ay = grab.y - anchor.y;
|
|
15385
|
+
let sx = isCorner || handle === "e" || handle === "w" ? Math.abs(ax) > 1e-9 ? (target.x - anchor.x) / ax : 1 : 1;
|
|
15386
|
+
let sy = isCorner || handle === "n" || handle === "s" ? Math.abs(ay) > 1e-9 ? (target.y - anchor.y) / ay : 1 : 1;
|
|
15387
|
+
if (isCorner && !m.shiftKey) {
|
|
15388
|
+
const k = Math.abs(sx) >= Math.abs(sy) ? sx : sy;
|
|
15389
|
+
sx = k;
|
|
15390
|
+
sy = k;
|
|
15391
|
+
}
|
|
15392
|
+
const scaled = [
|
|
15393
|
+
{ x: b.x, y: b.y },
|
|
15394
|
+
{ x: b.x + b.width, y: b.y },
|
|
15395
|
+
{ x: b.x + b.width, y: b.y + b.height },
|
|
15396
|
+
{ x: b.x, y: b.y + b.height }
|
|
15397
|
+
].map((c) => applyHomography(h, { x: anchor.x + (c.x - anchor.x) * sx, y: anchor.y + (c.y - anchor.y) * sy }));
|
|
15398
|
+
return scaled.every(Boolean) ? scaled : null;
|
|
15399
|
+
}
|
|
15400
|
+
/** Sets new corners and refreshes the preview (scheduled). Invalid shapes keep the last good one. */
|
|
15401
|
+
setCorners(corners) {
|
|
15402
|
+
const s = this.session;
|
|
15403
|
+
if (!s) return;
|
|
15404
|
+
const h = rectToQuad(s.box, corners);
|
|
15405
|
+
if (!h) return;
|
|
15406
|
+
this.corners = corners;
|
|
15407
|
+
this.typed = {};
|
|
15408
|
+
this.changed();
|
|
15409
|
+
this.preview();
|
|
15410
|
+
}
|
|
15411
|
+
preview() {
|
|
15412
|
+
if (this.pending) return;
|
|
15413
|
+
this.pending = true;
|
|
15414
|
+
const run = () => {
|
|
15415
|
+
this.pending = false;
|
|
15416
|
+
const s = this.session;
|
|
15417
|
+
if (!s || !this.corners) return;
|
|
15418
|
+
this.run(() => s.update({ corners: this.corners }, { scale: this.viewScale, clip: this.viewClip }));
|
|
15419
|
+
};
|
|
15420
|
+
if (this.options.schedule) this.options.schedule(run);
|
|
15421
|
+
else run();
|
|
15422
|
+
}
|
|
15423
|
+
run(fn) {
|
|
15424
|
+
try {
|
|
15425
|
+
fn();
|
|
15426
|
+
this.error = null;
|
|
15427
|
+
} catch (e) {
|
|
15428
|
+
this.error = e instanceof Error ? e.message : String(e);
|
|
15429
|
+
if (this.sessionRef && !this.sessionRef.isOpen) this.sessionRef = null;
|
|
15430
|
+
}
|
|
15431
|
+
this.changed();
|
|
15432
|
+
}
|
|
15433
|
+
/** The options bar's numbers for the current shape. */
|
|
15434
|
+
bar() {
|
|
15435
|
+
const s = this.session;
|
|
15436
|
+
if (!s || !this.corners) return null;
|
|
15437
|
+
const read = describeTransform({ box: s.box, corners: this.corners }, this.refBox);
|
|
15438
|
+
return {
|
|
15439
|
+
affine: read.affine,
|
|
15440
|
+
x: this.typed.x ?? read.x,
|
|
15441
|
+
y: this.typed.y ?? read.y,
|
|
15442
|
+
scaleX: this.typed.scaleX ?? read.scaleX,
|
|
15443
|
+
scaleY: this.typed.scaleY ?? read.scaleY,
|
|
15444
|
+
rotate: this.typed.rotate ?? read.rotate,
|
|
15445
|
+
skewX: this.typed.skewX ?? read.skewX,
|
|
15446
|
+
skewY: this.typed.skewY ?? 0
|
|
15447
|
+
};
|
|
15448
|
+
}
|
|
15449
|
+
/** Applies a typed options-bar value (W and H together while `proportional`). */
|
|
15450
|
+
setField(field, value) {
|
|
15451
|
+
const s = this.session;
|
|
15452
|
+
const cur = this.bar();
|
|
15453
|
+
if (!s || !cur || !Number.isFinite(value)) return;
|
|
15454
|
+
const next = { ...cur, [field]: value };
|
|
15455
|
+
if (this.proportional && field === "scaleX") next.scaleY = cur.scaleY === 0 ? value : value / cur.scaleX * cur.scaleY;
|
|
15456
|
+
if (this.proportional && field === "scaleY") next.scaleX = cur.scaleX === 0 ? value : value / cur.scaleY * cur.scaleX;
|
|
15457
|
+
if (!next.scaleX || !next.scaleY) {
|
|
15458
|
+
this.error = "Width and height must not be 0 %.";
|
|
15459
|
+
this.changed();
|
|
15460
|
+
return;
|
|
15461
|
+
}
|
|
15462
|
+
const m = matrixFromParams(s.box, {
|
|
15463
|
+
scaleX: next.scaleX,
|
|
15464
|
+
scaleY: next.scaleY,
|
|
15465
|
+
rotate: next.rotate,
|
|
15466
|
+
skewX: next.skewX,
|
|
15467
|
+
skewY: next.skewY,
|
|
15468
|
+
reference: this.refBox,
|
|
15469
|
+
dx: next.x - this.refBox.x,
|
|
15470
|
+
dy: next.y - this.refBox.y
|
|
15471
|
+
});
|
|
15472
|
+
this.setCorners(cornersFromMatrix(s.box, m));
|
|
15473
|
+
this.typed = { scaleX: next.scaleX, scaleY: next.scaleY, rotate: next.rotate, skewX: next.skewX, skewY: next.skewY, x: next.x, y: next.y };
|
|
15474
|
+
this.changed();
|
|
15475
|
+
}
|
|
15476
|
+
/** Finishes with full-quality resampling (one undo step). */
|
|
15477
|
+
commit() {
|
|
15478
|
+
const s = this.session;
|
|
15479
|
+
if (!s) return null;
|
|
15480
|
+
let res = null;
|
|
15481
|
+
this.pending = false;
|
|
15482
|
+
this.run(() => {
|
|
15483
|
+
if (this.corners) s.update({ corners: this.corners }, false);
|
|
15484
|
+
res = s.commit();
|
|
15485
|
+
});
|
|
15486
|
+
this.lastResult = res;
|
|
15487
|
+
this.sessionRef = null;
|
|
15488
|
+
this.corners = null;
|
|
15489
|
+
this.drag = null;
|
|
15490
|
+
this.changed();
|
|
15491
|
+
return res;
|
|
15492
|
+
}
|
|
15493
|
+
/** Abandons the transform and restores everything. */
|
|
15494
|
+
cancel() {
|
|
15495
|
+
this.sessionRef?.cancel();
|
|
15496
|
+
this.sessionRef = null;
|
|
15497
|
+
this.corners = null;
|
|
15498
|
+
this.drag = null;
|
|
15499
|
+
this.pending = false;
|
|
15500
|
+
this.error = null;
|
|
15501
|
+
this.changed();
|
|
15502
|
+
}
|
|
15503
|
+
/**
|
|
15504
|
+
* Keyboard: Enter commits, Escape cancels, arrows nudge by 1 px (Shift: 10). Ctrl/Cmd+T opens a
|
|
15505
|
+
* Free Transform when none is open. Returns true when the key was used (prevent its default).
|
|
15506
|
+
*/
|
|
15507
|
+
key(e) {
|
|
15508
|
+
const t = e.target;
|
|
15509
|
+
const typing = Boolean(t && (t.isContentEditable || t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.tagName === "SELECT"));
|
|
15510
|
+
if (!this.active) {
|
|
15511
|
+
if ((e.ctrlKey || e.metaKey) && !e.altKey && e.key.toLowerCase() === "t" && !typing) {
|
|
15512
|
+
this.begin();
|
|
15513
|
+
return true;
|
|
15514
|
+
}
|
|
15515
|
+
return false;
|
|
15516
|
+
}
|
|
15517
|
+
if (e.key === "Enter") {
|
|
15518
|
+
this.commit();
|
|
15519
|
+
return true;
|
|
15520
|
+
}
|
|
15521
|
+
if (e.key === "Escape") {
|
|
15522
|
+
this.cancel();
|
|
15523
|
+
return true;
|
|
15524
|
+
}
|
|
15525
|
+
if (typing) return false;
|
|
15526
|
+
const step = e.shiftKey ? 10 : 1;
|
|
15527
|
+
const nudge = { ArrowLeft: { x: -step, y: 0 }, ArrowRight: { x: step, y: 0 }, ArrowUp: { x: 0, y: -step }, ArrowDown: { x: 0, y: step } };
|
|
15528
|
+
const v = nudge[e.key];
|
|
15529
|
+
if (v && this.corners) {
|
|
15530
|
+
this.setCorners(this.corners.map((q2) => add(q2, v)));
|
|
15531
|
+
return true;
|
|
15532
|
+
}
|
|
15533
|
+
return false;
|
|
15534
|
+
}
|
|
15535
|
+
};
|
|
14127
15536
|
export {
|
|
14128
15537
|
ADJUSTMENT_KEYS,
|
|
14129
15538
|
ADJUSTMENT_KINDS,
|
|
@@ -14152,6 +15561,12 @@ export {
|
|
|
14152
15561
|
DabWalker,
|
|
14153
15562
|
EMPTY_DISTANCE,
|
|
14154
15563
|
EMPTY_MIXER_LOAD,
|
|
15564
|
+
FREE_TRANSFORM_CONTENTS,
|
|
15565
|
+
FREE_TRANSFORM_FIELDS,
|
|
15566
|
+
FREE_TRANSFORM_MODES,
|
|
15567
|
+
FREE_TRANSFORM_MODE_OPTIONS,
|
|
15568
|
+
FreeTransformSession,
|
|
15569
|
+
FreeTransformTool,
|
|
14155
15570
|
GUIDED_FILTER_EPS,
|
|
14156
15571
|
HISTOGRAM_BINS,
|
|
14157
15572
|
HUE_RANGE_LABELS,
|
|
@@ -14159,6 +15574,7 @@ export {
|
|
|
14159
15574
|
IMAGE_ADJUSTMENT_CONTROLS,
|
|
14160
15575
|
IMAGE_EDITOR_SCHEMA_VERSION,
|
|
14161
15576
|
IMAGE_EDITOR_STATE_VERSION,
|
|
15577
|
+
INTERPOLATIONS,
|
|
14162
15578
|
ImageEditorController,
|
|
14163
15579
|
ImageEditorRenderer,
|
|
14164
15580
|
LATEST_PROCESS_VERSION,
|
|
@@ -14173,6 +15589,7 @@ export {
|
|
|
14173
15589
|
MAX_BRUSH_SIZE,
|
|
14174
15590
|
MAX_EFFECT_REACH,
|
|
14175
15591
|
MAX_KELVIN,
|
|
15592
|
+
MAX_TRANSFORM_PIXELS,
|
|
14176
15593
|
MIN_KELVIN,
|
|
14177
15594
|
MIRED_PER_TEMPERATURE_UNIT,
|
|
14178
15595
|
MIXER_DRAIN,
|
|
@@ -14208,8 +15625,12 @@ export {
|
|
|
14208
15625
|
TINT_SCALE,
|
|
14209
15626
|
TONE_LUT_SIZE,
|
|
14210
15627
|
TONE_RANGE_PRESETS,
|
|
15628
|
+
TRANSFORM_ANCHORS,
|
|
14211
15629
|
TiledSurface,
|
|
15630
|
+
TransformTooLargeError,
|
|
14212
15631
|
VIBRANCE_GREY,
|
|
15632
|
+
WARP_STYLES,
|
|
15633
|
+
WARP_STYLE_LABELS,
|
|
14213
15634
|
XYZ_D50_TO_SRGB,
|
|
14214
15635
|
ZlibError,
|
|
14215
15636
|
activeAdjustmentLayers,
|
|
@@ -14221,16 +15642,19 @@ export {
|
|
|
14221
15642
|
adjustmentNodeAsLayer,
|
|
14222
15643
|
adjustmentNodeFromLegacy,
|
|
14223
15644
|
adler32,
|
|
15645
|
+
anchorPoint,
|
|
14224
15646
|
annotationLayerFor,
|
|
14225
15647
|
annotationLayerName,
|
|
14226
15648
|
applyAdjustmentLayers,
|
|
14227
15649
|
applyAdjustments,
|
|
14228
15650
|
applyBoxBlur,
|
|
14229
15651
|
applyDepthMask,
|
|
15652
|
+
applyHomography,
|
|
14230
15653
|
applyImageProjectionToContext,
|
|
14231
15654
|
applyLegacyAdjustments,
|
|
14232
15655
|
applyMatrix3,
|
|
14233
15656
|
applyOutputStage,
|
|
15657
|
+
applyParams,
|
|
14234
15658
|
applyProcess2Adjustments,
|
|
14235
15659
|
applyRegionBlur,
|
|
14236
15660
|
applyRegionMosaic,
|
|
@@ -14248,6 +15672,8 @@ export {
|
|
|
14248
15672
|
blurFilter,
|
|
14249
15673
|
borderCoverage,
|
|
14250
15674
|
boxMean,
|
|
15675
|
+
boxPointOf,
|
|
15676
|
+
boxQuad,
|
|
14251
15677
|
boxToCurvePoint,
|
|
14252
15678
|
bradfordAdaptation,
|
|
14253
15679
|
buildLayerMaskPlanes,
|
|
@@ -14267,12 +15693,14 @@ export {
|
|
|
14267
15693
|
colorRange,
|
|
14268
15694
|
combineSelections,
|
|
14269
15695
|
compileAdjustmentLayer,
|
|
15696
|
+
compositeSurfaceOver,
|
|
14270
15697
|
computeHistogram,
|
|
14271
15698
|
computeSelectionBounds,
|
|
14272
15699
|
contentBounds,
|
|
14273
15700
|
contractCoverage,
|
|
14274
15701
|
controlPosition,
|
|
14275
15702
|
controlTrackBackground,
|
|
15703
|
+
cornersFromMatrix,
|
|
14276
15704
|
coverageTileCoord,
|
|
14277
15705
|
coverageToByte,
|
|
14278
15706
|
coveringRect,
|
|
@@ -14285,6 +15713,7 @@ export {
|
|
|
14285
15713
|
createImagePaintTools,
|
|
14286
15714
|
createImagePsdTools,
|
|
14287
15715
|
createImageSelectionTools,
|
|
15716
|
+
createImageTransformTools,
|
|
14288
15717
|
createPigment,
|
|
14289
15718
|
createPigmentTools,
|
|
14290
15719
|
createSelectionMask,
|
|
@@ -14311,6 +15740,7 @@ export {
|
|
|
14311
15740
|
describeLayer,
|
|
14312
15741
|
describeLayerEffects,
|
|
14313
15742
|
describeSelection,
|
|
15743
|
+
describeTransform,
|
|
14314
15744
|
deserializeEffects,
|
|
14315
15745
|
deserializeLayer,
|
|
14316
15746
|
deserializeLayers,
|
|
@@ -14321,6 +15751,7 @@ export {
|
|
|
14321
15751
|
developTarget,
|
|
14322
15752
|
distanceTransform,
|
|
14323
15753
|
downloadPsd,
|
|
15754
|
+
dragWarpMeshPoint,
|
|
14324
15755
|
dropPlacement,
|
|
14325
15756
|
dropZone,
|
|
14326
15757
|
duplicateLayer,
|
|
@@ -14339,6 +15770,7 @@ export {
|
|
|
14339
15770
|
encodeRle,
|
|
14340
15771
|
encodeSelectionRle,
|
|
14341
15772
|
encodeSurface,
|
|
15773
|
+
evalWarpMesh,
|
|
14342
15774
|
evaluateAdjustmentLayers,
|
|
14343
15775
|
expandCoverage,
|
|
14344
15776
|
exportPsd,
|
|
@@ -14349,9 +15781,11 @@ export {
|
|
|
14349
15781
|
findLayerControl,
|
|
14350
15782
|
finishTile,
|
|
14351
15783
|
flattenLayers,
|
|
15784
|
+
flipCorners,
|
|
14352
15785
|
formatControlValue,
|
|
14353
15786
|
fullSelection,
|
|
14354
15787
|
gaussianBlur,
|
|
15788
|
+
geometryHomography,
|
|
14355
15789
|
getAnnotationBounds,
|
|
14356
15790
|
getControlPath,
|
|
14357
15791
|
getCropCursor,
|
|
@@ -14366,6 +15800,7 @@ export {
|
|
|
14366
15800
|
handlePaintShortcut,
|
|
14367
15801
|
hasActiveEffects,
|
|
14368
15802
|
hasResampledLegacyMask,
|
|
15803
|
+
hasWarp,
|
|
14369
15804
|
hashMaskContent,
|
|
14370
15805
|
hashNoise,
|
|
14371
15806
|
hexToRgb,
|
|
@@ -14373,6 +15808,8 @@ export {
|
|
|
14373
15808
|
hitTestAnnotation,
|
|
14374
15809
|
hitTestCrop,
|
|
14375
15810
|
hitTestCurvePoint,
|
|
15811
|
+
homographyFromAffine,
|
|
15812
|
+
homographyFromPoints,
|
|
14376
15813
|
hslToRgb,
|
|
14377
15814
|
hueRange,
|
|
14378
15815
|
hueRangeTables,
|
|
@@ -14380,6 +15817,8 @@ export {
|
|
|
14380
15817
|
hueSaturation,
|
|
14381
15818
|
identityCurve,
|
|
14382
15819
|
identityLevels,
|
|
15820
|
+
identityTransform,
|
|
15821
|
+
identityWarpMesh,
|
|
14383
15822
|
imageEditorStateMigrations,
|
|
14384
15823
|
imageToCanvasPoint,
|
|
14385
15824
|
importPsd,
|
|
@@ -14387,12 +15826,15 @@ export {
|
|
|
14387
15826
|
inflateRaw,
|
|
14388
15827
|
insertCurvePoint,
|
|
14389
15828
|
insertLayer,
|
|
15829
|
+
invertHomography,
|
|
14390
15830
|
invertMatrix3,
|
|
14391
15831
|
invertSelection,
|
|
14392
15832
|
isActiveFilter,
|
|
14393
15833
|
isAdjustmentKind,
|
|
14394
15834
|
isBlendMode,
|
|
14395
15835
|
isDefaultDepthMask,
|
|
15836
|
+
isIdentityTransform,
|
|
15837
|
+
isInterpolation,
|
|
14396
15838
|
isLayerEffectKind,
|
|
14397
15839
|
isLayerLocked,
|
|
14398
15840
|
isLegacyShape,
|
|
@@ -14404,7 +15846,10 @@ export {
|
|
|
14404
15846
|
isSelectionMode,
|
|
14405
15847
|
isSeparableBlendMode,
|
|
14406
15848
|
isSpatialFilter,
|
|
15849
|
+
isTransformAnchor,
|
|
15850
|
+
isValidWarp,
|
|
14407
15851
|
kelvinToTemperature,
|
|
15852
|
+
keysWeight,
|
|
14408
15853
|
layerBadge,
|
|
14409
15854
|
layerEffectControls,
|
|
14410
15855
|
layerEffectFields,
|
|
@@ -14436,6 +15881,8 @@ export {
|
|
|
14436
15881
|
mapCropHandle,
|
|
14437
15882
|
mapLayer,
|
|
14438
15883
|
maskSummary,
|
|
15884
|
+
matrixFromCorners,
|
|
15885
|
+
matrixFromParams,
|
|
14439
15886
|
mergeLayerEffect,
|
|
14440
15887
|
migrateSerializedImageEditorState,
|
|
14441
15888
|
migrateToLayeredState,
|
|
@@ -14486,6 +15933,8 @@ export {
|
|
|
14486
15933
|
polygonCoverage,
|
|
14487
15934
|
polygonSelection,
|
|
14488
15935
|
posterizeValue,
|
|
15936
|
+
presetWarp,
|
|
15937
|
+
previewStep,
|
|
14489
15938
|
projectImagePoint,
|
|
14490
15939
|
projectImageRect,
|
|
14491
15940
|
projectMaskCoverage,
|
|
@@ -14494,6 +15943,7 @@ export {
|
|
|
14494
15943
|
psdFileName,
|
|
14495
15944
|
psdPatternToEffectPattern,
|
|
14496
15945
|
psdToLayers,
|
|
15946
|
+
pullWarpMesh,
|
|
14497
15947
|
quickMaskPixels,
|
|
14498
15948
|
rangeLightness,
|
|
14499
15949
|
rasterMaskFromLegacy,
|
|
@@ -14503,6 +15953,9 @@ export {
|
|
|
14503
15953
|
readPsd,
|
|
14504
15954
|
readRegion,
|
|
14505
15955
|
rectSelection,
|
|
15956
|
+
rectToQuad,
|
|
15957
|
+
reduceSurface,
|
|
15958
|
+
referencePoint,
|
|
14506
15959
|
refineCoverage,
|
|
14507
15960
|
refineEdgeHalo,
|
|
14508
15961
|
refineSelection,
|
|
@@ -14518,6 +15971,7 @@ export {
|
|
|
14518
15971
|
requireUnlocked,
|
|
14519
15972
|
resizePixels,
|
|
14520
15973
|
resizeSelection,
|
|
15974
|
+
resizeWarpMesh,
|
|
14521
15975
|
resolveExternalAssets,
|
|
14522
15976
|
resolveMosaicRegionBounds,
|
|
14523
15977
|
rgbToGray,
|
|
@@ -14573,6 +16027,12 @@ export {
|
|
|
14573
16027
|
toneCurve,
|
|
14574
16028
|
toneLuts,
|
|
14575
16029
|
toneRange,
|
|
16030
|
+
transformHomography,
|
|
16031
|
+
transformMap,
|
|
16032
|
+
transformSurface,
|
|
16033
|
+
transformedBounds,
|
|
16034
|
+
transformedBox,
|
|
16035
|
+
transformedPoint,
|
|
14576
16036
|
translateSurface,
|
|
14577
16037
|
ungroupLayer,
|
|
14578
16038
|
uniformTile,
|
|
@@ -14589,6 +16049,11 @@ export {
|
|
|
14589
16049
|
vibranceLinear,
|
|
14590
16050
|
vibrancePixel,
|
|
14591
16051
|
walkDabs,
|
|
16052
|
+
warpMeshEditPoints,
|
|
16053
|
+
warpMeshGridCurves,
|
|
16054
|
+
warpMeshParamAt,
|
|
16055
|
+
warpPresetMesh,
|
|
16056
|
+
warpSurface,
|
|
14592
16057
|
wbMatrix,
|
|
14593
16058
|
whiteBalanceFromSample,
|
|
14594
16059
|
whiteBalanceMatrix,
|