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:
Terry Jia
2026-01-04 08:47:30 -05:00
committed by GitHub
parent 8d1f8edc5a
commit 7fcfa4c201
9 changed files with 280 additions and 27 deletions

View File

@@ -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,

View File

@@ -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'

View File

@@ -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>