mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-27 17:52:16 +00:00
Initial pan/zoom implementation
This commit is contained in:
44
src/components/ui/ZoomPane.vue
Normal file
44
src/components/ui/ZoomPane.vue
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, useTemplateRef } from 'vue'
|
||||||
|
|
||||||
|
const zoomPane = useTemplateRef('zoomPane')
|
||||||
|
|
||||||
|
const zoom = ref(1.0)
|
||||||
|
const panX = ref(0.0)
|
||||||
|
const panY = ref(0.0)
|
||||||
|
|
||||||
|
function handleWheel(e: WheelEvent) {
|
||||||
|
zoom.value -= e.deltaY
|
||||||
|
//TODO apply pan relative to mouse coords?
|
||||||
|
}
|
||||||
|
let dragging = false
|
||||||
|
function handleDown(e: PointerEvent) {
|
||||||
|
const zoomPaneEl = zoomPane.value
|
||||||
|
if (!zoomPaneEl) return
|
||||||
|
zoomPaneEl.setPointerCapture(e.pointerId)
|
||||||
|
dragging = true
|
||||||
|
}
|
||||||
|
function handleMove(e: PointerEvent) {
|
||||||
|
if (!dragging) return
|
||||||
|
panX.value += e.movementX
|
||||||
|
panY.value += e.movementY
|
||||||
|
}
|
||||||
|
|
||||||
|
const transform = computed(() => {
|
||||||
|
const scale = 1.1 ** (zoom.value / 30)
|
||||||
|
const matrix = [scale, 0, 0, scale, panX.value, panY.value]
|
||||||
|
return `matrix(${matrix.join(',')})`
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="zoomPane"
|
||||||
|
class="contain-content"
|
||||||
|
@wheel="handleWheel"
|
||||||
|
@pointerdown.prevent="handleDown"
|
||||||
|
@pointermove="handleMove"
|
||||||
|
@pointerup="dragging = false"
|
||||||
|
>
|
||||||
|
<slot :style="{ transform }" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -14,6 +14,7 @@ import { computed, ref, shallowRef, useTemplateRef, watch } from 'vue'
|
|||||||
|
|
||||||
import TopbarBadges from '@/components/topbar/TopbarBadges.vue'
|
import TopbarBadges from '@/components/topbar/TopbarBadges.vue'
|
||||||
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
|
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
|
||||||
|
import ZoomPane from '@/components/ui/ZoomPane.vue'
|
||||||
import Button from '@/components/ui/button/Button.vue'
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
import { safeWidgetMapper } from '@/composables/graph/useGraphNodeManager'
|
import { safeWidgetMapper } from '@/composables/graph/useGraphNodeManager'
|
||||||
import { d, t } from '@/i18n'
|
import { d, t } from '@/i18n'
|
||||||
@@ -267,6 +268,10 @@ function gotoPreviousOutput() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleCenterWheel(e: WheelEvent) {
|
function handleCenterWheel(e: WheelEvent) {
|
||||||
|
if (!e.ctrlKey && !e.metaKey) return
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
//TODO roll in litegraph/CanvasPointer and give slight stickiness when on trackpad
|
//TODO roll in litegraph/CanvasPointer and give slight stickiness when on trackpad
|
||||||
if (e.deltaY > 0) gotoNextOutput()
|
if (e.deltaY > 0) gotoNextOutput()
|
||||||
else {
|
else {
|
||||||
@@ -347,7 +352,7 @@ function handleCenterWheel(e: WheelEvent) {
|
|||||||
<SplitterPanel
|
<SplitterPanel
|
||||||
:size="98"
|
:size="98"
|
||||||
class="flex flex-col min-w-min gap-4 mx-12 my-8 relative"
|
class="flex flex-col min-w-min gap-4 mx-12 my-8 relative"
|
||||||
@wheel="handleCenterWheel"
|
@wheel.capture="handleCenterWheel"
|
||||||
>
|
>
|
||||||
<div class="flex gap-4 text-muted-foreground h-14 w-full items-center">
|
<div class="flex gap-4 text-muted-foreground h-14 w-full items-center">
|
||||||
<div
|
<div
|
||||||
@@ -374,18 +379,19 @@ function handleCenterWheel(e: WheelEvent) {
|
|||||||
<i class="icon-[lucide--ellipsis]" />
|
<i class="icon-[lucide--ellipsis]" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<img
|
<ZoomPane
|
||||||
v-if="preview?.mediaType === 'images'"
|
v-if="preview?.mediaType === 'images'"
|
||||||
class="object-contain flex-1 max-h-full"
|
v-slot="slotProps"
|
||||||
:src="preview.url"
|
class="flex-1 w-full"
|
||||||
/>
|
>
|
||||||
|
<img :src="preview.url" v-bind="slotProps" />
|
||||||
|
</ZoomPane>
|
||||||
<!--FIXME: core videos are type 'images', VHS still wrapped as 'gifs'-->
|
<!--FIXME: core videos are type 'images', VHS still wrapped as 'gifs'-->
|
||||||
<video
|
<video
|
||||||
v-else-if="preview?.mediaType === 'gifs'"
|
v-else-if="preview?.mediaType === 'gifs'"
|
||||||
class="object-contain flex-1 min-h-0"
|
class="object-contain flex-1 min-h-0"
|
||||||
controls
|
controls
|
||||||
:src="preview.url"
|
:src="preview.url"
|
||||||
@wheel.prevent
|
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
v-else
|
v-else
|
||||||
|
|||||||
Reference in New Issue
Block a user