From ef2871a983854d326a6759bd5ad3c4ff10513c8b Mon Sep 17 00:00:00 2001 From: ct Date: Mon, 16 Jun 2025 23:16:10 +0800 Subject: [PATCH] Update --- resources/js/modules/editor/editor.jsx | 30 ++++++++- .../editor/partials/canvas/video-editor.jsx | 6 +- .../editor/partials/canvas/video-export.jsx | 11 +-- .../editor/partials/canvas/video-preview.jsx | 39 ++++++++--- .../modules/editor/partials/text-sidebar.jsx | 67 +++++++++++++++++++ resources/js/stores/VideoEditorStore.js | 8 ++- 6 files changed, 142 insertions(+), 19 deletions(-) create mode 100644 resources/js/modules/editor/partials/text-sidebar.jsx diff --git a/resources/js/modules/editor/editor.jsx b/resources/js/modules/editor/editor.jsx index d7d2063..9989d15 100644 --- a/resources/js/modules/editor/editor.jsx +++ b/resources/js/modules/editor/editor.jsx @@ -1,12 +1,15 @@ import { useEffect, useLayoutEffect, useState } from 'react'; +import { useMitt } from '@/plugins/MittContext'; import useLocalSettingsStore from '@/stores/localSettingsStore'; import useMediaStore from '@/stores/MediaStore'; +import useVideoEditorStore from '@/stores/VideoEditorStore'; import EditNavSidebar from './partials/edit-nav-sidebar'; import EditSidebar from './partials/edit-sidebar'; import EditorCanvas from './partials/editor-canvas'; import EditorControls from './partials/editor-controls'; import EditorHeader from './partials/editor-header'; +import TextSidebar from './partials/text-sidebar'; import { calculateOptimalMaxWidth, calculateResponsiveWidth } from './utils/layout-constants'; // Hook to detect if viewport is below minimum width @@ -101,11 +104,13 @@ const useResponsiveDimensions = () => { const Editor = () => { const { init } = useMediaStore(); - const { getSetting } = useLocalSettingsStore(); + const { setSelectedTextElement } = useVideoEditorStore(); + const emitter = useMitt(); const [isEditNavSidebarOpen, setIsEditNavSidebarOpen] = useState(false); const [isEditSidebarOpen, setIsEditSidebarOpen] = useState(false); + const [isTextSidebarOpen, setIsTextSidebarOpen] = useState(false); const [isMuted, setIsMuted] = useState(true); // Video starts muted by default const { maxWidth, responsiveWidth } = useResponsiveDimensions(); const isBelowMinWidth = useViewportDetection(320); @@ -114,6 +119,23 @@ const Editor = () => { init(); }, []); + // Listen for text element selection + useEffect(() => { + const handleTextElementSelected = (textElement) => { + setSelectedTextElement(textElement); + setIsTextSidebarOpen(true); + // Close other sidebars when text sidebar opens + setIsEditSidebarOpen(false); + setIsEditNavSidebarOpen(false); + }; + + emitter.on('text-element-selected', handleTextElementSelected); + + return () => { + emitter.off('text-element-selected', handleTextElementSelected); + }; + }, [emitter, setSelectedTextElement]); + const handleEditNavClick = () => { setIsEditNavSidebarOpen(!isEditNavSidebarOpen); }; @@ -130,6 +152,11 @@ const Editor = () => { setIsEditSidebarOpen(false); }; + const handleTextSidebarClose = () => { + setIsTextSidebarOpen(false); + setSelectedTextElement(null); + }; + // Toggle mute functionality const handleToggleMute = () => { setIsMuted(!isMuted); @@ -139,6 +166,7 @@ const Editor = () => {
+ { emitter.on('video-play', handlePlay); emitter.on('video-reset', handleReset); emitter.on('video-seek', handleSeek); + emitter.on('text-update', ({ elementId, updates }) => { + handleElementUpdate(elementId, updates); + }); return () => { emitter.off('video-play', handlePlay); emitter.off('video-reset', handleReset); emitter.off('video-seek', handleSeek); + emitter.off('text-update'); }; - }, [emitter, handlePlay, handleReset, handleSeek]); + }, [emitter, handlePlay, handleReset, handleSeek, handleElementUpdate]); return (
diff --git a/resources/js/modules/editor/partials/canvas/video-export.jsx b/resources/js/modules/editor/partials/canvas/video-export.jsx index 9fa76a7..e6b9ae9 100644 --- a/resources/js/modules/editor/partials/canvas/video-export.jsx +++ b/resources/js/modules/editor/partials/canvas/video-export.jsx @@ -98,15 +98,18 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => { showConsoleLogs && console.log('🎵 Audio args:', audioArgs); + // Process text elements with centering texts.forEach((t, i) => { const escapedText = t.text.replace(/'/g, is_string ? "\\'" : "'").replace(/:/g, '\\:'); + // Center the text: x position is the center point, y is adjusted for baseline + const centerX = Math.round(t.x); + const centerY = Math.round(t.y + t.fontSize * 0.3); // Adjust for text baseline + filters.push( - `[${videoLayer}]drawtext=fontfile=/arial.ttf:text='${escapedText}':x=${Math.round( - t.x, - )}:y=${Math.round(t.y)}:fontsize=${t.fontSize}:fontcolor=${t.fill}:borderw=${t.strokeWidth}:bordercolor=${ + `[${videoLayer}]drawtext=fontfile=/arial.ttf:text='${escapedText}':x=${centerX}:y=${centerY}:fontsize=${t.fontSize}:fontcolor=${t.fill}:borderw=${t.strokeWidth}:bordercolor=${ t.stroke - }:enable='between(t,${t.startTime},${t.startTime + t.duration})'[t${i}]`, + }:text_align=center:enable='between(t,${t.startTime},${t.startTime + t.duration})'[t${i}]`, ); videoLayer = `t${i}`; }); diff --git a/resources/js/modules/editor/partials/canvas/video-preview.jsx b/resources/js/modules/editor/partials/canvas/video-preview.jsx index 05641a3..e35a3d8 100644 --- a/resources/js/modules/editor/partials/canvas/video-preview.jsx +++ b/resources/js/modules/editor/partials/canvas/video-preview.jsx @@ -1,3 +1,4 @@ +import { useMitt } from '@/plugins/MittContext'; import { useCallback, useEffect, useRef, useState } from 'react'; import { Image, Layer, Line, Stage, Text, Transformer } from 'react-konva'; @@ -36,6 +37,8 @@ const VideoPreview = ({ // Refs layerRef, }) => { + const emitter = useMitt(); + // Selection state const [selectedElementId, setSelectedElementId] = useState(null); const transformerRef = useRef(null); @@ -116,16 +119,28 @@ const VideoPreview = ({ }; // Handle element selection - const handleElementSelect = useCallback((elementId) => { - setSelectedElementId(elementId); - // Clear guide lines when selecting - setGuideLines({ - vertical: null, - horizontal: null, - showVertical: false, - showHorizontal: false, - }); - }, []); + const handleElementSelect = useCallback( + (elementId) => { + setSelectedElementId(elementId); + + // Find the selected element + const element = timelineElements.find((el) => el.id === elementId); + + // If it's a text element, emit text-element-selected event + if (element && element.type === 'text') { + emitter.emit('text-element-selected', element); + } + + // Clear guide lines when selecting + setGuideLines({ + vertical: null, + horizontal: null, + showVertical: false, + showHorizontal: false, + }); + }, + [emitter, timelineElements], + ); // Handle clicking on empty space to deselect const handleStageClick = useCallback((e) => { @@ -434,6 +449,10 @@ const VideoPreview = ({ stroke={element.stroke} strokeWidth={element.strokeWidth} rotation={element.rotation || 0} + // Center the text horizontally + align="center" + // Let text have natural width and height for multiline support + wrap="word" draggable dragBoundFunc={createDragBoundFunc(element.id)} onClick={() => handleElementSelect(element.id)} diff --git a/resources/js/modules/editor/partials/text-sidebar.jsx b/resources/js/modules/editor/partials/text-sidebar.jsx new file mode 100644 index 0000000..bcaba10 --- /dev/null +++ b/resources/js/modules/editor/partials/text-sidebar.jsx @@ -0,0 +1,67 @@ +import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet'; +import { Textarea } from '@/components/ui/textarea'; +import { useMitt } from '@/plugins/MittContext'; +import useVideoEditorStore from '@/stores/VideoEditorStore'; +import { Type } from 'lucide-react'; +import { useEffect, useState } from 'react'; + +export default function TextSidebar({ isOpen, onClose }) { + const { selectedTextElement } = useVideoEditorStore(); + const emitter = useMitt(); + const [textValue, setTextValue] = useState(''); + + // Update textarea when selected element changes + useEffect(() => { + if (selectedTextElement) { + setTextValue(selectedTextElement.text || ''); + } + }, [selectedTextElement]); + + // Handle text changes + const handleTextChange = (e) => { + const newText = e.target.value; + setTextValue(newText); + + if (selectedTextElement) { + emitter.emit('text-update', { + elementId: selectedTextElement.id, + updates: { text: newText }, + }); + } + }; + + return ( + !open && onClose()}> + + + + + Edit Text + + + +
+ {selectedTextElement ? ( + <> +
+ +