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:
ilyaChuk 2024-08-14 17:54:50 +03:00 committed by GitHub
parent 593f036c0d
commit 7f7fec2cea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 1491 additions and 5 deletions

View file

@ -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>