mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +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,200 @@
|
|||
<script lang="ts">
|
||||
import { onMount, afterUpdate, onDestroy, tick } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { getAssetOriginalUrl } from '$lib/utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||
|
||||
import { imgElement, cropAreaEl, resetCropStore, overlayEl, isResizingOrDragging, cropFrame } from './crop-store';
|
||||
import { draw } from './drawing';
|
||||
import { onImageLoad, resizeCanvas } from './image-loading';
|
||||
import { handleMouseDown, handleMouseMove, handleMouseUp } from './mouse-handlers';
|
||||
import { recalculateCrop, animateCropChange } from './crop-settings';
|
||||
import {
|
||||
changedOriention,
|
||||
cropAspectRatio,
|
||||
cropSettings,
|
||||
resetGlobalCropStore,
|
||||
rotateDegrees,
|
||||
} from '$lib/stores/asset-editor.store';
|
||||
|
||||
export let asset;
|
||||
let img: HTMLImageElement;
|
||||
|
||||
$: imgElement.set(img);
|
||||
|
||||
cropAspectRatio.subscribe((value) => {
|
||||
if (!img || !$cropAreaEl) {
|
||||
return;
|
||||
}
|
||||
const newCrop = recalculateCrop($cropSettings, $cropAreaEl, value, true);
|
||||
if (newCrop) {
|
||||
animateCropChange($cropSettings, newCrop, () => draw($cropSettings));
|
||||
}
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
resetGlobalCropStore();
|
||||
img = new Image();
|
||||
await tick();
|
||||
|
||||
img.src = getAssetOriginalUrl({ id: asset.id, checksum: asset.checksum });
|
||||
|
||||
img.addEventListener('load', () => onImageLoad(true));
|
||||
img.addEventListener('error', (error) => {
|
||||
handleError(error, $t('error_loading_image'));
|
||||
});
|
||||
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
resetCropStore();
|
||||
resetGlobalCropStore();
|
||||
});
|
||||
|
||||
afterUpdate(() => {
|
||||
resizeCanvas();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="canvas-container">
|
||||
<button
|
||||
class={`crop-area ${$changedOriention ? 'changedOriention' : ''}`}
|
||||
style={`rotate:${$rotateDegrees}deg`}
|
||||
bind:this={$cropAreaEl}
|
||||
on:mousedown={handleMouseDown}
|
||||
on:mouseup={handleMouseUp}
|
||||
aria-label="Crop area"
|
||||
type="button"
|
||||
>
|
||||
<img draggable="false" src={img?.src} alt={$getAltText(asset)} />
|
||||
<div class={`${$isResizingOrDragging ? 'resizing' : ''} crop-frame`} bind:this={$cropFrame}>
|
||||
<div class="grid"></div>
|
||||
<div class="corner top-left"></div>
|
||||
<div class="corner top-right"></div>
|
||||
<div class="corner bottom-left"></div>
|
||||
<div class="corner bottom-right"></div>
|
||||
</div>
|
||||
<div class={`${$isResizingOrDragging ? 'light' : ''} overlay`} bind:this={$overlayEl}></div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.canvas-container {
|
||||
width: calc(100% - 4rem);
|
||||
margin: auto;
|
||||
margin-top: 2rem;
|
||||
height: calc(100% - 4rem);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.crop-area {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
outline: none;
|
||||
transition: rotate 0.15s ease;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
width: max-content;
|
||||
}
|
||||
.crop-area.changedOriention {
|
||||
max-width: 92vh;
|
||||
max-height: calc(100vw - 400px - 1.5rem);
|
||||
}
|
||||
|
||||
.crop-frame.transition {
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.56);
|
||||
pointer-events: none;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
|
||||
.overlay.light {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.grid {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
--color: white;
|
||||
--shadow: #00000057;
|
||||
background-image: linear-gradient(var(--color) 1px, transparent 0),
|
||||
linear-gradient(90deg, var(--color) 1px, transparent 0), linear-gradient(var(--shadow) 3px, transparent 0),
|
||||
linear-gradient(90deg, var(--shadow) 3px, transparent 0);
|
||||
background-size: calc(100% / 3) calc(100% / 3);
|
||||
opacity: 0;
|
||||
transition: opacity 0.1s ease;
|
||||
}
|
||||
|
||||
.crop-frame.resizing .grid {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.crop-area img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.crop-frame {
|
||||
position: absolute;
|
||||
border: 2px solid white;
|
||||
box-sizing: border-box;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.corner {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
--size: 5.2px;
|
||||
--mSize: calc(-0.5 * var(--size));
|
||||
border: var(--size) solid white;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.top-left {
|
||||
top: var(--mSize);
|
||||
left: var(--mSize);
|
||||
border-right: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.top-right {
|
||||
top: var(--mSize);
|
||||
right: var(--mSize);
|
||||
border-left: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.bottom-left {
|
||||
bottom: var(--mSize);
|
||||
left: var(--mSize);
|
||||
border-right: none;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.bottom-right {
|
||||
bottom: var(--mSize);
|
||||
right: var(--mSize);
|
||||
border-left: none;
|
||||
border-top: none;
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue