From 98d6acaa6a9f9d75e8744a9746513f3afb9f581e Mon Sep 17 00:00:00 2001 From: Siddharth Date: Tue, 25 Nov 2025 18:45:37 -0700 Subject: [PATCH] keyframes --- package-lock.json | 8 +++ package.json | 1 + src/components/ui/item-content.tsx | 3 +- .../video-editor/timeline/Item.module.css | 12 ---- src/components/video-editor/timeline/Item.tsx | 17 +++-- .../timeline/ItemGlass.module.css | 1 - .../video-editor/timeline/KeyframeMarkers.tsx | 50 +++++++++++++++ .../video-editor/timeline/TimelineEditor.tsx | 62 ++++++++++++++++++- 8 files changed, 132 insertions(+), 22 deletions(-) create mode 100644 src/components/video-editor/timeline/KeyframeMarkers.tsx diff --git a/package-lock.json b/package-lock.json index 0cc70cb..5813422 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "devDependencies": { "@types/react": "^18.2.64", "@types/react-dom": "^18.2.21", + "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^7.1.1", "@typescript-eslint/parser": "^7.1.1", "@vitejs/plugin-react": "^4.2.1", @@ -3516,6 +3517,13 @@ "@types/node": "*" } }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/verror": { "version": "1.10.11", "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", diff --git a/package.json b/package.json index 17563dd..0a49b64 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "devDependencies": { "@types/react": "^18.2.64", "@types/react-dom": "^18.2.21", + "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^7.1.1", "@typescript-eslint/parser": "^7.1.1", "@vitejs/plugin-react": "^4.2.1", diff --git a/src/components/ui/item-content.tsx b/src/components/ui/item-content.tsx index 9d60271..ebaae9d 100644 --- a/src/components/ui/item-content.tsx +++ b/src/components/ui/item-content.tsx @@ -7,7 +7,8 @@ interface ItemContentProps extends PropsWithChildren { function ItemContent({ children, classes }: ItemContentProps) { return (
{children}
diff --git a/src/components/video-editor/timeline/Item.module.css b/src/components/video-editor/timeline/Item.module.css index ac79e29..e69de29 100644 --- a/src/components/video-editor/timeline/Item.module.css +++ b/src/components/video-editor/timeline/Item.module.css @@ -1,12 +0,0 @@ -.itemDark { - background: #23232a; - border: 1px solid #34B27B; - border-radius: 12px; - box-shadow: 0 2px 12px rgba(0,0,0,0.18); - color: #e5e7eb; - transition: box-shadow 0.2s, border 0.2s, background 0.2s; -} -.squircle { - border-radius: 12px; - -corner-smoothing: antialiased; -} diff --git a/src/components/video-editor/timeline/Item.tsx b/src/components/video-editor/timeline/Item.tsx index cfab696..66c10cb 100644 --- a/src/components/video-editor/timeline/Item.tsx +++ b/src/components/video-editor/timeline/Item.tsx @@ -42,19 +42,26 @@ export default function Item({ id, span, rowId, isSelected = false, onSelect, zo
{ event.stopPropagation(); onSelect?.(); }} > -
-
- +
+
{/* Content */}
diff --git a/src/components/video-editor/timeline/ItemGlass.module.css b/src/components/video-editor/timeline/ItemGlass.module.css index 303a347..adef673 100644 --- a/src/components/video-editor/timeline/ItemGlass.module.css +++ b/src/components/video-editor/timeline/ItemGlass.module.css @@ -28,7 +28,6 @@ position: absolute; top: 0; bottom: 0; - background: #34B27B; width: 4px; pointer-events: none; z-index: 2; diff --git a/src/components/video-editor/timeline/KeyframeMarkers.tsx b/src/components/video-editor/timeline/KeyframeMarkers.tsx new file mode 100644 index 0000000..ce4973d --- /dev/null +++ b/src/components/video-editor/timeline/KeyframeMarkers.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import { useTimelineContext } from "dnd-timeline"; + +interface Keyframe { + id: string; + time: number; +} + +interface KeyframeMarkersProps { + keyframes: Keyframe[]; + selectedKeyframeId: string | null; + setSelectedKeyframeId: (id: string | null) => void; +} + +const KeyframeMarkers: React.FC = ({ keyframes, selectedKeyframeId, setSelectedKeyframeId }) => { + const { sidebarWidth, range, valueToPixels } = useTimelineContext(); + return ( + <> + {keyframes.map(kf => { + const offset = valueToPixels(kf.time - range.start); + const isSelected = kf.id === selectedKeyframeId; + return ( +
{ + e.stopPropagation(); + setSelectedKeyframeId(kf.id); + }} + title={`Keyframe @ ${kf.time}ms`} + > +
+
+ ); + })} + + ); +}; + +export default KeyframeMarkers; diff --git a/src/components/video-editor/timeline/TimelineEditor.tsx b/src/components/video-editor/timeline/TimelineEditor.tsx index 8b62801..6599889 100644 --- a/src/components/video-editor/timeline/TimelineEditor.tsx +++ b/src/components/video-editor/timeline/TimelineEditor.tsx @@ -7,8 +7,10 @@ import { cn } from "@/lib/utils"; import TimelineWrapper from "./TimelineWrapper"; import Row from "./Row"; import Item from "./Item"; +import KeyframeMarkers from "./KeyframeMarkers"; import type { Range, Span } from "dnd-timeline"; import type { ZoomRegion } from "../types"; +import { v4 as uuidv4 } from 'uuid'; const ROW_ID = "row-1"; const FALLBACK_RANGE_MS = 1000; @@ -361,6 +363,7 @@ export default function TimelineEditor({ zoomRegions, onZoomAdded, onZoomSpanChange, + onZoomDelete, selectedZoomId, onSelectZoom, }: TimelineEditorProps) { @@ -373,6 +376,48 @@ export default function TimelineEditor({ ); const [range, setRange] = useState(() => createInitialRange(totalMs)); + const [keyframes, setKeyframes] = useState<{ id: string; time: number }[]>([]); + const [selectedKeyframeId, setSelectedKeyframeId] = useState(null); + + // Add keyframe at current playhead position + const addKeyframe = useCallback(() => { + if (totalMs === 0) return; + const time = Math.max(0, Math.min(currentTimeMs, totalMs)); + if (keyframes.some(kf => Math.abs(kf.time - time) < 1)) return; + setKeyframes(prev => [...prev, { id: uuidv4(), time }]); + }, [currentTimeMs, totalMs, keyframes]); + + // Delete selected keyframe + const deleteSelectedKeyframe = useCallback(() => { + if (!selectedKeyframeId) return; + setKeyframes(prev => prev.filter(kf => kf.id !== selectedKeyframeId)); + setSelectedKeyframeId(null); + }, [selectedKeyframeId]); + + // Delete selected zoom item + const deleteSelectedZoom = useCallback(() => { + if (!selectedZoomId) return; + onZoomDelete(selectedZoomId); + onSelectZoom(null); + }, [selectedZoomId, onZoomDelete, onSelectZoom]); + + // Listen for F key to add keyframe, Ctrl+D to remove selected keyframe or zoom item + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'f' || e.key === 'F') { + addKeyframe(); + } + if ((e.key === 'd' || e.key === 'D') && (e.ctrlKey || e.metaKey)) { + if (selectedKeyframeId) { + deleteSelectedKeyframe(); + } else if (selectedZoomId) { + deleteSelectedZoom(); + } + } + }; + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [addKeyframe, deleteSelectedKeyframe, deleteSelectedZoom, selectedKeyframeId, selectedZoomId]); useEffect(() => { setRange(createInitialRange(totalMs)); @@ -491,16 +536,22 @@ export default function TimelineEditor({
- ⇧ + ⌘ + Scroll + ⇧ + ⌘ + Scroll Pan - ⌘ + Scroll + ⌘ + Scroll Zoom + + F + Add Keyframe +
-
+
setSelectedKeyframeId(null)} + > +