feat: larger timeline timestamps with mouse-proximity magnification
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -36,6 +36,7 @@
|
|||||||
let isMinimapDragging = $state(false);
|
let isMinimapDragging = $state(false);
|
||||||
let minimapDragStartX = $state(0);
|
let minimapDragStartX = $state(0);
|
||||||
let minimapDragStartVisibleStart = $state(0);
|
let minimapDragStartVisibleStart = $state(0);
|
||||||
|
let mouseX = $state<number | null>(null);
|
||||||
|
|
||||||
let isZoomed = $derived(timelineState.zoom > 1.01);
|
let isZoomed = $derived(timelineState.zoom > 1.01);
|
||||||
|
|
||||||
@@ -84,7 +85,8 @@
|
|||||||
clipStore.pendingInPoint,
|
clipStore.pendingInPoint,
|
||||||
waveformData,
|
waveformData,
|
||||||
session.thumbnailSpritesheets,
|
session.thumbnailSpritesheets,
|
||||||
session.status === 'ready'
|
session.status === 'ready',
|
||||||
|
mouseX
|
||||||
);
|
);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
@@ -154,6 +156,7 @@
|
|||||||
void timelineState.width;
|
void timelineState.width;
|
||||||
void timelineState.height;
|
void timelineState.height;
|
||||||
void canvas;
|
void canvas;
|
||||||
|
void mouseX;
|
||||||
requestDraw();
|
requestDraw();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -284,6 +287,15 @@
|
|||||||
dragTarget = null;
|
dragTarget = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleMouseTrack(e: MouseEvent) {
|
||||||
|
mouseX = getCanvasX(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseLeave() {
|
||||||
|
handleMouseUp();
|
||||||
|
mouseX = null;
|
||||||
|
}
|
||||||
|
|
||||||
function handleWheel(e: WheelEvent) {
|
function handleWheel(e: WheelEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@@ -376,9 +388,12 @@
|
|||||||
<canvas
|
<canvas
|
||||||
bind:this={canvas}
|
bind:this={canvas}
|
||||||
onmousedown={handleMouseDown}
|
onmousedown={handleMouseDown}
|
||||||
onmousemove={handleMouseMove}
|
onmousemove={(e) => {
|
||||||
|
handleMouseMove(e);
|
||||||
|
handleMouseTrack(e);
|
||||||
|
}}
|
||||||
onmouseup={handleMouseUp}
|
onmouseup={handleMouseUp}
|
||||||
onmouseleave={handleMouseUp}
|
onmouseleave={handleMouseLeave}
|
||||||
onwheel={handleWheel}
|
onwheel={handleWheel}
|
||||||
></canvas>
|
></canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ const THUMB_LANE_HEIGHT = 50;
|
|||||||
const WAVEFORM_LANE_HEIGHT = 40;
|
const WAVEFORM_LANE_HEIGHT = 40;
|
||||||
const CONTROLS_LANE_HEIGHT = 30;
|
const CONTROLS_LANE_HEIGHT = 30;
|
||||||
const TICK_HEIGHT = 10;
|
const TICK_HEIGHT = 10;
|
||||||
|
const BASE_FONT_SIZE = 12;
|
||||||
|
const MAGNIFY_BONUS = 6;
|
||||||
|
const MAGNIFY_RADIUS = 80;
|
||||||
|
|
||||||
export function drawTimeline(
|
export function drawTimeline(
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
@@ -38,7 +41,8 @@ export function drawTimeline(
|
|||||||
pendingInPoint: number | null = null,
|
pendingInPoint: number | null = null,
|
||||||
waveform: WaveformData = { tiers: null },
|
waveform: WaveformData = { tiers: null },
|
||||||
thumbnailSpritesheets: ThumbnailSpritesheet[] = [],
|
thumbnailSpritesheets: ThumbnailSpritesheet[] = [],
|
||||||
hasVideo: boolean = false
|
hasVideo: boolean = false,
|
||||||
|
mouseX: number | null = null
|
||||||
): void {
|
): void {
|
||||||
const { width, height } = state;
|
const { width, height } = state;
|
||||||
ctx.clearRect(0, 0, width, height);
|
ctx.clearRect(0, 0, width, height);
|
||||||
@@ -46,7 +50,7 @@ export function drawTimeline(
|
|||||||
ctx.fillStyle = '#181825';
|
ctx.fillStyle = '#181825';
|
||||||
ctx.fillRect(0, 0, width, height);
|
ctx.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
drawTimeTicks(ctx, state, duration);
|
drawTimeTicks(ctx, state, duration, mouseX);
|
||||||
|
|
||||||
if (thumbnailSpritesheets.length > 0) {
|
if (thumbnailSpritesheets.length > 0) {
|
||||||
drawThumbnails(ctx, state, thumbnailSpritesheets, duration, 0, THUMB_LANE_HEIGHT);
|
drawThumbnails(ctx, state, thumbnailSpritesheets, duration, 0, THUMB_LANE_HEIGHT);
|
||||||
@@ -68,27 +72,40 @@ export function drawTimeline(
|
|||||||
function drawTimeTicks(
|
function drawTimeTicks(
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
state: TimelineState,
|
state: TimelineState,
|
||||||
duration: number
|
duration: number,
|
||||||
|
mouseX: number | null
|
||||||
): void {
|
): void {
|
||||||
const range = state.visibleEnd - state.visibleStart;
|
const range = state.visibleEnd - state.visibleStart;
|
||||||
const tickInterval = getTickInterval(range, state.width);
|
const tickInterval = getTickInterval(range, state.width);
|
||||||
const startTick = Math.floor(state.visibleStart / tickInterval) * tickInterval;
|
const startTick = Math.floor(state.visibleStart / tickInterval) * tickInterval;
|
||||||
|
|
||||||
ctx.fillStyle = '#6c7086';
|
|
||||||
ctx.font = '10px -apple-system, sans-serif';
|
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
|
const baselineY = state.height - CONTROLS_LANE_HEIGHT + TICK_HEIGHT + 14;
|
||||||
|
|
||||||
for (let t = startTick; t <= state.visibleEnd; t += tickInterval) {
|
for (let t = startTick; t <= state.visibleEnd; t += tickInterval) {
|
||||||
if (t < 0 || t > duration) continue;
|
if (t < 0 || t > duration) continue;
|
||||||
const x = timeToX(t, state);
|
const x = timeToX(t, state);
|
||||||
|
|
||||||
ctx.strokeStyle = '#45475a';
|
ctx.strokeStyle = '#45475a';
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(x, state.height - CONTROLS_LANE_HEIGHT);
|
ctx.moveTo(x, state.height - CONTROLS_LANE_HEIGHT);
|
||||||
ctx.lineTo(x, state.height - CONTROLS_LANE_HEIGHT + TICK_HEIGHT);
|
ctx.lineTo(x, state.height - CONTROLS_LANE_HEIGHT + TICK_HEIGHT);
|
||||||
ctx.stroke();
|
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);
|
const label = formatTickLabel(t);
|
||||||
ctx.fillText(label, x, state.height - CONTROLS_LANE_HEIGHT + TICK_HEIGHT + 12);
|
ctx.fillText(label, x, baselineY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user