diff options
| author | Adam Soltys <[email protected]> | 2026-10-16 21:57:44 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-16 21:57:44 -0700 |
| commit | 53d4d26bdaa889bc50d79082c373ae04d1a96e00 (patch) | |
| tree | 559f067742d4db3dd93befc960931c52f9e023b5 | |
| parent | ed3c047bf3969f81488a2cf9cb52af009407f07f (diff) | |
| download | coinosv3-53d4d26bdaa889bc50d79082c373ae04d1a96e00.tar.gz coinosv3-53d4d26bdaa889bc50d79082c373ae04d1a96e00.zip | |
A turn back against a flying feed brakes first: that notch eases the coast to a stop, the next is the gentle start back, the ones after wind up.
The gentle half 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"). Now, when the page is flying (over 1.2 px/ms — a notch from rest peaks near 0.9), the first notch back only brakes: the coast's decay is shortened to 40 ms from that moment (the frames a busy thread missed don't travel first), so a flight at 8 px/ms stops within ~300 px, eased. The notch after it, up to 2.5 s later, is the gentle start — half a notch's travel at half the speed — and the ones after wind up as usual. Against a stroll or a coast that only just died there is nothing worth braking: the turn back is the gentle start itself, as before.
feed-wheel-reverse-test: the brake moves nothing by itself and eases out within a short way, never the other way; the next notch creeps back half a notch; the ones after wind up; a notch back a second and a half later is still gentle, three seconds later a full push; a stroll turned back starts gently at once.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp
| -rw-r--r-- | src/app.js | 56 | ||||
| -rw-r--r-- | tools/feed-wheel-reverse-test.js | 64 |
2 files changed, 70 insertions, 50 deletions
@@ -6657,18 +6657,27 @@ if (INTENT_URI) { if (!raf && v) { last = performance.now(); raf = requestAnimationFrame(tick); } }; // ---- the wheel - // A turn back against the coast catches the page at once and is a gentler - // push the other way — half a notch's travel, at half the speed — so the - // reader who overshot a post settles back onto it instead of flying past - // it the other way. The next notch the same way is a plain one, winding - // up from whatever speed the half push left, so a spin on after a - // reversal gets going again at once. (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 — Adam, 2026-10-16: "could the first coast in the - // opposite direction be smaller/slower than usual? then speed up if we - // scroll again in the same direction".) - const REVERSE = 0.5; // of a notch: the first push back against a coast + // 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) => { @@ -6683,18 +6692,31 @@ if (INTENT_URI) { const now = e.timeStamp; // 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; - if (going && dir !== going) { - // 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 coast the rest of its (half) travel. + 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((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 diff --git a/tools/feed-wheel-reverse-test.js b/tools/feed-wheel-reverse-test.js index be5ad54..352bc0e 100644 --- a/tools/feed-wheel-reverse-test.js +++ b/tools/feed-wheel-reverse-test.js @@ -34,45 +34,43 @@ for (const dir of [1, -1]) { const f = fixture(); f.wheel(dir); f.advance(17); f.frame(); f.wheel(dir); f.advance(17); f.frame(); - // A busy main thread has missed several frames when the reverse arrives. + // A busy main thread has missed several frames when the turn back arrives. f.advance(120); const before = f.y; + // [flying: the first turn back is a brake] f.wheel(-dir); - assert((f.y - before) * dir < 0, 'first reverse notch responds immediately'); - assert(Math.abs(f.y - before) < 20, 'reverse starts with a small bounded step'); - const reversed = f.y; + assert.equal(f.y, before, 'the brake notch moves nothing by itself'); f.frame(0); // a queued animation frame can carry an older timestamp - assert.equal(f.y, reversed, 'old frame time cannot revive the previous direction'); - f.advance(17); f.frame(); - assert((f.y - reversed) * dir < 0, 'next frame continues in the new direction'); - assert(Math.abs(f.y - reversed) < 20, 'new velocity is not applied to the old frame delay'); + assert.equal(f.y, before, 'old frame time moves nothing'); + settle(f, 30); + const over = (f.y - before) * dir; + assert(over >= 0 && over < 120, 'the brake eases the flight out within a short way, never the other way: ' + over); + // [the next notch back: the gentle start] + const stopped = f.y; + f.wheel(-dir); + assert((stopped - f.y) * dir > 0 && (stopped - f.y) * dir < 20, 'the gentle start responds at once with a small step'); settle(f); - // The turn back is a gentler push than a notch: half a notch's travel, so - // a reader who overshot settles back rather than flying the other way. - const back = (before - f.y) * dir; - assert(back > 130 && back < 170, 'a reverse notch against a coast travels half a notch: ' + back); - // The next notch the same way is a plain one again, winding up from the - // half push's speed: a reversal and a quick second notch travel about a - // notch and a half. - let pair; - { f.wheel(-dir); f.advance(17); f.frame(); f.wheel(-dir); f.advance(120); f.frame(); // coasting again, the other way - const y = f.y; f.wheel(dir); f.advance(60); f.frame(); f.wheel(dir); settle(f); pair = (f.y - y) * dir; - assert(pair > 440 && pair < 560, 'a reversal and a quick second notch: about a notch and a half: ' + pair); } - // From rest the same two quick notches go further: a full first push, and the wind-up. - { f.advance(2500); const y = f.y; f.wheel(dir); f.advance(60); f.frame(); f.wheel(dir); settle(f); const s = (f.y - y) * dir; assert(s > pair + 100, 'two quick notches from rest go further: ' + s + ' vs ' + pair); } - // A spin that keeps going after a reversal winds up from its second - // notch: a reversal plus four quick notches goes well past four plain ones. - let spin; - { f.wheel(-dir); f.advance(17); f.frame(); f.wheel(-dir); f.advance(120); f.frame(); - const y = f.y; f.wheel(dir); for (let i = 0; i < 4; i++) { f.advance(60); f.frame(); f.wheel(dir); } settle(f); spin = (f.y - y) * dir; - assert(spin > 4 * 300 + 150, 'a spin on after a reversal winds up again: ' + spin); } - // ...and the same five quick notches from rest go about as far (the half push costs little). - { f.advance(2500); const y = f.y; f.wheel(dir); for (let i = 0; i < 4; i++) { f.advance(60); f.frame(); f.wheel(dir); } settle(f); const s = (f.y - y) * dir; - assert(s < spin * 1.3, 'five from rest vs five after a reversal: ' + s + ' vs ' + spin); settle(f, 200); } + const back = (stopped - f.y) * dir; + assert(back > 130 && back < 170, 'the notch after the brake creeps back half a notch: ' + back); + // [then the wind-up] + { const y = f.y; f.wheel(-dir); f.advance(60); f.frame(); f.wheel(-dir); settle(f); + const on = (y - f.y) * dir; assert(on > 600, 'the notches after wind up: ' + on); } + // The gentle start waits a while for its notch: a second and a half after the brake, still gentle. + { f.advance(2500); f.wheel(dir); f.advance(17); f.frame(); f.wheel(dir); f.advance(120); f.frame(); + f.wheel(-dir); settle(f, 30); f.advance(1500); const y = f.y; f.wheel(-dir); settle(f); + const s = (y - f.y) * dir; assert(s > 130 && s < 170, 'a notch back a while after the brake is still the gentle start: ' + s); } + // ...but long after, the brake forgotten, a notch back is a plain push from rest. + { f.advance(2500); f.wheel(dir); f.advance(17); f.frame(); f.wheel(dir); f.advance(120); f.frame(); + f.wheel(-dir); settle(f, 30); f.advance(3000); const y = f.y; f.wheel(-dir); settle(f); + const s = (y - f.y) * dir; assert(s > 280 && s < 330, 'long after the brake, a notch back is a full push: ' + s); } + // A stroll (one notch) turned back has nothing worth braking: the gentle start at once. + { f.advance(2500); f.wheel(dir); f.advance(100); f.frame(); const y = f.y; f.wheel(-dir); + assert((y - f.y) * dir > 0 && (y - f.y) * dir < 20, 'against a stroll the turn back starts at once, with a small step'); + settle(f); const s = (y - f.y) * dir; assert(s > 130 && s < 170, 'against a stroll the turn back is the gentle start: ' + s); } // From rest a notch either way is a full push. { f.advance(2500); const y = f.y; f.wheel(dir); settle(f); const s = (f.y - y) * dir; assert(s > 280 && s < 330, 'from rest a notch either way is a full push: ' + s); } - // A turn back against a coast that only just died is a reversal too: the half push. - { f.wheel(dir); settle(f, 40); f.advance(100); const y = f.y; f.wheel(-dir); settle(f); const s = (y - f.y) * dir; assert(s > 130 && s < 170, 'reversing right after the coast dies is the half push: ' + s); } + // A turn back against a coast that only just died is the gentle start too. + { f.wheel(dir); settle(f, 40); f.advance(100); const y = f.y; f.wheel(-dir); settle(f); const s = (y - f.y) * dir; assert(s > 130 && s < 170, 'reversing right after the coast dies is the gentle start: ' + s); } // Well after the coast died it is a plain notch from rest, whichever way. { f.advance(2500); const y = f.y; f.wheel(-dir); settle(f); const s = (y - f.y) * dir; assert(s > 280 && s < 330, 'a notch the other way from rest is a full push: ' + s); } } @@ -90,4 +88,4 @@ for (const sel of ['textarea', '.media-pager']) { const y = f.y; f.wheel(1); settle(f); assert.equal(f.y, y, 'a notch in ' + sel + ' is not ours'); } -console.log('✓ immediate reversal both ways, stale frame timestamps, busy frame recovery, a reversal is a half push that winds up again from the next notch, videos and threads coast'); +console.log('✓ a turn back against a flight brakes, the next notch is the gentle start, then the wind-up; against a stroll or a dead coast the gentle start at once; stale frames; videos and threads coast'); |
