This commit is contained in:
ct
2025-06-17 20:50:27 +08:00
parent 090182247f
commit 933e12d7fb
6 changed files with 192 additions and 25 deletions

View File

@@ -4,7 +4,7 @@ import { createInertiaApp } from '@inertiajs/react';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { ErrorBoundary } from 'react-error-boundary'; 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 { initializeTheme } from './hooks/use-appearance';
import { AxiosProvider } from './plugins/AxiosContext'; import { AxiosProvider } from './plugins/AxiosContext';
import { MittProvider } from './plugins/MittContext'; import { MittProvider } from './plugins/MittContext';

View File

@@ -60,7 +60,9 @@ const sampleTimelineElements = [
x: 50, x: 50,
y: 600, y: 600,
fontSize: 24, fontSize: 24,
fontWeight: 'bold', // ADD THIS LINE fontWeight: 'bold',
fontFamily: 'Montserrat',
fontStyle: 'normal',
fill: 'white', fill: 'white',
stroke: 'black', stroke: 'black',
strokeWidth: 1, strokeWidth: 1,
@@ -76,7 +78,9 @@ const sampleTimelineElements = [
x: 50, x: 50,
y: 650, y: 650,
fontSize: 20, fontSize: 20,
fontWeight: 'bold', // ADD THIS LINE fontWeight: 'bold',
fontFamily: 'Montserrat',
fontStyle: 'normal',
fill: 'yellow', fill: 'yellow',
stroke: 'red', stroke: 'red',
strokeWidth: 2, strokeWidth: 2,

View File

@@ -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( const handleElementUpdate = useCallback(
(elementId, updates) => { (elementId, updates) => {
setTimelineElements((prev) => setTimelineElements((prev) =>
@@ -553,7 +553,7 @@ const VideoEditor = ({ width, height }) => {
handleSeek={handleSeek} handleSeek={handleSeek}
copyFFmpegCommand={copyFFmpegCommand} copyFFmpegCommand={copyFFmpegCommand}
exportVideo={exportVideo} exportVideo={exportVideo}
onElementUpdate={handleElementUpdate} // NEW: Pass the update handler onElementUpdate={handleElementUpdate}
layerRef={layerRef} layerRef={layerRef}
/> />
</div> </div>

View File

@@ -2,6 +2,22 @@ import { FFmpeg } from '@ffmpeg/ffmpeg';
import { fetchFile, toBlobURL } from '@ffmpeg/util'; import { fetchFile, toBlobURL } from '@ffmpeg/util';
import { useCallback, useMemo, useState } from 'react'; 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 useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
const [showConsoleLogs] = useState(false); const [showConsoleLogs] = useState(false);
@@ -9,6 +25,25 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
const [exportProgress, setExportProgress] = useState(0); const [exportProgress, setExportProgress] = useState(0);
const [exportStatus, setExportStatus] = useState(''); 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( const generateFFmpegCommand = useCallback(
(is_string = true, useLocalFiles = false) => { (is_string = true, useLocalFiles = false) => {
showConsoleLogs && console.log('🎬 STARTING FFmpeg generation'); 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('Videos found:', videos.length);
showConsoleLogs && console.log('Images found:', images.length); showConsoleLogs && console.log('Images found:', images.length);
showConsoleLogs && console.log('Texts found:', texts.length);
if (videos.length === 0 && images.length === 0) { if (videos.length === 0 && images.length === 0) {
if (is_string) { if (is_string) {
@@ -98,16 +134,20 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
showConsoleLogs && console.log('🎵 Audio args:', audioArgs); showConsoleLogs && console.log('🎵 Audio args:', audioArgs);
// Process text elements with centering // Process text elements with proper font support
texts.forEach((t, i) => { texts.forEach((t, i) => {
const escapedText = t.text.replace(/'/g, is_string ? "\\'" : "'").replace(/:/g, '\\:'); 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 // Center the text: x position is the center point, y is adjusted for baseline
const centerX = Math.round(t.x); const centerX = Math.round(t.x);
const centerY = Math.round(t.y + t.fontSize * 0.3); // Adjust for text baseline const centerY = Math.round(t.y + t.fontSize * 0.3); // Adjust for text baseline
filters.push( 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 t.stroke
}:text_align=center: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}]`,
); );
@@ -137,7 +177,6 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
if (is_string) { if (is_string) {
let inputStrings = []; let inputStrings = [];
let inputIdx = 0;
videos.forEach((v, i) => { videos.forEach((v, i) => {
inputStrings.push(`-i "${useLocalFiles ? `input_video_${i}.webm` : v.source_webm}"`); inputStrings.push(`-i "${useLocalFiles ? `input_video_${i}.webm` : v.source_webm}"`);
@@ -209,12 +248,49 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
wasmURL: wasmBlobURL, wasmURL: wasmBlobURL,
}); });
showConsoleLogs && console.log('FFmpeg loaded!'); showConsoleLogs && console.log('FFmpeg loaded!');
setExportProgress(20); setExportProgress(10);
setExportStatus('Loading font...'); setExportStatus('Loading fonts...');
await ffmpeg.writeFile('arial.ttf', await fetchFile('https://raw.githubusercontent.com/ffmpegwasm/testdata/master/arial.ttf'));
showConsoleLogs && console.log('Font loaded!'); // Load all required fonts
setExportProgress(30); 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...'); setExportStatus('Downloading media...');
const videos = timelineElements.filter((el) => el.type === 'video'); 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++) { for (let i = 0; i < videos.length; i++) {
await ffmpeg.writeFile(`input_video_${i}.webm`, await fetchFile(videos[i].source_webm)); await ffmpeg.writeFile(`input_video_${i}.webm`, await fetchFile(videos[i].source_webm));
mediaProgress++; mediaProgress++;
setExportProgress(30 + Math.round((mediaProgress / totalMedia) * 30)); setExportProgress(20 + Math.round((mediaProgress / totalMedia) * 40));
} }
// Download images // Download images
for (let i = 0; i < images.length; i++) { for (let i = 0; i < images.length; i++) {
await ffmpeg.writeFile(`input_image_${i}.jpg`, await fetchFile(images[i].source)); await ffmpeg.writeFile(`input_image_${i}.jpg`, await fetchFile(images[i].source));
mediaProgress++; mediaProgress++;
setExportProgress(30 + Math.round((mediaProgress / totalMedia) * 30)); setExportProgress(20 + Math.round((mediaProgress / totalMedia) * 40));
} }
setExportStatus('Processing video...'); setExportStatus('Processing video...');

View File

@@ -71,6 +71,22 @@ const VideoPreview = ({
return null; 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 // Check if element uses center-offset positioning
const usesCenterPositioning = (elementType) => { const usesCenterPositioning = (elementType) => {
return elementType === 'video' || elementType === 'image'; return elementType === 'video' || elementType === 'image';
@@ -445,8 +461,8 @@ const VideoPreview = ({
x={element.x} x={element.x}
y={element.y} y={element.y}
fontSize={element.fontSize} fontSize={element.fontSize}
fontStyle={element.fontWeight === 'bold' || element.fontWeight === 700 ? 'bold' : 'normal'} // ADD THIS LINE fontStyle={getTextFontStyle(element)}
fontFamily="Arial" fontFamily={element.fontFamily || 'Arial'}
fill={element.fill} fill={element.fill}
stroke={element.stroke} stroke={element.stroke}
strokeWidth={element.strokeWidth} strokeWidth={element.strokeWidth}

View File

@@ -1,29 +1,48 @@
import { Button } from '@/components/ui/button'; 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 { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { useMitt } from '@/plugins/MittContext'; import { useMitt } from '@/plugins/MittContext';
import useVideoEditorStore from '@/stores/VideoEditorStore'; 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'; 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 }) { export default function TextSidebar({ isOpen, onClose }) {
const { selectedTextElement } = useVideoEditorStore(); const { selectedTextElement } = useVideoEditorStore();
const emitter = useMitt(); const emitter = useMitt();
const [textValue, setTextValue] = useState(''); const [textValue, setTextValue] = useState('');
const [fontSize, setFontSize] = useState(24); // Default font size const [fontSize, setFontSize] = useState(24);
const [isBold, setIsBold] = useState(true); // Default to bold const [isBold, setIsBold] = useState(true);
const [isItalic, setIsItalic] = useState(false);
const [fontFamily, setFontFamily] = useState('Montserrat');
// Font size constraints // Font size constraints
const MIN_FONT_SIZE = 8; const MIN_FONT_SIZE = 8;
const MAX_FONT_SIZE = 120; const MAX_FONT_SIZE = 120;
const FONT_SIZE_STEP = 2; const FONT_SIZE_STEP = 2;
// Update textarea, fontSize, and bold when selected element changes // Update state when selected element changes
useEffect(() => { useEffect(() => {
if (selectedTextElement) { if (selectedTextElement) {
setTextValue(selectedTextElement.text || ''); setTextValue(selectedTextElement.text || '');
setFontSize(selectedTextElement.fontSize || 24); 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]); }, [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 // Handle bold toggle
const handleBoldToggle = () => { const handleBoldToggle = () => {
const newBoldState = !isBold; 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 // Increase font size
const increaseFontSize = () => { const increaseFontSize = () => {
handleFontSizeChange(fontSize + FONT_SIZE_STEP); handleFontSizeChange(fontSize + FONT_SIZE_STEP);
@@ -101,6 +145,23 @@ export default function TextSidebar({ isOpen, onClose }) {
/> />
</div> </div>
{/* Font Family */}
<div>
<label className="text-sm font-medium">Font Family</label>
<Select value={fontFamily} onValueChange={handleFontFamilyChange}>
<SelectTrigger className="mt-2">
<SelectValue placeholder="Select font" />
</SelectTrigger>
<SelectContent>
{AVAILABLE_FONTS.map((font) => (
<SelectItem key={font.value} value={font.value}>
<span style={{ fontFamily: font.name }}>{font.name}</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Font Size Controls */} {/* Font Size Controls */}
<div> <div>
<label className="text-sm font-medium">Font Size</label> <label className="text-sm font-medium">Font Size</label>
@@ -140,15 +201,25 @@ export default function TextSidebar({ isOpen, onClose }) {
{/* Font Style Controls */} {/* Font Style Controls */}
<div> <div>
<label className="text-sm font-medium">Font Style</label> <label className="text-sm font-medium">Font Style</label>
<div className="mt-2"> <div className="mt-2 flex gap-2">
<Button <Button
variant={isBold ? 'default' : 'outline'} variant={isBold ? 'default' : 'outline'}
size="sm" size="sm"
onClick={handleBoldToggle} onClick={handleBoldToggle}
className="flex w-full items-center gap-2" className="flex flex-1 items-center gap-2"
> >
<Bold className="h-4 w-4" /> <Bold className="h-4 w-4" />
<span className={isBold ? 'font-bold' : 'font-normal'}>{isBold ? 'Bold' : 'Normal'}</span> <span className={isBold ? 'font-bold' : 'font-normal'}>Bold</span>
</Button>
<Button
variant={isItalic ? 'default' : 'outline'}
size="sm"
onClick={handleItalicToggle}
className="flex flex-1 items-center gap-2"
>
<Italic className="h-4 w-4" />
<span className={isItalic ? 'italic' : 'not-italic'}>Italic</span>
</Button> </Button>
</div> </div>
</div> </div>