Skip to content

Commit 567a771

Browse files
committed
feat: Add visual crop preview widget for ImageCrop node - widget ImageCrop
1 parent 10feb1f commit 567a771

File tree

12 files changed

+760
-1
lines changed

12 files changed

+760
-1
lines changed
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
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 { CropRegionValue } from '@/lib/litegraph/src/types/widgets'
111+
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
112+
113+
const props = defineProps<{
114+
nodeId: NodeId
115+
}>()
116+
117+
const modelValue = defineModel<CropRegionValue>({
118+
default: () => ({ x: 0, y: 0, width: 512, height: 512 })
119+
})
120+
121+
const imageEl = useTemplateRef<HTMLImageElement>('imageEl')
122+
const containerEl = useTemplateRef<HTMLDivElement>('containerEl')
123+
124+
const {
125+
imageUrl,
126+
isLoading,
127+
128+
cropX,
129+
cropY,
130+
cropWidth,
131+
cropHeight,
132+
133+
cropBoxStyle,
134+
cropImageStyle,
135+
resizeHandles,
136+
137+
handleImageLoad,
138+
handleImageError,
139+
handleInputChange,
140+
handleDragStart,
141+
handleDragMove,
142+
handleDragEnd,
143+
handleResizeStart,
144+
handleResizeMove,
145+
handleResizeEnd
146+
} = useImageCrop(props.nodeId, { imageEl, containerEl, modelValue })
147+
</script>

0 commit comments

Comments
 (0)