@rafaelaugusto/jolt 0.0.0-stage → 0.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +323 -2
  3. package/dist/animate.d.ts +26 -0
  4. package/dist/animate.d.ts.map +1 -0
  5. package/dist/animate.js +161 -0
  6. package/dist/animate.js.map +1 -0
  7. package/dist/driver/android.d.ts +3 -0
  8. package/dist/driver/android.d.ts.map +1 -0
  9. package/dist/driver/android.js +73 -0
  10. package/dist/driver/android.js.map +1 -0
  11. package/dist/driver/index.d.ts +18 -0
  12. package/dist/driver/index.d.ts.map +1 -0
  13. package/dist/driver/index.js +15 -0
  14. package/dist/driver/index.js.map +1 -0
  15. package/dist/driver/ios.d.ts +3 -0
  16. package/dist/driver/ios.d.ts.map +1 -0
  17. package/dist/driver/ios.js +77 -0
  18. package/dist/driver/ios.js.map +1 -0
  19. package/dist/easing.d.ts +8 -0
  20. package/dist/easing.d.ts.map +1 -0
  21. package/dist/easing.js +71 -0
  22. package/dist/easing.js.map +1 -0
  23. package/dist/engine.d.ts +48 -0
  24. package/dist/engine.d.ts.map +1 -0
  25. package/dist/engine.js +178 -0
  26. package/dist/engine.js.map +1 -0
  27. package/dist/index.d.ts +13 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +7 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/presets.d.ts +310 -0
  32. package/dist/presets.d.ts.map +1 -0
  33. package/dist/presets.js +141 -0
  34. package/dist/presets.js.map +1 -0
  35. package/dist/press.d.ts +13 -0
  36. package/dist/press.d.ts.map +1 -0
  37. package/dist/press.js +34 -0
  38. package/dist/press.js.map +1 -0
  39. package/dist/properties.d.ts +8 -0
  40. package/dist/properties.d.ts.map +1 -0
  41. package/dist/properties.js +42 -0
  42. package/dist/properties.js.map +1 -0
  43. package/dist/sequence.d.ts +26 -0
  44. package/dist/sequence.d.ts.map +1 -0
  45. package/dist/sequence.js +69 -0
  46. package/dist/sequence.js.map +1 -0
  47. package/dist/spring.d.ts +22 -0
  48. package/dist/spring.d.ts.map +1 -0
  49. package/dist/spring.js +54 -0
  50. package/dist/spring.js.map +1 -0
  51. package/dist/track.d.ts +28 -0
  52. package/dist/track.d.ts.map +1 -0
  53. package/dist/track.js +103 -0
  54. package/dist/track.js.map +1 -0
  55. package/dist/transition.d.ts +58 -0
  56. package/dist/transition.d.ts.map +1 -0
  57. package/dist/transition.js +82 -0
  58. package/dist/transition.js.map +1 -0
  59. package/dist/types.d.ts +40 -0
  60. package/dist/types.d.ts.map +1 -0
  61. package/dist/types.js +2 -0
  62. package/dist/types.js.map +1 -0
  63. package/package.json +67 -4
  64. package/src/animate.ts +246 -0
  65. package/src/driver/android.ts +101 -0
  66. package/src/driver/index.ts +32 -0
  67. package/src/driver/ios.ts +97 -0
  68. package/src/easing.ts +100 -0
  69. package/src/engine.ts +216 -0
  70. package/src/index.ts +12 -0
  71. package/src/presets.ts +158 -0
  72. package/src/press.ts +49 -0
  73. package/src/properties.ts +51 -0
  74. package/src/sequence.ts +88 -0
  75. package/src/spring.ts +95 -0
  76. package/src/track.ts +163 -0
  77. package/src/transition.ts +160 -0
  78. package/src/types.ts +41 -0
