Skip to content

Commit d489481

Browse files
committed
feat: Add visual crop preview widget for ImageCrop node - widget ImageCrop
1 parent 81df010 commit d489481

File tree

12 files changed

+757
-1
lines changed

12 files changed

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

0 commit comments

Comments
 (0)