@pixerate/editor 0.99.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.
@@ -3198,22 +3198,44 @@ var ImageEditorRenderer = class {
3198
3198
  ctx.save();
3199
3199
  try {
3200
3200
  const scr = (p) => projectImagePoint(p, projection);
3201
- const outline = t.outline.map(scr);
3202
3201
  ctx.beginPath();
3203
- outline.forEach((p, i) => i ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y));
3204
- ctx.closePath();
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
+ }
3205
3219
  ctx.lineWidth = 3;
3206
3220
  ctx.strokeStyle = "rgba(0,0,0,0.5)";
3207
3221
  ctx.stroke();
3208
3222
  ctx.lineWidth = 1;
3209
3223
  ctx.strokeStyle = "#ffffff";
3210
3224
  ctx.stroke();
3211
- for (const h of t.handles.map(scr)) {
3225
+ for (const h of [...t.handles, ...t.warp?.anchors ?? []].map(scr)) {
3212
3226
  ctx.fillStyle = "#ffffff";
3213
3227
  ctx.strokeStyle = "#1e293b";
3214
3228
  ctx.fillRect(h.x - 4, h.y - 4, 8, 8);
3215
3229
  ctx.strokeRect(h.x - 4, h.y - 4, 8, 8);
3216
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
+ }
3217
3239
  const r = scr(t.reference);
3218
3240
  ctx.beginPath();
3219
3241
  ctx.arc(r.x, r.y, 5, 0, Math.PI * 2);
@@ -15117,7 +15139,17 @@ var FREE_TRANSFORM_MODE_OPTIONS = [
15117
15139
  { id: "rotate", label: "Rotate", title: "Every handle rotates about the reference point" },
15118
15140
  { id: "skew", label: "Skew", title: "Sides slide along themselves" },
15119
15141
  { id: "distort", label: "Distort", title: "Each corner moves freely" },
15120
- { id: "perspective", label: "Perspective", title: "A corner and its partner move symmetrically" }
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 }
15121
15153
  ];
15122
15154
  var FREE_TRANSFORM_FIELDS = [
15123
15155
  { id: "x", label: "X", unit: "px", title: "Reference point X (document pixels)" },
@@ -15141,6 +15173,7 @@ var CURSOR = {
15141
15173
  inside: "move",
15142
15174
  outside: "alias"
15143
15175
  };
15176
+ var meshIndex = (h) => h && h.startsWith("mesh:") ? Number(h.slice(5)) : -1;
15144
15177
  var mod = (m) => Boolean(m.ctrlKey || m.metaKey);
15145
15178
  var add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
15146
15179
  var sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
@@ -15163,6 +15196,15 @@ function insideQuad(q2, p) {
15163
15196
  }
15164
15197
  return true;
15165
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
+ }
15166
15208
  var FreeTransformTool = class {
15167
15209
  constructor(controller, options = {}) {
15168
15210
  this.controller = controller;
@@ -15176,6 +15218,9 @@ var FreeTransformTool = class {
15176
15218
  refBox = { x: 0, y: 0 };
15177
15219
  corners = null;
15178
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 };
15179
15224
  pending = false;
15180
15225
  viewScale = 1;
15181
15226
  viewClip;
@@ -15222,6 +15267,8 @@ var FreeTransformTool = class {
15222
15267
  this.corners = s.geometry.corners;
15223
15268
  this.refBox = { x: s.box.x + s.box.width / 2, y: s.box.y + s.box.height / 2 };
15224
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 };
15225
15272
  this.typed = {};
15226
15273
  this.error = null;
15227
15274
  } catch (e) {
@@ -15232,7 +15279,39 @@ var FreeTransformTool = class {
15232
15279
  }
15233
15280
  setMode(mode) {
15234
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;
15235
15313
  this.changed();
15314
+ this.preview();
15236
15315
  }
15237
15316
  /** Sets the reference point to an anchor of the frame (the options bar's locator). */
15238
15317
  setReference(anchor) {
@@ -15271,12 +15350,54 @@ var FreeTransformTool = class {
15271
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 };
15272
15351
  return { id, x: p.x, y: p.y };
15273
15352
  });
15274
- return { outline: this.corners.map((p) => ({ x: p.x, y: p.y })), handles, reference: this.referencePoint() ?? { x: 0, y: 0 } };
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;
15275
15379
  }
