Files
astra-mobile/src/components/player/nowPlayingDismiss.ts
T
2026-07-28 14:08:15 -04:00

98 lines
3.2 KiB
TypeScript

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;
const DISMISS_DISTANCE = 140;
const DISMISS_VELOCITY = 1000;
interface NowPlayingDismissSpring {
velocity: number;
stiffness: number;
damping: number;
}
export type NowPlayingPanRelease = 'dismiss' | 'restore';
/**
* The overlay pan must briefly yield when its body is being replaced. Otherwise
* RNGH can cancel the old child tree after it has already written a partial
* translateY, leaving the newly mounted body parked off-screen.
*/
export function shouldEnableNowPlayingPan(
playerOpen: boolean,
childSheetOpen: boolean,
bodySwitching: boolean
): boolean {
return playerOpen && !childSheetOpen && !bodySwitching;
}
/**
* Queue/lyrics companion rails own their scroll gestures. A non-finite boundary
* means there is no companion rail, so the whole phone player remains eligible.
*/
export function shouldStartNowPlayingPan(
panEnabled: boolean,
absoluteX: number,
companionStartX: number
): boolean {
'worklet';
if (!panEnabled) return false;
if (!Number.isFinite(companionStartX)) return true;
return Number.isFinite(absoluteX) && absoluteX < companionStartX;
}
/**
* Cancellation is authoritative even if the last event carried a large
* translation or velocity. Only a successfully ended gesture may dismiss.
*/
export function resolveNowPlayingPanRelease(
translationY: number,
velocityY: number,
success: boolean
): NowPlayingPanRelease {
'worklet';
if (!success) return 'restore';
const distance = Number.isFinite(translationY) ? Math.max(0, translationY) : 0;
const velocity = Number.isFinite(velocityY) ? Math.max(0, velocityY) : 0;
return distance > DISMISS_DISTANCE || velocity > DISMISS_VELOCITY
? 'dismiss'
: 'restore';
}
/**
* 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),
};
}