@pixerate/editor 0.98.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.
|
@@ -3193,6 +3193,44 @@ 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
|
+
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
|
+
}
|
|
3196
3234
|
if (isActivelyCropping) {
|
|
3197
3235
|
const activeCrop = options.draftCrop !== void 0 && options.draftCrop !== null ? options.draftCrop : crop ?? { x: 0, y: 0, width: naturalWidth, height: naturalHeight };
|
|
3198
3236
|
this.drawCropOverlay(ctx, getViewportMetrics(geoState, vpWidth, vpHeight, true, activeCrop));
|
|
@@ -4001,7 +4039,7 @@ function removeAnnotationLayers(layers) {
|
|
|
4001
4039
|
function polygonCoverage(points, width, height, antiAlias = true) {
|
|
4002
4040
|
const m = new Float32Array(width * height);
|
|
4003
4041
|
if (points.length < 3 || width <= 0 || height <= 0) return m;
|
|
4004
|
-
const
|
|
4042
|
+
const sub2 = antiAlias ? 4 : 1;
|
|
4005
4043
|
const edges = [];
|
|
4006
4044
|
for (let i = 0; i < points.length; i++) {
|
|
4007
4045
|
const a = points[i];
|
|
@@ -4016,8 +4054,8 @@ function polygonCoverage(points, width, height, antiAlias = true) {
|
|
|
4016
4054
|
const xs = [];
|
|
4017
4055
|
for (let py = 0; py < height; py++) {
|
|
4018
4056
|
const row = py * width;
|
|
4019
|
-
for (let s = 0; s <
|
|
4020
|
-
const y = py + (s + 0.5) /
|
|
4057
|
+
for (let s = 0; s < sub2; s++) {
|
|
4058
|
+
const y = py + (s + 0.5) / sub2;
|
|
4021
4059
|
while (next < edges.length && edges[next].top <= y) active.push(edges[next++]);
|
|
4022
4060
|
if (active.length) active = active.filter((e) => e.bottom > y);
|
|
4023
4061
|
if (!active.length) continue;
|
|
@@ -4039,7 +4077,7 @@ function polygonCoverage(points, width, height, antiAlias = true) {
|
|
|
4039
4077
|
const c = cx + 0.5;
|
|
4040
4078
|
cov = c >= x0 && c < x1 ? 1 : 0;
|
|
4041
4079
|
}
|
|
4042
|
-
if (cov > 0) m[row + cx] += cov /
|
|
4080
|
+
if (cov > 0) m[row + cx] += cov / sub2;
|
|
4043
4081
|
}
|
|
4044
4082
|
}
|
|
4045
4083
|
}
|
|
@@ -11180,7 +11218,7 @@ var ImageEditorController = class {
|
|
|
11180
11218
|
const canvas = { x: 0, y: 0, width, height };
|
|
11181
11219
|
const warnings = [];
|
|
11182
11220
|
const note = /* @__PURE__ */ new Map();
|
|
11183
|
-
const
|
|
11221
|
+
const add2 = (msg, name) => note.set(msg, [...note.get(msg) ?? [], name]);
|
|
11184
11222
|
const canRasterize = this.renderer.canRasterize();
|
|
11185
11223
|
await this.renderer.prepareLayerAssets(this.state.layers, width, height);
|
|
11186
11224
|
const sources = this.renderer.compositorSources(width, height);
|
|
@@ -11188,7 +11226,7 @@ var ImageEditorController = class {
|
|
|
11188
11226
|
const bakeMask = (layer) => {
|
|
11189
11227
|
const mask = layer.mask;
|
|
11190
11228
|
if (!mask?.source) return mask;
|
|
11191
|
-
|
|
11229
|
+
add2("Parametric (depth) masks were baked into pixel masks", layer.name);
|
|
11192
11230
|
const plain3 = { ...layer, mask: { ...mask, density: 1, feather: 0, enabled: true } };
|
|
11193
11231
|
const ox = layer.type === "pixel" ? layer.offset.x : 0;
|
|
11194
11232
|
const oy = layer.type === "pixel" ? layer.offset.y : 0;
|
|
@@ -11204,7 +11242,7 @@ var ImageEditorController = class {
|
|
|
11204
11242
|
};
|
|
11205
11243
|
const prepare = async (layer) => {
|
|
11206
11244
|
const p = await prepareLayer(layer);
|
|
11207
|
-
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);
|
|
11208
11246
|
return p;
|
|
11209
11247
|
};
|
|
11210
11248
|
const prepareLayer = async (layer) => {
|
|
@@ -11219,10 +11257,10 @@ var ImageEditorController = class {
|
|
|
11219
11257
|
if (layer.type === "adjustment") return { ...layer, mask: bakeMask(layer) };
|
|
11220
11258
|
if (layer.type === "annotation") {
|
|
11221
11259
|
if (!canRasterize) {
|
|
11222
|
-
|
|
11260
|
+
add2("Annotation layers need a canvas (a browser) to rasterize and were left out", layer.name);
|
|
11223
11261
|
return null;
|
|
11224
11262
|
}
|
|
11225
|
-
|
|
11263
|
+
add2("Annotation layers were written as pixel layers", layer.name);
|
|
11226
11264
|
const blend = layer.blend === "normal" && layer.annotation.blend ? layer.annotation.blend : layer.blend;
|
|
11227
11265
|
const content = { ...layer, visible: true, opacity: 1, blend: "normal", annotation: { ...layer.annotation, blend: void 0 } };
|
|
11228
11266
|
delete content.mask;
|
|
@@ -11253,7 +11291,7 @@ var ImageEditorController = class {
|
|
|
11253
11291
|
}
|
|
11254
11292
|
const active = layer.filters?.filter(isActiveFilter) ?? [];
|
|
11255
11293
|
if (active.length) {
|
|
11256
|
-
|
|
11294
|
+
add2("Layer filters (develop, blur) were baked into the pixels", layer.name);
|
|
11257
11295
|
const content = { ...out, visible: true, opacity: 1, blend: "normal" };
|
|
11258
11296
|
delete content.mask;
|
|
11259
11297
|
delete content.clipped;
|
|
@@ -11262,7 +11300,7 @@ var ImageEditorController = class {
|
|
|
11262
11300
|
const data = compositor.composite([content], canvas, sources);
|
|
11263
11301
|
out = { ...out, surface: surfaceFromPixels({ width, height, data }, 4, -layer.offset.x, -layer.offset.y) };
|
|
11264
11302
|
}
|
|
11265
|
-
if (layer.filters?.some((f) => !f.enabled))
|
|
11303
|
+
if (layer.filters?.some((f) => !f.enabled)) add2("Disabled layer filters were left out", layer.name);
|
|
11266
11304
|
delete out.filters;
|
|
11267
11305
|
const mask = bakeMask(layer);
|
|
11268
11306
|
if (mask) out.mask = mask;
|
|
@@ -13416,7 +13454,7 @@ function createImageEffectTools(controller) {
|
|
|
13416
13454
|
annotations: { title: "List layer effects", ...READ3 },
|
|
13417
13455
|
handler: ({ layerId }) => result(layerArg(layerId), { globalLight: controller.getGlobalLight(), patterns: patternList(controller), kinds: [...LAYER_EFFECT_KINDS] })
|
|
13418
13456
|
};
|
|
13419
|
-
const
|
|
13457
|
+
const add2 = {
|
|
13420
13458
|
name: "add_layer_effect",
|
|
13421
13459
|
title: "Add a layer effect",
|
|
13422
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.",
|
|
@@ -13514,7 +13552,7 @@ function createImageEffectTools(controller) {
|
|
|
13514
13552
|
return { globalLight: controller.setGlobalLight(next) };
|
|
13515
13553
|
}
|
|
13516
13554
|
};
|
|
13517
|
-
return [list2,
|
|
13555
|
+
return [list2, add2, update, remove, setEnabled, light];
|
|
13518
13556
|
}
|
|
13519
13557
|
|
|
13520
13558
|
// src/image-editor/layers/paint-ui.ts
|
|
@@ -14779,6 +14817,7 @@ function layerMenu(controller) {
|
|
|
14779
14817
|
{ action: "apply-mask", label: "Apply Layer Mask", enabled: isPixel && Boolean(active?.mask) },
|
|
14780
14818
|
{ action: "delete-mask", label: "Delete Layer Mask", enabled: Boolean(active?.mask) },
|
|
14781
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) },
|
|
14782
14821
|
{ action: "rasterize", label: "Rasterize Layer", enabled: active?.type === "annotation" || isPixel && Boolean(active?.type === "pixel" && active.filters?.length) },
|
|
14783
14822
|
{ action: "toggle-clip", label: active?.clipped ? "Release Clipping Mask" : "Create Clipping Mask", enabled: Boolean(loc && loc.index > 0) }
|
|
14784
14823
|
];
|
|
@@ -14834,6 +14873,9 @@ async function runLayerMenuAction(controller, action) {
|
|
|
14834
14873
|
case "toggle-mask":
|
|
14835
14874
|
if (id && active?.mask) controller.setLayerMaskProps(id, { enabled: active.mask.enabled === false });
|
|
14836
14875
|
break;
|
|
14876
|
+
case "toggle-mask-link":
|
|
14877
|
+
if (id && active?.mask) controller.setLayerMaskProps(id, { linked: active.mask.linked === false });
|
|
14878
|
+
break;
|
|
14837
14879
|
case "rasterize":
|
|
14838
14880
|
if (id) await controller.rasterizeLayer(id);
|
|
14839
14881
|
break;
|
|
@@ -15052,6 +15094,445 @@ async function downloadPsd(controller, name = "image") {
|
|
|
15052
15094
|
}
|
|
15053
15095
|
return { fileName, warnings: res.warnings };
|
|
15054
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
|
+
};
|
|
15055
15536
|
export {
|
|
15056
15537
|
ADJUSTMENT_KEYS,
|
|
15057
15538
|
ADJUSTMENT_KINDS,
|
|
@@ -15081,8 +15562,11 @@ export {
|
|
|
15081
15562
|
EMPTY_DISTANCE,
|
|
15082
15563
|
EMPTY_MIXER_LOAD,
|
|
15083
15564
|
FREE_TRANSFORM_CONTENTS,
|
|
15565
|
+
FREE_TRANSFORM_FIELDS,
|
|
15084
15566
|
FREE_TRANSFORM_MODES,
|
|
15567
|
+
FREE_TRANSFORM_MODE_OPTIONS,
|
|
15085
15568
|
FreeTransformSession,
|
|
15569
|
+
FreeTransformTool,
|
|
15086
15570
|
GUIDED_FILTER_EPS,
|
|
15087
15571
|
HISTOGRAM_BINS,
|
|
15088
15572
|
HUE_RANGE_LABELS,
|