import { memo, useCallback, useMemo, useRef } from "react" import { useTimeline } from "@/features/timeline/hooks/use-timeline" import { TimelineTransition } from "@/features/timeline/types/timeline-transition" import { Transition } from "@/features/transitions/types/transitions" import { cn } from "@/lib/utils" import { TransitionHandles } from "./transition-handles" interface TimelineTransitionProps { transition: TimelineTransition transitionResource?: Transition trackHeight: number pixelsPerSecond: number onSelect?: (id: string) => void onDurationChange?: (id: string, duration: number) => void onPositionChange?: (id: string, position: number) => void onDelete?: (id: string) => void } /** * Компонент визуализации перехода на таймлайне * Отображает переход как отдельный объект между клипами */ export const TimelineTransitionComponent = memo(function TimelineTransitionComponent({ transition, transitionResource, trackHeight, pixelsPerSecond, onSelect, onDurationChange, onPositionChange, onDelete, }: TimelineTransitionProps) { const { uiState } = useTimeline() const timelineScale = uiState?.scale || 1 const containerRef = useRef(null) // Вычисляем размеры и позицию const width = transition.duration * pixelsPerSecond * timelineScale const left = transition.position * pixelsPerSecond * timelineScale // Стили в зависимости от типа перехода const transitionStyles = useMemo(() => { const baseColor = getTransitionColor(transitionResource?.category || "basic") const isComplex = transitionResource?.complexity === "advanced" || transitionResource?.complexity === "gpu-required" return { backgroundColor: transition.isEnabled ? baseColor : "#666", opacity: transition.isEnabled ? 1 : 0.6, background: isComplex ? `linear-gradient(135deg, ${baseColor} 0%, ${adjustColor(baseColor, -20)} 100%)` : baseColor, } }, [transition.isEnabled, transitionResource]) // Обработчик клика const handleClick = useCallback( (e: React.MouseEvent) => { e.stopPropagation() onSelect?.(transition.id) }, [transition.id, onSelect], ) // Обработчик удаления const handleDelete = useCallback( (e: React.MouseEvent) => { e.stopPropagation() onDelete?.(transition.id) }, [transition.id, onDelete], ) // Визуализация кривой перехода const curvePathData = useMemo(() => { if (transition.curve.type === "linear") { return `M 0,${trackHeight} L ${width},0` } if (transition.curve.type === "custom" && transition.curve.points.length > 0) { // Генерируем path из точек кривой const points = transition.curve.points let path = `M ${points[0].x * width},${(1 - points[0].y) * trackHeight}` for (let i = 1; i < points.length; i++) { const prev = points[i - 1] const curr = points[i] if (prev.handleOut && curr.handleIn) { // Bezier кривая path += ` C ${prev.handleOut.x * width},${(1 - prev.handleOut.y) * trackHeight} ${ curr.handleIn.x * width },${(1 - curr.handleIn.y) * trackHeight} ${curr.x * width},${(1 - curr.y) * trackHeight}` } else { // Прямая линия path += ` L ${curr.x * width},${(1 - curr.y) * trackHeight}` } } return path } // Стандартные easing кривые return generateEasingCurve(transition.curve.type, width, trackHeight) }, [transition.curve, width, trackHeight]) return (
{/* Форма перехода */} {/* Основная форма */} {/* Кривая перехода */} {/* Keyframes индикаторы */} {transition.keyframes.map((keyframe) => ( ))} {/* Название перехода */}
{transitionResource?.labels?.ru || transition.transitionId}
{/* Параметры при наведении */} {transition.isSelected && (
{transition.parameters.blur?.enabled &&
Blur: {transition.parameters.blur.amount}%
} {transition.parameters.color?.enabled &&
Color: {transition.parameters.color.tint}
}
)} {/* Handles для изменения длительности и позиции */} {!transition.isLocked && onDurationChange && onPositionChange && ( )} {/* Кнопка удаления */} {!transition.isLocked && ( )} {/* Индикатор GPU */} {transitionResource?.gpuAccelerated && (
G
)} {/* Статус кеша */} {transition.renderCache?.status === "ready" && (
)}
) }) /** * Получить цвет для категории перехода */ function getTransitionColor(category: string): string { const colors: Record = { basic: "#4A90E2", advanced: "#7B68EE", creative: "#FF6B6B", "3d": "#4ECDC4", artistic: "#FFD93D", cinematic: "#6C5CE7", dynamic: "#A29BFE", glitch: "#FD79A8", light: "#FDCB6E", film: "#6C5CE7", motion: "#00B894", seamless: "#00CEC9", } return colors[category] || "#4A90E2" } /** * Настроить яркость цвета */ function adjustColor(color: string, amount: number): string { const num = Number.parseInt(color.replace("#", ""), 16) const r = Math.min(255, Math.max(0, (num >> 16) + amount)) const g = Math.min(255, Math.max(0, ((num >> 8) & 0x00ff) + amount)) const b = Math.min(255, Math.max(0, (num & 0x0000ff) + amount)) return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, "0")}` } /** * Генерировать path для стандартных easing функций */ function generateEasingCurve(type: string, width: number, height: number): string { const steps = 50 let path = `M 0,${height}` for (let i = 1; i <= steps; i++) { const t = i / steps const x = t * width let y: number switch (type) { case "ease-in": y = (1 - t * t) * height break case "ease-out": y = (1 - (1 - t) * (1 - t)) * height break case "ease-in-out": y = (1 - (t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2)) * height break default: y = (1 - t) * height } path += ` L ${x},${y}` } return path }