@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 sub = antiAlias ? 4 : 1;
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 < sub; s++) {
4020
- const y = py + (s + 0.5) / sub;
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 / sub;
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 add = (msg, name) => note.set(msg, [...note.get(msg) ?? [], name]);
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
- add("Parametric (depth) masks were baked into pixel masks", layer.name);
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") add("The Pigment blend mode has no PSD equivalent and was written as Normal", layer.name);
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
- add("Annotation layers need a canvas (a browser) to rasterize and were left out", layer.name);
11282
+ add2("Annotation layers need a canvas (a browser) to rasterize and were left out", layer.name);
11223
11283
  return null;
11224
11284
  }
11225
- add("Annotation layers were written as pixel layers", layer.name);
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
- add("Layer filters (develop, blur) were baked into the pixels", layer.name);
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)) add("Disabled layer filters were left out", layer.name);
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 add = {
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, add, update, remove, setEnabled, light];
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,