feat: add canvas-based timeline with playhead, scrubbing, and zoom
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
40
tests/lib/timeline/interactions.test.ts
Normal file
40
tests/lib/timeline/interactions.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { clampZoom, computeZoom, type TimelineState } from '$lib/timeline/interactions';
|
||||
|
||||
describe('clampZoom', () => {
|
||||
it('clamps zoom above minimum', () => {
|
||||
expect(clampZoom(0.5, 60)).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('clamps zoom below maximum', () => {
|
||||
expect(clampZoom(10000, 60)).toBeLessThanOrEqual(600);
|
||||
});
|
||||
});
|
||||
|
||||
describe('computeZoom', () => {
|
||||
it('zooming in narrows the visible range', () => {
|
||||
const state: TimelineState = {
|
||||
visibleStart: 0,
|
||||
visibleEnd: 60,
|
||||
zoom: 1,
|
||||
width: 600,
|
||||
height: 160,
|
||||
};
|
||||
const result = computeZoom(state, 30, 1.1, 60);
|
||||
const newRange = result.visibleEnd - result.visibleStart;
|
||||
expect(newRange).toBeLessThan(60);
|
||||
});
|
||||
|
||||
it('zooming out widens the visible range', () => {
|
||||
const state: TimelineState = {
|
||||
visibleStart: 10,
|
||||
visibleEnd: 20,
|
||||
zoom: 6,
|
||||
width: 600,
|
||||
height: 160,
|
||||
};
|
||||
const result = computeZoom(state, 15, 0.9, 60);
|
||||
const newRange = result.visibleEnd - result.visibleStart;
|
||||
expect(newRange).toBeGreaterThan(10);
|
||||
});
|
||||
});
|
||||
57
tests/lib/timeline/renderer.test.ts
Normal file
57
tests/lib/timeline/renderer.test.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { timeToX, xToTime, type TimelineState } from '$lib/timeline/renderer';
|
||||
|
||||
function makeState(overrides: Partial<TimelineState> = {}): TimelineState {
|
||||
return {
|
||||
visibleStart: 0,
|
||||
visibleEnd: 60,
|
||||
zoom: 1,
|
||||
width: 600,
|
||||
height: 160,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('timeToX', () => {
|
||||
it('maps start of visible range to 0', () => {
|
||||
const state = makeState();
|
||||
expect(timeToX(0, state)).toBe(0);
|
||||
});
|
||||
|
||||
it('maps end of visible range to width', () => {
|
||||
const state = makeState();
|
||||
expect(timeToX(60, state)).toBe(600);
|
||||
});
|
||||
|
||||
it('maps midpoint correctly', () => {
|
||||
const state = makeState();
|
||||
expect(timeToX(30, state)).toBe(300);
|
||||
});
|
||||
|
||||
it('handles zoomed-in view', () => {
|
||||
const state = makeState({ visibleStart: 10, visibleEnd: 20 });
|
||||
expect(timeToX(15, state)).toBe(300);
|
||||
});
|
||||
});
|
||||
|
||||
describe('xToTime', () => {
|
||||
it('maps 0 to visible start', () => {
|
||||
const state = makeState();
|
||||
expect(xToTime(0, state)).toBe(0);
|
||||
});
|
||||
|
||||
it('maps width to visible end', () => {
|
||||
const state = makeState();
|
||||
expect(xToTime(600, state)).toBe(60);
|
||||
});
|
||||
|
||||
it('maps midpoint correctly', () => {
|
||||
const state = makeState();
|
||||
expect(xToTime(300, state)).toBe(30);
|
||||
});
|
||||
|
||||
it('inverse of timeToX', () => {
|
||||
const state = makeState({ visibleStart: 10, visibleEnd: 20 });
|
||||
expect(xToTime(timeToX(15, state), state)).toBeCloseTo(15);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user