mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-03 04:00:31 +00:00
feat: add animation progress bar for 3D nodes and viewer (#7839)
## Summary - Add draggable progress bar to AnimationControls component - Display current time / total duration - Allow seeking through animations when paused or playing - Add animation controls to 3D Viewer fix https://github.com/Comfy-Org/ComfyUI_frontend/issues/7830 and https://github.com/Comfy-Org/ComfyUI_frontend/issues/7831 ## Screenshots (if applicable) https://github.com/user-attachments/assets/f6d0668c-c7a4-497e-8345-9ef6e47a41c6 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7839-feat-add-animation-progress-bar-for-3D-nodes-and-viewer-2de6d73d36508101ac98f673206b30d9) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -33,6 +33,9 @@
|
||||
v-model:playing="playing"
|
||||
v-model:selected-speed="selectedSpeed"
|
||||
v-model:selected-animation="selectedAnimation"
|
||||
v-model:animation-progress="animationProgress"
|
||||
v-model:animation-duration="animationDuration"
|
||||
@seek="handleSeek"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
@@ -119,6 +122,8 @@ const {
|
||||
playing,
|
||||
selectedSpeed,
|
||||
selectedAnimation,
|
||||
animationProgress,
|
||||
animationDuration,
|
||||
loading,
|
||||
loadingMessage,
|
||||
|
||||
@@ -130,6 +135,7 @@ const {
|
||||
handleStopRecording,
|
||||
handleExportRecording,
|
||||
handleClearRecording,
|
||||
handleSeek,
|
||||
handleBackgroundImageUpdate,
|
||||
handleExportModel,
|
||||
handleModelDrop,
|
||||
|
||||
@@ -15,6 +15,16 @@
|
||||
@dragleave.stop="handleDragLeave"
|
||||
@drop.prevent.stop="handleDrop"
|
||||
/>
|
||||
<AnimationControls
|
||||
v-if="viewer.animations.value && viewer.animations.value.length > 0"
|
||||
v-model:animations="viewer.animations.value"
|
||||
v-model:playing="viewer.playing.value"
|
||||
v-model:selected-speed="viewer.selectedSpeed.value"
|
||||
v-model:selected-animation="viewer.selectedAnimation.value"
|
||||
v-model:animation-progress="viewer.animationProgress.value"
|
||||
v-model:animation-duration="viewer.animationDuration.value"
|
||||
@seek="viewer.handleSeek"
|
||||
/>
|
||||
<div
|
||||
v-if="isDragging"
|
||||
class="pointer-events-none absolute inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
||||
@@ -85,6 +95,7 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref, toRaw } from 'vue'
|
||||
|
||||
import AnimationControls from '@/components/load3d/controls/AnimationControls.vue'
|
||||
import CameraControls from '@/components/load3d/controls/viewer/ViewerCameraControls.vue'
|
||||
import ExportControls from '@/components/load3d/controls/viewer/ViewerExportControls.vue'
|
||||
import LightControls from '@/components/load3d/controls/viewer/ViewerLightControls.vue'
|
||||
|
||||
@@ -1,42 +1,64 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="animations && animations.length > 0"
|
||||
class="pointer-events-auto absolute top-0 left-0 z-10 flex w-full items-center justify-center gap-2 pt-2"
|
||||
class="pointer-events-auto absolute top-0 left-0 z-10 flex w-full flex-col items-center gap-2 pt-2"
|
||||
>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="textonly"
|
||||
class="rounded-full"
|
||||
:aria-label="$t('g.playPause')"
|
||||
@click="togglePlay"
|
||||
>
|
||||
<i
|
||||
:class="['pi', playing ? 'pi-pause' : 'pi-play', 'text-lg text-white']"
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="textonly"
|
||||
class="rounded-full"
|
||||
:aria-label="$t('g.playPause')"
|
||||
@click="togglePlay"
|
||||
>
|
||||
<i
|
||||
:class="[
|
||||
'pi',
|
||||
playing ? 'pi-pause' : 'pi-play',
|
||||
'text-lg text-white'
|
||||
]"
|
||||
/>
|
||||
</Button>
|
||||
|
||||
<Select
|
||||
v-model="selectedSpeed"
|
||||
:options="speedOptions"
|
||||
option-label="name"
|
||||
option-value="value"
|
||||
class="w-24"
|
||||
/>
|
||||
</Button>
|
||||
|
||||
<Select
|
||||
v-model="selectedSpeed"
|
||||
:options="speedOptions"
|
||||
option-label="name"
|
||||
option-value="value"
|
||||
class="w-24"
|
||||
/>
|
||||
<Select
|
||||
v-model="selectedAnimation"
|
||||
:options="animations"
|
||||
option-label="name"
|
||||
option-value="index"
|
||||
class="w-32"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Select
|
||||
v-model="selectedAnimation"
|
||||
:options="animations"
|
||||
option-label="name"
|
||||
option-value="index"
|
||||
class="w-32"
|
||||
/>
|
||||
<div class="flex w-full max-w-xs items-center gap-2 px-4">
|
||||
<Slider
|
||||
:model-value="[animationProgress]"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="0.1"
|
||||
class="flex-1"
|
||||
@update:model-value="handleSliderChange"
|
||||
/>
|
||||
<span class="min-w-16 text-xs text-white">
|
||||
{{ formatTime(currentTime) }} / {{ formatTime(animationDuration) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Select from 'primevue/select'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import Slider from '@/components/ui/slider/Slider.vue'
|
||||
|
||||
type Animation = { name: string; index: number }
|
||||
|
||||
@@ -44,6 +66,16 @@ const animations = defineModel<Animation[]>('animations')
|
||||
const playing = defineModel<boolean>('playing')
|
||||
const selectedSpeed = defineModel<number>('selectedSpeed')
|
||||
const selectedAnimation = defineModel<number>('selectedAnimation')
|
||||
const animationProgress = defineModel<number>('animationProgress', {
|
||||
default: 0
|
||||
})
|
||||
const animationDuration = defineModel<number>('animationDuration', {
|
||||
default: 0
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
seek: [progress: number]
|
||||
}>()
|
||||
|
||||
const speedOptions = [
|
||||
{ name: '0.1x', value: 0.1 },
|
||||
@@ -53,7 +85,25 @@ const speedOptions = [
|
||||
{ name: '2x', value: 2 }
|
||||
]
|
||||
|
||||
const togglePlay = () => {
|
||||
const currentTime = computed(() => {
|
||||
if (!animationDuration.value) return 0
|
||||
return (animationProgress.value / 100) * animationDuration.value
|
||||
})
|
||||
|
||||
function formatTime(seconds: number): string {
|
||||
const mins = Math.floor(seconds / 60)
|
||||
const secs = (seconds % 60).toFixed(1)
|
||||
return mins > 0 ? `${mins}:${secs.padStart(4, '0')}` : `${secs}s`
|
||||
}
|
||||
|
||||
function togglePlay() {
|
||||
playing.value = !playing.value
|
||||
}
|
||||
|
||||
function handleSliderChange(value: number[] | undefined) {
|
||||
if (!value) return
|
||||
const progress = value[0]
|
||||
animationProgress.value = progress
|
||||
emit('seek', progress)
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user