Skip to content

Commit 91fc236

Browse files
committed
feat: Add visual crop preview widget for ImageCrop node - widget ImageCrop
1 parent 14528aa commit 91fc236

File tree

12 files changed

+766
-1
lines changed

12 files changed

+766
-1
lines changed
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
<template>
2+
<div
3+
class="widget-expands relative flex h-full w-full flex-col gap-1"
4+
@pointerdown.stop
5+
@pointermove.stop
6+
@pointerup.stop
7+
>
8+
<!-- Number inputs row -->
9+
<div class="flex shrink-0 gap-1 px-1">
10+
<div class="flex flex-1 items-center gap-1">
11+
<label class="w-6 text-xs text-muted">X</label>
12+
<input
13+
v-model.number="cropX"
14+
type="number"
15+
:min="0"
16+
class="h-6 w-full rounded border border-border bg-input px-1 text-xs"
17+
@change="handleInputChange"
18+
/>
19+
</div>
20+
<div class="flex flex-1 items-center gap-1">
21+
<label class="w-6 text-xs text-muted">Y</label>
22+
<input
23+
v-model.number="cropY"
24+
type="number"
25+
:min="0"
26+
class="h-6 w-full rounded border border-border bg-input px-1 text-xs"
27+
@change="handleInputChange"
28+
/>
29+
</div>
30+
<div class="flex flex-1 items-center gap-1">
31+
<label class="w-6 text-xs text-muted">W</label>
32+
<input
33+
v-model.number="cropWidth"
34+
type="number"
35+
:min="1"
36+
class="h-6 w-full rounded border border-border bg-input px-1 text-xs"
37+
@change="handleInputChange"
38+
/>
39+
</div>
40+
<div class="flex flex-1 items-center gap-1">
41+
<label class="w-6 text-xs text-muted">H</label>
42+
<input
43+
v-model.number="cropHeight"
44+
type="number"
45+
:min="1"
46+
class="h-6 w-full rounded border border-border bg-input px-1 text-xs"
47+
@change="handleInputChange"
48+
/>
49+
</div>
50+
</div>
51+
52+
<!-- Image preview container -->
53+
<div
54+
ref="containerEl"
55+
class="relative min-h-0 flex-1 overflow-hidden rounded-[5px] bg-node-component-surface"
56+
>
57+
<div v-if="isLoading" class="flex size-full items-center justify-center">
58+
<span class="text-sm">{{ $t('imageCrop.loading') }}</span>
59+
</div>
60+
61+
<div
62+
v-else-if="!imageUrl"
63+
class="flex size-full flex-col items-center justify-center text-center"
64+
>
65+
<i class="mb-2 icon-[lucide--image] h-12 w-12" />
66+
<p class="text-sm">{{ $t('imageCrop.noInputImage') }}</p>
67+
</div>
68+
69+
<img
70+
v-else
71+
ref="imageEl"
72+
:src="imageUrl"
73+
:alt="$t('imageCrop.cropPreviewAlt')"
74+
draggable="false"
75+
class="block size-full object-contain select-none brightness-50"
76+
@load="handleImageLoad"
77+
@error="handleImageError"
78+
@dragstart.prevent
79+
/>
80+
81+
<div
82+
v-if="imageUrl && !isLoading"
83+
class="absolute box-border cursor-move overflow-hidden border-2 border-white"
84+
:style="cropBoxStyle"
85+
@pointerdown="handleDragStart"
86+
@pointermove="handleDragMove"
87+
@pointerup="handleDragEnd"
88+
>
89+
<div class="pointer-events-none size-full" :style="cropImageStyle" />
90+
</div>
91+
92+
<div
93+
v-for="handle in resizeHandles"
94+
v-show="imageUrl && !isLoading"
95+
:key="handle.direction"
96+
:class="['absolute', handle.class]"
97+
:style="handle.style"
98+
@pointerdown="(e) => handleResizeStart(e, handle.direction)"
99+
@pointermove="handleResizeMove"
100+
@pointerup="handleResizeEnd"
101+
/>
102+
</div>
103+
</div>
104+
</template>
105+
106+
<script setup lang="ts">
107+
import { useTemplateRef } from 'vue'
108+
109+
import { useImageCrop } from '@/composables/useImageCrop'
110+
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
111+
112+
export interface CropRegion {
113+
x: number
114+
y: number
115+
width: number
116+
height: number
117+
}
118+
119+
const props = defineProps<{
120+
nodeId: NodeId
121+
}>()
122+
123+
const modelValue = defineModel<CropRegion>({
124+
default: () => ({ x: 0, y: 0, width: 512, height: 512 })
125+
})
126+
127+
const imageEl = useTemplateRef<HTMLImageElement>('imageEl')
128+
const containerEl = useTemplateRef<HTMLDivElement>('containerEl')
129+
130+
const {
131+
imageUrl,
132+
isLoading,
133+
134+
cropX,
135+
cropY,
136+
cropWidth,
137+
cropHeight,
138+
139+
cropBoxStyle,
140+
cropImageStyle,
141+
resizeHandles,
142+
143+
handleImageLoad,
144+
handleImageError,
145+
handleInputChange,
146+
handleDragStart,
147+
handleDragMove,
148+
handleDragEnd,
149+
handleResizeStart,
150+
handleResizeMove,
151+
handleResizeEnd
152+
} = useImageCrop(props.nodeId, { imageEl, containerEl, modelValue })
153+
</script>

0 commit comments

Comments
 (0)