@pixerate/editor 0.18.0 → 0.20.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.
Files changed (118) hide show
  1. package/README.md +91 -2
  2. package/dist/agent/index.cjs +625 -2
  3. package/dist/agent/index.cjs.map +1 -1
  4. package/dist/agent/index.d.cts +286 -2
  5. package/dist/agent/index.d.ts +286 -2
  6. package/dist/agent/index.js +624 -1
  7. package/dist/agent/index.js.map +1 -1
  8. package/dist/canvas/index.cjs +1240 -22
  9. package/dist/canvas/index.cjs.map +1 -1
  10. package/dist/canvas/index.d.cts +515 -3
  11. package/dist/canvas/index.d.ts +515 -3
  12. package/dist/canvas/index.js +1219 -1
  13. package/dist/canvas/index.js.map +1 -1
  14. package/dist/chunk-3TYMNULT.js +1367 -0
  15. package/dist/chunk-3TYMNULT.js.map +1 -0
  16. package/dist/chunk-4QPLEUL2.cjs +4480 -0
  17. package/dist/chunk-4QPLEUL2.cjs.map +1 -0
  18. package/dist/chunk-4SUNJZWB.cjs +553 -0
  19. package/dist/chunk-4SUNJZWB.cjs.map +1 -0
  20. package/dist/chunk-A7DKIA72.js +2657 -0
  21. package/dist/chunk-A7DKIA72.js.map +1 -0
  22. package/dist/chunk-AXV2ZW34.cjs +118 -0
  23. package/dist/chunk-AXV2ZW34.cjs.map +1 -0
  24. package/dist/chunk-B2ZRLN4S.js +118 -0
  25. package/dist/chunk-B2ZRLN4S.js.map +1 -0
  26. package/dist/chunk-CYFJCKC4.js +4480 -0
  27. package/dist/chunk-CYFJCKC4.js.map +1 -0
  28. package/dist/chunk-GVW4P7BG.js +2706 -0
  29. package/dist/chunk-GVW4P7BG.js.map +1 -0
  30. package/dist/chunk-JACER6TE.cjs +1 -0
  31. package/dist/chunk-JACER6TE.cjs.map +1 -0
  32. package/dist/chunk-JYRIOFEN.cjs +2657 -0
  33. package/dist/chunk-JYRIOFEN.cjs.map +1 -0
  34. package/dist/chunk-KYRZDWM4.js +299 -0
  35. package/dist/chunk-KYRZDWM4.js.map +1 -0
  36. package/dist/chunk-LA7PEGU3.cjs +299 -0
  37. package/dist/chunk-LA7PEGU3.cjs.map +1 -0
  38. package/dist/chunk-N7FG6CBO.js +1 -0
  39. package/dist/chunk-N7FG6CBO.js.map +1 -0
  40. package/dist/chunk-OYXSKQFR.js +553 -0
  41. package/dist/chunk-OYXSKQFR.js.map +1 -0
  42. package/dist/chunk-TVRC4KVT.cjs +1367 -0
  43. package/dist/chunk-TVRC4KVT.cjs.map +1 -0
  44. package/dist/chunk-U6NDQFOK.cjs +2706 -0
  45. package/dist/chunk-U6NDQFOK.cjs.map +1 -0
  46. package/dist/controller-OUdxPY-X.d.cts +754 -0
  47. package/dist/controller-OUdxPY-X.d.ts +754 -0
  48. package/dist/extensions/index.cjs +144 -3
  49. package/dist/extensions/index.cjs.map +1 -1
  50. package/dist/extensions/index.d.cts +9 -144
  51. package/dist/extensions/index.d.ts +9 -144
  52. package/dist/extensions/index.js +145 -4
  53. package/dist/grammar/index.cjs +31 -3
  54. package/dist/grammar/index.cjs.map +1 -1
  55. package/dist/grammar/index.d.cts +239 -19
  56. package/dist/grammar/index.d.ts +239 -19
  57. package/dist/grammar/index.js +31 -3
  58. package/dist/history/index.d.cts +2 -41
  59. package/dist/history/index.d.ts +2 -41
  60. package/dist/image-editor/index.cjs +732 -497
  61. package/dist/image-editor/index.cjs.map +1 -1
  62. package/dist/image-editor/index.d.cts +105 -9
  63. package/dist/image-editor/index.d.ts +105 -9
  64. package/dist/image-editor/index.js +721 -486
  65. package/dist/image-editor/index.js.map +1 -1
  66. package/dist/index-Ng1pG7JE.d.ts +899 -0
  67. package/dist/index-k3mFbPx_.d.cts +899 -0
  68. package/dist/index.cjs +457 -20
  69. package/dist/index.cjs.map +1 -1
  70. package/dist/index.d.cts +99 -12
  71. package/dist/index.d.ts +99 -12
  72. package/dist/index.js +457 -20
  73. package/dist/index.js.map +1 -1
  74. package/dist/registry-iAHbqkYt.d.cts +128 -0
  75. package/dist/registry-iAHbqkYt.d.ts +128 -0
  76. package/dist/serializers/index.cjs +26 -3
  77. package/dist/serializers/index.cjs.map +1 -1
  78. package/dist/serializers/index.d.cts +119 -6
  79. package/dist/serializers/index.d.ts +119 -6
  80. package/dist/serializers/index.js +24 -1
  81. package/dist/spreadsheet/index.cjs +122 -2
  82. package/dist/spreadsheet/index.cjs.map +1 -1
  83. package/dist/spreadsheet/index.d.cts +371 -313
  84. package/dist/spreadsheet/index.d.ts +371 -313
  85. package/dist/spreadsheet/index.js +127 -7
  86. package/dist/tokens-CyLZmIei.d.cts +76 -0
  87. package/dist/tokens-CyLZmIei.d.ts +76 -0
  88. package/dist/track-changes-G8JcWB8b.d.cts +306 -0
  89. package/dist/track-changes-G8JcWB8b.d.ts +306 -0
  90. package/dist/types-BZgcDsC5.d.cts +42 -0
  91. package/dist/types-BZgcDsC5.d.ts +42 -0
  92. package/dist/{types-JV3S8VeR.d.cts → types-DMe0vFB1.d.cts} +1 -1
  93. package/dist/{types-JV3S8VeR.d.ts → types-DMe0vFB1.d.ts} +1 -1
  94. package/package.json +1 -1
  95. package/dist/chunk-7FWEWE6T.js +0 -1888
  96. package/dist/chunk-7FWEWE6T.js.map +0 -1
  97. package/dist/chunk-AIZUKWTL.js +0 -1715
  98. package/dist/chunk-AIZUKWTL.js.map +0 -1
  99. package/dist/chunk-GLSIZKOX.js +0 -17
  100. package/dist/chunk-GLSIZKOX.js.map +0 -1
  101. package/dist/chunk-LDES3EOS.cjs +0 -1888
  102. package/dist/chunk-LDES3EOS.cjs.map +0 -1
  103. package/dist/chunk-LLUAKTLW.cjs +0 -532
  104. package/dist/chunk-LLUAKTLW.cjs.map +0 -1
  105. package/dist/chunk-MPPXH7U5.cjs +0 -336
  106. package/dist/chunk-MPPXH7U5.cjs.map +0 -1
  107. package/dist/chunk-O7LOPQMH.js +0 -336
  108. package/dist/chunk-O7LOPQMH.js.map +0 -1
  109. package/dist/chunk-QGVMBCT5.js +0 -532
  110. package/dist/chunk-QGVMBCT5.js.map +0 -1
  111. package/dist/chunk-SEDR75VH.cjs +0 -17
  112. package/dist/chunk-SEDR75VH.cjs.map +0 -1
  113. package/dist/chunk-YQCI4CCN.cjs +0 -1715
  114. package/dist/chunk-YQCI4CCN.cjs.map +0 -1
  115. package/dist/mention-Dcevh3ff.d.cts +0 -32
  116. package/dist/mention-Dcevh3ff.d.ts +0 -32
  117. package/dist/tokens-7kRTEtPb.d.cts +0 -39
  118. package/dist/tokens-7kRTEtPb.d.ts +0 -39
@@ -148,6 +148,349 @@ function removeAlpha(imageData) {
148
148
  }
149
149
  }
150
150
 