15276
15380
  /** What a document point is over (`tolerance` in document pixels: a few screen pixels / scale). */
15277
15381
  hit(p, tolerance) {
15278
15382
  const o = this.overlay();
15279
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
+ }
15280
15401
  let best = null;
15281
15402
  let bestD = tolerance;
15282
15403
  for (const h of o.handles) {
@@ -15293,6 +15414,8 @@ var FreeTransformTool = class {
15293
15414
  /** The CSS cursor for a handle. */
15294
15415
  cursor(handle) {
15295
15416
  if (!handle) return "default";
15417
+ if (meshIndex(handle) >= 0) return "pointer";
15418
+ if (this.modeValue === "warp") return handle === "inside" ? "grab" : "default";
15296
15419
  if (this.modeValue === "rotate" && handle !== "inside" && handle !== "reference") return CURSOR.outside;
15297
15420
  return CURSOR[handle];
15298
15421
  }
@@ -15301,9 +15424,24 @@ var FreeTransformTool = class {
15301
15424
  if (!this.session || !this.corners) return null;
15302
15425
  const handle = this.hit(p, tolerance);
15303
15426
  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 };
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 };
15305
15436
  return handle;
15306
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
+ }
15307
15445
  /** Continues the drag to `p` with the modifiers held now. */
15308
15446
  move(p, m = {}) {
15309
15447
  const d = this.drag;
@@ -15317,6 +15455,20 @@ var FreeTransformTool = class {
15317
15455
  this.changed();
15318
15456
  return;
15319
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
+ }
15320
15472
  const corners = this.dragCorners(d, p, m);
15321
15473
  if (corners) this.setCorners(corners);
15322
15474
  }
@@ -15415,7 +15567,7 @@ var FreeTransformTool = class {
15415
15567
  this.pending = false;
15416
15568
  const s = this.session;
15417
15569
  if (!s || !this.corners) return;
15418
- this.run(() => s.update({ corners: this.corners }, { scale: this.viewScale, clip: this.viewClip }));
15570
+ this.run(() => s.update({ corners: this.corners, warp: this.mesh }, { scale: this.viewScale, clip: this.viewClip }));
15419
15571
  };
15420
15572
  if (this.options.schedule) this.options.schedule(run);
15421
15573
  else run();
@@ -15480,12 +15632,13 @@ var FreeTransformTool = class {
15480
15632
  let res = null;
15481
15633
  this.pending = false;
15482
15634
  this.run(() => {
15483
- if (this.corners) s.update({ corners: this.corners }, false);
15635
+ if (this.corners) s.update({ corners: this.corners, warp: this.mesh }, false);
15484
15636
  res = s.commit();
15485
15637
  });
15486
15638
  this.lastResult = res;
15487
15639
  this.sessionRef = null;
15488
15640
  this.corners = null;
15641
+ this.mesh = null;
15489
15642
  this.drag = null;
15490
15643
  this.changed();
15491
15644
  return res;
@@ -15495,6 +15648,7 @@ var FreeTransformTool = class {
15495
15648
  this.sessionRef?.cancel();
15496
15649
  this.sessionRef = null;
15497
15650
  this.corners = null;
15651
+ this.mesh = null;
15498
15652
  this.drag = null;
15499
15653
  this.pending = false;
15500
15654
  this.error = null;
@@ -15629,8 +15783,10 @@ export {
15629
15783
  TiledSurface,
15630
15784
  TransformTooLargeError,
15631
15785
  VIBRANCE_GREY,
15786
+ WARP_GRID_OPTIONS,
15632
15787
  WARP_STYLES,
15633
15788
  WARP_STYLE_LABELS,
15789
+ WARP_STYLE_OPTIONS,
15634
15790
  XYZ_D50_TO_SRGB,
15635
15791
  ZlibError,
15636
15792
  activeAdjustmentLayers,