@rafaelaugusto/jolt 0.1.0-beta.4 → 0.1.0-beta.5
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 +24 -1
- package/dist/animate.d.ts +3 -2
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +11 -3
- package/dist/animate.js.map +1 -1
- 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/driver/android-color.d.ts +3 -0
- package/dist/driver/android-color.d.ts.map +1 -1
- package/dist/driver/android-color.js +20 -18
- package/dist/driver/android-color.js.map +1 -1
- 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.map +1 -1
- package/dist/driver/android.js +23 -36
- package/dist/driver/android.js.map +1 -1
- package/dist/driver/index.d.ts +7 -0
- package/dist/driver/index.d.ts.map +1 -1
- package/dist/driver/index.js.map +1 -1
- package/dist/driver/ios-color.d.ts +2 -0
- package/dist/driver/ios-color.d.ts.map +1 -1
- package/dist/driver/ios-color.js +19 -8
- package/dist/driver/ios-color.js.map +1 -1
- 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-playback.d.ts +10 -0
- package/dist/driver/ios-playback.d.ts.map +1 -0
- package/dist/driver/ios-playback.js +33 -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 +75 -40
- package/dist/driver/ios.js.map +1 -1
- package/dist/engine.d.ts +48 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +210 -18
- package/dist/engine.js.map +1 -1
- package/dist/haptics.d.ts +4 -1
- package/dist/haptics.d.ts.map +1 -1
- package/dist/haptics.js +27 -3
- package/dist/haptics.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/playback-group.d.ts +6 -0
- package/dist/playback-group.d.ts.map +1 -0
- package/dist/playback-group.js +113 -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/properties.d.ts +2 -1
- package/dist/properties.d.ts.map +1 -1
- package/dist/properties.js +14 -1
- package/dist/properties.js.map +1 -1
- package/dist/sequence.d.ts.map +1 -1
- package/dist/sequence.js +6 -0
- package/dist/sequence.js.map +1 -1
- package/dist/track.d.ts.map +1 -1
- package/dist/track.js +10 -1
- package/dist/track.js.map +1 -1
- package/dist/transition.d.ts.map +1 -1
- package/dist/transition.js +4 -1
- package/dist/transition.js.map +1 -1
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/platforms/android/java/org/rafaelaugusto/jolt/ColorTarget.java +2 -2
- package/platforms/android/java/org/rafaelaugusto/jolt/DecorationTarget.java +154 -0
- package/platforms/android/proguard-rules.pro +1 -0
- package/src/animate.ts +18 -5
- package/src/decoration.ts +180 -0
- package/src/driver/android-color.ts +22 -20
- package/src/driver/android-decoration.ts +182 -0
- package/src/driver/android-playback.ts +157 -0
- package/src/driver/android.ts +43 -43
- package/src/driver/index.ts +13 -0
- package/src/driver/ios-color.ts +19 -10
- package/src/driver/ios-decoration.ts +332 -0
- package/src/driver/ios-playback.ts +37 -0
- package/src/driver/ios-shadow.ts +50 -0
- package/src/driver/ios.ts +85 -46
- package/src/engine.ts +220 -20
- package/src/haptics.ts +25 -2
- package/src/index.ts +1 -0
- package/src/playback-group.ts +110 -0
- package/src/playback.ts +62 -0
- package/src/properties.ts +18 -2
- package/src/sequence.ts +5 -0
- package/src/track.ts +9 -1
- package/src/transition.ts +4 -1
- package/src/types.ts +15 -0
package/src/engine.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createClock,
|
|
3
|
+
validateProgress,
|
|
4
|
+
type PlaybackAnimation,
|
|
5
|
+
type PlaybackClock,
|
|
6
|
+
type PlaybackCommand,
|
|
7
|
+
} from './playback.js'
|
|
1
8
|
import type { View } from '@nativescript/core'
|
|
2
9
|
import { getDriver } from './driver/index.js'
|
|
3
10
|
import { playHaptics, type HapticOptions } from './haptics.js'
|
|
@@ -31,8 +38,14 @@ interface Entry {
|
|
|
31
38
|
readonly owner: ViewAnimation
|
|
32
39
|
handle: unknown
|
|
33
40
|
done: boolean
|
|
41
|
+
finished: boolean
|
|
42
|
+
generation: number
|
|
43
|
+
token: symbol
|
|
44
|
+
completedValue?: number
|
|
34
45
|
}
|
|
35
46
|
|
|
47
|
+
const revisions = new WeakMap<View, Map<Property, symbol>>()
|
|
48
|
+
|
|
36
49
|
const running = new WeakMap<View, Map<Property, Entry>>()
|
|
37
50
|
const arrivals = new WeakMap<Animation, Promise<unknown>>()
|
|
38
51
|
|
|
@@ -48,7 +61,7 @@ export function setArrival(animation: Animation, arrival: Promise<unknown>) {
|
|
|
48
61
|
arrivals.set(animation, arrival)
|
|
49
62
|
}
|
|
50
63
|
|
|
51
|
-
export class ViewAnimation implements
|
|
64
|
+
export class ViewAnimation implements PlaybackAnimation {
|
|
52
65
|
readonly finished: Promise<boolean>
|
|
53
66
|
private resolve!: (finished: boolean) => void
|
|
54
67
|
private readonly entries: Entry[] = []
|
|
@@ -59,12 +72,180 @@ export class ViewAnimation implements Animation {
|
|
|
59
72
|
private arrive!: () => void
|
|
60
73
|
private cancelArrival: (() => void) | undefined
|
|
61
74
|
private haptics: ReturnType<typeof playHaptics>
|
|
75
|
+
private clock: PlaybackClock | undefined
|
|
76
|
+
private _paused = false
|
|
77
|
+
private controlled = false
|
|
78
|
+
private pendingProgress: number | undefined
|
|
79
|
+
private total = 0
|
|
80
|
+
private started = false
|
|
81
|
+
private arrival = 0
|
|
82
|
+
private arrived = false
|
|
83
|
+
private idleListener: (() => void) | undefined
|
|
84
|
+
private readyListener: (() => void) | undefined
|
|
85
|
+
private readonly endless: boolean
|
|
86
|
+
|
|
87
|
+
get paused() {
|
|
88
|
+
return this._paused
|
|
89
|
+
}
|
|
90
|
+
get duration() {
|
|
91
|
+
return this.started ? this.total : this.endless ? Infinity : 0
|
|
92
|
+
}
|
|
93
|
+
get currentTime() {
|
|
94
|
+
return this.time()
|
|
95
|
+
}
|
|
96
|
+
get ready() {
|
|
97
|
+
return this.started || this.settled
|
|
98
|
+
}
|
|
99
|
+
get idle() {
|
|
100
|
+
return this.settled || (this.started && this.pending === 0)
|
|
101
|
+
}
|
|
102
|
+
get progress() {
|
|
103
|
+
if (!Number.isFinite(this.duration))
|
|
104
|
+
throw new RangeError('Progress requires a finite animation.')
|
|
105
|
+
if (this.settled && this.complete) return 1
|
|
106
|
+
return (
|
|
107
|
+
this.pendingProgress ??
|
|
108
|
+
(this.duration ? Math.min(1, this.time() / this.duration) : this.settled ? 1 : 0)
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
set progress(value: number) {
|
|
112
|
+
validateProgress(value, this.duration)
|
|
113
|
+
if (this.settled) return
|
|
114
|
+
if (!this.started) {
|
|
115
|
+
this.pendingProgress = value
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
this.seekTime(this.duration * value)
|
|
119
|
+
}
|
|
120
|
+
onIdle(callback: () => void) {
|
|
121
|
+
this.idleListener = callback
|
|
122
|
+
}
|
|
123
|
+
onReady(callback: () => void) {
|
|
124
|
+
this.readyListener = callback
|
|
125
|
+
}
|
|
126
|
+
releaseGroup() {
|
|
127
|
+
this.held = false
|
|
128
|
+
if (this.idle && !this._paused) this.settle()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
pause() {
|
|
132
|
+
if (this.settled || this._paused) return
|
|
133
|
+
this.controlled = true
|
|
134
|
+
this._paused = true
|
|
135
|
+
this.clock?.pause()
|
|
136
|
+
this.cancelArrival?.()
|
|
137
|
+
this.haptics?.pause()
|
|
138
|
+
if (this.started) this.controlTracks(this.time(), 'pause')
|
|
139
|
+
}
|
|
140
|
+
play() {
|
|
141
|
+
if (this.settled || !this._paused) return
|
|
142
|
+
this.controlled = true
|
|
143
|
+
this._paused = false
|
|
144
|
+
this.clock?.play()
|
|
145
|
+
if (this.started) {
|
|
146
|
+
this.controlTracks(this.time(), 'play')
|
|
147
|
+
this.scheduleArrival()
|
|
148
|
+
this.haptics?.play()
|
|
149
|
+
if (this.pending === 0) this.becameIdle()
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
seekTime(time: number) {
|
|
153
|
+
if (this.settled) return
|
|
154
|
+
this.controlled = true
|
|
155
|
+
const position = Math.max(0, Math.min(this.total, time))
|
|
156
|
+
this.clock?.seek(position)
|
|
157
|
+
if (!this._paused) this.clock?.play()
|
|
158
|
+
for (const entry of this.entries) {
|
|
159
|
+
if (
|
|
160
|
+
entry.done &&
|
|
161
|
+
entry.finished &&
|
|
162
|
+
revisions.get(this.view)?.get(entry.track.property) === entry.token &&
|
|
163
|
+
readValue(this.view, entry.track.property) === entry.completedValue
|
|
164
|
+
) {
|
|
165
|
+
entry.done = false
|
|
166
|
+
entry.finished = false
|
|
167
|
+
this.pending++
|
|
168
|
+
entriesOf(this.view).set(entry.track.property, entry)
|
|
169
|
+
this.playEntry(entry, position)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
this.controlTracks(position)
|
|
173
|
+
this.scheduleArrival()
|
|
174
|
+
this.haptics?.seek()
|
|
175
|
+
if (this.pending === 0) this.becameIdle()
|
|
176
|
+
}
|
|
177
|
+
elapsed(entry: Entry, now: number) {
|
|
178
|
+
return this.controlled
|
|
179
|
+
? this.time()
|
|
180
|
+
: (getDriver().elapsed?.(entry.handle) ?? now - entry.startedAt)
|
|
181
|
+
}
|
|
182
|
+
private time() {
|
|
183
|
+
return this.clock?.time() ?? 0
|
|
184
|
+
}
|
|
185
|
+
private controlTracks(position: number, command: PlaybackCommand = 'seek') {
|
|
186
|
+
const driver = getDriver()
|
|
187
|
+
for (const entry of this.entries)
|
|
188
|
+
if (!entry.done)
|
|
189
|
+
driver.control?.(this.view, entry.handle, position, this._paused, command)
|
|
190
|
+
}
|
|
191
|
+
private scheduleArrival() {
|
|
192
|
+
this.cancelArrival?.()
|
|
193
|
+
this.cancelArrival = undefined
|
|
194
|
+
if (
|
|
195
|
+
this.pending === 0 ||
|
|
196
|
+
this.arrived ||
|
|
197
|
+
this._paused ||
|
|
198
|
+
!Number.isFinite(this.arrival) ||
|
|
199
|
+
this.total <= this.arrival
|
|
200
|
+
)
|
|
201
|
+
return
|
|
202
|
+
const remaining = this.arrival - this.time()
|
|
203
|
+
if (remaining <= 0) {
|
|
204
|
+
this.arrived = true
|
|
205
|
+
this.arrive()
|
|
206
|
+
} else
|
|
207
|
+
this.cancelArrival = getDriver().after(remaining, () => {
|
|
208
|
+
this.arrived = true
|
|
209
|
+
this.arrive()
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
private becameIdle() {
|
|
213
|
+
this.clock?.pause()
|
|
214
|
+
this.idleListener?.()
|
|
215
|
+
if (!this.complete || (!this.held && !this._paused)) this.settle()
|
|
216
|
+
}
|
|
217
|
+
private playEntry(entry: Entry, elapsed = 0) {
|
|
218
|
+
const initial = entry.generation === 0
|
|
219
|
+
const instance = ++entry.generation
|
|
220
|
+
const driver = getDriver()
|
|
221
|
+
entry.handle = driver.play(
|
|
222
|
+
this.view,
|
|
223
|
+
entry.track,
|
|
224
|
+
initial ? entry.startedAt : driver.now() - elapsed,
|
|
225
|
+
(finished = true) => {
|
|
226
|
+
if (entry.done || instance !== entry.generation) return
|
|
227
|
+
entry.done = true
|
|
228
|
+
entry.finished = finished
|
|
229
|
+
entry.completedValue = readValue(this.view, entry.track.property)
|
|
230
|
+
forget(entry)
|
|
231
|
+
this.ended(finished)
|
|
232
|
+
},
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
private unloaded = () => this.stop()
|
|
62
236
|
|
|
63
237
|
constructor(
|
|
64
238
|
readonly view: View,
|
|
65
239
|
plans: readonly Plan[],
|
|
66
240
|
private readonly haptic?: HapticOptions,
|
|
241
|
+
private held = false,
|
|
67
242
|
) {
|
|
243
|
+
this.endless = plans.some(
|
|
244
|
+
(plan) =>
|
|
245
|
+
!Number.isFinite(plan.transition.repeat) ||
|
|
246
|
+
(plan.transition.curve.type === 'tween' &&
|
|
247
|
+
!Number.isFinite(plan.transition.curve.duration)),
|
|
248
|
+
)
|
|
68
249
|
this.finished = new Promise((resolve) => (this.resolve = resolve))
|
|
69
250
|
setArrival(this, new Promise<void>((resolve) => (this.arrive = resolve)))
|
|
70
251
|
if (view.isLoaded && view.nativeViewProtected) this.start(plans)
|
|
@@ -95,10 +276,11 @@ export class ViewAnimation implements Animation {
|
|
|
95
276
|
this.complete = false
|
|
96
277
|
this.haptics?.cancel()
|
|
97
278
|
}
|
|
98
|
-
if (--this.pending === 0) this.
|
|
279
|
+
if (--this.pending === 0) this.becameIdle()
|
|
99
280
|
}
|
|
100
281
|
|
|
101
282
|
private start(plans: readonly Plan[]) {
|
|
283
|
+
this.started = true
|
|
102
284
|
const driver = getDriver()
|
|
103
285
|
const now = driver.now()
|
|
104
286
|
const entries = entriesOf(this.view)
|
|
@@ -110,7 +292,7 @@ export class ViewAnimation implements Animation {
|
|
|
110
292
|
if (previous) {
|
|
111
293
|
const elapsed = elapsedOf(previous, now)
|
|
112
294
|
current = driver.presented(this.view, property) ?? previous.track.valueAt(elapsed)
|
|
113
|
-
velocity = previous.track.velocityAt(elapsed)
|
|
295
|
+
velocity = previous.owner.paused ? 0 : previous.track.velocityAt(elapsed)
|
|
114
296
|
release(previous, now, false)
|
|
115
297
|
} else {
|
|
116
298
|
current =
|
|
@@ -133,8 +315,13 @@ export class ViewAnimation implements Animation {
|
|
|
133
315
|
})
|
|
134
316
|
|
|
135
317
|
for (const track of tracks) writeValue(this.view, track.property, track.to)
|
|
318
|
+
driver.sync?.(this.view)
|
|
136
319
|
|
|
137
320
|
for (const track of tracks) {
|
|
321
|
+
let tokens = revisions.get(this.view)
|
|
322
|
+
if (!tokens) revisions.set(this.view, (tokens = new Map()))
|
|
323
|
+
const token = Symbol(track.property)
|
|
324
|
+
tokens.set(track.property, token)
|
|
138
325
|
if (track.end === 0) continue
|
|
139
326
|
const entry: Entry = {
|
|
140
327
|
track,
|
|
@@ -142,28 +329,35 @@ export class ViewAnimation implements Animation {
|
|
|
142
329
|
owner: this,
|
|
143
330
|
handle: undefined,
|
|
144
331
|
done: false,
|
|
332
|
+
finished: false,
|
|
333
|
+
generation: 0,
|
|
334
|
+
token,
|
|
145
335
|
}
|
|
146
336
|
entries.set(track.property, entry)
|
|
147
337
|
this.entries.push(entry)
|
|
148
338
|
this.pending++
|
|
149
339
|
}
|
|
150
340
|
|
|
151
|
-
this.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
if (this.
|
|
164
|
-
|
|
165
|
-
this.
|
|
166
|
-
|
|
341
|
+
this.total = Math.max(0, ...tracks.map((track) => track.end))
|
|
342
|
+
this.arrival = Math.max(0, ...tracks.map((track) => track.arrival))
|
|
343
|
+
this.clock =
|
|
344
|
+
(this.pending ? driver.clock?.(this.total) : undefined) ??
|
|
345
|
+
createClock(() => driver.now())
|
|
346
|
+
if (this._paused) this.clock.pause()
|
|
347
|
+
this.view.on('unloaded', this.unloaded)
|
|
348
|
+
this.haptics = playHaptics(this.view, tracks, this.haptic, driver, now, () =>
|
|
349
|
+
this.time(),
|
|
350
|
+
)
|
|
351
|
+
if (this._paused) this.haptics?.pause()
|
|
352
|
+
for (const entry of this.entries) this.playEntry(entry)
|
|
353
|
+
if (this.pendingProgress !== undefined) {
|
|
354
|
+
const progress = this.pendingProgress
|
|
355
|
+
this.pendingProgress = undefined
|
|
356
|
+
this.seekTime(progress * this.total)
|
|
357
|
+
} else if (this._paused) this.controlTracks(0, 'pause')
|
|
358
|
+
this.scheduleArrival()
|
|
359
|
+
this.readyListener?.()
|
|
360
|
+
if (this.pending === 0) this.becameIdle()
|
|
167
361
|
}
|
|
168
362
|
|
|
169
363
|
private waitForLoad(plans: readonly Plan[]) {
|
|
@@ -187,6 +381,8 @@ export class ViewAnimation implements Animation {
|
|
|
187
381
|
if (this.settled) return
|
|
188
382
|
this.settled = true
|
|
189
383
|
this.cancelArrival?.()
|
|
384
|
+
this.clock?.cancel()
|
|
385
|
+
this.view.off('unloaded', this.unloaded)
|
|
190
386
|
if (this.complete) this.haptics?.finish()
|
|
191
387
|
else this.haptics?.cancel()
|
|
192
388
|
this.haptics = undefined
|
|
@@ -197,6 +393,9 @@ export class ViewAnimation implements Animation {
|
|
|
197
393
|
|
|
198
394
|
/** Stops whatever is animating these properties of the view, or all of them. */
|
|
199
395
|
export function stopView(view: View, properties?: readonly Property[]) {
|
|
396
|
+
const tokens = revisions.get(view)
|
|
397
|
+
for (const property of properties ?? tokens?.keys() ?? [])
|
|
398
|
+
tokens?.set(property, Symbol(property))
|
|
200
399
|
const entries = running.get(view)
|
|
201
400
|
if (!entries) return
|
|
202
401
|
const now = getDriver().now()
|
|
@@ -226,11 +425,12 @@ function release(entry: Entry, now: number, commit: boolean) {
|
|
|
226
425
|
const value =
|
|
227
426
|
driver.presented(view, property) ?? entry.track.valueAt(elapsedOf(entry, now))
|
|
228
427
|
writeValue(view, property, value)
|
|
428
|
+
driver.sync?.(view, property)
|
|
229
429
|
}
|
|
230
430
|
driver.stop(view, entry.handle)
|
|
231
431
|
entry.owner.ended(false)
|
|
232
432
|
}
|
|
233
433
|
|
|
234
434
|
function elapsedOf(entry: Entry, now: number) {
|
|
235
|
-
return
|
|
435
|
+
return entry.owner.elapsed(entry, now)
|
|
236
436
|
}
|
package/src/haptics.ts
CHANGED
|
@@ -48,6 +48,7 @@ export function playHaptics(
|
|
|
48
48
|
options: HapticOptions | undefined,
|
|
49
49
|
driver: Driver,
|
|
50
50
|
startedAt: number,
|
|
51
|
+
elapsedTime = () => driver.now() - startedAt,
|
|
51
52
|
) {
|
|
52
53
|
if (!isIOS || !options || typeof UIImpactFeedbackGenerator === 'undefined') return
|
|
53
54
|
const moving = tracks.filter(
|
|
@@ -69,6 +70,7 @@ export function playHaptics(
|
|
|
69
70
|
let generator: UIImpactFeedbackGenerator | undefined =
|
|
70
71
|
UIImpactFeedbackGenerator.alloc().initWithStyle(styles[options.style])
|
|
71
72
|
const native = view.nativeViewProtected as UIView
|
|
73
|
+
let paused = false
|
|
72
74
|
let next = 0
|
|
73
75
|
let cancelPrepare: (() => void) | undefined
|
|
74
76
|
let cancelImpact: (() => void) | undefined
|
|
@@ -79,6 +81,11 @@ export function playHaptics(
|
|
|
79
81
|
!!native.window &&
|
|
80
82
|
UIApplication.sharedApplication.applicationState === 0 // UIApplicationState.Active
|
|
81
83
|
|
|
84
|
+
const cancelTimers = () => {
|
|
85
|
+
cancelPrepare?.()
|
|
86
|
+
cancelImpact?.()
|
|
87
|
+
cancelPrepare = cancelImpact = undefined
|
|
88
|
+
}
|
|
82
89
|
const cancel = () => {
|
|
83
90
|
cancelPrepare?.()
|
|
84
91
|
cancelImpact?.()
|
|
@@ -88,7 +95,7 @@ export function playHaptics(
|
|
|
88
95
|
}
|
|
89
96
|
|
|
90
97
|
const fire = (tolerance = 0) => {
|
|
91
|
-
const elapsed =
|
|
98
|
+
const elapsed = elapsedTime()
|
|
92
99
|
// A busy UI thread can delay timers. Drop stale impacts instead of replaying
|
|
93
100
|
// several old taps at once when it becomes responsive again.
|
|
94
101
|
while (next < offsets.length && offsets[next]! <= elapsed + tolerance) {
|
|
@@ -98,8 +105,9 @@ export function playHaptics(
|
|
|
98
105
|
}
|
|
99
106
|
|
|
100
107
|
const schedule = () => {
|
|
108
|
+
if (paused) return
|
|
101
109
|
if (!generator || next === offsets.length) return cancel()
|
|
102
|
-
const remaining =
|
|
110
|
+
const remaining = offsets[next]! - elapsedTime()
|
|
103
111
|
if (active()) generator.prepare()
|
|
104
112
|
if (remaining > PREPARE_AHEAD) {
|
|
105
113
|
cancelPrepare = driver.after(remaining - PREPARE_AHEAD, () => {
|
|
@@ -116,6 +124,21 @@ export function playHaptics(
|
|
|
116
124
|
schedule()
|
|
117
125
|
return {
|
|
118
126
|
cancel,
|
|
127
|
+
pause() {
|
|
128
|
+
paused = true
|
|
129
|
+
cancelTimers()
|
|
130
|
+
},
|
|
131
|
+
play() {
|
|
132
|
+
if (paused) {
|
|
133
|
+
paused = false
|
|
134
|
+
schedule()
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
seek() {
|
|
138
|
+
cancelTimers()
|
|
139
|
+
while (next < offsets.length && offsets[next]! <= elapsedTime()) next++
|
|
140
|
+
schedule()
|
|
141
|
+
},
|
|
119
142
|
finish() {
|
|
120
143
|
// Native completion may beat the timer at the final keyframe by a frame.
|
|
121
144
|
fire(COMPLETION_TOLERANCE)
|
package/src/index.ts
CHANGED
|
@@ -27,6 +27,7 @@ export { springs } from './transition.js'
|
|
|
27
27
|
export type { Spring, SpringName, Timing, Transition, Tween } from './transition.js'
|
|
28
28
|
export { cubicBezier } from './easing.js'
|
|
29
29
|
export type { CubicBezier, Easing, EasingFunction, EasingName } from './easing.js'
|
|
30
|
+
export type { PlaybackAnimation } from './playback.js'
|
|
30
31
|
export type { Animation } from './engine.js'
|
|
31
32
|
export type { ColorValue, ColorKeyframes } from './color.js'
|
|
32
33
|
export type * from './types.js'
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { arrivalOf, setArrival, type ViewAnimation } from './engine.js'
|
|
2
|
+
import { getDriver } from './driver/index.js'
|
|
3
|
+
import { createClock, type PlaybackClock } from './playback.js'
|
|
4
|
+
import { validateProgress, type PlaybackAnimation } from './playback.js'
|
|
5
|
+
|
|
6
|
+
/** Staggered views share a finite time range. Shorter children remain seekable
|
|
7
|
+
* until the whole group settles, unless another animation has taken ownership. */
|
|
8
|
+
export function playbackGroup(children: readonly ViewAnimation[]): PlaybackAnimation {
|
|
9
|
+
let paused = false
|
|
10
|
+
let settled = false
|
|
11
|
+
let finalProgress: number | undefined
|
|
12
|
+
let clock: PlaybackClock | undefined
|
|
13
|
+
let pendingProgress: number | undefined
|
|
14
|
+
const duration = () => Math.max(0, ...children.map((child) => child.duration))
|
|
15
|
+
const ensureClock = () => {
|
|
16
|
+
if (clock || settled || children.some((child) => !child.ready)) return
|
|
17
|
+
const driver = getDriver()
|
|
18
|
+
clock = driver.clock?.(duration()) ?? createClock(() => driver.now())
|
|
19
|
+
clock.seek(Math.max(0, ...children.map((child) => child.currentTime)))
|
|
20
|
+
if (paused) clock.pause()
|
|
21
|
+
}
|
|
22
|
+
const finishIfIdle = () => {
|
|
23
|
+
if (settled || paused || children.some((child) => !child.idle || child.paused)) return
|
|
24
|
+
settled = true
|
|
25
|
+
clock?.cancel()
|
|
26
|
+
for (const child of children) child.releaseGroup()
|
|
27
|
+
}
|
|
28
|
+
const seek = (value: number) => {
|
|
29
|
+
const total = duration()
|
|
30
|
+
validateProgress(value, total)
|
|
31
|
+
if (settled) return
|
|
32
|
+
if (children.some((child) => !child.ready)) {
|
|
33
|
+
pendingProgress = value
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
pendingProgress = undefined
|
|
37
|
+
ensureClock()
|
|
38
|
+
clock?.seek(total * value)
|
|
39
|
+
for (const child of children) child.seekTime(total * value)
|
|
40
|
+
finishIfIdle()
|
|
41
|
+
}
|
|
42
|
+
for (const child of children) {
|
|
43
|
+
child.onIdle(finishIfIdle)
|
|
44
|
+
child.onReady(() => {
|
|
45
|
+
ensureClock()
|
|
46
|
+
if (pendingProgress !== undefined && children.every((child) => child.ready))
|
|
47
|
+
seek(pendingProgress)
|
|
48
|
+
finishIfIdle()
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
const finished = Promise.all(children.map((child) => child.finished)).then(
|
|
52
|
+
(results) => {
|
|
53
|
+
finalProgress ??= results.every(Boolean)
|
|
54
|
+
? 1
|
|
55
|
+
: duration()
|
|
56
|
+
? Math.min(1, (clock?.time() ?? 0) / duration())
|
|
57
|
+
: 0
|
|
58
|
+
settled = true
|
|
59
|
+
clock?.cancel()
|
|
60
|
+
return results.every(Boolean)
|
|
61
|
+
},
|
|
62
|
+
)
|
|
63
|
+
const animation: PlaybackAnimation = {
|
|
64
|
+
finished,
|
|
65
|
+
get duration() {
|
|
66
|
+
return duration()
|
|
67
|
+
},
|
|
68
|
+
get paused() {
|
|
69
|
+
return paused
|
|
70
|
+
},
|
|
71
|
+
get progress() {
|
|
72
|
+
if (!Number.isFinite(duration()))
|
|
73
|
+
throw new RangeError('Progress requires a finite animation.')
|
|
74
|
+
if (pendingProgress !== undefined) return pendingProgress
|
|
75
|
+
return (
|
|
76
|
+
finalProgress ??
|
|
77
|
+
(settled ? 1 : duration() ? Math.min(1, (clock?.time() ?? 0) / duration()) : 0)
|
|
78
|
+
)
|
|
79
|
+
},
|
|
80
|
+
set progress(value) {
|
|
81
|
+
seek(value)
|
|
82
|
+
},
|
|
83
|
+
pause() {
|
|
84
|
+
if (!settled && !paused) {
|
|
85
|
+
paused = true
|
|
86
|
+
clock?.pause()
|
|
87
|
+
children.forEach((child) => child.pause())
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
play() {
|
|
91
|
+
if (!settled && paused) {
|
|
92
|
+
paused = false
|
|
93
|
+
clock?.play()
|
|
94
|
+
children.forEach((child) => child.play())
|
|
95
|
+
finishIfIdle()
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
stop() {
|
|
99
|
+
finalProgress = duration() ? Math.min(1, (clock?.time() ?? 0) / duration()) : 0
|
|
100
|
+
settled = true
|
|
101
|
+
clock?.cancel()
|
|
102
|
+
children.forEach((child) => child.stop())
|
|
103
|
+
},
|
|
104
|
+
then: (onfulfilled, onrejected) => finished.then(onfulfilled, onrejected),
|
|
105
|
+
}
|
|
106
|
+
setArrival(animation, Promise.all(children.map(arrivalOf)))
|
|
107
|
+
ensureClock()
|
|
108
|
+
finishIfIdle()
|
|
109
|
+
return animation
|
|
110
|
+
}
|
package/src/playback.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Animation } from './engine.js'
|
|
2
|
+
|
|
3
|
+
/** Controls returned by animate() and decay(). Effects expose the base Animation. */
|
|
4
|
+
export interface PlaybackAnimation extends Animation {
|
|
5
|
+
/** Pauses only the properties owned by this animation. */
|
|
6
|
+
pause(): void
|
|
7
|
+
/** Continues from the current position. Completed/stopped animations remain terminal. */
|
|
8
|
+
play(): void
|
|
9
|
+
readonly paused: boolean
|
|
10
|
+
/** Total milliseconds, including delays and repeats. Infinity for endless loops. */
|
|
11
|
+
readonly duration: number
|
|
12
|
+
/** Finite timeline position, from 0 to 1. Seeking preserves the playing/paused state. */
|
|
13
|
+
progress: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type PlaybackCommand = 'pause' | 'play' | 'seek'
|
|
17
|
+
|
|
18
|
+
export interface PlaybackClock {
|
|
19
|
+
time(): number
|
|
20
|
+
pause(): void
|
|
21
|
+
play(): void
|
|
22
|
+
seek(ms: number): void
|
|
23
|
+
cancel(): void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Read only on user commands; there is no frame loop in JavaScript. */
|
|
27
|
+
export function createClock(now: () => number, initial = 0): PlaybackClock {
|
|
28
|
+
let offset = initial
|
|
29
|
+
let anchor = now()
|
|
30
|
+
let paused = false
|
|
31
|
+
const time = () => offset + (paused ? 0 : now() - anchor)
|
|
32
|
+
return {
|
|
33
|
+
time,
|
|
34
|
+
pause() {
|
|
35
|
+
offset = time()
|
|
36
|
+
paused = true
|
|
37
|
+
},
|
|
38
|
+
play() {
|
|
39
|
+
if (paused) {
|
|
40
|
+
anchor = now()
|
|
41
|
+
paused = false
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
seek(ms) {
|
|
45
|
+
offset = ms
|
|
46
|
+
anchor = now()
|
|
47
|
+
},
|
|
48
|
+
cancel() {
|
|
49
|
+
offset = time()
|
|
50
|
+
paused = true
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function validateProgress(value: number, duration: number) {
|
|
56
|
+
if (!Number.isFinite(value) || value < 0 || value > 1)
|
|
57
|
+
throw new RangeError('Animation progress must be between 0 and 1.')
|
|
58
|
+
if (!Number.isFinite(duration))
|
|
59
|
+
throw new RangeError(
|
|
60
|
+
'Progress requires a finite animation; endless loops can pause and play.',
|
|
61
|
+
)
|
|
62
|
+
}
|
package/src/properties.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { Color, type View } from '@nativescript/core'
|
|
2
|
+
import {
|
|
3
|
+
isDecorationProperty,
|
|
4
|
+
readDecoration,
|
|
5
|
+
writeDecoration,
|
|
6
|
+
type DecorationProperty,
|
|
7
|
+
} from './decoration.js'
|
|
2
8
|
import { isColorProperty, readColor, type ColorProperty } from './color.js'
|
|
3
9
|
|
|
4
10
|
export type NumericProperty =
|
|
5
11
|
'opacity' | 'x' | 'y' | 'scaleX' | 'scaleY' | 'rotate' | 'rotateX' | 'rotateY'
|
|
6
|
-
export type Property = NumericProperty | ColorProperty
|
|
12
|
+
export type Property = NumericProperty | ColorProperty | DecorationProperty
|
|
7
13
|
|
|
8
14
|
const viewKeys = {
|
|
9
15
|
opacity: 'opacity',
|
|
@@ -29,6 +35,14 @@ const defaults: Record<NumericProperty, number> = {
|
|
|
29
35
|
|
|
30
36
|
/** How close to its target a spring or decay has to be to count as settled. */
|
|
31
37
|
export const precision: Record<Property, number> = {
|
|
38
|
+
borderRadius: 0.1,
|
|
39
|
+
borderWidth: 0.1,
|
|
40
|
+
shadowOpacity: 0.002,
|
|
41
|
+
shadowRadius: 0.1,
|
|
42
|
+
shadowOffsetX: 0.1,
|
|
43
|
+
shadowOffsetY: 0.1,
|
|
44
|
+
elevation: 0.1,
|
|
45
|
+
translationZ: 0.1,
|
|
32
46
|
backgroundColor: 0.002,
|
|
33
47
|
borderColor: 0.002,
|
|
34
48
|
color: 0.002,
|
|
@@ -43,11 +57,12 @@ export const precision: Record<Property, number> = {
|
|
|
43
57
|
}
|
|
44
58
|
|
|
45
59
|
export function isProperty(key: string): key is Property {
|
|
46
|
-
return key
|
|
60
|
+
return Object.hasOwn(viewKeys, key) || isColorProperty(key) || isDecorationProperty(key)
|
|
47
61
|
}
|
|
48
62
|
|
|
49
63
|
export function readValue(view: View, property: Property): number {
|
|
50
64
|
if (isColorProperty(property)) return readColor(view, property)
|
|
65
|
+
if (isDecorationProperty(property)) return readDecoration(view, property)
|
|
51
66
|
const value = view[viewKeys[property]]
|
|
52
67
|
return typeof value === 'number' && !Number.isNaN(value) ? value : defaults[property]
|
|
53
68
|
}
|
|
@@ -57,5 +72,6 @@ export function writeValue(view: View, property: Property, value: number) {
|
|
|
57
72
|
view[property] = new Color(value)
|
|
58
73
|
return
|
|
59
74
|
}
|
|
75
|
+
if (isDecorationProperty(property)) return writeDecoration(view, property, value)
|
|
60
76
|
view[viewKeys[property]] = value
|
|
61
77
|
}
|
package/src/sequence.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getDriver } from './driver/index.js'
|
|
2
|
+
import { isDecorationProperty, validateDecorationTarget } from './decoration.js'
|
|
1
3
|
import type { View } from '@nativescript/core'
|
|
2
4
|
import { animate, startingValues, toViews } from './animate.js'
|
|
3
5
|
import { arrivalOf, setArrival, stopView, type Animation } from './engine.js'
|
|
@@ -69,6 +71,8 @@ function applyStartingValues(steps: readonly Step[]) {
|
|
|
69
71
|
const starts = startingValues(animation, options)
|
|
70
72
|
|
|
71
73
|
for (const view of toViews(target)) {
|
|
74
|
+
for (const property of starts.keys())
|
|
75
|
+
if (isDecorationProperty(property)) validateDecorationTarget(view, property)
|
|
72
76
|
let properties = claimed.get(view)
|
|
73
77
|
if (!properties) claimed.set(view, (properties = new Set()))
|
|
74
78
|
|
|
@@ -82,6 +86,7 @@ function applyStartingValues(steps: readonly Step[]) {
|
|
|
82
86
|
for (const [property, value] of fresh) {
|
|
83
87
|
if (value !== null) writeValue(view, property, value)
|
|
84
88
|
}
|
|
89
|
+
getDriver().sync?.(view)
|
|
85
90
|
for (const property of starts.keys()) properties.add(property)
|
|
86
91
|
}
|
|
87
92
|
}
|
package/src/track.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { clampDecoration, isDecorationProperty } from './decoration.js'
|
|
1
2
|
import { linear } from './easing.js'
|
|
2
3
|
import { solveDecay } from './decay.js'
|
|
3
4
|
import { precision, type Property } from './properties.js'
|
|
@@ -72,7 +73,7 @@ export function createTrack(
|
|
|
72
73
|
return reverse && iteration % 2 === 1 ? [duration - local, -1] : [local, 1]
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
|
|
76
|
+
const track: Track = {
|
|
76
77
|
property,
|
|
77
78
|
delay,
|
|
78
79
|
duration,
|
|
@@ -94,6 +95,13 @@ export function createTrack(
|
|
|
94
95
|
return motion.velocity(time) * direction
|
|
95
96
|
},
|
|
96
97
|
}
|
|
98
|
+
if (!isDecorationProperty(property)) return track
|
|
99
|
+
return {
|
|
100
|
+
...track,
|
|
101
|
+
values: track.values.map((value) => clampDecoration(property, value)),
|
|
102
|
+
to: clampDecoration(property, track.to),
|
|
103
|
+
valueAt: (elapsed) => clampDecoration(property, track.valueAt(elapsed)),
|
|
104
|
+
}
|
|
97
105
|
}
|
|
98
106
|
|
|
99
107
|
function colorTrack(
|