From 6154c9cda361115064fd54dfc4edcf5cb57e474f Mon Sep 17 00:00:00 2001 From: ilyaChuk <86570508+ilyaChuk@users.noreply.github.com> Date: Sat, 13 Jul 2024 02:01:28 +0300 Subject: [PATCH 01/47] cropping, panel --- .../asset-viewer/asset-viewer-nav-bar.svelte | 9 + .../asset-viewer/asset-viewer.svelte | 36 +- .../asset-viewer/editor/CropComponent.svelte | 87 +++ .../asset-viewer/editor/TuneComponent.svelte | 6 + .../asset-viewer/editor/crop-canvas.svelte | 587 ++++++++++++++++++ .../asset-viewer/editor/editor-panel.svelte | 83 +++ web/src/lib/stores/asset-editor.store.ts | 6 + web/src/lib/stores/preferences.store.ts | 1 + 8 files changed, 812 insertions(+), 3 deletions(-) create mode 100644 web/src/lib/components/asset-viewer/editor/CropComponent.svelte create mode 100644 web/src/lib/components/asset-viewer/editor/TuneComponent.svelte create mode 100644 web/src/lib/components/asset-viewer/editor/crop-canvas.svelte create mode 100644 web/src/lib/components/asset-viewer/editor/editor-panel.svelte create mode 100644 web/src/lib/stores/asset-editor.store.ts diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte index fc1239d396153..2c0acc305f139 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte @@ -21,6 +21,7 @@ mdiHeartOutline, mdiHistory, mdiImageAlbum, + mdiImageEditOutline, mdiImageMinusOutline, mdiImageOutline, mdiImageRefreshOutline, @@ -168,6 +169,14 @@ title={asset.isFavorite ? $t('unfavorite') : $t('to_favorite')} /> {/if} + {#if isOwner} + dispatch('showEditorHandler')} + title={$t('editor')} + /> + {/if} {#if isOwner} { + if (isShowActivity) { + isShowActivity = false; + } + $isShowEditor = !$isShowEditor; + }; const trashOrDelete = async (force: boolean = false) => { if (force || !isTrashEnabled) { @@ -532,6 +540,12 @@ } }; + let selectedEditType: string = ''; + + function handleUpdateSelectedEditType(event: CustomEvent) { + selectedEditType = event.detail; + } + $: if (!$user) { shouldShowShareModal = false; } @@ -581,6 +595,7 @@ on:delete={() => trashOrDelete()} on:permanentlyDelete={() => trashOrDelete(true)} on:favorite={toggleFavorite} + on:showEditorHandler={showEditorHandler} on:addToAlbum={() => openAlbumPicker(false)} on:restoreAsset={() => handleRestoreAsset()} on:addToSharedAlbum={() => openAlbumPicker(true)} @@ -597,7 +612,7 @@ {/if} - {#if $slideshowState === SlideshowState.None && showNavigation} + {#if $slideshowState === SlideshowState.None && showNavigation && !$isShowEditor}
navigateAsset('previous', e)} label={$t('view_previous_asset')}> @@ -668,7 +683,11 @@ .endsWith('.insp'))} {:else} + {#if $isShowEditor && selectedEditType=='crop'} + + {:else} + {/if} {/if} {:else} - {#if $slideshowState === SlideshowState.None && showNavigation} + {#if $slideshowState === SlideshowState.None && showNavigation && !$isShowEditor}
navigateAsset('next', e)} label={$t('view_next_asset')}> @@ -715,6 +734,17 @@
{/if} + {#if $isShowEditor} +
+ ($isShowEditor = false)} /> +
+ {/if} + {#if $stackAssetsStore.length > 0 && withStacked}
+ import Button from '$lib/components/elements/buttons/button.svelte'; + import Icon from '$lib/components/elements/icon.svelte'; + import { cropAspectRatio, cropImageScale, cropImageSize, cropSettings } from '$lib/stores/asset-editor.store'; + import { mdiBackupRestore, mdiCropFree, mdiSquareOutline } from '@mdi/js'; + import { get } from 'svelte/store'; + + let sizes = [ + { + icon: mdiCropFree, + name: 'free', + viewBox: '0 0 24 24', + }, + { + name: '1:1', + icon: mdiSquareOutline, + viewBox: '0 0 24 24', + }, + { + name: '16:9', + icon: `M200-280q-33 0-56.5-23.5T120-360v-240q0-33 23.5-56.5T200-680h560q33 0 56.5 23.5T840-600v240q0 33-23.5 56.5T760-280H200Zm0-80h560v-240H200v240Zm0 0v-240 240Z`, + viewBox: '50 -700 840 400', + }, + { + name: '3:2', + icon: `M200-240q-33 0-56.5-23.5T120-320v-320q0-33 23.5-56.5T200-720h560q33 0 56.5 23.5T840-640v320q0 33-23.5 56.5T760-240H200Zm0-80h560v-320H200v320Zm0 0v-320 320Z`, + viewBox: '50 -720 840 480', + }, + { + name: '7:5', + icon: `M200-200q-33 0-56.5-23.5T120-280v-400q0-33 23.5-56.5T200-760h560q33 0 56.5 23.5T840-680v400q0 33-23.5 56.5T760-200H200Zm0-80h560v-400H200v400Zm0 0v-400 400Z`, + viewBox: '50 -760 840 560', + }, + { + name: 'reset', + icon: mdiBackupRestore, + viewBox: '0 0 24 24', + }, + ]; + + let selectedSize = 'free'; + + function selectType(size) { + if (size == 'reset') { + selectedSize = 'free' + let cropImageSizeM = get(cropImageSize); + let cropImageScaleM = get(cropImageScale); + cropSettings.set({ + x: 0, + y: 0, + width: cropImageSizeM[0] * cropImageScaleM - 1, + height: cropImageSizeM[1] * cropImageScaleM - 1, + }); + cropAspectRatio.set(selectedSize); + console.log(get(cropSettings)) + return; + } + cropSettings.set(crop); + selectedSize = size; + cropAspectRatio.set(size); + } + + +
+
    + {#each sizes as size (size.name)} +
  • + +
  • + {/each} +
+
+ + diff --git a/web/src/lib/components/asset-viewer/editor/TuneComponent.svelte b/web/src/lib/components/asset-viewer/editor/TuneComponent.svelte new file mode 100644 index 0000000000000..4ce8970636e27 --- /dev/null +++ b/web/src/lib/components/asset-viewer/editor/TuneComponent.svelte @@ -0,0 +1,6 @@ + + +
+

Tune Tool

+
diff --git a/web/src/lib/components/asset-viewer/editor/crop-canvas.svelte b/web/src/lib/components/asset-viewer/editor/crop-canvas.svelte new file mode 100644 index 0000000000000..60a610cba10d7 --- /dev/null +++ b/web/src/lib/components/asset-viewer/editor/crop-canvas.svelte @@ -0,0 +1,587 @@ + + +
+ +
+ + diff --git a/web/src/lib/components/asset-viewer/editor/editor-panel.svelte b/web/src/lib/components/asset-viewer/editor/editor-panel.svelte new file mode 100644 index 0000000000000..7b0d37ed541ac --- /dev/null +++ b/web/src/lib/components/asset-viewer/editor/editor-panel.svelte @@ -0,0 +1,83 @@ + + +
+
+ dispatch('close')} /> +

{$t('editor')}

+
+
+ +
    + {#each editTypes as etype (etype.name)} +
  • + selectType(etype.name)}/> +
  • + {/each} +
+
+
+ +
+
+ + diff --git a/web/src/lib/stores/asset-editor.store.ts b/web/src/lib/stores/asset-editor.store.ts new file mode 100644 index 0000000000000..22076c37c4006 --- /dev/null +++ b/web/src/lib/stores/asset-editor.store.ts @@ -0,0 +1,6 @@ +import { writable } from 'svelte/store'; + +export const cropSettings = writable({ x: 0, y: 0, width: 100, height: 100 }); +export const cropImageSize = writable([1000, 1000]); +export const cropImageScale = writable(1); +export const cropAspectRatio = writable('free'); diff --git a/web/src/lib/stores/preferences.store.ts b/web/src/lib/stores/preferences.store.ts index 11473f80612a6..93ed2b62c4dcd 100644 --- a/web/src/lib/stores/preferences.store.ts +++ b/web/src/lib/stores/preferences.store.ts @@ -77,6 +77,7 @@ export const videoViewerVolume = persisted('video-viewer-volume', 1, {}) export const videoViewerMuted = persisted('video-viewer-muted', false, {}); export const isShowDetail = persisted('info-opened', false, {}); +export const isShowEditor = persisted('editor-opened', false, {}); export interface AlbumViewSettings { view: string; From e23fdaf028df90a514d520fceded64e5e081733f Mon Sep 17 00:00:00 2001 From: ilyaChuk <86570508+ilyaChuk@users.noreply.github.com> Date: Sat, 13 Jul 2024 02:06:41 +0300 Subject: [PATCH 02/47] fix presets --- web/src/lib/components/asset-viewer/editor/CropComponent.svelte | 1 - 1 file changed, 1 deletion(-) diff --git a/web/src/lib/components/asset-viewer/editor/CropComponent.svelte b/web/src/lib/components/asset-viewer/editor/CropComponent.svelte index 9a8a88f243f8e..146c4622720c3 100644 --- a/web/src/lib/components/asset-viewer/editor/CropComponent.svelte +++ b/web/src/lib/components/asset-viewer/editor/CropComponent.svelte @@ -55,7 +55,6 @@ console.log(get(cropSettings)) return; } - cropSettings.set(crop); selectedSize = size; cropAspectRatio.set(size); } From 100f83d9c0521e6dc360d79a050f70ce8867821b Mon Sep 17 00:00:00 2001 From: ilyaChuk <86570508+ilyaChuk@users.noreply.github.com> Date: Sat, 13 Jul 2024 17:47:30 +0300 Subject: [PATCH 03/47] types --- .../asset-viewer/asset-viewer-nav-bar.svelte | 1 + .../asset-viewer/editor/CropComponent.svelte | 2 +- .../asset-viewer/editor/crop-canvas.svelte | 50 ++++++++++++------- .../asset-viewer/editor/editor-panel.svelte | 2 +- web/src/lib/stores/asset-editor.store.ts | 11 +++- 5 files changed, 44 insertions(+), 22 deletions(-) diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte index 2c0acc305f139..e452be34ed5b9 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte @@ -74,6 +74,7 @@ playSlideShow: void; unstack: void; showShareModal: void; + showEditorHandler: void; }; const dispatch = createEventDispatcher(); diff --git a/web/src/lib/components/asset-viewer/editor/CropComponent.svelte b/web/src/lib/components/asset-viewer/editor/CropComponent.svelte index 146c4622720c3..9bb39ae62933a 100644 --- a/web/src/lib/components/asset-viewer/editor/CropComponent.svelte +++ b/web/src/lib/components/asset-viewer/editor/CropComponent.svelte @@ -40,7 +40,7 @@ let selectedSize = 'free'; - function selectType(size) { + function selectType(size:string) { if (size == 'reset') { selectedSize = 'free' let cropImageSizeM = get(cropImageSize); diff --git a/web/src/lib/components/asset-viewer/editor/crop-canvas.svelte b/web/src/lib/components/asset-viewer/editor/crop-canvas.svelte index 60a610cba10d7..0472ffd34390f 100644 --- a/web/src/lib/components/asset-viewer/editor/crop-canvas.svelte +++ b/web/src/lib/components/asset-viewer/editor/crop-canvas.svelte @@ -1,5 +1,5 @@ diff --git a/web/src/lib/components/asset-viewer/editor/CropComponent.svelte b/web/src/lib/components/asset-viewer/editor/crop-tool.svelte similarity index 100% rename from web/src/lib/components/asset-viewer/editor/CropComponent.svelte rename to web/src/lib/components/asset-viewer/editor/crop-tool.svelte diff --git a/web/src/lib/components/asset-viewer/editor/editor-panel.svelte b/web/src/lib/components/asset-viewer/editor/editor-panel.svelte index 5ee677d183f57..b5d0a4576ef41 100644 --- a/web/src/lib/components/asset-viewer/editor/editor-panel.svelte +++ b/web/src/lib/components/asset-viewer/editor/editor-panel.svelte @@ -1,13 +1,12 @@ - -
-

Tune Tool

-
diff --git a/web/src/lib/stores/asset-editor.store.ts b/web/src/lib/stores/asset-editor.store.ts index b20aa0f3368a6..daf5bb68e8656 100644 --- a/web/src/lib/stores/asset-editor.store.ts +++ b/web/src/lib/stores/asset-editor.store.ts @@ -4,7 +4,7 @@ export const cropSettings = writable({ x: 0, y: 0, width: 100, hei export const cropImageSize = writable([1000, 1000]); export const cropImageScale = writable(1); export const cropAspectRatio = writable('free'); -export const cropSettingsChanged = writable(false); +export const cropSettingsChanged = writable(false); export type CropAspectRatio = '1:1' | '16:9' | '3:2' | '7:5' | 'free' | 'reset'; From e4980550fe36593322249b99705441e50f332618 Mon Sep 17 00:00:00 2001 From: ilyaChuk <86570508+ilyaChuk@users.noreply.github.com> Date: Mon, 29 Jul 2024 04:21:41 +0300 Subject: [PATCH 12/47] the mini functionality of the save button --- .../components/asset-viewer/editor/editor-panel.svelte | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/web/src/lib/components/asset-viewer/editor/editor-panel.svelte b/web/src/lib/components/asset-viewer/editor/editor-panel.svelte index cdcc02d51a5c2..a43ad5ffb5b19 100644 --- a/web/src/lib/components/asset-viewer/editor/editor-panel.svelte +++ b/web/src/lib/components/asset-viewer/editor/editor-panel.svelte @@ -1,7 +1,7 @@
@@ -61,7 +65,7 @@
  • -
  • From c5cff019e6bf7c37a6ee23bc11373d6aa0f9a860 Mon Sep 17 00:00:00 2001 From: ilyaChuk <86570508+ilyaChuk@users.noreply.github.com> Date: Mon, 29 Jul 2024 04:54:34 +0300 Subject: [PATCH 13/47] fix aspect ratio --- .../asset-viewer/editor/crop-canvas.svelte | 51 ++++++++++++++----- .../asset-viewer/editor/crop-tool.svelte | 1 + 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/web/src/lib/components/asset-viewer/editor/crop-canvas.svelte b/web/src/lib/components/asset-viewer/editor/crop-canvas.svelte index bbada94b16953..0d4dccc5e1e77 100644 --- a/web/src/lib/components/asset-viewer/editor/crop-canvas.svelte +++ b/web/src/lib/components/asset-viewer/editor/crop-canvas.svelte @@ -460,10 +460,17 @@ newHeight = height + y - mouseY; newWidth = width; if (newHeight >= minSize && mouseY >= 0) { - const { newWidth: w, newHeight: h } = keepAspectRatio(newWidth, newHeight, aspectRatio); - crop.height = Math.min(h, canvas.height); - crop.y = Math.max(0, y + height - crop.height); - crop.width = Math.min(w, canvas.width); + const { newWidth: w, newHeight: h } = adjustDimensions( + newWidth, + newHeight, + aspectRatio, + canvas.width, + canvas.height, + ); + crop.y = Math.max(0, y + height - h); + + crop.width = w; + crop.height = h; } break; } @@ -471,9 +478,15 @@ newHeight = mouseY - y; newWidth = width; if (newHeight >= minSize && mouseY <= canvas.height) { - const { newWidth: w, newHeight: h } = keepAspectRatio(newWidth, newHeight, aspectRatio); - crop.height = Math.min(h, canvas.height - y); - crop.width = Math.min(w, canvas.width); + const { newWidth: w, newHeight: h } = adjustDimensions( + newWidth, + newHeight, + aspectRatio, + canvas.width, + canvas.height - y, + ); + crop.width = w; + crop.height = h; } break; } @@ -481,9 +494,15 @@ newWidth = width + x - mouseX; newHeight = height + y - mouseY; if (newWidth >= minSize && newHeight >= minSize && mouseX >= 0 && mouseY >= 0) { - const { newWidth: w, newHeight: h } = keepAspectRatio(newWidth, newHeight, aspectRatio); - crop.width = Math.min(w, canvas.width); - crop.height = Math.min(h, canvas.height); + const { newWidth: w, newHeight: h } = adjustDimensions( + newWidth, + newHeight, + aspectRatio, + canvas.width, + canvas.height, + ); + crop.width = w; + crop.height = h; crop.x = Math.max(0, x + width - crop.width); crop.y = Math.max(0, y + height - crop.height); } @@ -510,9 +529,15 @@ newWidth = width + x - mouseX; newHeight = mouseY - y; if (newWidth >= minSize && newHeight >= minSize && mouseX >= 0 && mouseY <= canvas.height) { - const { newWidth: w, newHeight: h } = keepAspectRatio(newWidth, newHeight, aspectRatio); - crop.width = Math.min(w, canvas.width); - crop.height = Math.min(h, canvas.height - y); + const { newWidth: w, newHeight: h } = adjustDimensions( + newWidth, + newHeight, + aspectRatio, + canvas.width, + canvas.height - y, + ); + crop.width = w; + crop.height = h; crop.x = Math.max(0, x + width - crop.width); } break; diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool.svelte b/web/src/lib/components/asset-viewer/editor/crop-tool.svelte index 8ad679e4d6655..b7bb2ce187251 100644 --- a/web/src/lib/components/asset-viewer/editor/crop-tool.svelte +++ b/web/src/lib/components/asset-viewer/editor/crop-tool.svelte @@ -45,6 +45,7 @@ ]; let selectedSize: CropAspectRatio = 'free'; + cropAspectRatio.set(selectedSize); function selectType(size: CropAspectRatio) { if (size == 'reset') { From 0f01afea33564f3e113feff26218d3a882544310 Mon Sep 17 00:00:00 2001 From: ilyaChuk <86570508+ilyaChuk@users.noreply.github.com> Date: Mon, 29 Jul 2024 05:04:15 +0300 Subject: [PATCH 14/47] hide editor button on mobiles --- web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte index e452be34ed5b9..f5a3c32b4d7be 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte @@ -173,6 +173,7 @@ {#if isOwner} dispatch('showEditorHandler')} title={$t('editor')} From 019ed53fa14a2210c00ab9829e4415daf8d1692b Mon Sep 17 00:00:00 2001 From: ilyaChuk <86570508+ilyaChuk@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:15:26 +0300 Subject: [PATCH 15/47] strict equality Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> --- web/src/lib/components/asset-viewer/asset-viewer.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 98da97ee9a8d5..87a18a005028e 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -682,7 +682,7 @@ .toLowerCase() .endsWith('.insp'))} - {:else if $isShowEditor && selectedEditType == 'crop'} + {:else if $isShowEditor && selectedEditType === 'crop'} {:else} From 1eb7f1c55f4ad8dfc5139ca942a18481a2419920 Mon Sep 17 00:00:00 2001 From: ilyaChuk <86570508+ilyaChuk@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:26:00 +0300 Subject: [PATCH 16/47] Use the dollar sign syntax for stores inside components --- .../asset-viewer/editor/crop-canvas.svelte | 17 ++++++++--------- .../asset-viewer/editor/crop-tool.svelte | 15 +++++++-------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/web/src/lib/components/asset-viewer/editor/crop-canvas.svelte b/web/src/lib/components/asset-viewer/editor/crop-canvas.svelte index 0d4dccc5e1e77..b26e4e3cad1d6 100644 --- a/web/src/lib/components/asset-viewer/editor/crop-canvas.svelte +++ b/web/src/lib/components/asset-viewer/editor/crop-canvas.svelte @@ -9,11 +9,10 @@ cropSettingsChanged, } from '$lib/stores/asset-editor.store'; import { onMount, afterUpdate } from 'svelte'; - import { get } from 'svelte/store'; - export let crop = get(cropSettings); + export let crop = $cropSettings; export let asset; - export let aspectRatio = get(cropAspectRatio); + export let aspectRatio = $cropAspectRatio; cropSettings.subscribe((value) => { crop = value; }); @@ -33,7 +32,7 @@ if (!ctx || !canvas) { return; } - cropSettings.set(crop); + $cropSettings = crop; ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(img, 0, 0, canvas.width, canvas.height); drawOverlay(); @@ -181,8 +180,8 @@ } } - cropImageSize.set([img.width, img.height]); - cropImageScale.set(scale); + $cropImageSize = [img.width, img.height]; + $cropImageScale = scale; crop = { x: 0, y: 0, width: img.width * scale - 1, height: img.height * scale - 1 }; @@ -683,12 +682,12 @@ fadeOverlay(true); // Darken the background setTimeout(() => { - let cropImageSizeParams = get(cropSettings); - let originalImgSize = get(cropImageSize).map((el) => el * get(cropImageScale)); + let cropImageSizeParams = $cropSettings; + let originalImgSize = $cropImageSize.map((el) => el * $cropImageScale); let changed = Math.abs(originalImgSize[0] - cropImageSizeParams.width) > 2 && Math.abs(originalImgSize[1] - cropImageSizeParams.height) > 2; - cropSettingsChanged.set(changed); + $cropSettingsChanged = changed; }, 1); }; diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool.svelte b/web/src/lib/components/asset-viewer/editor/crop-tool.svelte index b7bb2ce187251..b91fa6462d356 100644 --- a/web/src/lib/components/asset-viewer/editor/crop-tool.svelte +++ b/web/src/lib/components/asset-viewer/editor/crop-tool.svelte @@ -9,7 +9,6 @@ type CropAspectRatio, } from '$lib/stores/asset-editor.store'; import { mdiBackupRestore, mdiCropFree, mdiSquareOutline } from '@mdi/js'; - import { get } from 'svelte/store'; let sizes = [ { @@ -45,24 +44,24 @@ ]; let selectedSize: CropAspectRatio = 'free'; - cropAspectRatio.set(selectedSize); + $cropAspectRatio = selectedSize; function selectType(size: CropAspectRatio) { if (size == 'reset') { selectedSize = 'free'; - let cropImageSizeM = get(cropImageSize); - let cropImageScaleM = get(cropImageScale); - cropSettings.set({ + let cropImageSizeM = $cropImageSize; + let cropImageScaleM = $cropImageScale; + $cropSettings = { x: 0, y: 0, width: cropImageSizeM[0] * cropImageScaleM - 1, height: cropImageSizeM[1] * cropImageScaleM - 1, - }); - cropAspectRatio.set(selectedSize); + }; + $cropAspectRatio = selectedSize; return; } selectedSize = size; - cropAspectRatio.set(size); + $cropAspectRatio = size; } From 9de3298542d2feb247f702f52d0a5b0b236e9ac8 Mon Sep 17 00:00:00 2001 From: ilyaChuk <86570508+ilyaChuk@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:52:54 +0300 Subject: [PATCH 17/47] unobtrusive grid lines, circles at the corners --- .../asset-viewer/editor/crop-canvas.svelte | 71 +++++++++++++------ 1 file changed, 50 insertions(+), 21 deletions(-) diff --git a/web/src/lib/components/asset-viewer/editor/crop-canvas.svelte b/web/src/lib/components/asset-viewer/editor/crop-canvas.svelte index b26e4e3cad1d6..a8df40e06890a 100644 --- a/web/src/lib/components/asset-viewer/editor/crop-canvas.svelte +++ b/web/src/lib/components/asset-viewer/editor/crop-canvas.svelte @@ -24,6 +24,8 @@ let darkenLevel = 0.65; // Initial darkening level let animationFrame: ReturnType; let canvasCursor: string; + let isResizingOrDragging = false; + const getAssetUrl = (id: string, checksum: string) => { return `http://localhost:2283/api/assets/${id}/original?c=${checksum}`; }; @@ -54,36 +56,61 @@ ctx.strokeRect(crop.x, crop.y, crop.width, crop.height); // Set blend mode - ctx.strokeStyle = 'white'; - ctx.lineWidth = 1; + if (isResizingOrDragging) { + ctx.strokeStyle = isResizingOrDragging ? 'white' : 'rgba(255, 255, 255, 0.4)'; + ctx.lineWidth = 1; + + // Vertical lines + const thirdWidth = crop.width / 3; + ctx.beginPath(); + ctx.moveTo(crop.x + thirdWidth, crop.y); + ctx.lineTo(crop.x + thirdWidth, crop.y + crop.height); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(crop.x + 2 * thirdWidth, crop.y); + ctx.lineTo(crop.x + 2 * thirdWidth, crop.y + crop.height); + ctx.stroke(); + + // Horizontal lines + const thirdHeight = crop.height / 3; + ctx.beginPath(); + ctx.moveTo(crop.x, crop.y + thirdHeight); + ctx.lineTo(crop.x + crop.width, crop.y + thirdHeight); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(crop.x, crop.y + 2 * thirdHeight); + ctx.lineTo(crop.x + crop.width, crop.y + 2 * thirdHeight); + ctx.stroke(); + } - // Vertical lines - const thirdWidth = crop.width / 3; - ctx.beginPath(); - ctx.moveTo(crop.x + thirdWidth, crop.y); - ctx.lineTo(crop.x + thirdWidth, crop.y + crop.height); - ctx.stroke(); + // Draw circles on corners + ctx.globalCompositeOperation = 'source-over'; + const radius = 5; // Radius of the corner circles + ctx.fillStyle = 'white'; + // Top-left corner ctx.beginPath(); - ctx.moveTo(crop.x + 2 * thirdWidth, crop.y); - ctx.lineTo(crop.x + 2 * thirdWidth, crop.y + crop.height); - ctx.stroke(); + ctx.arc(crop.x, crop.y, radius, 0, 2 * Math.PI); + ctx.fill(); - // Horizontal lines - const thirdHeight = crop.height / 3; + // Top-right corner ctx.beginPath(); - ctx.moveTo(crop.x, crop.y + thirdHeight); - ctx.lineTo(crop.x + crop.width, crop.y + thirdHeight); - ctx.stroke(); + ctx.arc(crop.x + crop.width, crop.y, radius, 0, 2 * Math.PI); + ctx.fill(); + // Bottom-left corner ctx.beginPath(); - ctx.moveTo(crop.x, crop.y + 2 * thirdHeight); - ctx.lineTo(crop.x + crop.width, crop.y + 2 * thirdHeight); - ctx.stroke(); + ctx.arc(crop.x, crop.y + crop.height, radius, 0, 2 * Math.PI); + ctx.fill(); - // Restore blend mode - ctx.globalCompositeOperation = 'source-over'; + // Bottom-right corner + ctx.beginPath(); + ctx.arc(crop.x + crop.width, crop.y + crop.height, radius, 0, 2 * Math.PI); + ctx.fill(); }; + const drawOverlay = () => { ctx.fillStyle = `rgba(0, 0, 0, ${darkenLevel})`; @@ -102,6 +129,8 @@ const minDarkness = 0.4; const maxDarkness = 0.65; + isResizingOrDragging = !toDark; + const animate = () => { darkenLevel = Math.min(maxDarkness, Math.max(minDarkness, darkenLevel + step)); draw(); From b9e2f6ab2e31270f124a08838fc144118b348bbb Mon Sep 17 00:00:00 2001 From: ilyaChuk <86570508+ilyaChuk@users.noreply.github.com> Date: Mon, 29 Jul 2024 16:05:51 +0300 Subject: [PATCH 18/47] more correct image load, handleError --- .../asset-viewer/editor/crop-canvas.svelte | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/web/src/lib/components/asset-viewer/editor/crop-canvas.svelte b/web/src/lib/components/asset-viewer/editor/crop-canvas.svelte index a8df40e06890a..e327bed51be7a 100644 --- a/web/src/lib/components/asset-viewer/editor/crop-canvas.svelte +++ b/web/src/lib/components/asset-viewer/editor/crop-canvas.svelte @@ -8,7 +8,10 @@ type CropAspectRatio, cropSettingsChanged, } from '$lib/stores/asset-editor.store'; + import { getAssetOriginalUrl } from '$lib/utils'; + import { handleError } from '$lib/utils/handle-error'; import { onMount, afterUpdate } from 'svelte'; + import { t } from 'svelte-i18n'; export let crop = $cropSettings; export let asset; @@ -26,10 +29,6 @@ let canvasCursor: string; let isResizingOrDragging = false; - const getAssetUrl = (id: string, checksum: string) => { - return `http://localhost:2283/api/assets/${id}/original?c=${checksum}`; - }; - const draw = () => { if (!ctx || !canvas) { return; @@ -722,11 +721,11 @@ onMount(() => { img = new Image(); - img.src = getAssetUrl(asset.id, asset.checksum); + img.src = getAssetOriginalUrl({ id: asset.id, checksum: asset.checksum }) img.addEventListener('load', onImageLoad); - img.addEventListener('error', () => { - console.error('Failed to load image'); + img.addEventListener('error', (error) => { + handleError(error, $t('error_loading_image')); }); }); From f982a54e79f9276f6182f770f4adda2bc2426fdc Mon Sep 17 00:00:00 2001 From: ilyaChuk <86570508+ilyaChuk@users.noreply.github.com> Date: Mon, 29 Jul 2024 16:09:52 +0300 Subject: [PATCH 19/47] more strict equality --- web/src/lib/components/asset-viewer/editor/crop-tool.svelte | 4 ++-- .../lib/components/asset-viewer/editor/editor-panel.svelte | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool.svelte b/web/src/lib/components/asset-viewer/editor/crop-tool.svelte index b91fa6462d356..287c8f91a708a 100644 --- a/web/src/lib/components/asset-viewer/editor/crop-tool.svelte +++ b/web/src/lib/components/asset-viewer/editor/crop-tool.svelte @@ -47,7 +47,7 @@ $cropAspectRatio = selectedSize; function selectType(size: CropAspectRatio) { - if (size == 'reset') { + if (size === 'reset') { selectedSize = 'free'; let cropImageSizeM = $cropImageSize; let cropImageScaleM = $cropImageScale; @@ -70,7 +70,7 @@ {#each sizes as size (size.name)}
@@ -762,6 +761,5 @@ } canvas { cursor: default; - border: none; } diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool.svelte b/web/src/lib/components/asset-viewer/editor/crop-tool.svelte index 287c8f91a708a..9b3708810d738 100644 --- a/web/src/lib/components/asset-viewer/editor/crop-tool.svelte +++ b/web/src/lib/components/asset-viewer/editor/crop-tool.svelte @@ -83,9 +83,3 @@ {/each}
- - diff --git a/web/src/lib/components/asset-viewer/editor/editor-panel.svelte b/web/src/lib/components/asset-viewer/editor/editor-panel.svelte index 1a4506dbc2875..504147b2cfd2d 100644 --- a/web/src/lib/components/asset-viewer/editor/editor-panel.svelte +++ b/web/src/lib/components/asset-viewer/editor/editor-panel.svelte @@ -63,7 +63,7 @@

{$t('editor')}

-
    +
- - From d17c0da0997c03bf0167fc4ae3776fb3ea0d8162 Mon Sep 17 00:00:00 2001 From: ilyaChuk <86570508+ilyaChuk@users.noreply.github.com> Date: Mon, 29 Jul 2024 16:20:07 +0300 Subject: [PATCH 21/47] dont store isShowEditor --- .../components/asset-viewer/asset-viewer.svelte | 15 ++++++++------- web/src/lib/stores/preferences.store.ts | 1 - 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 87a18a005028e..b7dbd9c7ef128 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -5,7 +5,7 @@ import { updateNumberOfComments } from '$lib/stores/activity.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import type { AssetStore } from '$lib/stores/assets.store'; - import { isShowDetail, isShowEditor, showDeleteModal } from '$lib/stores/preferences.store'; + import { isShowDetail, showDeleteModal } from '$lib/stores/preferences.store'; import { featureFlags } from '$lib/stores/server-config.store'; import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; import { stackAssetsStore } from '$lib/stores/stacked-asset.store'; @@ -105,6 +105,7 @@ let shuffleSlideshowUnsubscribe: () => void; let previewStackedAsset: AssetResponseDto | undefined; let isShowActivity = false; + let isShowEditor = false; let isLiked: ActivityResponseDto | null = null; let numberOfComments: number; let fullscreenElement: Element; @@ -340,7 +341,7 @@ if (isShowActivity) { isShowActivity = false; } - $isShowEditor = !$isShowEditor; + isShowEditor = !isShowEditor; }; const trashOrDelete = async (force: boolean = false) => { @@ -612,7 +613,7 @@ {/if} - {#if $slideshowState === SlideshowState.None && showNavigation && !$isShowEditor} + {#if $slideshowState === SlideshowState.None && showNavigation && !isShowEditor}
navigateAsset('previous', e)} label={$t('view_previous_asset')}> @@ -682,7 +683,7 @@ .toLowerCase() .endsWith('.insp'))} - {:else if $isShowEditor && selectedEditType === 'crop'} + {:else if isShowEditor && selectedEditType === 'crop'} {:else} @@ -713,7 +714,7 @@ {/if}
- {#if $slideshowState === SlideshowState.None && showNavigation && !$isShowEditor} + {#if $slideshowState === SlideshowState.None && showNavigation && !isShowEditor}
navigateAsset('next', e)} label={$t('view_next_asset')}> @@ -732,7 +733,7 @@
{/if} - {#if $isShowEditor} + {#if isShowEditor}
($isShowEditor = false)} + on:close={() => (isShowEditor = false)} />
{/if} diff --git a/web/src/lib/stores/preferences.store.ts b/web/src/lib/stores/preferences.store.ts index 93ed2b62c4dcd..11473f80612a6 100644 --- a/web/src/lib/stores/preferences.store.ts +++ b/web/src/lib/stores/preferences.store.ts @@ -77,7 +77,6 @@ export const videoViewerVolume = persisted('video-viewer-volume', 1, {}) export const videoViewerMuted = persisted('video-viewer-muted', false, {}); export const isShowDetail = persisted('info-opened', false, {}); -export const isShowEditor = persisted('editor-opened', false, {}); export interface AlbumViewSettings { view: string; From 840d55f7365fdf5fddca799d5ecaaa6f698a33e3 Mon Sep 17 00:00:00 2001 From: ilyaChuk <86570508+ilyaChuk@users.noreply.github.com> Date: Mon, 29 Jul 2024 19:31:32 +0300 Subject: [PATCH 22/47] if showEditor - hide navbar & shortcuts --- .../asset-viewer/asset-viewer.svelte | 25 ++++++++++++++++--- .../asset-viewer/editor/crop-canvas.svelte | 4 +-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index b7dbd9c7ef128..2bc9bddc58471 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -113,6 +113,7 @@ let zoomToggle = () => void 0; let copyImage: () => Promise; $: isFullScreen = fullscreenElement !== null; + $: showNavbar = ![isShowEditor].some(Boolean) // can add another variables $: { if (asset.stackCount && asset.stack) { @@ -275,12 +276,18 @@ }; const toggleDetailPanel = () => { + if(!showNavbar){ + return; + } isShowActivity = false; $isShowDetail = !$isShowDetail; }; const closeViewer = async () => { - if ($slideshowState === SlideshowState.None) { + if(isShowEditor){ + isShowEditor = false; + } + else if ($slideshowState === SlideshowState.None) { dispatch('close'); await navigate({ targetRoute: 'current', assetId: null }); } else { @@ -305,6 +312,9 @@ }; const navigateAsset = async (order?: 'previous' | 'next', e?: Event) => { + if(!showNavbar){ + return; + } if (!order) { if ($slideshowState === SlideshowState.PlaySlideshow) { order = $slideshowNavigation === SlideshowNavigation.AscendingOrder ? 'previous' : 'next'; @@ -345,6 +355,9 @@ }; const trashOrDelete = async (force: boolean = false) => { + if(!showNavbar){ + return; + } if (force || !isTrashEnabled) { if ($showDeleteModal) { isShowDeleteConfirmation = true; @@ -391,6 +404,9 @@ }; const toggleFavorite = async () => { + if(!showNavbar){ + return; + } try { const data = await updateAsset({ id: asset.id, @@ -446,6 +462,9 @@ }; const toggleAssetArchive = async () => { + if(!showNavbar){ + return; + } const updatedAsset = await toggleArchive(asset); if (updatedAsset) { dispatch('action', { type: asset.isArchived ? AssetAction.ARCHIVE : AssetAction.UNARCHIVE, asset: asset }); @@ -557,7 +576,7 @@ { shortcut: { key: 'a', shift: true }, onShortcut: toggleAssetArchive }, { shortcut: { key: 'ArrowLeft' }, onShortcut: () => navigateAsset('previous') }, { shortcut: { key: 'ArrowRight' }, onShortcut: () => navigateAsset('next') }, - { shortcut: { key: 'd', shift: true }, onShortcut: () => downloadFile(asset) }, + { shortcut: { key: 'd', shift: true }, onShortcut: () => showNavbar?downloadFile(asset):null }, { shortcut: { key: 'Delete' }, onShortcut: () => trashOrDelete(asset.isTrashed) }, { shortcut: { key: 'Delete', shift: true }, onShortcut: () => trashOrDelete(true) }, { shortcut: { key: 'Escape' }, onShortcut: closeViewer }, @@ -574,7 +593,7 @@ use:focusTrap > - {#if $slideshowState === SlideshowState.None} + {#if $slideshowState === SlideshowState.None && !isShowEditor}
Date: Mon, 29 Jul 2024 21:57:54 +0300 Subject: [PATCH 23/47] crop-canvas decomposition (danger) I could have accidentally broken something.. but I checked the work and it seems ok. --- .../asset-viewer/asset-viewer.svelte | 21 +- .../asset-viewer/editor/crop-canvas.svelte | 765 ------------------ .../editor/crop-tool/canvas-drawing.ts | 90 +++ .../editor/crop-tool/crop-canvas.svelte | 78 ++ .../editor/crop-tool/crop-settings.ts | 150 ++++ .../editor/crop-tool/crop-store.ts | 25 + .../editor/{ => crop-tool}/crop-tool.svelte | 7 +- .../editor/crop-tool/image-loading.ts | 81 ++ .../editor/crop-tool/mouse-handlers.ts | 459 +++++++++++ .../asset-viewer/editor/editor-panel.svelte | 2 +- web/src/lib/stores/asset-editor.store.ts | 8 + 11 files changed, 908 insertions(+), 778 deletions(-) delete mode 100644 web/src/lib/components/asset-viewer/editor/crop-canvas.svelte create mode 100644 web/src/lib/components/asset-viewer/editor/crop-tool/canvas-drawing.ts create mode 100644 web/src/lib/components/asset-viewer/editor/crop-tool/crop-canvas.svelte create mode 100644 web/src/lib/components/asset-viewer/editor/crop-tool/crop-settings.ts create mode 100644 web/src/lib/components/asset-viewer/editor/crop-tool/crop-store.ts rename web/src/lib/components/asset-viewer/editor/{ => crop-tool}/crop-tool.svelte (95%) create mode 100644 web/src/lib/components/asset-viewer/editor/crop-tool/image-loading.ts create mode 100644 web/src/lib/components/asset-viewer/editor/crop-tool/mouse-handlers.ts diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 2bc9bddc58471..7dbb8a2a6a8ad 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -62,7 +62,7 @@ import { t } from 'svelte-i18n'; import { focusTrap } from '$lib/actions/focus-trap'; import EditorPanel from './editor/editor-panel.svelte'; - import CropCanvas from './editor/crop-canvas.svelte'; + import CropCanvas from './editor/crop-tool/crop-canvas.svelte'; export let assetStore: AssetStore | null = null; export let asset: AssetResponseDto; @@ -113,7 +113,7 @@ let zoomToggle = () => void 0; let copyImage: () => Promise; $: isFullScreen = fullscreenElement !== null; - $: showNavbar = ![isShowEditor].some(Boolean) // can add another variables + $: showNavbar = ![isShowEditor].some(Boolean); // can add another variables $: { if (asset.stackCount && asset.stack) { @@ -276,7 +276,7 @@ }; const toggleDetailPanel = () => { - if(!showNavbar){ + if (!showNavbar) { return; } isShowActivity = false; @@ -284,10 +284,9 @@ }; const closeViewer = async () => { - if(isShowEditor){ + if (isShowEditor) { isShowEditor = false; - } - else if ($slideshowState === SlideshowState.None) { + } else if ($slideshowState === SlideshowState.None) { dispatch('close'); await navigate({ targetRoute: 'current', assetId: null }); } else { @@ -312,7 +311,7 @@ }; const navigateAsset = async (order?: 'previous' | 'next', e?: Event) => { - if(!showNavbar){ + if (!showNavbar) { return; } if (!order) { @@ -355,7 +354,7 @@ }; const trashOrDelete = async (force: boolean = false) => { - if(!showNavbar){ + if (!showNavbar) { return; } if (force || !isTrashEnabled) { @@ -404,7 +403,7 @@ }; const toggleFavorite = async () => { - if(!showNavbar){ + if (!showNavbar) { return; } try { @@ -462,7 +461,7 @@ }; const toggleAssetArchive = async () => { - if(!showNavbar){ + if (!showNavbar) { return; } const updatedAsset = await toggleArchive(asset); @@ -576,7 +575,7 @@ { shortcut: { key: 'a', shift: true }, onShortcut: toggleAssetArchive }, { shortcut: { key: 'ArrowLeft' }, onShortcut: () => navigateAsset('previous') }, { shortcut: { key: 'ArrowRight' }, onShortcut: () => navigateAsset('next') }, - { shortcut: { key: 'd', shift: true }, onShortcut: () => showNavbar?downloadFile(asset):null }, + { shortcut: { key: 'd', shift: true }, onShortcut: () => (showNavbar ? downloadFile(asset) : null) }, { shortcut: { key: 'Delete' }, onShortcut: () => trashOrDelete(asset.isTrashed) }, { shortcut: { key: 'Delete', shift: true }, onShortcut: () => trashOrDelete(true) }, { shortcut: { key: 'Escape' }, onShortcut: closeViewer }, diff --git a/web/src/lib/components/asset-viewer/editor/crop-canvas.svelte b/web/src/lib/components/asset-viewer/editor/crop-canvas.svelte deleted file mode 100644 index 722f7a60c7f8d..0000000000000 --- a/web/src/lib/components/asset-viewer/editor/crop-canvas.svelte +++ /dev/null @@ -1,765 +0,0 @@ - - -
- -
- - diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/canvas-drawing.ts b/web/src/lib/components/asset-viewer/editor/crop-tool/canvas-drawing.ts new file mode 100644 index 0000000000000..6676acb7921bb --- /dev/null +++ b/web/src/lib/components/asset-viewer/editor/crop-tool/canvas-drawing.ts @@ -0,0 +1,90 @@ +import type { CropSettings } from '$lib/stores/asset-editor.store'; +import { get } from 'svelte/store'; +import { context2D, darkenLevel, imgElement, isResizingOrDragging } from './crop-store'; + +export function draw(canvas: HTMLCanvasElement | null, crop: CropSettings) { + const ctx = get(context2D); + const img = get(imgElement); + if (!ctx || !canvas || !img) { + return; + } + + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + drawOverlay(canvas, crop); + drawCropRect(crop); +} + +export function drawCropRect(crop: CropSettings) { + const ctx = get(context2D); + if (!ctx) { + return; + } + + ctx.globalCompositeOperation = 'exclusion'; + ctx.strokeStyle = 'white'; + ctx.lineWidth = 2; + ctx.strokeRect(crop.x, crop.y, crop.width, crop.height); + + if (get(isResizingOrDragging)) { + ctx.strokeStyle = 'white'; + ctx.lineWidth = 1; + + const thirdWidth = crop.width / 3; + ctx.beginPath(); + ctx.moveTo(crop.x + thirdWidth, crop.y); + ctx.lineTo(crop.x + thirdWidth, crop.y + crop.height); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(crop.x + 2 * thirdWidth, crop.y); + ctx.lineTo(crop.x + 2 * thirdWidth, crop.y + crop.height); + ctx.stroke(); + + const thirdHeight = crop.height / 3; + ctx.beginPath(); + ctx.moveTo(crop.x, crop.y + thirdHeight); + ctx.lineTo(crop.x + crop.width, crop.y + thirdHeight); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(crop.x, crop.y + 2 * thirdHeight); + ctx.lineTo(crop.x + crop.width, crop.y + 2 * thirdHeight); + ctx.stroke(); + } + + ctx.globalCompositeOperation = 'source-over'; + const radius = 5; + ctx.fillStyle = 'white'; + + ctx.beginPath(); + ctx.arc(crop.x, crop.y, radius, 0, 2 * Math.PI); + ctx.fill(); + + ctx.beginPath(); + ctx.arc(crop.x + crop.width, crop.y, radius, 0, 2 * Math.PI); + ctx.fill(); + + ctx.beginPath(); + ctx.arc(crop.x, crop.y + crop.height, radius, 0, 2 * Math.PI); + ctx.fill(); + + ctx.beginPath(); + ctx.arc(crop.x + crop.width, crop.y + crop.height, radius, 0, 2 * Math.PI); + ctx.fill(); +} + +export function drawOverlay(canvas: HTMLCanvasElement, crop: CropSettings) { + const ctx = get(context2D); + const darken = get(darkenLevel); + if (!ctx) { + return; + } + + ctx.fillStyle = `rgba(0, 0, 0, ${darken})`; + + ctx.fillRect(0, 0, canvas.width, crop.y); + ctx.fillRect(0, crop.y, crop.x, crop.height); + ctx.fillRect(crop.x + crop.width, crop.y, canvas.width - crop.x - crop.width, crop.height); + ctx.fillRect(0, crop.y + crop.height, canvas.width, canvas.height - crop.y - crop.height); +} diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-canvas.svelte b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-canvas.svelte new file mode 100644 index 0000000000000..26a65533f4414 --- /dev/null +++ b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-canvas.svelte @@ -0,0 +1,78 @@ + + +
+ +
+ + diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-settings.ts b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-settings.ts new file mode 100644 index 0000000000000..9b4e87a5b8aab --- /dev/null +++ b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-settings.ts @@ -0,0 +1,150 @@ +import type { CropAspectRatio, CropSettings } from '$lib/stores/asset-editor.store'; + +export function recalculateCrop( + crop: CropSettings, + canvas: HTMLCanvasElement, + aspectRatio: CropAspectRatio, + returnNewCrop = false, +): CropSettings | null { + if (!canvas) { + return null; + } + const { width, height, x, y } = crop; + let newWidth = width; + let newHeight = height; + + const { newWidth: w, newHeight: h } = keepAspectRatio(newWidth, newHeight, aspectRatio); + + if (w > canvas.width) { + newWidth = canvas.width; + newHeight = canvas.width / (w / h); + } else if (h > canvas.height) { + newHeight = canvas.height; + newWidth = canvas.height * (w / h); + } else { + newWidth = w; + newHeight = h; + } + newWidth -= 1; + newHeight -= 1; + + const newCrop = { + width: newWidth, + height: newHeight, + x: Math.max(0, x + (width - newWidth) / 2), + y: Math.max(0, y + (height - newHeight) / 2), + }; + + if (newCrop.x + newWidth > canvas.width) { + newCrop.x = canvas.width - newWidth; + } + if (newCrop.y + newHeight > canvas.height) { + newCrop.y = canvas.height - newHeight; + } + + if (returnNewCrop) { + return newCrop; + } else { + crop.width = newWidth; + crop.height = newHeight; + crop.x = newCrop.x; + crop.y = newCrop.y; + return null; + } +} + +export function animateCropChange(crop: CropSettings, newCrop: CropSettings, draw: () => void, duration = 100) { + if (!newCrop) { + 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) { + switch (aspectRatio) { + case '1:1': { + return { newWidth: newHeight, newHeight }; + } + case '16:9': { + return { newWidth: (newHeight * 16) / 9, newHeight }; + } + case '3:2': { + return { newWidth: (newHeight * 3) / 2, newHeight }; + } + case '7:5': { + return { newWidth: (newHeight * 7) / 5, newHeight }; + } + default: { + return { newWidth, newHeight }; + } + } +} + +export function adjustDimensions( + newWidth: number, + newHeight: number, + aspectRatio: CropAspectRatio, + xLimit: number, + yLimit: number, +) { + let w = newWidth, + h = newHeight; + + let aspectMultiplier; + switch (aspectRatio) { + case '1:1': { + aspectMultiplier = 1; + break; + } + case '16:9': { + aspectMultiplier = 16 / 9; + break; + } + case '3:2': { + aspectMultiplier = 3 / 2; + break; + } + case '7:5': { + aspectMultiplier = 7 / 5; + break; + } + default: { + aspectMultiplier = newWidth / newHeight; + } + } + + if (aspectRatio !== 'free') { + h = w / aspectMultiplier; + } + + if (w > xLimit) { + w = xLimit; + h = w / aspectMultiplier; + } + + if (h > yLimit) { + h = yLimit; + w = h * aspectMultiplier; + } + + return { newWidth: w, newHeight: h }; +} diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-store.ts b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-store.ts new file mode 100644 index 0000000000000..32631faffb468 --- /dev/null +++ b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-store.ts @@ -0,0 +1,25 @@ +import { writable } from 'svelte/store'; + +export const darkenLevel = writable(0.65); +export const isResizingOrDragging = writable(false); +export const animationFrame = writable | null>(null); +export const canvasCursor = writable('default'); +export const dragOffset = writable({ x: 0, y: 0 }); +export const resizeSide = writable(''); +export const imgElement = writable(null); +export const canvasElement = writable(null); +export const context2D = writable(null); +export const isDragging = writable(false); + +export function resetCropStore() { + darkenLevel.set(0.65); + isResizingOrDragging.set(false); + animationFrame.set(null); + canvasCursor.set('default'); + dragOffset.set({ x: 0, y: 0 }); + resizeSide.set(''); + imgElement.set(null); + canvasElement.set(null); + context2D.set(null); + isDragging.set(false); +} diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool.svelte b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-tool.svelte similarity index 95% rename from web/src/lib/components/asset-viewer/editor/crop-tool.svelte rename to web/src/lib/components/asset-viewer/editor/crop-tool/crop-tool.svelte index 9b3708810d738..a037c941c1cba 100644 --- a/web/src/lib/components/asset-viewer/editor/crop-tool.svelte +++ b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-tool.svelte @@ -10,7 +10,12 @@ } from '$lib/stores/asset-editor.store'; import { mdiBackupRestore, mdiCropFree, mdiSquareOutline } from '@mdi/js'; - let sizes = [ + interface Size { + icon: string; + name: CropAspectRatio; + viewBox: string; + } + let sizes: Size[] = [ { icon: mdiCropFree, name: 'free', diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/image-loading.ts b/web/src/lib/components/asset-viewer/editor/crop-tool/image-loading.ts new file mode 100644 index 0000000000000..34cdb9a7ff27b --- /dev/null +++ b/web/src/lib/components/asset-viewer/editor/crop-tool/image-loading.ts @@ -0,0 +1,81 @@ +import { cropImageScale, cropImageSize, cropSettings } from '$lib/stores/asset-editor.store'; +import { get } from 'svelte/store'; +import { draw } from './canvas-drawing'; +import { canvasElement, context2D, imgElement } from './crop-store'; + +export function onImageLoad() { + const img = get(imgElement); + const canvas = get(canvasElement); + let ctx = get(context2D); + + if (!canvas || !img) { + return; + } + + if (!ctx) { + ctx = canvas.getContext('2d'); + context2D.set(ctx); + } + + resizeCanvas(); + + const containerWidth = canvas.parentElement?.clientWidth ?? 0; + const containerHeight = canvas.parentElement?.clientHeight ?? 0; + const imageAspectRatio = img.width / img.height; + + let scale: number; + if (imageAspectRatio > 1) { + scale = containerWidth / img.width; + if (img.height * scale > containerHeight) { + scale = containerHeight / img.height; + } + } else { + scale = containerHeight / img.height; + if (img.width * scale > containerWidth) { + scale = containerWidth / img.width; + } + } + + cropImageSize.set([img.width, img.height]); + cropImageScale.set(scale); + + cropSettings.update((crop) => { + crop.x = 0; + crop.y = 0; + crop.width = img.width * scale - 1; + crop.height = img.height * scale - 1; + return crop; + }); + + draw(canvas, get(cropSettings)); +} + +export function resizeCanvas() { + const img = get(imgElement); + const canvas = get(canvasElement); + if (!canvas || !img) { + return; + } + + const containerWidth = canvas.parentElement?.clientWidth ?? 0; + const containerHeight = canvas.parentElement?.clientHeight ?? 0; + const imageAspectRatio = img.width / img.height; + + let scale; + if (imageAspectRatio > 1) { + scale = containerWidth / img.width; + if (img.height * scale > containerHeight) { + scale = containerHeight / img.height; + } + } else { + scale = containerHeight / img.height; + if (img.width * scale > containerWidth) { + scale = containerWidth / img.width; + } + } + + canvas.width = img.width * scale; + canvas.height = img.height * scale; + + draw(canvas, get(cropSettings)); +} diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/mouse-handlers.ts b/web/src/lib/components/asset-viewer/editor/crop-tool/mouse-handlers.ts new file mode 100644 index 0000000000000..bbcff7f9301cf --- /dev/null +++ b/web/src/lib/components/asset-viewer/editor/crop-tool/mouse-handlers.ts @@ -0,0 +1,459 @@ +import { + cropAspectRatio, + cropImageScale, + cropImageSize, + cropSettings, + cropSettingsChanged, + type CropSettings, +} from '$lib/stores/asset-editor.store'; +import { get } from 'svelte/store'; +import { draw } from './canvas-drawing'; +import { adjustDimensions, keepAspectRatio } from './crop-settings'; +import { + animationFrame, + canvasCursor, + canvasElement, + darkenLevel, + dragOffset, + isDragging, + isResizingOrDragging, + resizeSide, +} from './crop-store'; + +export function handleMouseDown(e: MouseEvent) { + const canvas = get(canvasElement); + if (!canvas) { + return; + } + + const crop = get(cropSettings); + const { mouseX, mouseY } = getMousePosition(e, canvas); + + const { + onLeftBoundary, + onRightBoundary, + onTopBoundary, + onBottomBoundary, + onTopLeftCorner, + onTopRightCorner, + onBottomLeftCorner, + onBottomRightCorner, + } = isOnCropBoundary(mouseX, mouseY, crop); + + if ( + onTopLeftCorner || + onTopRightCorner || + onBottomLeftCorner || + onBottomRightCorner || + onLeftBoundary || + onRightBoundary || + onTopBoundary || + onBottomBoundary + ) { + setResizeSide(mouseX, mouseY); + } else if (isInCropArea(mouseX, mouseY, crop)) { + startDragging(mouseX, mouseY); + } + window.addEventListener('mouseup', handleMouseUp); +} + +export function handleMouseMove(e: MouseEvent) { + const canvas = get(canvasElement); + if (!canvas) { + return; + } + + const resizeSideValue = get(resizeSide); + const { mouseX, mouseY } = getMousePosition(e, canvas); + + if (get(isDragging)) { + moveCrop(mouseX, mouseY); + } else if (resizeSideValue) { + resizeCrop(mouseX, mouseY); + } else { + updateCursor(mouseX, mouseY); + } +} + +export function handleMouseUp() { + stopInteraction(); +} + +export function handleMouseOut() { + stopInteraction(); + window.removeEventListener('mouseup', handleMouseUp); +} + +function getMousePosition(e: MouseEvent, canvas: HTMLCanvasElement) { + return { mouseX: e.offsetX, mouseY: e.offsetY }; +} + +function isOnCropBoundary(mouseX: number, mouseY: number, crop: CropSettings) { + const { x, y, width, height } = crop; + const sensitivity = 10; + const cornerSensitivity = 15; + + const onLeftBoundary = mouseX >= x - sensitivity && mouseX <= x + sensitivity && mouseY >= y && mouseY <= y + height; + const onRightBoundary = + mouseX >= x + width - sensitivity && mouseX <= x + width + sensitivity && mouseY >= y && mouseY <= y + height; + const onTopBoundary = mouseY >= y - sensitivity && mouseY <= y + sensitivity && mouseX >= x && mouseX <= x + width; + const onBottomBoundary = + mouseY >= y + height - sensitivity && mouseY <= y + height + sensitivity && mouseX >= x && mouseX <= x + width; + + const onTopLeftCorner = + mouseX >= x - cornerSensitivity && + mouseX <= x + cornerSensitivity && + mouseY >= y - cornerSensitivity && + mouseY <= y + cornerSensitivity; + const onTopRightCorner = + mouseX >= x + width - cornerSensitivity && + mouseX <= x + width + cornerSensitivity && + mouseY >= y - cornerSensitivity && + mouseY <= y + cornerSensitivity; + const onBottomLeftCorner = + mouseX >= x - cornerSensitivity && + mouseX <= x + cornerSensitivity && + mouseY >= y + height - cornerSensitivity && + mouseY <= y + height + cornerSensitivity; + const onBottomRightCorner = + mouseX >= x + width - cornerSensitivity && + mouseX <= x + width + cornerSensitivity && + mouseY >= y + height - cornerSensitivity && + mouseY <= y + height + cornerSensitivity; + + return { + onLeftBoundary, + onRightBoundary, + onTopBoundary, + onBottomBoundary, + onTopLeftCorner, + onTopRightCorner, + onBottomLeftCorner, + onBottomRightCorner, + }; +} + +function isInCropArea(mouseX: number, mouseY: number, crop: CropSettings) { + const { x, y, width, height } = crop; + return mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height; +} + +function setResizeSide(mouseX: number, mouseY: number) { + const crop = get(cropSettings); + const { + onLeftBoundary, + onRightBoundary, + onTopBoundary, + onBottomBoundary, + onTopLeftCorner, + onTopRightCorner, + onBottomLeftCorner, + onBottomRightCorner, + } = isOnCropBoundary(mouseX, mouseY, crop); + + if (onTopLeftCorner) { + resizeSide.set('top-left'); + } else if (onTopRightCorner) { + resizeSide.set('top-right'); + } else if (onBottomLeftCorner) { + resizeSide.set('bottom-left'); + } else if (onBottomRightCorner) { + resizeSide.set('bottom-right'); + } else if (onLeftBoundary) { + resizeSide.set('left'); + } else if (onRightBoundary) { + resizeSide.set('right'); + } else if (onTopBoundary) { + resizeSide.set('top'); + } else if (onBottomBoundary) { + resizeSide.set('bottom'); + } +} + +function startDragging(mouseX: number, mouseY: number) { + isDragging.set(true); + const crop = get(cropSettings); + isResizingOrDragging.set(true); + dragOffset.set({ x: mouseX - crop.x, y: mouseY - crop.y }); + fadeOverlay(false); // Lighten the background +} + +function moveCrop(mouseX: number, mouseY: number) { + const canvas = get(canvasElement); + if (!canvas) { + return; + } + + const crop = get(cropSettings); + const { x, y } = get(dragOffset); + + let newX = mouseX - x; + let newY = mouseY - y; + + newX = Math.max(0, Math.min(canvas.width - crop.width, newX)); + newY = Math.max(0, Math.min(canvas.height - crop.height, newY)); + + cropSettings.update((crop) => { + crop.x = newX; + crop.y = newY; + return crop; + }); + + draw(canvas, crop); +} + +function resizeCrop(mouseX: number, mouseY: number) { + const canvas = get(canvasElement); + const crop = get(cropSettings); + const resizeSideValue = get(resizeSide); + if (!canvas || !resizeSideValue) { + return; + } + fadeOverlay(false); + + const { x, y, width, height } = crop; + const minSize = 10; + let newWidth, newHeight; + + switch (resizeSideValue) { + case 'left': { + newWidth = width + x - mouseX; + newHeight = height; + if (newWidth >= minSize && mouseX >= 0) { + const { newWidth: w, newHeight: h } = keepAspectRatio(newWidth, newHeight, get(cropAspectRatio)); + cropSettings.update((crop) => { + crop.width = Math.min(w, canvas.width); + crop.height = Math.min(h, canvas.height); + crop.x = Math.max(0, x + width - crop.width); + return crop; + }); + } + break; + } + case 'right': { + newWidth = mouseX - x; + newHeight = height; + if (newWidth >= minSize && mouseX <= canvas.width) { + const { newWidth: w, newHeight: h } = keepAspectRatio(newWidth, newHeight, get(cropAspectRatio)); + cropSettings.update((crop) => { + crop.width = Math.min(w, canvas.width - x); + crop.height = Math.min(h, canvas.height); + return crop; + }); + } + break; + } + case 'top': { + newHeight = height + y - mouseY; + newWidth = width; + if (newHeight >= minSize && mouseY >= 0) { + const { newWidth: w, newHeight: h } = adjustDimensions( + newWidth, + newHeight, + get(cropAspectRatio), + canvas.width, + canvas.height, + ); + cropSettings.update((crop) => { + crop.y = Math.max(0, y + height - h); + crop.width = w; + crop.height = h; + return crop; + }); + } + break; + } + case 'bottom': { + newHeight = mouseY - y; + newWidth = width; + if (newHeight >= minSize && mouseY <= canvas.height) { + const { newWidth: w, newHeight: h } = adjustDimensions( + newWidth, + newHeight, + get(cropAspectRatio), + canvas.width, + canvas.height - y, + ); + cropSettings.update((crop) => { + crop.width = w; + crop.height = h; + return crop; + }); + } + break; + } + case 'top-left': { + newWidth = width + x - mouseX; + newHeight = height + y - mouseY; + if (newWidth >= minSize && newHeight >= minSize && mouseX >= 0 && mouseY >= 0) { + const { newWidth: w, newHeight: h } = adjustDimensions( + newWidth, + newHeight, + get(cropAspectRatio), + canvas.width, + canvas.height, + ); + cropSettings.update((crop) => { + crop.width = w; + crop.height = h; + crop.x = Math.max(0, x + width - crop.width); + crop.y = Math.max(0, y + height - crop.height); + return crop; + }); + } + break; + } + case 'top-right': { + newWidth = mouseX - x; + newHeight = height + y - mouseY; + if (newWidth >= minSize && newHeight >= minSize && mouseX <= canvas.width && mouseY >= 0) { + const { newWidth: w, newHeight: h } = adjustDimensions( + newWidth, + newHeight, + get(cropAspectRatio), + canvas.width - x, + y + height, + ); + cropSettings.update((crop) => { + crop.width = w; + crop.height = h; + crop.y = y + height - h; + return crop; + }); + } + break; + } + case 'bottom-left': { + newWidth = width + x - mouseX; + newHeight = mouseY - y; + if (newWidth >= minSize && newHeight >= minSize && mouseX >= 0 && mouseY <= canvas.height) { + const { newWidth: w, newHeight: h } = adjustDimensions( + newWidth, + newHeight, + get(cropAspectRatio), + canvas.width, + canvas.height - y, + ); + cropSettings.update((crop) => { + crop.width = w; + crop.height = h; + crop.x = Math.max(0, x + width - crop.width); + return crop; + }); + } + break; + } + case 'bottom-right': { + newWidth = mouseX - x; + newHeight = mouseY - y; + if (newWidth >= minSize && newHeight >= minSize && mouseX <= canvas.width && mouseY <= canvas.height) { + const { newWidth: w, newHeight: h } = adjustDimensions( + newWidth, + newHeight, + get(cropAspectRatio), + canvas.width - x, + canvas.height - y, + ); + cropSettings.update((crop) => { + crop.width = w; + crop.height = h; + return crop; + }); + } + break; + } + } + + cropSettings.update((crop) => { + crop.x = Math.max(0, Math.min(crop.x, canvas.width - crop.width)); + crop.y = Math.max(0, Math.min(crop.y, canvas.height - crop.height)); + return crop; + }); + + draw(canvas, crop); +} + +function updateCursor(mouseX: number, mouseY: number) { + const canvas = get(canvasElement); + if (!canvas) { + return; + } + + const crop = get(cropSettings); + const { + onLeftBoundary, + onRightBoundary, + onTopBoundary, + onBottomBoundary, + onTopLeftCorner, + onTopRightCorner, + onBottomLeftCorner, + onBottomRightCorner, + } = isOnCropBoundary(mouseX, mouseY, crop); + + if (onTopLeftCorner || onBottomRightCorner) { + setCursor('nwse-resize'); + } else if (onTopRightCorner || onBottomLeftCorner) { + setCursor('nesw-resize'); + } else if (onLeftBoundary || onRightBoundary) { + setCursor('ew-resize'); + } else if (onTopBoundary || onBottomBoundary) { + setCursor('ns-resize'); + } else if (isInCropArea(mouseX, mouseY, crop)) { + setCursor('move'); + } else { + setCursor('default'); + } + + function setCursor(cursorName: string) { + if (get(canvasCursor) != cursorName && canvas) { + canvasCursor.set(cursorName); + canvas.style.cursor = cursorName; + } + } +} + +function stopInteraction() { + isResizingOrDragging.set(false); + isDragging.set(false); + resizeSide.set(''); + fadeOverlay(true); // Darken the background + + setTimeout(() => { + checkEdits(); + }, 1); +} + +export function checkEdits() { + const cropImageSizeParams = get(cropSettings); + const originalImgSize = get(cropImageSize).map((el) => el * get(cropImageScale)); + const changed = + Math.abs(originalImgSize[0] - cropImageSizeParams.width) > 2 || + Math.abs(originalImgSize[1] - cropImageSizeParams.height) > 2; + cropSettingsChanged.set(changed); +} + +function fadeOverlay(toDark: boolean) { + const step = toDark ? 0.05 : -0.05; + const minDarkness = 0.4; + const maxDarkness = 0.65; + + isResizingOrDragging.set(!toDark); + + const animate = () => { + darkenLevel.update((level) => { + const newLevel = Math.min(maxDarkness, Math.max(minDarkness, level + step)); + draw(get(canvasElement), get(cropSettings)); + return newLevel; + }); + + if ((toDark && get(darkenLevel) < maxDarkness) || (!toDark && get(darkenLevel) > minDarkness)) { + animationFrame.set(requestAnimationFrame(animate)); + } else { + cancelAnimationFrame(get(animationFrame) as number); + } + }; + + cancelAnimationFrame(get(animationFrame) as number); + animationFrame.set(requestAnimationFrame(animate)); +} diff --git a/web/src/lib/components/asset-viewer/editor/editor-panel.svelte b/web/src/lib/components/asset-viewer/editor/editor-panel.svelte index 504147b2cfd2d..a6412d9fc0541 100644 --- a/web/src/lib/components/asset-viewer/editor/editor-panel.svelte +++ b/web/src/lib/components/asset-viewer/editor/editor-panel.svelte @@ -5,7 +5,7 @@ import { createEventDispatcher, onMount } from 'svelte'; import CircleIconButton from '../../elements/buttons/circle-icon-button.svelte'; import { t } from 'svelte-i18n'; - import CropComponent from './crop-tool.svelte'; + import CropComponent from './crop-tool/crop-tool.svelte'; import Button from '$lib/components/elements/buttons/button.svelte'; import { cropSettingsChanged } from '$lib/stores/asset-editor.store'; import { derived } from 'svelte/store'; diff --git a/web/src/lib/stores/asset-editor.store.ts b/web/src/lib/stores/asset-editor.store.ts index daf5bb68e8656..cc2ce7615a3c8 100644 --- a/web/src/lib/stores/asset-editor.store.ts +++ b/web/src/lib/stores/asset-editor.store.ts @@ -6,6 +6,14 @@ export const cropImageScale = writable(1); export const cropAspectRatio = writable('free'); export const cropSettingsChanged = writable(false); +export function resetGlobalCropStore() { + cropSettings.set({ x: 0, y: 0, width: 100, height: 100 }); + cropImageSize.set([1000, 1000]); + cropImageScale.set(1); + cropAspectRatio.set('free'); + cropSettingsChanged.set(false); +} + export type CropAspectRatio = '1:1' | '16:9' | '3:2' | '7:5' | 'free' | 'reset'; export type CropSettings = { From 25f8d2d68d801538a3924b1f864d4e0682f8e97d Mon Sep 17 00:00:00 2001 From: ilyaChuk <86570508+ilyaChuk@users.noreply.github.com> Date: Mon, 29 Jul 2024 23:58:50 +0300 Subject: [PATCH 24/47] fix lint --- .../components/asset-viewer/editor/crop-tool/mouse-handlers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/mouse-handlers.ts b/web/src/lib/components/asset-viewer/editor/crop-tool/mouse-handlers.ts index bbcff7f9301cf..0d7588b4c270f 100644 --- a/web/src/lib/components/asset-viewer/editor/crop-tool/mouse-handlers.ts +++ b/web/src/lib/components/asset-viewer/editor/crop-tool/mouse-handlers.ts @@ -84,7 +84,7 @@ export function handleMouseOut() { window.removeEventListener('mouseup', handleMouseUp); } -function getMousePosition(e: MouseEvent, canvas: HTMLCanvasElement) { +function getMousePosition(e: MouseEvent) { return { mouseX: e.offsetX, mouseY: e.offsetY }; } From 0790090469215e1702fb9dfbac6c8d27841311cb Mon Sep 17 00:00:00 2001 From: ilyaChuk <86570508+ilyaChuk@users.noreply.github.com> Date: Tue, 30 Jul 2024 16:07:35 +0300 Subject: [PATCH 25/47] fix ts --- .../asset-viewer/editor/crop-tool/mouse-handlers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/mouse-handlers.ts b/web/src/lib/components/asset-viewer/editor/crop-tool/mouse-handlers.ts index 0d7588b4c270f..1b21ca6a027dd 100644 --- a/web/src/lib/components/asset-viewer/editor/crop-tool/mouse-handlers.ts +++ b/web/src/lib/components/asset-viewer/editor/crop-tool/mouse-handlers.ts @@ -27,7 +27,7 @@ export function handleMouseDown(e: MouseEvent) { } const crop = get(cropSettings); - const { mouseX, mouseY } = getMousePosition(e, canvas); + const { mouseX, mouseY } = getMousePosition(e); const { onLeftBoundary, @@ -64,7 +64,7 @@ export function handleMouseMove(e: MouseEvent) { } const resizeSideValue = get(resizeSide); - const { mouseX, mouseY } = getMousePosition(e, canvas); + const { mouseX, mouseY } = getMousePosition(e); if (get(isDragging)) { moveCrop(mouseX, mouseY); From ef1ff628f738e81bfdb77fdf2ea37e47a7bec65a Mon Sep 17 00:00:00 2001 From: ilyaChuk <86570508+ilyaChuk@users.noreply.github.com> Date: Tue, 30 Jul 2024 20:36:36 +0300 Subject: [PATCH 26/47] callback function as props --- .../components/asset-viewer/asset-viewer.svelte | 8 ++++---- .../asset-viewer/editor/editor-panel.svelte | 16 +++++++--------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 7dbb8a2a6a8ad..26e4883c9a83a 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -561,8 +561,8 @@ let selectedEditType: string = ''; - function handleUpdateSelectedEditType(event: CustomEvent) { - selectedEditType = event.detail; + function handleUpdateSelectedEditType(type: string) { + selectedEditType = type; } $: if (!$user) { @@ -760,8 +760,8 @@ > (isShowEditor = false)} + onUpdateSelectedType={handleUpdateSelectedEditType} + onClose={() => (isShowEditor = false)} />
{/if} diff --git a/web/src/lib/components/asset-viewer/editor/editor-panel.svelte b/web/src/lib/components/asset-viewer/editor/editor-panel.svelte index a6412d9fc0541..bb2d65e5bd98e 100644 --- a/web/src/lib/components/asset-viewer/editor/editor-panel.svelte +++ b/web/src/lib/components/asset-viewer/editor/editor-panel.svelte @@ -2,7 +2,7 @@ import { websocketEvents } from '$lib/stores/websocket'; import { type AssetResponseDto } from '@immich/sdk'; import { mdiClose, mdiCropRotate } from '@mdi/js'; - import { createEventDispatcher, onMount } from 'svelte'; + import { onMount } from 'svelte'; import CircleIconButton from '../../elements/buttons/circle-icon-button.svelte'; import { t } from 'svelte-i18n'; import CropComponent from './crop-tool/crop-tool.svelte'; @@ -20,10 +20,8 @@ }); }); - const dispatch = createEventDispatcher<{ - updateSelectedType: string; - close: void; - }>(); + export let onUpdateSelectedType: (type: string) => void; + export let onClose: () => void; let editTypes = [ { @@ -45,21 +43,21 @@ ); setTimeout(() => { - dispatch('updateSelectedType', selectedType); + onUpdateSelectedType(selectedType); }, 1); function selectType(name: string) { selectedType = name; - dispatch('updateSelectedType', selectedType); + onUpdateSelectedType(selectedType); } function save() { - dispatch('close'); + onClose(); }
- dispatch('close')} /> +

{$t('editor')}

From bfe85a3b74348ae83dfa37d3e3cdf5e418477802 Mon Sep 17 00:00:00 2001 From: ilyaChuk <86570508+ilyaChuk@users.noreply.github.com> Date: Tue, 30 Jul 2024 20:44:58 +0300 Subject: [PATCH 27/47] correctly disabling shortcuts --- .../asset-viewer/asset-viewer.svelte | 44 +++++++------------ 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 26e4883c9a83a..4293434599dfa 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -276,17 +276,12 @@ }; const toggleDetailPanel = () => { - if (!showNavbar) { - return; - } isShowActivity = false; $isShowDetail = !$isShowDetail; }; const closeViewer = async () => { - if (isShowEditor) { - isShowEditor = false; - } else if ($slideshowState === SlideshowState.None) { + if ($slideshowState === SlideshowState.None) { dispatch('close'); await navigate({ targetRoute: 'current', assetId: null }); } else { @@ -294,6 +289,10 @@ } }; + const closeEditor = () => { + isShowEditor = false + } + const navigateAssetRandom = async () => { if (!assetStore) { return; @@ -311,9 +310,6 @@ }; const navigateAsset = async (order?: 'previous' | 'next', e?: Event) => { - if (!showNavbar) { - return; - } if (!order) { if ($slideshowState === SlideshowState.PlaySlideshow) { order = $slideshowNavigation === SlideshowNavigation.AscendingOrder ? 'previous' : 'next'; @@ -354,9 +350,6 @@ }; const trashOrDelete = async (force: boolean = false) => { - if (!showNavbar) { - return; - } if (force || !isTrashEnabled) { if ($showDeleteModal) { isShowDeleteConfirmation = true; @@ -403,9 +396,6 @@ }; const toggleFavorite = async () => { - if (!showNavbar) { - return; - } try { const data = await updateAsset({ id: asset.id, @@ -461,9 +451,6 @@ }; const toggleAssetArchive = async () => { - if (!showNavbar) { - return; - } const updatedAsset = await toggleArchive(asset); if (updatedAsset) { dispatch('action', { type: asset.isArchived ? AssetAction.ARCHIVE : AssetAction.UNARCHIVE, asset: asset }); @@ -568,19 +555,20 @@ $: if (!$user) { shouldShowShareModal = false; } + //TODO: refactor shortcut conditions navigateAsset('previous') }, - { shortcut: { key: 'ArrowRight' }, onShortcut: () => navigateAsset('next') }, - { shortcut: { key: 'd', shift: true }, onShortcut: () => (showNavbar ? downloadFile(asset) : null) }, - { shortcut: { key: 'Delete' }, onShortcut: () => trashOrDelete(asset.isTrashed) }, - { shortcut: { key: 'Delete', shift: true }, onShortcut: () => trashOrDelete(true) }, - { shortcut: { key: 'Escape' }, onShortcut: closeViewer }, - { shortcut: { key: 'f' }, onShortcut: toggleFavorite }, - { shortcut: { key: 'i' }, onShortcut: toggleDetailPanel }, + { shortcut: { key: 'a', shift: true }, onShortcut: () => showNavbar && toggleAssetArchive() }, + { shortcut: { key: 'ArrowLeft' }, onShortcut: () => showNavbar && navigateAsset('previous') }, + { shortcut: { key: 'ArrowRight' }, onShortcut: () => showNavbar && navigateAsset('next') }, + { shortcut: { key: 'd', shift: true }, onShortcut: () => (showNavbar && downloadFile(asset)) }, + { shortcut: { key: 'Delete' }, onShortcut: () => showNavbar && trashOrDelete(asset.isTrashed) }, + { shortcut: { key: 'Delete', shift: true }, onShortcut: () => showNavbar && trashOrDelete(true) }, + { shortcut: { key: 'Escape' }, onShortcut: () => isShowEditor ? closeEditor() : closeViewer() }, + { shortcut: { key: 'f' }, onShortcut: () => showNavbar && toggleFavorite() }, + { shortcut: { key: 'i' }, onShortcut: () => showNavbar && toggleDetailPanel() }, ]} /> @@ -761,7 +749,7 @@ (isShowEditor = false)} + onClose={closeEditor} /> {/if} From 2bd17ba71a4031096f2d738e96ea2e491282ba85 Mon Sep 17 00:00:00 2001 From: ilyaChuk <86570508+ilyaChuk@users.noreply.github.com> Date: Wed, 31 Jul 2024 00:34:57 +0300 Subject: [PATCH 28/47] convenient canvas borders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • 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. --- .../asset-viewer/asset-viewer.svelte | 14 +-- .../editor/crop-tool/canvas-drawing.ts | 50 +++++--- .../editor/crop-tool/crop-canvas.svelte | 6 +- .../editor/crop-tool/crop-settings.ts | 33 +++-- .../editor/crop-tool/crop-store.ts | 1 + .../editor/crop-tool/image-loading.ts | 7 +- .../editor/crop-tool/mouse-handlers.ts | 115 +++++++++++------- 7 files changed, 138 insertions(+), 88 deletions(-) diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 4293434599dfa..94c16fa0ebf3d 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -290,8 +290,8 @@ }; const closeEditor = () => { - isShowEditor = false - } + isShowEditor = false; + }; const navigateAssetRandom = async () => { if (!assetStore) { @@ -563,10 +563,10 @@ { shortcut: { key: 'a', shift: true }, onShortcut: () => showNavbar && toggleAssetArchive() }, { shortcut: { key: 'ArrowLeft' }, onShortcut: () => showNavbar && navigateAsset('previous') }, { shortcut: { key: 'ArrowRight' }, onShortcut: () => showNavbar && navigateAsset('next') }, - { shortcut: { key: 'd', shift: true }, onShortcut: () => (showNavbar && downloadFile(asset)) }, + { shortcut: { key: 'd', shift: true }, onShortcut: () => showNavbar && downloadFile(asset) }, { shortcut: { key: 'Delete' }, onShortcut: () => showNavbar && trashOrDelete(asset.isTrashed) }, { shortcut: { key: 'Delete', shift: true }, onShortcut: () => showNavbar && trashOrDelete(true) }, - { shortcut: { key: 'Escape' }, onShortcut: () => isShowEditor ? closeEditor() : closeViewer() }, + { shortcut: { key: 'Escape' }, onShortcut: () => (isShowEditor ? closeEditor() : closeViewer()) }, { shortcut: { key: 'f' }, onShortcut: () => showNavbar && toggleFavorite() }, { shortcut: { key: 'i' }, onShortcut: () => showNavbar && toggleDetailPanel() }, ]} @@ -746,11 +746,7 @@ class="z-[1002] row-start-1 row-span-4 w-[460px] overflow-y-auto bg-immich-bg transition-all dark:border-l dark:border-l-immich-dark-gray dark:bg-immich-dark-bg" translate="yes" > - + {/if} diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/canvas-drawing.ts b/web/src/lib/components/asset-viewer/editor/crop-tool/canvas-drawing.ts index 6676acb7921bb..57c8489a7db87 100644 --- a/web/src/lib/components/asset-viewer/editor/crop-tool/canvas-drawing.ts +++ b/web/src/lib/components/asset-viewer/editor/crop-tool/canvas-drawing.ts @@ -1,7 +1,7 @@ import type { CropSettings } from '$lib/stores/asset-editor.store'; import { get } from 'svelte/store'; -import { context2D, darkenLevel, imgElement, isResizingOrDragging } from './crop-store'; - +import { context2D, darkenLevel, imgElement, isResizingOrDragging, padding } from './crop-store'; +const mPadding = get(padding); export function draw(canvas: HTMLCanvasElement | null, crop: CropSettings) { const ctx = get(context2D); const img = get(imgElement); @@ -10,7 +10,7 @@ export function draw(canvas: HTMLCanvasElement | null, crop: CropSettings) { } ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + ctx.drawImage(img, mPadding, mPadding, canvas.width - 2 * mPadding, canvas.height - 2 * mPadding); drawOverlay(canvas, crop); drawCropRect(crop); } @@ -24,7 +24,7 @@ export function drawCropRect(crop: CropSettings) { ctx.globalCompositeOperation = 'exclusion'; ctx.strokeStyle = 'white'; ctx.lineWidth = 2; - ctx.strokeRect(crop.x, crop.y, crop.width, crop.height); + ctx.strokeRect(crop.x + mPadding, crop.y + mPadding, crop.width, crop.height); if (get(isResizingOrDragging)) { ctx.strokeStyle = 'white'; @@ -32,24 +32,24 @@ export function drawCropRect(crop: CropSettings) { const thirdWidth = crop.width / 3; ctx.beginPath(); - ctx.moveTo(crop.x + thirdWidth, crop.y); - ctx.lineTo(crop.x + thirdWidth, crop.y + crop.height); + ctx.moveTo(crop.x + thirdWidth + mPadding, crop.y + mPadding); + ctx.lineTo(crop.x + thirdWidth + mPadding, crop.y + crop.height + mPadding); ctx.stroke(); ctx.beginPath(); - ctx.moveTo(crop.x + 2 * thirdWidth, crop.y); - ctx.lineTo(crop.x + 2 * thirdWidth, crop.y + crop.height); + ctx.moveTo(crop.x + 2 * thirdWidth + mPadding, crop.y + mPadding); + ctx.lineTo(crop.x + 2 * thirdWidth + mPadding, crop.y + crop.height + mPadding); ctx.stroke(); const thirdHeight = crop.height / 3; ctx.beginPath(); - ctx.moveTo(crop.x, crop.y + thirdHeight); - ctx.lineTo(crop.x + crop.width, crop.y + thirdHeight); + ctx.moveTo(crop.x + mPadding, crop.y + thirdHeight + mPadding); + ctx.lineTo(crop.x + crop.width + mPadding, crop.y + thirdHeight + mPadding); ctx.stroke(); ctx.beginPath(); - ctx.moveTo(crop.x, crop.y + 2 * thirdHeight); - ctx.lineTo(crop.x + crop.width, crop.y + 2 * thirdHeight); + ctx.moveTo(crop.x + mPadding, crop.y + 2 * thirdHeight + mPadding); + ctx.lineTo(crop.x + crop.width + mPadding, crop.y + 2 * thirdHeight + mPadding); ctx.stroke(); } @@ -58,19 +58,19 @@ export function drawCropRect(crop: CropSettings) { ctx.fillStyle = 'white'; ctx.beginPath(); - ctx.arc(crop.x, crop.y, radius, 0, 2 * Math.PI); + ctx.arc(crop.x + mPadding, crop.y + mPadding, radius, 0, 2 * Math.PI); ctx.fill(); ctx.beginPath(); - ctx.arc(crop.x + crop.width, crop.y, radius, 0, 2 * Math.PI); + ctx.arc(crop.x + crop.width + mPadding, crop.y + mPadding, radius, 0, 2 * Math.PI); ctx.fill(); ctx.beginPath(); - ctx.arc(crop.x, crop.y + crop.height, radius, 0, 2 * Math.PI); + ctx.arc(crop.x + mPadding, crop.y + crop.height + mPadding, radius, 0, 2 * Math.PI); ctx.fill(); ctx.beginPath(); - ctx.arc(crop.x + crop.width, crop.y + crop.height, radius, 0, 2 * Math.PI); + ctx.arc(crop.x + crop.width + mPadding, crop.y + crop.height + mPadding, radius, 0, 2 * Math.PI); ctx.fill(); } @@ -83,8 +83,18 @@ export function drawOverlay(canvas: HTMLCanvasElement, crop: CropSettings) { ctx.fillStyle = `rgba(0, 0, 0, ${darken})`; - ctx.fillRect(0, 0, canvas.width, crop.y); - ctx.fillRect(0, crop.y, crop.x, crop.height); - ctx.fillRect(crop.x + crop.width, crop.y, canvas.width - crop.x - crop.width, crop.height); - ctx.fillRect(0, crop.y + crop.height, canvas.width, canvas.height - crop.y - crop.height); + ctx.fillRect(mPadding, mPadding, canvas.width - 2 * mPadding, crop.y); + ctx.fillRect(mPadding, crop.y + mPadding, crop.x, crop.height); + ctx.fillRect( + crop.x + crop.width + mPadding, + crop.y + mPadding, + canvas.width - crop.x - crop.width - 2 * mPadding, + crop.height, + ); + ctx.fillRect( + mPadding, + crop.y + crop.height + mPadding, + canvas.width - 2 * mPadding, + canvas.height - crop.y - crop.height - 2 * mPadding, + ); } diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-canvas.svelte b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-canvas.svelte index 26a65533f4414..7edff7572500c 100644 --- a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-canvas.svelte +++ b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-canvas.svelte @@ -39,9 +39,12 @@ $imgElement.addEventListener('error', (error) => { handleError(error, $t('error_loading_image')); }); + + window.addEventListener('mousemove', handleMouseMove) }); onDestroy(() => { + window.removeEventListener('mousemove', handleMouseMove) resetCropStore(); resetGlobalCropStore(); }); @@ -55,7 +58,6 @@ @@ -63,7 +65,7 @@ diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-canvas.svelte b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-canvas.svelte deleted file mode 100644 index a56187b0bb7c5..0000000000000 --- a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-canvas.svelte +++ /dev/null @@ -1,76 +0,0 @@ - - -
- -
- - diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-settings.ts b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-settings.ts index 8b1dbef350c20..415ef83cc5a69 100644 --- a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-settings.ts +++ b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-settings.ts @@ -1,23 +1,19 @@ import type { CropAspectRatio, CropSettings } from '$lib/stores/asset-editor.store'; import { get } from 'svelte/store'; -import { padding } from './crop-store'; +import { cropAreaEl } from './crop-store'; import { checkEdits } from './mouse-handlers'; -const mPadding = get(padding); export function recalculateCrop( crop: CropSettings, - canvas: HTMLCanvasElement, + canvas: HTMLElement, aspectRatio: CropAspectRatio, returnNewCrop = false, ): CropSettings | null { - if (!canvas) { - return null; - } - const { width, height, x, y } = crop; - let newWidth = width; - let newHeight = height; - const canvasW = canvas.width - mPadding * 2; - const canvasH = canvas.height - mPadding * 2; + 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); @@ -31,23 +27,17 @@ export function recalculateCrop( newWidth = w; newHeight = h; } - newWidth -= 1; - newHeight -= 1; + + 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: Math.max(0, x + (width - newWidth) / 2), - y: Math.max(0, y + (height - newHeight) / 2), + x: newX, + y: newY, }; - if (newCrop.x + newWidth > canvasW) { - newCrop.x = canvasW - newWidth; - } - if (newCrop.y + newHeight > canvasH) { - newCrop.y = canvasH - newHeight; - } - if (returnNewCrop) { setTimeout(() => { checkEdits(); @@ -56,16 +46,23 @@ export function recalculateCrop( } else { crop.width = newWidth; crop.height = newHeight; - crop.x = newCrop.x; - crop.y = newCrop.y; + crop.x = newX; + crop.y = newY; return null; } } export function animateCropChange(crop: CropSettings, newCrop: CropSettings, draw: () => void, duration = 100) { - if (!newCrop) { + 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 }; @@ -128,7 +125,6 @@ export function adjustDimensions( h = w / aspectMultiplier; } } - if (h > yLimit) { h = yLimit; if (aspectRatio !== 'free') { diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-store.ts b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-store.ts index a1c2052afc8cd..8e27d41f21926 100644 --- a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-store.ts +++ b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-store.ts @@ -7,10 +7,11 @@ export const canvasCursor = writable('default'); export const dragOffset = writable({ x: 0, y: 0 }); export const resizeSide = writable(''); export const imgElement = writable(null); -export const canvasElement = writable(null); -export const context2D = writable(null); +export const cropAreaEl = writable(null); export const isDragging = writable(false); -export const padding = writable(2.5); + +export const overlayEl = writable(null); +export const cropFrame = writable(null); export function resetCropStore() { darkenLevel.set(0.65); @@ -20,7 +21,7 @@ export function resetCropStore() { dragOffset.set({ x: 0, y: 0 }); resizeSide.set(''); imgElement.set(null); - canvasElement.set(null); - context2D.set(null); + cropAreaEl.set(null); isDragging.set(false); + overlayEl.set(null); } diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/drawing.ts b/web/src/lib/components/asset-viewer/editor/crop-tool/drawing.ts new file mode 100644 index 0000000000000..85e7f4b1c408d --- /dev/null +++ b/web/src/lib/components/asset-viewer/editor/crop-tool/drawing.ts @@ -0,0 +1,40 @@ +import type { CropSettings } from '$lib/stores/asset-editor.store'; +import { get } from 'svelte/store'; +import { cropFrame, overlayEl } from './crop-store'; + +export function draw(crop: CropSettings) { + const mCropFrame = get(cropFrame); + + if (!mCropFrame) { + return; + } + + mCropFrame.style.left = `${crop.x}px`; + mCropFrame.style.top = `${crop.y}px`; + mCropFrame.style.width = `${crop.width}px`; + mCropFrame.style.height = `${crop.height}px`; + + drawOverlay(crop); +} + +export function drawOverlay(crop: CropSettings) { + const overlay = get(overlayEl); + if (!overlay) { + return; + } + + overlay.style.clipPath = ` + polygon( + 0% 0%, + 0% 100%, + 100% 100%, + 100% 0%, + 0% 0%, + ${crop.x}px ${crop.y}px, + ${crop.x + crop.width}px ${crop.y}px, + ${crop.x + crop.width}px ${crop.y + crop.height}px, + ${crop.x}px ${crop.y + crop.height}px, + ${crop.x}px ${crop.y}px + ) + `; +} diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/image-loading.ts b/web/src/lib/components/asset-viewer/editor/crop-tool/image-loading.ts index aea9e7bd9bd54..32fe630bc5701 100644 --- a/web/src/lib/components/asset-viewer/editor/crop-tool/image-loading.ts +++ b/web/src/lib/components/asset-viewer/editor/crop-tool/image-loading.ts @@ -1,27 +1,18 @@ import { cropImageScale, cropImageSize, cropSettings } from '$lib/stores/asset-editor.store'; import { get } from 'svelte/store'; -import { draw } from './canvas-drawing'; -import { canvasElement, context2D, imgElement, padding } from './crop-store'; +import { cropAreaEl, imgElement } from './crop-store'; +import { draw } from './drawing'; -const mPadding = get(padding); export function onImageLoad() { const img = get(imgElement); - const canvas = get(canvasElement); - let ctx = get(context2D); + const cropArea = get(cropAreaEl); - if (!canvas || !img) { + if (!cropArea || !img) { return; } - if (!ctx) { - ctx = canvas.getContext('2d'); - context2D.set(ctx); - } - - resizeCanvas(); - - const containerWidth = canvas.parentElement?.clientWidth ?? 0; - const containerHeight = canvas.parentElement?.clientHeight ?? 0; + const containerWidth = cropArea?.clientWidth ?? 0; + const containerHeight = cropArea?.clientHeight ?? 0; const imageAspectRatio = img.width / img.height; let scale: number; @@ -36,7 +27,6 @@ export function onImageLoad() { scale = containerWidth / img.width; } } - cropImageSize.set([img.width, img.height]); cropImageScale.set(scale); @@ -48,18 +38,22 @@ export function onImageLoad() { return crop; }); - draw(canvas, get(cropSettings)); + img.style.width = `${img.width * scale}px`; + img.style.height = `${img.height * scale}px`; + + draw(get(cropSettings)); } export function resizeCanvas() { const img = get(imgElement); - const canvas = get(canvasElement); - if (!canvas || !img) { + const cropArea = get(cropAreaEl); + + if (!cropArea || !img) { return; } - const containerWidth = canvas.parentElement?.clientWidth ?? 0; - const containerHeight = canvas.parentElement?.clientHeight ?? 0; + const containerWidth = cropArea?.clientWidth ?? 0; + const containerHeight = cropArea?.clientHeight ?? 0; const imageAspectRatio = img.width / img.height; let scale; @@ -75,8 +69,14 @@ export function resizeCanvas() { } } - canvas.width = img.width * scale + 2 * mPadding; - canvas.height = img.height * scale + 2 * mPadding; + img.style.width = `${img.width * scale}px`; + img.style.height = `${img.height * scale}px`; + + const cropFrame = cropArea.querySelector('.crop-frame') as HTMLElement; + if (cropFrame) { + cropFrame.style.width = `${img.width * scale}px`; + cropFrame.style.height = `${img.height * scale}px`; + } - draw(canvas, get(cropSettings)); + draw(get(cropSettings)); } diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/mouse-handlers.ts b/web/src/lib/components/asset-viewer/editor/crop-tool/mouse-handlers.ts index fdbb0fa74d038..efc3d286982da 100644 --- a/web/src/lib/components/asset-viewer/editor/crop-tool/mouse-handlers.ts +++ b/web/src/lib/components/asset-viewer/editor/crop-tool/mouse-handlers.ts @@ -8,23 +8,20 @@ import { type CropSettings, } from '$lib/stores/asset-editor.store'; import { get } from 'svelte/store'; -import { draw } from './canvas-drawing'; import { adjustDimensions, keepAspectRatio } from './crop-settings'; import { - animationFrame, canvasCursor, - canvasElement, - darkenLevel, + cropAreaEl, dragOffset, isDragging, isResizingOrDragging, - padding, + overlayEl, resizeSide, } from './crop-store'; -const mPadding = get(padding); +import { draw } from './drawing'; export function handleMouseDown(e: MouseEvent) { - const canvas = get(canvasElement); + const canvas = get(cropAreaEl); if (!canvas) { return; } @@ -63,7 +60,7 @@ export function handleMouseDown(e: MouseEvent) { } export function handleMouseMove(e: MouseEvent) { - const canvas = get(canvasElement); + const canvas = get(cropAreaEl); if (!canvas) { return; } @@ -86,19 +83,13 @@ export function handleMouseUp() { stopInteraction(); } -export function handleMouseOut() { - // stopInteraction(); - // window.removeEventListener('mouseup', handleMouseUp); -} - function getMousePosition(e: MouseEvent) { - let offsetX = e.clientX - mPadding; - let offsetY = e.clientY - mPadding; - const clienRect = getBoundingClientRectCached(get(canvasElement)); + let offsetX = e.clientX; + let offsetY = e.clientY; + const clienRect = getBoundingClientRectCached(get(cropAreaEl)); offsetX -= clienRect?.left ?? 0; offsetY -= clienRect?.top ?? 0; - return { mouseX: offsetX, mouseY: offsetY }; } @@ -123,6 +114,20 @@ function isOnCropBoundary(mouseX: number, mouseY: number, crop: CropSettings) { const sensitivity = 10; const cornerSensitivity = 15; + const outOfBound = mouseX > get(cropImageSize)[0] || mouseY > get(cropImageSize)[1] || mouseX < 0 || mouseY < 0; + if (outOfBound) { + return { + onLeftBoundary: false, + onRightBoundary: false, + onTopBoundary: false, + onBottomBoundary: false, + onTopLeftCorner: false, + onTopRightCorner: false, + onBottomLeftCorner: false, + onBottomRightCorner: false, + }; + } + const onLeftBoundary = mouseX >= x - sensitivity && mouseX <= x + sensitivity && mouseY >= y && mouseY <= y + height; const onRightBoundary = mouseX >= x + width - sensitivity && mouseX <= x + width + sensitivity && mouseY >= y && mouseY <= y + height; @@ -204,13 +209,13 @@ function startDragging(mouseX: number, mouseY: number) { isDragging.set(true); const crop = get(cropSettings); isResizingOrDragging.set(true); - dragOffset.set({ x: mouseX - crop.x - mPadding, y: mouseY - crop.y - mPadding }); + dragOffset.set({ x: mouseX - crop.x, y: mouseY - crop.y }); fadeOverlay(false); } function moveCrop(mouseX: number, mouseY: number) { - const canvas = get(canvasElement); - if (!canvas) { + const cropArea = get(cropAreaEl); + if (!cropArea) { return; } @@ -220,20 +225,20 @@ function moveCrop(mouseX: number, mouseY: number) { let newX = mouseX - x; let newY = mouseY - y; - newX = Math.max(mPadding, Math.min(canvas.width - crop.width - mPadding, newX)); - newY = Math.max(mPadding, Math.min(canvas.height - crop.height - mPadding, newY)); + newX = Math.max(0, Math.min(cropArea.clientWidth - crop.width, newX)); + newY = Math.max(0, Math.min(cropArea.clientHeight - crop.height, newY)); cropSettings.update((crop) => { - crop.x = newX - mPadding; - crop.y = newY - mPadding; + crop.x = newX; + crop.y = newY; return crop; }); - draw(canvas, crop); + draw(crop); } function resizeCrop(mouseX: number, mouseY: number) { - const canvas = get(canvasElement); + const canvas = get(cropAreaEl); const crop = get(cropSettings); const resizeSideValue = get(resizeSide); if (!canvas || !resizeSideValue) { @@ -242,10 +247,9 @@ function resizeCrop(mouseX: number, mouseY: number) { fadeOverlay(false); const { x, y, width, height } = crop; - const minSize = 10; - let newWidth, newHeight; - const canvasW = canvas.width - mPadding * 2; - const canvasH = canvas.height - mPadding * 2; + const minSize = 50; + let newWidth = width; + let newHeight = height; switch (resizeSideValue) { case 'left': { newWidth = width + x - mouseX; @@ -253,8 +257,8 @@ function resizeCrop(mouseX: number, mouseY: number) { if (newWidth >= minSize && mouseX >= 0) { const { newWidth: w, newHeight: h } = keepAspectRatio(newWidth, newHeight, get(cropAspectRatio)); cropSettings.update((crop) => { - crop.width = Math.min(w, canvasW); - crop.height = Math.min(h, canvasH); + crop.width = Math.max(minSize, Math.min(w, canvas.clientWidth)); + crop.height = Math.max(minSize, Math.min(h, canvas.clientHeight)); crop.x = Math.max(0, x + width - crop.width); return crop; }); @@ -264,11 +268,11 @@ function resizeCrop(mouseX: number, mouseY: number) { case 'right': { newWidth = mouseX - x; newHeight = height; - if (newWidth >= minSize && mouseX <= canvasW) { + if (newWidth >= minSize && mouseX <= canvas.clientWidth) { const { newWidth: w, newHeight: h } = keepAspectRatio(newWidth, newHeight, get(cropAspectRatio)); cropSettings.update((crop) => { - crop.width = Math.min(w, canvasW - x); - crop.height = Math.min(h, canvasH); + crop.width = Math.max(minSize, Math.min(w, canvas.clientWidth - x)); + crop.height = Math.max(minSize, Math.min(h, canvas.clientHeight)); return crop; }); } @@ -282,13 +286,13 @@ function resizeCrop(mouseX: number, mouseY: number) { newWidth, newHeight, get(cropAspectRatio), - canvasW, - canvasH, + canvas.clientWidth, + canvas.clientHeight, ); cropSettings.update((crop) => { crop.y = Math.max(0, y + height - h); - crop.width = w; - crop.height = h; + crop.width = Math.max(minSize, w); + crop.height = Math.max(minSize, h); return crop; }); } @@ -297,17 +301,17 @@ function resizeCrop(mouseX: number, mouseY: number) { case 'bottom': { newHeight = mouseY - y; newWidth = width; - if (newHeight >= minSize && mouseY <= canvasH) { + if (newHeight >= minSize && mouseY <= canvas.clientHeight) { const { newWidth: w, newHeight: h } = adjustDimensions( newWidth, newHeight, get(cropAspectRatio), - canvasW, - canvasH - y, + canvas.clientWidth, + canvas.clientHeight - y, ); cropSettings.update((crop) => { - crop.width = w; - crop.height = h; + crop.width = Math.max(minSize, w); + crop.height = Math.max(minSize, h); return crop; }); } @@ -316,96 +320,88 @@ function resizeCrop(mouseX: number, mouseY: number) { case 'top-left': { newWidth = width + x - Math.max(mouseX, 0); newHeight = height + y - Math.max(mouseY, 0); - if (newWidth >= minSize && newHeight >= minSize) { - const { newWidth: w, newHeight: h } = adjustDimensions( - newWidth, - newHeight, - get(cropAspectRatio), - canvasW, - canvasH, - ); - cropSettings.update((crop) => { - crop.width = w; - crop.height = h; - crop.x = Math.max(0, x + width - crop.width); - crop.y = Math.max(0, y + height - crop.height); - return crop; - }); - } + const { newWidth: w, newHeight: h } = adjustDimensions( + newWidth, + newHeight, + get(cropAspectRatio), + canvas.clientWidth, + canvas.clientHeight, + ); + cropSettings.update((crop) => { + crop.width = Math.max(minSize, w); + crop.height = Math.max(minSize, h); + crop.x = Math.max(0, x + width - crop.width); + crop.y = Math.max(0, y + height - crop.height); + return crop; + }); break; } case 'top-right': { newWidth = Math.max(mouseX, 0) - x; newHeight = height + y - Math.max(mouseY, 0); - if (newWidth >= minSize && newHeight >= minSize) { - const { newWidth: w, newHeight: h } = adjustDimensions( - newWidth, - newHeight, - get(cropAspectRatio), - canvasW - x, - y + height, - ); - cropSettings.update((crop) => { - crop.width = w; - crop.height = h; - crop.y = y + height - h; - return crop; - }); - } + const { newWidth: w, newHeight: h } = adjustDimensions( + newWidth, + newHeight, + get(cropAspectRatio), + canvas.clientWidth - x, + y + height, + ); + cropSettings.update((crop) => { + crop.width = Math.max(minSize, w); + crop.height = Math.max(minSize, h); + crop.y = Math.max(0, y + height - crop.height); + return crop; + }); break; } case 'bottom-left': { newWidth = width + x - Math.max(mouseX, 0); newHeight = Math.max(mouseY, 0) - y; - if (newWidth >= minSize && newHeight >= minSize) { - const { newWidth: w, newHeight: h } = adjustDimensions( - newWidth, - newHeight, - get(cropAspectRatio), - canvasW, - canvasH - y, - ); - cropSettings.update((crop) => { - crop.width = w; - crop.height = h; - crop.x = Math.max(0, x + width - crop.width); - return crop; - }); - } + const { newWidth: w, newHeight: h } = adjustDimensions( + newWidth, + newHeight, + get(cropAspectRatio), + canvas.clientWidth, + canvas.clientHeight - y, + ); + cropSettings.update((crop) => { + crop.width = Math.max(minSize, w); + crop.height = Math.max(minSize, h); + crop.x = Math.max(0, x + width - crop.width); + return crop; + }); break; } case 'bottom-right': { newWidth = Math.max(mouseX, 0) - x; newHeight = Math.max(mouseY, 0) - y; - if (newWidth >= minSize && newHeight >= minSize) { - const { newWidth: w, newHeight: h } = adjustDimensions( - newWidth, - newHeight, - get(cropAspectRatio), - canvasW - x, - canvasH - y, - ); - cropSettings.update((crop) => { - crop.width = w; - crop.height = h; - return crop; - }); - } + const { newWidth: w, newHeight: h } = adjustDimensions( + newWidth, + newHeight, + get(cropAspectRatio), + canvas.clientWidth - x, + canvas.clientHeight - y, + ); + cropSettings.update((crop) => { + crop.width = Math.max(minSize, w); + crop.height = Math.max(minSize, h); + return crop; + }); break; } } cropSettings.update((crop) => { - crop.x = Math.max(0, Math.min(crop.x, canvasW - crop.width)); - crop.y = Math.max(0, Math.min(crop.y, canvasH - crop.height)); + crop.x = Math.max(0, Math.min(crop.x, canvas.clientWidth - crop.width)); + crop.y = Math.max(0, Math.min(crop.y, canvas.clientHeight - crop.height)); return crop; }); - draw(canvas, crop); + draw(crop); } function updateCursor(mouseX: number, mouseY: number) { - const canvas = get(canvasElement); + const canvas = get(cropAreaEl); if (!canvas) { return; } @@ -466,26 +462,16 @@ export function checkEdits() { } function fadeOverlay(toDark: boolean) { - const step = toDark ? 0.05 : -0.05; - const minDarkness = 0.4; - const maxDarkness = 0.65; + const overlay = get(overlayEl); + const cropFrame = document.querySelector('.crop-frame'); - isResizingOrDragging.set(!toDark); - - const animate = () => { - darkenLevel.update((level) => { - const newLevel = Math.min(maxDarkness, Math.max(minDarkness, level + step)); - draw(get(canvasElement), get(cropSettings)); - return newLevel; - }); - - if ((toDark && get(darkenLevel) < maxDarkness) || (!toDark && get(darkenLevel) > minDarkness)) { - animationFrame.set(requestAnimationFrame(animate)); - } else { - cancelAnimationFrame(get(animationFrame) as number); - } - }; + if (toDark) { + overlay?.classList.remove('light'); + cropFrame?.classList.remove('resizing'); + } else { + overlay?.classList.add('light'); + cropFrame?.classList.add('resizing'); + } - cancelAnimationFrame(get(animationFrame) as number); - animationFrame.set(requestAnimationFrame(animate)); + isResizingOrDragging.set(!toDark); } From 5028a9650412d3d7978fe74226e0c692acac1966 Mon Sep 17 00:00:00 2001 From: ilyaChuk <86570508+ilyaChuk@users.noreply.github.com> Date: Fri, 9 Aug 2024 22:52:56 +0300 Subject: [PATCH 37/47] fix changes detections --- .../components/asset-viewer/editor/crop-tool/crop-tool.svelte | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-tool.svelte b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-tool.svelte index 63b023dad97b7..e081306e12cd9 100644 --- a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-tool.svelte +++ b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-tool.svelte @@ -6,6 +6,7 @@ cropImageScale, cropImageSize, cropSettings, + cropSettingsChanged, type CropAspectRatio, } from '$lib/stores/asset-editor.store'; import { mdiBackupRestore, mdiCropFree, mdiSquareOutline } from '@mdi/js'; @@ -97,6 +98,7 @@ height: cropImageSizeM[1] * cropImageScaleM - 1, }; $cropAspectRatio = selectedSize; + $cropSettingsChanged = false; return; } selectedSize = size; From 6678521947c4b0f944625cd7d580b0fe90233e88 Mon Sep 17 00:00:00 2001 From: ilyaChuk <86570508+ilyaChuk@users.noreply.github.com> Date: Sat, 10 Aug 2024 05:08:05 +0300 Subject: [PATCH 38/47] rotation --- .../editor/crop-tool/crop-area.svelte | 4 +- .../editor/crop-tool/crop-tool.svelte | 50 +++++++++++++-- .../editor/crop-tool/mouse-handlers.ts | 63 +++++++++++++++++-- web/src/lib/i18n/en.json | 4 ++ web/src/lib/i18n/ru.json | 4 ++ web/src/lib/stores/asset-editor.store.ts | 7 +++ 6 files changed, 122 insertions(+), 10 deletions(-) diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-area.svelte b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-area.svelte index 58af4e108a2e2..27137ca9cc8e3 100644 --- a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-area.svelte +++ b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-area.svelte @@ -10,7 +10,7 @@ import { onImageLoad, resizeCanvas } from './image-loading'; import { handleMouseDown, handleMouseMove, handleMouseUp } from './mouse-handlers'; import { recalculateCrop, animateCropChange } from './crop-settings'; - import { cropAspectRatio, cropSettings, resetGlobalCropStore } from '$lib/stores/asset-editor.store'; + import { cropAspectRatio, cropSettings, resetGlobalCropStore, rotateDegrees } from '$lib/stores/asset-editor.store'; export let asset; let img: HTMLImageElement; @@ -56,6 +56,7 @@
{/each} +
+

{$t('editor_crop_tool_h2_rotation').toUpperCase()}

+
+
    +
  • rotate(false)} icon={mdiRotateLeft} />
  • +
  • rotate(true)} icon={mdiRotateRight} />
  • +
diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/mouse-handlers.ts b/web/src/lib/components/asset-viewer/editor/crop-tool/mouse-handlers.ts index efc3d286982da..48eeb54d0922f 100644 --- a/web/src/lib/components/asset-viewer/editor/crop-tool/mouse-handlers.ts +++ b/web/src/lib/components/asset-viewer/editor/crop-tool/mouse-handlers.ts @@ -4,6 +4,8 @@ import { cropImageSize, cropSettings, cropSettingsChanged, + normaizedRorateDegrees, + rotateDegrees, showCancelConfirmDialog, type CropSettings, } from '$lib/stores/asset-editor.store'; @@ -87,9 +89,21 @@ function getMousePosition(e: MouseEvent) { let offsetX = e.clientX; let offsetY = e.clientY; const clienRect = getBoundingClientRectCached(get(cropAreaEl)); - - offsetX -= clienRect?.left ?? 0; - offsetY -= clienRect?.top ?? 0; + const rotateDeg = get(normaizedRorateDegrees); + + if (rotateDeg == 90) { + offsetX = e.clientY - (clienRect?.top ?? 0); + offsetY = window.innerWidth - e.clientX - (window.innerWidth - (clienRect?.right ?? 0)); + } else if (rotateDeg == 180) { + offsetX = window.innerWidth - e.clientX - (window.innerWidth - (clienRect?.right ?? 0)); + offsetY = window.innerHeight - e.clientY - (window.innerHeight - (clienRect?.bottom ?? 0)); + } else if (rotateDeg == 270) { + offsetX = window.innerHeight - e.clientY - (window.innerHeight - (clienRect?.bottom ?? 0)); + offsetY = e.clientX - (clienRect?.left ?? 0); + } else if (rotateDeg == 0) { + offsetX -= clienRect?.left ?? 0; + offsetY -= clienRect?.top ?? 0; + } return { mouseX: offsetX, mouseY: offsetY }; } @@ -98,7 +112,9 @@ let getBoundingClientRectCache: { data: BoundingClientRect | null; time: number data: null, time: 0, }; - +rotateDegrees.subscribe(() => { + getBoundingClientRectCache.time = 0; +}); function getBoundingClientRectCached(el: HTMLElement | null) { if (Date.now() - getBoundingClientRectCache.time > 5000 || getBoundingClientRectCache.data === null) { getBoundingClientRectCache = { @@ -407,7 +423,9 @@ function updateCursor(mouseX: number, mouseY: number) { } const crop = get(cropSettings); - const { + const rotateDeg = get(normaizedRorateDegrees); + + let { onLeftBoundary, onRightBoundary, onTopBoundary, @@ -418,6 +436,41 @@ function updateCursor(mouseX: number, mouseY: number) { onBottomRightCorner, } = isOnCropBoundary(mouseX, mouseY, crop); + if (rotateDeg == 90) { + [onTopBoundary, onRightBoundary, onBottomBoundary, onLeftBoundary] = [ + onLeftBoundary, + onTopBoundary, + onRightBoundary, + onBottomBoundary, + ]; + + [onTopLeftCorner, onTopRightCorner, onBottomRightCorner, onBottomLeftCorner] = [ + onBottomLeftCorner, + onTopLeftCorner, + onTopRightCorner, + onBottomRightCorner, + ]; + } else if (rotateDeg == 180) { + [onTopBoundary, onBottomBoundary] = [onBottomBoundary, onTopBoundary]; + [onLeftBoundary, onRightBoundary] = [onRightBoundary, onLeftBoundary]; + + [onTopLeftCorner, onBottomRightCorner] = [onBottomRightCorner, onTopLeftCorner]; + [onTopRightCorner, onBottomLeftCorner] = [onBottomLeftCorner, onTopRightCorner]; + } else if (rotateDeg == 270) { + [onTopBoundary, onRightBoundary, onBottomBoundary, onLeftBoundary] = [ + onRightBoundary, + onBottomBoundary, + onLeftBoundary, + onTopBoundary, + ]; + + [onTopLeftCorner, onTopRightCorner, onBottomRightCorner, onBottomLeftCorner] = [ + onTopRightCorner, + onBottomRightCorner, + onBottomLeftCorner, + onTopLeftCorner, + ]; + } if (onTopLeftCorner || onBottomRightCorner) { setCursor('nwse-resize'); } else if (onTopRightCorner || onBottomLeftCorner) { diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index 3d92dd07e1e94..e3a83dc5c3dd1 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -357,6 +357,7 @@ "allow_edits": "Allow edits", "allow_public_user_to_download": "Allow public user to download", "allow_public_user_to_upload": "Allow public user to upload", + "anti_clockwise": "Anti-clockwise", "api_key": "API Key", "api_key_description": "This value will only be shown once. Please be sure to copy it before closing the window.", "api_key_empty": "Your API Key name shouldn't be empty", @@ -432,6 +433,7 @@ "clear_all_recent_searches": "Clear all recent searches", "clear_message": "Clear message", "clear_value": "Clear value", + "clockwise": "Сlockwise", "close": "Close", "collapse": "Collapse", "collapse_all": "Collapse all", @@ -535,6 +537,8 @@ "edited": "Edited", "editor_close_without_save_prompt": "The changes will not be saved", "editor_close_without_save_title": "Close editor?", + "editor_crop_tool_h2_aspect_ratios": "Aspect ratios", + "editor_crop_tool_h2_rotation": "Rotation", "email": "Email", "empty_trash": "Empty trash", "empty_trash_confirmation": "Are you sure you want to empty the trash? This will remove all the assets in trash permanently from Immich.\nYou cannot undo this action!", diff --git a/web/src/lib/i18n/ru.json b/web/src/lib/i18n/ru.json index a6e093273353a..417b5f614ab4e 100644 --- a/web/src/lib/i18n/ru.json +++ b/web/src/lib/i18n/ru.json @@ -360,6 +360,7 @@ "allow_edits": "Разрешить редактирование", "allow_public_user_to_download": "Разрешить скачивание публичным пользователям", "allow_public_user_to_upload": "Разрешить публичным пользователям загружать файлы", + "anti_clockwise": "Против часовой", "api_key": "API Ключ", "api_key_description": "Это значение будет показано только один раз. Пожалуйста, убедитесь, что скопировали его перед закрытием окна.", "api_key_empty": "Ваш API ключ не должен быть пустым", @@ -441,6 +442,7 @@ "clear_all_recent_searches": "Очистить все недавние результаты поиска", "clear_message": "Очистить сообщение", "clear_value": "Очистить значение", + "clockwise": "По часовой", "close": "Закрыть", "collapse": "Свернуть", "collapse_all": "Свернуть всё", @@ -552,6 +554,8 @@ "editor": "Редактор", "editor_close_without_save_prompt": "Изменения не будут сохранены", "editor_close_without_save_title": "Закрыть редактор?", + "editor_crop_tool_h2_aspect_ratios": "Соотношения сторон", + "editor_crop_tool_h2_rotation": "Вращение", "email": "Электронная почта", "empty": "", "empty_album": "Пустой альбом", diff --git a/web/src/lib/stores/asset-editor.store.ts b/web/src/lib/stores/asset-editor.store.ts index 2631e2e315fd2..bcf41e745532c 100644 --- a/web/src/lib/stores/asset-editor.store.ts +++ b/web/src/lib/stores/asset-editor.store.ts @@ -8,6 +8,12 @@ export const cropImageScale = writable(1); export const cropAspectRatio = writable('free'); export const cropSettingsChanged = writable(false); +export const rotateDegrees = writable(0); +export const normaizedRorateDegrees = derived(rotateDegrees, (v) => { + const newAngle = v % 360; + return newAngle < 0 ? newAngle + 360 : newAngle; +}); + export const showCancelConfirmDialog = writable(false); export const editTypes = [ @@ -41,6 +47,7 @@ export function resetGlobalCropStore() { cropAspectRatio.set('free'); cropSettingsChanged.set(false); showCancelConfirmDialog.set(false); + rotateDegrees.set(0); } export type CropAspectRatio = From 1ed064705f6ad0d1e17cf1d2260040992d7e4b5f Mon Sep 17 00:00:00 2001 From: ilyaChuk <86570508+ilyaChuk@users.noreply.github.com> Date: Sat, 10 Aug 2024 05:09:30 +0300 Subject: [PATCH 39/47] hide detail panel if showing editor --- web/src/lib/components/asset-viewer/asset-viewer.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index d46dd6ee8a02e..24586f53cd131 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -538,7 +538,7 @@ {/if} - {#if enableDetailPanel && $slideshowState === SlideshowState.None && $isShowDetail} + {#if enableDetailPanel && $slideshowState === SlideshowState.None && $isShowDetail && !isShowEditor}
Date: Sat, 10 Aug 2024 05:23:24 +0300 Subject: [PATCH 40/47] fix aspect ratios near min size --- .../editor/crop-tool/crop-settings.ts | 29 ++++++++++++++++-- .../editor/crop-tool/mouse-handlers.ts | 30 +++++++++++-------- 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-settings.ts b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-settings.ts index 415ef83cc5a69..a0390d2d4d47e 100644 --- a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-settings.ts +++ b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-settings.ts @@ -102,11 +102,12 @@ export function adjustDimensions( aspectRatio: CropAspectRatio, xLimit: number, yLimit: number, + minSize: number, ) { - let w = newWidth, - h = newHeight; + let w = newWidth; + let h = newHeight; - let aspectMultiplier; + let aspectMultiplier: number; if (aspectRatio === 'free') { aspectMultiplier = newWidth / newHeight; @@ -132,5 +133,27 @@ export function adjustDimensions( } } + 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 }; } diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/mouse-handlers.ts b/web/src/lib/components/asset-viewer/editor/crop-tool/mouse-handlers.ts index 48eeb54d0922f..656fd09294abb 100644 --- a/web/src/lib/components/asset-viewer/editor/crop-tool/mouse-handlers.ts +++ b/web/src/lib/components/asset-viewer/editor/crop-tool/mouse-handlers.ts @@ -304,11 +304,12 @@ function resizeCrop(mouseX: number, mouseY: number) { get(cropAspectRatio), canvas.clientWidth, canvas.clientHeight, + minSize, ); cropSettings.update((crop) => { crop.y = Math.max(0, y + height - h); - crop.width = Math.max(minSize, w); - crop.height = Math.max(minSize, h); + crop.width = w; + crop.height = h; return crop; }); } @@ -324,10 +325,11 @@ function resizeCrop(mouseX: number, mouseY: number) { get(cropAspectRatio), canvas.clientWidth, canvas.clientHeight - y, + minSize, ); cropSettings.update((crop) => { - crop.width = Math.max(minSize, w); - crop.height = Math.max(minSize, h); + crop.width = w; + crop.height = h; return crop; }); } @@ -342,10 +344,11 @@ function resizeCrop(mouseX: number, mouseY: number) { get(cropAspectRatio), canvas.clientWidth, canvas.clientHeight, + minSize, ); cropSettings.update((crop) => { - crop.width = Math.max(minSize, w); - crop.height = Math.max(minSize, h); + crop.width = w; + crop.height = h; crop.x = Math.max(0, x + width - crop.width); crop.y = Math.max(0, y + height - crop.height); return crop; @@ -361,10 +364,11 @@ function resizeCrop(mouseX: number, mouseY: number) { get(cropAspectRatio), canvas.clientWidth - x, y + height, + minSize, ); cropSettings.update((crop) => { - crop.width = Math.max(minSize, w); - crop.height = Math.max(minSize, h); + crop.width = w; + crop.height = h; crop.y = Math.max(0, y + height - crop.height); return crop; }); @@ -379,10 +383,11 @@ function resizeCrop(mouseX: number, mouseY: number) { get(cropAspectRatio), canvas.clientWidth, canvas.clientHeight - y, + minSize, ); cropSettings.update((crop) => { - crop.width = Math.max(minSize, w); - crop.height = Math.max(minSize, h); + crop.width = w; + crop.height = h; crop.x = Math.max(0, x + width - crop.width); return crop; }); @@ -397,10 +402,11 @@ function resizeCrop(mouseX: number, mouseY: number) { get(cropAspectRatio), canvas.clientWidth - x, canvas.clientHeight - y, + minSize, ); cropSettings.update((crop) => { - crop.width = Math.max(minSize, w); - crop.height = Math.max(minSize, h); + crop.width = w; + crop.height = h; return crop; }); break; From 73ab707e25409d8245d26df75955c819f0fb22eb Mon Sep 17 00:00:00 2001 From: ilyaChuk <86570508+ilyaChuk@users.noreply.github.com> Date: Sat, 10 Aug 2024 23:14:54 +0300 Subject: [PATCH 41/47] fix crop area when changing image size when rotate --- .../editor/crop-tool/crop-area.svelte | 24 +++++-- .../editor/crop-tool/crop-tool.svelte | 7 +- .../editor/crop-tool/image-loading.ts | 71 ++++++++++++++----- web/src/lib/stores/asset-editor.store.ts | 6 +- 4 files changed, 83 insertions(+), 25 deletions(-) diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-area.svelte b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-area.svelte index 27137ca9cc8e3..baf5122bd4a1a 100644 --- a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-area.svelte +++ b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-area.svelte @@ -10,7 +10,13 @@ import { onImageLoad, resizeCanvas } from './image-loading'; import { handleMouseDown, handleMouseMove, handleMouseUp } from './mouse-handlers'; import { recalculateCrop, animateCropChange } from './crop-settings'; - import { cropAspectRatio, cropSettings, resetGlobalCropStore, rotateDegrees } from '$lib/stores/asset-editor.store'; + import { + changedOriention, + cropAspectRatio, + cropSettings, + resetGlobalCropStore, + rotateDegrees, + } from '$lib/stores/asset-editor.store'; export let asset; let img: HTMLImageElement; @@ -34,7 +40,7 @@ img.src = getAssetOriginalUrl({ id: asset.id, checksum: asset.checksum }); - img.addEventListener('load', onImageLoad); + img.addEventListener('load', () => onImageLoad(false)); img.addEventListener('error', (error) => { handleError(error, $t('error_loading_image')); }); @@ -55,7 +61,7 @@
+ diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-tool.svelte b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-tool.svelte index 27fb963d897b8..dba3be5d671ff 100644 --- a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-tool.svelte +++ b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-tool.svelte @@ -1,7 +1,5 @@