feat: add clip management with I/O marking, timeline markers, and clip list
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,10 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { session } from '$lib/stores/videoSession.svelte';
|
||||
import {
|
||||
clipStore,
|
||||
selectClip,
|
||||
updateClip,
|
||||
} from '$lib/stores/clips.svelte';
|
||||
import { drawTimeline, type TimelineState } from '$lib/timeline/renderer';
|
||||
import {
|
||||
computeZoom,
|
||||
handleClick as computeClickTime,
|
||||
} from '$lib/timeline/interactions';
|
||||
import { hitTestClip } from '$lib/timeline/clipRenderer';
|
||||
|
||||
type ClipDragTarget = { clipId: string; edge: 'start' | 'end' };
|
||||
|
||||
let canvas = $state<HTMLCanvasElement | null>(null);
|
||||
let containerEl = $state<HTMLDivElement | null>(null);
|
||||
@@ -19,6 +27,7 @@
|
||||
});
|
||||
|
||||
let isDragging = $state(false);
|
||||
let dragTarget = $state<ClipDragTarget | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
if (session.duration > 0) {
|
||||
@@ -53,7 +62,15 @@
|
||||
if (!ctx || !canvas) return;
|
||||
ctx.save();
|
||||
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
|
||||
drawTimeline(ctx, timelineState, session.currentTime, session.duration);
|
||||
drawTimeline(
|
||||
ctx,
|
||||
timelineState,
|
||||
session.currentTime,
|
||||
session.duration,
|
||||
clipStore.clips,
|
||||
clipStore.selectedClipId,
|
||||
clipStore.pendingInPoint
|
||||
);
|
||||
ctx.restore();
|
||||
animFrameId = requestAnimationFrame(draw);
|
||||
}
|
||||
@@ -68,6 +85,12 @@
|
||||
return e.clientX - rect.left;
|
||||
}
|
||||
|
||||
function getCanvasY(e: MouseEvent): number {
|
||||
if (!canvas) return 0;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
return e.clientY - rect.top;
|
||||
}
|
||||
|
||||
function seekTo(time: number) {
|
||||
session.currentTime = time;
|
||||
const videoEl = document.querySelector('video');
|
||||
@@ -77,8 +100,22 @@
|
||||
}
|
||||
|
||||
function handleMouseDown(e: MouseEvent) {
|
||||
isDragging = true;
|
||||
const x = getCanvasX(e);
|
||||
const y = getCanvasY(e);
|
||||
|
||||
const hit = hitTestClip(x, y, timelineState, clipStore.clips);
|
||||
if (hit) {
|
||||
if (hit.edge === 'body') {
|
||||
selectClip(hit.clipId);
|
||||
} else if (hit.edge === 'start' || hit.edge === 'end') {
|
||||
dragTarget = { clipId: hit.clipId, edge: hit.edge };
|
||||
selectClip(hit.clipId);
|
||||
}
|
||||
isDragging = true;
|
||||
return;
|
||||
}
|
||||
|
||||
isDragging = true;
|
||||
const targetTime = computeClickTime(x, timelineState, session.duration);
|
||||
seekTo(targetTime);
|
||||
}
|
||||
@@ -86,12 +123,31 @@
|
||||
function handleMouseMove(e: MouseEvent) {
|
||||
if (!isDragging) return;
|
||||
const x = getCanvasX(e);
|
||||
|
||||
if (dragTarget) {
|
||||
const time = computeClickTime(x, timelineState, session.duration);
|
||||
switch (dragTarget.edge) {
|
||||
case 'start':
|
||||
updateClip(dragTarget.clipId, { startTime: time });
|
||||
break;
|
||||
case 'end':
|
||||
updateClip(dragTarget.clipId, { endTime: time });
|
||||
break;
|
||||
default: {
|
||||
const _exhaustive: never = dragTarget.edge;
|
||||
void _exhaustive;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const targetTime = computeClickTime(x, timelineState, session.duration);
|
||||
seekTo(targetTime);
|
||||
}
|
||||
|
||||
function handleMouseUp() {
|
||||
isDragging = false;
|
||||
dragTarget = null;
|
||||
}
|
||||
|
||||
function handleWheel(e: WheelEvent) {
|
||||
|
||||
Reference in New Issue
Block a user