Let the page flick work anywhere, with a setting to turn it off #13
5 changed files with 71 additions and 11 deletions
|
|
@ -23,7 +23,7 @@ The image builds the PWA and serves it with nginx. Put an HTTPS reverse proxy in
|
||||||
## Using it
|
## Using it
|
||||||
|
|
||||||
- **Tools** (bottom bar): Text `T`, Pen `P`, Highlighter `H`, Eraser `E`, Import PDF, Undo/Redo, zoom. The pen's eraser end erases too.
|
- **Tools** (bottom bar): Text `T`, Pen `P`, Highlighter `H`, Eraser `E`, Import PDF, Undo/Redo, zoom. The pen's eraser end erases too.
|
||||||
- **Pages**: the "+" circles beside the active page add a page there (a quick finger fling past a free edge highlights that side's "+"); `Alt+Arrow` moves to (or creates) the neighbour. Right-click a page or use its `⋯` button to insert, resize or delete it.
|
- **Pages**: the "+" circles beside the active page add a page there (a quick finger flick toward a free side offers one there; turn it off under Settings → Pen & touch); `Alt+Arrow` moves to (or creates) the neighbour. Right-click a page or use its `⋯` button to insert, resize or delete it.
|
||||||
- **Grid**: the faint background grid has one cell per active page; Settings → Appearance can hide it or subdivide it.
|
- **Grid**: the faint background grid has one cell per active page; Settings → Appearance can hide it or subdivide it.
|
||||||
- **Importing a PDF** into a non-empty canvas shows every free spot; click one to place the chain. Into a blank canvas it simply replaces the empty page. The Files panel can also import a PDF as a new canvas.
|
- **Importing a PDF** into a non-empty canvas shows every free spot; click one to place the chain. Into a blank canvas it simply replaces the empty page. The Files panel can also import a PDF as a new canvas.
|
||||||
- **View**: wheel/trackpad pans, `Ctrl`+wheel or pinch zooms, `Ctrl+0` fits the page, `Ctrl+9` shows the whole tree, space-drag or middle-drag pans.
|
- **View**: wheel/trackpad pans, `Ctrl`+wheel or pinch zooms, `Ctrl+0` fits the page, `Ctrl+9` shows the whole tree, space-drag or middle-drag pans.
|
||||||
|
|
|
||||||
2
spec.md
2
spec.md
|
|
@ -9,7 +9,7 @@ One canvas = one tree = one `.pdf` file.
|
||||||
- A branch can only ever continue in the direction it started (strictly linear — no sub-branches off a branch).
|
- A branch can only ever continue in the direction it started (strictly linear — no sub-branches off a branch).
|
||||||
- A trunk page can have at most one left branch and one right branch.
|
- A trunk page can have at most one left branch and one right branch.
|
||||||
- **PDF import**: creates a chain of pages in whatever direction you pick when placing it (up/down = extends the trunk, left/right = becomes a branch off the trunk page you clicked). Only one trunk exists per canvas — additional imported PDFs must attach as a branch off some trunk page, or extend an existing branch further outward in its established direction.
|
- **PDF import**: creates a chain of pages in whatever direction you pick when placing it (up/down = extends the trunk, left/right = becomes a branch off the trunk page you clicked). Only one trunk exists per canvas — additional imported PDFs must attach as a branch off some trunk page, or extend an existing branch further outward in its established direction.
|
||||||
- Each free side of the active page shows a small "+" circle just off its edge; tapping it (or `Alt+Arrow`) adds a blank page there. A quick finger fling past a free edge highlights that side's "+" for a few seconds, but never adds a page by itself.
|
- Each free side of the active page shows a small "+" circle just off its edge; tapping it (or `Alt+Arrow`) adds a blank page there. A quick finger fling toward a free side offers a page there for a few seconds: that side's "+" pulses, or, if it is off screen, an "Add page" button appears at that edge of the screen. The fling never adds a page by itself, and can be turned off in Settings.
|
||||||
- The canvas background is a faint grid with one cell per active page, aligned to its edges; Settings can hide it or subdivide it.
|
- The canvas background is a faint grid with one cell per active page, aligned to its edges; Settings can hide it or subdivide it.
|
||||||
- Deleting a page removes it (undo via Ctrl+Z); no permanent trash.
|
- Deleting a page removes it (undo via Ctrl+Z); no permanent trash.
|
||||||
- Default zoom: one page fills the screen. Zoom out reveals the tree.
|
- Default zoom: one page fills the screen. Zoom out reveals the tree.
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,8 @@
|
||||||
|
|
||||||
const placeSlots = $derived(tools.placing ? freeSlots(doc.tree) : []);
|
const placeSlots = $derived(tools.placing ? freeSlots(doc.tree) : []);
|
||||||
|
|
||||||
// A finger fling past the active page's free edge highlights the plus on that side.
|
// A finger fling toward a free side of the active page offers a new page there: the plus
|
||||||
|
// pulses if it's on screen, otherwise a button appears at that edge of the screen.
|
||||||
let offer = $state<Slot | null>(null);
|
let offer = $state<Slot | null>(null);
|
||||||
let offerTimer: ReturnType<typeof setTimeout> | undefined;
|
let offerTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
|
|
@ -130,18 +131,21 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function offerAfterFling(samples: Sample[]) {
|
function offerAfterFling(samples: Sample[]) {
|
||||||
|
if (!settings.data.flingToAdd) return;
|
||||||
const dir = flingDir(samples);
|
const dir = flingDir(samples);
|
||||||
const id = doc.activeId;
|
const id = doc.activeId;
|
||||||
const r = id && doc.rects.get(id);
|
|
||||||
const slot = dir && ghostSlots.find((s) => s.anchor === id && s.dir === dir);
|
const slot = dir && ghostSlots.find((s) => s.anchor === id && s.dir === dir);
|
||||||
if (!r || !slot) return;
|
if (slot) setOffer(slot);
|
||||||
// Only once that edge has come into view, i.e. the fling went past the end.
|
}
|
||||||
|
|
||||||
|
/** The offered plus is off screen, so the offer shows as an edge-of-screen button instead. */
|
||||||
|
const offerOffscreen = $derived.by(() => {
|
||||||
|
const r = offer && plusRect(offer);
|
||||||
|
if (!r) return false;
|
||||||
const a = viewport.toScreen(r.x, r.y);
|
const a = viewport.toScreen(r.x, r.y);
|
||||||
const b = viewport.toScreen(r.x + r.width, r.y + r.height);
|
const b = viewport.toScreen(r.x + r.width, r.y + r.height);
|
||||||
const edge = { left: a.x, right: b.x, up: a.y, down: b.y }[slot.dir];
|
return b.x < 0 || b.y < 0 || a.x > viewport.width || a.y > viewport.height;
|
||||||
const size = slot.dir === 'left' || slot.dir === 'right' ? viewport.width : viewport.height;
|
});
|
||||||
if (edge >= 0 && edge <= size) setOffer(slot);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show every possible spot when an imported PDF is waiting to be placed.
|
// Show every possible spot when an imported PDF is waiting to be placed.
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
|
|
@ -338,7 +342,7 @@
|
||||||
const fingerInks = tools.inking && settings.data.fingerDraw && !palm.penSeen;
|
const fingerInks = tools.inking && settings.data.fingerDraw && !palm.penSeen;
|
||||||
// Taps on an add-page button are palm-checked like navigation: still rejected for
|
// Taps on an add-page button are palm-checked like navigation: still rejected for
|
||||||
// pen-down, palm-sized contacts and right after pen use, but not for the 5 s pen session.
|
// pen-down, palm-sized contacts and right after pen use, but not for the 5 s pen session.
|
||||||
const plus = target.closest<HTMLElement>('.ghost');
|
const plus = target.closest<HTMLElement>('.ghost, .offer-pill');
|
||||||
if (e.pointerType === 'touch') {
|
if (e.pointerType === 'touch') {
|
||||||
const decision = palm.evaluate(e, touchIntent(fingerInks, !!plus));
|
const decision = palm.evaluate(e, touchIntent(fingerInks, !!plus));
|
||||||
if (!decision.accept) return; // palm: ignore entirely
|
if (!decision.accept) return; // palm: ignore entirely
|
||||||
|
|
@ -707,6 +711,20 @@
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if offer && offerOffscreen}
|
||||||
|
<!-- Like the plus buttons, taps go through the palm-checked gesture. -->
|
||||||
|
<button
|
||||||
|
class="offer-pill"
|
||||||
|
data-anchor={offer.anchor}
|
||||||
|
data-dir={offer.dir}
|
||||||
|
data-side={offer.dir}
|
||||||
|
onclick={(e) => e.detail === 0 && offer && addPage(offer.anchor, offer.dir)}
|
||||||
|
>
|
||||||
|
<CirclePlusIcon size={18} strokeWidth={1.75} />
|
||||||
|
Add page
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if eraserAt && tools.tool === 'eraser'}
|
{#if eraserAt && tools.tool === 'eraser'}
|
||||||
<div class="eraser-ring" style="left:{eraserAt.x}px; top:{eraserAt.y}px; width:{tools.eraserRadius * 2}px; height:{tools.eraserRadius * 2}px"></div>
|
<div class="eraser-ring" style="left:{eraserAt.x}px; top:{eraserAt.y}px; width:{tools.eraserRadius * 2}px; height:{tools.eraserRadius * 2}px"></div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -834,6 +852,41 @@
|
||||||
transform: scale(1.3);
|
transform: scale(1.3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.offer-pill {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 10px 16px 10px 12px;
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--accent);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.offer-pill[data-side='right'] {
|
||||||
|
right: 12px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
.offer-pill[data-side='left'] {
|
||||||
|
left: 12px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
.offer-pill[data-side='up'] {
|
||||||
|
top: 12px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
.offer-pill[data-side='down'] {
|
||||||
|
/* Clear of the toolbar. */
|
||||||
|
bottom: 72px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.ghost.offer .plus {
|
.ghost.offer .plus {
|
||||||
animation: none;
|
animation: none;
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,10 @@
|
||||||
<div><div class="name">Draw with finger</div><div class="desc">When off, a single finger always pans; only pen and mouse draw. Once you use a pen, fingers only pan and zoom.</div></div>
|
<div><div class="name">Draw with finger</div><div class="desc">When off, a single finger always pans; only pen and mouse draw. Once you use a pen, fingers only pan and zoom.</div></div>
|
||||||
<input type="checkbox" class="toggle" bind:checked={s.fingerDraw} onchange={save} />
|
<input type="checkbox" class="toggle" bind:checked={s.fingerDraw} onchange={save} />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div><div class="name">Swipe to add pages</div><div class="desc">A quick one-finger flick toward a free side of the active page offers to add a page there.</div></div>
|
||||||
|
<input type="checkbox" class="toggle" bind:checked={s.flingToAdd} onchange={save} />
|
||||||
|
</div>
|
||||||
<h3>Palm rejection</h3>
|
<h3>Palm rejection</h3>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div><div class="name">Pen session lockout</div><div class="desc">Ignore touches while the pen is writing, and finger ink for 5 seconds after it was used.</div></div>
|
<div><div class="name">Pen session lockout</div><div class="desc">Ignore touches while the pen is writing, and finger ink for 5 seconds after it was used.</div></div>
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ interface SettingsData {
|
||||||
pageStyle: PageStylePref;
|
pageStyle: PageStylePref;
|
||||||
grid: GridSettings;
|
grid: GridSettings;
|
||||||
fingerDraw: boolean;
|
fingerDraw: boolean;
|
||||||
|
/** A finger fling toward a free side of the active page offers a new page there. */
|
||||||
|
flingToAdd: boolean;
|
||||||
palm: PalmOptions;
|
palm: PalmOptions;
|
||||||
github: GitHubSettings;
|
github: GitHubSettings;
|
||||||
/** Minutes between automatic pushes (0 = manual only). */
|
/** Minutes between automatic pushes (0 = manual only). */
|
||||||
|
|
@ -39,6 +41,7 @@ const DEFAULTS: SettingsData = {
|
||||||
pageStyle: 'match',
|
pageStyle: 'match',
|
||||||
grid: { show: true, divisions: 1 },
|
grid: { show: true, divisions: 1 },
|
||||||
fingerDraw: true,
|
fingerDraw: true,
|
||||||
|
flingToAdd: true,
|
||||||
palm: DEFAULT_PALM,
|
palm: DEFAULT_PALM,
|
||||||
github: { token: '', owner: '', repo: '', branch: 'main', dir: '' },
|
github: { token: '', owner: '', repo: '', branch: 'main', dir: '' },
|
||||||
pushInterval: 5,
|
pushInterval: 5,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue