Update
This commit is contained in:
@@ -63,9 +63,9 @@ const sampleTimelineElements = [
|
|||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
fontFamily: 'Montserrat',
|
fontFamily: 'Montserrat',
|
||||||
fontStyle: 'normal',
|
fontStyle: 'normal',
|
||||||
fill: 'white',
|
fill: '#ffffff',
|
||||||
stroke: 'black',
|
stroke: '#000000',
|
||||||
strokeWidth: 1,
|
strokeWidth: 3,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -81,8 +81,8 @@ const sampleTimelineElements = [
|
|||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
fontFamily: 'Montserrat',
|
fontFamily: 'Montserrat',
|
||||||
fontStyle: 'normal',
|
fontStyle: 'normal',
|
||||||
fill: 'yellow',
|
fill: '#ffff00',
|
||||||
stroke: 'red',
|
stroke: '#ff0000',
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -44,6 +44,25 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper function to convert color format for FFmpeg
|
||||||
|
const formatColorForFFmpeg = (color) => {
|
||||||
|
// Handle hex colors (e.g., #ffffff or #fff)
|
||||||
|
if (color && color.startsWith('#')) {
|
||||||
|
// Remove the # and ensure it's 6 characters
|
||||||
|
let hex = color.slice(1);
|
||||||
|
if (hex.length === 3) {
|
||||||
|
// Convert short hex to full hex (e.g., fff -> ffffff)
|
||||||
|
hex = hex
|
||||||
|
.split('')
|
||||||
|
.map((char) => char + char)
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
return `0x${hex}`;
|
||||||
|
}
|
||||||
|
// Handle named colors or other formats - fallback to original
|
||||||
|
return color || '0xffffff';
|
||||||
|
};
|
||||||
|
|
||||||
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');
|
||||||
@@ -134,7 +153,7 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
|||||||
|
|
||||||
showConsoleLogs && console.log('🎵 Audio args:', audioArgs);
|
showConsoleLogs && console.log('🎵 Audio args:', audioArgs);
|
||||||
|
|
||||||
// Process text elements with proper font support
|
// Process text elements with proper font support and color handling
|
||||||
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, '\\:');
|
||||||
|
|
||||||
@@ -146,11 +165,22 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
|||||||
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(
|
// Format colors for FFmpeg
|
||||||
`[${videoLayer}]drawtext=fontfile=/${fontFileName}:text='${escapedText}':x=${centerX}:y=${centerY}:fontsize=${t.fontSize}:fontcolor=${t.fill}:borderw=${t.strokeWidth}:bordercolor=${
|
const fontColor = formatColorForFFmpeg(t.fill);
|
||||||
t.stroke
|
const borderColor = formatColorForFFmpeg(t.stroke);
|
||||||
}:text_align=center:enable='between(t,${t.startTime},${t.startTime + t.duration})'[t${i}]`,
|
const borderWidth = Math.max(0, t.strokeWidth || 0); // Ensure non-negative
|
||||||
);
|
|
||||||
|
// Build drawtext filter with proper border handling
|
||||||
|
let drawTextFilter = `[${videoLayer}]drawtext=fontfile=/${fontFileName}:text='${escapedText}':x=${centerX}:y=${centerY}:fontsize=${t.fontSize}:fontcolor=${fontColor}`;
|
||||||
|
|
||||||
|
// Only add border if strokeWidth > 0
|
||||||
|
if (borderWidth > 0) {
|
||||||
|
drawTextFilter += `:borderw=${borderWidth}:bordercolor=${borderColor}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawTextFilter += `:text_align=center:enable='between(t,${t.startTime},${t.startTime + t.duration})'[t${i}]`;
|
||||||
|
|
||||||
|
filters.push(drawTextFilter);
|
||||||
videoLayer = `t${i}`;
|
videoLayer = `t${i}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -476,9 +476,11 @@ const VideoPreview = ({
|
|||||||
fontSize={element.fontSize}
|
fontSize={element.fontSize}
|
||||||
fontStyle={getTextFontStyle(element)}
|
fontStyle={getTextFontStyle(element)}
|
||||||
fontFamily={element.fontFamily || 'Arial'}
|
fontFamily={element.fontFamily || 'Arial'}
|
||||||
fill={element.fill}
|
fill={element.fill || '#ffffff'}
|
||||||
stroke={element.stroke}
|
stroke={element.strokeWidth > 0 ? element.stroke || '#000000' : undefined}
|
||||||
strokeWidth={element.strokeWidth}
|
strokeWidth={element.strokeWidth || 0}
|
||||||
|
fillAfterStrokeEnabled={true}
|
||||||
|
strokeScaleEnabled={false}
|
||||||
rotation={element.rotation || 0}
|
rotation={element.rotation || 0}
|
||||||
// Center the text horizontally
|
// Center the text horizontally
|
||||||
align="center"
|
align="center"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
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';
|
||||||
@@ -29,20 +30,31 @@ export default function TextSidebar({ isOpen, onClose }) {
|
|||||||
const [isBold, setIsBold] = useState(true);
|
const [isBold, setIsBold] = useState(true);
|
||||||
const [isItalic, setIsItalic] = useState(false);
|
const [isItalic, setIsItalic] = useState(false);
|
||||||
const [fontFamily, setFontFamily] = useState('Montserrat');
|
const [fontFamily, setFontFamily] = useState('Montserrat');
|
||||||
|
const [fillColor, setFillColor] = useState('#ffffff');
|
||||||
|
const [strokeColor, setStrokeColor] = useState('#000000');
|
||||||
|
const [strokeWidth, setStrokeWidth] = useState(2);
|
||||||
|
|
||||||
// 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;
|
||||||
|
|
||||||
|
// Stroke width constraints
|
||||||
|
const MIN_STROKE_WIDTH = 0;
|
||||||
|
const MAX_STROKE_WIDTH = 10;
|
||||||
|
const STROKE_WIDTH_STEP = 1;
|
||||||
|
|
||||||
// Update state when selected element changes - THIS KEEPS SIDEBAR IN SYNC WITH TRANSFORMER
|
// Update state when selected element changes - THIS KEEPS SIDEBAR IN SYNC WITH TRANSFORMER
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedTextElement) {
|
if (selectedTextElement) {
|
||||||
setTextValue(selectedTextElement.text || '');
|
setTextValue(selectedTextElement.text || '');
|
||||||
setFontSize(selectedTextElement.fontSize || 24); // Always use current fontSize from element
|
setFontSize(selectedTextElement.fontSize || 24);
|
||||||
setIsBold(selectedTextElement.fontWeight === 'bold' || selectedTextElement.fontWeight === 700 || true);
|
setIsBold(selectedTextElement.fontWeight === 'bold' || selectedTextElement.fontWeight === 700 || true);
|
||||||
setIsItalic(selectedTextElement.fontStyle === 'italic' || false);
|
setIsItalic(selectedTextElement.fontStyle === 'italic' || false);
|
||||||
setFontFamily(selectedTextElement.fontFamily || 'Montserrat');
|
setFontFamily(selectedTextElement.fontFamily || 'Montserrat');
|
||||||
|
setFillColor(selectedTextElement.fill || '#ffffff');
|
||||||
|
setStrokeColor(selectedTextElement.stroke || '#000000');
|
||||||
|
setStrokeWidth(selectedTextElement.strokeWidth || 2);
|
||||||
}
|
}
|
||||||
}, [selectedTextElement]);
|
}, [selectedTextElement]);
|
||||||
|
|
||||||
@@ -72,6 +84,45 @@ export default function TextSidebar({ isOpen, onClose }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle stroke width changes
|
||||||
|
const handleStrokeWidthChange = (newWidth) => {
|
||||||
|
const clampedWidth = Math.max(MIN_STROKE_WIDTH, Math.min(MAX_STROKE_WIDTH, newWidth));
|
||||||
|
setStrokeWidth(clampedWidth);
|
||||||
|
|
||||||
|
if (selectedTextElement) {
|
||||||
|
emitter.emit('text-update', {
|
||||||
|
elementId: selectedTextElement.id,
|
||||||
|
updates: { strokeWidth: clampedWidth },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle fill color changes
|
||||||
|
const handleFillColorChange = (e) => {
|
||||||
|
const newColor = e.target.value;
|
||||||
|
setFillColor(newColor);
|
||||||
|
|
||||||
|
if (selectedTextElement) {
|
||||||
|
emitter.emit('text-update', {
|
||||||
|
elementId: selectedTextElement.id,
|
||||||
|
updates: { fill: newColor },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle stroke color changes
|
||||||
|
const handleStrokeColorChange = (e) => {
|
||||||
|
const newColor = e.target.value;
|
||||||
|
setStrokeColor(newColor);
|
||||||
|
|
||||||
|
if (selectedTextElement) {
|
||||||
|
emitter.emit('text-update', {
|
||||||
|
elementId: selectedTextElement.id,
|
||||||
|
updates: { stroke: newColor },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Handle font family changes
|
// Handle font family changes
|
||||||
const handleFontFamilyChange = (newFontFamily) => {
|
const handleFontFamilyChange = (newFontFamily) => {
|
||||||
setFontFamily(newFontFamily);
|
setFontFamily(newFontFamily);
|
||||||
@@ -120,9 +171,19 @@ export default function TextSidebar({ isOpen, onClose }) {
|
|||||||
handleFontSizeChange(fontSize - FONT_SIZE_STEP);
|
handleFontSizeChange(fontSize - FONT_SIZE_STEP);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Increase stroke width
|
||||||
|
const increaseStrokeWidth = () => {
|
||||||
|
handleStrokeWidthChange(strokeWidth + STROKE_WIDTH_STEP);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Decrease stroke width
|
||||||
|
const decreaseStrokeWidth = () => {
|
||||||
|
handleStrokeWidthChange(strokeWidth - STROKE_WIDTH_STEP);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
<Sheet open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||||
<SheetContent side="right" className="max-[140px] w-full overflow-y-auto">
|
<SheetContent side="right" className="max-[140px] w-full overflow-y-auto dark:bg-neutral-900">
|
||||||
<SheetHeader>
|
<SheetHeader>
|
||||||
<SheetTitle className="flex items-center gap-3">
|
<SheetTitle className="flex items-center gap-3">
|
||||||
<Type className="h-6 w-6" />
|
<Type className="h-6 w-6" />
|
||||||
@@ -140,13 +201,23 @@ export default function TextSidebar({ isOpen, onClose }) {
|
|||||||
value={textValue}
|
value={textValue}
|
||||||
onChange={handleTextChange}
|
onChange={handleTextChange}
|
||||||
placeholder="Enter your text..."
|
placeholder="Enter your text..."
|
||||||
className="mt-2 text-center text-nowrap"
|
className="mt-2 text-center text-nowrap dark:bg-neutral-800"
|
||||||
rows={4}
|
rows={4}
|
||||||
style={{
|
style={{
|
||||||
fontFamily: fontFamily,
|
fontFamily: fontFamily,
|
||||||
fontSize: `${fontSize * 0.6}px`, // Cap preview size for readability
|
fontSize: `${fontSize * 0.6}px`, // Cap preview size for readability
|
||||||
fontWeight: isBold ? 'bold' : 'normal',
|
fontWeight: isBold ? 'bold' : 'normal',
|
||||||
fontStyle: isItalic ? 'italic' : 'normal',
|
fontStyle: isItalic ? 'italic' : 'normal',
|
||||||
|
color: fillColor,
|
||||||
|
textShadow:
|
||||||
|
strokeWidth > 0
|
||||||
|
? `
|
||||||
|
-${strokeWidth * 0.6}px -${strokeWidth * 0.6}px 0 ${strokeColor},
|
||||||
|
${strokeWidth * 0.6}px -${strokeWidth * 0.6}px 0 ${strokeColor},
|
||||||
|
-${strokeWidth * 0.6}px ${strokeWidth * 0.6}px 0 ${strokeColor},
|
||||||
|
${strokeWidth * 0.6}px ${strokeWidth * 0.6}px 0 ${strokeColor}
|
||||||
|
`
|
||||||
|
: 'none',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -181,7 +252,7 @@ export default function TextSidebar({ isOpen, onClose }) {
|
|||||||
<label className="text-sm font-medium">Font Size</label>
|
<label className="text-sm font-medium">Font Size</label>
|
||||||
<div className="mt-2 flex items-center justify-between rounded-lg border p-2">
|
<div className="mt-2 flex items-center justify-between rounded-lg border p-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="default"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={decreaseFontSize}
|
onClick={decreaseFontSize}
|
||||||
disabled={fontSize <= MIN_FONT_SIZE}
|
disabled={fontSize <= MIN_FONT_SIZE}
|
||||||
@@ -196,7 +267,7 @@ export default function TextSidebar({ isOpen, onClose }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="default"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={increaseFontSize}
|
onClick={increaseFontSize}
|
||||||
disabled={fontSize >= MAX_FONT_SIZE}
|
disabled={fontSize >= MAX_FONT_SIZE}
|
||||||
@@ -220,8 +291,8 @@ export default function TextSidebar({ isOpen, onClose }) {
|
|||||||
<label className="text-sm font-medium">Font Style</label>
|
<label className="text-sm font-medium">Font Style</label>
|
||||||
<div className="mt-2 flex gap-2">
|
<div className="mt-2 flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant={isBold ? 'default' : 'outline'}
|
variant={isBold ? 'default' : 'secondary'}
|
||||||
size="sm"
|
size=""
|
||||||
onClick={handleBoldToggle}
|
onClick={handleBoldToggle}
|
||||||
className="flex flex-1 items-center gap-2"
|
className="flex flex-1 items-center gap-2"
|
||||||
>
|
>
|
||||||
@@ -230,8 +301,8 @@ export default function TextSidebar({ isOpen, onClose }) {
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant={isItalic ? 'default' : 'outline'}
|
variant={isItalic ? 'default' : 'secondary'}
|
||||||
size="sm"
|
size=""
|
||||||
onClick={handleItalicToggle}
|
onClick={handleItalicToggle}
|
||||||
className="flex flex-1 items-center gap-2"
|
className="flex flex-1 items-center gap-2"
|
||||||
>
|
>
|
||||||
@@ -240,6 +311,55 @@ export default function TextSidebar({ isOpen, onClose }) {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Font Color */}
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium">Font Color</label>
|
||||||
|
<div className="mt-2 flex items-center gap-2">
|
||||||
|
<Input className="h-10 w-20" type="color" value={fillColor} onChange={handleFillColorChange} />
|
||||||
|
<Input type="text" value={fillColor} onChange={handleFillColorChange} placeholder="#ffffff" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Outline Color */}
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium">Outline Color</label>
|
||||||
|
<div className="mt-2 flex items-center gap-2">
|
||||||
|
<Input className="h-10 w-20" type="color" value={strokeColor} onChange={handleStrokeColorChange} />
|
||||||
|
<Input type="text" value={strokeColor} onChange={handleStrokeColorChange} placeholder="#000000" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Outline Thickness */}
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium">Outline Thickness</label>
|
||||||
|
<div className="mt-2 flex items-center justify-between rounded-lg border p-2">
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="icon"
|
||||||
|
onClick={decreaseStrokeWidth}
|
||||||
|
disabled={strokeWidth <= MIN_STROKE_WIDTH}
|
||||||
|
className="h-8 w-8"
|
||||||
|
>
|
||||||
|
<Minus className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-lg font-semibold">{strokeWidth}</span>
|
||||||
|
<span className="text-sm text-gray-500">px</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="icon"
|
||||||
|
onClick={increaseStrokeWidth}
|
||||||
|
disabled={strokeWidth >= MAX_STROKE_WIDTH}
|
||||||
|
className="h-8 w-8"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center text-gray-500">
|
<div className="text-center text-gray-500">
|
||||||
|
|||||||
Reference in New Issue
Block a user