Files
20260512-skg-tk/web/components/keyframe-gallery.tsx

93 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import { useState } from "react"
import { motion, AnimatePresence } from "framer-motion"
import { Check, Plus } from "lucide-react"
import { type KeyFrame } from "@/lib/api"
interface Props {
frames: KeyFrame[]
selected: Set<number>
onToggle: (frameIndex: number) => void
maxSelect?: number
}
function formatTimestamp(t: number) {
const m = Math.floor(t / 60)
const s = Math.floor(t % 60)
return `${m}:${s.toString().padStart(2, "0")}`
}
export function KeyframeGallery({ frames, selected, onToggle, maxSelect = 10 }: Props) {
const [hoverIdx, setHoverIdx] = useState<number | null>(null)
if (frames.length === 0) {
return (
<div className="glass-card flex h-64 items-center justify-center text-white/30 text-sm">
</div>
)
}
return (
<div className="relative">
<div className="absolute -top-8 right-0 text-xs text-white/40">
<span className="text-white/80 font-medium">{selected.size}</span> / {maxSelect}
</div>
<div className="flex gap-3 overflow-x-auto pb-3 -mx-2 px-2 snap-x snap-mandatory">
{frames.map((f) => {
const isSelected = selected.has(f.index)
const isHover = hoverIdx === f.index
const disabled = !isSelected && selected.size >= maxSelect
return (
<motion.button
key={f.index}
type="button"
onClick={() => !disabled && onToggle(f.index)}
onMouseEnter={() => setHoverIdx(f.index)}
onMouseLeave={() => setHoverIdx(null)}
disabled={disabled}
animate={{
scale: isSelected ? 1 : isHover ? 0.98 : 0.92,
opacity: isSelected ? 1 : disabled ? 0.3 : isHover ? 0.95 : 0.7,
}}
transition={{ duration: 0.3, ease: [0.32, 0.72, 0, 1] }}
className="relative shrink-0 snap-start rounded-lg overflow-hidden border border-white/10 disabled:cursor-not-allowed"
style={{ width: 200, height: 120 }}
>
<img
src={f.url}
alt={`frame ${f.index}`}
className="w-full h-full object-cover"
loading="lazy"
/>
{/* 选中态ambient glow */}
<AnimatePresence>
{isSelected && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="absolute inset-0 ring-2 ring-white rounded-lg shadow-[0_0_30px_rgba(255,255,255,0.4)]"
/>
)}
</AnimatePresence>
{/* 时间戳 */}
<div className="absolute bottom-1.5 left-1.5 rounded bg-black/60 px-1.5 py-0.5 text-[10px] font-mono text-white/90">
{formatTimestamp(f.timestamp)}
</div>
{/* 选中标记 */}
<div
className={`absolute top-1.5 right-1.5 h-6 w-6 rounded-full flex items-center justify-center ${
isSelected ? "bg-white text-black" : "bg-black/50 text-white/60 border border-white/20"
}`}
>
{isSelected ? <Check className="h-3.5 w-3.5" /> : <Plus className="h-3.5 w-3.5" />}
</div>
</motion.button>
)
})}
</div>
</div>
)
}