Fix animation stale reference
This commit is contained in:
@@ -108,6 +108,7 @@ export const NativeRouterView = defineComponent({
|
||||
'data-native-presentation': transaction?.presentation,
|
||||
'data-native-direction': transaction?.direction,
|
||||
'data-native-transaction': transaction?.id,
|
||||
'data-native-velocity': transaction ? String(transaction.velocity) : undefined,
|
||||
}, children)
|
||||
}
|
||||
},
|
||||
@@ -166,6 +167,7 @@ function createPointerGesture(
|
||||
let ending = false
|
||||
let bufferedProgress = 0
|
||||
let bufferedVelocity = 0
|
||||
let gestureSign = 0
|
||||
|
||||
const down = (event: PointerEvent) => {
|
||||
if (!event.isPrimary || event.button !== 0 || shouldIgnoreGesture(event.target)) return
|
||||
@@ -176,6 +178,9 @@ function createPointerGesture(
|
||||
captured = false
|
||||
beginPromise = null
|
||||
ending = false
|
||||
bufferedProgress = 0
|
||||
bufferedVelocity = 0
|
||||
gestureSign = 0
|
||||
}
|
||||
const move = async (event: PointerEvent) => {
|
||||
if (event.pointerId !== pointerId) return
|
||||
@@ -187,20 +192,22 @@ function createPointerGesture(
|
||||
if (!direction) return reset()
|
||||
captured = true
|
||||
ending = false
|
||||
gestureSign = Math.sign(dx)
|
||||
element()?.setPointerCapture(pointerId)
|
||||
beginPromise = begin(direction)
|
||||
}
|
||||
event.preventDefault()
|
||||
const elapsed = Math.max(8, event.timeStamp - lastTime)
|
||||
bufferedVelocity = Math.abs(event.clientX - lastX) / elapsed
|
||||
bufferedProgress = Math.min(1, Math.abs(dx) / Math.max(1, element()?.clientWidth ?? window.innerWidth))
|
||||
const width = Math.max(1, element()?.clientWidth ?? window.innerWidth)
|
||||
bufferedVelocity = ((event.clientX - lastX) * gestureSign * 1000) / (elapsed * width)
|
||||
bufferedProgress = Math.max(0, Math.min(1, (dx * gestureSign) / width))
|
||||
lastX = event.clientX
|
||||
lastTime = event.timeStamp
|
||||
const pending = beginPromise
|
||||
if (pending) {
|
||||
const id = await pending
|
||||
if (id === null) return reset()
|
||||
if (pending !== beginPromise || ending) return
|
||||
if (id === null) return reset()
|
||||
const runtime = injectRuntimeFromElement(element())
|
||||
runtime?.updateInteractive(bufferedProgress, bufferedVelocity)
|
||||
}
|
||||
@@ -209,19 +216,27 @@ function createPointerGesture(
|
||||
if (event.pointerId !== pointerId) return
|
||||
ending = true
|
||||
const runtime = injectRuntimeFromElement(element())
|
||||
const id = beginPromise ? await beginPromise : null
|
||||
if (captured && id !== null && runtime) {
|
||||
runtime.updateInteractive(bufferedProgress, bufferedVelocity)
|
||||
const pending = beginPromise
|
||||
const shouldFinish = captured
|
||||
const progress = bufferedProgress
|
||||
const velocity = bufferedVelocity
|
||||
// Detach this pointer before awaiting preload/navigation/animation work. A
|
||||
// new gesture may now start without this release callback erasing it.
|
||||
reset()
|
||||
const id = pending ? await pending : null
|
||||
if (shouldFinish && id !== null && runtime?.transaction.value?.id === id) {
|
||||
runtime.updateInteractive(progress, velocity)
|
||||
await runtime.finishInteractive()
|
||||
}
|
||||
reset()
|
||||
}
|
||||
const cancel = async () => {
|
||||
ending = true
|
||||
const runtime = injectRuntimeFromElement(element())
|
||||
const id = beginPromise ? await beginPromise : null
|
||||
if (captured && id !== null && runtime) await runtime.cancelInteractive()
|
||||
const pending = beginPromise
|
||||
const shouldCancel = captured
|
||||
reset()
|
||||
const id = pending ? await pending : null
|
||||
if (shouldCancel && id !== null && runtime?.transaction.value?.id === id) await runtime.cancelInteractive()
|
||||
}
|
||||
const reset = () => {
|
||||
pointerId = -1
|
||||
@@ -230,6 +245,7 @@ function createPointerGesture(
|
||||
ending = false
|
||||
bufferedProgress = 0
|
||||
bufferedVelocity = 0
|
||||
gestureSign = 0
|
||||
}
|
||||
return { down, move, up, cancel }
|
||||
}
|
||||
@@ -429,32 +445,39 @@ export const NativeDismissGesture = defineComponent({
|
||||
}
|
||||
event.preventDefault()
|
||||
progress = Math.max(0, Math.min(1, dy / Math.max(1, root.value?.clientHeight ?? window.innerHeight)))
|
||||
velocity = Math.max(0, event.clientY - lastY) / Math.max(8, event.timeStamp - lastTime)
|
||||
const height = Math.max(1, root.value?.clientHeight ?? window.innerHeight)
|
||||
velocity = ((event.clientY - lastY) * 1000) / (Math.max(8, event.timeStamp - lastTime) * height)
|
||||
lastY = event.clientY
|
||||
lastTime = event.timeStamp
|
||||
const pending = beginPromise
|
||||
if (pending) {
|
||||
const id = await pending
|
||||
if (id === null) return reset()
|
||||
if (pending !== beginPromise || ending) return
|
||||
if (id === null) return reset()
|
||||
runtime.updateInteractive(progress, velocity)
|
||||
}
|
||||
}
|
||||
const up = async (event: PointerEvent) => {
|
||||
if (event.pointerId !== pointerId) return
|
||||
ending = true
|
||||
const id = beginPromise ? await beginPromise : null
|
||||
if (captured && id !== null) {
|
||||
runtime.updateInteractive(progress, velocity)
|
||||
const pending = beginPromise
|
||||
const shouldFinish = captured
|
||||
const finalProgress = progress
|
||||
const finalVelocity = velocity
|
||||
reset()
|
||||
const id = pending ? await pending : null
|
||||
if (shouldFinish && id !== null && runtime.transaction.value?.id === id) {
|
||||
runtime.updateInteractive(finalProgress, finalVelocity)
|
||||
await runtime.finishInteractive()
|
||||
}
|
||||
reset()
|
||||
}
|
||||
const cancel = async () => {
|
||||
ending = true
|
||||
const id = beginPromise ? await beginPromise : null
|
||||
if (captured && id !== null) await runtime.cancelInteractive()
|
||||
const pending = beginPromise
|
||||
const shouldCancel = captured
|
||||
reset()
|
||||
const id = pending ? await pending : null
|
||||
if (shouldCancel && id !== null && runtime.transaction.value?.id === id) await runtime.cancelInteractive()
|
||||
}
|
||||
onBeforeUnmount(() => void cancel())
|
||||
return () => h(props.as, {
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { createApp, defineComponent, nextTick } from 'vue'
|
||||
import { createMemoryHistory, createRouter } from 'vue-router'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createNativeRouter, definePresentation, shouldCommitGesture } from './runtime'
|
||||
import {
|
||||
createNativeRouter,
|
||||
definePresentation,
|
||||
shouldCommitGesture,
|
||||
springTimeScaleForVelocity,
|
||||
} from './runtime'
|
||||
|
||||
const Page = defineComponent({ template: '<div>page</div>' })
|
||||
|
||||
@@ -36,9 +41,16 @@ beforeEach(() => {
|
||||
describe('gesture decisions', () => {
|
||||
it('uses progress or a deliberate velocity to commit', () => {
|
||||
expect(shouldCommitGesture(0.4, 0)).toBe(true)
|
||||
expect(shouldCommitGesture(0.12, 0.7)).toBe(true)
|
||||
expect(shouldCommitGesture(0.04, 1.4)).toBe(false)
|
||||
expect(shouldCommitGesture(0.2, 0.1)).toBe(false)
|
||||
expect(shouldCommitGesture(0.12, 1.4)).toBe(true)
|
||||
expect(shouldCommitGesture(0.04, 4)).toBe(false)
|
||||
expect(shouldCommitGesture(0.2, 0.4)).toBe(false)
|
||||
})
|
||||
|
||||
it('settles a fast flick more quickly without unbounded spring steps', () => {
|
||||
expect(springTimeScaleForVelocity(0)).toBe(1)
|
||||
expect(springTimeScaleForVelocity(2)).toBeCloseTo(1.6)
|
||||
expect(springTimeScaleForVelocity(8)).toBe(3)
|
||||
expect(springTimeScaleForVelocity(-20)).toBe(3)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -49,7 +49,16 @@ function entryFor(route: RouteLocationNormalizedLoaded, status: NativeViewEntry[
|
||||
}
|
||||
|
||||
export function shouldCommitGesture(progress: number, velocity: number, threshold = 0.36) {
|
||||
return progress >= threshold || (progress >= 0.08 && velocity >= 0.52)
|
||||
return progress >= threshold || (progress >= 0.08 && velocity >= 1.1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts release velocity (normalized route progress per second) into the
|
||||
* rate at which the spring is simulated. A deliberate flick can settle up to
|
||||
* three times faster while a stationary release keeps the baseline spring.
|
||||
*/
|
||||
export function springTimeScaleForVelocity(velocity: number) {
|
||||
return 1 + Math.min(2, Math.abs(velocity) * 0.3)
|
||||
}
|
||||
|
||||
export function definePresentation(definition: NativePresentationDefinition) {
|
||||
@@ -555,17 +564,24 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
|
||||
}
|
||||
return new Promise<void>((resolve) => {
|
||||
let position = transaction.progress
|
||||
let velocity = Math.max(-2, Math.min(2, initialVelocity))
|
||||
let velocity = Math.max(-12, Math.min(12, initialVelocity))
|
||||
const timeScale = springTimeScaleForVelocity(initialVelocity)
|
||||
let previous = now()
|
||||
const step = (time: number) => {
|
||||
const live = this.mutableTransaction.value
|
||||
if (!live || live.id !== transaction.id) return resolve()
|
||||
const dt = Math.min(0.032, Math.max(0.001, (time - previous) / 1000))
|
||||
const elapsed = Math.min(0.032, Math.max(0.001, (time - previous) / 1000)) * timeScale
|
||||
previous = time
|
||||
const displacement = target - position
|
||||
const acceleration = displacement * 280 - velocity * 30
|
||||
velocity += acceleration * dt
|
||||
position += velocity * dt
|
||||
// Substeps keep the spring stable when a high-velocity flick advances
|
||||
// several frames of simulated time in one display frame.
|
||||
const iterations = Math.max(1, Math.ceil(elapsed / (1 / 120)))
|
||||
const dt = elapsed / iterations
|
||||
for (let iteration = 0; iteration < iterations; iteration += 1) {
|
||||
const displacement = target - position
|
||||
const acceleration = displacement * 280 - velocity * 30
|
||||
velocity += acceleration * dt
|
||||
position += velocity * dt
|
||||
}
|
||||
const done = Math.abs(target - position) < 0.002 && Math.abs(velocity) < 0.02
|
||||
this.mutableTransaction.value = {
|
||||
...live,
|
||||
|
||||
Reference in New Issue
Block a user