package/src/animate.ts ADDED
@@ -0,0 +1,246 @@
1
+ import { View } from '@nativescript/core'
2
+ import {
3
+ arrivalOf,
4
+ setArrival,
5
+ stopView,
6
+ ViewAnimation,
7
+ type Animation,
8
+ type Plan,
9
+ } from './engine.js'
10
+ import { getPreset, type PresetName } from './presets.js'
11
+ import { isProperty, type Property } from './properties.js'
12
+ import {
13
+ hasCurve,
14
+ mergeTransitions,
15
+ resolveTransition,
16
+ springs,
17
+ type SpringName,
18
+ type Transition,
19
+ } from './transition.js'
20
+ import type {
21
+ AnimateOptions,
22
+ Keyframes,
23
+ KeyframeValue,
24
+ Stagger,
25
+ Target,
26
+ } from './types.js'
27
+
28
+ interface Options {
29
+ base: Transition
30
+ overrides: Partial<Record<keyof Keyframes, Transition>>
31
+ stagger: number | Stagger | undefined
32
+ relative: boolean | undefined
33
+ }
34
+
35
+ /**
36
+ * Animates one view, or several. Takes keyframes or the name of a preset,
37
+ * and a transition or the name of a spring.
38
+ *
39
+ * ```ts
40
+ * animate(card, 'fadeInUp')
41
+ * animate(card, { scale: 1.1, rotate: [0, 90] }, 'bouncy')
42
+ * animate(rows, 'fadeInUp', { stagger: 40 })
43
+ * ```
44
+ */
45
+ export function animate(
46
+ target: Target | readonly Target[],
47
+ animation: Keyframes | PresetName,
48
+ options?: AnimateOptions | SpringName,
49
+ ): Animation {
50
+ const { keyframes, base, overrides, stagger, relative } = prepare(animation, options)
51
+
52
+ if (!isList(target)) {
53
+ return new ViewAnimation(
54
+ toView(target),
55
+ plan(keyframes, base, overrides, 0, relative),
56
+ )
57
+ }
58
+
59
+ const views = target.map(toView)
60
+ return group(
61
+ views.map(
62
+ (view, index) =>
63
+ new ViewAnimation(
64
+ view,
65
+ plan(
66
+ keyframes,
67
+ base,
68
+ overrides,
69
+ staggerDelay(stagger, index, views.length),
70
+ relative,
71
+ ),
72
+ ),
73
+ ),
74
+ )
75
+ }
76
+
77
+ /** Stops all or selected properties at their current values. `scale` stops both axes. */
78
+ export function stop(
79
+ target: Target | readonly Target[],
80
+ properties?: readonly (keyof Keyframes)[],
81
+ ) {
82
+ const expanded = properties?.flatMap<Property>((property) =>
83
+ property === 'scale' ? ['scaleX', 'scaleY'] : [property],
84
+ )
85
+ for (const view of toViews(target)) stopView(view, expanded)
86
+ }
87
+
88
+ /** Plays several animations as one: finished once they all are, and stopped together. */
89
+ export function group(animations: readonly Animation[]): Animation {
90
+ const finished = Promise.all(animations.map((animation) => animation.finished)).then(
91
+ (results) => results.every(Boolean),
92
+ )
93
+ const animation: Animation = {
94
+ finished,
95
+ stop: () => animations.forEach((animation) => animation.stop()),
96
+ then: (onfulfilled, onrejected) => finished.then(onfulfilled, onrejected),
97
+ }
98
+ setArrival(animation, Promise.all(animations.map(arrivalOf)))
99
+ return animation
100
+ }
101
+
102
+ export function toView(target: Target): View {
103
+ if (target instanceof View) return target
104
+ const view =
105
+ (target as { nativeView?: unknown } | null)?.nativeView ??
106
+ (target as { nativeElement?: unknown } | null)?.nativeElement
107
+ if (view instanceof View) return view
108
+ throw new TypeError(`Expected a NativeScript view, got ${String(target)}.`)
109
+ }
110
+
111
+ export function toViews(target: Target | readonly Target[]): View[] {
112
+ return isList(target) ? target.map(toView) : [toView(target)]
113
+ }
114
+
115
+ function isList<T>(target: T | readonly T[]): target is readonly T[] {
116
+ return Array.isArray(target)
117
+ }
118
+
119
+ function prepare(
120
+ animation: Keyframes | PresetName,
121
+ options?: AnimateOptions | SpringName,
122
+ ) {
123
+ const user = normalize(options)
124
+ if (typeof animation !== 'string') return { keyframes: animation, ...user }
125
+
126
+ const preset = getPreset(animation)
127
+ const own = normalize(preset.transition)
128
+ return {
129
+ keyframes: preset.keyframes,
130
+ base: mergeTransitions(own.base, user.base),
131
+ overrides: hasCurve(user.base)
132
+ ? user.overrides
133
+ : { ...own.overrides, ...user.overrides },
134
+ stagger: user.stagger ?? own.stagger,
135
+ relative: user.relative ?? own.relative,
136
+ }
137
+ }
138
+
139
+ function normalize(options: AnimateOptions | SpringName | undefined): Options {
140
+ if (options === undefined)
141
+ return { base: {}, overrides: {}, stagger: undefined, relative: undefined }
142
+ if (typeof options === 'string')
143
+ return {
144
+ base: spring(options),
145
+ overrides: {},
146
+ stagger: undefined,
147
+ relative: undefined,
148
+ }
149
+
150
+ const base: Record<string, unknown> = {}
151
+ const overrides: Options['overrides'] = {}
152
+ let stagger: Options['stagger']
153
+ let relative: Options['relative']
154
+ for (const [key, value] of Object.entries(options)) {
155
+ if (value === undefined) continue
156
+ if (key === 'stagger') stagger = value as Options['stagger']
157
+ else if (key === 'relative') relative = value as Options['relative']
158
+ else if (key === 'scale' || isProperty(key)) {
159
+ overrides[key] = typeof value === 'string' ? spring(value as SpringName) : value
160
+ } else base[key] = value
161
+ }
162
+ return { base, overrides, stagger, relative }
163
+ }
164
+
165
+ function plan(
166
+ keyframes: Keyframes,
167
+ base: Transition,
168
+ overrides: Options['overrides'],
169
+ offset: number,
170
+ relative = false,
171
+ ): Plan[] {
172
+ return expand(keyframes).map(([property, frames]) => {
173
+ const override =
174
+ overrides[property] ?? (property.startsWith('scale') ? overrides.scale : undefined)
175
+ const transition = resolveTransition(
176
+ override ? mergeTransitions(base, override) : base,
177
+ property,
178
+ frames.length,
179
+ )
180
+ transition.delay += offset
181
+ return { property, keyframes: frames, transition, relative }
182
+ })
183
+ }
184
+
185
+ /** Each property the animation touches, with the value it starts from when it sets one. */
186
+ export function startingValues(
187
+ animation: Keyframes | PresetName,
188
+ options?: AnimateOptions | SpringName,
189
+ ) {
190
+ const { keyframes, relative } = prepare(animation, options)
191
+ if (!keyframes) throw new Error(`Unknown preset "${animation}".`)
192
+ return new Map(
193
+ expand(keyframes).map(([property, [first]]) => [property, relative ? null : first!]),
194
+ )
195
+ }
196
+
197
+ function expand(keyframes: Keyframes): [Property, (number | null)[]][] {
198
+ const expanded: [Property, (number | null)[]][] = []
199
+ for (const [key, value] of Object.entries(keyframes) as [
200
+ keyof Keyframes,
201
+ KeyframeValue | undefined,
202
+ ][]) {
203
+ if (value === undefined) continue
204
+ if (key !== 'scale' && !isProperty(key)) throw new Error(`Can't animate "${key}".`)
205
+
206
+ const properties: Property[] = key === 'scale' ? ['scaleX', 'scaleY'] : [key]
207
+ for (const property of properties) {
208
+ if (key === 'scale' && keyframes[property] !== undefined) continue
209
+ expanded.push([property, toKeyframes(property, value)])
210
+ }
211
+ }
212
+ return expanded
213
+ }
214
+
215
+ function toKeyframes(property: Property, value: KeyframeValue): (number | null)[] {
216
+ const frames = typeof value === 'number' ? [null, value] : [...value]
217
+ if (frames.length === 1) frames.unshift(null)
218
+ if (
219
+ frames.length === 0 ||
220
+ frames.some((frame) => frame !== null && !Number.isFinite(frame))
221
+ ) {
222
+ throw new Error(`Invalid keyframes for "${property}": ${JSON.stringify(value)}.`)
223
+ }
224
+ return frames
225
+ }
226
+
227
+ function staggerDelay(stagger: Options['stagger'], index: number, count: number) {
228
+ if (stagger === undefined) return 0
229
+ const { each, from = 'first' } =
230
+ typeof stagger === 'number' ? { each: stagger } : stagger
231
+ const origin =
232
+ from === 'first'
233
+ ? 0
234
+ : from === 'last'
235
+ ? count - 1
236
+ : from === 'center'
237
+ ? (count - 1) / 2
238
+ : from
239
+ return each * Math.abs(index - origin)
240
+ }
241
+
242
+ function spring(name: SpringName): Transition {
243
+ const transition = springs[name]
244
+ if (!transition) throw new Error(`Unknown spring "${name}".`)
245
+ return transition
246
+ }
@@ -0,0 +1,101 @@
1
+ import { Utils, type View } from '@nativescript/core'
2
+ import type { Property } from '../properties.js'
3
+ import { after, type Driver } from './index.js'
4
+
5
+ type NativeView = android.view.View
6
+
7
+ const accessors: Record<
8
+ Property,
9
+ { get(view: NativeView): number; set(view: NativeView, value: number): void }
10
+ > = {
11
+ opacity: { get: (v) => v.getAlpha(), set: (v, value) => v.setAlpha(value) },
12
+ x: { get: (v) => v.getTranslationX(), set: (v, value) => v.setTranslationX(value) },
13
+ y: { get: (v) => v.getTranslationY(), set: (v, value) => v.setTranslationY(value) },
14
+ scaleX: { get: (v) => v.getScaleX(), set: (v, value) => v.setScaleX(value) },
15
+ scaleY: { get: (v) => v.getScaleY(), set: (v, value) => v.setScaleY(value) },
16
+ rotate: { get: (v) => v.getRotation(), set: (v, value) => v.setRotation(value) },
17
+ rotateX: { get: (v) => v.getRotationX(), set: (v, value) => v.setRotationX(value) },
18
+ rotateY: { get: (v) => v.getRotationY(), set: (v, value) => v.setRotationY(value) },
19
+ }
20
+
21
+ const unit = (property: Property) =>
22
+ property === 'x' || property === 'y' ? Utils.layout.getDisplayDensity() : 1
23
+
24
+ export function createAndroidDriver(): Driver {
25
+ const { View } = android.view
26
+ const nativeProperties = {
27
+ opacity: View.ALPHA,
28
+ x: View.TRANSLATION_X,
29
+ y: View.TRANSLATION_Y,
30
+ scaleX: View.SCALE_X,
31
+ scaleY: View.SCALE_Y,
32
+ rotate: View.ROTATION,
33
+ rotateX: View.ROTATION_X,
34
+ rotateY: View.ROTATION_Y,
35
+ } satisfies Record<Property, unknown>
36
+ const interpolator = new android.view.animation.LinearInterpolator()
37
+
38
+ return {
39
+ now: () => android.os.SystemClock.uptimeMillis(),
40
+
41
+ play(view, track, _startedAt, onEnd) {
42
+ const nativeView = view.nativeViewProtected as NativeView
43
+ const scale = unit(track.property)
44
+
45
+ const keyframes = Array.create(android.animation.Keyframe, track.values.length)
46
+ track.values.forEach((value, i) => {
47
+ keyframes[i] = android.animation.Keyframe.ofFloat(track.times[i]!, value * scale)
48
+ })
49
+ const holders = Array.create(android.animation.PropertyValuesHolder, 1)
50
+ holders[0] = android.animation.PropertyValuesHolder.ofKeyframe(
51
+ nativeProperties[track.property],
52
+ keyframes,
53
+ )
54
+
55
+ const animator = android.animation.ObjectAnimator.ofPropertyValuesHolder(
56
+ nativeView,
57
+ holders,
58
+ )
59
+ animator.setDuration(Math.max(track.duration, 1))
60
+ animator.setStartDelay(track.delay)
61
+ animator.setInterpolator(interpolator)
62
+ if (track.repeat > 0) {
63
+ animator.setRepeatCount(
64
+ Number.isFinite(track.repeat)
65
+ ? track.repeat
66
+ : android.animation.ValueAnimator.INFINITE,
67
+ )
68
+ animator.setRepeatMode(
69
+ track.reverse
70
+ ? android.animation.ValueAnimator.REVERSE
71
+ : android.animation.ValueAnimator.RESTART,
72
+ )
73
+ }
74
+ animator.addListener(
75
+ new android.animation.Animator.AnimatorListener({
76
+ onAnimationStart() {},
77
+ onAnimationRepeat() {},
78
+ onAnimationCancel() {},
79
+ onAnimationEnd() {
80
+ onEnd()
81
+ },
82
+ }),
83
+ )
84
+
85
+ accessors[track.property].set(nativeView, track.values[0]! * scale)
86
+ animator.start()
87
+ return animator
88
+ },
89
+
90
+ stop(_view, handle) {
91
+ ;(handle as android.animation.ObjectAnimator).cancel()
92
+ },
93
+
94
+ presented(view, property) {
95
+ const nativeView = view.nativeViewProtected as NativeView | undefined
96
+ return nativeView ? accessors[property].get(nativeView) / unit(property) : undefined
97
+ },
98
+
99
+ after,
100
+ }
101
+ }
@@ -0,0 +1,32 @@
1
+ import { isAndroid, type View } from '@nativescript/core'
2
+ import type { Property } from '../properties.js'
3
+ import type { Track } from '../track.js'
4
+ import { createAndroidDriver } from './android.js'
5
+ import { createIOSDriver } from './ios.js'
6
+
7
+ export interface Driver {
8
+ /** Milliseconds, on the clock the platform's animations run on. */
9
+ now(): number
10
+ /** Starts the track at `startedAt + track.delay` and calls `onEnd` once, however it ends. */
11
+ play(view: View, track: Track, startedAt: number, onEnd: () => void): unknown
12
+ stop(view: View, handle: unknown): void
13
+ /** The value on screen, when the platform knows it better than the track does. */
14
+ presented(view: View, property: Property): number | undefined
15
+ /** Calls back after `ms` on the same clock, and returns a function that cancels it. */
16
+ after(ms: number, callback: () => void): () => void
17
+ }
18
+
19
+ export function after(ms: number, callback: () => void) {
20
+ const timer = setTimeout(callback, ms)
21
+ return () => clearTimeout(timer)
22
+ }
23
+
24
+ let driver: Driver | undefined
25
+
26
+ export function getDriver(): Driver {
27
+ return (driver ??= isAndroid ? createAndroidDriver() : createIOSDriver())
28
+ }
29
+
30
+ export function setDriver(next: Driver | undefined) {
31
+ driver = next
32
+ }
@@ -0,0 +1,97 @@
1
+ import type { View } from '@nativescript/core'
2
+ import type { Property } from '../properties.js'
3
+ import { after, type Driver } from './index.js'
4
+
5
+ interface Handle {
6
+ layers: CALayer[]
7
+ key: string
8
+ onStop?: () => void
9
+ }
10
+
11
+ const keyPaths: Record<Property, string> = {
12
+ opacity: 'opacity',
13
+ x: 'transform.translation.x',
14
+ y: 'transform.translation.y',
15
+ scaleX: 'transform.scale.x',
16
+ scaleY: 'transform.scale.y',
17
+ rotate: 'transform.rotation.z',
18
+ rotateX: 'transform.rotation.x',
19
+ rotateY: 'transform.rotation.y',
20
+ }
21
+
22
+ const radians = (degrees: number) => (degrees * Math.PI) / 180
23
+
24
+ function layersOf(view: View): CALayer[] {
25
+ const nativeView = view.nativeViewProtected as UIView & {
26
+ outerShadowContainerLayer?: CALayer
27
+ }
28
+ return nativeView.outerShadowContainerLayer
29
+ ? [nativeView.layer, nativeView.outerShadowContainerLayer]
30
+ : [nativeView.layer]
31
+ }
32
+
33
+ /** A 3D rotation only looks 3D with perspective, which core leaves out once the rotation ends at 0. */
34
+ function ensurePerspective(view: View, layers: CALayer[]) {
35
+ CATransaction.begin()
36
+ CATransaction.setDisableActions(true)
37
+ for (const layer of layers) {
38
+ const transform = layer.transform
39
+ if (transform.m34 === 0) {
40
+ transform.m34 = -1 / (view.perspective || 300)
41
+ layer.transform = transform
42
+ }
43
+ }
44
+ CATransaction.commit()
45
+ }
46
+
47
+ export function createIOSDriver(): Driver {
48
+ return {
49
+ now: () => CACurrentMediaTime() * 1000,
50
+
51
+ play(view, track, startedAt, onEnd): Handle {
52
+ const layers = layersOf(view)
53
+ const key = `jolt:${track.property}`
54
+ const rotation = track.property.startsWith('rotate')
55
+ const infinite = track.repeat === Infinity
56
+ if (track.property === 'rotateX' || track.property === 'rotateY') {
57
+ ensurePerspective(view, layers)
58
+ }
59
+
60
+ const animation = CAKeyframeAnimation.animationWithKeyPath(keyPaths[track.property])
61
+ animation.values = NSArray.arrayWithArray(
62
+ track.values.map((value) => (rotation ? radians(value) : value)),
63
+ )
64
+ animation.keyTimes = NSArray.arrayWithArray(track.times as number[])
65
+ animation.duration = Math.max(track.duration, 1) / 1000
66
+ animation.beginTime = (startedAt + track.delay) / 1000
67
+ animation.fillMode = kCAFillModeBackwards
68
+ // Keep infinite loops when iOS removes animations on backgrounding.
69
+ // They are removed explicitly when stopped or replaced.
70
+ animation.removedOnCompletion = !infinite
71
+ if (track.repeat > 0) {
72
+ animation.autoreverses = track.reverse
73
+ animation.repeatCount = track.reverse ? (track.repeat + 1) / 2 : track.repeat + 1
74
+ }
75
+
76
+ CATransaction.begin()
77
+ // iOS completes the transaction on backgrounding even if the loop is retained.
78
+ if (!infinite) CATransaction.setCompletionBlock(onEnd)
79
+ for (const layer of layers) layer.addAnimationForKey(animation, key)
80
+ CATransaction.commit()
81
+
82
+ return { layers, key, onStop: infinite ? onEnd : undefined }
83
+ },
84
+
85
+ stop(_view, handle) {
86
+ const playing = handle as Handle
87
+ const { layers, key, onStop } = playing
88
+ playing.onStop = undefined
89
+ for (const layer of layers) layer.removeAnimationForKey(key)
90
+ onStop?.()
91
+ },
92
+
93
+ presented: () => undefined,
94
+
95
+ after,
96
+ }
97
+ }
package/src/easing.ts ADDED
@@ -0,0 +1,100 @@
1
+ export type EasingFunction = (progress: number) => number
2
+
3
+ export type CubicBezier = readonly [x1: number, y1: number, x2: number, y2: number]
4
+
5
+ export type EasingName =
6
+ | 'linear'
7
+ | 'ease'
8
+ | 'easeIn'
9
+ | 'easeOut'
10
+ | 'easeInOut'
11
+ | 'circIn'
12
+ | 'circOut'
13
+ | 'circInOut'
14
+ | 'backIn'
15
+ | 'backOut'
16
+ | 'backInOut'
17
+ | 'anticipate'
18
+
19
+ export type Easing = EasingName | CubicBezier | EasingFunction
20
+
21
+ export const linear: EasingFunction = (p) => p
22
+
23
+ export function cubicBezier(x1: number, y1: number, x2: number, y2: number) {
24
+ if (x1 === y1 && x2 === y2) return linear
25
+
26
+ const cx = 3 * x1
27
+ const bx = 3 * (x2 - x1) - cx
28
+ const ax = 1 - cx - bx
29
+ const cy = 3 * y1
30
+ const by = 3 * (y2 - y1) - cy
31
+ const ay = 1 - cy - by
32
+
33
+ const sampleX = (t: number) => ((ax * t + bx) * t + cx) * t
34
+ const sampleY = (t: number) => ((ay * t + by) * t + cy) * t
35
+ const slopeX = (t: number) => (3 * ax * t + 2 * bx) * t + cx
36
+
37
+ const solveX = (x: number) => {
38
+ let t = x
39
+ for (let i = 0; i < 8; i++) {
40
+ const error = sampleX(t) - x
41
+ if (Math.abs(error) < 1e-7) return t
42
+ const slope = slopeX(t)
43
+ if (Math.abs(slope) < 1e-6) break
44
+ t -= error / slope
45
+ }
46
+
47
+ let low = 0
48
+ let high = 1
49
+ t = x
50
+ for (let i = 0; i < 40; i++) {
51
+ const value = sampleX(t)
52
+ if (Math.abs(value - x) < 1e-7) break
53
+ if (value < x) low = t
54
+ else high = t
55
+ t = (low + high) / 2
56
+ }
57
+ return t
58
+ }
59
+
60
+ return (p: number) => (p <= 0 ? 0 : p >= 1 ? 1 : sampleY(solveX(p)))
61
+ }
62
+
63
+ const backIn = cubicBezier(0.36, 0, 0.66, -0.56)
64
+ const circIn: EasingFunction = (p) => 1 - Math.sqrt(1 - Math.min(p * p, 1))
65
+
66
+ const reverse =
67
+ (easing: EasingFunction): EasingFunction =>
68
+ (p) =>
69
+ 1 - easing(1 - p)
70
+
71
+ const mirror =
72
+ (easing: EasingFunction): EasingFunction =>
73
+ (p) =>
74
+ p <= 0.5 ? easing(2 * p) / 2 : (2 - easing(2 * (1 - p))) / 2
75
+
76
+ const easings: Record<EasingName, EasingFunction> = {
77
+ linear,
78
+ ease: cubicBezier(0.25, 0.1, 0.25, 1),
79
+ easeIn: cubicBezier(0.42, 0, 1, 1),
80
+ easeOut: cubicBezier(0, 0, 0.58, 1),
81
+ easeInOut: cubicBezier(0.42, 0, 0.58, 1),
82
+ circIn,
83
+ circOut: reverse(circIn),
84
+ circInOut: mirror(circIn),
85
+ backIn,
86
+ backOut: reverse(backIn),
87
+ backInOut: mirror(backIn),
88
+ anticipate: (p) =>
89
+ p <= 0.5 ? backIn(2 * p) / 2 : p >= 1 ? 1 : 1 - Math.pow(2, 10 - 20 * p) / 2,
90
+ }
91
+
92
+ export function resolveEasing(easing: Easing): EasingFunction {
93
+ if (typeof easing === 'function') return easing
94
+ if (typeof easing === 'string') {
95
+ const resolved = easings[easing]
96
+ if (!resolved) throw new Error(`Unknown easing "${easing}".`)
97
+ return resolved
98
+ }
99
+ return cubicBezier(...easing)
100
+ }