import React, { useEffect, useMemo, useState } from 'react'; import { Play, Pause } from 'lucide-react'; import { cn } from '../lib/utils'; import { useStore } from '../store/useStore'; export function FrameTimeline() { const frames = useStore((state) => state.frames); const currentProject = useStore((state) => state.currentProject); const currentFrameIndex = useStore((state) => state.currentFrameIndex); const setCurrentFrame = useStore((state) => state.setCurrentFrame); const [isPlaying, setIsPlaying] = useState(false); const totalFrames = frames.length; const currentFrame = totalFrames > 0 ? currentFrameIndex + 1 : 0; const playbackFps = useMemo(() => { const fps = currentProject?.parse_fps || currentProject?.original_fps || 12; return Math.min(Math.max(fps, 1), 30); }, [currentProject?.original_fps, currentProject?.parse_fps]); useEffect(() => { if (!isPlaying || totalFrames <= 1) return; const timer = window.setTimeout(() => { if (currentFrameIndex >= totalFrames - 1) { setIsPlaying(false); return; } setCurrentFrame(currentFrameIndex + 1); }, 1000 / playbackFps); return () => window.clearTimeout(timer); }, [currentFrameIndex, isPlaying, playbackFps, setCurrentFrame, totalFrames]); useEffect(() => { if (totalFrames === 0) { setIsPlaying(false); } }, [totalFrames]); // show frames around current frame const frameWindow = 20; const displayIndices = totalFrames > 0 ? Array.from({ length: 41 }, (_, i) => currentFrameIndex - frameWindow + i) : []; return (
setCurrentFrame(parseInt(e.target.value) - 1)} className="w-full absolute inset-0 opacity-0 cursor-ew-resize z-20" disabled={totalFrames === 0} />
0 ? (currentFrame / totalFrames) * 100 : 0}%` }} />
0 ? (currentFrame / totalFrames) * 100 : 0}%` }} />
{isPlaying ? '暂停 (SPACE)' : '播放序列 (F5)'}
{totalFrames === 0 ? (
暂无帧数据
) : ( displayIndices.map((idx) => { if (idx < 0 || idx >= totalFrames) { return
} const frame = frames[idx]; const isCurrent = idx === currentFrameIndex; return (
setCurrentFrame(idx)} className={cn( "relative shrink-0 rounded-sm transition-all cursor-pointer flex items-center justify-center overflow-hidden group mx-0.5", isCurrent ? "w-28 h-16 border-2 border-cyan-500 bg-gray-700 shadow-[0_0_15px_rgba(6,182,212,0.3)] z-10" : "w-16 h-12 border border-white/5 bg-gray-800/50 opacity-40 hover:opacity-100" )} > {frame.url ? ( {`frame-${idx}`} ) : (
)} {(idx + 1).toString().padStart(4, '0')}
); }) )}
{currentFrame} / {totalFrames}
底层时序视频图层截帧导航轴
); }