better velocity on now playing swipedown

This commit is contained in:
Boof2015
2026-07-15 15:32:25 -04:00
parent 3f1f43b416
commit 7a4e1cc283
4 changed files with 115 additions and 4 deletions
+1 -1
View File
@@ -80,7 +80,7 @@
"test:sleep": "node --experimental-strip-types --test src/audio/sleepTimerState.test.mts",
"test:troubleshooting": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/db/libraryMaintenance.test.mts src/lib/cacheInvalidation.test.mts",
"test:settings-search": "node --experimental-strip-types --test src/components/search/settingsSearchRoutes.test.mts",
"test:now-playing-layout": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/components/player/nowPlayingLayout.test.mts src/components/player/nowPlayingPreferences.test.mts",
"test:now-playing-layout": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/components/player/nowPlayingLayout.test.mts src/components/player/nowPlayingPreferences.test.mts src/components/player/nowPlayingDismiss.test.mts",
"test:memory-lifecycle": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/components/delayedPresence.test.mts scripts/android-memory-profile.test.mjs",
"test:haptics": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/lib/haptics.test.mts",
"test:home-greeting": "node --experimental-strip-types --test src/home/homeGreeting.test.mts",
+9 -3
View File
@@ -40,6 +40,7 @@ import { ScopeRack } from '@/components/player/ScopeRack';
import { NowPlayingCompanionPane } from '@/components/player/NowPlayingCompanionPane';
import { PlayerStateIcon } from '@/components/player/PlayerStateIcon';
import { CachedLyricPeek } from '@/components/player/CachedLyricPeek';
import { resolveNowPlayingDismissSpring } from '@/components/player/nowPlayingDismiss';
import { useDelayedUnmountPresence } from '@/components/delayedPresence';
import { SleepTimerControls } from '@/components/player/SleepTimerControls';
import { AppSheet, AppSheetTitle } from '@/components/sheets/AppSheet';
@@ -442,13 +443,18 @@ export function NowPlayingOverlay() {
})
.onEnd((e) => {
if (e.translationY > DISMISS_DISTANCE || e.velocityY > DISMISS_VELOCITY) {
const releaseSpring = resolveNowPlayingDismissSpring(
e.velocityY,
windowHeight - translateY.value
);
translateY.value = withSpring(
windowHeight,
{
damping: 28,
stiffness: 240,
velocity: e.velocityY,
damping: releaseSpring.damping,
stiffness: releaseSpring.stiffness,
velocity: releaseSpring.velocity,
overshootClamping: true,
energyThreshold: 1e-4,
},
(finished) => {
if (finished) runOnJS(dismiss)();
@@ -0,0 +1,60 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { resolveNowPlayingDismissSpring } from './nowPlayingDismiss.ts';
test('preserves ordinary downward release velocity and spring', () => {
for (const velocity of [0, 1, 500, 999, 1000]) {
assert.deepEqual(resolveNowPlayingDismissSpring(velocity, 760), {
velocity,
stiffness: 240,
damping: 28,
});
}
});
test('normalizes upward and invalid release velocity', () => {
for (const velocity of [-1000, -1, Number.NaN, Number.POSITIVE_INFINITY]) {
assert.deepEqual(resolveNowPlayingDismissSpring(velocity, 760), {
velocity: 0,
stiffness: 240,
damping: 28,
});
}
});
test('keeps extreme release velocity continuous while increasing spring control', () => {
const rawVelocities = [1001, 1400, 2600, 5000, 10_000];
const springs = rawVelocities.map((velocity) =>
resolveNowPlayingDismissSpring(velocity, 760)
);
for (let index = 0; index < springs.length; index += 1) {
assert.equal(springs[index].velocity, rawVelocities[index]);
assert.ok(springs[index].stiffness > 240);
assert.ok(springs[index].stiffness < 480);
assert.ok(springs[index].damping > 28);
assert.ok(springs[index].damping < 60);
}
for (let index = 1; index < springs.length; index += 1) {
assert.ok(springs[index].stiffness > springs[index - 1].stiffness);
assert.ok(springs[index].damping > springs[index - 1].damping);
}
const springAt5000 = resolveNowPlayingDismissSpring(5000, 760);
const expectedProgress = 1 - Math.exp(-1);
assert.ok(Math.abs(springAt5000.stiffness - (240 + 240 * expectedProgress)) < 1e-9);
assert.ok(Math.abs(springAt5000.damping - (28 + 32 * expectedProgress)) < 1e-9);
});
test('adds damping only when an outlier velocity could overshoot the remaining travel', () => {
const ordinaryFastFlick = resolveNowPlayingDismissSpring(10_000, 760);
const outlierFlick = resolveNowPlayingDismissSpring(50_000, 760);
const outlierNearBottom = resolveNowPlayingDismissSpring(50_000, 400);
assert.ok(ordinaryFastFlick.damping < 60);
assert.equal(outlierFlick.velocity, 50_000);
assert.ok(outlierFlick.damping > 60);
assert.ok(outlierNearBottom.damping > outlierFlick.damping);
});
@@ -0,0 +1,45 @@
const ADAPTIVE_VELOCITY_ONSET = 1000;
const ADAPTIVE_VELOCITY_RANGE = 4000;
const BASE_STIFFNESS = 240;
const MAX_STIFFNESS = 480;
const BASE_DAMPING = 28;
const MAX_DAMPING = 60;
const MIN_REMAINING_DISTANCE = 120;
const VELOCITY_TRAVEL_DAMPING = 1.35;
interface NowPlayingDismissSpring {
velocity: number;
stiffness: number;
damping: number;
}
/**
* Preserve the exact release velocity, then make the spring progressively
* stronger and more damped for hard flicks so it sheds speed after handoff.
*/
export function resolveNowPlayingDismissSpring(
velocityY: number,
remainingDistance: number
): NowPlayingDismissSpring {
'worklet';
const velocity = Number.isFinite(velocityY) && velocityY > 0 ? velocityY : 0;
const excessVelocity = Math.max(0, velocity - ADAPTIVE_VELOCITY_ONSET);
const adaptiveProgress = 1 - Math.exp(-excessVelocity / ADAPTIVE_VELOCITY_RANGE);
const safeRemainingDistance = Number.isFinite(remainingDistance)
? Math.max(MIN_REMAINING_DISTANCE, remainingDistance)
: MIN_REMAINING_DISTANCE;
const adaptiveDamping =
BASE_DAMPING + (MAX_DAMPING - BASE_DAMPING) * adaptiveProgress;
// Extreme gesture velocities can otherwise cross the remaining screen travel
// in a single frame and trip overshootClamping. Scale damping only above that
// risk boundary so the already-good ordinary flick path remains unchanged.
const travelDamping =
velocity > 0 ? (velocity / safeRemainingDistance) * VELOCITY_TRAVEL_DAMPING : 0;
return {
velocity,
stiffness: BASE_STIFFNESS + (MAX_STIFFNESS - BASE_STIFFNESS) * adaptiveProgress,
damping: Math.max(adaptiveDamping, travelDamping),
};
}