151
+ // src/image-editor/geometry.ts
152
+ function normalizeAngle(degrees) {
153
+ const a = (degrees % 360 + 360) % 360;
154
+ return a === 360 ? 0 : a;
155
+ }
156
+ function cosSin(angle) {
157
+ switch (angle) {
158
+ case 0:
159
+ return [1, 0];
160
+ case 90:
161
+ return [0, 1];
162
+ case 180:
163
+ return [-1, 0];
164
+ case 270:
165
+ return [0, -1];
166
+ default: {
167
+ const rad = angle * Math.PI / 180;
168
+ return [Math.cos(rad), Math.sin(rad)];
169
+ }
170
+ }
171
+ }
172
+ function isQuarterTurnSwapped(transform) {
173
+ const angle = normalizeAngle(transform.rotate);
174
+ return angle === 90 || angle === 270;
175
+ }
176
+ function getRotatedSize(width, height, transform) {
177
+ const [c, s] = cosSin(normalizeAngle(transform.rotate));
178
+ return {
179
+ width: Math.abs(width * c) + Math.abs(height * s),
180
+ height: Math.abs(width * s) + Math.abs(height * c)
181
+ };
182
+ }
183
+ function projectImagePoint(pt, p) {
184
+ const [c, s] = cosSin(p.angle);
185
+ let x = pt.x - (p.region.x + p.region.width / 2);
186
+ let y = pt.y - (p.region.y + p.region.height / 2);
187
+ if (p.flipH) x = -x;
188
+ if (p.flipV) y = -y;
189
+ const rx = x * c - y * s;
190
+ const ry = x * s + y * c;
191
+ return { x: p.originX + rx * p.scale, y: p.originY + ry * p.scale };
192
+ }
193
+ function unprojectCanvasPoint(pt, p) {
194
+ const [c, s] = cosSin(p.angle);
195
+ const rx = (pt.x - p.originX) / p.scale;
196
+ const ry = (pt.y - p.originY) / p.scale;
197
+ let x = rx * c + ry * s;
198
+ let y = -rx * s + ry * c;
199
+ if (p.flipH) x = -x;
200
+ if (p.flipV) y = -y;
201
+ return { x: x + p.region.x + p.region.width / 2, y: y + p.region.y + p.region.height / 2 };
202
+ }
203
+ function applyImageProjectionToContext(ctx, p) {
204
+ ctx.translate(p.originX, p.originY);
205
+ ctx.scale(p.scale, p.scale);
206
+ if (p.angle !== 0) ctx.rotate(p.angle * Math.PI / 180);
207
+ if (p.flipH || p.flipV) ctx.scale(p.flipH ? -1 : 1, p.flipV ? -1 : 1);
208
+ ctx.translate(-(p.region.x + p.region.width / 2), -(p.region.y + p.region.height / 2));
209
+ }
210
+ function projectImageRect(rect, p) {
211
+ const corners = [
212
+ projectImagePoint({ x: rect.x, y: rect.y }, p),
213
+ projectImagePoint({ x: rect.x + rect.width, y: rect.y }, p),
214
+ projectImagePoint({ x: rect.x, y: rect.y + rect.height }, p),
215
+ projectImagePoint({ x: rect.x + rect.width, y: rect.y + rect.height }, p)
216
+ ];
217
+ const xs = corners.map((c) => c.x);
218
+ const ys = corners.map((c) => c.y);
219
+ const minX = Math.min(...xs);
220
+ const minY = Math.min(...ys);
221
+ return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
222
+ }
223
+ function clampCropToImage(crop, width, height) {
224
+ const x = Math.max(0, Math.min(width - 1, crop.x));
225
+ const y = Math.max(0, Math.min(height - 1, crop.y));
226
+ return {
227
+ x,
228
+ y,
229
+ width: Math.max(1, Math.min(width - x, crop.width)),
230
+ height: Math.max(1, Math.min(height - y, crop.height))
231
+ };
232
+ }
233
+ function resolveViewport(state, vpWidth, vpHeight, isActivelyCropping, cropOverride) {
234
+ const { imageDimensions, crop: stateCrop, transform, zoom, pan } = state;
235
+ const naturalWidth = imageDimensions.width || 800;
236
+ const naturalHeight = imageDimensions.height || 600;
237
+ const full = { x: 0, y: 0, width: naturalWidth, height: naturalHeight };
238
+ const activeCrop = cropOverride !== void 0 && cropOverride !== null ? cropOverride : stateCrop ?? (isActivelyCropping ? full : null);
239
+ const region = !isActivelyCropping && activeCrop ? clampCropToImage(activeCrop, naturalWidth, naturalHeight) : full;
240
+ const { width: baseW, height: baseH } = getRotatedSize(region.width, region.height, transform);
241
+ const fitScale = Math.min(vpWidth * 0.85 / baseW, vpHeight * 0.85 / baseH, 1);
242
+ const centerX = vpWidth / 2 + pan.x;
243
+ const centerY = vpHeight / 2 + pan.y;
244
+ const projection = {
245
+ region,
246
+ angle: normalizeAngle(transform.rotate),
247
+ flipH: transform.flipH,
248
+ flipV: transform.flipV,
249
+ scale: zoom * fitScale,
250
+ originX: centerX,
251
+ originY: centerY
252
+ };
253
+ return { activeCrop, baseW, baseH, fitScale, centerX, centerY, projection };
254
+ }
255
+ function getViewportProjection(state, vpWidth, vpHeight, isActivelyCropping = false, cropOverride) {
256
+ return resolveViewport(state, vpWidth, vpHeight, isActivelyCropping, cropOverride).projection;
257
+ }
258
+ function getViewportMetrics(state, vpWidth, vpHeight, isActivelyCropping = false, cropOverride) {
259
+ const { zoom } = state;
260
+ const { activeCrop, baseW, baseH, fitScale, centerX, centerY, projection } = resolveViewport(
261
+ state,
262
+ vpWidth,
263
+ vpHeight,
264
+ isActivelyCropping,
265
+ cropOverride
266
+ );
267
+ const renderW = baseW * fitScale;
268
+ const renderH = baseH * fitScale;
269
+ const imageRect = {
270
+ x: centerX - renderW * zoom / 2,
271
+ y: centerY - renderH * zoom / 2,
272
+ width: renderW * zoom,
273
+ height: renderH * zoom
274
+ };
275
+ const cropRect = activeCrop ? projectImageRect(activeCrop, projection) : null;
276
+ return {
277
+ vpWidth,
278
+ vpHeight,
279
+ centerX,
280
+ centerY,
281
+ baseW,
282
+ baseH,
283
+ fitScale,
284
+ renderW,
285
+ renderH,
286
+ totalScale: zoom * fitScale,
287
+ imageRect,
288
+ cropRect
289
+ };
290
+ }
291
+ function canvasToImagePoint(canvasPt, state, vpWidth, vpHeight, isActivelyCropping = false) {
292
+ return unprojectCanvasPoint(canvasPt, getViewportProjection(state, vpWidth, vpHeight, isActivelyCropping));
293
+ }
294
+ function imageToCanvasPoint(imgPt, state, vpWidth, vpHeight, isActivelyCropping = false) {
295
+ return projectImagePoint(imgPt, getViewportProjection(state, vpWidth, vpHeight, isActivelyCropping));
296
+ }
297
+ var HANDLE_VECTORS = {
298
+ nw: [-1, -1],
299
+ n: [0, -1],
300
+ ne: [1, -1],
301
+ e: [1, 0],
302
+ se: [1, 1],
303
+ s: [0, 1],
304
+ sw: [-1, 1],
305
+ w: [-1, 0]
306
+ };
307
+ function vectorToHandle(x, y) {
308
+ const vx = Math.abs(x) < 0.38 ? 0 : Math.sign(x);
309
+ const vy = Math.abs(y) < 0.38 ? 0 : Math.sign(y);
310
+ for (const [name, [hx, hy]] of Object.entries(HANDLE_VECTORS)) {
311
+ if (hx === vx && hy === vy) return name;
312
+ }
313
+ return null;
314
+ }
315
+ function mapCropHandle(handle, transform, toScreen = false) {
316
+ if (handle === null || handle === "inside") return handle;
317
+ const [hx, hy] = HANDLE_VECTORS[handle];
318
+ const [c, s] = cosSin(normalizeAngle(transform.rotate));
319
+ const fx = transform.flipH ? -1 : 1;
320
+ const fy = transform.flipV ? -1 : 1;
321
+ if (toScreen) {
322
+ const x2 = hx * fx;
323
+ const y2 = hy * fy;
324
+ return vectorToHandle(x2 * c - y2 * s, x2 * s + y2 * c);
325
+ }
326
+ const x = hx * c + hy * s;
327
+ const y = -hx * s + hy * c;
328
+ return vectorToHandle(x * fx, y * fy);
329
+ }
330
+ function hitTestCrop(canvasPt, state, vpWidth, vpHeight, handleThreshold = 14, cropOverride) {
331
+ const metrics = getViewportMetrics(state, vpWidth, vpHeight, true, cropOverride);
332
+ if (!metrics.cropRect) return null;
333
+ const screenHandle = hitTestCropRect(canvasPt, metrics.cropRect, handleThreshold);
334
+ return mapCropHandle(screenHandle, state.transform);
335
+ }
336
+ function hitTestCropRect(canvasPt, rect, handleThreshold) {
337
+ const { x, y, width: w, height: h } = rect;
338
+ const px = canvasPt.x;
339
+ const py = canvasPt.y;
340
+ const dist = (x1, y1, x2, y2) => Math.hypot(x1 - x2, y1 - y2);
341
+ if (dist(px, py, x, y) <= handleThreshold) return "nw";
342
+ if (dist(px, py, x + w, y) <= handleThreshold) return "ne";
343
+ if (dist(px, py, x + w, y + h) <= handleThreshold) return "se";
344
+ if (dist(px, py, x, y + h) <= handleThreshold) return "sw";
345
+ const nearX = px >= x - handleThreshold && px <= x + w + handleThreshold;
346
+ const nearY = py >= y - handleThreshold && py <= y + h + handleThreshold;
347
+ if (nearX && Math.abs(py - y) <= handleThreshold) return "n";
348
+ if (nearX && Math.abs(py - (y + h)) <= handleThreshold) return "s";
349
+ if (nearY && Math.abs(px - x) <= handleThreshold) return "w";
350
+ if (nearY && Math.abs(px - (x + w)) <= handleThreshold) return "e";
351
+ if (px >= x && px <= x + w && py >= y && py <= y + h) {
352
+ return "inside";
353
+ }
354
+ return null;
355
+ }
356
+ function getCropCursor(handle, transform) {
357
+ const screenHandle = transform ? mapCropHandle(handle, transform, true) : handle;
358
+ switch (screenHandle) {
359
+ case "nw":
360
+ case "se":
361
+ return "nwse-resize";
362
+ case "ne":
363
+ case "sw":
364
+ return "nesw-resize";
365
+ case "n":
366
+ case "s":
367
+ return "ns-resize";
368
+ case "e":
369
+ case "w":
370
+ return "ew-resize";
371
+ case "inside":
372
+ return "move";
373
+ default:
374
+ return "crosshair";
375
+ }
376
+ }
377
+ function calculateCropDrag(startCrop, handle, startImgPt, currentImgPt, imgWidth, imgHeight, aspectRatio, transform) {
378
+ if (aspectRatio && transform && isQuarterTurnSwapped(transform)) {
379
+ aspectRatio = 1 / aspectRatio;
380
+ }
381
+ const dx = currentImgPt.x - startImgPt.x;
382
+ const dy = currentImgPt.y - startImgPt.y;
383
+ if (handle === "inside") {
384
+ const maxX = Math.max(0, imgWidth - startCrop.width);
385
+ const maxY = Math.max(0, imgHeight - startCrop.height);
386
+ const newX = Math.max(0, Math.min(maxX, startCrop.x + dx));
387
+ const newY = Math.max(0, Math.min(maxY, startCrop.y + dy));
388
+ return {
389
+ x: Math.round(newX),
390
+ y: Math.round(newY),
391
+ width: startCrop.width,
392
+ height: startCrop.height
393
+ };
394
+ }
395
+ let left = startCrop.x;
396
+ let top = startCrop.y;
397
+ let right = startCrop.x + startCrop.width;
398
+ let bottom = startCrop.y + startCrop.height;
399
+ const minSize = 20;
400
+ if (handle === "e" || handle === "ne" || handle === "se") {
401
+ right = Math.min(imgWidth, Math.max(left + minSize, startCrop.x + startCrop.width + dx));
402
+ }
403
+ if (handle === "w" || handle === "nw" || handle === "sw") {
404
+ left = Math.max(0, Math.min(right - minSize, startCrop.x + dx));
405
+ }
406
+ if (handle === "s" || handle === "se" || handle === "sw") {
407
+ bottom = Math.min(imgHeight, Math.max(top + minSize, startCrop.y + startCrop.height + dy));
408
+ }
409
+ if (handle === "n" || handle === "nw" || handle === "ne") {
410
+ top = Math.max(0, Math.min(bottom - minSize, startCrop.y + dy));
411
+ }
412
+ if (aspectRatio && aspectRatio > 0) {
413
+ let currentW = right - left;
414
+ let currentH = bottom - top;
415
+ if (handle === "e" || handle === "w") {
416
+ currentH = Math.min(imgHeight, currentW / aspectRatio);
417
+ bottom = Math.min(imgHeight, top + currentH);
418
+ } else if (handle === "n" || handle === "s") {
419
+ currentW = Math.min(imgWidth, currentH * aspectRatio);
420
+ right = Math.min(imgWidth, left + currentW);
421
+ } else {
422
+ const targetH = currentW / aspectRatio;
423
+ if (top + targetH <= imgHeight) {
424
+ bottom = top + targetH;
425
+ } else {
426
+ const targetW = currentH * aspectRatio;
427
+ right = Math.min(imgWidth, left + targetW);
428
+ }
429
+ }
430
+ }
431
+ return {
432
+ x: Math.round(left),
433
+ y: Math.round(top),
434
+ width: Math.round(Math.max(minSize, right - left)),
435
+ height: Math.round(Math.max(minSize, bottom - top))
436
+ };
437
+ }
438
+ function getAnnotationBounds(ann) {
439
+ switch (ann.type) {
440
+ case "rect":
441
+ case "image":
442
+ return {
443
+ x: Math.min(ann.x, ann.x + ann.width),
444
+ y: Math.min(ann.y, ann.y + ann.height),
445
+ width: Math.abs(ann.width),
446
+ height: Math.abs(ann.height)
447
+ };
448
+ case "circle":
449
+ return {
450
+ x: ann.x - Math.abs(ann.radiusX),
451
+ y: ann.y - Math.abs(ann.radiusY),
452
+ width: Math.abs(ann.radiusX) * 2,
453
+ height: Math.abs(ann.radiusY) * 2
454
+ };
455
+ case "arrow":
456
+ case "line": {
457
+ const minX = Math.min(ann.x, ann.endX);
458
+ const minY = Math.min(ann.y, ann.endY);
459
+ return {
460
+ x: minX,
461
+ y: minY,
462
+ width: Math.max(Math.abs(ann.endX - ann.x), 10),
463
+ height: Math.max(Math.abs(ann.endY - ann.y), 10)
464
+ };
465
+ }
466
+ case "pen": {
467
+ if (!ann.points.length) return { x: ann.x, y: ann.y, width: 0, height: 0 };
468
+ let minX = ann.points[0].x;
469
+ let maxX = ann.points[0].x;
470
+ let minY = ann.points[0].y;
471
+ let maxY = ann.points[0].y;
472
+ for (const pt of ann.points) {
473
+ if (pt.x < minX) minX = pt.x;
474
+ if (pt.x > maxX) maxX = pt.x;
475
+ if (pt.y < minY) minY = pt.y;
476
+ if (pt.y > maxY) maxY = pt.y;
477
+ }
478
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
479
+ }
480
+ case "text":
481
+ return {
482
+ x: ann.x,
483
+ y: ann.y,
484
+ width: ann.text.length * ann.fontSize * 0.6,
485
+ height: ann.fontSize * 1.2
486
+ };
487
+ }
488
+ }
489
+ function hitTestAnnotation(imgPt, ann, padding = 10) {
490
+ const b = getAnnotationBounds(ann);
491
+ return imgPt.x >= b.x - padding && imgPt.x <= b.x + b.width + padding && imgPt.y >= b.y - padding && imgPt.y <= b.y + b.height + padding;
492
+ }
493
+
151
494
  // src/image-editor/renderer.ts
