mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-12 05:10:52 +02:00
better velocity on now playing swipedown
This commit is contained in:
+1
-1
@@ -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",
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user