@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.
- package/LICENSE +21 -0
- package/README.md +323 -2
- package/dist/animate.d.ts +26 -0
- package/dist/animate.d.ts.map +1 -0
- package/dist/animate.js +161 -0
- package/dist/animate.js.map +1 -0
- package/dist/driver/android.d.ts +3 -0
- package/dist/driver/android.d.ts.map +1 -0
- package/dist/driver/android.js +73 -0
- package/dist/driver/android.js.map +1 -0
- package/dist/driver/index.d.ts +18 -0
- package/dist/driver/index.d.ts.map +1 -0
- package/dist/driver/index.js +15 -0
- package/dist/driver/index.js.map +1 -0
- package/dist/driver/ios.d.ts +3 -0
- package/dist/driver/ios.d.ts.map +1 -0
- package/dist/driver/ios.js +77 -0
- package/dist/driver/ios.js.map +1 -0
- package/dist/easing.d.ts +8 -0
- package/dist/easing.d.ts.map +1 -0
- package/dist/easing.js +71 -0
- package/dist/easing.js.map +1 -0
- package/dist/engine.d.ts +48 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +178 -0
- package/dist/engine.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/presets.d.ts +310 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +141 -0
- package/dist/presets.js.map +1 -0
- package/dist/press.d.ts +13 -0
- package/dist/press.d.ts.map +1 -0
- package/dist/press.js +34 -0
- package/dist/press.js.map +1 -0
- package/dist/properties.d.ts +8 -0
- package/dist/properties.d.ts.map +1 -0
- package/dist/properties.js +42 -0
- package/dist/properties.js.map +1 -0
- package/dist/sequence.d.ts +26 -0
- package/dist/sequence.d.ts.map +1 -0
- package/dist/sequence.js +69 -0
- package/dist/sequence.js.map +1 -0
- package/dist/spring.d.ts +22 -0
- package/dist/spring.d.ts.map +1 -0
- package/dist/spring.js +54 -0
- package/dist/spring.js.map +1 -0
- package/dist/track.d.ts +28 -0
- package/dist/track.d.ts.map +1 -0
- package/dist/track.js +103 -0
- package/dist/track.js.map +1 -0
- package/dist/transition.d.ts +58 -0
- package/dist/transition.d.ts.map +1 -0
- package/dist/transition.js +82 -0
- package/dist/transition.js.map +1 -0
- package/dist/types.d.ts +40 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +67 -4
- package/src/animate.ts +246 -0
- package/src/driver/android.ts +101 -0
- package/src/driver/index.ts +32 -0
- package/src/driver/ios.ts +97 -0
- package/src/easing.ts +100 -0
- package/src/engine.ts +216 -0
- package/src/index.ts +12 -0
- package/src/presets.ts +158 -0
- package/src/press.ts +49 -0
- package/src/properties.ts +51 -0
- package/src/sequence.ts +88 -0
- package/src/spring.ts +95 -0
- package/src/track.ts +163 -0
- package/src/transition.ts +160 -0
- 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
|
+
}
|