forked from immich-app/immich
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(web): image editor - panel and cropping (immich-app#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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> Co-authored-by: Alex Tran <[email protected]>
- Loading branch information
1 parent
8a12a0c
commit dfc2fed
Showing
14 changed files
with
1,491 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
200 changes: 200 additions & 0 deletions
200
web/src/lib/components/asset-viewer/editor/crop-tool/crop-area.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> |
Oops, something went wrong.