Offer a new page after a finger fling past the page edge
A quick one-finger flick toward a free side of the active page, once that edge is on screen, highlights and pulses the plus button on that side for three seconds. It never adds a page by itself, so ordinary panning can't create pages; a tap on the highlighted plus does. Fling detection lives in ink/fling.ts with tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GaoZtJQZwLXkEgWs8twCia
This commit is contained in:
parent
abb11ad407
commit
5a501a5c87
3 changed files with 148 additions and 3 deletions
43
src/lib/ink/fling.test.ts
Normal file
43
src/lib/ink/fling.test.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { flingDir, pushSample, type Sample } from './fling';
|
||||
|
||||
/** A straight drag from (0,0) by (dx,dy) over `ms`, sampled every 16 ms. */
|
||||
function drag(dx: number, dy: number, ms: number): Sample[] {
|
||||
const out: Sample[] = [];
|
||||
for (let t = 0; t <= ms; t += 16) pushSample(out, { x: (dx * t) / ms, y: (dy * t) / ms, t });
|
||||
return out;
|
||||
}
|
||||
|
||||
describe('flingDir', () => {
|
||||
it('maps a flick to the side it reveals', () => {
|
||||
expect(flingDir(drag(-200, 0, 96))).toBe('right');
|
||||
expect(flingDir(drag(200, 0, 96))).toBe('left');
|
||||
expect(flingDir(drag(0, -200, 96))).toBe('down');
|
||||
expect(flingDir(drag(0, 200, 96))).toBe('up');
|
||||
});
|
||||
|
||||
it('ignores slow drags', () => {
|
||||
expect(flingDir(drag(-200, 0, 800))).toBeNull();
|
||||
});
|
||||
|
||||
it('ignores diagonal flicks', () => {
|
||||
expect(flingDir(drag(-200, -150, 96))).toBeNull();
|
||||
});
|
||||
|
||||
it('ignores a drag that stops before lifting', () => {
|
||||
const s = drag(-300, 0, 96);
|
||||
const end = s.at(-1)!;
|
||||
for (let t = 16; t <= 160; t += 16) pushSample(s, { ...end, t: end.t + t });
|
||||
expect(flingDir(s)).toBeNull();
|
||||
});
|
||||
|
||||
it('needs at least two samples', () => {
|
||||
expect(flingDir([])).toBeNull();
|
||||
expect(flingDir([{ x: 0, y: 0, t: 0 }])).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps the trail short', () => {
|
||||
const s = drag(-2000, 0, 2000);
|
||||
expect(s.at(-1)!.t - s[0].t).toBeLessThanOrEqual(216);
|
||||
});
|
||||
});
|
||||
41
src/lib/ink/fling.ts
Normal file
41
src/lib/ink/fling.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// Fling detection for one-finger pans: a quick flick that ends the drag.
|
||||
|
||||
import type { Dir } from '$lib/model/tree';
|
||||
|
||||
export interface Sample {
|
||||
x: number;
|
||||
y: number;
|
||||
t: number;
|
||||
}
|
||||
|
||||
/** Only the last stretch of the drag counts. */
|
||||
const WINDOW_MS = 100;
|
||||
/** px/ms; a slow drag that stops is not a fling. */
|
||||
const MIN_SPEED = 0.6;
|
||||
/** The main axis must dominate by this much. */
|
||||
const MIN_RATIO = 2;
|
||||
|
||||
/**
|
||||
* Side of the canvas a fling heads for, or null. Moving the finger left pulls in
|
||||
* what's on the right, so a leftward flick returns 'right'.
|
||||
*/
|
||||
export function flingDir(samples: readonly Sample[]): Dir | null {
|
||||
const last = samples.at(-1);
|
||||
if (!last) return null;
|
||||
const first = samples.find((s) => last.t - s.t <= WINDOW_MS);
|
||||
if (!first || first === last) return null;
|
||||
const dt = last.t - first.t;
|
||||
if (dt <= 0) return null;
|
||||
const dx = last.x - first.x;
|
||||
const dy = last.y - first.y;
|
||||
if (Math.hypot(dx, dy) / dt < MIN_SPEED) return null;
|
||||
if (Math.abs(dx) >= Math.abs(dy) * MIN_RATIO) return dx < 0 ? 'right' : 'left';
|
||||
if (Math.abs(dy) >= Math.abs(dx) * MIN_RATIO) return dy < 0 ? 'down' : 'up';
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Keeps a short trail of pointer samples for flingDir. */
|
||||
export function pushSample(samples: Sample[], s: Sample) {
|
||||
samples.push(s);
|
||||
while (samples.length > 2 && s.t - samples[0].t > WINDOW_MS * 2) samples.shift();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue