diff --git a/src/components/video-editor/AnnotationOverlay.tsx b/src/components/video-editor/AnnotationOverlay.tsx
index 7da795d..c1f743a 100644
--- a/src/components/video-editor/AnnotationOverlay.tsx
+++ b/src/components/video-editor/AnnotationOverlay.tsx
@@ -2,6 +2,14 @@ import { useRef } from "react";
import { Rnd } from "react-rnd";
import type { AnnotationRegion } from "./types";
import { cn } from "@/lib/utils";
+import {
+ FaArrowUp, FaArrowDown, FaArrowLeft, FaArrowRight,
+ FaCircle, FaSquare, FaStar, FaHeart, FaPlay
+} from "react-icons/fa";
+import {
+ BsArrowUpRight, BsArrowDownRight, BsArrowDownLeft, BsArrowUpLeft
+} from "react-icons/bs";
+import { BiRectangle } from "react-icons/bi";
interface AnnotationOverlayProps {
annotation: AnnotationRegion;
@@ -45,6 +53,84 @@ export function AnnotationOverlay({
const isDraggingRef = useRef(false);
+ const renderArrow = () => {
+ const direction = annotation.figureData?.arrowDirection || 'right';
+ const color = annotation.figureData?.color || '#34B27B';
+ const iconProps = {
+ style: {
+ width: '100%',
+ height: '100%',
+ color,
+ filter: 'drop-shadow(0 2px 8px rgba(0,0,0,0.3))'
+ }
+ };
+
+ switch (direction) {
+ case 'up': return ;
+ case 'down': return ;
+ case 'left': return ;
+ case 'right': return ;
+ case 'up-right': return ;
+ case 'up-left': return ;
+ case 'down-right': return ;
+ case 'down-left': return ;
+ default: return ;
+ }
+ };
+
+ const renderShape = () => {
+ const shapeType = annotation.figureData?.shapeType || 'circle';
+ const color = annotation.figureData?.color || '#34B27B';
+ const filled = annotation.figureData?.filled ?? true;
+ const strokeWidth = annotation.figureData?.strokeWidth || 4;
+
+ const shapeStyle: React.CSSProperties = {
+ width: '100%',
+ height: '100%',
+ color: filled ? color : 'transparent',
+ stroke: color,
+ strokeWidth: filled ? 0 : strokeWidth,
+ filter: 'drop-shadow(0 2px 8px rgba(0,0,0,0.3))'
+ };
+
+ const IconComponent = (() => {
+ switch (shapeType) {
+ case 'circle': return FaCircle;
+ case 'square': return FaSquare;
+ case 'triangle': return FaPlay;
+ case 'rectangle': return BiRectangle;
+ case 'star': return FaStar;
+ case 'heart': return FaHeart;
+ default: return FaCircle;
+ }
+ })();
+
+ return filled ? (
+
+ ) : (
+
+ );
+ };
+
const renderContent = () => {
switch (annotation.type) {
case 'text':
@@ -69,7 +155,6 @@ export function AnnotationOverlay({
textAlign: annotation.style.textAlign,
wordBreak: 'break-word',
whiteSpace: 'pre-wrap',
- textShadow: '0 2px 8px rgba(0,0,0,0.5), 0 0 2px rgba(0,0,0,0.8)',
boxDecorationBreak: 'clone',
WebkitBoxDecorationBreak: 'clone',
padding: '0.1em 0.2em',
@@ -82,8 +167,6 @@ export function AnnotationOverlay({
);
-
-
case 'image':
if (annotation.content && annotation.content.startsWith('data:image')) {
return (
@@ -101,6 +184,44 @@ export function AnnotationOverlay({
);
+ case 'figure':
+ if (!annotation.figureData) {
+ return (
+
+ No figure data
+
+ );
+ }
+
+ const figureType = annotation.figureData.figureType;
+
+ if (figureType === 'arrow') {
+ return (
+
+ {renderArrow()}
+
+ );
+ }
+
+ if (figureType === 'shape') {
+ return (
+
+ {renderShape()}
+
+ );
+ }
+
+ if (figureType === 'emoji') {
+ const emojiSize = annotation.figureData.emojiSize || 64;
+ return (
+
+ {annotation.figureData.emoji || '😊'}
+
+ );
+ }
+
+ return null;
+
default:
return null;
}
@@ -197,6 +318,7 @@ export function AnnotationOverlay({
"w-full h-full rounded-lg",
annotation.type === 'text' && "bg-transparent",
annotation.type === 'image' && "bg-transparent",
+ annotation.type === 'figure' && "bg-transparent",
isSelected && "shadow-lg"
)}
>
diff --git a/src/components/video-editor/AnnotationSettingsPanel.tsx b/src/components/video-editor/AnnotationSettingsPanel.tsx
index bc84a36..c8fdb6b 100644
--- a/src/components/video-editor/AnnotationSettingsPanel.tsx
+++ b/src/components/video-editor/AnnotationSettingsPanel.tsx
@@ -1,20 +1,34 @@
import { useState, useRef } from "react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
-import { Trash2, Type, Image as ImageIcon, Upload, Bold, Italic, Underline, AlignLeft, AlignCenter, AlignRight, ChevronDown, Info } from "lucide-react";
+import { Trash2, Type, Image as ImageIcon, Upload, Bold, Italic, Underline, AlignLeft, AlignCenter, AlignRight, ChevronDown, Info, Shapes, Smile } from "lucide-react";
import { toast } from "sonner";
import Colorful from '@uiw/react-color-colorful';
import { hsvaToHex, hexToHsva } from '@uiw/color-convert';
-import type { AnnotationRegion, AnnotationType } from "./types";
+import type { AnnotationRegion, AnnotationType, FigureType, ArrowDirection, ShapeType, FigureData } from "./types";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
+import { Switch } from "@/components/ui/switch";
+import { Slider } from "@/components/ui/slider";
+import { cn } from "@/lib/utils";
+import EmojiPicker, { EmojiClickData } from 'emoji-picker-react';
+import {
+ FaArrowUp, FaArrowDown, FaArrowLeft, FaArrowRight,
+ FaCircle, FaSquare, FaStar, FaHeart
+} from "react-icons/fa";
+import {
+ BsArrowUpRight, BsArrowDownRight, BsArrowDownLeft, BsArrowUpLeft
+} from "react-icons/bs";
+import { FaPlay } from "react-icons/fa";
+import { BiRectangle } from "react-icons/bi";
interface AnnotationSettingsPanelProps {
annotation: AnnotationRegion;
onContentChange: (content: string) => void;
onTypeChange: (type: AnnotationType) => void;
onStyleChange: (style: Partial) => void;
+ onFigureDataChange?: (figureData: FigureData) => void;
onDelete: () => void;
}
@@ -36,11 +50,16 @@ export function AnnotationSettingsPanel({
onContentChange,
onTypeChange,
onStyleChange,
+ onFigureDataChange,
onDelete,
}: AnnotationSettingsPanelProps) {
const fileInputRef = useRef(null);
const [textColorHsva, setTextColorHsva] = useState(hexToHsva(annotation.style.color));
const [bgColorHsva, setBgColorHsva] = useState(hexToHsva(annotation.style.backgroundColor || '#00000000'));
+ const [figureColorHsva, setFigureColorHsva] = useState(
+ hexToHsva(annotation.figureData?.color || '#34B27B')
+ );
+ const [showEmojiPicker, setShowEmojiPicker] = useState(false);
@@ -92,7 +111,7 @@ export function AnnotationSettingsPanel({
{/* Type Selector */}
onTypeChange(value as AnnotationType)} className="mb-6">
-
+
Text
@@ -101,6 +120,10 @@ export function AnnotationSettingsPanel({
Image
+
+
+ Figure
+
{/* Text Content */}
@@ -108,7 +131,7 @@ export function AnnotationSettingsPanel({
+
+ Add Annotation
+ A
+
Add Keyframe
F
diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx
index c369c86..7d938f4 100644
--- a/src/components/video-editor/SettingsPanel.tsx
+++ b/src/components/video-editor/SettingsPanel.tsx
@@ -73,6 +73,7 @@ interface SettingsPanelProps {
onAnnotationContentChange?: (id: string, content: string) => void;
onAnnotationTypeChange?: (id: string, type: AnnotationType) => void;
onAnnotationStyleChange?: (id: string, style: Partial
) => void;
+ onAnnotationFigureDataChange?: (id: string, figureData: any) => void;
onAnnotationDelete?: (id: string) => void;
}
@@ -114,6 +115,7 @@ export function SettingsPanel({
onAnnotationContentChange,
onAnnotationTypeChange,
onAnnotationStyleChange,
+ onAnnotationFigureDataChange,
onAnnotationDelete,
}: SettingsPanelProps) {
const [wallpaperPaths, setWallpaperPaths] = useState([]);
@@ -204,6 +206,7 @@ export function SettingsPanel({
onContentChange={(content) => onAnnotationContentChange(selectedAnnotation.id, content)}
onTypeChange={(type) => onAnnotationTypeChange(selectedAnnotation.id, type)}
onStyleChange={(style) => onAnnotationStyleChange(selectedAnnotation.id, style)}
+ onFigureDataChange={onAnnotationFigureDataChange ? (figureData) => onAnnotationFigureDataChange(selectedAnnotation.id, figureData) : undefined}
onDelete={() => onAnnotationDelete(selectedAnnotation.id)}
/>
);
diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx
index 15917db..39ad6fc 100644
--- a/src/components/video-editor/VideoEditor.tsx
+++ b/src/components/video-editor/VideoEditor.tsx
@@ -19,12 +19,14 @@ import {
DEFAULT_ANNOTATION_POSITION,
DEFAULT_ANNOTATION_SIZE,
DEFAULT_ANNOTATION_STYLE,
+ DEFAULT_FIGURE_DATA,
type ZoomDepth,
type ZoomFocus,
type ZoomRegion,
type TrimRegion,
type AnnotationRegion,
type CropRegion,
+ type FigureData,
} from "./types";
import { VideoExporter, type ExportProgress } from "@/lib/exporter";
import { type AspectRatio, getAspectRatioValue } from "@/utils/aspectRatioUtils";
@@ -291,11 +293,18 @@ export default function VideoEditor() {
const handleAnnotationContentChange = useCallback((id: string, content: string) => {
console.log('[VideoEditor] Annotation content changed:', { id, content });
setAnnotationRegions((prev) => {
- const updated = prev.map((region) =>
- region.id === id
- ? { ...region, content }
- : region,
- );
+ const updated = prev.map((region) => {
+ if (region.id !== id) return region;
+
+ // Store content in type-specific fields
+ if (region.type === 'text') {
+ return { ...region, content, textContent: content };
+ } else if (region.type === 'image') {
+ return { ...region, content, imageContent: content };
+ } else {
+ return { ...region, content };
+ }
+ });
console.log('[VideoEditor] Updated annotation regions:', updated);
return updated;
});
@@ -304,15 +313,25 @@ export default function VideoEditor() {
const handleAnnotationTypeChange = useCallback((id: string, type: AnnotationRegion['type']) => {
console.log('[VideoEditor] Annotation type changed:', { id, type });
setAnnotationRegions((prev) => {
- const updated = prev.map((region) =>
- region.id === id
- ? {
- ...region,
- type,
- content: type === 'text' ? 'Enter text...' : ''
- }
- : region,
- );
+ const updated = prev.map((region) => {
+ if (region.id !== id) return region;
+
+ const updatedRegion = { ...region, type };
+
+ // Restore content from type-specific storage
+ if (type === 'text') {
+ updatedRegion.content = region.textContent || 'Enter text...';
+ } else if (type === 'image') {
+ updatedRegion.content = region.imageContent || '';
+ } else if (type === 'figure') {
+ updatedRegion.content = '';
+ if (!region.figureData) {
+ updatedRegion.figureData = { ...DEFAULT_FIGURE_DATA };
+ }
+ }
+
+ return updatedRegion;
+ });
console.log('[VideoEditor] Updated annotation regions after type change:', updated);
return updated;
});
@@ -329,6 +348,17 @@ export default function VideoEditor() {
);
}, []);
+ const handleAnnotationFigureDataChange = useCallback((id: string, figureData: FigureData) => {
+ console.log('Annotation figure data changed:', { id, figureData });
+ setAnnotationRegions((prev) =>
+ prev.map((region) =>
+ region.id === id
+ ? { ...region, figureData }
+ : region,
+ ),
+ );
+ }, []);
+
const handleAnnotationPositionChange = useCallback((id: string, position: { x: number; y: number }) => {
setAnnotationRegions((prev) =>
prev.map((region) =>
@@ -686,6 +716,7 @@ export default function VideoEditor() {
onAnnotationContentChange={handleAnnotationContentChange}
onAnnotationTypeChange={handleAnnotationTypeChange}
onAnnotationStyleChange={handleAnnotationStyleChange}
+ onAnnotationFigureDataChange={handleAnnotationFigureDataChange}
onAnnotationDelete={handleAnnotationDelete}
/>
diff --git a/src/components/video-editor/types.ts b/src/components/video-editor/types.ts
index d1868b1..4b289de 100644
--- a/src/components/video-editor/types.ts
+++ b/src/components/video-editor/types.ts
@@ -19,9 +19,22 @@ export interface TrimRegion {
endMs: number;
}
-export type AnnotationType = 'text' | 'image';
+export type AnnotationType = 'text' | 'image' | 'figure';
+export type FigureType = 'arrow' | 'shape' | 'emoji';
+export type ArrowDirection = 'up' | 'down' | 'left' | 'right' | 'up-right' | 'up-left' | 'down-right' | 'down-left';
+export type ShapeType = 'circle' | 'square' | 'rectangle' | 'triangle' | 'star' | 'heart';
+export interface FigureData {
+ figureType: FigureType;
+ arrowDirection?: ArrowDirection;
+ shapeType?: ShapeType;
+ emoji?: string;
+ emojiSize?: number;
+ color: string;
+ strokeWidth: number;
+ filled: boolean;
+}
export interface AnnotationPosition {
x: number;
@@ -49,11 +62,14 @@ export interface AnnotationRegion {
startMs: number;
endMs: number;
type: AnnotationType;
- content: string;
+ content: string; // Legacy - still used for current type
+ textContent?: string; // Separate storage for text
+ imageContent?: string; // Separate storage for image data URL
position: AnnotationPosition;
size: AnnotationSize;
style: AnnotationTextStyle;
zIndex: number;
+ figureData?: FigureData;
}
export const DEFAULT_ANNOTATION_POSITION: AnnotationPosition = {
@@ -77,6 +93,15 @@ export const DEFAULT_ANNOTATION_STYLE: AnnotationTextStyle = {
textAlign: 'center',
};
+export const DEFAULT_FIGURE_DATA: FigureData = {
+ figureType: 'arrow',
+ arrowDirection: 'right',
+ color: '#34B27B',
+ strokeWidth: 4,
+ filled: true,
+ emojiSize: 64,
+};
+
export interface CropRegion {