final annotation settings
This commit is contained in:
@@ -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 <FaArrowUp {...iconProps} />;
|
||||
case 'down': return <FaArrowDown {...iconProps} />;
|
||||
case 'left': return <FaArrowLeft {...iconProps} />;
|
||||
case 'right': return <FaArrowRight {...iconProps} />;
|
||||
case 'up-right': return <BsArrowUpRight {...iconProps} />;
|
||||
case 'up-left': return <BsArrowUpLeft {...iconProps} />;
|
||||
case 'down-right': return <BsArrowDownRight {...iconProps} />;
|
||||
case 'down-left': return <BsArrowDownLeft {...iconProps} />;
|
||||
default: return <FaArrowRight {...iconProps} />;
|
||||
}
|
||||
};
|
||||
|
||||
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 ? (
|
||||
<IconComponent style={shapeStyle} />
|
||||
) : (
|
||||
<svg viewBox="0 0 100 100" style={{ width: '100%', height: '100%' }}>
|
||||
{shapeType === 'circle' && (
|
||||
<circle cx="50" cy="50" r="40" fill="none" stroke={color} strokeWidth={strokeWidth} />
|
||||
)}
|
||||
{shapeType === 'square' && (
|
||||
<rect x="10" y="10" width="80" height="80" fill="none" stroke={color} strokeWidth={strokeWidth} />
|
||||
)}
|
||||
{shapeType === 'rectangle' && (
|
||||
<rect x="5" y="20" width="90" height="60" fill="none" stroke={color} strokeWidth={strokeWidth} />
|
||||
)}
|
||||
{shapeType === 'triangle' && (
|
||||
<polygon points="50,10 90,90 10,90" fill="none" stroke={color} strokeWidth={strokeWidth} />
|
||||
)}
|
||||
{shapeType === 'star' && (
|
||||
<polygon points="50,5 61,38 95,38 68,59 79,92 50,71 21,92 32,59 5,38 39,38" fill="none" stroke={color} strokeWidth={strokeWidth} />
|
||||
)}
|
||||
{shapeType === 'heart' && (
|
||||
<path d="M50,85 C50,85 10,60 10,35 C10,20 20,10 30,10 C40,10 50,20 50,20 C50,20 60,10 70,10 C80,10 90,20 90,35 C90,60 50,85 50,85 Z" fill="none" stroke={color} strokeWidth={strokeWidth} />
|
||||
)}
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
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({
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
|
||||
case 'image':
|
||||
if (annotation.content && annotation.content.startsWith('data:image')) {
|
||||
return (
|
||||
@@ -101,6 +184,44 @@ export function AnnotationOverlay({
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'figure':
|
||||
if (!annotation.figureData) {
|
||||
return (
|
||||
<div className="w-full h-full flex items-center justify-center text-slate-400 text-sm">
|
||||
No figure data
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const figureType = annotation.figureData.figureType;
|
||||
|
||||
if (figureType === 'arrow') {
|
||||
return (
|
||||
<div className="w-full h-full flex items-center justify-center p-2">
|
||||
{renderArrow()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (figureType === 'shape') {
|
||||
return (
|
||||
<div className="w-full h-full flex items-center justify-center p-2">
|
||||
{renderShape()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (figureType === 'emoji') {
|
||||
const emojiSize = annotation.figureData.emojiSize || 64;
|
||||
return (
|
||||
<div className="w-full h-full flex items-center justify-center" style={{ fontSize: `${emojiSize}px` }}>
|
||||
{annotation.figureData.emoji || '😊'}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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"
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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<AnnotationRegion['style']>) => void;
|
||||
onFigureDataChange?: (figureData: FigureData) => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
@@ -36,11 +50,16 @@ export function AnnotationSettingsPanel({
|
||||
onContentChange,
|
||||
onTypeChange,
|
||||
onStyleChange,
|
||||
onFigureDataChange,
|
||||
onDelete,
|
||||
}: AnnotationSettingsPanelProps) {
|
||||
const fileInputRef = useRef<HTMLInputElement>(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 */}
|
||||
<Tabs value={annotation.type} onValueChange={(value) => onTypeChange(value as AnnotationType)} className="mb-6">
|
||||
<TabsList className="mb-4 bg-white/5 border border-white/5 p-1 w-full grid grid-cols-2 h-auto rounded-xl">
|
||||
<TabsList className="mb-4 bg-white/5 border border-white/5 p-1 w-full grid grid-cols-3 h-auto rounded-xl">
|
||||
<TabsTrigger value="text" className="data-[state=active]:bg-[#34B27B] data-[state=active]:text-white text-slate-400 py-2 rounded-lg transition-all gap-2">
|
||||
<Type className="w-4 h-4" />
|
||||
Text
|
||||
@@ -101,6 +120,10 @@ export function AnnotationSettingsPanel({
|
||||
<ImageIcon className="w-4 h-4" />
|
||||
Image
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="figure" className="data-[state=active]:bg-[#34B27B] data-[state=active]:text-white text-slate-400 py-2 rounded-lg transition-all gap-2">
|
||||
<Shapes className="w-4 h-4" />
|
||||
Figure
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Text Content */}
|
||||
@@ -108,7 +131,7 @@ export function AnnotationSettingsPanel({
|
||||
<div>
|
||||
<label className="text-xs font-medium text-slate-200 mb-2 block">Text Content</label>
|
||||
<textarea
|
||||
value={annotation.content}
|
||||
value={annotation.textContent || annotation.content}
|
||||
onChange={(e) => onContentChange(e.target.value)}
|
||||
placeholder="Enter your text..."
|
||||
rows={5}
|
||||
@@ -339,9 +362,246 @@ export function AnnotationSettingsPanel({
|
||||
Supported formats: JPG, PNG, GIF, WebP
|
||||
</p>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="figure" className="mt-0 space-y-4">
|
||||
<div>
|
||||
<label className="text-xs font-medium text-slate-200 mb-2 block">Figure Type</label>
|
||||
<Tabs
|
||||
value={annotation.figureData?.figureType || 'arrow'}
|
||||
onValueChange={(value) => {
|
||||
const newFigureData: FigureData = {
|
||||
...annotation.figureData!,
|
||||
figureType: value as FigureType,
|
||||
};
|
||||
onFigureDataChange?.(newFigureData);
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
<TabsList className="w-full grid grid-cols-3 bg-white/5 border border-white/5 p-1 h-auto rounded-lg">
|
||||
<TabsTrigger value="arrow" className="data-[state=active]:bg-[#34B27B] data-[state=active]:text-white text-slate-400 py-2 rounded-md transition-all gap-1.5 text-xs">
|
||||
<FaArrowRight className="w-3 h-3" />
|
||||
Arrow
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="shape" className="data-[state=active]:bg-[#34B27B] data-[state=active]:text-white text-slate-400 py-2 rounded-md transition-all gap-1.5 text-xs">
|
||||
<FaCircle className="w-3 h-3" />
|
||||
Shape
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="emoji" className="data-[state=active]:bg-[#34B27B] data-[state=active]:text-white text-slate-400 py-2 rounded-md transition-all gap-1.5 text-xs">
|
||||
<Smile className="w-3 h-3" />
|
||||
Emoji
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="arrow" className="mt-4 space-y-4">
|
||||
<div>
|
||||
<label className="text-xs font-medium text-slate-200 mb-3 block">Arrows</label>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{[
|
||||
{ value: 'up' as ArrowDirection, icon: FaArrowUp },
|
||||
{ value: 'down' as ArrowDirection, icon: FaArrowDown },
|
||||
{ value: 'left' as ArrowDirection, icon: FaArrowLeft },
|
||||
{ value: 'right' as ArrowDirection, icon: FaArrowRight },
|
||||
{ value: 'up-right' as ArrowDirection, icon: BsArrowUpRight },
|
||||
{ value: 'up-left' as ArrowDirection, icon: BsArrowUpLeft },
|
||||
{ value: 'down-right' as ArrowDirection, icon: BsArrowDownRight },
|
||||
{ value: 'down-left' as ArrowDirection, icon: BsArrowDownLeft },
|
||||
].map(({ value, icon: Icon }) => (
|
||||
<button
|
||||
key={value}
|
||||
onClick={() => {
|
||||
const newFigureData: FigureData = {
|
||||
...annotation.figureData!,
|
||||
arrowDirection: value,
|
||||
};
|
||||
onFigureDataChange?.(newFigureData);
|
||||
}}
|
||||
className={cn(
|
||||
"h-16 rounded-lg border flex flex-col items-center justify-center gap-1 transition-all",
|
||||
annotation.figureData?.arrowDirection === value
|
||||
? "bg-[#34B27B] border-[#34B27B] text-white"
|
||||
: "bg-white/5 border-white/10 text-slate-400 hover:bg-white/10 hover:border-white/20"
|
||||
)}
|
||||
>
|
||||
<Icon className="w-5 h-5" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="shape" className="mt-4 space-y-4">
|
||||
<div>
|
||||
<label className="text-xs font-medium text-slate-200 mb-3 block">Shape Type</label>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{[
|
||||
{ value: 'circle' as ShapeType, icon: FaCircle, label: 'Circle' },
|
||||
{ value: 'square' as ShapeType, icon: FaSquare, label: 'Square' },
|
||||
{ value: 'rectangle' as ShapeType, icon: BiRectangle, label: 'Rectangle' },
|
||||
{ value: 'triangle' as ShapeType, icon: FaPlay, label: 'Triangle' },
|
||||
{ value: 'star' as ShapeType, icon: FaStar, label: 'Star' },
|
||||
{ value: 'heart' as ShapeType, icon: FaHeart, label: 'Heart' },
|
||||
].map(({ value, icon: Icon, label }) => (
|
||||
<button
|
||||
key={value}
|
||||
onClick={() => {
|
||||
const newFigureData: FigureData = {
|
||||
...annotation.figureData!,
|
||||
shapeType: value,
|
||||
};
|
||||
onFigureDataChange?.(newFigureData);
|
||||
}}
|
||||
className={cn(
|
||||
"h-16 rounded-lg border flex flex-col items-center justify-center gap-1 transition-all",
|
||||
annotation.figureData?.shapeType === value
|
||||
? "bg-[#34B27B] border-[#34B27B] text-white"
|
||||
: "bg-white/5 border-white/10 text-slate-400 hover:bg-white/10 hover:border-white/20"
|
||||
)}
|
||||
>
|
||||
<Icon className="w-5 h-5" />
|
||||
<span className="text-[10px]">{label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-3 bg-white/5 rounded-lg border border-white/5">
|
||||
<label className="text-xs font-medium text-slate-200">Filled</label>
|
||||
<Switch
|
||||
checked={annotation.figureData?.filled ?? true}
|
||||
onCheckedChange={(checked) => {
|
||||
const newFigureData: FigureData = {
|
||||
...annotation.figureData!,
|
||||
filled: checked,
|
||||
};
|
||||
onFigureDataChange?.(newFigureData);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!annotation.figureData?.filled && (
|
||||
<div>
|
||||
<label className="text-xs font-medium text-slate-200 mb-2 block">
|
||||
Stroke Width: {annotation.figureData?.strokeWidth || 4}px
|
||||
</label>
|
||||
<Slider
|
||||
value={[annotation.figureData?.strokeWidth || 4]}
|
||||
onValueChange={([value]) => {
|
||||
const newFigureData: FigureData = {
|
||||
...annotation.figureData!,
|
||||
strokeWidth: value,
|
||||
};
|
||||
onFigureDataChange?.(newFigureData);
|
||||
}}
|
||||
min={1}
|
||||
max={20}
|
||||
step={1}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="emoji" className="mt-4 space-y-4">
|
||||
<div>
|
||||
<label className="text-xs font-medium text-slate-200 mb-2 block">Selected Emoji</label>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-1 h-16 bg-white/5 border border-white/10 rounded-lg flex items-center justify-center text-4xl">
|
||||
{annotation.figureData?.emoji || '😊'}
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
|
||||
variant="outline"
|
||||
className="h-16 px-6 bg-[#34B27B] text-white border-[#34B27B] hover:bg-[#2a9163] hover:border-[#2a9163]"
|
||||
>
|
||||
{showEmojiPicker ? 'Close' : 'Pick'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs font-medium text-slate-200 mb-2 block">
|
||||
Emoji Size: {annotation.figureData?.emojiSize || 64}px
|
||||
</label>
|
||||
<Slider
|
||||
value={[annotation.figureData?.emojiSize || 64]}
|
||||
onValueChange={([value]) => {
|
||||
const newFigureData: FigureData = {
|
||||
...annotation.figureData!,
|
||||
emojiSize: value,
|
||||
};
|
||||
onFigureDataChange?.(newFigureData);
|
||||
}}
|
||||
min={16}
|
||||
max={200}
|
||||
step={4}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showEmojiPicker && (
|
||||
<div className="border border-white/10 rounded-lg overflow-hidden">
|
||||
<EmojiPicker
|
||||
onEmojiClick={(emojiData: EmojiClickData) => {
|
||||
const newFigureData: FigureData = {
|
||||
...annotation.figureData!,
|
||||
emoji: emojiData.emoji,
|
||||
};
|
||||
onFigureDataChange?.(newFigureData);
|
||||
setShowEmojiPicker(false);
|
||||
}}
|
||||
width="100%"
|
||||
height={300}
|
||||
searchPlaceHolder="Search emoji..."
|
||||
previewConfig={{ showPreview: false }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{annotation.figureData?.figureType !== 'emoji' && (
|
||||
<div>
|
||||
<label className="text-xs font-medium text-slate-200 mb-2 block">Color</label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full h-10 justify-start gap-2 bg-white/5 border-white/10 hover:bg-white/10"
|
||||
>
|
||||
<div
|
||||
className="w-5 h-5 rounded-full border border-white/20"
|
||||
style={{ backgroundColor: annotation.figureData?.color || '#34B27B' }}
|
||||
/>
|
||||
<span className="text-xs text-slate-300 truncate flex-1 text-left">
|
||||
{annotation.figureData?.color || '#34B27B'}
|
||||
</span>
|
||||
<ChevronDown className="h-3 w-3 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0 border-none bg-transparent shadow-xl">
|
||||
<div className="p-2 bg-[#1a1a1c] border border-white/10 rounded-xl">
|
||||
<Colorful
|
||||
color={figureColorHsva}
|
||||
disableAlpha={true}
|
||||
onChange={(color) => {
|
||||
setFigureColorHsva(color.hsva);
|
||||
const newFigureData: FigureData = {
|
||||
...annotation.figureData!,
|
||||
color: hsvaToHex(color.hsva),
|
||||
};
|
||||
onFigureDataChange?.(newFigureData);
|
||||
}}
|
||||
style={{ width: '100%', borderRadius: '8px' }}
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{/* Delete Button */}
|
||||
<Button
|
||||
onClick={onDelete}
|
||||
variant="destructive"
|
||||
|
||||
@@ -11,6 +11,10 @@ export function KeyboardShortcutsHelp() {
|
||||
<span className="text-slate-400">Add Zoom</span>
|
||||
<kbd className="px-1 py-0.5 bg-white/5 border border-white/10 rounded text-[#34B27B] font-mono">Z</kbd>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-slate-400">Add Annotation</span>
|
||||
<kbd className="px-1 py-0.5 bg-white/5 border border-white/10 rounded text-[#34B27B] font-mono">A</kbd>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-slate-400">Add Keyframe</span>
|
||||
<kbd className="px-1 py-0.5 bg-white/5 border border-white/10 rounded text-[#34B27B] font-mono">F</kbd>
|
||||
|
||||
@@ -73,6 +73,7 @@ interface SettingsPanelProps {
|
||||
onAnnotationContentChange?: (id: string, content: string) => void;
|
||||
onAnnotationTypeChange?: (id: string, type: AnnotationType) => void;
|
||||
onAnnotationStyleChange?: (id: string, style: Partial<AnnotationRegion['style']>) => 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<string[]>([]);
|
||||
@@ -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)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user