From 27c199e0901bd108c96c6518e77b43fbe5b08d4e Mon Sep 17 00:00:00 2001 From: cottongin Date: Tue, 22 Sep 2026 16:51:56 -0400 Subject: [PATCH] feat: larger timeline timestamps with mouse-proximity magnification Co-authored-by: Cursor --- src/lib/components/Timeline.svelte | 21 ++++++++++++++++++--- src/lib/timeline/renderer.ts | 29 +++++++++++++++++++++++------ 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/src/lib/components/Timeline.svelte b/src/lib/components/Timeline.svelte index 3c321e5..9eca3fa 100644 --- a/src/lib/components/Timeline.svelte +++ b/src/lib/components/Timeline.svelte @@ -36,6 +36,7 @@ let isMinimapDragging = $state(false); let minimapDragStartX = $state(0); let minimapDragStartVisibleStart = $state(0); + let mouseX = $state(null); let isZoomed = $derived(timelineState.zoom > 1.01); @@ -84,7 +85,8 @@ clipStore.pendingInPoint, waveformData, session.thumbnailSpritesheets, - session.status === 'ready' + session.status === 'ready', + mouseX ); ctx.restore(); } @@ -154,6 +156,7 @@ void timelineState.width; void timelineState.height; void canvas; + void mouseX; requestDraw(); }); @@ -284,6 +287,15 @@ dragTarget = null; } + function handleMouseTrack(e: MouseEvent) { + mouseX = getCanvasX(e); + } + + function handleMouseLeave() { + handleMouseUp(); + mouseX = null; + } + function handleWheel(e: WheelEvent) { e.preventDefault(); @@ -376,9 +388,12 @@ { + handleMouseMove(e); + handleMouseTrack(e); + }} onmouseup={handleMouseUp} - onmouseleave={handleMouseUp} + onmouseleave={handleMouseLeave} onwheel={handleWheel} > diff --git a/src/lib/timeline/renderer.ts b/src/lib/timeline/renderer.ts index 932c782..782f63c 100644 --- a/src/lib/timeline/renderer.ts +++ b/src/lib/timeline/renderer.ts @@ -27,6 +27,9 @@ const THUMB_LANE_HEIGHT = 50; const WAVEFORM_LANE_HEIGHT = 40; const CONTROLS_LANE_HEIGHT = 30; const TICK_HEIGHT = 10; +const BASE_FONT_SIZE = 12; +const MAGNIFY_BONUS = 6; +const MAGNIFY_RADIUS = 80; export function drawTimeline( ctx: CanvasRenderingContext2D, @@ -38,7 +41,8 @@ export function drawTimeline( pendingInPoint: number | null = null, waveform: WaveformData = { tiers: null }, thumbnailSpritesheets: ThumbnailSpritesheet[] = [], - hasVideo: boolean = false + hasVideo: boolean = false, + mouseX: number | null = null ): void { const { width, height } = state; ctx.clearRect(0, 0, width, height); @@ -46,7 +50,7 @@ export function drawTimeline( ctx.fillStyle = '#181825'; ctx.fillRect(0, 0, width, height); - drawTimeTicks(ctx, state, duration); + drawTimeTicks(ctx, state, duration, mouseX); if (thumbnailSpritesheets.length > 0) { drawThumbnails(ctx, state, thumbnailSpritesheets, duration, 0, THUMB_LANE_HEIGHT); @@ -68,27 +72,40 @@ export function drawTimeline( function drawTimeTicks( ctx: CanvasRenderingContext2D, state: TimelineState, - duration: number + duration: number, + mouseX: number | null ): void { const range = state.visibleEnd - state.visibleStart; const tickInterval = getTickInterval(range, state.width); const startTick = Math.floor(state.visibleStart / tickInterval) * tickInterval; - ctx.fillStyle = '#6c7086'; - ctx.font = '10px -apple-system, sans-serif'; ctx.textAlign = 'center'; + const baselineY = state.height - CONTROLS_LANE_HEIGHT + TICK_HEIGHT + 14; for (let t = startTick; t <= state.visibleEnd; t += tickInterval) { if (t < 0 || t > duration) continue; const x = timeToX(t, state); + ctx.strokeStyle = '#45475a'; ctx.beginPath(); ctx.moveTo(x, state.height - CONTROLS_LANE_HEIGHT); ctx.lineTo(x, state.height - CONTROLS_LANE_HEIGHT + TICK_HEIGHT); ctx.stroke(); + let fontSize = BASE_FONT_SIZE; + if (mouseX !== null) { + const distance = Math.abs(x - mouseX); + if (distance < MAGNIFY_RADIUS) { + const scale = (1 + Math.cos((Math.PI * distance) / MAGNIFY_RADIUS)) / 2; + fontSize = BASE_FONT_SIZE + MAGNIFY_BONUS * scale; + } + } + + ctx.fillStyle = '#6c7086'; + ctx.font = `${Math.round(fontSize)}px -apple-system, sans-serif`; + const label = formatTickLabel(t); - ctx.fillText(label, x, state.height - CONTROLS_LANE_HEIGHT + TICK_HEIGHT + 12); + ctx.fillText(label, x, baselineY); } }