| 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111 |
| import {
InertiaOptions,
PlaybackControls,
AnimationOptions,
SpringOptions,
} from "./types"
import { animate } from "."
import { velocityPerSecond } from "../utils/velocity-per-second"
import { getFrameData } from "framesync"
export function inertia({
from = 0,
velocity = 0,
min,
max,
power = 0.8,
timeConstant = 750,
bounceStiffness = 500,
bounceDamping = 10,
restDelta = 1,
modifyTarget,
driver,
onUpdate,
onComplete,
}: InertiaOptions) {
let currentAnimation: PlaybackControls
function isOutOfBounds(v: number) {
return (min !== undefined && v < min) || (max !== undefined && v > max)
}
function boundaryNearest(v: number) {
if (min === undefined) return max
if (max === undefined) return min
return Math.abs(min - v) < Math.abs(max - v) ? min : max
}
function startAnimation(options: AnimationOptions<number>) {
currentAnimation?.stop()
currentAnimation = animate({
...options,
driver,
onUpdate: (v: number) => {
onUpdate?.(v)
options.onUpdate?.(v)
},
onComplete,
})
}
function startSpring(options: SpringOptions) {
startAnimation({
type: "spring",
stiffness: bounceStiffness,
damping: bounceDamping,
restDelta,
...options,
})
}
if (isOutOfBounds(from)) {
// Start the animation with spring if outside the defined boundaries
startSpring({ from, velocity, to: boundaryNearest(from) })
} else {
/**
* Or if the value is out of bounds, simulate the inertia movement
* with the decay animation.
*
* Pre-calculate the target so we can detect if it's out-of-bounds.
* If it is, we want to check per frame when to switch to a spring
* animation
*/
let target = power * velocity + from
if (typeof modifyTarget !== "undefined") target = modifyTarget(target)
const boundary = boundaryNearest(target)
const heading = boundary === min ? -1 : 1
let prev: number
let current: number
const checkBoundary = (v: number) => {
prev = current
current = v
velocity = velocityPerSecond(v - prev, getFrameData().delta)
if (
(heading === 1 && v > boundary) ||
(heading === -1 && v < boundary)
) {
startSpring({ from: v, to: boundary, velocity })
}
}
startAnimation({
type: "decay",
from,
velocity,
timeConstant,
power,
restDelta,
modifyTarget,
onUpdate: isOutOfBounds(target) ? checkBoundary : undefined,
})
}
return {
stop: () => currentAnimation?.stop(),
}
}
|