diff --git a/resources/js/app.tsx b/resources/js/app.tsx index d27b0f7..9a8aaf6 100644 --- a/resources/js/app.tsx +++ b/resources/js/app.tsx @@ -4,7 +4,7 @@ import { createInertiaApp } from '@inertiajs/react'; import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; import { createRoot } from 'react-dom/client'; import { ErrorBoundary } from 'react-error-boundary'; -import DetailedErrorFallback from './components/DetailedErrorFallback'; // Import your component +import DetailedErrorFallback from './components/custom/detailed-error-fallback'; // Import your component import { initializeTheme } from './hooks/use-appearance'; import { AxiosProvider } from './plugins/AxiosContext'; import { MittProvider } from './plugins/MittContext'; diff --git a/resources/js/modules/editor/partials/canvas/sample-timeline-data.jsx b/resources/js/modules/editor/partials/canvas/sample-timeline-data.jsx index 5eeec54..b9fb083 100644 --- a/resources/js/modules/editor/partials/canvas/sample-timeline-data.jsx +++ b/resources/js/modules/editor/partials/canvas/sample-timeline-data.jsx @@ -60,7 +60,9 @@ const sampleTimelineElements = [ x: 50, y: 600, fontSize: 24, - fontWeight: 'bold', // ADD THIS LINE + fontWeight: 'bold', + fontFamily: 'Montserrat', + fontStyle: 'normal', fill: 'white', stroke: 'black', strokeWidth: 1, @@ -76,7 +78,9 @@ const sampleTimelineElements = [ x: 50, y: 650, fontSize: 20, - fontWeight: 'bold', // ADD THIS LINE + fontWeight: 'bold', + fontFamily: 'Montserrat', + fontStyle: 'normal', fill: 'yellow', stroke: 'red', strokeWidth: 2, diff --git a/resources/js/modules/editor/partials/canvas/video-editor.jsx b/resources/js/modules/editor/partials/canvas/video-editor.jsx index 2fa8754..78a9955 100644 --- a/resources/js/modules/editor/partials/canvas/video-editor.jsx +++ b/resources/js/modules/editor/partials/canvas/video-editor.jsx @@ -70,7 +70,7 @@ const VideoEditor = ({ width, height }) => { }); }; - // NEW: Handle element transformations (position, scale, rotation) + // Handle element transformations (position, scale, rotation) and text properties const handleElementUpdate = useCallback( (elementId, updates) => { setTimelineElements((prev) => @@ -553,7 +553,7 @@ const VideoEditor = ({ width, height }) => { handleSeek={handleSeek} copyFFmpegCommand={copyFFmpegCommand} exportVideo={exportVideo} - onElementUpdate={handleElementUpdate} // NEW: Pass the update handler + onElementUpdate={handleElementUpdate} layerRef={layerRef} /> diff --git a/resources/js/modules/editor/partials/canvas/video-export.jsx b/resources/js/modules/editor/partials/canvas/video-export.jsx index e6b9ae9..030c0e2 100644 --- a/resources/js/modules/editor/partials/canvas/video-export.jsx +++ b/resources/js/modules/editor/partials/canvas/video-export.jsx @@ -2,6 +2,22 @@ import { FFmpeg } from '@ffmpeg/ffmpeg'; import { fetchFile, toBlobURL } from '@ffmpeg/util'; import { useCallback, useMemo, useState } from 'react'; +// Font configuration mapping +const FONT_CONFIG = { + Montserrat: { + normal: '/fonts/Montserrat/static/Montserrat-Regular.ttf', + bold: '/fonts/Montserrat/static/Montserrat-Bold.ttf', + italic: '/fonts/Montserrat/static/Montserrat-Italic.ttf', + boldItalic: '/fonts/Montserrat/static/Montserrat-BoldItalic.ttf', + }, + Arial: { + normal: '/arial.ttf', + bold: '/arial.ttf', + italic: '/arial.ttf', + boldItalic: '/arial.ttf', + }, +}; + const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => { const [showConsoleLogs] = useState(false); @@ -9,6 +25,25 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => { const [exportProgress, setExportProgress] = useState(0); const [exportStatus, setExportStatus] = useState(''); + // Helper function to get font file path based on font family and style + const getFontFilePath = (fontFamily, fontWeight, fontStyle) => { + const family = fontFamily || 'Arial'; + const config = FONT_CONFIG[family] || FONT_CONFIG.Arial; + + const isBold = fontWeight === 'bold' || fontWeight === 700; + const isItalic = fontStyle === 'italic'; + + if (isBold && isItalic) { + return config.boldItalic; + } else if (isBold) { + return config.bold; + } else if (isItalic) { + return config.italic; + } else { + return config.normal; + } + }; + const generateFFmpegCommand = useCallback( (is_string = true, useLocalFiles = false) => { showConsoleLogs && console.log('🎬 STARTING FFmpeg generation'); @@ -19,6 +54,7 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => { showConsoleLogs && console.log('Videos found:', videos.length); showConsoleLogs && console.log('Images found:', images.length); + showConsoleLogs && console.log('Texts found:', texts.length); if (videos.length === 0 && images.length === 0) { if (is_string) { @@ -98,16 +134,20 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => { showConsoleLogs && console.log('🎵 Audio args:', audioArgs); - // Process text elements with centering + // Process text elements with proper font support texts.forEach((t, i) => { const escapedText = t.text.replace(/'/g, is_string ? "\\'" : "'").replace(/:/g, '\\:'); + // Get the appropriate font file path + const fontFilePath = getFontFilePath(t.fontFamily, t.fontWeight, t.fontStyle); + const fontFileName = fontFilePath.split('/').pop(); + // 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=${centerX}:y=${centerY}:fontsize=${t.fontSize}:fontcolor=${t.fill}:borderw=${t.strokeWidth}:bordercolor=${ + `[${videoLayer}]drawtext=fontfile=/${fontFileName}:text='${escapedText}':x=${centerX}:y=${centerY}:fontsize=${t.fontSize}:fontcolor=${t.fill}:borderw=${t.strokeWidth}:bordercolor=${ t.stroke }:text_align=center:enable='between(t,${t.startTime},${t.startTime + t.duration})'[t${i}]`, ); @@ -137,7 +177,6 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => { if (is_string) { let inputStrings = []; - let inputIdx = 0; videos.forEach((v, i) => { inputStrings.push(`-i "${useLocalFiles ? `input_video_${i}.webm` : v.source_webm}"`); @@ -209,12 +248,49 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => { wasmURL: wasmBlobURL, }); showConsoleLogs && console.log('FFmpeg loaded!'); - setExportProgress(20); + setExportProgress(10); - setExportStatus('Loading font...'); - await ffmpeg.writeFile('arial.ttf', await fetchFile('https://raw.githubusercontent.com/ffmpegwasm/testdata/master/arial.ttf')); - showConsoleLogs && console.log('Font loaded!'); - setExportProgress(30); + setExportStatus('Loading fonts...'); + + // Load all required fonts + const fontsToLoad = new Set(); + + // Add Arial font (fallback) + fontsToLoad.add('arial.ttf'); + + // Add fonts used by text elements + timelineElements + .filter((el) => el.type === 'text') + .forEach((text) => { + const fontFilePath = getFontFilePath(text.fontFamily, text.fontWeight, text.fontStyle); + const fontFileName = fontFilePath.split('/').pop(); + fontsToLoad.add(fontFileName); + }); + + // Load each unique font + let fontProgress = 0; + for (const fontFile of fontsToLoad) { + try { + if (fontFile === 'arial.ttf') { + await ffmpeg.writeFile( + 'arial.ttf', + await fetchFile('https://raw.githubusercontent.com/ffmpegwasm/testdata/master/arial.ttf'), + ); + } else { + // Load Montserrat fonts from local filesystem + const fontPath = `/fonts/Montserrat/static/${fontFile}`; + await ffmpeg.writeFile(fontFile, await fetchFile(fontPath)); + } + fontProgress++; + setExportProgress(10 + Math.round((fontProgress / fontsToLoad.size) * 10)); + } catch (error) { + console.warn(`Failed to load font ${fontFile}, falling back to arial.ttf:`, error); + // If font loading fails, we'll use arial.ttf as fallback + } + } + + showConsoleLogs && console.log('Fonts loaded!'); + setExportProgress(20); setExportStatus('Downloading media...'); const videos = timelineElements.filter((el) => el.type === 'video'); @@ -227,14 +303,14 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => { for (let i = 0; i < videos.length; i++) { await ffmpeg.writeFile(`input_video_${i}.webm`, await fetchFile(videos[i].source_webm)); mediaProgress++; - setExportProgress(30 + Math.round((mediaProgress / totalMedia) * 30)); + setExportProgress(20 + Math.round((mediaProgress / totalMedia) * 40)); } // Download images for (let i = 0; i < images.length; i++) { await ffmpeg.writeFile(`input_image_${i}.jpg`, await fetchFile(images[i].source)); mediaProgress++; - setExportProgress(30 + Math.round((mediaProgress / totalMedia) * 30)); + setExportProgress(20 + Math.round((mediaProgress / totalMedia) * 40)); } setExportStatus('Processing video...'); diff --git a/resources/js/modules/editor/partials/canvas/video-preview.jsx b/resources/js/modules/editor/partials/canvas/video-preview.jsx index 56f1f6a..33d8b07 100644 --- a/resources/js/modules/editor/partials/canvas/video-preview.jsx +++ b/resources/js/modules/editor/partials/canvas/video-preview.jsx @@ -71,6 +71,22 @@ const VideoPreview = ({ return null; }; + // Helper function to get font style for text elements + const getTextFontStyle = (element) => { + const isBold = element.fontWeight === 'bold' || element.fontWeight === 700; + const isItalic = element.fontStyle === 'italic'; + + if (isBold && isItalic) { + return 'bold italic'; + } else if (isBold) { + return 'bold'; + } else if (isItalic) { + return 'italic'; + } else { + return 'normal'; + } + }; + // Check if element uses center-offset positioning const usesCenterPositioning = (elementType) => { return elementType === 'video' || elementType === 'image'; @@ -445,8 +461,8 @@ const VideoPreview = ({ x={element.x} y={element.y} fontSize={element.fontSize} - fontStyle={element.fontWeight === 'bold' || element.fontWeight === 700 ? 'bold' : 'normal'} // ADD THIS LINE - fontFamily="Arial" + fontStyle={getTextFontStyle(element)} + fontFamily={element.fontFamily || 'Arial'} fill={element.fill} stroke={element.stroke} strokeWidth={element.strokeWidth} diff --git a/resources/js/modules/editor/partials/text-sidebar.jsx b/resources/js/modules/editor/partials/text-sidebar.jsx index 443b55c..e22b1ae 100644 --- a/resources/js/modules/editor/partials/text-sidebar.jsx +++ b/resources/js/modules/editor/partials/text-sidebar.jsx @@ -1,29 +1,48 @@ import { Button } from '@/components/ui/button'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; 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 { Bold, Minus, Plus, Type } from 'lucide-react'; +import { Bold, Italic, Minus, Plus, Type } from 'lucide-react'; import { useEffect, useState } from 'react'; +// Font configuration - extensible for adding more fonts +const AVAILABLE_FONTS = [ + { + name: 'Montserrat', + value: 'Montserrat', + fontFiles: { + normal: '/fonts/Montserrat/static/Montserrat-Regular.ttf', + bold: '/fonts/Montserrat/static/Montserrat-Bold.ttf', + italic: '/fonts/Montserrat/static/Montserrat-Italic.ttf', + boldItalic: '/fonts/Montserrat/static/Montserrat-BoldItalic.ttf', + }, + }, +]; + export default function TextSidebar({ isOpen, onClose }) { const { selectedTextElement } = useVideoEditorStore(); const emitter = useMitt(); const [textValue, setTextValue] = useState(''); - const [fontSize, setFontSize] = useState(24); // Default font size - const [isBold, setIsBold] = useState(true); // Default to bold + const [fontSize, setFontSize] = useState(24); + const [isBold, setIsBold] = useState(true); + const [isItalic, setIsItalic] = useState(false); + const [fontFamily, setFontFamily] = useState('Montserrat'); // Font size constraints const MIN_FONT_SIZE = 8; const MAX_FONT_SIZE = 120; const FONT_SIZE_STEP = 2; - // Update textarea, fontSize, and bold when selected element changes + // Update state when selected element changes useEffect(() => { if (selectedTextElement) { setTextValue(selectedTextElement.text || ''); setFontSize(selectedTextElement.fontSize || 24); - setIsBold(selectedTextElement.fontWeight === 'bold' || selectedTextElement.fontWeight === 700 || true); // Default to bold if not set + setIsBold(selectedTextElement.fontWeight === 'bold' || selectedTextElement.fontWeight === 700 || true); + setIsItalic(selectedTextElement.fontStyle === 'italic' || false); + setFontFamily(selectedTextElement.fontFamily || 'Montserrat'); } }, [selectedTextElement]); @@ -53,6 +72,18 @@ export default function TextSidebar({ isOpen, onClose }) { } }; + // Handle font family changes + const handleFontFamilyChange = (newFontFamily) => { + setFontFamily(newFontFamily); + + if (selectedTextElement) { + emitter.emit('text-update', { + elementId: selectedTextElement.id, + updates: { fontFamily: newFontFamily }, + }); + } + }; + // Handle bold toggle const handleBoldToggle = () => { const newBoldState = !isBold; @@ -66,6 +97,19 @@ export default function TextSidebar({ isOpen, onClose }) { } }; + // Handle italic toggle + const handleItalicToggle = () => { + const newItalicState = !isItalic; + setIsItalic(newItalicState); + + if (selectedTextElement) { + emitter.emit('text-update', { + elementId: selectedTextElement.id, + updates: { fontStyle: newItalicState ? 'italic' : 'normal' }, + }); + } + }; + // Increase font size const increaseFontSize = () => { handleFontSizeChange(fontSize + FONT_SIZE_STEP); @@ -101,6 +145,23 @@ export default function TextSidebar({ isOpen, onClose }) { /> + {/* Font Family */} +
+ + +
+ {/* Font Size Controls */}
@@ -140,15 +201,25 @@ export default function TextSidebar({ isOpen, onClose }) { {/* Font Style Controls */}
-
+
+ +