import { useMitt } from '@/plugins/MittContext'; import { useEffect, useRef } from 'react'; import { Image, Layer, Line, Stage, Text, Transformer } from 'react-konva'; // Import our custom hooks and utilities import { useElementSelection } from './video-preview/video-preview-element-selection'; import { useElementTransform } from './video-preview/video-preview-element-transform'; import { getImageSource, getTextFontStyle } from './video-preview/video-preview-utils'; const VideoPreview = ({ // Dimensions dimensions, // Timeline state currentTime, totalDuration, isPlaying, status, // Export state isExporting, exportProgress, exportStatus, // Data timelineElements, activeElements, videoElements, loadedVideos, videoStates, ffmpegCommand, // Event handlers handlePlay, handlePause, handleReset, handleSeek, copyFFmpegCommand, exportVideo, onElementUpdate, // New prop for updating element properties // Refs layerRef, }) => { const emitter = useMitt(); // Refs for elements and transformer const transformerRef = useRef(null); const stageRef = useRef(null); const elementRefs = useRef({}); // Use our custom hooks const { selectedElementId, setSelectedElementId, handleElementSelect, handleStageClick: baseHandleStageClick, } = useElementSelection(emitter, timelineElements); const { guideLines, handleDragMove, handleDragEnd, createDragBoundFunc, handleTransform, clearGuideLines } = useElementTransform( timelineElements, dimensions, onElementUpdate, elementRefs, ); // Enhanced stage click handler that also clears guide lines const handleStageClick = (e) => { baseHandleStageClick(e); if (e.target === e.target.getStage()) { clearGuideLines(); } }; // Update transformer when selection changes useEffect(() => { if (transformerRef.current) { const selectedNode = selectedElementId ? elementRefs.current[selectedElementId] : null; if (selectedNode) { transformerRef.current.nodes([selectedNode]); transformerRef.current.getLayer().batchDraw(); } else { transformerRef.current.nodes([]); } } }, [selectedElementId, activeElements]); return (
{activeElements.map((element) => { const isSelected = selectedElementId === element.id; if (element.type === 'video') { const imageSource = getImageSource(element, videoStates, isPlaying); if (!imageSource) { return null; } return ( { if (node) { elementRefs.current[element.id] = node; } }} image={imageSource} // Use center position for x,y when offset is set x={element.x + element.width / 2} y={element.y + element.height / 2} width={element.width} height={element.height} // Set offset to center for proper rotation offsetX={element.width / 2} offsetY={element.height / 2} rotation={element.rotation || 0} draggable dragBoundFunc={createDragBoundFunc(element.id)} onClick={() => handleElementSelect(element.id)} onTap={() => handleElementSelect(element.id)} onDragMove={(e) => handleDragMove(element.id, e)} onDragEnd={(e) => handleDragEnd(element.id, e)} onTransform={() => handleTransform(element.id)} // Visual feedback for selection stroke={isSelected ? '#0066ff' : undefined} strokeWidth={isSelected ? 2 : 0} strokeScaleEnabled={false} /> ); } else if (element.type === 'text') { return ( { if (node) { elementRefs.current[element.id] = node; } }} text={element.text} x={element.x} y={element.y} fontSize={element.fontSize} fontStyle={getTextFontStyle(element)} fontFamily={element.fontFamily || 'Arial'} fill={element.fill || '#ffffff'} stroke={element.strokeWidth > 0 ? element.stroke || '#000000' : undefined} strokeWidth={element.strokeWidth * 3 || 0} fillAfterStrokeEnabled={true} strokeScaleEnabled={false} rotation={element.rotation || 0} // Center the text horizontally align="center" verticalAlign="middle" // Let text have natural width and height for multiline support wrap="word" // Always scale 1 - size changes go through fontSize scaleX={1} scaleY={1} draggable dragBoundFunc={createDragBoundFunc(element.id)} onClick={() => handleElementSelect(element.id)} onTap={() => handleElementSelect(element.id)} onDragMove={(e) => handleDragMove(element.id, e)} onDragEnd={(e) => handleDragEnd(element.id, e)} onTransform={() => handleTransform(element.id)} // Apply fixedWidth and offsetX if they exist width={element.fixedWidth} offsetX={element.offsetX} // Visual feedback for selection /> ); } else if (element.type === 'image' && element.imageElement && element.isImageReady) { return ( { if (node) { elementRefs.current[element.id] = node; } }} image={element.imageElement} // Use center position for x,y when offset is set x={element.x + element.width / 2} y={element.y + element.height / 2} width={element.width} height={element.height} // Set offset to center for proper rotation offsetX={element.width / 2} offsetY={element.height / 2} rotation={element.rotation || 0} draggable dragBoundFunc={createDragBoundFunc(element.id)} onClick={() => handleElementSelect(element.id)} onTap={() => handleElementSelect(element.id)} onDragMove={(e) => handleDragMove(element.id, e)} onDragEnd={(e) => handleDragEnd(element.id, e)} onTransform={() => handleTransform(element.id)} // Visual feedback for selection stroke={isSelected ? '#0066ff' : undefined} strokeWidth={isSelected ? 2 : 0} strokeScaleEnabled={false} /> ); } return null; })} {/* Guide Lines Layer */} {guideLines.showVertical && ( )} {guideLines.showHorizontal && ( )} {/* Transformer for selected element */} { // Limit resize to prevent elements from becoming too small if (newBox.width < 20 || newBox.height < 20) { return oldBox; } return newBox; }} // Transformer styling - Figma-like appearance borderStroke="#0066ff" borderStrokeWidth={2} anchorStroke="#0066ff" anchorFill="white" anchorSize={14} anchorCornerRadius={2} // Enable only corner anchors for aspect ratio enabledAnchors={['top-left', 'top-right', 'bottom-right', 'bottom-left']} // Rotation handle rotateAnchorOffset={30} // Built-in Konva rotation snapping rotationSnaps={[0, 90, 180, 270]} rotationSnapTolerance={8} // Clear guide lines when transform ends onTransformEnd={clearGuideLines} // Style the rotation anchor to be circular anchorStyleFunc={(anchor) => { if (anchor.hasName('.rotater')) { // Make it circular by setting corner radius to half the width anchor.cornerRadius(anchor.width() / 2); anchor.fill('#0066ff'); anchor.stroke('white'); anchor.strokeWidth(1); } }} />
); }; export default VideoPreview;