mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
refactor(web): crop area tool (#26843)
This commit is contained in:
@@ -1007,6 +1007,8 @@
|
|||||||
"editor_edits_applied_success": "Edits applied successfully",
|
"editor_edits_applied_success": "Edits applied successfully",
|
||||||
"editor_flip_horizontal": "Flip horizontal",
|
"editor_flip_horizontal": "Flip horizontal",
|
||||||
"editor_flip_vertical": "Flip vertical",
|
"editor_flip_vertical": "Flip vertical",
|
||||||
|
"editor_handle_corner": "{corner, select, top_left {Top-left} top_right {Top-right} bottom_left {Bottom-left} bottom_right {Bottom-right} other {A}} corner handle",
|
||||||
|
"editor_handle_edge": "{edge, select, top {Top} bottom {Bottom} left {Left} right {Right} other {An}} edge handle",
|
||||||
"editor_orientation": "Orientation",
|
"editor_orientation": "Orientation",
|
||||||
"editor_reset_all_changes": "Reset changes",
|
"editor_reset_all_changes": "Reset changes",
|
||||||
"editor_rotate_left": "Rotate 90° counterclockwise",
|
"editor_rotate_left": "Rotate 90° counterclockwise",
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
import { getResizeObserverMock } from '$lib/__mocks__/resize-observer.mock';
|
|
||||||
import CropArea from '$lib/components/asset-viewer/editor/transform-tool/crop-area.svelte';
|
|
||||||
import { transformManager } from '$lib/managers/edit/transform-manager.svelte';
|
|
||||||
import { getAssetMediaUrl } from '$lib/utils';
|
|
||||||
import { assetFactory } from '@test-data/factories/asset-factory';
|
|
||||||
import { render } from '@testing-library/svelte';
|
|
||||||
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
|
|
||||||
|
|
||||||
vi.mock('$lib/utils');
|
|
||||||
|
|
||||||
describe('CropArea', () => {
|
|
||||||
beforeAll(() => {
|
|
||||||
vi.stubGlobal('ResizeObserver', getResizeObserverMock());
|
|
||||||
vi.mocked(getAssetMediaUrl).mockReturnValue('/mock-image.jpg');
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
transformManager.reset();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('clears cursor styles on reset', () => {
|
|
||||||
const asset = assetFactory.build();
|
|
||||||
const { getByRole } = render(CropArea, { asset });
|
|
||||||
const cropArea = getByRole('button', { name: 'Crop area' });
|
|
||||||
|
|
||||||
transformManager.region = { x: 100, y: 100, width: 200, height: 200 };
|
|
||||||
transformManager.cropImageSize = { width: 1000, height: 1000 };
|
|
||||||
transformManager.cropImageScale = 1;
|
|
||||||
transformManager.updateCursor(100, 150);
|
|
||||||
|
|
||||||
expect(document.body.style.cursor).toBe('ew-resize');
|
|
||||||
expect(cropArea.style.cursor).toBe('ew-resize');
|
|
||||||
|
|
||||||
transformManager.reset();
|
|
||||||
|
|
||||||
expect(document.body.style.cursor).toBe('');
|
|
||||||
expect(cropArea.style.cursor).toBe('');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sets cursor style at x: $x, y: $y to be $cursor', () => {
|
|
||||||
const data = [
|
|
||||||
{ x: 299, y: 84, cursor: '' },
|
|
||||||
{ x: 299, y: 85, cursor: 'nesw-resize' },
|
|
||||||
{ x: 299, y: 115, cursor: 'nesw-resize' },
|
|
||||||
{ x: 299, y: 116, cursor: 'ew-resize' },
|
|
||||||
{ x: 299, y: 284, cursor: 'ew-resize' },
|
|
||||||
{ x: 299, y: 285, cursor: 'nwse-resize' },
|
|
||||||
{ x: 299, y: 300, cursor: 'nwse-resize' },
|
|
||||||
{ x: 299, y: 301, cursor: '' },
|
|
||||||
{ x: 300, y: 84, cursor: '' },
|
|
||||||
{ x: 300, y: 85, cursor: 'nesw-resize' },
|
|
||||||
{ x: 300, y: 86, cursor: 'nesw-resize' },
|
|
||||||
{ x: 300, y: 114, cursor: 'nesw-resize' },
|
|
||||||
{ x: 300, y: 115, cursor: 'nesw-resize' },
|
|
||||||
{ x: 300, y: 116, cursor: 'ew-resize' },
|
|
||||||
{ x: 300, y: 284, cursor: 'ew-resize' },
|
|
||||||
{ x: 300, y: 285, cursor: 'nwse-resize' },
|
|
||||||
{ x: 300, y: 286, cursor: 'nwse-resize' },
|
|
||||||
{ x: 300, y: 300, cursor: 'nwse-resize' },
|
|
||||||
{ x: 300, y: 301, cursor: '' },
|
|
||||||
{ x: 301, y: 300, cursor: '' },
|
|
||||||
{ x: 301, y: 301, cursor: '' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const element = document.createElement('div');
|
|
||||||
|
|
||||||
for (const { x, y, cursor } of data) {
|
|
||||||
const message = `x: ${x}, y: ${y} - ${cursor}`;
|
|
||||||
transformManager.reset();
|
|
||||||
transformManager.region = { x: 100, y: 100, width: 200, height: 200 };
|
|
||||||
transformManager.cropImageSize = { width: 600, height: 600 };
|
|
||||||
transformManager.cropAreaEl = element;
|
|
||||||
transformManager.cropImageScale = 0.5;
|
|
||||||
transformManager.updateCursor(x, y);
|
|
||||||
expect(element.style.cursor, message).toBe(cursor);
|
|
||||||
expect(document.body.style.cursor, message).toBe(cursor);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { transformManager } from '$lib/managers/edit/transform-manager.svelte';
|
import { ResizeBoundary, transformManager } from '$lib/managers/edit/transform-manager.svelte';
|
||||||
import { getAssetMediaUrl } from '$lib/utils';
|
import { getAssetMediaUrl } from '$lib/utils';
|
||||||
import { getAltText } from '$lib/utils/thumbnail-util';
|
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import { AssetMediaSize, type AssetResponseDto } from '@immich/sdk';
|
import { AssetMediaSize, type AssetResponseDto } from '@immich/sdk';
|
||||||
|
import { Icon } from '@immich/ui';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
asset: AssetResponseDto;
|
asset: AssetResponseDto;
|
||||||
@@ -11,6 +14,9 @@
|
|||||||
|
|
||||||
let { asset }: Props = $props();
|
let { asset }: Props = $props();
|
||||||
|
|
||||||
|
// viewBox 0 0 24 24 is assumed. Without rotation this icon is top-left.
|
||||||
|
const cornerIcon = 'M 12 24 L 12 12 L 24 12';
|
||||||
|
|
||||||
let canvasContainer = $state<HTMLElement | null>(null);
|
let canvasContainer = $state<HTMLElement | null>(null);
|
||||||
|
|
||||||
let imageSrc = $derived(
|
let imageSrc = $derived(
|
||||||
@@ -30,16 +36,23 @@
|
|||||||
return transforms.join(' ');
|
return transforms.join(' ');
|
||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
const edges = [ResizeBoundary.Top, ResizeBoundary.Right, ResizeBoundary.Bottom, ResizeBoundary.Left];
|
||||||
if (!canvasContainer) {
|
const corners = [
|
||||||
return;
|
ResizeBoundary.TopLeft,
|
||||||
|
ResizeBoundary.TopRight,
|
||||||
|
ResizeBoundary.BottomRight,
|
||||||
|
ResizeBoundary.BottomLeft,
|
||||||
|
];
|
||||||
|
function rotateBoundary(arr: ResizeBoundary[], input: ResizeBoundary, times: number) {
|
||||||
|
return arr[(arr.indexOf(input) + times) % 4];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
transformManager.resizeCanvas();
|
transformManager.resizeCanvas();
|
||||||
});
|
});
|
||||||
|
|
||||||
resizeObserver.observe(canvasContainer);
|
resizeObserver.observe(canvasContainer!);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
resizeObserver.disconnect();
|
resizeObserver.disconnect();
|
||||||
@@ -47,153 +60,144 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="canvas-container" bind:this={canvasContainer}>
|
<div class="flex flex-col items-center justify-center w-full h-full p-8" bind:this={canvasContainer}>
|
||||||
<button
|
<div
|
||||||
class={`crop-area ${transformManager.orientationChanged ? 'changedOriention' : ''}`}
|
class="crop-area max-w-full max-h-full transition-transform motion-reduce:transition-none"
|
||||||
style={`rotate:${transformManager.imageRotation}deg`}
|
class:rotated={transformManager.normalizedRotation % 180 > 0}
|
||||||
|
style:rotate={transformManager.imageRotation + 'deg'}
|
||||||
bind:this={transformManager.cropAreaEl}
|
bind:this={transformManager.cropAreaEl}
|
||||||
onmousedown={(e) => transformManager.handleMouseDown(e)}
|
|
||||||
onmouseup={() => transformManager.handleMouseUp()}
|
|
||||||
aria-label="Crop area"
|
aria-label="Crop area"
|
||||||
type="button"
|
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
draggable="false"
|
draggable="false"
|
||||||
src={imageSrc}
|
src={imageSrc}
|
||||||
alt={$getAltText(toTimelineAsset(asset))}
|
alt={$getAltText(toTimelineAsset(asset))}
|
||||||
style={imageTransform ? `transform: ${imageTransform}` : ''}
|
class="h-full select-none transition-transform motion-reduce:transition-none"
|
||||||
|
style:transform={imageTransform}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class={`${transformManager.isInteracting ? 'resizing' : ''} crop-frame`}
|
class={[
|
||||||
bind:this={transformManager.cropFrame}
|
'overlay w-full h-full absolute top-0 transition-colors motion-reduce:transition-none pointer-events-none',
|
||||||
>
|
transformManager.isInteracting ? 'bg-black/30' : 'bg-black/56',
|
||||||
<div class="grid"></div>
|
]}
|
||||||
<div class="corner top-left"></div>
|
|
||||||
<div class="corner top-right"></div>
|
|
||||||
<div class="corner bottom-left"></div>
|
|
||||||
<div class="corner bottom-right"></div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class={`${transformManager.isInteracting ? 'light' : ''} overlay`}
|
|
||||||
bind:this={transformManager.overlayEl}
|
bind:this={transformManager.overlayEl}
|
||||||
></div>
|
></div>
|
||||||
|
<div class="crop-frame absolute border-2 border-white" bind:this={transformManager.cropFrame}>
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div
|
||||||
|
class={[
|
||||||
|
'grid w-full h-full cursor-move transition-opacity motion-reduce:transition-none',
|
||||||
|
transformManager.isInteracting ? 'opacity-100' : 'opacity-0',
|
||||||
|
]}
|
||||||
|
onmousedown={(e) => transformManager.handleMouseDownOn(e, ResizeBoundary.None)}
|
||||||
|
></div>
|
||||||
|
|
||||||
|
{#each edges as edge (edge)}
|
||||||
|
{@const rotatedEdge = rotateBoundary(edges, edge, transformManager.normalizedRotation / 90)}
|
||||||
|
<button
|
||||||
|
class={['absolute', edge]}
|
||||||
|
style={`${edge}: -10px`}
|
||||||
|
onmousedown={(e) => transformManager.handleMouseDownOn(e, edge)}
|
||||||
|
type="button"
|
||||||
|
aria-label={$t('editor_handle_edge', { values: { edge: rotatedEdge } })}
|
||||||
|
></button>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#each corners as corner (corner)}
|
||||||
|
{@const rotatedCorner = rotateBoundary(corners, corner, transformManager.normalizedRotation / 90)}
|
||||||
|
<button
|
||||||
|
class={['corner', corner]}
|
||||||
|
onmousedown={(e) => transformManager.handleMouseDownOn(e, corner)}
|
||||||
|
type="button"
|
||||||
|
aria-label={$t('editor_handle_corner', { values: { corner: rotatedCorner.replace('-', '_') } })}
|
||||||
|
>
|
||||||
|
<Icon icon={cornerIcon} size="30" strokeWidth={4} strokeColor="white" color="transparent" />
|
||||||
</button>
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.canvas-container {
|
|
||||||
width: calc(100% - 4rem);
|
|
||||||
margin: auto;
|
|
||||||
margin-top: 2rem;
|
|
||||||
height: calc(100% - 4rem);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.crop-area {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
outline: none;
|
|
||||||
transition: rotate 0.15s ease;
|
|
||||||
max-height: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
width: max-content;
|
|
||||||
}
|
|
||||||
.crop-area.changedOriention {
|
|
||||||
max-width: 92vh;
|
|
||||||
max-height: calc(100vw - 400px - 1.5rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.crop-frame.transition {
|
.crop-frame.transition {
|
||||||
transition: all 0.15s ease;
|
transition: all 0.15s ease;
|
||||||
}
|
}
|
||||||
.overlay {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.56);
|
|
||||||
pointer-events: none;
|
|
||||||
transition: background 0.1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overlay.light {
|
|
||||||
background: rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid {
|
.grid {
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
--color: white;
|
--color: white;
|
||||||
--shadow: #00000057;
|
--shadow: #00000057;
|
||||||
background-image:
|
background-image:
|
||||||
linear-gradient(var(--color) 1px, transparent 0), linear-gradient(90deg, var(--color) 1px, transparent 0),
|
linear-gradient(var(--color) 1px, transparent 0), linear-gradient(90deg, var(--color) 1px, transparent 0),
|
||||||
linear-gradient(var(--shadow) 3px, transparent 0), linear-gradient(90deg, var(--shadow) 3px, transparent 0);
|
linear-gradient(var(--shadow) 3px, transparent 0), linear-gradient(90deg, var(--shadow) 3px, transparent 0);
|
||||||
background-size: calc(100% / 3) calc(100% / 3);
|
background-size: calc(100% / 3) calc(100% / 3);
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.1s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.crop-frame.resizing .grid {
|
.left,
|
||||||
opacity: 1;
|
.right {
|
||||||
}
|
top: 0;
|
||||||
|
width: 20px;
|
||||||
.crop-area img {
|
|
||||||
display: block;
|
|
||||||
max-width: 100%;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
user-select: none;
|
cursor: ew-resize;
|
||||||
transition: transform 0.15s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.crop-frame {
|
.top,
|
||||||
position: absolute;
|
.bottom {
|
||||||
border: 2px solid white;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
height: 20px;
|
||||||
pointer-events: none;
|
cursor: ns-resize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.corner {
|
.corner {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 20px;
|
--offset: -15px;
|
||||||
height: 20px;
|
|
||||||
--size: 5.2px;
|
|
||||||
--mSize: calc(-0.5 * var(--size));
|
|
||||||
border: var(--size) solid white;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-left {
|
.top-left {
|
||||||
top: var(--mSize);
|
top: var(--offset);
|
||||||
left: var(--mSize);
|
left: var(--offset);
|
||||||
border-right: none;
|
cursor: nwse-resize;
|
||||||
border-bottom: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-right {
|
.top-right {
|
||||||
top: var(--mSize);
|
top: var(--offset);
|
||||||
right: var(--mSize);
|
right: var(--offset);
|
||||||
border-left: none;
|
cursor: nesw-resize;
|
||||||
border-bottom: none;
|
rotate: 90deg;
|
||||||
}
|
|
||||||
|
|
||||||
.bottom-left {
|
|
||||||
bottom: var(--mSize);
|
|
||||||
left: var(--mSize);
|
|
||||||
border-right: none;
|
|
||||||
border-top: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-right {
|
.bottom-right {
|
||||||
bottom: var(--mSize);
|
bottom: var(--offset);
|
||||||
right: var(--mSize);
|
right: var(--offset);
|
||||||
border-left: none;
|
cursor: nwse-resize;
|
||||||
border-top: none;
|
rotate: 180deg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-left {
|
||||||
|
bottom: var(--offset);
|
||||||
|
left: var(--offset);
|
||||||
|
cursor: nesw-resize;
|
||||||
|
rotate: 270deg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crop-area.rotated {
|
||||||
|
max-width: calc(100vh - 16 * var(--spacing));
|
||||||
|
max-height: calc(100vw - 400px - 16 * var(--spacing));
|
||||||
|
|
||||||
|
.left,
|
||||||
|
.right {
|
||||||
|
cursor: ns-resize;
|
||||||
|
}
|
||||||
|
.top,
|
||||||
|
.bottom {
|
||||||
|
cursor: ew-resize;
|
||||||
|
}
|
||||||
|
.top-left,
|
||||||
|
.bottom-right {
|
||||||
|
cursor: nesw-resize;
|
||||||
|
}
|
||||||
|
.top-right,
|
||||||
|
.bottom-left {
|
||||||
|
cursor: nwse-resize;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { editManager, type EditActions, type EditToolManager } from '$lib/managers/edit/edit-manager.svelte';
|
import { type EditActions, type EditToolManager } from '$lib/managers/edit/edit-manager.svelte';
|
||||||
import { getAssetMediaUrl } from '$lib/utils';
|
import { getAssetMediaUrl } from '$lib/utils';
|
||||||
import { getDimensions } from '$lib/utils/asset-utils';
|
import { getDimensions } from '$lib/utils/asset-utils';
|
||||||
import { normalizeTransformEdits } from '$lib/utils/editor';
|
import { normalizeTransformEdits } from '$lib/utils/editor';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { AssetEditAction, AssetMediaSize, MirrorAxis, type AssetResponseDto, type CropParameters } from '@immich/sdk';
|
import { AssetEditAction, AssetMediaSize, MirrorAxis, type AssetResponseDto, type CropParameters } from '@immich/sdk';
|
||||||
|
import { clamp } from 'lodash-es';
|
||||||
import { tick } from 'svelte';
|
import { tick } from 'svelte';
|
||||||
|
|
||||||
export type CropAspectRatio =
|
export type CropAspectRatio =
|
||||||
@@ -37,17 +38,27 @@ type RegionConvertParams = {
|
|||||||
to: ImageDimensions;
|
to: ImageDimensions;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum ResizeBoundary {
|
||||||
|
None = 'none',
|
||||||
|
TopLeft = 'top-left',
|
||||||
|
TopRight = 'top-right',
|
||||||
|
BottomLeft = 'bottom-left',
|
||||||
|
BottomRight = 'bottom-right',
|
||||||
|
Left = 'left',
|
||||||
|
Right = 'right',
|
||||||
|
Top = 'top',
|
||||||
|
Bottom = 'bottom',
|
||||||
|
}
|
||||||
|
|
||||||
class TransformManager implements EditToolManager {
|
class TransformManager implements EditToolManager {
|
||||||
canReset: boolean = $derived.by(() => this.checkEdits());
|
canReset: boolean = $derived.by(() => this.checkEdits());
|
||||||
hasChanges: boolean = $state(false);
|
hasChanges: boolean = $state(false);
|
||||||
|
|
||||||
darkenLevel = $state(0.65);
|
|
||||||
isInteracting = $state(false);
|
isInteracting = $state(false);
|
||||||
isDragging = $state(false);
|
isDragging = $state(false);
|
||||||
animationFrame = $state<ReturnType<typeof requestAnimationFrame> | null>(null);
|
animationFrame = $state<ReturnType<typeof requestAnimationFrame> | null>(null);
|
||||||
canvasCursor = $state('default');
|
dragAnchor = $state({ x: 0, y: 0 });
|
||||||
dragOffset = $state({ x: 0, y: 0 });
|
resizeSide = $state(ResizeBoundary.None);
|
||||||
resizeSide = $state('');
|
|
||||||
imgElement = $state<HTMLImageElement | null>(null);
|
imgElement = $state<HTMLImageElement | null>(null);
|
||||||
cropAreaEl = $state<HTMLElement | null>(null);
|
cropAreaEl = $state<HTMLElement | null>(null);
|
||||||
overlayEl = $state<HTMLElement | null>(null);
|
overlayEl = $state<HTMLElement | null>(null);
|
||||||
@@ -69,7 +80,6 @@ class TransformManager implements EditToolManager {
|
|||||||
const newAngle = this.imageRotation % 360;
|
const newAngle = this.imageRotation % 360;
|
||||||
return newAngle < 0 ? newAngle + 360 : newAngle;
|
return newAngle < 0 ? newAngle + 360 : newAngle;
|
||||||
});
|
});
|
||||||
orientationChanged = $derived.by(() => this.normalizedRotation % 180 > 0);
|
|
||||||
|
|
||||||
edits = $derived.by(() => this.getEdits());
|
edits = $derived.by(() => this.getEdits());
|
||||||
|
|
||||||
@@ -81,9 +91,9 @@ class TransformManager implements EditToolManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newCrop = transformManager.recalculateCrop(aspectRatio);
|
const newCrop = this.recalculateCrop(aspectRatio);
|
||||||
if (newCrop) {
|
if (newCrop) {
|
||||||
transformManager.animateCropChange(this.cropAreaEl, this.region, newCrop);
|
this.animateCropChange(newCrop);
|
||||||
this.region = newCrop;
|
this.region = newCrop;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,17 +226,11 @@ class TransformManager implements EditToolManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.darkenLevel = 0.65;
|
|
||||||
this.isInteracting = false;
|
this.isInteracting = false;
|
||||||
this.animationFrame = null;
|
this.animationFrame = null;
|
||||||
this.canvasCursor = 'default';
|
this.dragAnchor = { x: 0, y: 0 };
|
||||||
this.dragOffset = { x: 0, y: 0 };
|
this.resizeSide = ResizeBoundary.None;
|
||||||
this.resizeSide = '';
|
|
||||||
this.imgElement = null;
|
this.imgElement = null;
|
||||||
if (this.cropAreaEl) {
|
|
||||||
this.cropAreaEl.style.cursor = '';
|
|
||||||
}
|
|
||||||
document.body.style.cursor = '';
|
|
||||||
this.cropAreaEl = null;
|
this.cropAreaEl = null;
|
||||||
this.isDragging = false;
|
this.isDragging = false;
|
||||||
this.overlayEl = null;
|
this.overlayEl = null;
|
||||||
@@ -295,12 +299,12 @@ class TransformManager implements EditToolManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
animateCropChange(element: HTMLElement, from: Region, to: Region, duration = 100) {
|
animateCropChange(to: Region, duration = 100) {
|
||||||
const cropFrame = element.querySelector('.crop-frame') as HTMLElement;
|
if (!this.cropFrame) {
|
||||||
if (!cropFrame) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const from = this.region;
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
const initialCrop = { ...from };
|
const initialCrop = { ...from };
|
||||||
|
|
||||||
@@ -334,28 +338,6 @@ class TransformManager implements EditToolManager {
|
|||||||
return { newWidth, newHeight };
|
return { newWidth, newHeight };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate constrained dimensions based on aspect ratio and limits
|
|
||||||
getConstrainedDimensions(
|
|
||||||
desiredWidth: number,
|
|
||||||
desiredHeight: number,
|
|
||||||
maxWidth: number,
|
|
||||||
maxHeight: number,
|
|
||||||
minSize = 50,
|
|
||||||
) {
|
|
||||||
const { newWidth, newHeight } = this.adjustDimensions(
|
|
||||||
desiredWidth,
|
|
||||||
desiredHeight,
|
|
||||||
this.cropAspectRatio,
|
|
||||||
maxWidth,
|
|
||||||
maxHeight,
|
|
||||||
minSize,
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
width: Math.max(minSize, Math.min(newWidth, maxWidth)),
|
|
||||||
height: Math.max(minSize, Math.min(newHeight, maxHeight)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
adjustDimensions(
|
adjustDimensions(
|
||||||
newWidth: number,
|
newWidth: number,
|
||||||
newHeight: number,
|
newHeight: number,
|
||||||
@@ -364,49 +346,45 @@ class TransformManager implements EditToolManager {
|
|||||||
yLimit: number,
|
yLimit: number,
|
||||||
minSize: number,
|
minSize: number,
|
||||||
) {
|
) {
|
||||||
|
if (aspectRatio === 'free') {
|
||||||
|
return {
|
||||||
|
newWidth: clamp(newWidth, minSize, xLimit),
|
||||||
|
newHeight: clamp(newHeight, minSize, yLimit),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let w = newWidth;
|
let w = newWidth;
|
||||||
let h = newHeight;
|
let h = newHeight;
|
||||||
|
|
||||||
let aspectMultiplier: number;
|
const [ratioWidth, ratioHeight] = aspectRatio.split(':').map(Number);
|
||||||
|
const aspectMultiplier = ratioWidth && ratioHeight ? ratioWidth / ratioHeight : w / h;
|
||||||
|
|
||||||
if (aspectRatio === 'free') {
|
|
||||||
aspectMultiplier = newWidth / newHeight;
|
|
||||||
} else {
|
|
||||||
const [widthRatio, heightRatio] = aspectRatio.split(':').map(Number);
|
|
||||||
aspectMultiplier = widthRatio && heightRatio ? widthRatio / heightRatio : newWidth / newHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (aspectRatio !== 'free') {
|
|
||||||
h = w / aspectMultiplier;
|
h = w / aspectMultiplier;
|
||||||
|
// When dragging a corner, use the biggest region that fits 'inside' the mouse location.
|
||||||
|
if (h < newHeight) {
|
||||||
|
h = newHeight;
|
||||||
|
w = h * aspectMultiplier;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (w > xLimit) {
|
if (w > xLimit) {
|
||||||
w = xLimit;
|
w = xLimit;
|
||||||
if (aspectRatio !== 'free') {
|
|
||||||
h = w / aspectMultiplier;
|
h = w / aspectMultiplier;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (h > yLimit) {
|
if (h > yLimit) {
|
||||||
h = yLimit;
|
h = yLimit;
|
||||||
if (aspectRatio !== 'free') {
|
|
||||||
w = h * aspectMultiplier;
|
w = h * aspectMultiplier;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (w < minSize) {
|
if (w < minSize) {
|
||||||
w = minSize;
|
w = minSize;
|
||||||
if (aspectRatio !== 'free') {
|
|
||||||
h = w / aspectMultiplier;
|
h = w / aspectMultiplier;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (h < minSize) {
|
if (h < minSize) {
|
||||||
h = minSize;
|
h = minSize;
|
||||||
if (aspectRatio !== 'free') {
|
|
||||||
w = h * aspectMultiplier;
|
w = h * aspectMultiplier;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (aspectRatio !== 'free' && w / h !== aspectMultiplier) {
|
if (w / h !== aspectMultiplier) {
|
||||||
if (w < minSize) {
|
if (w < minSize) {
|
||||||
h = w / aspectMultiplier;
|
h = w / aspectMultiplier;
|
||||||
}
|
}
|
||||||
@@ -428,10 +406,6 @@ class TransformManager implements EditToolManager {
|
|||||||
this.cropFrame.style.width = `${crop.width}px`;
|
this.cropFrame.style.width = `${crop.width}px`;
|
||||||
this.cropFrame.style.height = `${crop.height}px`;
|
this.cropFrame.style.height = `${crop.height}px`;
|
||||||
|
|
||||||
this.drawOverlay(crop);
|
|
||||||
}
|
|
||||||
|
|
||||||
drawOverlay(crop: Region) {
|
|
||||||
if (!this.overlayEl) {
|
if (!this.overlayEl) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -465,7 +439,6 @@ class TransformManager implements EditToolManager {
|
|||||||
const cropFrameEl = this.cropFrame;
|
const cropFrameEl = this.cropFrame;
|
||||||
cropFrameEl?.classList.add('transition');
|
cropFrameEl?.classList.add('transition');
|
||||||
this.region = this.normalizeCropArea(scale);
|
this.region = this.normalizeCropArea(scale);
|
||||||
cropFrameEl?.classList.add('transition');
|
|
||||||
cropFrameEl?.addEventListener('transitionend', () => cropFrameEl?.classList.remove('transition'), {
|
cropFrameEl?.addEventListener('transitionend', () => cropFrameEl?.classList.remove('transition'), {
|
||||||
passive: true,
|
passive: true,
|
||||||
});
|
});
|
||||||
@@ -540,7 +513,7 @@ class TransformManager implements EditToolManager {
|
|||||||
normalizeCropArea(scale: number) {
|
normalizeCropArea(scale: number) {
|
||||||
const img = this.imgElement;
|
const img = this.imgElement;
|
||||||
if (!img) {
|
if (!img) {
|
||||||
return { ...this.region };
|
return this.region;
|
||||||
}
|
}
|
||||||
|
|
||||||
const scaleRatio = scale / this.cropImageScale;
|
const scaleRatio = scale / this.cropImageScale;
|
||||||
@@ -576,38 +549,17 @@ class TransformManager implements EditToolManager {
|
|||||||
this.draw();
|
this.draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMouseDown(e: MouseEvent) {
|
handleMouseDownOn(e: MouseEvent, resizeBoundary: ResizeBoundary) {
|
||||||
const canvas = this.cropAreaEl;
|
if (e.button !== 0) {
|
||||||
if (!canvas) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.isInteracting = true;
|
||||||
|
this.resizeSide = resizeBoundary;
|
||||||
|
if (resizeBoundary === ResizeBoundary.None) {
|
||||||
|
this.isDragging = true;
|
||||||
const { mouseX, mouseY } = this.getMousePosition(e);
|
const { mouseX, mouseY } = this.getMousePosition(e);
|
||||||
|
this.dragAnchor = { x: mouseX - this.region.x, y: mouseY - this.region.y };
|
||||||
const {
|
|
||||||
onLeftBoundary,
|
|
||||||
onRightBoundary,
|
|
||||||
onTopBoundary,
|
|
||||||
onBottomBoundary,
|
|
||||||
onTopLeftCorner,
|
|
||||||
onTopRightCorner,
|
|
||||||
onBottomLeftCorner,
|
|
||||||
onBottomRightCorner,
|
|
||||||
} = this.isOnCropBoundary(mouseX, mouseY);
|
|
||||||
|
|
||||||
if (
|
|
||||||
onTopLeftCorner ||
|
|
||||||
onTopRightCorner ||
|
|
||||||
onBottomLeftCorner ||
|
|
||||||
onBottomRightCorner ||
|
|
||||||
onLeftBoundary ||
|
|
||||||
onRightBoundary ||
|
|
||||||
onTopBoundary ||
|
|
||||||
onBottomBoundary
|
|
||||||
) {
|
|
||||||
this.setResizeSide(mouseX, mouseY);
|
|
||||||
} else if (this.isInCropArea(mouseX, mouseY)) {
|
|
||||||
this.startDragging(mouseX, mouseY);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.body.style.userSelect = 'none';
|
document.body.style.userSelect = 'none';
|
||||||
@@ -615,20 +567,16 @@ class TransformManager implements EditToolManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleMouseMove(e: MouseEvent) {
|
handleMouseMove(e: MouseEvent) {
|
||||||
const canvas = this.cropAreaEl;
|
if (!this.cropAreaEl) {
|
||||||
if (!canvas) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resizeSideValue = this.resizeSide;
|
|
||||||
const { mouseX, mouseY } = this.getMousePosition(e);
|
const { mouseX, mouseY } = this.getMousePosition(e);
|
||||||
|
|
||||||
if (this.isDragging) {
|
if (this.isDragging) {
|
||||||
this.moveCrop(mouseX, mouseY);
|
this.moveCrop(mouseX, mouseY);
|
||||||
} else if (resizeSideValue) {
|
} else if (this.resizeSide !== ResizeBoundary.None) {
|
||||||
this.resizeCrop(mouseX, mouseY);
|
this.resizeCrop(mouseX, mouseY);
|
||||||
} else {
|
|
||||||
this.updateCursor(mouseX, mouseY);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -638,131 +586,42 @@ class TransformManager implements EditToolManager {
|
|||||||
|
|
||||||
this.isInteracting = false;
|
this.isInteracting = false;
|
||||||
this.isDragging = false;
|
this.isDragging = false;
|
||||||
this.resizeSide = '';
|
this.resizeSide = ResizeBoundary.None;
|
||||||
this.fadeOverlay(true); // Darken the background
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getMousePosition(e: MouseEvent) {
|
getMousePosition(e: MouseEvent) {
|
||||||
let offsetX = e.clientX;
|
if (!this.cropAreaEl) {
|
||||||
let offsetY = e.clientY;
|
throw new Error('Crop area is undefined');
|
||||||
const clienRect = this.cropAreaEl?.getBoundingClientRect();
|
|
||||||
const rotateDeg = this.normalizedRotation;
|
|
||||||
|
|
||||||
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 };
|
|
||||||
}
|
}
|
||||||
|
const clientRect = this.cropAreaEl.getBoundingClientRect();
|
||||||
|
|
||||||
// Boundary detection helpers
|
switch (this.normalizedRotation) {
|
||||||
private isInRange(value: number, target: number, sensitivity: number): boolean {
|
case 90: {
|
||||||
return value >= target - sensitivity && value <= target + sensitivity;
|
|
||||||
}
|
|
||||||
|
|
||||||
private isWithinBounds(value: number, min: number, max: number): boolean {
|
|
||||||
return value >= min && value <= max;
|
|
||||||
}
|
|
||||||
|
|
||||||
isOnCropBoundary(mouseX: number, mouseY: number) {
|
|
||||||
const { x, y, width, height } = this.region;
|
|
||||||
const sensitivity = 10;
|
|
||||||
const cornerSensitivity = 15;
|
|
||||||
const { width: imgWidth, height: imgHeight } = this.previewImageSize;
|
|
||||||
|
|
||||||
const outOfBound = mouseX > imgWidth || mouseY > imgHeight || mouseX < 0 || mouseY < 0;
|
|
||||||
if (outOfBound) {
|
|
||||||
return {
|
return {
|
||||||
onLeftBoundary: false,
|
mouseX: e.clientY - clientRect.top,
|
||||||
onRightBoundary: false,
|
mouseY: -e.clientX + clientRect.right,
|
||||||
onTopBoundary: false,
|
|
||||||
onBottomBoundary: false,
|
|
||||||
onTopLeftCorner: false,
|
|
||||||
onTopRightCorner: false,
|
|
||||||
onBottomLeftCorner: false,
|
|
||||||
onBottomRightCorner: false,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case 180: {
|
||||||
const onLeftBoundary = this.isInRange(mouseX, x, sensitivity) && this.isWithinBounds(mouseY, y, y + height);
|
|
||||||
const onRightBoundary =
|
|
||||||
this.isInRange(mouseX, x + width, sensitivity) && this.isWithinBounds(mouseY, y, y + height);
|
|
||||||
const onTopBoundary = this.isInRange(mouseY, y, sensitivity) && this.isWithinBounds(mouseX, x, x + width);
|
|
||||||
const onBottomBoundary =
|
|
||||||
this.isInRange(mouseY, y + height, sensitivity) && this.isWithinBounds(mouseX, x, x + width);
|
|
||||||
|
|
||||||
const onTopLeftCorner =
|
|
||||||
this.isInRange(mouseX, x, cornerSensitivity) && this.isInRange(mouseY, y, cornerSensitivity);
|
|
||||||
const onTopRightCorner =
|
|
||||||
this.isInRange(mouseX, x + width, cornerSensitivity) && this.isInRange(mouseY, y, cornerSensitivity);
|
|
||||||
const onBottomLeftCorner =
|
|
||||||
this.isInRange(mouseX, x, cornerSensitivity) && this.isInRange(mouseY, y + height, cornerSensitivity);
|
|
||||||
const onBottomRightCorner =
|
|
||||||
this.isInRange(mouseX, x + width, cornerSensitivity) && this.isInRange(mouseY, y + height, cornerSensitivity);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onLeftBoundary,
|
mouseX: -e.clientX + clientRect.right,
|
||||||
onRightBoundary,
|
mouseY: -e.clientY + clientRect.bottom,
|
||||||
onTopBoundary,
|
|
||||||
onBottomBoundary,
|
|
||||||
onTopLeftCorner,
|
|
||||||
onTopRightCorner,
|
|
||||||
onBottomLeftCorner,
|
|
||||||
onBottomRightCorner,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case 270: {
|
||||||
isInCropArea(mouseX: number, mouseY: number) {
|
return {
|
||||||
const { x, y, width, height } = this.region;
|
mouseX: -e.clientY + clientRect.bottom,
|
||||||
return mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height;
|
mouseY: e.clientX - clientRect.left,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
// also case 0:
|
||||||
setResizeSide(mouseX: number, mouseY: number) {
|
default: {
|
||||||
const {
|
return {
|
||||||
onLeftBoundary,
|
mouseX: e.clientX - clientRect.left,
|
||||||
onRightBoundary,
|
mouseY: e.clientY - clientRect.top,
|
||||||
onTopBoundary,
|
};
|
||||||
onBottomBoundary,
|
|
||||||
onTopLeftCorner,
|
|
||||||
onTopRightCorner,
|
|
||||||
onBottomLeftCorner,
|
|
||||||
onBottomRightCorner,
|
|
||||||
} = this.isOnCropBoundary(mouseX, mouseY);
|
|
||||||
|
|
||||||
if (onTopLeftCorner) {
|
|
||||||
this.resizeSide = 'top-left';
|
|
||||||
} else if (onTopRightCorner) {
|
|
||||||
this.resizeSide = 'top-right';
|
|
||||||
} else if (onBottomLeftCorner) {
|
|
||||||
this.resizeSide = 'bottom-left';
|
|
||||||
} else if (onBottomRightCorner) {
|
|
||||||
this.resizeSide = 'bottom-right';
|
|
||||||
} else if (onLeftBoundary) {
|
|
||||||
this.resizeSide = 'left';
|
|
||||||
} else if (onRightBoundary) {
|
|
||||||
this.resizeSide = 'right';
|
|
||||||
} else if (onTopBoundary) {
|
|
||||||
this.resizeSide = 'top';
|
|
||||||
} else if (onBottomBoundary) {
|
|
||||||
this.resizeSide = 'bottom';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
startDragging(mouseX: number, mouseY: number) {
|
|
||||||
this.isDragging = true;
|
|
||||||
const crop = this.region;
|
|
||||||
this.isInteracting = true;
|
|
||||||
this.dragOffset = { x: mouseX - crop.x, y: mouseY - crop.y };
|
|
||||||
this.fadeOverlay(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
moveCrop(mouseX: number, mouseY: number) {
|
moveCrop(mouseX: number, mouseY: number) {
|
||||||
@@ -772,65 +631,85 @@ class TransformManager implements EditToolManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.hasChanges = true;
|
this.hasChanges = true;
|
||||||
const newX = Math.max(0, Math.min(mouseX - this.dragOffset.x, cropArea.clientWidth - this.region.width));
|
this.region.x = clamp(mouseX - this.dragAnchor.x, 0, cropArea.clientWidth - this.region.width);
|
||||||
const newY = Math.max(0, Math.min(mouseY - this.dragOffset.y, cropArea.clientHeight - this.region.height));
|
this.region.y = clamp(mouseY - this.dragAnchor.y, 0, cropArea.clientHeight - this.region.height);
|
||||||
|
|
||||||
this.region = {
|
|
||||||
...this.region,
|
|
||||||
x: newX,
|
|
||||||
y: newY,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.draw();
|
this.draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
resizeCrop(mouseX: number, mouseY: number) {
|
resizeCrop(mouseX: number, mouseY: number) {
|
||||||
const canvas = this.cropAreaEl;
|
const canvas = this.cropAreaEl;
|
||||||
const crop = this.region;
|
const currentCrop = this.region;
|
||||||
const resizeSideValue = this.resizeSide;
|
if (!canvas) {
|
||||||
if (!canvas || !resizeSideValue) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.fadeOverlay(false);
|
this.isInteracting = true;
|
||||||
|
|
||||||
this.hasChanges = true;
|
this.hasChanges = true;
|
||||||
const { x, y, width, height } = crop;
|
const { x, y, width, height } = currentCrop;
|
||||||
const minSize = 50;
|
const minSize = 50;
|
||||||
let newRegion = { ...crop };
|
let newRegion = { ...currentCrop };
|
||||||
|
|
||||||
switch (resizeSideValue) {
|
let desiredWidth = width;
|
||||||
case 'left': {
|
let desiredHeight = height;
|
||||||
const desiredWidth = width + (x - mouseX);
|
|
||||||
if (desiredWidth >= minSize && mouseX >= 0) {
|
// Width
|
||||||
|
switch (this.resizeSide) {
|
||||||
|
case ResizeBoundary.Left:
|
||||||
|
case ResizeBoundary.TopLeft:
|
||||||
|
case ResizeBoundary.BottomLeft: {
|
||||||
|
desiredWidth = Math.max(minSize, width + (x - Math.max(mouseX, 0)));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ResizeBoundary.Right:
|
||||||
|
case ResizeBoundary.TopRight:
|
||||||
|
case ResizeBoundary.BottomRight: {
|
||||||
|
desiredWidth = Math.max(minSize, Math.max(mouseX, 0) - x);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Height
|
||||||
|
switch (this.resizeSide) {
|
||||||
|
case ResizeBoundary.Top:
|
||||||
|
case ResizeBoundary.TopLeft:
|
||||||
|
case ResizeBoundary.TopRight: {
|
||||||
|
desiredHeight = Math.max(minSize, height + (y - Math.max(mouseY, 0)));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ResizeBoundary.Bottom:
|
||||||
|
case ResizeBoundary.BottomLeft:
|
||||||
|
case ResizeBoundary.BottomRight: {
|
||||||
|
desiredHeight = Math.max(minSize, Math.max(mouseY, 0) - y);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Old
|
||||||
|
switch (this.resizeSide) {
|
||||||
|
case ResizeBoundary.Left: {
|
||||||
const { newWidth: w, newHeight: h } = this.keepAspectRatio(desiredWidth, height);
|
const { newWidth: w, newHeight: h } = this.keepAspectRatio(desiredWidth, height);
|
||||||
const finalWidth = Math.max(minSize, Math.min(w, canvas.clientWidth));
|
const finalWidth = clamp(w, minSize, canvas.clientWidth);
|
||||||
const finalHeight = Math.max(minSize, Math.min(h, canvas.clientHeight));
|
|
||||||
newRegion = {
|
newRegion = {
|
||||||
x: Math.max(0, x + width - finalWidth),
|
x: Math.max(0, x + width - finalWidth),
|
||||||
y,
|
y,
|
||||||
width: finalWidth,
|
width: finalWidth,
|
||||||
height: finalHeight,
|
height: clamp(h, minSize, canvas.clientHeight),
|
||||||
};
|
};
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'right': {
|
case ResizeBoundary.Right: {
|
||||||
const desiredWidth = mouseX - x;
|
|
||||||
if (desiredWidth >= minSize && mouseX <= canvas.clientWidth) {
|
|
||||||
const { newWidth: w, newHeight: h } = this.keepAspectRatio(desiredWidth, height);
|
const { newWidth: w, newHeight: h } = this.keepAspectRatio(desiredWidth, height);
|
||||||
newRegion = {
|
newRegion = {
|
||||||
...newRegion,
|
...newRegion,
|
||||||
width: Math.max(minSize, Math.min(w, canvas.clientWidth - x)),
|
width: clamp(w, minSize, canvas.clientWidth - x),
|
||||||
height: Math.max(minSize, Math.min(h, canvas.clientHeight)),
|
height: clamp(h, minSize, canvas.clientHeight),
|
||||||
};
|
};
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'top': {
|
case ResizeBoundary.Top: {
|
||||||
const desiredHeight = height + (y - mouseY);
|
|
||||||
if (desiredHeight >= minSize && mouseY >= 0) {
|
|
||||||
const { newWidth: w, newHeight: h } = this.adjustDimensions(
|
const { newWidth: w, newHeight: h } = this.adjustDimensions(
|
||||||
width,
|
desiredWidth,
|
||||||
desiredHeight,
|
desiredHeight,
|
||||||
this.cropAspectRatio,
|
this.cropAspectRatio,
|
||||||
canvas.clientWidth,
|
canvas.clientWidth,
|
||||||
@@ -843,14 +722,11 @@ class TransformManager implements EditToolManager {
|
|||||||
width: w,
|
width: w,
|
||||||
height: h,
|
height: h,
|
||||||
};
|
};
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'bottom': {
|
case ResizeBoundary.Bottom: {
|
||||||
const desiredHeight = mouseY - y;
|
|
||||||
if (desiredHeight >= minSize && mouseY <= canvas.clientHeight) {
|
|
||||||
const { newWidth: w, newHeight: h } = this.adjustDimensions(
|
const { newWidth: w, newHeight: h } = this.adjustDimensions(
|
||||||
width,
|
desiredWidth,
|
||||||
desiredHeight,
|
desiredHeight,
|
||||||
this.cropAspectRatio,
|
this.cropAspectRatio,
|
||||||
canvas.clientWidth,
|
canvas.clientWidth,
|
||||||
@@ -862,12 +738,9 @@ class TransformManager implements EditToolManager {
|
|||||||
width: w,
|
width: w,
|
||||||
height: h,
|
height: h,
|
||||||
};
|
};
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'top-left': {
|
case ResizeBoundary.TopLeft: {
|
||||||
const desiredWidth = width + (x - Math.max(mouseX, 0));
|
|
||||||
const desiredHeight = height + (y - Math.max(mouseY, 0));
|
|
||||||
const { newWidth: w, newHeight: h } = this.adjustDimensions(
|
const { newWidth: w, newHeight: h } = this.adjustDimensions(
|
||||||
desiredWidth,
|
desiredWidth,
|
||||||
desiredHeight,
|
desiredHeight,
|
||||||
@@ -884,9 +757,7 @@ class TransformManager implements EditToolManager {
|
|||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'top-right': {
|
case ResizeBoundary.TopRight: {
|
||||||
const desiredWidth = Math.max(mouseX, 0) - x;
|
|
||||||
const desiredHeight = height + (y - Math.max(mouseY, 0));
|
|
||||||
const { newWidth: w, newHeight: h } = this.adjustDimensions(
|
const { newWidth: w, newHeight: h } = this.adjustDimensions(
|
||||||
desiredWidth,
|
desiredWidth,
|
||||||
desiredHeight,
|
desiredHeight,
|
||||||
@@ -903,9 +774,7 @@ class TransformManager implements EditToolManager {
|
|||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'bottom-left': {
|
case ResizeBoundary.BottomLeft: {
|
||||||
const desiredWidth = width + (x - Math.max(mouseX, 0));
|
|
||||||
const desiredHeight = Math.max(mouseY, 0) - y;
|
|
||||||
const { newWidth: w, newHeight: h } = this.adjustDimensions(
|
const { newWidth: w, newHeight: h } = this.adjustDimensions(
|
||||||
desiredWidth,
|
desiredWidth,
|
||||||
desiredHeight,
|
desiredHeight,
|
||||||
@@ -922,9 +791,7 @@ class TransformManager implements EditToolManager {
|
|||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'bottom-right': {
|
case ResizeBoundary.BottomRight: {
|
||||||
const desiredWidth = Math.max(mouseX, 0) - x;
|
|
||||||
const desiredHeight = Math.max(mouseY, 0) - y;
|
|
||||||
const { newWidth: w, newHeight: h } = this.adjustDimensions(
|
const { newWidth: w, newHeight: h } = this.adjustDimensions(
|
||||||
desiredWidth,
|
desiredWidth,
|
||||||
desiredHeight,
|
desiredHeight,
|
||||||
@@ -952,95 +819,6 @@ class TransformManager implements EditToolManager {
|
|||||||
this.draw();
|
this.draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCursor(mouseX: number, mouseY: number) {
|
|
||||||
if (!this.cropAreaEl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let {
|
|
||||||
onLeftBoundary,
|
|
||||||
onRightBoundary,
|
|
||||||
onTopBoundary,
|
|
||||||
onBottomBoundary,
|
|
||||||
onTopLeftCorner,
|
|
||||||
onTopRightCorner,
|
|
||||||
onBottomLeftCorner,
|
|
||||||
onBottomRightCorner,
|
|
||||||
} = this.isOnCropBoundary(mouseX, mouseY);
|
|
||||||
|
|
||||||
if (this.normalizedRotation == 90) {
|
|
||||||
[onTopBoundary, onRightBoundary, onBottomBoundary, onLeftBoundary] = [
|
|
||||||
onLeftBoundary,
|
|
||||||
onTopBoundary,
|
|
||||||
onRightBoundary,
|
|
||||||
onBottomBoundary,
|
|
||||||
];
|
|
||||||
|
|
||||||
[onTopLeftCorner, onTopRightCorner, onBottomRightCorner, onBottomLeftCorner] = [
|
|
||||||
onBottomLeftCorner,
|
|
||||||
onTopLeftCorner,
|
|
||||||
onTopRightCorner,
|
|
||||||
onBottomRightCorner,
|
|
||||||
];
|
|
||||||
} else if (this.normalizedRotation == 180) {
|
|
||||||
[onTopBoundary, onBottomBoundary] = [onBottomBoundary, onTopBoundary];
|
|
||||||
[onLeftBoundary, onRightBoundary] = [onRightBoundary, onLeftBoundary];
|
|
||||||
|
|
||||||
[onTopLeftCorner, onBottomRightCorner] = [onBottomRightCorner, onTopLeftCorner];
|
|
||||||
[onTopRightCorner, onBottomLeftCorner] = [onBottomLeftCorner, onTopRightCorner];
|
|
||||||
} else if (this.normalizedRotation == 270) {
|
|
||||||
[onTopBoundary, onRightBoundary, onBottomBoundary, onLeftBoundary] = [
|
|
||||||
onRightBoundary,
|
|
||||||
onBottomBoundary,
|
|
||||||
onLeftBoundary,
|
|
||||||
onTopBoundary,
|
|
||||||
];
|
|
||||||
|
|
||||||
[onTopLeftCorner, onTopRightCorner, onBottomRightCorner, onBottomLeftCorner] = [
|
|
||||||
onTopRightCorner,
|
|
||||||
onBottomRightCorner,
|
|
||||||
onBottomLeftCorner,
|
|
||||||
onTopLeftCorner,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
let cursorName: string;
|
|
||||||
if (onTopLeftCorner || onBottomRightCorner) {
|
|
||||||
cursorName = 'nwse-resize';
|
|
||||||
} else if (onTopRightCorner || onBottomLeftCorner) {
|
|
||||||
cursorName = 'nesw-resize';
|
|
||||||
} else if (onLeftBoundary || onRightBoundary) {
|
|
||||||
cursorName = 'ew-resize';
|
|
||||||
} else if (onTopBoundary || onBottomBoundary) {
|
|
||||||
cursorName = 'ns-resize';
|
|
||||||
} else if (this.isInCropArea(mouseX, mouseY)) {
|
|
||||||
cursorName = 'move';
|
|
||||||
} else {
|
|
||||||
cursorName = 'default';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.canvasCursor != cursorName && this.cropAreaEl && !editManager.isShowingConfirmDialog) {
|
|
||||||
this.canvasCursor = cursorName;
|
|
||||||
document.body.style.cursor = cursorName;
|
|
||||||
this.cropAreaEl.style.cursor = cursorName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fadeOverlay(toDark: boolean) {
|
|
||||||
const overlay = this.overlayEl;
|
|
||||||
const cropFrame = document.querySelector('.crop-frame');
|
|
||||||
|
|
||||||
if (toDark) {
|
|
||||||
overlay?.classList.remove('light');
|
|
||||||
cropFrame?.classList.remove('resizing');
|
|
||||||
} else {
|
|
||||||
overlay?.classList.add('light');
|
|
||||||
cropFrame?.classList.add('resizing');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isInteracting = !toDark;
|
|
||||||
}
|
|
||||||
|
|
||||||
resetCrop() {
|
resetCrop() {
|
||||||
this.cropAspectRatio = 'free';
|
this.cropAspectRatio = 'free';
|
||||||
this.region = {
|
this.region = {
|
||||||
|
|||||||
Reference in New Issue
Block a user