aboutsummaryrefslogtreecommitdiffstats
path: root/src/app.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/app.js')
-rw-r--r--src/app.js100
1 files changed, 65 insertions, 35 deletions
diff --git a/src/app.js b/src/app.js
index 7f6baa3..f904cc3 100644
--- a/src/app.js
+++ b/src/app.js
@@ -6617,13 +6617,25 @@ if (INTENT_URI) {
// selection, and a drag that moved isn't a click on what it started on.
(() => {
if (typeof window === 'undefined' || matchMedia('(prefers-reduced-motion: reduce)').matches) return;
- const SCOPE = '.feed-page, .notes-feed';
+ // The wheel coasts whenever a feed or a thread is the page being scrolled
+ // — wherever the pointer happens to be: over the posts, the header, the
+ // rail, the wallet column beside the feed on a wide screen (Adam,
+ // 2026-10-16: "when my cursor is hovered over the right side bar or any
+ // part of the page that's not the feed, wheeling scrolls without
+ // coasting. can we make it coast no matter where my pointer is?") —
+ // except over something that takes the wheel itself: a text box, the
+ // chips strip, the lightbox, the pager, a box that scrolls on its own.
+ const SCOPE = '.feed-page, .notes-feed, .thread-page'; // the pages that coast (a thread scrolls the same as a feed)
const TAU = 325; // ms: how quickly a coast dies down (the phone's feel)
const VMAX = 16; // px/ms
const inScope = (t) => {
const el = t && t.closest ? t : t && t.parentElement;
- if (!el || !el.closest(SCOPE)) return false;
- if (el.closest('input, textarea, select, video, audio, [contenteditable], .feed-chips, .lightbox, .media-pager')) return false;
+ if (!el || !document.querySelector(SCOPE)) return false;
+ // (A video or an audio player is part of the feed: a notch over a clip
+ // coasts like one over the words — the wheel does nothing to a player
+ // itself, so there is nothing to leave it. Adam, 2026-10-16: "over a
+ // video it doesn't initiate a coast, just a single notch".)
+ if (el.closest('input, textarea, select, [contenteditable], .feed-chips, .lightbox, .media-pager')) return false;
// something inside that scrolls on its own (a caption, a list) keeps its wheel
for (let n = el; n && n !== document.body; n = n.parentElement) {
if (n.scrollHeight > n.clientHeight + 1 && /(auto|scroll)/.test(getComputedStyle(n).overflowY)) return false;
@@ -6653,48 +6665,66 @@ if (INTENT_URI) {
if (!raf && v) { last = performance.now(); raf = requestAnimationFrame(tick); }
};
// ---- the wheel
- // A turn back against the coast is a reader who flew past the post they
- // wanted and is hunting for it: the page is caught at once, and that notch
- // and the next few are short, quick steps (a quarter of a notch's travel,
- // settling fast) rather than a fresh flight the other way. Each notch on
- // in the same direction grows the step back towards a full one, so a few
- // flicks have the wheel flying again; a turn back while hunting starts the
- // small steps over, and a pause ends the hunt — the next notch is the
- // plain push from rest it has always been.
- const HUNT = [0.25, 0.35, 0.5, 0.7, 0.85]; // of a notch's travel, notch by notch
- const HUNT_TAU = 110; // ms: a hunting step settles quickly
- const HUNT_IDLE = 2000; // ms without a notch, and the hunt is over
- let hunt = -1, huntDir = 0, huntAt = 0; // the next HUNT entry while hunting
- const halt = () => { stop(); hunt = -1; };
+ // A turn back against a page that is flying is a brake: that notch only
+ // eases the coast to a stop (a quick easing, not a dead halt), and goes
+ // nowhere the other way. The next notch the same way is a gentle start
+ // back — half a notch's travel at half the speed — and the ones after
+ // wind up as usual. So the reader flying past a post stops on the first
+ // turn back, creeps back on the second, and gets going on the third.
+ // Against a coast that is merely strolling, or one that only just died,
+ // there is nothing worth braking: that turn back is the gentle start
+ // itself. (A spell of quarter-size "hunting" notches ramping back up over
+ // five turns was tried here and felt like a stutter and a slow patch; a
+ // full notch then two without wind-up felt like an overshoot; a gentle
+ // push straight away from a flight still overshot — Adam, 2026-10-16:
+ // "the first click of the wheel should just stop the coasting/flying
+ // maybe with a quick easing rather than a sudden stop and then the next
+ // notch should start going slowly in reverse and the next ones should
+ // start speeding up".)
+ const REVERSE = 0.5; // of a notch: the gentle start back
+ const BRAKE_V = 1.2; // px/ms: flying, worth a brake (a notch from rest peaks near 0.9)
+ const BRAKE_TAU = 40; // ms: the brake's easing (a flight at 8 px/ms stops within ~300 px)
+ const BRAKE_FOR = 2500; // ms: how long after a brake the next notch that way is the gentle start
+ let brake = null; // { dir, until }: the turn back that braked, awaiting its gentle start
+ // (lastDir/lastMoveAt: the tick's, above)
let smoothUntil = 0; // a trackpad's events were just seen: hands off
window.addEventListener('wheel', (e) => {
if (e.ctrlKey || e.shiftKey || e.defaultPrevented) return;
const notch = e.deltaMode === 1 || (e.wheelDeltaY && e.wheelDeltaY % 120 === 0 && !e.deltaX);
- if (!notch) { smoothUntil = e.timeStamp + 400; halt(); return; }
+ if (!notch) { smoothUntil = e.timeStamp + 400; stop(); return; }
if (e.timeStamp < smoothUntil || matchMedia('(pointer: coarse)').matches || !inScope(e.target)) return;
e.preventDefault();
const px = e.deltaMode === 1 ? e.deltaY * 40 : e.deltaY;
const dir = Math.sign(px);
if (!dir) return;
const now = e.timeStamp;
- if (hunt >= 0 && now - huntAt > HUNT_IDLE) hunt = -1;
- // the way the page is going: its coast, the coast that just died, or the hunt
- const going = v ? Math.sign(v) : now - lastMoveAt < 400 ? lastDir : hunt >= 0 ? huntDir : 0;
- if (going && dir !== going) { stop(); hunt = 0; huntDir = dir; }
- else if (hunt >= 0 && ++hunt >= HUNT.length) hunt = -1;
- if (hunt >= 0) {
- // A reversal catches the old coast immediately. Discard its fractional
- // step and frame clock too: after a busy frame, applying the NEW velocity
- // to the OLD elapsed time made the first reverse step jump. Give the
- // notch a small response now, then a quick coast over the rest of its
- // (short) travel; quick flicks still add up.
- huntAt = now;
- const travel = px * 3 * HUNT[hunt];
- const first = dir * Math.min(16, Math.abs(travel / HUNT_TAU) * 16);
+ // the way the page is going: its coast, or the coast that only just died
+ const going = v ? Math.sign(v) : now - lastMoveAt < 400 ? lastDir : 0;
+ const gentle = () => {
+ // The gentle start back catches whatever coast is left immediately.
+ // Discard its fractional step and frame clock too: after a busy frame,
+ // applying the NEW velocity to the OLD elapsed time made the first
+ // reverse step jump. A small response now, then the rest of its
+ // (half) travel coasts.
+ stop();
+ brake = null;
+ const travel = px * 3 * REVERSE;
+ const first = dir * Math.min(16, Math.abs(travel / TAU) * 16);
window.scrollBy({ top: first, behavior: 'instant' });
lastDir = dir; lastMoveAt = now;
- coast(v + (travel - first) / HUNT_TAU, HUNT_TAU);
+ coast((travel - first) / TAU, TAU);
+ };
+ if (going && dir !== going && Math.abs(v) >= BRAKE_V) {
+ // flying: this notch only brakes — the coast eases out over a few
+ // frames (from now: the frames a busy thread missed don't travel first)
+ tau = BRAKE_TAU; acc = 0; last = performance.now();
+ brake = { dir, until: now + BRAKE_FOR };
+ } else if (going && dir !== going) {
+ gentle();
+ } else if (brake && brake.dir === dir && now < brake.until) {
+ gentle(); // the notch after a brake: the gentle start back
} else {
+ brake = null;
// A notch into a page already moving pushes harder: the faster it
// goes, the more each turn adds (up to 2.5x), so keeping the wheel
// turning winds the speed up instead of settling at a stroll. From
@@ -6704,12 +6734,12 @@ if (INTENT_URI) {
}
}, { passive: false });
// anything else that moves the page takes over from a coast
- window.addEventListener('keydown', (e) => { if (/^(Arrow|Page|Home|End| )/.test(e.key)) halt(); });
- window.addEventListener('touchstart', halt, { passive: true });
+ window.addEventListener('keydown', (e) => { if (/^(Arrow|Page|Home|End| )/.test(e.key)) stop(); });
+ window.addEventListener('touchstart', stop, { passive: true });
// ---- grab and flick
let drag = null;
document.addEventListener('pointerdown', (e) => {
- if (raf) halt(); // a click catches a coasting page, like a finger does
+ if (raf) stop(); // a click catches a coasting page, like a finger does
if (e.pointerType !== 'mouse' || e.button !== 0 || !inScope(e.target)) return;
drag = { x: e.clientX, y: e.clientY, ly: e.clientY, on: false, pts: [] };
});