diff --git a/i18n/en.json b/i18n/en.json
index fa2fb345ef..c3b998ec13 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -1007,6 +1007,8 @@
"editor_edits_applied_success": "Edits applied successfully",
"editor_flip_horizontal": "Flip horizontal",
"editor_flip_vertical": "Flip vertical",
+ "editor_handle_corner": "{corner, select, top_left {Top-left} top_right {Top-right} bottom_left {Bottom-left} bottom_right {Bottom-right} other {A}} corner handle",
+ "editor_handle_edge": "{edge, select, top {Top} bottom {Bottom} left {Left} right {Right} other {An}} edge handle",
"editor_orientation": "Orientation",
"editor_reset_all_changes": "Reset changes",
"editor_rotate_left": "Rotate 90° counterclockwise",
diff --git a/web/src/lib/components/asset-viewer/editor/transform-tool/crop-area.spec.ts b/web/src/lib/components/asset-viewer/editor/transform-tool/crop-area.spec.ts
deleted file mode 100644
index d0d7f99ad3..0000000000
--- a/web/src/lib/components/asset-viewer/editor/transform-tool/crop-area.spec.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-import { getResizeObserverMock } from '$lib/__mocks__/resize-observer.mock';
-import CropArea from '$lib/components/asset-viewer/editor/transform-tool/crop-area.svelte';
-import { transformManager } from '$lib/managers/edit/transform-manager.svelte';
-import { getAssetMediaUrl } from '$lib/utils';
-import { assetFactory } from '@test-data/factories/asset-factory';
-import { render } from '@testing-library/svelte';
-import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
-
-vi.mock('$lib/utils');
-
-describe('CropArea', () => {
- beforeAll(() => {
- vi.stubGlobal('ResizeObserver', getResizeObserverMock());
- vi.mocked(getAssetMediaUrl).mockReturnValue('/mock-image.jpg');
- });
-
- afterEach(() => {
- transformManager.reset();
- });
-
- it('clears cursor styles on reset', () => {
- const asset = assetFactory.build();
- const { getByRole } = render(CropArea, { asset });
- const cropArea = getByRole('button', { name: 'Crop area' });
-
- transformManager.region = { x: 100, y: 100, width: 200, height: 200 };
- transformManager.cropImageSize = { width: 1000, height: 1000 };
- transformManager.cropImageScale = 1;
- transformManager.updateCursor(100, 150);
-
- expect(document.body.style.cursor).toBe('ew-resize');
- expect(cropArea.style.cursor).toBe('ew-resize');
-
- transformManager.reset();
-
- expect(document.body.style.cursor).toBe('');
- expect(cropArea.style.cursor).toBe('');
- });
-
- it('sets cursor style at x: $x, y: $y to be $cursor', () => {
- const data = [
- { x: 299, y: 84, cursor: '' },
- { x: 299, y: 85, cursor: 'nesw-resize' },
- { x: 299, y: 115, cursor: 'nesw-resize' },
- { x: 299, y: 116, cursor: 'ew-resize' },
- { x: 299, y: 284, cursor: 'ew-resize' },
- { x: 299, y: 285, cursor: 'nwse-resize' },
- { x: 299, y: 300, cursor: 'nwse-resize' },
- { x: 299, y: 301, cursor: '' },
- { x: 300, y: 84, cursor: '' },
- { x: 300, y: 85, cursor: 'nesw-resize' },
- { x: 300, y: 86, cursor: 'nesw-resize' },
- { x: 300, y: 114, cursor: 'nesw-resize' },
- { x: 300, y: 115, cursor: 'nesw-resize' },
- { x: 300, y: 116, cursor: 'ew-resize' },
- { x: 300, y: 284, cursor: 'ew-resize' },
- { x: 300, y: 285, cursor: 'nwse-resize' },
- { x: 300, y: 286, cursor: 'nwse-resize' },
- { x: 300, y: 300, cursor: 'nwse-resize' },
- { x: 300, y: 301, cursor: '' },
- { x: 301, y: 300, cursor: '' },
- { x: 301, y: 301, cursor: '' },
- ];
-
- const element = document.createElement('div');
-
- for (const { x, y, cursor } of data) {
- const message = `x: ${x}, y: ${y} - ${cursor}`;
- transformManager.reset();
- transformManager.region = { x: 100, y: 100, width: 200, height: 200 };
- transformManager.cropImageSize = { width: 600, height: 600 };
- transformManager.cropAreaEl = element;
- transformManager.cropImageScale = 0.5;
- transformManager.updateCursor(x, y);
- expect(element.style.cursor, message).toBe(cursor);
- expect(document.body.style.cursor, message).toBe(cursor);
- }
- });
-});
diff --git a/web/src/lib/components/asset-viewer/editor/transform-tool/crop-area.svelte b/web/src/lib/components/asset-viewer/editor/transform-tool/crop-area.svelte
index 7a84612fe8..011f30d445 100644
--- a/web/src/lib/components/asset-viewer/editor/transform-tool/crop-area.svelte
+++ b/web/src/lib/components/asset-viewer/editor/transform-tool/crop-area.svelte
@@ -1,9 +1,12 @@
-
diff --git a/web/src/lib/managers/edit/transform-manager.svelte.ts b/web/src/lib/managers/edit/transform-manager.svelte.ts
index 77290d3e6d..652cd0bee9 100644
--- a/web/src/lib/managers/edit/transform-manager.svelte.ts
+++ b/web/src/lib/managers/edit/transform-manager.svelte.ts
@@ -1,9 +1,10 @@
-import { editManager, type EditActions, type EditToolManager } from '$lib/managers/edit/edit-manager.svelte';
+import { type EditActions, type EditToolManager } from '$lib/managers/edit/edit-manager.svelte';
import { getAssetMediaUrl } from '$lib/utils';
import { getDimensions } from '$lib/utils/asset-utils';
import { normalizeTransformEdits } from '$lib/utils/editor';
import { handleError } from '$lib/utils/handle-error';
import { AssetEditAction, AssetMediaSize, MirrorAxis, type AssetResponseDto, type CropParameters } from '@immich/sdk';
+import { clamp } from 'lodash-es';
import { tick } from 'svelte';
export type CropAspectRatio =
@@ -37,17 +38,27 @@ type RegionConvertParams = {
to: ImageDimensions;
};
+export enum ResizeBoundary {
+ None = 'none',
+ TopLeft = 'top-left',
+ TopRight = 'top-right',
+ BottomLeft = 'bottom-left',
+ BottomRight = 'bottom-right',
+ Left = 'left',
+ Right = 'right',
+ Top = 'top',
+ Bottom = 'bottom',
+}
+
class TransformManager implements EditToolManager {
canReset: boolean = $derived.by(() => this.checkEdits());
hasChanges: boolean = $state(false);
- darkenLevel = $state(0.65);
isInteracting = $state(false);
isDragging = $state(false);
animationFrame = $state | null>(null);
- canvasCursor = $state('default');
- dragOffset = $state({ x: 0, y: 0 });
- resizeSide = $state('');
+ dragAnchor = $state({ x: 0, y: 0 });
+ resizeSide = $state(ResizeBoundary.None);
imgElement = $state(null);
cropAreaEl = $state(null);
overlayEl = $state(null);
@@ -69,7 +80,6 @@ class TransformManager implements EditToolManager {
const newAngle = this.imageRotation % 360;
return newAngle < 0 ? newAngle + 360 : newAngle;
});
- orientationChanged = $derived.by(() => this.normalizedRotation % 180 > 0);
edits = $derived.by(() => this.getEdits());
@@ -81,9 +91,9 @@ class TransformManager implements EditToolManager {
return;
}
- const newCrop = transformManager.recalculateCrop(aspectRatio);
+ const newCrop = this.recalculateCrop(aspectRatio);
if (newCrop) {
- transformManager.animateCropChange(this.cropAreaEl, this.region, newCrop);
+ this.animateCropChange(newCrop);
this.region = newCrop;
}
}
@@ -216,17 +226,11 @@ class TransformManager implements EditToolManager {
}
reset() {
- this.darkenLevel = 0.65;
this.isInteracting = false;
this.animationFrame = null;
- this.canvasCursor = 'default';
- this.dragOffset = { x: 0, y: 0 };
- this.resizeSide = '';
+ this.dragAnchor = { x: 0, y: 0 };
+ this.resizeSide = ResizeBoundary.None;
this.imgElement = null;
- if (this.cropAreaEl) {
- this.cropAreaEl.style.cursor = '';
- }
- document.body.style.cursor = '';
this.cropAreaEl = null;
this.isDragging = false;
this.overlayEl = null;
@@ -295,12 +299,12 @@ class TransformManager implements EditToolManager {
};
}
- animateCropChange(element: HTMLElement, from: Region, to: Region, duration = 100) {
- const cropFrame = element.querySelector('.crop-frame') as HTMLElement;
- if (!cropFrame) {
+ animateCropChange(to: Region, duration = 100) {
+ if (!this.cropFrame) {
return;
}
+ const from = this.region;
const startTime = performance.now();
const initialCrop = { ...from };
@@ -334,28 +338,6 @@ class TransformManager implements EditToolManager {
return { newWidth, newHeight };
}
- // Calculate constrained dimensions based on aspect ratio and limits
- getConstrainedDimensions(
- desiredWidth: number,
- desiredHeight: number,
- maxWidth: number,
- maxHeight: number,
- minSize = 50,
- ) {
- const { newWidth, newHeight } = this.adjustDimensions(
- desiredWidth,
- desiredHeight,
- this.cropAspectRatio,
- maxWidth,
- maxHeight,
- minSize,
- );
- return {
- width: Math.max(minSize, Math.min(newWidth, maxWidth)),
- height: Math.max(minSize, Math.min(newHeight, maxHeight)),
- };
- }
-
adjustDimensions(
newWidth: number,
newHeight: number,
@@ -364,49 +346,45 @@ class TransformManager implements EditToolManager {
yLimit: number,
minSize: number,
) {
+ if (aspectRatio === 'free') {
+ return {
+ newWidth: clamp(newWidth, minSize, xLimit),
+ newHeight: clamp(newHeight, minSize, yLimit),
+ };
+ }
+
let w = newWidth;
let h = newHeight;
- let aspectMultiplier: number;
+ const [ratioWidth, ratioHeight] = aspectRatio.split(':').map(Number);
+ const aspectMultiplier = ratioWidth && ratioHeight ? ratioWidth / ratioHeight : w / h;
- if (aspectRatio === 'free') {
- aspectMultiplier = newWidth / newHeight;
- } else {
- const [widthRatio, heightRatio] = aspectRatio.split(':').map(Number);
- aspectMultiplier = widthRatio && heightRatio ? widthRatio / heightRatio : newWidth / newHeight;
- }
-
- if (aspectRatio !== 'free') {
- h = w / aspectMultiplier;
+ h = w / aspectMultiplier;
+ // When dragging a corner, use the biggest region that fits 'inside' the mouse location.
+ if (h < newHeight) {
+ h = newHeight;
+ w = h * aspectMultiplier;
}
if (w > xLimit) {
w = xLimit;
- if (aspectRatio !== 'free') {
- h = w / aspectMultiplier;
- }
+ h = w / aspectMultiplier;
}
if (h > yLimit) {
h = yLimit;
- if (aspectRatio !== 'free') {
- w = h * aspectMultiplier;
- }
+ w = h * aspectMultiplier;
}
if (w < minSize) {
w = minSize;
- if (aspectRatio !== 'free') {
- h = w / aspectMultiplier;
- }
+ h = w / aspectMultiplier;
}
if (h < minSize) {
h = minSize;
- if (aspectRatio !== 'free') {
- w = h * aspectMultiplier;
- }
+ w = h * aspectMultiplier;
}
- if (aspectRatio !== 'free' && w / h !== aspectMultiplier) {
+ if (w / h !== aspectMultiplier) {
if (w < minSize) {
h = w / aspectMultiplier;
}
@@ -428,10 +406,6 @@ class TransformManager implements EditToolManager {
this.cropFrame.style.width = `${crop.width}px`;
this.cropFrame.style.height = `${crop.height}px`;
- this.drawOverlay(crop);
- }
-
- drawOverlay(crop: Region) {
if (!this.overlayEl) {
return;
}
@@ -465,7 +439,6 @@ class TransformManager implements EditToolManager {
const cropFrameEl = this.cropFrame;
cropFrameEl?.classList.add('transition');
this.region = this.normalizeCropArea(scale);
- cropFrameEl?.classList.add('transition');
cropFrameEl?.addEventListener('transitionend', () => cropFrameEl?.classList.remove('transition'), {
passive: true,
});
@@ -540,7 +513,7 @@ class TransformManager implements EditToolManager {
normalizeCropArea(scale: number) {
const img = this.imgElement;
if (!img) {
- return { ...this.region };
+ return this.region;
}
const scaleRatio = scale / this.cropImageScale;
@@ -576,38 +549,17 @@ class TransformManager implements EditToolManager {
this.draw();
}
- handleMouseDown(e: MouseEvent) {
- const canvas = this.cropAreaEl;
- if (!canvas) {
+ handleMouseDownOn(e: MouseEvent, resizeBoundary: ResizeBoundary) {
+ if (e.button !== 0) {
return;
}
- const { mouseX, mouseY } = this.getMousePosition(e);
-
- const {
- onLeftBoundary,
- onRightBoundary,
- onTopBoundary,
- onBottomBoundary,
- onTopLeftCorner,
- onTopRightCorner,
- onBottomLeftCorner,
- onBottomRightCorner,
- } = this.isOnCropBoundary(mouseX, mouseY);
-
- if (
- onTopLeftCorner ||
- onTopRightCorner ||
- onBottomLeftCorner ||
- onBottomRightCorner ||
- onLeftBoundary ||
- onRightBoundary ||
- onTopBoundary ||
- onBottomBoundary
- ) {
- this.setResizeSide(mouseX, mouseY);
- } else if (this.isInCropArea(mouseX, mouseY)) {
- this.startDragging(mouseX, mouseY);
+ this.isInteracting = true;
+ this.resizeSide = resizeBoundary;
+ if (resizeBoundary === ResizeBoundary.None) {
+ this.isDragging = true;
+ const { mouseX, mouseY } = this.getMousePosition(e);
+ this.dragAnchor = { x: mouseX - this.region.x, y: mouseY - this.region.y };
}
document.body.style.userSelect = 'none';
@@ -615,20 +567,16 @@ class TransformManager implements EditToolManager {
}
handleMouseMove(e: MouseEvent) {
- const canvas = this.cropAreaEl;
- if (!canvas) {
+ if (!this.cropAreaEl) {
return;
}
- const resizeSideValue = this.resizeSide;
const { mouseX, mouseY } = this.getMousePosition(e);
if (this.isDragging) {
this.moveCrop(mouseX, mouseY);
- } else if (resizeSideValue) {
+ } else if (this.resizeSide !== ResizeBoundary.None) {
this.resizeCrop(mouseX, mouseY);
- } else {
- this.updateCursor(mouseX, mouseY);
}
}
@@ -638,131 +586,42 @@ class TransformManager implements EditToolManager {
this.isInteracting = false;
this.isDragging = false;
- this.resizeSide = '';
- this.fadeOverlay(true); // Darken the background
+ this.resizeSide = ResizeBoundary.None;
}
getMousePosition(e: MouseEvent) {
- let offsetX = e.clientX;
- let offsetY = e.clientY;
- const clienRect = this.cropAreaEl?.getBoundingClientRect();
- const rotateDeg = this.normalizedRotation;
-
- if (rotateDeg == 90) {
- offsetX = e.clientY - (clienRect?.top ?? 0);
- offsetY = window.innerWidth - e.clientX - (window.innerWidth - (clienRect?.right ?? 0));
- } else if (rotateDeg == 180) {
- offsetX = window.innerWidth - e.clientX - (window.innerWidth - (clienRect?.right ?? 0));
- offsetY = window.innerHeight - e.clientY - (window.innerHeight - (clienRect?.bottom ?? 0));
- } else if (rotateDeg == 270) {
- offsetX = window.innerHeight - e.clientY - (window.innerHeight - (clienRect?.bottom ?? 0));
- offsetY = e.clientX - (clienRect?.left ?? 0);
- } else if (rotateDeg == 0) {
- offsetX -= clienRect?.left ?? 0;
- offsetY -= clienRect?.top ?? 0;
+ if (!this.cropAreaEl) {
+ throw new Error('Crop area is undefined');
}
- return { mouseX: offsetX, mouseY: offsetY };
- }
+ const clientRect = this.cropAreaEl.getBoundingClientRect();
- // Boundary detection helpers
- private isInRange(value: number, target: number, sensitivity: number): boolean {
- return value >= target - sensitivity && value <= target + sensitivity;
- }
-
- private isWithinBounds(value: number, min: number, max: number): boolean {
- return value >= min && value <= max;
- }
-
- isOnCropBoundary(mouseX: number, mouseY: number) {
- const { x, y, width, height } = this.region;
- const sensitivity = 10;
- const cornerSensitivity = 15;
- const { width: imgWidth, height: imgHeight } = this.previewImageSize;
-
- const outOfBound = mouseX > imgWidth || mouseY > imgHeight || mouseX < 0 || mouseY < 0;
- if (outOfBound) {
- return {
- onLeftBoundary: false,
- onRightBoundary: false,
- onTopBoundary: false,
- onBottomBoundary: false,
- onTopLeftCorner: false,
- onTopRightCorner: false,
- onBottomLeftCorner: false,
- onBottomRightCorner: false,
- };
+ switch (this.normalizedRotation) {
+ case 90: {
+ return {
+ mouseX: e.clientY - clientRect.top,
+ mouseY: -e.clientX + clientRect.right,
+ };
+ }
+ case 180: {
+ return {
+ mouseX: -e.clientX + clientRect.right,
+ mouseY: -e.clientY + clientRect.bottom,
+ };
+ }
+ case 270: {
+ return {
+ mouseX: -e.clientY + clientRect.bottom,
+ mouseY: e.clientX - clientRect.left,
+ };
+ }
+ // also case 0:
+ default: {
+ return {
+ mouseX: e.clientX - clientRect.left,
+ mouseY: e.clientY - clientRect.top,
+ };
+ }
}
-
- const onLeftBoundary = this.isInRange(mouseX, x, sensitivity) && this.isWithinBounds(mouseY, y, y + height);
- const onRightBoundary =
- this.isInRange(mouseX, x + width, sensitivity) && this.isWithinBounds(mouseY, y, y + height);
- const onTopBoundary = this.isInRange(mouseY, y, sensitivity) && this.isWithinBounds(mouseX, x, x + width);
- const onBottomBoundary =
- this.isInRange(mouseY, y + height, sensitivity) && this.isWithinBounds(mouseX, x, x + width);
-
- const onTopLeftCorner =
- this.isInRange(mouseX, x, cornerSensitivity) && this.isInRange(mouseY, y, cornerSensitivity);
- const onTopRightCorner =
- this.isInRange(mouseX, x + width, cornerSensitivity) && this.isInRange(mouseY, y, cornerSensitivity);
- const onBottomLeftCorner =
- this.isInRange(mouseX, x, cornerSensitivity) && this.isInRange(mouseY, y + height, cornerSensitivity);
- const onBottomRightCorner =
- this.isInRange(mouseX, x + width, cornerSensitivity) && this.isInRange(mouseY, y + height, cornerSensitivity);
-
- return {
- onLeftBoundary,
- onRightBoundary,
- onTopBoundary,
- onBottomBoundary,
- onTopLeftCorner,
- onTopRightCorner,
- onBottomLeftCorner,
- onBottomRightCorner,
- };
- }
-
- isInCropArea(mouseX: number, mouseY: number) {
- const { x, y, width, height } = this.region;
- return mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height;
- }
-
- setResizeSide(mouseX: number, mouseY: number) {
- const {
- onLeftBoundary,
- onRightBoundary,
- onTopBoundary,
- onBottomBoundary,
- onTopLeftCorner,
- onTopRightCorner,
- onBottomLeftCorner,
- onBottomRightCorner,
- } = this.isOnCropBoundary(mouseX, mouseY);
-
- if (onTopLeftCorner) {
- this.resizeSide = 'top-left';
- } else if (onTopRightCorner) {
- this.resizeSide = 'top-right';
- } else if (onBottomLeftCorner) {
- this.resizeSide = 'bottom-left';
- } else if (onBottomRightCorner) {
- this.resizeSide = 'bottom-right';
- } else if (onLeftBoundary) {
- this.resizeSide = 'left';
- } else if (onRightBoundary) {
- this.resizeSide = 'right';
- } else if (onTopBoundary) {
- this.resizeSide = 'top';
- } else if (onBottomBoundary) {
- this.resizeSide = 'bottom';
- }
- }
-
- startDragging(mouseX: number, mouseY: number) {
- this.isDragging = true;
- const crop = this.region;
- this.isInteracting = true;
- this.dragOffset = { x: mouseX - crop.x, y: mouseY - crop.y };
- this.fadeOverlay(false);
}
moveCrop(mouseX: number, mouseY: number) {
@@ -772,102 +631,116 @@ class TransformManager implements EditToolManager {
}
this.hasChanges = true;
- const newX = Math.max(0, Math.min(mouseX - this.dragOffset.x, cropArea.clientWidth - this.region.width));
- const newY = Math.max(0, Math.min(mouseY - this.dragOffset.y, cropArea.clientHeight - this.region.height));
-
- this.region = {
- ...this.region,
- x: newX,
- y: newY,
- };
+ this.region.x = clamp(mouseX - this.dragAnchor.x, 0, cropArea.clientWidth - this.region.width);
+ this.region.y = clamp(mouseY - this.dragAnchor.y, 0, cropArea.clientHeight - this.region.height);
this.draw();
}
resizeCrop(mouseX: number, mouseY: number) {
const canvas = this.cropAreaEl;
- const crop = this.region;
- const resizeSideValue = this.resizeSide;
- if (!canvas || !resizeSideValue) {
+ const currentCrop = this.region;
+ if (!canvas) {
return;
}
- this.fadeOverlay(false);
+ this.isInteracting = true;
this.hasChanges = true;
- const { x, y, width, height } = crop;
+ const { x, y, width, height } = currentCrop;
const minSize = 50;
- let newRegion = { ...crop };
+ let newRegion = { ...currentCrop };
- switch (resizeSideValue) {
- case 'left': {
- const desiredWidth = width + (x - mouseX);
- if (desiredWidth >= minSize && mouseX >= 0) {
- const { newWidth: w, newHeight: h } = this.keepAspectRatio(desiredWidth, height);
- const finalWidth = Math.max(minSize, Math.min(w, canvas.clientWidth));
- const finalHeight = Math.max(minSize, Math.min(h, canvas.clientHeight));
- newRegion = {
- x: Math.max(0, x + width - finalWidth),
- y,
- width: finalWidth,
- height: finalHeight,
- };
- }
+ let desiredWidth = width;
+ let desiredHeight = height;
+
+ // Width
+ switch (this.resizeSide) {
+ case ResizeBoundary.Left:
+ case ResizeBoundary.TopLeft:
+ case ResizeBoundary.BottomLeft: {
+ desiredWidth = Math.max(minSize, width + (x - Math.max(mouseX, 0)));
break;
}
- case 'right': {
- const desiredWidth = mouseX - x;
- if (desiredWidth >= minSize && mouseX <= canvas.clientWidth) {
- const { newWidth: w, newHeight: h } = this.keepAspectRatio(desiredWidth, height);
- newRegion = {
- ...newRegion,
- width: Math.max(minSize, Math.min(w, canvas.clientWidth - x)),
- height: Math.max(minSize, Math.min(h, canvas.clientHeight)),
- };
- }
+ case ResizeBoundary.Right:
+ case ResizeBoundary.TopRight:
+ case ResizeBoundary.BottomRight: {
+ desiredWidth = Math.max(minSize, Math.max(mouseX, 0) - x);
break;
}
- case 'top': {
- const desiredHeight = height + (y - mouseY);
- if (desiredHeight >= minSize && mouseY >= 0) {
- const { newWidth: w, newHeight: h } = this.adjustDimensions(
- width,
- desiredHeight,
- this.cropAspectRatio,
- canvas.clientWidth,
- canvas.clientHeight,
- minSize,
- );
- newRegion = {
- x,
- y: Math.max(0, y + height - h),
- width: w,
- height: h,
- };
- }
+ }
+
+ // Height
+ switch (this.resizeSide) {
+ case ResizeBoundary.Top:
+ case ResizeBoundary.TopLeft:
+ case ResizeBoundary.TopRight: {
+ desiredHeight = Math.max(minSize, height + (y - Math.max(mouseY, 0)));
break;
}
- case 'bottom': {
- const desiredHeight = mouseY - y;
- if (desiredHeight >= minSize && mouseY <= canvas.clientHeight) {
- const { newWidth: w, newHeight: h } = this.adjustDimensions(
- width,
- desiredHeight,
- this.cropAspectRatio,
- canvas.clientWidth,
- canvas.clientHeight - y,
- minSize,
- );
- newRegion = {
- ...newRegion,
- width: w,
- height: h,
- };
- }
+ case ResizeBoundary.Bottom:
+ case ResizeBoundary.BottomLeft:
+ case ResizeBoundary.BottomRight: {
+ desiredHeight = Math.max(minSize, Math.max(mouseY, 0) - y);
break;
}
- case 'top-left': {
- const desiredWidth = width + (x - Math.max(mouseX, 0));
- const desiredHeight = height + (y - Math.max(mouseY, 0));
+ }
+
+ // Old
+ switch (this.resizeSide) {
+ case ResizeBoundary.Left: {
+ const { newWidth: w, newHeight: h } = this.keepAspectRatio(desiredWidth, height);
+ const finalWidth = clamp(w, minSize, canvas.clientWidth);
+ newRegion = {
+ x: Math.max(0, x + width - finalWidth),
+ y,
+ width: finalWidth,
+ height: clamp(h, minSize, canvas.clientHeight),
+ };
+ break;
+ }
+ case ResizeBoundary.Right: {
+ const { newWidth: w, newHeight: h } = this.keepAspectRatio(desiredWidth, height);
+ newRegion = {
+ ...newRegion,
+ width: clamp(w, minSize, canvas.clientWidth - x),
+ height: clamp(h, minSize, canvas.clientHeight),
+ };
+ break;
+ }
+ case ResizeBoundary.Top: {
+ const { newWidth: w, newHeight: h } = this.adjustDimensions(
+ desiredWidth,
+ desiredHeight,
+ this.cropAspectRatio,
+ canvas.clientWidth,
+ canvas.clientHeight,
+ minSize,
+ );
+ newRegion = {
+ x,
+ y: Math.max(0, y + height - h),
+ width: w,
+ height: h,
+ };
+ break;
+ }
+ case ResizeBoundary.Bottom: {
+ const { newWidth: w, newHeight: h } = this.adjustDimensions(
+ desiredWidth,
+ desiredHeight,
+ this.cropAspectRatio,
+ canvas.clientWidth,
+ canvas.clientHeight - y,
+ minSize,
+ );
+ newRegion = {
+ ...newRegion,
+ width: w,
+ height: h,
+ };
+ break;
+ }
+ case ResizeBoundary.TopLeft: {
const { newWidth: w, newHeight: h } = this.adjustDimensions(
desiredWidth,
desiredHeight,
@@ -884,9 +757,7 @@ class TransformManager implements EditToolManager {
};
break;
}
- case 'top-right': {
- const desiredWidth = Math.max(mouseX, 0) - x;
- const desiredHeight = height + (y - Math.max(mouseY, 0));
+ case ResizeBoundary.TopRight: {
const { newWidth: w, newHeight: h } = this.adjustDimensions(
desiredWidth,
desiredHeight,
@@ -903,9 +774,7 @@ class TransformManager implements EditToolManager {
};
break;
}
- case 'bottom-left': {
- const desiredWidth = width + (x - Math.max(mouseX, 0));
- const desiredHeight = Math.max(mouseY, 0) - y;
+ case ResizeBoundary.BottomLeft: {
const { newWidth: w, newHeight: h } = this.adjustDimensions(
desiredWidth,
desiredHeight,
@@ -922,9 +791,7 @@ class TransformManager implements EditToolManager {
};
break;
}
- case 'bottom-right': {
- const desiredWidth = Math.max(mouseX, 0) - x;
- const desiredHeight = Math.max(mouseY, 0) - y;
+ case ResizeBoundary.BottomRight: {
const { newWidth: w, newHeight: h } = this.adjustDimensions(
desiredWidth,
desiredHeight,
@@ -952,95 +819,6 @@ class TransformManager implements EditToolManager {
this.draw();
}
- updateCursor(mouseX: number, mouseY: number) {
- if (!this.cropAreaEl) {
- return;
- }
-
- let {
- onLeftBoundary,
- onRightBoundary,
- onTopBoundary,
- onBottomBoundary,
- onTopLeftCorner,
- onTopRightCorner,
- onBottomLeftCorner,
- onBottomRightCorner,
- } = this.isOnCropBoundary(mouseX, mouseY);
-
- if (this.normalizedRotation == 90) {
- [onTopBoundary, onRightBoundary, onBottomBoundary, onLeftBoundary] = [
- onLeftBoundary,
- onTopBoundary,
- onRightBoundary,
- onBottomBoundary,
- ];
-
- [onTopLeftCorner, onTopRightCorner, onBottomRightCorner, onBottomLeftCorner] = [
- onBottomLeftCorner,
- onTopLeftCorner,
- onTopRightCorner,
- onBottomRightCorner,
- ];
- } else if (this.normalizedRotation == 180) {
- [onTopBoundary, onBottomBoundary] = [onBottomBoundary, onTopBoundary];
- [onLeftBoundary, onRightBoundary] = [onRightBoundary, onLeftBoundary];
-
- [onTopLeftCorner, onBottomRightCorner] = [onBottomRightCorner, onTopLeftCorner];
- [onTopRightCorner, onBottomLeftCorner] = [onBottomLeftCorner, onTopRightCorner];
- } else if (this.normalizedRotation == 270) {
- [onTopBoundary, onRightBoundary, onBottomBoundary, onLeftBoundary] = [
- onRightBoundary,
- onBottomBoundary,
- onLeftBoundary,
- onTopBoundary,
- ];
-
- [onTopLeftCorner, onTopRightCorner, onBottomRightCorner, onBottomLeftCorner] = [
- onTopRightCorner,
- onBottomRightCorner,
- onBottomLeftCorner,
- onTopLeftCorner,
- ];
- }
-
- let cursorName: string;
- if (onTopLeftCorner || onBottomRightCorner) {
- cursorName = 'nwse-resize';
- } else if (onTopRightCorner || onBottomLeftCorner) {
- cursorName = 'nesw-resize';
- } else if (onLeftBoundary || onRightBoundary) {
- cursorName = 'ew-resize';
- } else if (onTopBoundary || onBottomBoundary) {
- cursorName = 'ns-resize';
- } else if (this.isInCropArea(mouseX, mouseY)) {
- cursorName = 'move';
- } else {
- cursorName = 'default';
- }
-
- if (this.canvasCursor != cursorName && this.cropAreaEl && !editManager.isShowingConfirmDialog) {
- this.canvasCursor = cursorName;
- document.body.style.cursor = cursorName;
- this.cropAreaEl.style.cursor = cursorName;
- }
- }
-
- fadeOverlay(toDark: boolean) {
- const overlay = this.overlayEl;
- const cropFrame = document.querySelector('.crop-frame');
-
- if (toDark) {
- overlay?.classList.remove('light');
- cropFrame?.classList.remove('resizing');
- } else {
- overlay?.classList.add('light');
- cropFrame?.classList.add('resizing');
- }
-
- this.isInteracting = !toDark;
- }
-
resetCrop() {
this.cropAspectRatio = 'free';
this.region = {