mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-19 04:06:43 +02:00
98 lines
3.2 KiB
TypeScript
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),
|
|
};
|
|
}
|