immich/web/src/lib/components/asset-viewer/editor/crop-tool/crop-settings.ts
ilyaChuk 7f7fec2cea
feat(web): image editor - panel and cropping (#11074)
* cropping, panel

* fix presets

* types

* prettier

* fix lint

* fix aspect ratio, performance optimization

* improved tool selection, removed placeholder

* fix the mouse's exit from canvas

* fix error

* the "save" button and change tracking

* lint, format

* the mini functionality of the save button

* fix aspect ratio

* hide editor button on mobiles

* strict equality

Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>

* Use the dollar sign syntax for stores inside components

* unobtrusive grid lines, circles at the corners

* more correct image load, handleError

* more strict equality

* fix styles. unused and tailwind

Co-Authored-By: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>

* dont store isShowEditor

* if showEditor - hide navbar & shortcuts

* crop-canvas decomposition (danger)

I could have accidentally broken something.. but I checked the work and it seems ok.

* fix lint

* fix ts

* callback function as props

* correctly disabling shortcuts

* convenient canvas borders

• you can use the mouse to go beyond the boundaries and freely change the crop.
• the circles on the corners of the canvas are not cut off.

* -the editor button for video files, -save button

* hide editor btn if panoramic || gif || live

* corners instead of circles (preview), fix lint&format

* confirm close editor without save

* vertical aspect ratios

* recovery after merge. editor's closing shortcut

* fix format

* move from canvas to html elements

* fix changes detections

* rotation

* hide detail panel if showing editor

* fix aspect ratios near min size

* fix crop area when changing image size when rotate

* fix of fix

* better layout - grouping

https://github.com/user-attachments/assets/48f15172-9666-4588-acb6-3cb5eda873a8

* hide the button

* fix i18n, format

* hide button

* hide button v2

---------

Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-08-14 09:54:50 -05:00

159 lines
3.8 KiB
TypeScript

import type { CropAspectRatio, CropSettings } from '$lib/stores/asset-editor.store';
import { get } from 'svelte/store';
import { cropAreaEl } from './crop-store';
import { checkEdits } from './mouse-handlers';
export function recalculateCrop(
crop: CropSettings,
canvas: HTMLElement,
aspectRatio: CropAspectRatio,
returnNewCrop = false,
): CropSettings | null {
const canvasW = canvas.clientWidth;
const canvasH = canvas.clientHeight;
let newWidth = crop.width;
let newHeight = crop.height;
const { newWidth: w, newHeight: h } = keepAspectRatio(newWidth, newHeight, aspectRatio);
if (w > canvasW) {
newWidth = canvasW;
newHeight = canvasW / (w / h);
} else if (h > canvasH) {
newHeight = canvasH;
newWidth = canvasH * (w / h);
} else {
newWidth = w;
newHeight = h;
}
const newX = Math.max(0, Math.min(crop.x, canvasW - newWidth));
const newY = Math.max(0, Math.min(crop.y, canvasH - newHeight));
const newCrop = {
width: newWidth,
height: newHeight,
x: newX,
y: newY,
};
if (returnNewCrop) {
setTimeout(() => {
checkEdits();
}, 1);
return newCrop;
} else {
crop.width = newWidth;
crop.height = newHeight;
crop.x = newX;
crop.y = newY;
return null;
}
}
export function animateCropChange(crop: CropSettings, newCrop: CropSettings, draw: () => void, duration = 100) {
const cropArea = get(cropAreaEl);
if (!cropArea) {
return;
}
const cropFrame = cropArea.querySelector('.crop-frame') as HTMLElement;
if (!cropFrame) {
return;
}
const startTime = performance.now();
const initialCrop = { ...crop };
const animate = (currentTime: number) => {
const elapsedTime = currentTime - startTime;
const progress = Math.min(elapsedTime / duration, 1);
crop.x = initialCrop.x + (newCrop.x - initialCrop.x) * progress;
crop.y = initialCrop.y + (newCrop.y - initialCrop.y) * progress;
crop.width = initialCrop.width + (newCrop.width - initialCrop.width) * progress;
crop.height = initialCrop.height + (newCrop.height - initialCrop.height) * progress;
draw();
if (progress < 1) {
requestAnimationFrame(animate);
}
};
requestAnimationFrame(animate);
}
export function keepAspectRatio(newWidth: number, newHeight: number, aspectRatio: CropAspectRatio) {
const [widthRatio, heightRatio] = aspectRatio.split(':').map(Number);
if (widthRatio && heightRatio) {
const calculatedWidth = (newHeight * widthRatio) / heightRatio;
return { newWidth: calculatedWidth, newHeight };
}
return { newWidth, newHeight };
}
export function adjustDimensions(
newWidth: number,
newHeight: number,
aspectRatio: CropAspectRatio,
xLimit: number,
yLimit: number,
minSize: number,
) {
let w = newWidth;
let h = newHeight;
let aspectMultiplier: number;
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;
}
if (w > xLimit) {
w = xLimit;
if (aspectRatio !== 'free') {
h = w / aspectMultiplier;
}
}
if (h > yLimit) {
h = yLimit;
if (aspectRatio !== 'free') {
w = h * aspectMultiplier;
}
}
if (w < minSize) {
w = minSize;
if (aspectRatio !== 'free') {
h = w / aspectMultiplier;
}
}
if (h < minSize) {
h = minSize;
if (aspectRatio !== 'free') {
w = h * aspectMultiplier;
}
}
if (aspectRatio !== 'free' && w / h !== aspectMultiplier) {
if (w < minSize) {
h = w / aspectMultiplier;
}
if (h < minSize) {
w = h * aspectMultiplier;
}
}
return { newWidth: w, newHeight: h };
}