@pixerate/editor 0.98.0 → 0.100.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.
|
@@ -3193,6 +3193,66 @@ var ImageEditorRenderer = class {
|
|
|
3193
3193
|
}
|
|
3194
3194
|
ctx.restore();
|
|
3195
3195
|
}
|
|
3196
|
+
if (options.freeTransform) {
|
|
3197
|
+
const t = options.freeTransform;
|
|
3198
|
+
ctx.save();
|
|
3199
|
+
try {
|
|
3200
|
+
const scr = (p) => projectImagePoint(p, projection);
|
|
3201
|
+
ctx.beginPath();
|
|
3202
|
+
if (t.warp) {
|
|
3203
|
+
for (const c of t.warp.curves) {
|
|
3204
|
+
const [a, b, d, e] = c.map(scr);
|
|
3205
|
+
ctx.moveTo(a.x, a.y);
|
|
3206
|
+
ctx.bezierCurveTo(b.x, b.y, d.x, d.y, e.x, e.y);
|
|
3207
|
+
}
|
|
3208
|
+
for (const h of t.warp.handles) {
|
|
3209
|
+
const a = scr(h.anchor);
|
|
3210
|
+
const q2 = scr(h.point);
|
|
3211
|
+
ctx.moveTo(a.x, a.y);
|
|
3212
|
+
ctx.lineTo(q2.x, q2.y);
|
|
3213
|
+
}
|
|
3214
|
+
} else {
|
|
3215
|
+
const outline = t.outline.map(scr);
|
|
3216
|
+
outline.forEach((p, i) => i ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y));
|
|
3217
|
+
ctx.closePath();
|
|
3218
|
+
}
|
|
3219
|
+
ctx.lineWidth = 3;
|
|
3220
|
+
ctx.strokeStyle = "rgba(0,0,0,0.5)";
|
|
3221
|
+
ctx.stroke();
|
|
3222
|
+
ctx.lineWidth = 1;
|
|
3223
|
+
ctx.strokeStyle = "#ffffff";
|
|
3224
|
+
ctx.stroke();
|
|
3225
|
+
for (const h of [...t.handles, ...t.warp?.anchors ?? []].map(scr)) {
|
|
3226
|
+
ctx.fillStyle = "#ffffff";
|
|
3227
|
+
ctx.strokeStyle = "#1e293b";
|
|
3228
|
+
ctx.fillRect(h.x - 4, h.y - 4, 8, 8);
|
|
3229
|
+
ctx.strokeRect(h.x - 4, h.y - 4, 8, 8);
|
|
3230
|
+
}
|
|
3231
|
+
for (const h of (t.warp?.handles ?? []).map((w) => scr(w.point))) {
|
|
3232
|
+
ctx.beginPath();
|
|
3233
|
+
ctx.arc(h.x, h.y, 3, 0, Math.PI * 2);
|
|
3234
|
+
ctx.fillStyle = "#ffffff";
|
|
3235
|
+
ctx.fill();
|
|
3236
|
+
ctx.strokeStyle = "#1e293b";
|
|
3237
|
+
ctx.stroke();
|
|
3238
|
+
}
|
|
3239
|
+
const r = scr(t.reference);
|
|
3240
|
+
ctx.beginPath();
|
|
3241
|
+
ctx.arc(r.x, r.y, 5, 0, Math.PI * 2);
|
|
3242
|
+
ctx.moveTo(r.x - 8, r.y);
|
|
3243
|
+
ctx.lineTo(r.x + 8, r.y);
|
|
3244
|
+
ctx.moveTo(r.x, r.y - 8);
|
|
3245
|
+
ctx.lineTo(r.x, r.y + 8);
|
|
3246
|
+
ctx.lineWidth = 3;
|
|
3247
|
+
ctx.strokeStyle = "rgba(0,0,0,0.5)";
|
|
3248
|
+
ctx.stroke();
|
|
3249
|
+
ctx.lineWidth = 1;
|
|
3250
|
+
ctx.strokeStyle = "#ffffff";
|
|
3251
|
+
ctx.stroke();
|
|
3252
|
+
} catch {
|
|
3253
|
+
}
|
|
3254
|
+
ctx.restore();
|
|
3255
|
+
}
|
|
3196
3256
|
if (isActivelyCropping) {
|
|
3197
3257
|
const activeCrop = options.draftCrop !== void 0 && options.draftCrop !== null ? options.draftCrop : crop ?? { x: 0, y: 0, width: naturalWidth, height: naturalHeight };
|
|
3198
3258
|
this.drawCropOverlay(ctx, getViewportMetrics(geoState, vpWidth, vpHeight, true, activeCrop));
|
|
@@ -4001,7 +4061,7 @@ function removeAnnotationLayers(layers) {
|
|
|
4001
4061
|
function polygonCoverage(points, width, height, antiAlias = true) {
|
|
4002
4062
|
const m = new Float32Array(width * height);
|
|
4003
4063
|
if (points.length < 3 || width <= 0 || height <= 0) return m;
|
|
4004
|
-
const
|
|
4064
|
+
const sub2 = antiAlias ? 4 : 1;
|
|
4005
4065
|
const edges = [];
|
|
4006
4066
|
for (let i = 0; i < points.length; i++) {
|
|
4007
4067
|
const a = points[i];
|
|
@@ -4016,8 +4076,8 @@ function polygonCoverage(points, width, height, antiAlias = true) {
|
|
|
4016
4076
|
const xs = [];
|
|
4017
4077
|
for (let py = 0; py < height; py++) {
|
|
4018
4078
|
const row = py * width;
|
|
4019
|
-
for (let s = 0; s <
|
|
4020
|
-
const y = py + (s + 0.5) /
|
|
4079
|
+
for (let s = 0; s < sub2; s++) {
|
|
4080
|
+
const y = py + (s + 0.5) / sub2;
|
|
4021
4081
|
while (next < edges.length && edges[next].top <= y) active.push(edges[next++]);
|
|
4022
4082
|
if (active.length) active = active.filter((e) => e.bottom > y);
|
|
4023
4083
|
if (!active.length) continue;
|
|
@@ -4039,7 +4099,7 @@ function polygonCoverage(points, width, height, antiAlias = true) {
|
|
|
4039
4099
|
const c = cx + 0.5;
|
|
4040
4100
|
cov = c >= x0 && c < x1 ? 1 : 0;
|
|
4041
4101
|
}
|
|
4042
|
-
if (cov > 0) m[row + cx] += cov /
|
|
4102
|
+
if (cov > 0) m[row + cx] += cov / sub2;
|
|
4043
4103
|
}
|
|
4044
4104
|
}
|
|
4045
4105
|
}
|
|
@@ -11180,7 +11240,7 @@ var ImageEditorController = class {
|
|
|
11180
11240
|
const canvas = { x: 0, y: 0, width, height };
|
|
11181
11241
|
const warnings = [];
|
|
11182
11242
|
const note = /* @__PURE__ */ new Map();
|
|
11183
|
-
const
|
|
11243
|
+
const add2 = (msg, name) => note.set(msg, [...note.get(msg) ?? [], name]);
|
|
11184
11244
|
const canRasterize = this.renderer.canRasterize();
|
|
11185
11245
|
await this.renderer.prepareLayerAssets(this.state.layers, width, height);
|
|
11186
11246
|
const sources = this.renderer.compositorSources(width, height);
|
|
@@ -11188,7 +11248,7 @@ var ImageEditorController = class {
|
|
|
11188
11248
|
const bakeMask = (layer) => {
|
|
11189
11249
|
const mask = layer.mask;
|
|
11190
11250
|
if (!mask?.source) return mask;
|
|
11191
|
-
|
|
11251
|
+
add2("Parametric (depth) masks were baked into pixel masks", layer.name);
|
|
11192
11252
|
const plain3 = { ...layer, mask: { ...mask, density: 1, feather: 0, enabled: true } };
|
|
11193
11253
|
const ox = layer.type === "pixel" ? layer.offset.x : 0;
|
|
11194
11254
|
const oy = layer.type === "pixel" ? layer.offset.y : 0;
|
|
@@ -11204,7 +11264,7 @@ var ImageEditorController = class {
|
|
|
11204
11264
|
};
|
|
11205
11265
|
const prepare = async (layer) => {
|
|
11206
11266
|
const p = await prepareLayer(layer);
|
|
11207
|
-
if (p?.blend === "pigment")
|
|
11267
|
+
if (p?.blend === "pigment") add2("The Pigment blend mode has no PSD equivalent and was written as Normal", layer.name);
|
|
11208
11268
|
return p;
|
|
11209
11269
|
};
|
|
11210
11270
|
const prepareLayer = async (layer) => {
|
|
@@ -11219,10 +11279,10 @@ var ImageEditorController = class {
|
|
|
11219
11279
|
if (layer.type === "adjustment") return { ...layer, mask: bakeMask(layer) };
|
|
11220
11280
|
if (layer.type === "annotation") {
|
|
11221
11281
|
if (!canRasterize) {
|
|
11222
|
-
|
|
11282
|
+
add2("Annotation layers need a canvas (a browser) to rasterize and were left out", layer.name);
|
|
11223
11283
|
return null;
|
|
11224
11284
|
}
|
|
11225
|
-
|
|
11285
|
+
add2("Annotation layers were written as pixel layers", layer.name);
|
|
11226
11286
|
const blend = layer.blend === "normal" && layer.annotation.blend ? layer.annotation.blend : layer.blend;
|
|
11227
11287
|
const content = { ...layer, visible: true, opacity: 1, blend: "normal", annotation: { ...layer.annotation, blend: void 0 } };
|
|
11228
11288
|
delete content.mask;
|
|
@@ -11253,7 +11313,7 @@ var ImageEditorController = class {
|
|
|
11253
11313
|
}
|
|
11254
11314
|
const active = layer.filters?.filter(isActiveFilter) ?? [];
|
|
11255
11315
|
if (active.length) {
|
|
11256
|
-
|
|
11316
|
+
add2("Layer filters (develop, blur) were baked into the pixels", layer.name);
|
|
11257
11317
|
const content = { ...out, visible: true, opacity: 1, blend: "normal" };
|
|
11258
11318
|
delete content.mask;
|
|
11259
11319
|
delete content.clipped;
|
|
@@ -11262,7 +11322,7 @@ var ImageEditorController = class {
|
|
|
11262
11322
|
const data = compositor.composite([content], canvas, sources);
|
|
11263
11323
|
out = { ...out, surface: surfaceFromPixels({ width, height, data }, 4, -layer.offset.x, -layer.offset.y) };
|
|
11264
11324
|
}
|
|
11265
|
-
if (layer.filters?.some((f) => !f.enabled))
|
|
11325
|
+
if (layer.filters?.some((f) => !f.enabled)) add2("Disabled layer filters were left out", layer.name);
|
|
11266
11326
|
delete out.filters;
|
|
11267
11327
|
const mask = bakeMask(layer);
|
|
11268
11328
|
if (mask) out.mask = mask;
|
|
@@ -13416,7 +13476,7 @@ function createImageEffectTools(controller) {
|
|
|
13416
13476
|
annotations: { title: "List layer effects", ...READ3 },
|
|
13417
13477
|
handler: ({ layerId }) => result(layerArg(layerId), { globalLight: controller.getGlobalLight(), patterns: patternList(controller), kinds: [...LAYER_EFFECT_KINDS] })
|
|
13418
13478
|
};
|
|
13419
|
-
const
|
|
13479
|
+
const add2 = {
|
|
13420
13480
|
name: "add_layer_effect",
|
|
13421
13481
|
title: "Add a layer effect",
|
|
13422
13482
|
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.",
|
|
@@ -13514,7 +13574,7 @@ function createImageEffectTools(controller) {
|
|
|
13514
13574
|
return { globalLight: controller.setGlobalLight(next) };
|
|
13515
13575
|
}
|
|
13516
13576
|
};
|
|
13517
|
-
return [list2,
|
|
13577
|
+
return [list2, add2, update, remove, setEnabled, light];
|
|
13518
13578
|
}
|
|
13519
13579
|
|
|
13520
13580
|
// src/image-editor/layers/paint-ui.ts
|
|
@@ -14779,6 +14839,7 @@ function layerMenu(controller) {
|
|
|
14779
14839
|
{ action: "apply-mask", label: "Apply Layer Mask", enabled: isPixel && Boolean(active?.mask) },
|
|
14780
14840
|
{ action: "delete-mask", label: "Delete Layer Mask", enabled: Boolean(active?.mask) },
|
|
14781
14841
|
{ action: "toggle-mask", label: active?.mask?.enabled === false ? "Enable Layer Mask" : "Disable Layer Mask", enabled: Boolean(active?.mask) },
|
|
14842
|
+
{ action: "toggle-mask-link", label: active?.mask?.linked === false ? "Link Layer Mask" : "Unlink Layer Mask", enabled: Boolean(active?.mask && !active.mask.source) },
|
|
14782
14843
|
{ action: "rasterize", label: "Rasterize Layer", enabled: active?.type === "annotation" || isPixel && Boolean(active?.type === "pixel" && active.filters?.length) },
|
|
14783
14844
|
{ action: "toggle-clip", label: active?.clipped ? "Release Clipping Mask" : "Create Clipping Mask", enabled: Boolean(loc && loc.index > 0) }
|
|
14784
14845
|
];
|
|
@@ -14834,6 +14895,9 @@ async function runLayerMenuAction(controller, action) {
|
|
|
14834
14895
|
case "toggle-mask":
|
|
14835
14896
|
if (id && active?.mask) controller.setLayerMaskProps(id, { enabled: active.mask.enabled === false });
|
|
14836
14897
|
break;
|
|
14898
|
+
case "toggle-mask-link":
|
|
14899
|
+
if (id && active?.mask) controller.setLayerMaskProps(id, { linked: active.mask.linked === false });
|
|
14900
|
+
break;
|
|
14837
14901
|
case "rasterize":
|
|
14838
14902
|
if (id) await controller.rasterizeLayer(id);
|
|
14839
14903
|
break;
|
|
@@ -15052,6 +15116,577 @@ async function downloadPsd(controller, name = "image") {
|
|
|
15052
15116
|
}
|
|
15053
15117
|
return { fileName, warnings: res.warnings };
|
|
15054
15118
|
}
|
|
15119
|
+
|
|
15120
|
+
// src/image-editor/layers/transform-ui.ts
|
|
15121
|
+
var CORNERS = ["nw", "ne", "se", "sw"];
|
|
15122
|
+
var SIDES = ["n", "e", "s", "w"];
|
|
15123
|
+
var CORNER_INDEX = { nw: 0, ne: 1, se: 2, sw: 3 };
|
|
15124
|
+
var SIDE_CORNERS = { n: [0, 1], e: [1, 2], s: [2, 3], w: [3, 0] };
|
|
15125
|
+
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] };
|
|
15126
|
+
var ANCHOR_FRACTION2 = {
|
|
15127
|
+
"top-left": [0, 0],
|
|
15128
|
+
top: [0.5, 0],
|
|
15129
|
+
"top-right": [1, 0],
|
|
15130
|
+
left: [0, 0.5],
|
|
15131
|
+
center: [0.5, 0.5],
|
|
15132
|
+
right: [1, 0.5],
|
|
15133
|
+
"bottom-left": [0, 1],
|
|
15134
|
+
bottom: [0.5, 1],
|
|
15135
|
+
"bottom-right": [1, 1]
|
|
15136
|
+
};
|
|
15137
|
+
var FREE_TRANSFORM_MODE_OPTIONS = [
|
|
15138
|
+
{ id: "scale", label: "Free", title: "Free Transform: corners scale, outside rotates, Ctrl/Cmd distorts" },
|
|
15139
|
+
{ id: "rotate", label: "Rotate", title: "Every handle rotates about the reference point" },
|
|
15140
|
+
{ id: "skew", label: "Skew", title: "Sides slide along themselves" },
|
|
15141
|
+
{ id: "distort", label: "Distort", title: "Each corner moves freely" },
|
|
15142
|
+
{ id: "perspective", label: "Perspective", title: "A corner and its partner move symmetrically" },
|
|
15143
|
+
{ id: "warp", label: "Warp", title: "Bend through a mesh: drag its points or pull inside; presets in the bar" }
|
|
15144
|
+
];
|
|
15145
|
+
var WARP_STYLE_OPTIONS = [
|
|
15146
|
+
{ id: "custom", label: "Custom" },
|
|
15147
|
+
...WARP_STYLES.map((id) => ({ id, label: WARP_STYLE_LABELS[id] }))
|
|
15148
|
+
];
|
|
15149
|
+
var WARP_GRID_OPTIONS = [
|
|
15150
|
+
{ id: "1x1", label: "Default", columns: 1, rows: 1 },
|
|
15151
|
+
{ id: "3x3", label: "3 \xD7 3", columns: 3, rows: 3 },
|
|
15152
|
+
{ id: "5x5", label: "5 \xD7 5", columns: 5, rows: 5 }
|
|
15153
|
+
];
|
|
15154
|
+
var FREE_TRANSFORM_FIELDS = [
|
|
15155
|
+
{ id: "x", label: "X", unit: "px", title: "Reference point X (document pixels)" },
|
|
15156
|
+
{ id: "y", label: "Y", unit: "px", title: "Reference point Y (document pixels)" },
|
|
15157
|
+
{ id: "scaleX", label: "W", unit: "%", title: "Width in percent of the original" },
|
|
15158
|
+
{ id: "scaleY", label: "H", unit: "%", title: "Height in percent of the original" },
|
|
15159
|
+
{ id: "rotate", label: "\u2220", unit: "\xB0", title: "Rotation in degrees, clockwise" },
|
|
15160
|
+
{ id: "skewX", label: "H skew", unit: "\xB0", title: "Horizontal skew in degrees" },
|
|
15161
|
+
{ id: "skewY", label: "V skew", unit: "\xB0", title: "Vertical skew in degrees" }
|
|
15162
|
+
];
|
|
15163
|
+
var CURSOR = {
|
|
15164
|
+
nw: "nwse-resize",
|
|
15165
|
+
se: "nwse-resize",
|
|
15166
|
+
ne: "nesw-resize",
|
|
15167
|
+
sw: "nesw-resize",
|
|
15168
|
+
n: "ns-resize",
|
|
15169
|
+
s: "ns-resize",
|
|
15170
|
+
e: "ew-resize",
|
|
15171
|
+
w: "ew-resize",
|
|
15172
|
+
reference: "move",
|
|
15173
|
+
inside: "move",
|
|
15174
|
+
outside: "alias"
|
|
15175
|
+
};
|
|
15176
|
+
var meshIndex = (h) => h && h.startsWith("mesh:") ? Number(h.slice(5)) : -1;
|
|
15177
|
+
var mod = (m) => Boolean(m.ctrlKey || m.metaKey);
|
|
15178
|
+
var add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
|
|
15179
|
+
var sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
|
|
15180
|
+
function rotateAbout(p, c, rad) {
|
|
15181
|
+
const cos = Math.cos(rad);
|
|
15182
|
+
const sin = Math.sin(rad);
|
|
15183
|
+
const d = sub(p, c);
|
|
15184
|
+
return { x: c.x + d.x * cos - d.y * sin, y: c.y + d.x * sin + d.y * cos };
|
|
15185
|
+
}
|
|
15186
|
+
function insideQuad(q2, p) {
|
|
15187
|
+
let sign = 0;
|
|
15188
|
+
for (let i = 0; i < 4; i++) {
|
|
15189
|
+
const a = q2[i];
|
|
15190
|
+
const b = q2[(i + 1) % 4];
|
|
15191
|
+
const c = (b.x - a.x) * (p.y - a.y) - (b.y - a.y) * (p.x - a.x);
|
|
15192
|
+
if (Math.abs(c) < 1e-12) continue;
|
|
15193
|
+
const s = Math.sign(c);
|
|
15194
|
+
if (sign && s !== sign) return false;
|
|
15195
|
+
sign = s;
|
|
15196
|
+
}
|
|
15197
|
+
return true;
|
|
15198
|
+
}
|
|
15199
|
+
function insidePolygon(poly, p) {
|
|
15200
|
+
let inside = false;
|
|
15201
|
+
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
|
|
15202
|
+
const a = poly[i];
|
|
15203
|
+
const b = poly[j];
|
|
15204
|
+
if (a.y > p.y !== b.y > p.y && p.x < (b.x - a.x) * (p.y - a.y) / (b.y - a.y) + a.x) inside = !inside;
|
|
15205
|
+
}
|
|
15206
|
+
return inside;
|
|
15207
|
+
}
|
|
15208
|
+
var FreeTransformTool = class {
|
|
15209
|
+
constructor(controller, options = {}) {
|
|
15210
|
+
this.controller = controller;
|
|
15211
|
+
this.options = options;
|
|
15212
|
+
}
|
|
15213
|
+
controller;
|
|
15214
|
+
options;
|
|
15215
|
+
sessionRef = null;
|
|
15216
|
+
modeValue = "scale";
|
|
15217
|
+
/** The reference point in the untransformed frame's space (follows the transform). */
|
|
15218
|
+
refBox = { x: 0, y: 0 };
|
|
15219
|
+
corners = null;
|
|
15220
|
+
drag = null;
|
|
15221
|
+
/** The warp mesh in the frame's own space (null: no warp). */
|
|
15222
|
+
mesh = null;
|
|
15223
|
+
warpSettings = { style: "custom", bend: 50, horizontal: 0, vertical: 0, orientation: "horizontal", columns: 1, rows: 1 };
|
|
15224
|
+
pending = false;
|
|
15225
|
+
viewScale = 1;
|
|
15226
|
+
viewClip;
|
|
15227
|
+
/** Values typed into the options bar since the last drag (V skew is not recovered from the shape). */
|
|
15228
|
+
typed = {};
|
|
15229
|
+
/** Width and height scale together when typed (the chain icon). */
|
|
15230
|
+
proportional = true;
|
|
15231
|
+
error = null;
|
|
15232
|
+
lastResult = null;
|
|
15233
|
+
get active() {
|
|
15234
|
+
return Boolean(this.sessionRef?.isOpen);
|
|
15235
|
+
}
|
|
15236
|
+
get session() {
|
|
15237
|
+
return this.active ? this.sessionRef : null;
|
|
15238
|
+
}
|
|
15239
|
+
get mode() {
|
|
15240
|
+
return this.modeValue;
|
|
15241
|
+
}
|
|
15242
|
+
get interpolation() {
|
|
15243
|
+
return this.sessionRef?.interpolation ?? "bicubic";
|
|
15244
|
+
}
|
|
15245
|
+
listeners = /* @__PURE__ */ new Set();
|
|
15246
|
+
/** Calls `listener` whenever the overlay, readout, mode or error changes. Returns the unsubscribe. */
|
|
15247
|
+
subscribe(listener) {
|
|
15248
|
+
this.listeners.add(listener);
|
|
15249
|
+
return () => {
|
|
15250
|
+
this.listeners.delete(listener);
|
|
15251
|
+
};
|
|
15252
|
+
}
|
|
15253
|
+
changed() {
|
|
15254
|
+
this.options.onChange?.();
|
|
15255
|
+
for (const l of this.listeners) l();
|
|
15256
|
+
}
|
|
15257
|
+
/** Opens a Free Transform (default: the selected pixels of the active layer, or the whole layer). Returns an error message or `null`. */
|
|
15258
|
+
begin(init = {}) {
|
|
15259
|
+
if (this.active) return null;
|
|
15260
|
+
try {
|
|
15261
|
+
if (!init.layerId && init.content !== "selection-outline" && !this.controller.getState().activeLayerId) {
|
|
15262
|
+
const top = [...flattenLayers(this.controller.getState().layers)].reverse().find((l) => l.type === "pixel" && l.visible);
|
|
15263
|
+
if (top) this.controller.setActiveLayer(top.id);
|
|
15264
|
+
}
|
|
15265
|
+
const s = this.controller.beginFreeTransform(init);
|
|
15266
|
+
this.sessionRef = s;
|
|
15267
|
+
this.corners = s.geometry.corners;
|
|
15268
|
+
this.refBox = { x: s.box.x + s.box.width / 2, y: s.box.y + s.box.height / 2 };
|
|
15269
|
+
this.modeValue = "scale";
|
|
15270
|
+
this.mesh = null;
|
|
15271
|
+
this.warpSettings = { style: "custom", bend: 50, horizontal: 0, vertical: 0, orientation: "horizontal", columns: 1, rows: 1 };
|
|
15272
|
+
this.typed = {};
|
|
15273
|
+
this.error = null;
|
|
15274
|
+
} catch (e) {
|
|
15275
|
+
this.error = e instanceof Error ? e.message : String(e);
|
|
15276
|
+
}
|
|
15277
|
+
this.changed();
|
|
15278
|
+
return this.error;
|
|
15279
|
+
}
|
|
15280
|
+
setMode(mode) {
|
|
15281
|
+
this.modeValue = mode;
|
|
15282
|
+
const s = this.session;
|
|
15283
|
+
if (mode === "warp" && s && !this.mesh) this.mesh = identityWarpMesh(s.box, this.warpSettings.columns, this.warpSettings.rows);
|
|
15284
|
+
this.changed();
|
|
15285
|
+
}
|
|
15286
|
+
/** The Warp options bar's settings. */
|
|
15287
|
+
warpBar() {
|
|
15288
|
+
return { ...this.warpSettings };
|
|
15289
|
+
}
|
|
15290
|
+
/** The warp mesh (frame space), or null. */
|
|
15291
|
+
get warp() {
|
|
15292
|
+
return this.mesh;
|
|
15293
|
+
}
|
|
15294
|
+
/**
|
|
15295
|
+
* Applies Warp options: a preset `style` with `bend` / `horizontal` / `vertical` (percent) and
|
|
15296
|
+
* `orientation` replaces the mesh; `custom` keeps it; the grid size re-splits it (exactly when it
|
|
15297
|
+
* can). Switches to Warp mode.
|
|
15298
|
+
*/
|
|
15299
|
+
setWarp(changes) {
|
|
15300
|
+
const s = this.session;
|
|
15301
|
+
if (!s) return;
|
|
15302
|
+
this.modeValue = "warp";
|
|
15303
|
+
const next = { ...this.warpSettings, ...changes };
|
|
15304
|
+
this.warpSettings = next;
|
|
15305
|
+
let mesh = this.mesh ?? identityWarpMesh(s.box, next.columns, next.rows);
|
|
15306
|
+
if (next.style !== "custom" && ("style" in changes || "bend" in changes || "horizontal" in changes || "vertical" in changes || "orientation" in changes)) {
|
|
15307
|
+
const preset = { style: next.style, bend: next.bend, horizontal: next.horizontal, vertical: next.vertical, orientation: next.orientation };
|
|
15308
|
+
mesh = warpPresetMesh(preset, s.box) ?? identityWarpMesh(s.box);
|
|
15309
|
+
mesh = resizeWarpMesh(mesh, next.columns, next.rows);
|
|
15310
|
+
}
|
|
15311
|
+
if ("columns" in changes || "rows" in changes) mesh = resizeWarpMesh(mesh, next.columns, next.rows);
|
|
15312
|
+
this.mesh = mesh;
|
|
15313
|
+
this.changed();
|
|
15314
|
+
this.preview();
|
|
15315
|
+
}
|
|
15316
|
+
/** Sets the reference point to an anchor of the frame (the options bar's locator). */
|
|
15317
|
+
setReference(anchor) {
|
|
15318
|
+
const s = this.session;
|
|
15319
|
+
if (!s) return;
|
|
15320
|
+
const [fx, fy] = ANCHOR_FRACTION2[anchor];
|
|
15321
|
+
this.refBox = { x: s.box.x + fx * s.box.width, y: s.box.y + fy * s.box.height };
|
|
15322
|
+
this.changed();
|
|
15323
|
+
}
|
|
15324
|
+
setInterpolation(interpolation) {
|
|
15325
|
+
const s = this.session;
|
|
15326
|
+
if (!s) return;
|
|
15327
|
+
this.run(() => s.update({ interpolation }, false));
|
|
15328
|
+
}
|
|
15329
|
+
/** Screen pixels per document pixel and the visible document area, for previews. */
|
|
15330
|
+
setView(scale, clip) {
|
|
15331
|
+
this.viewScale = scale > 0 ? scale : 1;
|
|
15332
|
+
this.viewClip = clip;
|
|
15333
|
+
}
|
|
15334
|
+
homography() {
|
|
15335
|
+
const s = this.session;
|
|
15336
|
+
return s && this.corners ? rectToQuad(s.box, this.corners) : null;
|
|
15337
|
+
}
|
|
15338
|
+
/** The reference point in document pixels. */
|
|
15339
|
+
referencePoint() {
|
|
15340
|
+
const h = this.homography();
|
|
15341
|
+
return h ? applyHomography(h, this.refBox) : null;
|
|
15342
|
+
}
|
|
15343
|
+
/** The outline, handles and reference point to draw, or `null` when no transform is open. */
|
|
15344
|
+
overlay() {
|
|
15345
|
+
const s = this.session;
|
|
15346
|
+
const h = this.homography();
|
|
15347
|
+
if (!s || !h || !this.corners) return null;
|
|
15348
|
+
const handles = [...CORNERS, ...SIDES].map((id) => {
|
|
15349
|
+
const [fx, fy] = HANDLE_FRACTION[id];
|
|
15350
|
+
const p = applyHomography(h, { x: s.box.x + fx * s.box.width, y: s.box.y + fy * s.box.height }) ?? { x: 0, y: 0 };
|
|
15351
|
+
return { id, x: p.x, y: p.y };
|
|
15352
|
+
});
|
|
15353
|
+
const out = { outline: this.corners.map((p) => ({ x: p.x, y: p.y })), handles, reference: this.referencePoint() ?? { x: 0, y: 0 } };
|
|
15354
|
+
if (this.modeValue === "warp" && this.mesh) {
|
|
15355
|
+
const m = this.mesh;
|
|
15356
|
+
const at = (p) => applyHomography(h, p) ?? p;
|
|
15357
|
+
const pts = m.points.map(at);
|
|
15358
|
+
const edit = warpMeshEditPoints(m);
|
|
15359
|
+
out.handles = [];
|
|
15360
|
+
out.outline = warpMeshGridCurves({ ...m, points: pts }).length ? this.meshOutline(pts, m) : out.outline;
|
|
15361
|
+
out.warp = {
|
|
15362
|
+
curves: warpMeshGridCurves({ ...m, points: pts }),
|
|
15363
|
+
anchors: edit.anchors.map((i) => pts[i]),
|
|
15364
|
+
handles: edit.handles.map((e) => ({ point: pts[e.index], anchor: pts[e.anchor] }))
|
|
15365
|
+
};
|
|
15366
|
+
}
|
|
15367
|
+
return out;
|
|
15368
|
+
}
|
|
15369
|
+
/** The mesh's border anchors, clockwise (for the inside test). */
|
|
15370
|
+
meshOutline(pts, m) {
|
|
15371
|
+
const nx = 3 * (m.us.length - 1) + 1;
|
|
15372
|
+
const ny = 3 * (m.vs.length - 1) + 1;
|
|
15373
|
+
const ring = [];
|
|
15374
|
+
for (let i = 0; i < nx; i++) ring.push(pts[i]);
|
|
15375
|
+
for (let j = 1; j < ny; j++) ring.push(pts[j * nx + nx - 1]);
|
|
15376
|
+
for (let i = nx - 2; i >= 0; i--) ring.push(pts[(ny - 1) * nx + i]);
|
|
15377
|
+
for (let j = ny - 2; j > 0; j--) ring.push(pts[j * nx]);
|
|
15378
|
+
return ring;
|
|
15379
|
+
}
|
|
15380
|
+
/** What a document point is over (`tolerance` in document pixels: a few screen pixels / scale). */
|
|
15381
|
+
hit(p, tolerance) {
|
|
15382
|
+
const o = this.overlay();
|
|
15383
|
+
if (!o) return null;
|
|
15384
|
+
if (this.modeValue === "warp" && this.mesh) {
|
|
15385
|
+
const h = this.homography();
|
|
15386
|
+
const edit = warpMeshEditPoints(this.mesh);
|
|
15387
|
+
let best2 = -1;
|
|
15388
|
+
let bestD2 = tolerance;
|
|
15389
|
+
for (const i of [...edit.anchors, ...edit.handles.map((e) => e.index)]) {
|
|
15390
|
+
const q2 = applyHomography(h, this.mesh.points[i]);
|
|
15391
|
+
if (!q2) continue;
|
|
15392
|
+
const d = Math.hypot(q2.x - p.x, q2.y - p.y);
|
|
15393
|
+
if (d <= bestD2) {
|
|
15394
|
+
best2 = i;
|
|
15395
|
+
bestD2 = d;
|
|
15396
|
+
}
|
|
15397
|
+
}
|
|
15398
|
+
if (best2 >= 0) return `mesh:${best2}`;
|
|
15399
|
+
return insidePolygon(o.outline, p) ? "inside" : "outside";
|
|
15400
|
+
}
|
|
15401
|
+
let best = null;
|
|
15402
|
+
let bestD = tolerance;
|
|
15403
|
+
for (const h of o.handles) {
|
|
15404
|
+
const d = Math.hypot(h.x - p.x, h.y - p.y);
|
|
15405
|
+
if (d <= bestD) {
|
|
15406
|
+
best = h.id;
|
|
15407
|
+
bestD = d;
|
|
15408
|
+
}
|
|
15409
|
+
}
|
|
15410
|
+
if (Math.hypot(o.reference.x - p.x, o.reference.y - p.y) < bestD) best = "reference";
|
|
15411
|
+
if (best) return best;
|
|
15412
|
+
return insideQuad(o.outline, p) ? "inside" : "outside";
|
|
15413
|
+
}
|
|
15414
|
+
/** The CSS cursor for a handle. */
|
|
15415
|
+
cursor(handle) {
|
|
15416
|
+
if (!handle) return "default";
|
|
15417
|
+
if (meshIndex(handle) >= 0) return "pointer";
|
|
15418
|
+
if (this.modeValue === "warp") return handle === "inside" ? "grab" : "default";
|
|
15419
|
+
if (this.modeValue === "rotate" && handle !== "inside" && handle !== "reference") return CURSOR.outside;
|
|
15420
|
+
return CURSOR[handle];
|
|
15421
|
+
}
|
|
15422
|
+
/** Starts a drag on what `p` is over. */
|
|
15423
|
+
down(p, tolerance) {
|
|
15424
|
+
if (!this.session || !this.corners) return null;
|
|
15425
|
+
const handle = this.hit(p, tolerance);
|
|
15426
|
+
if (!handle) return null;
|
|
15427
|
+
let st = null;
|
|
15428
|
+
if (this.modeValue === "warp" && this.mesh && handle === "inside") {
|
|
15429
|
+
const q2 = this.toFrame(p, this.corners);
|
|
15430
|
+
if (q2) {
|
|
15431
|
+
const r = warpMeshParamAt(this.mesh, q2);
|
|
15432
|
+
st = { s: r.s, t: r.t };
|
|
15433
|
+
}
|
|
15434
|
+
}
|
|
15435
|
+
this.drag = { handle, start: { x: p.x, y: p.y }, corners: this.corners, refBox: { ...this.refBox }, ref: this.referencePoint() ?? p, mesh: this.mesh, st };
|
|
15436
|
+
return handle;
|
|
15437
|
+
}
|
|
15438
|
+
/** A document point in the frame's own space under `corners`. */
|
|
15439
|
+
toFrame(p, corners) {
|
|
15440
|
+
const s = this.session;
|
|
15441
|
+
const h = s ? rectToQuad(s.box, corners) : null;
|
|
15442
|
+
const inv = h && invertHomography(h);
|
|
15443
|
+
return inv ? applyHomography(inv, p) : null;
|
|
15444
|
+
}
|
|
15445
|
+
/** Continues the drag to `p` with the modifiers held now. */
|
|
15446
|
+
move(p, m = {}) {
|
|
15447
|
+
const d = this.drag;
|
|
15448
|
+
const s = this.session;
|
|
15449
|
+
if (!d || !s) return;
|
|
15450
|
+
if (d.handle === "reference") {
|
|
15451
|
+
const h = rectToQuad(s.box, d.corners);
|
|
15452
|
+
const inv = h && invertHomography(h);
|
|
15453
|
+
const q2 = inv && applyHomography(inv, p);
|
|
15454
|
+
if (q2) this.refBox = q2;
|
|
15455
|
+
this.changed();
|
|
15456
|
+
return;
|
|
15457
|
+
}
|
|
15458
|
+
if (this.modeValue === "warp" && d.mesh) {
|
|
15459
|
+
const a = this.toFrame(d.start, d.corners);
|
|
15460
|
+
const b = this.toFrame(p, d.corners);
|
|
15461
|
+
if (!a || !b) return;
|
|
15462
|
+
const delta = { x: b.x - a.x, y: b.y - a.y };
|
|
15463
|
+
const i = meshIndex(d.handle);
|
|
15464
|
+
if (i >= 0) this.mesh = dragWarpMeshPoint(d.mesh, i, delta);
|
|
15465
|
+
else if (d.handle === "inside" && d.st) this.mesh = pullWarpMesh(d.mesh, d.st.s, d.st.t, delta);
|
|
15466
|
+
else return;
|
|
15467
|
+
this.warpSettings = { ...this.warpSettings, style: "custom" };
|
|
15468
|
+
this.changed();
|
|
15469
|
+
this.preview();
|
|
15470
|
+
return;
|
|
15471
|
+
}
|
|
15472
|
+
const corners = this.dragCorners(d, p, m);
|
|
15473
|
+
if (corners) this.setCorners(corners);
|
|
15474
|
+
}
|
|
15475
|
+
/** Ends the drag (the transform stays open). */
|
|
15476
|
+
up() {
|
|
15477
|
+
this.drag = null;
|
|
15478
|
+
}
|
|
15479
|
+
dragCorners(d, p, m) {
|
|
15480
|
+
const s = this.session;
|
|
15481
|
+
const c0 = d.corners;
|
|
15482
|
+
let delta = sub(p, d.start);
|
|
15483
|
+
const map = (f) => c0.map(f);
|
|
15484
|
+
const handle = d.handle;
|
|
15485
|
+
const isCorner = handle in CORNER_INDEX;
|
|
15486
|
+
const isSide = handle in SIDE_CORNERS;
|
|
15487
|
+
const rotating = handle === "outside" || this.modeValue === "rotate" && (isCorner || isSide);
|
|
15488
|
+
if (handle === "inside") {
|
|
15489
|
+
if (m.shiftKey) delta = Math.abs(delta.x) >= Math.abs(delta.y) ? { x: delta.x, y: 0 } : { x: 0, y: delta.y };
|
|
15490
|
+
return map((q3) => add(q3, delta));
|
|
15491
|
+
}
|
|
15492
|
+
if (rotating) {
|
|
15493
|
+
const r = d.ref;
|
|
15494
|
+
let a = Math.atan2(p.y - r.y, p.x - r.x) - Math.atan2(d.start.y - r.y, d.start.x - r.x);
|
|
15495
|
+
if (m.shiftKey) a = Math.round(a / (Math.PI / 12)) * (Math.PI / 12);
|
|
15496
|
+
return map((q3) => rotateAbout(q3, r, a));
|
|
15497
|
+
}
|
|
15498
|
+
const perspective = this.modeValue === "perspective" || mod(m) && m.altKey && m.shiftKey;
|
|
15499
|
+
const distort = this.modeValue === "distort" || mod(m) && !perspective;
|
|
15500
|
+
const skew = this.modeValue === "skew" || mod(m) && isSide;
|
|
15501
|
+
if (isSide && (skew || perspective)) {
|
|
15502
|
+
const [i, j] = SIDE_CORNERS[handle];
|
|
15503
|
+
const dir = sub(c0[j], c0[i]);
|
|
15504
|
+
const len = Math.hypot(dir.x, dir.y) || 1;
|
|
15505
|
+
const t = (delta.x * dir.x + delta.y * dir.y) / len;
|
|
15506
|
+
const v = { x: dir.x / len * t, y: dir.y / len * t };
|
|
15507
|
+
const [oi, oj] = SIDE_CORNERS[{ n: "s", s: "n", e: "w", w: "e" }[handle]];
|
|
15508
|
+
return map((q3, k) => k === i || k === j ? add(q3, v) : m.altKey && (k === oi || k === oj) ? sub(q3, v) : q3);
|
|
15509
|
+
}
|
|
15510
|
+
if (isCorner && perspective) {
|
|
15511
|
+
const k = CORNER_INDEX[handle];
|
|
15512
|
+
if (Math.abs(delta.x) >= Math.abs(delta.y)) {
|
|
15513
|
+
const partner2 = [1, 0, 3, 2][k];
|
|
15514
|
+
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);
|
|
15515
|
+
}
|
|
15516
|
+
const partner = [3, 2, 1, 0][k];
|
|
15517
|
+
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);
|
|
15518
|
+
}
|
|
15519
|
+
if (distort || isCorner && this.modeValue === "skew") {
|
|
15520
|
+
if (isCorner) return map((q3, i2) => i2 === CORNER_INDEX[handle] ? add(q3, delta) : q3);
|
|
15521
|
+
const [i, j] = SIDE_CORNERS[handle];
|
|
15522
|
+
return map((q3, k) => k === i || k === j ? add(q3, delta) : q3);
|
|
15523
|
+
}
|
|
15524
|
+
const h = rectToQuad(s.box, c0);
|
|
15525
|
+
const inv = h && invertHomography(h);
|
|
15526
|
+
if (!h || !inv) return null;
|
|
15527
|
+
const q2 = applyHomography(inv, p);
|
|
15528
|
+
const q0 = applyHomography(inv, d.start);
|
|
15529
|
+
if (!q2 || !q0) return null;
|
|
15530
|
+
const b = s.box;
|
|
15531
|
+
const [fx, fy] = HANDLE_FRACTION[handle];
|
|
15532
|
+
const grab = { x: b.x + fx * b.width, y: b.y + fy * b.height };
|
|
15533
|
+
const anchor = m.altKey ? d.refBox : { x: b.x + (1 - fx) * b.width, y: b.y + (1 - fy) * b.height };
|
|
15534
|
+
const target = add(grab, sub(q2, q0));
|
|
15535
|
+
const ax = grab.x - anchor.x;
|
|
15536
|
+
const ay = grab.y - anchor.y;
|
|
15537
|
+
let sx = isCorner || handle === "e" || handle === "w" ? Math.abs(ax) > 1e-9 ? (target.x - anchor.x) / ax : 1 : 1;
|
|
15538
|
+
let sy = isCorner || handle === "n" || handle === "s" ? Math.abs(ay) > 1e-9 ? (target.y - anchor.y) / ay : 1 : 1;
|
|
15539
|
+
if (isCorner && !m.shiftKey) {
|
|
15540
|
+
const k = Math.abs(sx) >= Math.abs(sy) ? sx : sy;
|
|
15541
|
+
sx = k;
|
|
15542
|
+
sy = k;
|
|
15543
|
+
}
|
|
15544
|
+
const scaled = [
|
|
15545
|
+
{ x: b.x, y: b.y },
|
|
15546
|
+
{ x: b.x + b.width, y: b.y },
|
|
15547
|
+
{ x: b.x + b.width, y: b.y + b.height },
|
|
15548
|
+
{ x: b.x, y: b.y + b.height }
|
|
15549
|
+
].map((c) => applyHomography(h, { x: anchor.x + (c.x - anchor.x) * sx, y: anchor.y + (c.y - anchor.y) * sy }));
|
|
15550
|
+
return scaled.every(Boolean) ? scaled : null;
|
|
15551
|
+
}
|
|
15552
|
+
/** Sets new corners and refreshes the preview (scheduled). Invalid shapes keep the last good one. */
|
|
15553
|
+
setCorners(corners) {
|
|
15554
|
+
const s = this.session;
|
|
15555
|
+
if (!s) return;
|
|
15556
|
+
const h = rectToQuad(s.box, corners);
|
|
15557
|
+
if (!h) return;
|
|
15558
|
+
this.corners = corners;
|
|
15559
|
+
this.typed = {};
|
|
15560
|
+
this.changed();
|
|
15561
|
+
this.preview();
|
|
15562
|
+
}
|
|
15563
|
+
preview() {
|
|
15564
|
+
if (this.pending) return;
|
|
15565
|
+
this.pending = true;
|
|
15566
|
+
const run = () => {
|
|
15567
|
+
this.pending = false;
|
|
15568
|
+
const s = this.session;
|
|
15569
|
+
if (!s || !this.corners) return;
|
|
15570
|
+
this.run(() => s.update({ corners: this.corners, warp: this.mesh }, { scale: this.viewScale, clip: this.viewClip }));
|
|
15571
|
+
};
|
|
15572
|
+
if (this.options.schedule) this.options.schedule(run);
|
|
15573
|
+
else run();
|
|
15574
|
+
}
|
|
15575
|
+
run(fn) {
|
|
15576
|
+
try {
|
|
15577
|
+
fn();
|
|
15578
|
+
this.error = null;
|
|
15579
|
+
} catch (e) {
|
|
15580
|
+
this.error = e instanceof Error ? e.message : String(e);
|
|
15581
|
+
if (this.sessionRef && !this.sessionRef.isOpen) this.sessionRef = null;
|
|
15582
|
+
}
|
|
15583
|
+
this.changed();
|
|
15584
|
+
}
|
|
15585
|
+
/** The options bar's numbers for the current shape. */
|
|
15586
|
+
bar() {
|
|
15587
|
+
const s = this.session;
|
|
15588
|
+
if (!s || !this.corners) return null;
|
|
15589
|
+
const read = describeTransform({ box: s.box, corners: this.corners }, this.refBox);
|
|
15590
|
+
return {
|
|
15591
|
+
affine: read.affine,
|
|
15592
|
+
x: this.typed.x ?? read.x,
|
|
15593
|
+
y: this.typed.y ?? read.y,
|
|
15594
|
+
scaleX: this.typed.scaleX ?? read.scaleX,
|
|
15595
|
+
scaleY: this.typed.scaleY ?? read.scaleY,
|
|
15596
|
+
rotate: this.typed.rotate ?? read.rotate,
|
|
15597
|
+
skewX: this.typed.skewX ?? read.skewX,
|
|
15598
|
+
skewY: this.typed.skewY ?? 0
|
|
15599
|
+
};
|
|
15600
|
+
}
|
|
15601
|
+
/** Applies a typed options-bar value (W and H together while `proportional`). */
|
|
15602
|
+
setField(field, value) {
|
|
15603
|
+
const s = this.session;
|
|
15604
|
+
const cur = this.bar();
|
|
15605
|
+
if (!s || !cur || !Number.isFinite(value)) return;
|
|
15606
|
+
const next = { ...cur, [field]: value };
|
|
15607
|
+
if (this.proportional && field === "scaleX") next.scaleY = cur.scaleY === 0 ? value : value / cur.scaleX * cur.scaleY;
|
|
15608
|
+
if (this.proportional && field === "scaleY") next.scaleX = cur.scaleX === 0 ? value : value / cur.scaleY * cur.scaleX;
|
|
15609
|
+
if (!next.scaleX || !next.scaleY) {
|
|
15610
|
+
this.error = "Width and height must not be 0 %.";
|
|
15611
|
+
this.changed();
|
|
15612
|
+
return;
|
|
15613
|
+
}
|
|
15614
|
+
const m = matrixFromParams(s.box, {
|
|
15615
|
+
scaleX: next.scaleX,
|
|
15616
|
+
scaleY: next.scaleY,
|
|
15617
|
+
rotate: next.rotate,
|
|
15618
|
+
skewX: next.skewX,
|
|
15619
|
+
skewY: next.skewY,
|
|
15620
|
+
reference: this.refBox,
|
|
15621
|
+
dx: next.x - this.refBox.x,
|
|
15622
|
+
dy: next.y - this.refBox.y
|
|
15623
|
+
});
|
|
15624
|
+
this.setCorners(cornersFromMatrix(s.box, m));
|
|
15625
|
+
this.typed = { scaleX: next.scaleX, scaleY: next.scaleY, rotate: next.rotate, skewX: next.skewX, skewY: next.skewY, x: next.x, y: next.y };
|
|
15626
|
+
this.changed();
|
|
15627
|
+
}
|
|
15628
|
+
/** Finishes with full-quality resampling (one undo step). */
|
|
15629
|
+
commit() {
|
|
15630
|
+
const s = this.session;
|
|
15631
|
+
if (!s) return null;
|
|
15632
|
+
let res = null;
|
|
15633
|
+
this.pending = false;
|
|
15634
|
+
this.run(() => {
|
|
15635
|
+
if (this.corners) s.update({ corners: this.corners, warp: this.mesh }, false);
|
|
15636
|
+
res = s.commit();
|
|
15637
|
+
});
|
|
15638
|
+
this.lastResult = res;
|
|
15639
|
+
this.sessionRef = null;
|
|
15640
|
+
this.corners = null;
|
|
15641
|
+
this.mesh = null;
|
|
15642
|
+
this.drag = null;
|
|
15643
|
+
this.changed();
|
|
15644
|
+
return res;
|
|
15645
|
+
}
|
|
15646
|
+
/** Abandons the transform and restores everything. */
|
|
15647
|
+
cancel() {
|
|
15648
|
+
this.sessionRef?.cancel();
|
|
15649
|
+
this.sessionRef = null;
|
|
15650
|
+
this.corners = null;
|
|
15651
|
+
this.mesh = null;
|
|
15652
|
+
this.drag = null;
|
|
15653
|
+
this.pending = false;
|
|
15654
|
+
this.error = null;
|
|
15655
|
+
this.changed();
|
|
15656
|
+
}
|
|
15657
|
+
/**
|
|
15658
|
+
* Keyboard: Enter commits, Escape cancels, arrows nudge by 1 px (Shift: 10). Ctrl/Cmd+T opens a
|
|
15659
|
+
* Free Transform when none is open. Returns true when the key was used (prevent its default).
|
|
15660
|
+
*/
|
|
15661
|
+
key(e) {
|
|
15662
|
+
const t = e.target;
|
|
15663
|
+
const typing = Boolean(t && (t.isContentEditable || t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.tagName === "SELECT"));
|
|
15664
|
+
if (!this.active) {
|
|
15665
|
+
if ((e.ctrlKey || e.metaKey) && !e.altKey && e.key.toLowerCase() === "t" && !typing) {
|
|
15666
|
+
this.begin();
|
|
15667
|
+
return true;
|
|
15668
|
+
}
|
|
15669
|
+
return false;
|
|
15670
|
+
}
|
|
15671
|
+
if (e.key === "Enter") {
|
|
15672
|
+
this.commit();
|
|
15673
|
+
return true;
|
|
15674
|
+
}
|
|
15675
|
+
if (e.key === "Escape") {
|
|
15676
|
+
this.cancel();
|
|
15677
|
+
return true;
|
|
15678
|
+
}
|
|
15679
|
+
if (typing) return false;
|
|
15680
|
+
const step = e.shiftKey ? 10 : 1;
|
|
15681
|
+
const nudge = { ArrowLeft: { x: -step, y: 0 }, ArrowRight: { x: step, y: 0 }, ArrowUp: { x: 0, y: -step }, ArrowDown: { x: 0, y: step } };
|
|
15682
|
+
const v = nudge[e.key];
|
|
15683
|
+
if (v && this.corners) {
|
|
15684
|
+
this.setCorners(this.corners.map((q2) => add(q2, v)));
|
|
15685
|
+
return true;
|
|
15686
|
+
}
|
|
15687
|
+
return false;
|
|
15688
|
+
}
|
|
15689
|
+
};
|
|
15055
15690
|
export {
|
|
15056
15691
|
ADJUSTMENT_KEYS,
|
|
15057
15692
|
ADJUSTMENT_KINDS,
|
|
@@ -15081,8 +15716,11 @@ export {
|
|
|
15081
15716
|
EMPTY_DISTANCE,
|
|
15082
15717
|
EMPTY_MIXER_LOAD,
|
|
15083
15718
|
FREE_TRANSFORM_CONTENTS,
|
|
15719
|
+
FREE_TRANSFORM_FIELDS,
|
|
15084
15720
|
FREE_TRANSFORM_MODES,
|
|
15721
|
+
FREE_TRANSFORM_MODE_OPTIONS,
|
|
15085
15722
|
FreeTransformSession,
|
|
15723
|
+
FreeTransformTool,
|
|
15086
15724
|
GUIDED_FILTER_EPS,
|
|
15087
15725
|
HISTOGRAM_BINS,
|
|
15088
15726
|
HUE_RANGE_LABELS,
|
|
@@ -15145,8 +15783,10 @@ export {
|
|
|
15145
15783
|
TiledSurface,
|
|
15146
15784
|
TransformTooLargeError,
|
|
15147
15785
|
VIBRANCE_GREY,
|
|
15786
|
+
WARP_GRID_OPTIONS,
|
|
15148
15787
|
WARP_STYLES,
|
|
15149
15788
|
WARP_STYLE_LABELS,
|
|
15789
|
+
WARP_STYLE_OPTIONS,
|
|
15150
15790
|
XYZ_D50_TO_SRGB,
|
|
15151
15791
|
ZlibError,
|
|
15152
15792
|
activeAdjustmentLayers,
|