mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
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>
This commit is contained in:
parent
593f036c0d
commit
7f7fec2cea
14 changed files with 1491 additions and 5 deletions
|
|
@ -0,0 +1,159 @@
|
|||
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 };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue