@pixerate/editor 0.18.0 → 0.19.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.
- package/dist/image-editor/index.cjs +732 -497
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +105 -9
- package/dist/image-editor/index.d.ts +105 -9
- package/dist/image-editor/index.js +721 -486
- package/dist/image-editor/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -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 : _nullishCoalesce(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 = class {constructor() { _class.prototype.__init.call(this); }
|
|
153
496
|
__init() {this.cache = /* @__PURE__ */ new Map()}
|
|
@@ -191,10 +534,12 @@ var ImageCache = (_class = class {constructor() { _class.prototype.__init.call(t
|
|
|
191
534
|
}
|
|
192
535
|
}, _class);
|
|
193
536
|
var globalImageCache = new ImageCache();
|
|
194
|
-
var ImageEditorRenderer = (_class2 = class {constructor() { _class2.prototype.__init2.call(this);_class2.prototype.__init3.call(this);_class2.prototype.__init4.call(this); }
|
|
537
|
+
var ImageEditorRenderer = (_class2 = class {constructor() { _class2.prototype.__init2.call(this);_class2.prototype.__init3.call(this);_class2.prototype.__init4.call(this);_class2.prototype.__init5.call(this);_class2.prototype.__init6.call(this); }
|
|
195
538
|
__init2() {this.imageCache = globalImageCache}
|
|
196
539
|
__init3() {this.cachedBaseCanvas = null}
|
|
197
540
|
__init4() {this.cachedBaseKey = ""}
|
|
541
|
+
__init5() {this.sourceIds = /* @__PURE__ */ new WeakMap()}
|
|
542
|
+
__init6() {this.nextSourceId = 0}
|
|
198
543
|
/**
|
|
199
544
|
* Helper to create a canvas element safely in browser/JSDOM.
|
|
200
545
|
*/
|
|
@@ -226,8 +571,8 @@ var ImageEditorRenderer = (_class2 = class {constructor() { _class2.prototype.__
|
|
|
226
571
|
} else {
|
|
227
572
|
img = imageSource;
|
|
228
573
|
}
|
|
229
|
-
const
|
|
230
|
-
const
|
|
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 = (_class2 = class {constructor() { _class2.prototype.__
|
|
|
239
584
|
return;
|
|
240
585
|
}
|
|
241
586
|
const isActivelyCropping = Boolean(options.showCropOverlay || state.activeTool === "crop");
|
|
242
|
-
const
|
|
243
|
-
const
|
|
244
|
-
const
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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},${_optionalChain([depthMask, 'access', _ => _.depthRange, 'optionalAccess', _2 => _2[0]])}-${_optionalChain([depthMask, 'access', _3 => _3.depthRange, 'optionalAccess', _4 => _4[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.
|
|
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 (e3) {
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
ctx.
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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 (
|
|
315
|
-
|
|
625
|
+
if (isActivelyCropping) {
|
|
626
|
+
const activeCrop = options.draftCrop !== void 0 && options.draftCrop !== null ? options.draftCrop : _nullishCoalesce(crop, () => ( { x: 0, y: 0, width: naturalWidth, height: naturalHeight }));
|
|
627
|
+
this.drawCropOverlay(ctx, getViewportMetrics(geoState, vpWidth, vpHeight, true, activeCrop));
|
|
316
628
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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 (e3) {
|
|
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 = (_class2 = class {constructor() { _class2.prototype.__
|
|
|
448
822
|
ctx.strokeStyle = "#3b82f6";
|
|
449
823
|
ctx.lineWidth = 1.5;
|
|
450
824
|
ctx.setLineDash([4, 4]);
|
|
451
|
-
const bounds =
|
|
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 = (_class2 = class {constructor() { _class2.prototype.__
|
|
|
467
841
|
}
|
|
468
842
|
ctx.restore();
|
|
469
843
|
}
|
|
844
|
+
/** @deprecated Use the standalone `getAnnotationBounds` from the geometry module. */
|
|
470
845
|
getAnnotationBounds(ann) {
|
|
471
|
-
|
|
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,
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
const
|
|
527
|
-
const
|
|
528
|
-
const
|
|
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(
|
|
543
|
-
ctx.fillRect(
|
|
544
|
-
ctx.fillRect(
|
|
545
|
-
ctx.fillRect(startX + cropBoxW, startY,
|
|
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 = (_class2 = class {constructor() { _class2.prototype.__
|
|
|
600
923
|
} else {
|
|
601
924
|
img = imageSource;
|
|
602
925
|
}
|
|
603
|
-
const
|
|
604
|
-
const
|
|
605
|
-
const
|
|
606
|
-
const
|
|
607
|
-
const
|
|
608
|
-
const
|
|
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
|
|
620
|
-
|
|
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 (e5) {
|
|
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(
|
|
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 = (_class2 = class {constructor() { _class2.prototype.__
|
|
|
661
948
|
return exportCanvas;
|
|
662
949
|
}
|
|
663
950
|
/**
|
|
664
|
-
* Generates a binary mask canvas (white
|
|
665
|
-
*
|
|
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 =
|
|
961
|
+
ctx.fillStyle = MASK_BLACK;
|
|
674
962
|
ctx.fillRect(0, 0, naturalWidth, naturalHeight);
|
|
675
963
|
const useCrop = _nullishCoalesce(_optionalChain([options, 'optionalAccess', _5 => _5.useCrop]), () => ( true));
|
|
676
964
|
const useAnnotations = _nullishCoalesce(_optionalChain([options, 'optionalAccess', _6 => _6.useAnnotations]), () => ( true));
|
|
677
965
|
if (useCrop && state.crop) {
|
|
678
|
-
ctx.fillStyle =
|
|
966
|
+
ctx.fillStyle = MASK_WHITE;
|
|
679
967
|
ctx.fillRect(state.crop.x, state.crop.y, state.crop.width, state.crop.height);
|
|
680
968
|
}
|
|
681
969
|
if (useAnnotations && state.annotations.length > 0) {
|
|
682
970
|
for (const ann of state.annotations) {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
971
|
+
this.drawAnnotationMask(ctx, ann);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
const imageData = _optionalChain([ctx, 'access', _7 => _7.getImageData, 'optionalCall', _8 => _8(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.
|
|
712
|
-
ctx.
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1036
|
+
ctx.restore();
|
|
722
1037
|
}
|
|
723
1038
|
}, _class2);
|
|
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 = {
|
|
@@ -748,39 +1083,39 @@ var DEFAULT_DEPTH_MASK = {
|
|
|
748
1083
|
var ImageEditorController = (_class3 = class {
|
|
749
1084
|
|
|
750
1085
|
|
|
751
|
-
|
|
752
|
-
|
|
1086
|
+
__init7() {this.imageElement = null}
|
|
1087
|
+
__init8() {this.listeners = /* @__PURE__ */ new Set()}
|
|
753
1088
|
|
|
754
1089
|
// History stacks
|
|
755
|
-
|
|
756
|
-
|
|
1090
|
+
__init9() {this.undoStack = []}
|
|
1091
|
+
__init10() {this.redoStack = []}
|
|
757
1092
|
|
|
758
|
-
|
|
1093
|
+
__init11() {this.isApplyingHistory = false}
|
|
759
1094
|
|
|
760
|
-
constructor(options = {}) {;_class3.prototype.
|
|
1095
|
+
constructor(options = {}) {;_class3.prototype.__init7.call(this);_class3.prototype.__init8.call(this);_class3.prototype.__init9.call(this);_class3.prototype.__init10.call(this);_class3.prototype.__init11.call(this);
|
|
761
1096
|
this.maxHistoryDepth = _nullishCoalesce(options.maxHistoryDepth, () => ( 50));
|
|
762
1097
|
this.onStateChange = options.onStateChange;
|
|
763
1098
|
this.renderer = new ImageEditorRenderer();
|
|
764
1099
|
const initialAdjustments = {
|
|
765
1100
|
...DEFAULT_ADJUSTMENTS,
|
|
766
|
-
..._optionalChain([options, 'access',
|
|
1101
|
+
..._optionalChain([options, 'access', _9 => _9.initialState, 'optionalAccess', _10 => _10.adjustments])
|
|
767
1102
|
};
|
|
768
1103
|
const initialTransform = {
|
|
769
1104
|
...DEFAULT_TRANSFORM,
|
|
770
|
-
..._optionalChain([options, 'access',
|
|
1105
|
+
..._optionalChain([options, 'access', _11 => _11.initialState, 'optionalAccess', _12 => _12.transform])
|
|
771
1106
|
};
|
|
772
1107
|
const initialDepthMask = {
|
|
773
1108
|
...DEFAULT_DEPTH_MASK,
|
|
774
|
-
..._optionalChain([options, 'access',
|
|
1109
|
+
..._optionalChain([options, 'access', _13 => _13.initialState, 'optionalAccess', _14 => _14.depthMask])
|
|
775
1110
|
};
|
|
776
1111
|
this.state = {
|
|
777
1112
|
sourceUrl: typeof options.image === "string" ? options.image : null,
|
|
778
1113
|
imageDimensions: { width: 800, height: 600 },
|
|
779
|
-
crop: _nullishCoalesce(_optionalChain([options, 'access',
|
|
1114
|
+
crop: _nullishCoalesce(_optionalChain([options, 'access', _15 => _15.initialState, 'optionalAccess', _16 => _16.crop]), () => ( null)),
|
|
780
1115
|
transform: initialTransform,
|
|
781
1116
|
adjustments: initialAdjustments,
|
|
782
1117
|
depthMask: initialDepthMask,
|
|
783
|
-
annotations: _nullishCoalesce(_optionalChain([options, 'access',
|
|
1118
|
+
annotations: _nullishCoalesce(_optionalChain([options, 'access', _17 => _17.initialState, 'optionalAccess', _18 => _18.annotations]), () => ( [])),
|
|
784
1119
|
selectedAnnotationId: null,
|
|
785
1120
|
activeTool: "select",
|
|
786
1121
|
zoom: 1,
|
|
@@ -789,39 +1124,56 @@ var ImageEditorController = (_class3 = 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.
|
|
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
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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 (
|
|
1165
|
+
} catch (e5) {
|
|
1166
|
+
return { sourceUrl: src, element: null, dimensions: { ...this.state.imageDimensions } };
|
|
814
1167
|
}
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
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;
|
|
@@ -839,13 +1191,32 @@ var ImageEditorController = (_class3 = class {
|
|
|
839
1191
|
}
|
|
840
1192
|
notify() {
|
|
841
1193
|
const currentState = { ...this.state };
|
|
842
|
-
_optionalChain([this, 'access',
|
|
1194
|
+
_optionalChain([this, 'access', _19 => _19.onStateChange, 'optionalCall', _20 => _20(currentState)]);
|
|
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.
|
|
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 = (_class3 = class {
|
|
|
860
1231
|
undo() {
|
|
861
1232
|
if (!this.canUndo) return false;
|
|
862
1233
|
this.isApplyingHistory = true;
|
|
863
|
-
|
|
864
|
-
this.redoStack.push(current);
|
|
1234
|
+
this.redoStack.push(this.captureHistoryEntry());
|
|
865
1235
|
const prev = this.undoStack.pop();
|
|
866
|
-
this.
|
|
1236
|
+
this.restoreHistoryEntry(prev);
|
|
867
1237
|
this.isApplyingHistory = false;
|
|
868
1238
|
this.notify();
|
|
869
1239
|
return true;
|
|
@@ -871,17 +1241,16 @@ var ImageEditorController = (_class3 = class {
|
|
|
871
1241
|
redo() {
|
|
872
1242
|
if (!this.canRedo) return false;
|
|
873
1243
|
this.isApplyingHistory = true;
|
|
874
|
-
|
|
875
|
-
this.undoStack.push(current);
|
|
1244
|
+
this.undoStack.push(this.captureHistoryEntry());
|
|
876
1245
|
const next = this.redoStack.pop();
|
|
877
|
-
this.
|
|
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.
|
|
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 = (_class3 = 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 = (_class3 = class {
|
|
|
945
1317
|
rotate(degrees) {
|
|
946
1318
|
this.recordHistory();
|
|
947
1319
|
const current = this.state.transform.rotate;
|
|
948
|
-
this.state.transform.rotate
|
|
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
|
|
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
|
|
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
|
|
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 = (_class3 = 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", _nullishCoalesce(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.
|
|
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
|
-
|
|
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 = (_class3 = class {
|
|
|
1157
1583
|
this.state.annotations = JSON.parse(JSON.stringify(data.annotations || []));
|
|
1158
1584
|
}
|
|
1159
1585
|
}, _class3);
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
const activeCrop = cropOverride !== void 0 && cropOverride !== null ? cropOverride : _nullishCoalesce(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 {
|
|
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
|
-
|
|
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
|
|
|
1381
1606
|
|
|
1382
1607
|
|
|
@@ -1395,5 +1620,15 @@ function hitTestAnnotation(imgPt, ann, padding = 10) {
|
|
|
1395
1620
|
|
|
1396
1621
|
|
|
1397
1622
|
|
|
1398
|
-
|
|
1623
|
+
|
|
1624
|
+
|
|
1625
|
+
|
|
1626
|
+
|
|
1627
|
+
|
|
1628
|
+
|
|
1629
|
+
|
|
1630
|
+
|
|
1631
|
+
|
|
1632
|
+
|
|
1633
|
+
exports.DEFAULT_ADJUSTMENTS = DEFAULT_ADJUSTMENTS; exports.DEFAULT_DEPTH_MASK = DEFAULT_DEPTH_MASK; exports.DEFAULT_TRANSFORM = DEFAULT_TRANSFORM; exports.ImageEditorController = ImageEditorController; exports.ImageEditorRenderer = ImageEditorRenderer; exports.applyAdjustments = applyAdjustments; exports.applyDepthMask = applyDepthMask; exports.applyImageProjectionToContext = applyImageProjectionToContext; exports.calculateCropDrag = calculateCropDrag; exports.canvasToImagePoint = canvasToImagePoint; exports.clampCropToImage = clampCropToImage; exports.getAnnotationBounds = getAnnotationBounds; exports.getCropCursor = getCropCursor; exports.getRotatedSize = getRotatedSize; exports.getViewportMetrics = getViewportMetrics; exports.getViewportProjection = getViewportProjection; exports.globalImageCache = globalImageCache; exports.hitTestAnnotation = hitTestAnnotation; exports.hitTestCrop = hitTestCrop; exports.imageToCanvasPoint = imageToCanvasPoint; exports.isQuarterTurnSwapped = isQuarterTurnSwapped; exports.mapCropHandle = mapCropHandle; exports.normalizeAngle = normalizeAngle; exports.projectImagePoint = projectImagePoint; exports.projectImageRect = projectImageRect; exports.removeAlpha = removeAlpha; exports.unprojectCanvasPoint = unprojectCanvasPoint;
|
|
1399
1634
|
//# sourceMappingURL=index.cjs.map
|