@rafaelaugusto/jolt 0.1.0-beta.1 → 0.1.0-beta.10
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/README.md +367 -18
- package/dist/animate.d.ts +19 -3
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +186 -14
- package/dist/animate.js.map +1 -1
- package/dist/color.d.ts +11 -0
- package/dist/color.d.ts.map +1 -0
- package/dist/color.js +73 -0
- package/dist/color.js.map +1 -0
- package/dist/config.d.ts +31 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +73 -0
- package/dist/config.js.map +1 -0
- package/dist/decay.d.ts +11 -0
- package/dist/decay.d.ts.map +1 -0
- package/dist/decay.js +22 -0
- package/dist/decay.js.map +1 -0
- package/dist/decoration.d.ts +18 -0
- package/dist/decoration.d.ts.map +1 -0
- package/dist/decoration.js +155 -0
- package/dist/decoration.js.map +1 -0
- package/dist/drag-input.d.ts +15 -0
- package/dist/drag-input.d.ts.map +1 -0
- package/dist/drag-input.js +66 -0
- package/dist/drag-input.js.map +1 -0
- package/dist/drag-physics.d.ts +20 -0
- package/dist/drag-physics.d.ts.map +1 -0
- package/dist/drag-physics.js +75 -0
- package/dist/drag-physics.js.map +1 -0
- package/dist/drag.d.ts +58 -0
- package/dist/drag.d.ts.map +1 -0
- package/dist/drag.js +324 -0
- package/dist/drag.js.map +1 -0
- package/dist/driver/android-color.d.ts +20 -0
- package/dist/driver/android-color.d.ts.map +1 -0
- package/dist/driver/android-color.js +136 -0
- package/dist/driver/android-color.js.map +1 -0
- package/dist/driver/android-decoration.d.ts +22 -0
- package/dist/driver/android-decoration.d.ts.map +1 -0
- package/dist/driver/android-decoration.js +139 -0
- package/dist/driver/android-decoration.js.map +1 -0
- package/dist/driver/android-playback.d.ts +13 -0
- package/dist/driver/android-playback.d.ts.map +1 -0
- package/dist/driver/android-playback.js +141 -0
- package/dist/driver/android-playback.js.map +1 -0
- package/dist/driver/android.d.ts +1 -1
- package/dist/driver/android.d.ts.map +1 -1
- package/dist/driver/android.js +165 -13
- package/dist/driver/android.js.map +1 -1
- package/dist/driver/index.d.ts +25 -2
- package/dist/driver/index.d.ts.map +1 -1
- package/dist/driver/index.js.map +1 -1
- package/dist/driver/ios-color.d.ts +16 -0
- package/dist/driver/ios-color.d.ts.map +1 -0
- package/dist/driver/ios-color.js +392 -0
- package/dist/driver/ios-color.js.map +1 -0
- package/dist/driver/ios-decoration.d.ts +17 -0
- package/dist/driver/ios-decoration.d.ts.map +1 -0
- package/dist/driver/ios-decoration.js +271 -0
- package/dist/driver/ios-decoration.js.map +1 -0
- package/dist/driver/ios-layout.d.ts +9 -0
- package/dist/driver/ios-layout.d.ts.map +1 -0
- package/dist/driver/ios-layout.js +52 -0
- package/dist/driver/ios-layout.js.map +1 -0
- package/dist/driver/ios-playback.d.ts +10 -0
- package/dist/driver/ios-playback.d.ts.map +1 -0
- package/dist/driver/ios-playback.js +47 -0
- package/dist/driver/ios-playback.js.map +1 -0
- package/dist/driver/ios-shadow.d.ts +6 -0
- package/dist/driver/ios-shadow.d.ts.map +1 -0
- package/dist/driver/ios-shadow.js +52 -0
- package/dist/driver/ios-shadow.js.map +1 -0
- package/dist/driver/ios.d.ts.map +1 -1
- package/dist/driver/ios.js +163 -28
- package/dist/driver/ios.js.map +1 -1
- package/dist/engine.d.ts +75 -5
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +293 -23
- package/dist/engine.js.map +1 -1
- package/dist/haptics.d.ts +21 -0
- package/dist/haptics.d.ts.map +1 -0
- package/dist/haptics.js +120 -0
- package/dist/haptics.js.map +1 -0
- package/dist/image-effect-native.d.ts +26 -0
- package/dist/image-effect-native.d.ts.map +1 -0
- package/dist/image-effect-native.js +73 -0
- package/dist/image-effect-native.js.map +1 -0
- package/dist/image-effect.d.ts +50 -0
- package/dist/image-effect.d.ts.map +1 -0
- package/dist/image-effect.js +290 -0
- package/dist/image-effect.js.map +1 -0
- package/dist/image-owner.d.ts +3 -0
- package/dist/image-owner.d.ts.map +1 -0
- package/dist/image-owner.js +11 -0
- package/dist/image-owner.js.map +1 -0
- package/dist/index.d.ts +43 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -1
- package/dist/index.js.map +1 -1
- package/dist/interaction-surface.d.ts +12 -0
- package/dist/interaction-surface.d.ts.map +1 -0
- package/dist/interaction-surface.js +36 -0
- package/dist/interaction-surface.js.map +1 -0
- package/dist/items.d.ts +20 -0
- package/dist/items.d.ts.map +1 -0
- package/dist/items.js +109 -0
- package/dist/items.js.map +1 -0
- package/dist/keyboard.d.ts +41 -0
- package/dist/keyboard.d.ts.map +1 -0
- package/dist/keyboard.js +256 -0
- package/dist/keyboard.js.map +1 -0
- package/dist/label.d.ts +7 -0
- package/dist/label.d.ts.map +1 -0
- package/dist/label.js +60 -0
- package/dist/label.js.map +1 -0
- package/dist/layout.d.ts +13 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +348 -0
- package/dist/layout.js.map +1 -0
- package/dist/liquid-grid.d.ts +3 -0
- package/dist/liquid-grid.d.ts.map +1 -0
- package/dist/liquid-grid.js +24 -0
- package/dist/liquid-grid.js.map +1 -0
- package/dist/liquid.d.ts +18 -0
- package/dist/liquid.d.ts.map +1 -0
- package/dist/liquid.js +234 -0
- package/dist/liquid.js.map +1 -0
- package/dist/loader.d.ts +19 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +173 -0
- package/dist/loader.js.map +1 -0
- package/dist/motion-scope.d.ts +39 -0
- package/dist/motion-scope.d.ts.map +1 -0
- package/dist/motion-scope.js +238 -0
- package/dist/motion-scope.js.map +1 -0
- package/dist/motion-states.d.ts +36 -0
- package/dist/motion-states.d.ts.map +1 -0
- package/dist/motion-states.js +165 -0
- package/dist/motion-states.js.map +1 -0
- package/dist/particles.d.ts +39 -0
- package/dist/particles.d.ts.map +1 -0
- package/dist/particles.js +265 -0
- package/dist/particles.js.map +1 -0
- package/dist/path.d.ts +12 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +90 -0
- package/dist/path.js.map +1 -0
- package/dist/playback-group.d.ts +6 -0
- package/dist/playback-group.d.ts.map +1 -0
- package/dist/playback-group.js +141 -0
- package/dist/playback-group.js.map +1 -0
- package/dist/playback.d.ts +25 -0
- package/dist/playback.d.ts.map +1 -0
- package/dist/playback.js +35 -0
- package/dist/playback.js.map +1 -0
- package/dist/press.d.ts.map +1 -1
- package/dist/press.js +23 -7
- package/dist/press.js.map +1 -1
- package/dist/properties.d.ts +6 -3
- package/dist/properties.d.ts.map +1 -1
- package/dist/properties.js +26 -2
- package/dist/properties.js.map +1 -1
- package/dist/reveal.d.ts +25 -0
- package/dist/reveal.d.ts.map +1 -0
- package/dist/reveal.js +294 -0
- package/dist/reveal.js.map +1 -0
- package/dist/roll-diff.d.ts +22 -0
- package/dist/roll-diff.d.ts.map +1 -0
- package/dist/roll-diff.js +36 -0
- package/dist/roll-diff.js.map +1 -0
- package/dist/roll.d.ts +29 -0
- package/dist/roll.d.ts.map +1 -0
- package/dist/roll.js +442 -0
- package/dist/roll.js.map +1 -0
- package/dist/scroll-linked.d.ts +10 -0
- package/dist/scroll-linked.d.ts.map +1 -0
- package/dist/scroll-linked.js +58 -0
- package/dist/scroll-linked.js.map +1 -0
- package/dist/sequence.d.ts +10 -5
- package/dist/sequence.d.ts.map +1 -1
- package/dist/sequence.js +208 -14
- package/dist/sequence.js.map +1 -1
- package/dist/shimmer.d.ts +12 -0
- package/dist/shimmer.d.ts.map +1 -0
- package/dist/shimmer.js +161 -0
- package/dist/shimmer.js.map +1 -0
- package/dist/svg-path.d.ts +14 -0
- package/dist/svg-path.d.ts.map +1 -0
- package/dist/svg-path.js +281 -0
- package/dist/svg-path.js.map +1 -0
- package/dist/symbol-effect.d.ts +23 -0
- package/dist/symbol-effect.d.ts.map +1 -0
- package/dist/symbol-effect.js +187 -0
- package/dist/symbol-effect.js.map +1 -0
- package/dist/text-layout.d.ts +30 -0
- package/dist/text-layout.d.ts.map +1 -0
- package/dist/text-layout.js +185 -0
- package/dist/text-layout.js.map +1 -0
- package/dist/text.d.ts +17 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +263 -0
- package/dist/text.js.map +1 -0
- package/dist/track.d.ts +3 -1
- package/dist/track.d.ts.map +1 -1
- package/dist/track.js +266 -13
- package/dist/track.js.map +1 -1
- package/dist/transition.d.ts +17 -1
- package/dist/transition.d.ts.map +1 -1
- package/dist/transition.js +41 -6
- package/dist/transition.js.map +1 -1
- package/dist/types.d.ts +48 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/vue/index.d.ts +95 -0
- package/dist/vue/index.d.ts.map +1 -0
- package/dist/vue/index.js +163 -0
- package/dist/vue/index.js.map +1 -0
- package/dist/zoom-native.d.ts +30 -0
- package/dist/zoom-native.d.ts.map +1 -0
- package/dist/zoom-native.js +56 -0
- package/dist/zoom-native.js.map +1 -0
- package/dist/zoomable.d.ts +48 -0
- package/dist/zoomable.d.ts.map +1 -0
- package/dist/zoomable.js +249 -0
- package/dist/zoomable.js.map +1 -0
- package/package.json +29 -5
- package/platforms/android/include.gradle +7 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/ColorTarget.java +102 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/DecorationTarget.java +154 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/ImageEffect.java +288 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFollower.java +91 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardObserver.java +97 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/NumberRoll.java +190 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/PhotoZoom.java +250 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/TextReveal.java +180 -0
- package/platforms/android/proguard-rules.pro +19 -0
- package/platforms/ios/build.xcconfig +1 -0
- package/platforms/ios/src/JoltImageEffect.h +25 -0
- package/platforms/ios/src/JoltImageEffect.mm +242 -0
- package/platforms/ios/src/JoltInteractiveAnimation.h +18 -0
- package/platforms/ios/src/JoltInteractiveAnimation.m +123 -0
- package/platforms/ios/src/JoltPhotoZoom.h +23 -0
- package/platforms/ios/src/JoltPhotoZoom.m +229 -0
- package/platforms/ios/src/module.modulemap +6 -0
- package/src/android-plugin.d.ts +155 -0
- package/src/animate.ts +249 -36
- package/src/color.ts +101 -0
- package/src/config.ts +84 -0
- package/src/decay.ts +45 -0
- package/src/decoration.ts +180 -0
- package/src/drag-input.ts +74 -0
- package/src/drag-physics.ts +110 -0
- package/src/drag.ts +395 -0
- package/src/driver/android-color.ts +170 -0
- package/src/driver/android-decoration.ts +182 -0
- package/src/driver/android-playback.ts +157 -0
- package/src/driver/android.ts +208 -20
- package/src/driver/index.ts +37 -2
- package/src/driver/ios-color.ts +441 -0
- package/src/driver/ios-decoration.ts +332 -0
- package/src/driver/ios-layout.ts +63 -0
- package/src/driver/ios-playback.ts +51 -0
- package/src/driver/ios-shadow.ts +50 -0
- package/src/driver/ios.ts +193 -33
- package/src/engine.ts +321 -27
- package/src/haptics.ts +148 -0
- package/src/image-effect-native.ts +129 -0
- package/src/image-effect.ts +336 -0
- package/src/image-owner.ts +12 -0
- package/src/index.ts +59 -2
- package/src/interaction-surface.ts +41 -0
- package/src/ios-plugin.d.ts +81 -0
- package/src/items.ts +140 -0
- package/src/keyboard.ts +291 -0
- package/src/label.ts +76 -0
- package/src/layout.ts +408 -0
- package/src/liquid-grid.ts +28 -0
- package/src/liquid.ts +287 -0
- package/src/loader.ts +202 -0
- package/src/motion-scope.ts +279 -0
- package/src/motion-states.ts +215 -0
- package/src/particles.ts +323 -0
- package/src/path.ts +124 -0
- package/src/playback-group.ts +143 -0
- package/src/playback.ts +62 -0
- package/src/press.ts +26 -10
- package/src/properties.ts +32 -6
- package/src/reveal.ts +374 -0
- package/src/roll-diff.ts +57 -0
- package/src/roll.ts +515 -0
- package/src/scroll-linked.ts +73 -0
- package/src/sequence.ts +204 -14
- package/src/shimmer.ts +180 -0
- package/src/svg-path.ts +355 -0
- package/src/symbol-effect.ts +226 -0
- package/src/text-layout.ts +228 -0
- package/src/text.ts +279 -0
- package/src/track.ts +347 -12
- package/src/transition.ts +63 -6
- package/src/types.ts +49 -1
- package/src/vue/index.ts +231 -0
- package/src/zoom-native.ts +124 -0
- package/src/zoomable.ts +330 -0
package/src/sequence.ts
CHANGED
|
@@ -1,22 +1,36 @@
|
|
|
1
|
+
import { timeScale } from './config.js'
|
|
2
|
+
import { getDriver } from './driver/index.js'
|
|
3
|
+
import { isDecorationProperty, validateDecorationTarget } from './decoration.js'
|
|
1
4
|
import type { View } from '@nativescript/core'
|
|
2
5
|
import { animate, startingValues, toViews } from './animate.js'
|
|
3
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
arrivalOf,
|
|
8
|
+
arrivalTimingOf,
|
|
9
|
+
setArrival,
|
|
10
|
+
stopView,
|
|
11
|
+
type Animation,
|
|
12
|
+
} from './engine.js'
|
|
13
|
+
import { createClock } from './playback.js'
|
|
4
14
|
import type { PresetName } from './presets.js'
|
|
5
15
|
import { writeValue, type Property } from './properties.js'
|
|
6
16
|
import type { SpringName } from './transition.js'
|
|
7
17
|
import type { AnimateOptions, Keyframes, Target } from './types.js'
|
|
8
18
|
|
|
19
|
+
/** Absolute milliseconds, a signed offset from the previous arrival, or its start. */
|
|
20
|
+
export type SequencePosition = number | '<' | `${'+' | '-'}${number}`
|
|
21
|
+
export type SequenceOptions = AnimateOptions & { at?: SequencePosition }
|
|
22
|
+
|
|
9
23
|
export type Step =
|
|
10
24
|
| readonly [
|
|
11
25
|
target: Target | readonly Target[],
|
|
12
26
|
animation: Keyframes | PresetName,
|
|
13
|
-
options?:
|
|
27
|
+
options?: SequenceOptions | SpringName,
|
|
14
28
|
]
|
|
15
29
|
| (() => Animation)
|
|
16
30
|
|
|
17
31
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
32
|
+
* By default a step starts once the previous one looks done. `at` can overlap
|
|
33
|
+
* that arrival, share the previous start, or select an absolute timeline time.
|
|
20
34
|
* Views in later steps take their first absolute keyframe right away, so a title
|
|
21
35
|
* that fades in third doesn't show up before its turn. Relative and null keyframes
|
|
22
36
|
* read the current value when their step starts.
|
|
@@ -24,26 +38,180 @@ export type Step =
|
|
|
24
38
|
* ```ts
|
|
25
39
|
* await sequence([
|
|
26
40
|
* [logo, 'popIn'],
|
|
27
|
-
* [title, 'fadeInUp'],
|
|
28
|
-
* [buttons, 'fadeInUp', { stagger: 50 }],
|
|
41
|
+
* [title, 'fadeInUp', { at: '-150' }],
|
|
42
|
+
* [buttons, 'fadeInUp', { stagger: 50, at: '<' }],
|
|
29
43
|
* ])
|
|
30
44
|
* ```
|
|
31
45
|
*/
|
|
32
46
|
export function sequence(steps: readonly Step[]): Animation {
|
|
33
|
-
|
|
47
|
+
const prepared = steps.map((step) => {
|
|
48
|
+
if (typeof step === 'function') return { step, position: undefined }
|
|
49
|
+
const [target, animation, options] = step
|
|
50
|
+
if (typeof options !== 'object' || options === null)
|
|
51
|
+
return { step, position: undefined }
|
|
52
|
+
const { at, ...transition } = options
|
|
53
|
+
const position = parsePosition(at)
|
|
54
|
+
return { step: [target, animation, transition] as Step, position }
|
|
55
|
+
})
|
|
56
|
+
applyStartingValues(prepared.map(({ step }) => step))
|
|
34
57
|
|
|
58
|
+
const driver = getDriver()
|
|
59
|
+
const slowdown = 1 / timeScale()
|
|
60
|
+
// Absolute positions share the platform clock, including Android duration scale.
|
|
61
|
+
const clock = prepared.some(({ position }) => typeof position === 'number')
|
|
62
|
+
? (driver.clock?.(Infinity) ?? createClock(() => driver.now()))
|
|
63
|
+
: undefined
|
|
35
64
|
const started: Animation[] = []
|
|
65
|
+
const cancellations = new Set<() => void>()
|
|
36
66
|
let stopped = false
|
|
67
|
+
let resolveScheduled!: () => void
|
|
68
|
+
let rejectScheduled!: (reason: unknown) => void
|
|
69
|
+
const scheduled = new Promise<void>((resolve, reject) => {
|
|
70
|
+
resolveScheduled = resolve
|
|
71
|
+
rejectScheduled = reject
|
|
72
|
+
})
|
|
73
|
+
let pending = prepared.length
|
|
74
|
+
const states = prepared.map(({ step, position }) => {
|
|
75
|
+
return {
|
|
76
|
+
step,
|
|
77
|
+
position,
|
|
78
|
+
onStart: new Set<() => void>(),
|
|
79
|
+
animation: undefined as Animation | undefined,
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
const cancelSchedule = () => {
|
|
83
|
+
for (const cancel of cancellations) cancel()
|
|
84
|
+
cancellations.clear()
|
|
85
|
+
clock?.cancel()
|
|
86
|
+
}
|
|
87
|
+
const fail = (error: unknown) => {
|
|
88
|
+
if (stopped) return
|
|
89
|
+
stopped = true
|
|
90
|
+
cancelSchedule()
|
|
91
|
+
states.forEach((state) => state.onStart.clear())
|
|
92
|
+
started.forEach((child) => child.stop())
|
|
93
|
+
rejectScheduled(error)
|
|
94
|
+
}
|
|
95
|
+
const launch = (index: number) => {
|
|
96
|
+
const state = states[index]!
|
|
97
|
+
if (stopped || state.animation) return
|
|
98
|
+
try {
|
|
99
|
+
const step = state.step
|
|
100
|
+
const child = typeof step === 'function' ? step() : animate(...step)
|
|
101
|
+
state.animation = child
|
|
102
|
+
started.push(child)
|
|
103
|
+
// Observe setup failures immediately, even while other steps are still
|
|
104
|
+
// scheduled. Nested sequences can reject before this one reaches arrival.
|
|
105
|
+
void child.finished.catch(fail)
|
|
106
|
+
pending--
|
|
107
|
+
for (const callback of [...state.onStart]) callback()
|
|
108
|
+
state.onStart.clear()
|
|
109
|
+
if (pending === 0) {
|
|
110
|
+
cancelSchedule()
|
|
111
|
+
resolveScheduled()
|
|
112
|
+
}
|
|
113
|
+
} catch (error) {
|
|
114
|
+
fail(error)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const wait = (ms: number, callback: () => void) => {
|
|
118
|
+
if (stopped) return
|
|
119
|
+
if (ms <= 0) {
|
|
120
|
+
callback()
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
let cancel = () => {}
|
|
124
|
+
let fired = false
|
|
125
|
+
const off = driver.after(ms, () => {
|
|
126
|
+
fired = true
|
|
127
|
+
cancellations.delete(cancel)
|
|
128
|
+
if (!stopped) callback()
|
|
129
|
+
})
|
|
130
|
+
cancel = off
|
|
131
|
+
if (!fired) cancellations.add(cancel)
|
|
132
|
+
}
|
|
37
133
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
134
|
+
for (let index = 0; index < states.length; index++) {
|
|
135
|
+
const { position } = states[index]!
|
|
136
|
+
const begin = () => launch(index)
|
|
137
|
+
if (typeof position === 'number') wait(position * slowdown - clock!.time(), begin)
|
|
138
|
+
else if (index === 0)
|
|
139
|
+
wait(
|
|
140
|
+
typeof position === 'object' ? Math.max(0, position.offset) * slowdown : 0,
|
|
141
|
+
begin,
|
|
142
|
+
)
|
|
143
|
+
else {
|
|
144
|
+
const previous = states[index - 1]!
|
|
145
|
+
const follow = () => {
|
|
146
|
+
if (stopped || !previous.animation) return
|
|
147
|
+
if (position === '<') {
|
|
148
|
+
begin()
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
const offset = (position?.offset ?? 0) * slowdown
|
|
152
|
+
if (offset < 0) {
|
|
153
|
+
const timing = arrivalTimingOf(previous.animation)
|
|
154
|
+
if (!timing) {
|
|
155
|
+
fail(
|
|
156
|
+
new TypeError(
|
|
157
|
+
'A negative at offset needs an animate() or decay() arrival forecast.',
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
let off = () => {}
|
|
163
|
+
let cancelTimer = () => {}
|
|
164
|
+
let fired = false
|
|
165
|
+
let disposed = false
|
|
166
|
+
let queued = false
|
|
167
|
+
const dispose = () => {
|
|
168
|
+
disposed = true
|
|
169
|
+
off()
|
|
170
|
+
cancelTimer()
|
|
171
|
+
cancellations.delete(dispose)
|
|
172
|
+
}
|
|
173
|
+
const update = () => {
|
|
174
|
+
cancelTimer()
|
|
175
|
+
if (stopped || fired || disposed) return
|
|
176
|
+
const state = timing.read()
|
|
177
|
+
if (!state.arrived && (state.paused || state.remaining === undefined)) return
|
|
178
|
+
const remaining = state.arrived ? 0 : state.remaining! + offset
|
|
179
|
+
if (!Number.isFinite(remaining)) return
|
|
180
|
+
const fire = () => {
|
|
181
|
+
if (stopped || fired || disposed) return
|
|
182
|
+
fired = true
|
|
183
|
+
dispose()
|
|
184
|
+
begin()
|
|
185
|
+
}
|
|
186
|
+
if (remaining <= 0) fire()
|
|
187
|
+
else {
|
|
188
|
+
const cancel = driver.after(remaining, fire)
|
|
189
|
+
if (disposed) cancel()
|
|
190
|
+
else cancelTimer = cancel
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
off = timing.subscribe(() => {
|
|
194
|
+
cancelTimer()
|
|
195
|
+
// An interruption can notify from inside another animation's
|
|
196
|
+
// constructor. Start followers after its ownership changes finish.
|
|
197
|
+
if (queued || disposed) return
|
|
198
|
+
queued = true
|
|
199
|
+
void Promise.resolve().then(() => {
|
|
200
|
+
queued = false
|
|
201
|
+
update()
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
cancellations.add(dispose)
|
|
205
|
+
update()
|
|
206
|
+
} else void arrivalOf(previous.animation).then(() => wait(offset, begin), fail)
|
|
207
|
+
}
|
|
208
|
+
if (previous.animation) follow()
|
|
209
|
+
else previous.onStart.add(follow)
|
|
44
210
|
}
|
|
45
|
-
}
|
|
211
|
+
}
|
|
212
|
+
if (pending === 0) resolveScheduled()
|
|
46
213
|
|
|
214
|
+
const arrived = scheduled.then(() => Promise.all(started.map(arrivalOf)))
|
|
47
215
|
const finished = arrived.then(async () => {
|
|
48
216
|
const results = await Promise.all(started.map((animation) => animation.finished))
|
|
49
217
|
return !stopped && results.length === steps.length && results.every(Boolean)
|
|
@@ -52,7 +220,11 @@ export function sequence(steps: readonly Step[]): Animation {
|
|
|
52
220
|
const animation: Animation = {
|
|
53
221
|
finished,
|
|
54
222
|
stop() {
|
|
223
|
+
if (stopped) return
|
|
55
224
|
stopped = true
|
|
225
|
+
cancelSchedule()
|
|
226
|
+
states.forEach((state) => state.onStart.clear())
|
|
227
|
+
resolveScheduled()
|
|
56
228
|
started.forEach((animation) => animation.stop())
|
|
57
229
|
},
|
|
58
230
|
then: (onfulfilled, onrejected) => finished.then(onfulfilled, onrejected),
|
|
@@ -61,6 +233,21 @@ export function sequence(steps: readonly Step[]): Animation {
|
|
|
61
233
|
return animation
|
|
62
234
|
}
|
|
63
235
|
|
|
236
|
+
type Position = number | '<' | { offset: number } | undefined
|
|
237
|
+
function parsePosition(at: SequencePosition | undefined): Position {
|
|
238
|
+
if (at === undefined || at === '<') return at
|
|
239
|
+
if (typeof at === 'number' && Number.isFinite(at) && at >= 0) return at
|
|
240
|
+
if (
|
|
241
|
+
typeof at === 'string' &&
|
|
242
|
+
/^[+-](?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i.test(at) &&
|
|
243
|
+
Number.isFinite(Number(at))
|
|
244
|
+
)
|
|
245
|
+
return { offset: Number(at) }
|
|
246
|
+
throw new RangeError(
|
|
247
|
+
'Sequence at must be a nonnegative finite time, "<", or a signed finite offset.',
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
|
|
64
251
|
function applyStartingValues(steps: readonly Step[]) {
|
|
65
252
|
const claimed = new Map<View, Set<Property>>()
|
|
66
253
|
for (const step of steps) {
|
|
@@ -69,6 +256,8 @@ function applyStartingValues(steps: readonly Step[]) {
|
|
|
69
256
|
const starts = startingValues(animation, options)
|
|
70
257
|
|
|
71
258
|
for (const view of toViews(target)) {
|
|
259
|
+
for (const property of starts.keys())
|
|
260
|
+
if (isDecorationProperty(property)) validateDecorationTarget(view, property)
|
|
72
261
|
let properties = claimed.get(view)
|
|
73
262
|
if (!properties) claimed.set(view, (properties = new Set()))
|
|
74
263
|
|
|
@@ -82,6 +271,7 @@ function applyStartingValues(steps: readonly Step[]) {
|
|
|
82
271
|
for (const [property, value] of fresh) {
|
|
83
272
|
if (value !== null) writeValue(view, property, value)
|
|
84
273
|
}
|
|
274
|
+
getDriver().sync?.(view)
|
|
85
275
|
for (const property of starts.keys()) properties.add(property)
|
|
86
276
|
}
|
|
87
277
|
}
|
package/src/shimmer.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { Application, isIOS, type View } from '@nativescript/core'
|
|
2
|
+
import { toView } from './animate.js'
|
|
3
|
+
import type { Target } from './types.js'
|
|
4
|
+
import { reducedMotion } from './config.js'
|
|
5
|
+
|
|
6
|
+
export interface ShimmerOptions {
|
|
7
|
+
/** Milliseconds per sweep. Defaults to 1500. */
|
|
8
|
+
duration?: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const running = new WeakMap<View, () => void>()
|
|
12
|
+
const key = 'jolt:shimmer'
|
|
13
|
+
|
|
14
|
+
// NativeScript saves a mask here while applying clip-path or non-uniform borders.
|
|
15
|
+
type MaskedView = UIView & { originalMask?: CALayer | null }
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Sweeps a native gradient mask across an iOS placeholder until stopped.
|
|
19
|
+
* Waits for layout, respects Reduce Motion and pauses while the view is unloaded.
|
|
20
|
+
* Android is a no-op. Returns an idempotent cleanup function.
|
|
21
|
+
*/
|
|
22
|
+
export function shimmer(target: Target, options: ShimmerOptions = {}): () => void {
|
|
23
|
+
const view = toView(target)
|
|
24
|
+
const duration = options.duration ?? 1500
|
|
25
|
+
if (!Number.isFinite(duration) || duration <= 0)
|
|
26
|
+
throw new RangeError('Shimmer duration must be a positive finite number.')
|
|
27
|
+
running.get(view)?.()
|
|
28
|
+
if (!isIOS) return () => {}
|
|
29
|
+
return new Shimmer(view, duration).stop
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class Shimmer {
|
|
33
|
+
private stopped = false
|
|
34
|
+
private queued = false
|
|
35
|
+
private waitingForLoad: boolean
|
|
36
|
+
private native: MaskedView | undefined
|
|
37
|
+
private owner: CALayer | undefined
|
|
38
|
+
private gradient: CAGradientLayer | undefined
|
|
39
|
+
private observer: ReturnType<typeof Application.ios.addNotificationObserver> | undefined
|
|
40
|
+
|
|
41
|
+
constructor(
|
|
42
|
+
private readonly view: View,
|
|
43
|
+
private readonly duration: number,
|
|
44
|
+
) {
|
|
45
|
+
this.waitingForLoad = !view.isLoaded
|
|
46
|
+
running.set(view, this.stop)
|
|
47
|
+
view.on('loaded', this.loaded)
|
|
48
|
+
view.on('layoutChanged', this.schedule)
|
|
49
|
+
view.on('unloaded', this.unloaded)
|
|
50
|
+
view.style.on('backgroundInternalChange', this.schedule)
|
|
51
|
+
this.update()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
stop = () => {
|
|
55
|
+
if (this.stopped) return
|
|
56
|
+
this.stopped = true
|
|
57
|
+
const { view } = this
|
|
58
|
+
view.off('loaded', this.loaded)
|
|
59
|
+
view.off('layoutChanged', this.schedule)
|
|
60
|
+
view.off('unloaded', this.unloaded)
|
|
61
|
+
view.style.off('backgroundInternalChange', this.schedule)
|
|
62
|
+
if (running.get(view) === this.stop) running.delete(view)
|
|
63
|
+
this.unloaded()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private loaded = () => {
|
|
67
|
+
this.waitingForLoad = false
|
|
68
|
+
this.schedule()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private unloaded = () => {
|
|
72
|
+
this.waitingForLoad = true
|
|
73
|
+
if (this.observer) {
|
|
74
|
+
Application.ios.removeNotificationObserver(
|
|
75
|
+
this.observer,
|
|
76
|
+
UIAccessibilityReduceMotionStatusDidChangeNotification,
|
|
77
|
+
)
|
|
78
|
+
this.observer = undefined
|
|
79
|
+
}
|
|
80
|
+
this.remove()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private schedule = () => {
|
|
84
|
+
if (this.stopped || this.queued) return
|
|
85
|
+
this.queued = true
|
|
86
|
+
// Core raises layoutChanged before redrawing the background and its mask.
|
|
87
|
+
// Attach after that pass, coalescing events from the same layout.
|
|
88
|
+
void Promise.resolve().then(() => {
|
|
89
|
+
this.queued = false
|
|
90
|
+
this.update()
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private update = () => {
|
|
95
|
+
if (this.stopped || this.waitingForLoad || !this.view.isLoaded) return
|
|
96
|
+
const native = this.view.nativeViewProtected as MaskedView | undefined
|
|
97
|
+
if (!native) return
|
|
98
|
+
this.observer ??= Application.ios.addNotificationObserver(
|
|
99
|
+
UIAccessibilityReduceMotionStatusDidChangeNotification,
|
|
100
|
+
this.update,
|
|
101
|
+
)
|
|
102
|
+
if (reducedMotion()) return this.remove()
|
|
103
|
+
const bounds = native.layer.bounds
|
|
104
|
+
if (bounds.size.width <= 0 || bounds.size.height <= 0) return this.remove()
|
|
105
|
+
if (this.native && this.native !== native) this.remove()
|
|
106
|
+
|
|
107
|
+
CATransaction.begin()
|
|
108
|
+
CATransaction.setDisableActions(true)
|
|
109
|
+
try {
|
|
110
|
+
this.native = native
|
|
111
|
+
const gradient = (this.gradient ??= this.createGradient())
|
|
112
|
+
// Leave Core's mask at the root so it can keep updating its path on resize.
|
|
113
|
+
let owner = native.layer
|
|
114
|
+
while (owner.mask && owner.mask !== gradient) owner = owner.mask
|
|
115
|
+
if (this.owner && this.owner !== owner && this.owner.mask === gradient)
|
|
116
|
+
this.owner.mask = null
|
|
117
|
+
this.owner = owner
|
|
118
|
+
owner.mask = gradient
|
|
119
|
+
// Don't let a later background redraw resurrect our mask after cleanup.
|
|
120
|
+
if (native.originalMask === gradient) native.originalMask = null
|
|
121
|
+
|
|
122
|
+
// Core's CAShapeLayer masks draw a path with zero bounds. In that case the
|
|
123
|
+
// path uses the view's coordinates; other masks have their own bounds.
|
|
124
|
+
const area =
|
|
125
|
+
owner.bounds.size.width > 0 && owner.bounds.size.height > 0
|
|
126
|
+
? owner.bounds
|
|
127
|
+
: bounds
|
|
128
|
+
gradient.frame = CGRectMake(
|
|
129
|
+
area.origin.x - area.size.width,
|
|
130
|
+
area.origin.y,
|
|
131
|
+
area.size.width * 3,
|
|
132
|
+
area.size.height,
|
|
133
|
+
)
|
|
134
|
+
} finally {
|
|
135
|
+
CATransaction.commit()
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private createGradient(): CAGradientLayer {
|
|
140
|
+
const gradient = CAGradientLayer.new()
|
|
141
|
+
const dim = UIColor.colorWithWhiteAlpha(1, 0.35).CGColor
|
|
142
|
+
gradient.colors = NSArray.arrayWithArray([dim, UIColor.whiteColor.CGColor, dim])
|
|
143
|
+
gradient.startPoint = CGPointMake(0, 0.5)
|
|
144
|
+
gradient.endPoint = CGPointMake(1, 0.5)
|
|
145
|
+
|
|
146
|
+
// A three-width mask lets the band enter and leave while every stop stays
|
|
147
|
+
// within the documented 0...1 range for CAGradientLayer.locations.
|
|
148
|
+
const from = NSArray.arrayWithArray([0, 0.125, 0.25])
|
|
149
|
+
const to = NSArray.arrayWithArray([0.75, 0.875, 1])
|
|
150
|
+
gradient.locations = from
|
|
151
|
+
const animation = CABasicAnimation.animationWithKeyPath('locations')
|
|
152
|
+
animation.fromValue = from
|
|
153
|
+
animation.toValue = to
|
|
154
|
+
animation.duration = this.duration / 1000
|
|
155
|
+
animation.repeatCount = Infinity
|
|
156
|
+
animation.removedOnCompletion = false
|
|
157
|
+
animation.timingFunction = CAMediaTimingFunction.functionWithName(
|
|
158
|
+
kCAMediaTimingFunctionLinear,
|
|
159
|
+
)
|
|
160
|
+
gradient.addAnimationForKey(animation, key)
|
|
161
|
+
return gradient
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private remove() {
|
|
165
|
+
const { gradient, owner, native } = this
|
|
166
|
+
if (!gradient) return
|
|
167
|
+
CATransaction.begin()
|
|
168
|
+
CATransaction.setDisableActions(true)
|
|
169
|
+
try {
|
|
170
|
+
gradient.removeAnimationForKey(key)
|
|
171
|
+
if (owner?.mask === gradient) owner.mask = null
|
|
172
|
+
if (native?.originalMask === gradient) native.originalMask = null
|
|
173
|
+
} finally {
|
|
174
|
+
CATransaction.commit()
|
|
175
|
+
}
|
|
176
|
+
this.owner = undefined
|
|
177
|
+
this.gradient = undefined
|
|
178
|
+
this.native = undefined
|
|
179
|
+
}
|
|
180
|
+
}
|