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 dff0fb6..0d512b9 100644 --- a/resources/js/modules/editor/partials/canvas/sample-timeline-data.jsx +++ b/resources/js/modules/editor/partials/canvas/sample-timeline-data.jsx @@ -63,9 +63,9 @@ const sampleTimelineElements = [ fontWeight: 'bold', fontFamily: 'Montserrat', fontStyle: 'normal', - fill: 'white', - stroke: 'black', - strokeWidth: 1, + fill: '#ffffff', + stroke: '#000000', + strokeWidth: 3, rotation: 0, }, { @@ -81,8 +81,8 @@ const sampleTimelineElements = [ fontWeight: 'bold', fontFamily: 'Montserrat', fontStyle: 'normal', - fill: 'yellow', - stroke: 'red', + fill: '#ffff00', + stroke: '#ff0000', strokeWidth: 2, rotation: 0, }, diff --git a/resources/js/modules/editor/partials/canvas/video-export.jsx b/resources/js/modules/editor/partials/canvas/video-export.jsx index 030c0e2..864ec09 100644 --- a/resources/js/modules/editor/partials/canvas/video-export.jsx +++ b/resources/js/modules/editor/partials/canvas/video-export.jsx @@ -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( (is_string = true, useLocalFiles = false) => { showConsoleLogs && console.log('🎬 STARTING FFmpeg generation'); @@ -134,7 +153,7 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => { 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) => { 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 centerY = Math.round(t.y + t.fontSize * 0.3); // Adjust for text baseline - filters.push( - `[${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}]`, - ); + // Format colors for FFmpeg + const fontColor = formatColorForFFmpeg(t.fill); + const borderColor = formatColorForFFmpeg(t.stroke); + 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}`; }); diff --git a/resources/js/modules/editor/partials/canvas/video-preview.jsx b/resources/js/modules/editor/partials/canvas/video-preview.jsx index 2374c0c..78abd23 100644 --- a/resources/js/modules/editor/partials/canvas/video-preview.jsx +++ b/resources/js/modules/editor/partials/canvas/video-preview.jsx @@ -476,9 +476,11 @@ const VideoPreview = ({ fontSize={element.fontSize} fontStyle={getTextFontStyle(element)} fontFamily={element.fontFamily || 'Arial'} - fill={element.fill} - stroke={element.stroke} - strokeWidth={element.strokeWidth} + fill={element.fill || '#ffffff'} + stroke={element.strokeWidth > 0 ? element.stroke || '#000000' : undefined} + strokeWidth={element.strokeWidth || 0} + fillAfterStrokeEnabled={true} + strokeScaleEnabled={false} rotation={element.rotation || 0} // Center the text horizontally align="center" diff --git a/resources/js/modules/editor/partials/text-sidebar.jsx b/resources/js/modules/editor/partials/text-sidebar.jsx index 2dc55af..0b07766 100644 --- a/resources/js/modules/editor/partials/text-sidebar.jsx +++ b/resources/js/modules/editor/partials/text-sidebar.jsx @@ -1,4 +1,5 @@ import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; 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'; @@ -29,20 +30,31 @@ export default function TextSidebar({ isOpen, onClose }) { const [isBold, setIsBold] = useState(true); const [isItalic, setIsItalic] = useState(false); const [fontFamily, setFontFamily] = useState('Montserrat'); + const [fillColor, setFillColor] = useState('#ffffff'); + const [strokeColor, setStrokeColor] = useState('#000000'); + const [strokeWidth, setStrokeWidth] = useState(2); // Font size constraints const MIN_FONT_SIZE = 8; const MAX_FONT_SIZE = 120; 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 useEffect(() => { if (selectedTextElement) { 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); setIsItalic(selectedTextElement.fontStyle === 'italic' || false); setFontFamily(selectedTextElement.fontFamily || 'Montserrat'); + setFillColor(selectedTextElement.fill || '#ffffff'); + setStrokeColor(selectedTextElement.stroke || '#000000'); + setStrokeWidth(selectedTextElement.strokeWidth || 2); } }, [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 const handleFontFamilyChange = (newFontFamily) => { setFontFamily(newFontFamily); @@ -120,9 +171,19 @@ export default function TextSidebar({ isOpen, onClose }) { 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 ( !open && onClose()}> - + @@ -140,13 +201,23 @@ export default function TextSidebar({ isOpen, onClose }) { value={textValue} onChange={handleTextChange} placeholder="Enter your text..." - className="mt-2 text-center text-nowrap" + className="mt-2 text-center text-nowrap dark:bg-neutral-800" rows={4} style={{ fontFamily: fontFamily, fontSize: `${fontSize * 0.6}px`, // Cap preview size for readability fontWeight: isBold ? 'bold' : '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', }} /> @@ -181,7 +252,7 @@ export default function TextSidebar({ isOpen, onClose }) {
+ + {/* Font Color */} +
+ +
+ + +
+
+ + {/* Outline Color */} +
+ +
+ + +
+
+ + {/* Outline Thickness */} +
+ +
+ + +
+ {strokeWidth} + px +
+ + +
+
) : (