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 ? (
+ <>
+
+
+
+
+ >
+ ) : (
+
+
+
Select a text element to edit
+
+ )}
+
+
+
+ );
+}
diff --git a/resources/js/stores/VideoEditorStore.js b/resources/js/stores/VideoEditorStore.js
index e9e2aab..7a37e9a 100644
--- a/resources/js/stores/VideoEditorStore.js
+++ b/resources/js/stores/VideoEditorStore.js
@@ -1,17 +1,19 @@
-import axiosInstance from '@/plugins/axios-plugin';
import { mountStoreDevtool } from 'simple-zustand-devtools';
-import { toast } from 'sonner';
-import { route } from 'ziggy-js';
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
const useVideoEditorStore = create(
devtools((set, get) => ({
videoIsPlaying: false,
+ selectedTextElement: null,
setVideoIsPlaying: (isPlaying) => {
set({ videoIsPlaying: isPlaying });
},
+
+ setSelectedTextElement: (element) => {
+ set({ selectedTextElement: element });
+ },
})),
{