152
495
  var ImageCache = class {
153
496
  cache = /* @__PURE__ */ new Map();
@@ -195,6 +538,8 @@ var ImageEditorRenderer = class {
195
538
  imageCache = globalImageCache;
196
539
  cachedBaseCanvas = null;
197
540
  cachedBaseKey = "";
541
+ sourceIds = /* @__PURE__ */ new WeakMap();
542
+ nextSourceId = 0;
198
543
  /**
199
544
  * Helper to create a canvas element safely in browser/JSDOM.
200
545
  */
@@ -226,8 +571,8 @@ var ImageEditorRenderer = class {
226
571
  } else {
227
572
  img = imageSource;
228
573
  }
229
- const naturalWidth = img.naturalWidth || img.videoWidth || img.width || 800;
230
- const naturalHeight = img.naturalHeight || img.videoHeight || img.height || 600;
574
+ const { width: naturalWidth, height: naturalHeight } = getSourceSize(img);
575
+ const geoState = state.imageDimensions.width === naturalWidth && state.imageDimensions.height === naturalHeight ? state : { ...state, imageDimensions: { width: naturalWidth, height: naturalHeight } };
231
576
  const { crop, transform, adjustments, depthMask, annotations, selectedAnnotationId, zoom, pan, isOriginalCompared } = state;
232
577
  const vpWidth = options.viewportWidth || targetCanvas.width;
233
578
  const vpHeight = options.viewportHeight || targetCanvas.height;
@@ -239,93 +584,122 @@ var ImageEditorRenderer = class {
239
584
  return;
240
585
  }
241
586
  const isActivelyCropping = Boolean(options.showCropOverlay || state.activeTool === "crop");
242
- const cropX = !isActivelyCropping && crop ? Math.max(0, crop.x) : 0;
243
- const cropY = !isActivelyCropping && crop ? Math.max(0, crop.y) : 0;
244
- const cropW = !isActivelyCropping && crop ? Math.min(naturalWidth - cropX, crop.width) : naturalWidth;
245
- const cropH = !isActivelyCropping && crop ? Math.min(naturalHeight - cropY, crop.height) : naturalHeight;
246
- const angle = (transform.rotate % 360 + 360) % 360;
247
- const isSwapped = angle === 90 || angle === 270;
248
- const baseW = isSwapped ? cropH : cropW;
249
- const baseH = isSwapped ? cropW : cropH;
250
- const baseKey = `${state.sourceUrl}-${naturalWidth}x${naturalHeight}-${cropX},${cropY},${cropW},${cropH}-${angle},${transform.flipH},${transform.flipV}-${adjustments.brightness},${adjustments.contrast},${adjustments.saturation},${adjustments.exposure},${adjustments.temperature},${adjustments.blur},${adjustments.opacity}-${depthMask.enabled},${depthMask.depthRange?.[0]}-${depthMask.depthRange?.[1]},${depthMask.softness},${depthMask.invert},${depthMask.mode}`;
587
+ const projection = getViewportProjection(geoState, vpWidth, vpHeight, isActivelyCropping);
588
+ const { region } = projection;
589
+ const rotated = getRotatedSize(region.width, region.height, transform);
590
+ const baseKey = [
591
+ this.getSourceId(img),
592
+ state.sourceUrl,
593
+ `${naturalWidth}x${naturalHeight}`,
594
+ `${region.x},${region.y},${region.width},${region.height}`,
595
+ `${projection.angle},${transform.flipH},${transform.flipV}`,
596
+ `${adjustments.brightness},${adjustments.contrast},${adjustments.saturation},${adjustments.exposure},${adjustments.temperature},${adjustments.blur},${adjustments.opacity}`,
597
+ `${depthMask.enabled},${depthMask.maskSource},${depthMask.depthRange?.[0]}-${depthMask.depthRange?.[1]},${depthMask.softness},${depthMask.invert},${depthMask.mode}`
598
+ ].join("|");
251
599
  let baseCanvas = this.cachedBaseCanvas;
252
600
  if (!baseCanvas || this.cachedBaseKey !== baseKey) {
253
- baseCanvas = this.createCanvas(baseW, baseH);
254
- const baseCtx = baseCanvas.getContext("2d");
255
- if (baseCtx) {
256
- baseCtx.save();
257
- const rad = angle * Math.PI / 180;
258
- baseCtx.translate(baseW / 2, baseH / 2);
259
- baseCtx.rotate(rad);
260
- baseCtx.scale(transform.flipH ? -1 : 1, transform.flipV ? -1 : 1);
261
- baseCtx.drawImage(
262
- img,
263
- cropX,
264
- cropY,
265
- cropW,
266
- cropH,
267
- -cropW / 2,
268
- -cropH / 2,
269
- cropW,
270
- cropH
271
- );
272
- baseCtx.restore();
273
- const imageData = baseCtx.getImageData(0, 0, baseW, baseH);
274
- if (imageData) {
275
- applyAdjustments(imageData, adjustments);
276
- if (depthMask.enabled && depthMask.maskSource) {
277
- try {
278
- const maskImg = await this.imageCache.get(depthMask.maskSource);
279
- const maskCanvas = this.createCanvas(baseW, baseH);
280
- const maskCtx = maskCanvas.getContext("2d");
281
- if (maskCtx) {
282
- maskCtx.drawImage(maskImg, cropX, cropY, cropW, cropH, 0, 0, baseW, baseH);
283
- const maskData = maskCtx.getImageData(0, 0, baseW, baseH);
284
- if (maskData) {
285
- applyDepthMask(imageData, maskData, depthMask);
286
- }
287
- }
288
- } catch {
289
- }
290
- }
291
- baseCtx.putImageData(imageData, 0, 0);
292
- }
293
- }
601
+ baseCanvas = await this.renderBaseImage(img, naturalWidth, naturalHeight, region, transform, state);
294
602
  this.cachedBaseCanvas = baseCanvas;
295
603
  this.cachedBaseKey = baseKey;
296
604
  }
297
- ctx.save();
298
- ctx.translate(vpWidth / 2 + pan.x, vpHeight / 2 + pan.y);
299
- ctx.scale(zoom, zoom);
300
- const fitScale = Math.min(vpWidth * 0.85 / baseW, vpHeight * 0.85 / baseH, 1);
301
- const renderW = baseW * fitScale;
302
- const renderH = baseH * fitScale;
303
- ctx.drawImage(baseCanvas, -renderW / 2, -renderH / 2, renderW, renderH);
304
- const scaleFactor = fitScale;
305
- ctx.save();
306
- ctx.translate(-renderW / 2, -renderH / 2);
307
- ctx.scale(scaleFactor, scaleFactor);
308
- for (const ann of annotations) {
309
- await this.drawAnnotation(ctx, ann);
310
- if (options.showSelectionOverlay && ann.id === selectedAnnotationId) {
311
- this.drawAnnotationSelection(ctx, ann);
605
+ const drawW = rotated.width * projection.scale;
606
+ const drawH = rotated.height * projection.scale;
607
+ ctx.drawImage(baseCanvas, projection.originX - drawW / 2, projection.originY - drawH / 2, drawW, drawH);
608
+ if (annotations.length > 0 || options.draftAnnotation) {
609
+ ctx.save();
610
+ applyImageProjectionToContext(ctx, projection);
611
+ ctx.beginPath();
612
+ ctx.rect(region.x, region.y, region.width, region.height);
613
+ ctx.clip();
614
+ for (const ann of annotations) {
615
+ await this.drawAnnotation(ctx, ann);
616
+ if (options.showSelectionOverlay && ann.id === selectedAnnotationId) {
617
+ this.drawAnnotationSelection(ctx, ann);
618
+ }
312
619
  }
620
+ if (options.draftAnnotation) {
621
+ await this.drawAnnotation(ctx, options.draftAnnotation);
622
+ }
623
+ ctx.restore();
313
624
  }
314
- if (options.draftAnnotation) {
315
- await this.drawAnnotation(ctx, options.draftAnnotation);
625
+ if (isActivelyCropping) {
626
+ const activeCrop = options.draftCrop !== void 0 && options.draftCrop !== null ? options.draftCrop : crop ?? { x: 0, y: 0, width: naturalWidth, height: naturalHeight };
627
+ this.drawCropOverlay(ctx, getViewportMetrics(geoState, vpWidth, vpHeight, true, activeCrop));
316
628
  }
317
- ctx.restore();
318
- ctx.restore();
319
- const isCroppingActive = Boolean(options.showCropOverlay || state.activeTool === "crop");
320
- const activeCrop = options.draftCrop !== void 0 && options.draftCrop !== null ? options.draftCrop : crop ?? (isCroppingActive ? {
321
- x: 0,
322
- y: 0,
323
- width: naturalWidth,
324
- height: naturalHeight
325
- } : null);
326
- if (isCroppingActive && activeCrop) {
327
- this.drawCropOverlay(ctx, state, vpWidth, vpHeight, naturalWidth, naturalHeight, activeCrop);
629
+ }
630
+ /**
631
+ * Draws `region` of the source image with rotation/flip applied, then pixel adjustments
632
+ * and the depth mask (which receives the identical transform so it stays aligned).
633
+ */
634
+ async renderBaseImage(img, naturalWidth, naturalHeight, region, transform, state) {
635
+ const { adjustments, depthMask } = state;
636
+ const rotated = getRotatedSize(region.width, region.height, transform);
637
+ const baseW = Math.max(1, Math.round(rotated.width));
638
+ const baseH = Math.max(1, Math.round(rotated.height));
639
+ const baseCanvas = this.createCanvas(baseW, baseH);
640
+ const baseCtx = baseCanvas.getContext("2d");
641
+ if (!baseCtx) return baseCanvas;
642
+ const projection = getBaseProjection(region, transform, baseW, baseH);
643
+ baseCtx.save();
644
+ applyImageProjectionToContext(baseCtx, projection);
645
+ baseCtx.drawImage(
646
+ img,
647
+ region.x,
648
+ region.y,
649
+ region.width,
650
+ region.height,
651
+ region.x,
652
+ region.y,
653
+ region.width,
654
+ region.height
655
+ );
656
+ baseCtx.restore();
657
+ const imageData = baseCtx.getImageData(0, 0, baseW, baseH);
658
+ if (imageData) {
659
+ applyAdjustments(imageData, adjustments);
660
+ if (depthMask.enabled && depthMask.maskSource) {
661
+ try {
662
+ const maskImg = await this.imageCache.get(depthMask.maskSource);
663
+ const maskSize = getSourceSize(maskImg, naturalWidth, naturalHeight);
664
+ const sx = maskSize.width / naturalWidth;
665
+ const sy = maskSize.height / naturalHeight;
666
+ const maskCanvas = this.createCanvas(baseW, baseH);
667
+ const maskCtx = maskCanvas.getContext("2d");
668
+ if (maskCtx) {
669
+ maskCtx.save();
670
+ applyImageProjectionToContext(maskCtx, projection);
671
+ maskCtx.drawImage(
672
+ maskImg,
673
+ region.x * sx,
674
+ region.y * sy,
675
+ region.width * sx,
676
+ region.height * sy,
677
+ region.x,
678
+ region.y,
679
+ region.width,
680
+ region.height
681
+ );
682
+ maskCtx.restore();
683
+ const maskData = maskCtx.getImageData(0, 0, baseW, baseH);
684
+ if (maskData) {
685
+ applyDepthMask(imageData, maskData, depthMask);
686
+ }
687
+ }
688
+ } catch {
689
+ }
690
+ }
691
+ baseCtx.putImageData(imageData, 0, 0);
692
+ }
693
+ return baseCanvas;
694
+ }
695
+ getSourceId(img) {
696
+ const key = img;
697
+ let id = this.sourceIds.get(key);
698
+ if (id === void 0) {
699
+ id = ++this.nextSourceId;
700
+ this.sourceIds.set(key, id);
328
701
  }
702
+ return id;
329
703
  }
330
704
  /**
331
705
  * Draws an individual annotation onto the canvas.
@@ -448,7 +822,7 @@ var ImageEditorRenderer = class {
448
822
  ctx.strokeStyle = "#3b82f6";
449
823
  ctx.lineWidth = 1.5;
450
824
  ctx.setLineDash([4, 4]);
451
- const bounds = this.getAnnotationBounds(ann);
825
+ const bounds = getAnnotationBounds(ann);
452
826
  const pad = 4;
453
827
  ctx.strokeRect(bounds.x - pad, bounds.y - pad, bounds.width + pad * 2, bounds.height + pad * 2);
454
828
  ctx.fillStyle = "#ffffff";
@@ -467,82 +841,31 @@ var ImageEditorRenderer = class {
467
841
  }
468
842
  ctx.restore();
469
843
  }
844
+ /** @deprecated Use the standalone `getAnnotationBounds` from the geometry module. */
470
845
  getAnnotationBounds(ann) {
471
- switch (ann.type) {
472
- case "rect":
473
- return { x: ann.x, y: ann.y, width: ann.width, height: ann.height };
474
- case "circle":
475
- return {
476
- x: ann.x - Math.abs(ann.radiusX),
477
- y: ann.y - Math.abs(ann.radiusY),
478
- width: Math.abs(ann.radiusX) * 2,
479
- height: Math.abs(ann.radiusY) * 2
480
- };
481
- case "arrow":
482
- case "line": {
483
- const minX = Math.min(ann.x, ann.endX);
484
- const minY = Math.min(ann.y, ann.endY);
485
- return {
486
- x: minX,
487
- y: minY,
488
- width: Math.max(Math.abs(ann.endX - ann.x), 10),
489
- height: Math.max(Math.abs(ann.endY - ann.y), 10)
490
- };
491
- }
492
- case "pen": {
493
- if (!ann.points.length) return { x: ann.x, y: ann.y, width: 0, height: 0 };
494
- let minX = ann.points[0].x, maxX = ann.points[0].x;
495
- let minY = ann.points[0].y, maxY = ann.points[0].y;
496
- for (const pt of ann.points) {
497
- if (pt.x < minX) minX = pt.x;
498
- if (pt.x > maxX) maxX = pt.x;
499
- if (pt.y < minY) minY = pt.y;
500
- if (pt.y > maxY) maxY = pt.y;
501
- }
502
- return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
503
- }
504
- case "text":
505
- return {
506
- x: ann.x,
507
- y: ann.y,
508
- width: ann.text.length * ann.fontSize * 0.6,
509
- height: ann.fontSize * 1.2
510
- };
511
- case "image":
512
- return { x: ann.x, y: ann.y, width: ann.width, height: ann.height };
513
- }
846
+ return getAnnotationBounds(ann);
514
847
  }
515
848
  /**
516
- * Draws a rule-of-thirds crop grid overlay.
849
+ * Draws a rule-of-thirds crop grid overlay. All rectangles come from
850
+ * `getViewportMetrics`, i.e. the crop box projected through rotation/flip.
517
851
  */
518
- drawCropOverlay(ctx, state, vpWidth, vpHeight, imgWidth, imgHeight, cropOverride) {
519
- const crop = cropOverride !== void 0 && cropOverride !== null ? cropOverride : state.crop ?? {
520
- x: 0,
521
- y: 0,
522
- width: imgWidth,
523
- height: imgHeight
524
- };
525
- if (!crop) return;
526
- const { transform, pan, zoom } = state;
527
- const angle = (transform.rotate % 360 + 360) % 360;
528
- const isSwapped = angle === 90 || angle === 270;
529
- const baseW = isSwapped ? imgHeight : imgWidth;
530
- const baseH = isSwapped ? imgWidth : imgHeight;
852
+ drawCropOverlay(ctx, metrics) {
853
+ const { imageRect, cropRect } = metrics;
854
+ if (!cropRect) return;
855
+ const imgL = imageRect.x;
856
+ const imgT = imageRect.y;
857
+ const imgR = imageRect.x + imageRect.width;
858
+ const imgB = imageRect.y + imageRect.height;
859
+ const startX = cropRect.x;
860
+ const startY = cropRect.y;
861
+ const cropBoxW = cropRect.width;
862
+ const cropBoxH = cropRect.height;
531
863
  ctx.save();
532
- ctx.translate(vpWidth / 2 + pan.x, vpHeight / 2 + pan.y);
533
- ctx.scale(zoom, zoom);
534
- const fitScale = Math.min(vpWidth * 0.85 / baseW, vpHeight * 0.85 / baseH, 1);
535
- const renderW = baseW * fitScale;
536
- const renderH = baseH * fitScale;
537
- const startX = -renderW / 2 + crop.x / imgWidth * renderW;
538
- const startY = -renderH / 2 + crop.y / imgHeight * renderH;
539
- const cropBoxW = crop.width / imgWidth * renderW;
540
- const cropBoxH = crop.height / imgHeight * renderH;
541
864
  ctx.fillStyle = "rgba(0, 0, 0, 0.45)";
542
- ctx.fillRect(-renderW / 2, -renderH / 2, renderW, startY - -renderH / 2);
543
- ctx.fillRect(-renderW / 2, startY + cropBoxH, renderW, renderH / 2 - (startY + cropBoxH));
544
- ctx.fillRect(-renderW / 2, startY, startX - -renderW / 2, cropBoxH);
545
- ctx.fillRect(startX + cropBoxW, startY, renderW / 2 - (startX + cropBoxW), cropBoxH);
865
+ ctx.fillRect(imgL, imgT, imageRect.width, Math.max(0, startY - imgT));
866
+ ctx.fillRect(imgL, startY + cropBoxH, imageRect.width, Math.max(0, imgB - (startY + cropBoxH)));
867
+ ctx.fillRect(imgL, startY, Math.max(0, startX - imgL), cropBoxH);
868
+ ctx.fillRect(startX + cropBoxW, startY, Math.max(0, imgR - (startX + cropBoxW)), cropBoxH);
546
869
  ctx.strokeStyle = "#ffffff";
547
870
  ctx.lineWidth = 2;
548
871
  ctx.strokeRect(startX, startY, cropBoxW, cropBoxH);
@@ -600,59 +923,23 @@ var ImageEditorRenderer = class {
600
923
  } else {
601
924
  img = imageSource;
602
925
  }
603
- const naturalWidth = img.naturalWidth || img.videoWidth || img.width || 800;
604
- const naturalHeight = img.naturalHeight || img.videoHeight || img.height || 600;
605
- const { crop, transform, adjustments, depthMask, annotations } = state;
606
- const cropX = crop ? Math.max(0, crop.x) : 0;
607
- const cropY = crop ? Math.max(0, crop.y) : 0;
608
- const cropW = crop ? Math.min(naturalWidth - cropX, crop.width) : naturalWidth;
609
- const cropH = crop ? Math.min(naturalHeight - cropY, crop.height) : naturalHeight;
610
- const angle = (transform.rotate % 360 + 360) % 360;
611
- const isSwapped = angle === 90 || angle === 270;
612
- const baseW = isSwapped ? cropH : cropW;
613
- const baseH = isSwapped ? cropW : cropH;
926
+ const { width: naturalWidth, height: naturalHeight } = getSourceSize(img);
927
+ const { crop, transform, annotations } = state;
928
+ const region = crop ? clampCropToImage(crop, naturalWidth, naturalHeight) : { x: 0, y: 0, width: naturalWidth, height: naturalHeight };
929
+ const rotated = getRotatedSize(region.width, region.height, transform);
930
+ const baseW = Math.max(1, Math.round(rotated.width));
931
+ const baseH = Math.max(1, Math.round(rotated.height));
614
932
  const exportW = options.width || baseW;
615
933
  const exportH = options.height || baseH;
616
934
  const exportCanvas = this.createCanvas(exportW, exportH);
617
935
  const ctx = exportCanvas.getContext("2d");
618
936
  if (!ctx) return exportCanvas;
619
- const intermediateCanvas = this.createCanvas(baseW, baseH);
620
- const interCtx = intermediateCanvas.getContext("2d");
621
- if (interCtx) {
622
- interCtx.save();
623
- const rad = angle * Math.PI / 180;
624
- interCtx.translate(baseW / 2, baseH / 2);
625
- interCtx.rotate(rad);
626
- interCtx.scale(transform.flipH ? -1 : 1, transform.flipV ? -1 : 1);
627
- interCtx.drawImage(img, cropX, cropY, cropW, cropH, -cropW / 2, -cropH / 2, cropW, cropH);
628
- interCtx.restore();
629
- const imageData = interCtx.getImageData(0, 0, baseW, baseH);
630
- if (imageData) {
631
- applyAdjustments(imageData, adjustments);
632
- if (depthMask.enabled && depthMask.maskSource) {
633
- try {
634
- const maskImg = await this.imageCache.get(depthMask.maskSource);
635
- const maskCanvas = this.createCanvas(baseW, baseH);
636
- const maskCtx = maskCanvas.getContext("2d");
637
- if (maskCtx) {
638
- maskCtx.drawImage(maskImg, cropX, cropY, cropW, cropH, 0, 0, baseW, baseH);
639
- const maskData = maskCtx.getImageData(0, 0, baseW, baseH);
640
- if (maskData) {
641
- applyDepthMask(imageData, maskData, depthMask);
642
- }
643
- }
644
- } catch {
645
- }
646
- }
647
- interCtx.putImageData(imageData, 0, 0);
648
- }
649
- }
650
- ctx.drawImage(intermediateCanvas, 0, 0, baseW, baseH, 0, 0, exportW, exportH);
937
+ const baseCanvas = await this.renderBaseImage(img, naturalWidth, naturalHeight, region, transform, state);
938
+ ctx.drawImage(baseCanvas, 0, 0, baseW, baseH, 0, 0, exportW, exportH);
651
939
  if (options.includeAnnotations !== false && annotations.length > 0) {
652
- const annScaleX = exportW / baseW;
653
- const annScaleY = exportH / baseH;
654
940
  ctx.save();
655
- ctx.scale(annScaleX, annScaleY);
941
+ ctx.scale(exportW / baseW, exportH / baseH);
942
+ applyImageProjectionToContext(ctx, getBaseProjection(region, transform, baseW, baseH));
656
943
  for (const ann of annotations) {
657
944
  await this.drawAnnotation(ctx, ann);
658
945
  }
@@ -661,66 +948,114 @@ var ImageEditorRenderer = class {
661
948
  return exportCanvas;
662
949
  }
663
950
  /**
664
- * Generates a binary mask canvas (white for mask/selected area, black for unmasked).
665
- * Useful for AI inpainting and selective generative fill.
951
+ * Generates a strictly binary mask canvas (white = area to regenerate, black = keep) in
952
+ * natural image pixel space of the current source image — the same space the crop box
953
+ * and annotations are stored in. Pair it with `toSourceDataURL()` for AI inpainting.
666
954
  */
667
955
  async renderMask(state, options) {
668
- const naturalWidth = state.imageDimensions.width;
669
- const naturalHeight = state.imageDimensions.height;
956
+ const naturalWidth = Math.max(1, Math.round(state.imageDimensions.width));
957
+ const naturalHeight = Math.max(1, Math.round(state.imageDimensions.height));
670
958
  const maskCanvas = this.createCanvas(naturalWidth, naturalHeight);
671
959
  const ctx = maskCanvas.getContext("2d");
672
960
  if (!ctx) return maskCanvas;
673
- ctx.fillStyle = "#000000";
961
+ ctx.fillStyle = MASK_BLACK;
674
962
  ctx.fillRect(0, 0, naturalWidth, naturalHeight);
675
963
  const useCrop = options?.useCrop ?? true;
676
964
  const useAnnotations = options?.useAnnotations ?? true;
677
965
  if (useCrop && state.crop) {
678
- ctx.fillStyle = "#ffffff";
966
+ ctx.fillStyle = MASK_WHITE;
679
967
  ctx.fillRect(state.crop.x, state.crop.y, state.crop.width, state.crop.height);
680
968
  }
681
- if (useAnnotations && state.annotations.length > 0) {
682
- for (const ann of state.annotations) {
683
- ctx.save();
684
- if (ann.rotation) {
685
- ctx.translate(ann.x, ann.y);
686
- ctx.rotate(ann.rotation * Math.PI / 180);
687
- ctx.translate(-ann.x, -ann.y);
688
- }
689
- if (ann.type === "pen") {
690
- const pen = ann;
691
- if (pen.points.length > 0) {
692
- ctx.beginPath();
693
- ctx.strokeStyle = "#ffffff";
694
- ctx.lineWidth = Math.max(pen.strokeWidth, 8);
695
- ctx.lineCap = "round";
696
- ctx.lineJoin = "round";
697
- ctx.moveTo(pen.points[0].x, pen.points[0].y);
698
- for (let i = 1; i < pen.points.length; i++) {
699
- ctx.lineTo(pen.points[i].x, pen.points[i].y);
700
- }
701
- ctx.stroke();
702
- }
703
- } else if (ann.type === "rect") {
704
- const rect = ann;
705
- ctx.fillStyle = "#ffffff";
706
- ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
707
- } else if (ann.type === "circle") {
708
- const circ = ann;
709
- ctx.fillStyle = "#ffffff";
969
+ if (useAnnotations && state.annotations.length > 0) {
970
+ for (const ann of state.annotations) {
971
+ this.drawAnnotationMask(ctx, ann);
972
+ }
973
+ }
974
+ const imageData = ctx.getImageData?.(0, 0, naturalWidth, naturalHeight);
975
+ if (imageData) {
976
+ const d = imageData.data;
977
+ for (let i = 0; i < d.length; i += 4) {
978
+ const v = d[i] >= 128 ? 255 : 0;
979
+ d[i] = v;
980
+ d[i + 1] = v;
981
+ d[i + 2] = v;
982
+ d[i + 3] = 255;
983
+ }
984
+ ctx.putImageData(imageData, 0, 0);
985
+ }
986
+ return maskCanvas;
987
+ }
988
+ drawAnnotationMask(ctx, ann) {
989
+ ctx.save();
990
+ ctx.globalAlpha = 1;
991
+ ctx.fillStyle = MASK_WHITE;
992
+ ctx.strokeStyle = MASK_WHITE;
993
+ ctx.lineCap = "round";
994
+ ctx.lineJoin = "round";
995
+ if (ann.rotation) {
996
+ ctx.translate(ann.x, ann.y);
997
+ ctx.rotate(ann.rotation * Math.PI / 180);
998
+ ctx.translate(-ann.x, -ann.y);
999
+ }
1000
+ switch (ann.type) {
1001
+ case "pen": {
1002
+ if (ann.points.length > 0) {
710
1003
  ctx.beginPath();
711
- ctx.ellipse(circ.x, circ.y, circ.radiusX, circ.radiusY, 0, 0, Math.PI * 2);
712
- ctx.fill();
713
- } else {
714
- ctx.strokeStyle = "#ffffff";
715
- ctx.fillStyle = "#ffffff";
716
- await this.drawAnnotation(ctx, ann);
1004
+ ctx.lineWidth = Math.max(ann.strokeWidth || 0, 8);
1005
+ ctx.moveTo(ann.points[0].x, ann.points[0].y);
1006
+ for (let i = 1; i < ann.points.length; i++) {
1007
+ ctx.lineTo(ann.points[i].x, ann.points[i].y);
1008
+ }
1009
+ ctx.stroke();
717
1010
  }
718
- ctx.restore();
1011
+ break;
1012
+ }
1013
+ case "circle": {
1014
+ ctx.beginPath();
1015
+ ctx.ellipse(ann.x, ann.y, Math.abs(ann.radiusX), Math.abs(ann.radiusY), 0, 0, Math.PI * 2);
1016
+ ctx.fill();
1017
+ break;
1018
+ }
1019
+ case "arrow":
1020
+ case "line": {
1021
+ ctx.beginPath();
1022
+ ctx.lineWidth = Math.max(ann.strokeWidth || 0, 8);
1023
+ ctx.moveTo(ann.x, ann.y);
1024
+ ctx.lineTo(ann.endX, ann.endY);
1025
+ ctx.stroke();
1026
+ break;
1027
+ }
1028
+ case "rect":
1029
+ case "image":
1030
+ case "text": {
1031
+ const b = getAnnotationBounds(ann);
1032
+ ctx.fillRect(b.x, b.y, b.width, b.height);
1033
+ break;
719
1034
  }
720
1035
  }
721
- return maskCanvas;
1036
+ ctx.restore();
722
1037
  }
723
1038
  };
1039
+ var MASK_WHITE = "#ffffff";
1040
+ var MASK_BLACK = "#000000";
1041
+ function getSourceSize(img, fallbackW = 800, fallbackH = 600) {
1042
+ const anyImg = img;
1043
+ return {
1044
+ width: anyImg.naturalWidth || anyImg.videoWidth || anyImg.width || fallbackW,
1045
+ height: anyImg.naturalHeight || anyImg.videoHeight || anyImg.height || fallbackH
1046
+ };
1047
+ }
1048
+ function getBaseProjection(region, transform, baseW, baseH) {
1049
+ return {
1050
+ region,
1051
+ angle: normalizeAngle(transform.rotate),
1052
+ flipH: transform.flipH,
1053
+ flipV: transform.flipV,
1054
+ scale: 1,
1055
+ originX: baseW / 2,
1056
+ originY: baseH / 2
1057
+ };
1058
+ }
724
1059
 
725
1060
  // src/image-editor/controller.ts
726
1061
  var DEFAULT_ADJUSTMENTS = {
@@ -789,39 +1124,56 @@ var ImageEditorController = class {
789
1124
  };
790
1125
  if (options.image instanceof Object && "width" in options.image) {
791
1126
  this.imageElement = options.image;
1127
+ this.state.sourceUrl = options.image.src || null;
792
1128
  this.state.imageDimensions = {
793
1129
  width: options.image.naturalWidth || options.image.width || 800,
794
1130
  height: options.image.naturalHeight || options.image.height || 600
795
1131
  };
796
1132
  }
797
- this.initialSnapshot = this.serializeState();
1133
+ this.initialSnapshot = this.captureHistoryEntry();
798
1134
  if (typeof options.image === "string") {
799
1135
  this.loadImage(options.image);
800
1136
  }
801
1137
  }
802
1138
  // --- Image Loading ---
1139
+ /**
1140
+ * Loads a new source image and makes it the baseline that `reset()` returns to.
1141
+ * Does not record history; use `applyInpaintedImage` / `applyBackgroundRemovedImage`
1142
+ * for undoable source replacement.
1143
+ */
803
1144
  async loadImage(src) {
1145
+ if (typeof src === "string") this.state.sourceUrl = src;
1146
+ const source = await this.resolveSource(src);
1147
+ this.imageElement = source.element;
1148
+ this.state.sourceUrl = source.sourceUrl;
1149
+ this.state.imageDimensions = source.dimensions;
1150
+ this.initialSnapshot = this.captureHistoryEntry();
1151
+ this.notify();
1152
+ }
1153
+ async resolveSource(src) {
804
1154
  if (typeof src === "string") {
805
- this.state.sourceUrl = src;
806
1155
  try {
807
1156
  const img = await this.renderer.imageCache.get(src);
808
- this.imageElement = img;
809
- this.state.imageDimensions = {
810
- width: img.naturalWidth || img.width || 800,
811
- height: img.naturalHeight || img.height || 600
1157
+ return {
1158
+ sourceUrl: src,
1159
+ element: img,
1160
+ dimensions: {
1161
+ width: img.naturalWidth || img.width || 800,
1162
+ height: img.naturalHeight || img.height || 600
1163
+ }
812
1164
  };
813
- } catch (err) {
1165
+ } catch {
1166
+ return { sourceUrl: src, element: null, dimensions: { ...this.state.imageDimensions } };
814
1167
  }
815
- } else {
816
- this.imageElement = src;
817
- this.state.sourceUrl = src.src || null;
818
- this.state.imageDimensions = {
1168
+ }
1169
+ return {
1170
+ sourceUrl: src.src || null,
1171
+ element: src,
1172
+ dimensions: {
819
1173
  width: src.naturalWidth || src.width || 800,
820
1174
  height: src.naturalHeight || src.height || 600
821
- };
822
- }
823
- this.initialSnapshot = this.serializeState();
824
- this.notify();
1175
+ }
1176
+ };
825
1177
  }
826
1178
  getImageElement() {
827
1179
  return this.imageElement;
@@ -843,9 +1195,28 @@ var ImageEditorController = class {
843
1195
  this.listeners.forEach((l) => l(currentState));
844
1196
  }
845
1197
  // --- History (Undo / Redo / Reset) ---
1198
+ captureHistoryEntry() {
1199
+ return {
1200
+ state: this.serializeState(),
1201
+ source: {
1202
+ sourceUrl: this.state.sourceUrl,
1203
+ element: this.imageElement,
1204
+ dimensions: { ...this.state.imageDimensions }
1205
+ }
1206
+ };
1207
+ }
1208
+ restoreHistoryEntry(entry) {
1209
+ this.applySerializedState(entry.state);
1210
+ this.imageElement = entry.source.element;
1211
+ this.state.sourceUrl = entry.source.sourceUrl;
1212
+ this.state.imageDimensions = { ...entry.source.dimensions };
1213
+ if (this.state.selectedAnnotationId && !this.state.annotations.some((a) => a.id === this.state.selectedAnnotationId)) {
1214
+ this.state.selectedAnnotationId = null;
1215
+ }
1216
+ }
846
1217
  recordHistory() {
847
1218
  if (this.isApplyingHistory) return;
848
- this.undoStack.push(this.serializeState());
1219
+ this.undoStack.push(this.captureHistoryEntry());
849
1220
  if (this.undoStack.length > this.maxHistoryDepth) {
850
1221
  this.undoStack.shift();
851
1222
  }
@@ -860,10 +1231,9 @@ var ImageEditorController = class {
860
1231
  undo() {
861
1232
  if (!this.canUndo) return false;
862
1233
  this.isApplyingHistory = true;
863
- const current = this.serializeState();
864
- this.redoStack.push(current);
1234
+ this.redoStack.push(this.captureHistoryEntry());
865
1235
  const prev = this.undoStack.pop();
866
- this.applySerializedState(prev);
1236
+ this.restoreHistoryEntry(prev);
867
1237
  this.isApplyingHistory = false;
868
1238
  this.notify();
869
1239
  return true;
@@ -871,17 +1241,16 @@ var ImageEditorController = class {
871
1241
  redo() {
872
1242
  if (!this.canRedo) return false;
873
1243
  this.isApplyingHistory = true;
874
- const current = this.serializeState();
875
- this.undoStack.push(current);
1244
+ this.undoStack.push(this.captureHistoryEntry());
876
1245
  const next = this.redoStack.pop();
877
- this.applySerializedState(next);
1246
+ this.restoreHistoryEntry(next);
878
1247
  this.isApplyingHistory = false;
879
1248
  this.notify();
880
1249
  return true;
881
1250
  }
882
1251
  reset() {
883
1252
  this.recordHistory();
884
- this.applySerializedState(this.initialSnapshot);
1253
+ this.restoreHistoryEntry(this.initialSnapshot);
885
1254
  this.state.zoom = 1;
886
1255
  this.state.pan = { x: 0, y: 0 };
887
1256
  this.state.activeTool = "select";
@@ -916,6 +1285,9 @@ var ImageEditorController = class {
916
1285
  applyCropPreset(ratio) {
917
1286
  this.recordHistory();
918
1287
  const { width, height } = this.state.imageDimensions;
1288
+ if (ratio && isQuarterTurnSwapped(this.state.transform)) {
1289
+ ratio = 1 / ratio;
1290
+ }
919
1291
  if (!ratio) {
920
1292
  this.state.crop = { x: 0, y: 0, width, height };
921
1293
  } else {
@@ -945,22 +1317,22 @@ var ImageEditorController = class {
945
1317
  rotate(degrees) {
946
1318
  this.recordHistory();
947
1319
  const current = this.state.transform.rotate;
948
- this.state.transform.rotate = (current + degrees) % 360;
1320
+ this.state.transform = { ...this.state.transform, rotate: (current + degrees) % 360 };
949
1321
  this.notify();
950
1322
  }
951
1323
  setRotation(degrees) {
952
1324
  this.recordHistory();
953
- this.state.transform.rotate = degrees % 360;
1325
+ this.state.transform = { ...this.state.transform, rotate: degrees % 360 };
954
1326
  this.notify();
955
1327
  }
956
1328
  flipHorizontal() {
957
1329
  this.recordHistory();
958
- this.state.transform.flipH = !this.state.transform.flipH;
1330
+ this.state.transform = { ...this.state.transform, flipH: !this.state.transform.flipH };
959
1331
  this.notify();
960
1332
  }
961
1333
  flipVertical() {
962
1334
  this.recordHistory();
963
- this.state.transform.flipV = !this.state.transform.flipV;
1335
+ this.state.transform = { ...this.state.transform, flipV: !this.state.transform.flipV };
964
1336
  this.notify();
965
1337
  }
966
1338
  // --- Adjustments ---
@@ -1115,14 +1487,68 @@ var ImageEditorController = class {
1115
1487
  const canvas = await this.renderMask(options);
1116
1488
  return canvas.toDataURL("image/png");
1117
1489
  }
1490
+ /**
1491
+ * Renders the current source image at natural size with no crop, rotation, flip,
1492
+ * adjustments or annotations. This is the image that `toMaskDataURL()` is aligned with
1493
+ * and the space that `applyInpaintedImage` / `applyBackgroundRemovedImage` expect back.
1494
+ */
1495
+ async renderSource() {
1496
+ const { width, height } = this.state.imageDimensions;
1497
+ const canvas = this.renderer.createCanvas(width, height);
1498
+ const source = this.imageElement || this.state.sourceUrl;
1499
+ const ctx = canvas.getContext("2d");
1500
+ if (!ctx || !source) return canvas;
1501
+ const img = typeof source === "string" ? await this.renderer.imageCache.get(source) : source;
1502
+ ctx.drawImage(img, 0, 0, width, height);
1503
+ return canvas;
1504
+ }
1505
+ async toSourceDataURL(options = {}) {
1506
+ const canvas = await this.renderSource();
1507
+ return canvas.toDataURL(options.format || "image/png", options.quality ?? 0.92);
1508
+ }
1509
+ /**
1510
+ * Replaces the source image with an inpainted result (natural image space, see
1511
+ * `toSourceDataURL`) and clears the mask annotations, as ONE undoable step.
1512
+ */
1118
1513
  async applyInpaintedImage(newDataUrl) {
1119
- this.recordHistory();
1120
- await this.loadImage(newDataUrl);
1121
- this.clearAnnotations();
1514
+ await this.replaceSource(newDataUrl, true);
1122
1515
  }
1516
+ /**
1517
+ * Replaces the source image with a background-removed result as ONE undoable step.
1518
+ * Crop and annotations are kept (rescaled if the new image has different dimensions).
1519
+ */
1123
1520
  async applyBackgroundRemovedImage(newDataUrl) {
1521
+ await this.replaceSource(newDataUrl, false);
1522
+ }
1523
+ async replaceSource(src, clearAnnotations) {
1524
+ const next = await this.resolveSource(src);
1124
1525
  this.recordHistory();
1125
- await this.loadImage(newDataUrl);
1526
+ const prev = this.state.imageDimensions;
1527
+ const sx = next.dimensions.width / (prev.width || next.dimensions.width);
1528
+ const sy = next.dimensions.height / (prev.height || next.dimensions.height);
1529
+ this.imageElement = next.element;
1530
+ this.state.sourceUrl = next.sourceUrl;
1531
+ this.state.imageDimensions = next.dimensions;
1532
+ if (sx !== 1 || sy !== 1) {
1533
+ if (this.state.crop) {
1534
+ this.state.crop = clampCropToImage(
1535
+ {
1536
+ x: Math.round(this.state.crop.x * sx),
1537
+ y: Math.round(this.state.crop.y * sy),
1538
+ width: Math.round(this.state.crop.width * sx),
1539
+ height: Math.round(this.state.crop.height * sy)
1540
+ },
1541
+ next.dimensions.width,
1542
+ next.dimensions.height
1543
+ );
1544
+ }
1545
+ this.state.annotations = this.state.annotations.map((ann) => scaleAnnotation(ann, sx, sy));
1546
+ }
1547
+ if (clearAnnotations) {
1548
+ this.state.annotations = [];
1549
+ this.state.selectedAnnotationId = null;
1550
+ }
1551
+ this.notify();
1126
1552
  }
1127
1553
  // --- Serialization ---
1128
1554
  toJSON() {
@@ -1157,226 +1583,25 @@ var ImageEditorController = class {
1157
1583
  this.state.annotations = JSON.parse(JSON.stringify(data.annotations || []));
1158
1584
  }
1159
1585
  };
1160
-
1161
- // src/image-editor/geometry.ts
1162
- function getViewportMetrics(state, vpWidth, vpHeight, isActivelyCropping = false, cropOverride) {
1163
- const { imageDimensions, crop: stateCrop, transform, zoom, pan } = state;
1164
- const naturalWidth = imageDimensions.width || 800;
1165
- const naturalHeight = imageDimensions.height || 600;
1166
- const activeCrop = cropOverride !== void 0 && cropOverride !== null ? cropOverride : stateCrop ?? (isActivelyCropping ? { x: 0, y: 0, width: naturalWidth, height: naturalHeight } : null);
1167
- const cropW = !isActivelyCropping && activeCrop ? activeCrop.width : naturalWidth;
1168
- const cropH = !isActivelyCropping && activeCrop ? activeCrop.height : naturalHeight;
1169
- const angle = (transform.rotate % 360 + 360) % 360;
1170
- const isSwapped = angle === 90 || angle === 270;
1171
- const baseW = isSwapped ? cropH : cropW;
1172
- const baseH = isSwapped ? cropW : cropH;
1173
- const fitScale = Math.min(vpWidth * 0.85 / baseW, vpHeight * 0.85 / baseH, 1);
1174
- const renderW = baseW * fitScale;
1175
- const renderH = baseH * fitScale;
1176
- const totalScale = zoom * fitScale;
1177
- const centerX = vpWidth / 2 + pan.x;
1178
- const centerY = vpHeight / 2 + pan.y;
1179
- const imageRect = {
1180
- x: centerX - renderW * zoom / 2,
1181
- y: centerY - renderH * zoom / 2,
1182
- width: renderW * zoom,
1183
- height: renderH * zoom
1184
- };
1185
- let cropRect = null;
1186
- if (activeCrop) {
1187
- const startX = -renderW / 2 + activeCrop.x / naturalWidth * renderW;
1188
- const startY = -renderH / 2 + activeCrop.y / naturalHeight * renderH;
1189
- const cropBoxW = activeCrop.width / naturalWidth * renderW;
1190
- const cropBoxH = activeCrop.height / naturalHeight * renderH;
1191
- cropRect = {
1192
- x: centerX + zoom * startX,
1193
- y: centerY + zoom * startY,
1194
- width: zoom * cropBoxW,
1195
- height: zoom * cropBoxH
1196
- };
1197
- }
1198
- return {
1199
- vpWidth,
1200
- vpHeight,
1201
- centerX,
1202
- centerY,
1203
- baseW,
1204
- baseH,
1205
- fitScale,
1206
- renderW,
1207
- renderH,
1208
- totalScale,
1209
- imageRect,
1210
- cropRect
1211
- };
1212
- }
1213
- function canvasToImagePoint(canvasPt, state, vpWidth, vpHeight, isActivelyCropping = false) {
1214
- const metrics = getViewportMetrics(state, vpWidth, vpHeight, isActivelyCropping);
1215
- const unzoomedX = (canvasPt.x - metrics.centerX) / state.zoom;
1216
- const unzoomedY = (canvasPt.y - metrics.centerY) / state.zoom;
1217
- const x = (unzoomedX + metrics.renderW / 2) / metrics.fitScale;
1218
- const y = (unzoomedY + metrics.renderH / 2) / metrics.fitScale;
1219
- return { x: Math.round(x), y: Math.round(y) };
1220
- }
1221
- function imageToCanvasPoint(imgPt, state, vpWidth, vpHeight, isActivelyCropping = false) {
1222
- const metrics = getViewportMetrics(state, vpWidth, vpHeight, isActivelyCropping);
1223
- const unzoomedX = (imgPt.x - metrics.baseW / 2) * metrics.fitScale;
1224
- const unzoomedY = (imgPt.y - metrics.baseH / 2) * metrics.fitScale;
1225
- const canvasX = metrics.centerX + unzoomedX * state.zoom;
1226
- const canvasY = metrics.centerY + unzoomedY * state.zoom;
1227
- return { x: Math.round(canvasX), y: Math.round(canvasY) };
1228
- }
1229
- function hitTestCrop(canvasPt, state, vpWidth, vpHeight, handleThreshold = 14, cropOverride) {
1230
- const metrics = getViewportMetrics(state, vpWidth, vpHeight, true, cropOverride);
1231
- if (!metrics.cropRect) return null;
1232
- const { x, y, width: w, height: h } = metrics.cropRect;
1233
- const px = canvasPt.x;
1234
- const py = canvasPt.y;
1235
- const dist = (x1, y1, x2, y2) => Math.hypot(x1 - x2, y1 - y2);
1236
- if (dist(px, py, x, y) <= handleThreshold) return "nw";
1237
- if (dist(px, py, x + w, y) <= handleThreshold) return "ne";
1238
- if (dist(px, py, x + w, y + h) <= handleThreshold) return "se";
1239
- if (dist(px, py, x, y + h) <= handleThreshold) return "sw";
1240
- const nearX = px >= x - handleThreshold && px <= x + w + handleThreshold;
1241
- const nearY = py >= y - handleThreshold && py <= y + h + handleThreshold;
1242
- if (nearX && Math.abs(py - y) <= handleThreshold) return "n";
1243
- if (nearX && Math.abs(py - (y + h)) <= handleThreshold) return "s";
1244
- if (nearY && Math.abs(px - x) <= handleThreshold) return "w";
1245
- if (nearY && Math.abs(px - (x + w)) <= handleThreshold) return "e";
1246
- if (px >= x && px <= x + w && py >= y && py <= y + h) {
1247
- return "inside";
1248
- }
1249
- return null;
1250
- }
1251
- function getCropCursor(handle) {
1252
- switch (handle) {
1253
- case "nw":
1254
- case "se":
1255
- return "nwse-resize";
1256
- case "ne":
1257
- case "sw":
1258
- return "nesw-resize";
1259
- case "n":
1260
- case "s":
1261
- return "ns-resize";
1262
- case "e":
1263
- case "w":
1264
- return "ew-resize";
1265
- case "inside":
1266
- return "move";
1267
- default:
1268
- return "crosshair";
1269
- }
1270
- }
1271
- function calculateCropDrag(startCrop, handle, startImgPt, currentImgPt, imgWidth, imgHeight, aspectRatio) {
1272
- const dx = currentImgPt.x - startImgPt.x;
1273
- const dy = currentImgPt.y - startImgPt.y;
1274
- if (handle === "inside") {
1275
- const maxX = Math.max(0, imgWidth - startCrop.width);
1276
- const maxY = Math.max(0, imgHeight - startCrop.height);
1277
- const newX = Math.max(0, Math.min(maxX, startCrop.x + dx));
1278
- const newY = Math.max(0, Math.min(maxY, startCrop.y + dy));
1279
- return {
1280
- x: Math.round(newX),
1281
- y: Math.round(newY),
1282
- width: startCrop.width,
1283
- height: startCrop.height
1284
- };
1285
- }
1286
- let left = startCrop.x;
1287
- let top = startCrop.y;
1288
- let right = startCrop.x + startCrop.width;
1289
- let bottom = startCrop.y + startCrop.height;
1290
- const minSize = 20;
1291
- if (handle === "e" || handle === "ne" || handle === "se") {
1292
- right = Math.min(imgWidth, Math.max(left + minSize, startCrop.x + startCrop.width + dx));
1293
- }
1294
- if (handle === "w" || handle === "nw" || handle === "sw") {
1295
- left = Math.max(0, Math.min(right - minSize, startCrop.x + dx));
1296
- }
1297
- if (handle === "s" || handle === "se" || handle === "sw") {
1298
- bottom = Math.min(imgHeight, Math.max(top + minSize, startCrop.y + startCrop.height + dy));
1299
- }
1300
- if (handle === "n" || handle === "nw" || handle === "ne") {
1301
- top = Math.max(0, Math.min(bottom - minSize, startCrop.y + dy));
1302
- }
1303
- if (aspectRatio && aspectRatio > 0) {
1304
- let currentW = right - left;
1305
- let currentH = bottom - top;
1306
- if (handle === "e" || handle === "w") {
1307
- currentH = Math.min(imgHeight, currentW / aspectRatio);
1308
- bottom = Math.min(imgHeight, top + currentH);
1309
- } else if (handle === "n" || handle === "s") {
1310
- currentW = Math.min(imgWidth, currentH * aspectRatio);
1311
- right = Math.min(imgWidth, left + currentW);
1312
- } else {
1313
- const targetH = currentW / aspectRatio;
1314
- if (top + targetH <= imgHeight) {
1315
- bottom = top + targetH;
1316
- } else {
1317
- const targetW = currentH * aspectRatio;
1318
- right = Math.min(imgWidth, left + targetW);
1319
- }
1320
- }
1321
- }
1322
- return {
1323
- x: Math.round(left),
1324
- y: Math.round(top),
1325
- width: Math.round(Math.max(minSize, right - left)),
1326
- height: Math.round(Math.max(minSize, bottom - top))
1327
- };
1328
- }
1329
- function getAnnotationBounds(ann) {
1330
- switch (ann.type) {
1586
+ function scaleAnnotation(ann, sx, sy) {
1587
+ const k = Math.sqrt(Math.abs(sx * sy));
1588
+ const base = { ...ann, x: ann.x * sx, y: ann.y * sy };
1589
+ switch (base.type) {
1590
+ case "pen":
1591
+ return { ...base, points: base.points.map((p) => ({ x: p.x * sx, y: p.y * sy })), strokeWidth: base.strokeWidth * k };
1331
1592
  case "rect":
1332
- return { x: ann.x, y: ann.y, width: ann.width, height: ann.height };
1593
+ return { ...base, width: base.width * sx, height: base.height * sy, strokeWidth: base.strokeWidth * k };
1594
+ case "image":
1595
+ return { ...base, width: base.width * sx, height: base.height * sy };
1333
1596
  case "circle":
1334
- return {
1335
- x: ann.x - ann.radiusX,
1336
- y: ann.y - ann.radiusY,
1337
- width: ann.radiusX * 2,
1338
- height: ann.radiusY * 2
1339
- };
1597
+ return { ...base, radiusX: base.radiusX * sx, radiusY: base.radiusY * sy, strokeWidth: base.strokeWidth * k };
1340
1598
  case "arrow":
1341
- case "line": {
1342
- const minX = Math.min(ann.x, ann.endX);
1343
- const minY = Math.min(ann.y, ann.endY);
1344
- return {
1345
- x: minX,
1346
- y: minY,
1347
- width: Math.max(Math.abs(ann.endX - ann.x), 10),
1348
- height: Math.max(Math.abs(ann.endY - ann.y), 10)
1349
- };
1350
- }
1351
- case "pen": {
1352
- if (!ann.points.length) return { x: ann.x, y: ann.y, width: 0, height: 0 };
1353
- let minX = ann.points[0].x;
1354
- let maxX = ann.points[0].x;
1355
- let minY = ann.points[0].y;
1356
- let maxY = ann.points[0].y;
1357
- for (const pt of ann.points) {
1358
- if (pt.x < minX) minX = pt.x;
1359
- if (pt.x > maxX) maxX = pt.x;
1360
- if (pt.y < minY) minY = pt.y;
1361
- if (pt.y > maxY) maxY = pt.y;
1362
- }
1363
- return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
1364
- }
1599
+ case "line":
1600
+ return { ...base, endX: base.endX * sx, endY: base.endY * sy, strokeWidth: base.strokeWidth * k };
1365
1601
  case "text":
1366
- return {
1367
- x: ann.x,
1368
- y: ann.y,
1369
- width: ann.text.length * ann.fontSize * 0.6,
1370
- height: ann.fontSize * 1.2
1371
- };
1372
- case "image":
1373
- return { x: ann.x, y: ann.y, width: ann.width, height: ann.height };
1602
+ return { ...base, fontSize: base.fontSize * k };
1374
1603
  }
1375
1604
  }
1376
- function hitTestAnnotation(imgPt, ann, padding = 10) {
1377
- const b = getAnnotationBounds(ann);
1378
- return imgPt.x >= b.x - padding && imgPt.x <= b.x + b.width + padding && imgPt.y >= b.y - padding && imgPt.y <= b.y + b.height + padding;
1379
- }
1380
1605
  export {
1381
1606
  DEFAULT_ADJUSTMENTS,
1382
1607
  DEFAULT_DEPTH_MASK,
@@ -1385,15 +1610,25 @@ export {
1385
1610
  ImageEditorRenderer,
1386
1611
  applyAdjustments,
1387
1612
  applyDepthMask,
1613
+ applyImageProjectionToContext,
1388
1614
  calculateCropDrag,
1389
1615
  canvasToImagePoint,
1616
+ clampCropToImage,
1390
1617
  getAnnotationBounds,
1391
1618
  getCropCursor,
1619
+ getRotatedSize,
1392
1620
  getViewportMetrics,
1621
+ getViewportProjection,
1393
1622
  globalImageCache,
1394
1623
  hitTestAnnotation,
1395
1624
  hitTestCrop,
1396
1625
  imageToCanvasPoint,
1397
- removeAlpha
1626
+ isQuarterTurnSwapped,
1627
+ mapCropHandle,
1628
+ normalizeAngle,
1629
+ projectImagePoint,
1630
+ projectImageRect,
1631
+ removeAlpha,
1632
+ unprojectCanvasPoint
1398
1633
  };
1399
1634
  //# sourceMappingURL=index.js.map