@rafaelaugusto/jolt 0.1.0-beta.11 → 0.1.0-beta.13

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 (151) hide show
  1. package/README.md +42 -8
  2. package/dist/animate.d.ts +15 -3
  3. package/dist/animate.d.ts.map +1 -1
  4. package/dist/animate.js +52 -34
  5. package/dist/animate.js.map +1 -1
  6. package/dist/drag.d.ts +17 -3
  7. package/dist/drag.d.ts.map +1 -1
  8. package/dist/drag.js +34 -11
  9. package/dist/drag.js.map +1 -1
  10. package/dist/driver/android.d.ts.map +1 -1
  11. package/dist/driver/android.js +2 -0
  12. package/dist/driver/android.js.map +1 -1
  13. package/dist/driver/frame.d.ts +3 -0
  14. package/dist/driver/frame.d.ts.map +1 -0
  15. package/dist/driver/frame.js +7 -0
  16. package/dist/driver/frame.js.map +1 -0
  17. package/dist/driver/index.d.ts +2 -0
  18. package/dist/driver/index.d.ts.map +1 -1
  19. package/dist/driver/index.js.map +1 -1
  20. package/dist/driver/ios.d.ts.map +1 -1
  21. package/dist/driver/ios.js +2 -0
  22. package/dist/driver/ios.js.map +1 -1
  23. package/dist/easing.d.ts +1 -1
  24. package/dist/easing.d.ts.map +1 -1
  25. package/dist/easing.js +11 -0
  26. package/dist/easing.js.map +1 -1
  27. package/dist/engine.d.ts +20 -2
  28. package/dist/engine.d.ts.map +1 -1
  29. package/dist/engine.js +166 -38
  30. package/dist/engine.js.map +1 -1
  31. package/dist/gradient.d.ts.map +1 -1
  32. package/dist/gradient.js +10 -4
  33. package/dist/gradient.js.map +1 -1
  34. package/dist/image-effect.d.ts +1 -0
  35. package/dist/image-effect.d.ts.map +1 -1
  36. package/dist/image-effect.js +10 -2
  37. package/dist/image-effect.js.map +1 -1
  38. package/dist/index.d.ts +8 -2
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +3 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/keyboard.d.ts.map +1 -1
  43. package/dist/keyboard.js +63 -10
  44. package/dist/keyboard.js.map +1 -1
  45. package/dist/mixer.d.ts +49 -0
  46. package/dist/mixer.d.ts.map +1 -0
  47. package/dist/mixer.js +238 -0
  48. package/dist/mixer.js.map +1 -0
  49. package/dist/motion-scope.d.ts +6 -2
  50. package/dist/motion-scope.d.ts.map +1 -1
  51. package/dist/motion-scope.js +12 -4
  52. package/dist/motion-scope.js.map +1 -1
  53. package/dist/motion-states.d.ts +10 -5
  54. package/dist/motion-states.d.ts.map +1 -1
  55. package/dist/motion-states.js +55 -22
  56. package/dist/motion-states.js.map +1 -1
  57. package/dist/motion-value.d.ts +152 -0
  58. package/dist/motion-value.d.ts.map +1 -0
  59. package/dist/motion-value.js +635 -0
  60. package/dist/motion-value.js.map +1 -0
  61. package/dist/native/index.d.ts +6 -0
  62. package/dist/native/index.d.ts.map +1 -0
  63. package/dist/native/index.js +3 -0
  64. package/dist/native/index.js.map +1 -0
  65. package/dist/native-properties.d.ts +28 -0
  66. package/dist/native-properties.d.ts.map +1 -0
  67. package/dist/native-properties.js +154 -0
  68. package/dist/native-properties.js.map +1 -0
  69. package/dist/native-property-driver.d.ts +19 -0
  70. package/dist/native-property-driver.d.ts.map +1 -0
  71. package/dist/native-property-driver.js +69 -0
  72. package/dist/native-property-driver.js.map +1 -0
  73. package/dist/native-property.d.ts +35 -0
  74. package/dist/native-property.d.ts.map +1 -0
  75. package/dist/native-property.js +277 -0
  76. package/dist/native-property.js.map +1 -0
  77. package/dist/path.d.ts.map +1 -1
  78. package/dist/path.js +2 -2
  79. package/dist/path.js.map +1 -1
  80. package/dist/playback-group.d.ts.map +1 -1
  81. package/dist/playback-group.js +17 -1
  82. package/dist/playback-group.js.map +1 -1
  83. package/dist/playback.d.ts +15 -0
  84. package/dist/playback.d.ts.map +1 -1
  85. package/dist/playback.js +23 -0
  86. package/dist/playback.js.map +1 -1
  87. package/dist/sequence.d.ts +24 -0
  88. package/dist/sequence.d.ts.map +1 -1
  89. package/dist/sequence.js +181 -19
  90. package/dist/sequence.js.map +1 -1
  91. package/dist/timed-animation.d.ts +38 -0
  92. package/dist/timed-animation.d.ts.map +1 -0
  93. package/dist/timed-animation.js +177 -0
  94. package/dist/timed-animation.js.map +1 -0
  95. package/dist/track.d.ts.map +1 -1
  96. package/dist/track.js +31 -12
  97. package/dist/track.js.map +1 -1
  98. package/dist/transition.d.ts +15 -4
  99. package/dist/transition.d.ts.map +1 -1
  100. package/dist/transition.js +46 -7
  101. package/dist/transition.js.map +1 -1
  102. package/dist/types.d.ts +5 -0
  103. package/dist/types.d.ts.map +1 -1
  104. package/dist/value-observers.d.ts +19 -0
  105. package/dist/value-observers.d.ts.map +1 -0
  106. package/dist/value-observers.js +111 -0
  107. package/dist/value-observers.js.map +1 -0
  108. package/dist/value-track.d.ts +13 -0
  109. package/dist/value-track.d.ts.map +1 -0
  110. package/dist/value-track.js +59 -0
  111. package/dist/value-track.js.map +1 -0
  112. package/dist/vue/index.d.ts +28 -2
  113. package/dist/vue/index.d.ts.map +1 -1
  114. package/dist/vue/index.js +78 -2
  115. package/dist/vue/index.js.map +1 -1
  116. package/package.json +6 -2
  117. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFrame.java +53 -0
  118. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFrameFollower.java +57 -0
  119. package/src/android-plugin.d.ts +64 -0
  120. package/src/animate.ts +106 -17
  121. package/src/drag.ts +67 -21
  122. package/src/driver/android.ts +2 -0
  123. package/src/driver/frame.ts +10 -0
  124. package/src/driver/index.ts +2 -0
  125. package/src/driver/ios.ts +2 -0
  126. package/src/easing.ts +14 -0
  127. package/src/engine.ts +181 -55
  128. package/src/gradient.ts +10 -4
  129. package/src/image-effect.ts +10 -2
  130. package/src/index.ts +23 -1
  131. package/src/ios-plugin.d.ts +33 -0
  132. package/src/keyboard.ts +83 -18
  133. package/src/mixer.ts +264 -0
  134. package/src/motion-scope.ts +17 -5
  135. package/src/motion-states.ts +79 -29
  136. package/src/motion-value.ts +795 -0
  137. package/src/native/index.ts +16 -0
  138. package/src/native-properties.ts +188 -0
  139. package/src/native-property-driver.ts +123 -0
  140. package/src/native-property.ts +353 -0
  141. package/src/path.ts +2 -1
  142. package/src/playback-group.ts +17 -1
  143. package/src/playback.ts +41 -0
  144. package/src/sequence.ts +209 -20
  145. package/src/timed-animation.ts +192 -0
  146. package/src/track.ts +37 -12
  147. package/src/transition.ts +69 -10
  148. package/src/types.ts +5 -0
  149. package/src/value-observers.ts +133 -0
  150. package/src/value-track.ts +71 -0
  151. package/src/vue/index.ts +98 -1
@@ -6,7 +6,7 @@ import {
6
6
  type ViewAnimation,
7
7
  } from './engine.js'
8
8
  import { getDriver } from './driver/index.js'
9
- import { createClock, type PlaybackClock } from './playback.js'
9
+ import { createClock, registerPlayback, type PlaybackClock } from './playback.js'
10
10
  import { validateProgress, type PlaybackAnimation } from './playback.js'
11
11
 
12
12
  /** Staggered views share a finite time range. Shorter children remain seekable
@@ -17,6 +17,7 @@ export function playbackGroup(children: readonly ViewAnimation[]): PlaybackAnima
17
17
  let finalProgress: number | undefined
18
18
  let clock: PlaybackClock | undefined
19
19
  let pendingProgress: number | undefined
20
+ let changed = () => {}
20
21
  const duration = () => Math.max(0, ...children.map((child) => child.duration))
21
22
  const ensureClock = () => {
22
23
  if (clock || settled || children.some((child) => !child.ready)) return
@@ -44,6 +45,7 @@ export function playbackGroup(children: readonly ViewAnimation[]): PlaybackAnima
44
45
  clock?.seek(total * value)
45
46
  for (const child of children) child.seekTime(total * value)
46
47
  finishIfIdle()
48
+ changed()
47
49
  }
48
50
  for (const child of children) {
49
51
  child.onIdle(finishIfIdle)
@@ -52,6 +54,7 @@ export function playbackGroup(children: readonly ViewAnimation[]): PlaybackAnima
52
54
  if (pendingProgress !== undefined && children.every((child) => child.ready))
53
55
  seek(pendingProgress)
54
56
  finishIfIdle()
57
+ changed()
55
58
  })
56
59
  }
57
60
  const finished = Promise.all(children.map((child) => child.finished)).then(
@@ -63,6 +66,7 @@ export function playbackGroup(children: readonly ViewAnimation[]): PlaybackAnima
63
66
  : 0
64
67
  settled = true
65
68
  clock?.cancel()
69
+ changed()
66
70
  return results.every(Boolean)
67
71
  },
68
72
  )
@@ -91,6 +95,7 @@ export function playbackGroup(children: readonly ViewAnimation[]): PlaybackAnima
91
95
  paused = true
92
96
  clock?.pause()
93
97
  children.forEach((child) => child.pause())
98
+ changed()
94
99
  }
95
100
  },
96
101
  play() {
@@ -99,6 +104,7 @@ export function playbackGroup(children: readonly ViewAnimation[]): PlaybackAnima
99
104
  clock?.play()
100
105
  children.forEach((child) => child.play())
101
106
  finishIfIdle()
107
+ changed()
102
108
  }
103
109
  },
104
110
  stop() {
@@ -106,9 +112,19 @@ export function playbackGroup(children: readonly ViewAnimation[]): PlaybackAnima
106
112
  settled = true
107
113
  clock?.cancel()
108
114
  children.forEach((child) => child.stop())
115
+ changed()
109
116
  },
110
117
  then: (onfulfilled, onrejected) => finished.then(onfulfilled, onrejected),
111
118
  }
119
+ changed = registerPlayback(animation, () => ({
120
+ position: pendingProgress,
121
+ time: finalProgress === undefined ? (clock?.time() ?? 0) : finalProgress * duration(),
122
+ duration: duration(),
123
+ paused,
124
+ ready: children.every((child) => child.ready),
125
+ settled,
126
+ completed: finalProgress === 1,
127
+ }))
112
128
  setArrival(animation, Promise.all(children.map(arrivalOf)))
113
129
  const listeners = new Set<() => void>()
114
130
  const timings = children.map((child) => arrivalTimingOf(child)!)
package/src/playback.ts CHANGED
@@ -23,6 +23,47 @@ export interface PlaybackClock {
23
23
  cancel(): void
24
24
  }
25
25
 
26
+ /** Boundary notifications, never frame callbacks. Consumers sample this same clock. */
27
+ export interface PlaybackState {
28
+ readonly position?: number
29
+ readonly time: number
30
+ readonly duration: number
31
+ readonly paused: boolean
32
+ readonly ready: boolean
33
+ readonly settled: boolean
34
+ readonly completed: boolean
35
+ }
36
+
37
+ const sources = new WeakMap<
38
+ Animation,
39
+ {
40
+ read(): PlaybackState
41
+ listeners: Set<() => void>
42
+ }
43
+ >()
44
+
45
+ export function registerPlayback(animation: Animation, read: () => PlaybackState) {
46
+ const source = { read, listeners: new Set<() => void>() }
47
+ sources.set(animation, source)
48
+ return () => {
49
+ for (const listener of [...source.listeners]) listener()
50
+ }
51
+ }
52
+
53
+ export function playbackSource(animation: Animation) {
54
+ const source = sources.get(animation)
55
+ if (!source) throw new TypeError('This animation does not expose a playback clock.')
56
+ return {
57
+ read: source.read,
58
+ subscribe(listener: () => void) {
59
+ source.listeners.add(listener)
60
+ return () => {
61
+ source.listeners.delete(listener)
62
+ }
63
+ },
64
+ }
65
+ }
66
+
26
67
  /** Read only on user commands; there is no frame loop in JavaScript. */
27
68
  export function createClock(now: () => number, initial = 0): PlaybackClock {
28
69
  let offset = initial
package/src/sequence.ts CHANGED
@@ -10,7 +10,12 @@ import {
10
10
  stopView,
11
11
  type Animation,
12
12
  } from './engine.js'
13
- import { createClock } from './playback.js'
13
+ import { createClock, registerPlayback, playbackSource } from './playback.js'
14
+ import {
15
+ isMotionValue,
16
+ type MotionValue,
17
+ type ValueAnimationOptions,
18
+ } from './motion-value.js'
14
19
  import type { PresetName } from './presets.js'
15
20
  import { writeValue, type Property } from './properties.js'
16
21
  import type { SpringName } from './transition.js'
@@ -28,6 +33,34 @@ export type Step =
28
33
  ]
29
34
  | (() => Animation)
30
35
 
36
+ /** A participant that can genuinely freeze and continue, rather than restart. */
37
+ export interface PlayableAnimation extends Animation {
38
+ pause(): void
39
+ play(): void
40
+ readonly paused: boolean
41
+ readonly duration?: number
42
+ }
43
+
44
+ export type TimelineStep =
45
+ | Exclude<Step, () => Animation>
46
+ | readonly [
47
+ MotionValue,
48
+ number | readonly (number | null)[],
49
+ ((ValueAnimationOptions & { at?: SequencePosition }) | SpringName)?,
50
+ ]
51
+ | { readonly at?: SequencePosition; readonly run: () => PlayableAnimation }
52
+ | (() => PlayableAnimation)
53
+
54
+ export interface TimelineAnimation extends PlayableAnimation {
55
+ /** Unknown until every step has activated and every participant reports a duration. */
56
+ readonly duration: number | undefined
57
+ }
58
+
59
+ /** Overlapping steps with a shared pause/resume control, including pending launches. */
60
+ export function timeline(steps: readonly TimelineStep[]): TimelineAnimation {
61
+ return compose(steps, true) as TimelineAnimation
62
+ }
63
+
31
64
  /**
32
65
  * By default a step starts once the previous one looks done. `at` can overlap
33
66
  * that arrival, share the previous start, or select an absolute timeline time.
@@ -44,26 +77,54 @@ export type Step =
44
77
  * ```
45
78
  */
46
79
  export function sequence(steps: readonly Step[]): Animation {
80
+ return compose(steps, false)
81
+ }
82
+
83
+ function compose(
84
+ steps: readonly TimelineStep[] | readonly Step[],
85
+ controlled: boolean,
86
+ ): Animation {
47
87
  const prepared = steps.map((step) => {
48
88
  if (typeof step === 'function') return { step, position: undefined }
49
- const [target, animation, options] = step
89
+ if (!Array.isArray(step)) {
90
+ const descriptor = step as { at?: SequencePosition; run: () => PlayableAnimation }
91
+ if (typeof descriptor.run !== 'function')
92
+ throw new TypeError('A timeline step needs a run() factory.')
93
+ return { step: descriptor.run, position: parsePosition(descriptor.at) }
94
+ }
95
+ const [target, animation, options] = step as Exclude<
96
+ TimelineStep,
97
+ Function | { run: () => PlayableAnimation }
98
+ >
50
99
  if (typeof options !== 'object' || options === null)
51
100
  return { step, position: undefined }
52
101
  const { at, ...transition } = options
53
102
  const position = parsePosition(at)
54
103
  return { step: [target, animation, transition] as Step, position }
55
104
  })
56
- applyStartingValues(prepared.map(({ step }) => step))
105
+ applyStartingValues(
106
+ prepared
107
+ .map(({ step }) => step as Step)
108
+ .filter((step) => typeof step === 'function' || !isMotionValue(step[0])),
109
+ )
57
110
 
58
111
  const driver = getDriver()
59
112
  const slowdown = 1 / timeScale()
60
113
  // 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
114
+ const clock =
115
+ controlled || prepared.some(({ position }) => typeof position === 'number')
116
+ ? (driver.clock?.(Infinity) ?? createClock(() => driver.now()))
117
+ : undefined
64
118
  const started: Animation[] = []
65
119
  const cancellations = new Set<() => void>()
66
120
  let stopped = false
121
+ let paused = false
122
+ let settled = false
123
+ let completed = false
124
+ let changed = () => {}
125
+ const resumeChildren = new Set<PlayableAnimation>()
126
+ const waits = new Set<{ at: number; fire(): void; cancel(): void; schedule(): void }>()
127
+ const time = () => clock?.time() ?? 0
67
128
  let resolveScheduled!: () => void
68
129
  let rejectScheduled!: (reason: unknown) => void
69
130
  const scheduled = new Promise<void>((resolve, reject) => {
@@ -77,28 +138,55 @@ export function sequence(steps: readonly Step[]): Animation {
77
138
  position,
78
139
  onStart: new Set<() => void>(),
79
140
  animation: undefined as Animation | undefined,
141
+ start: undefined as number | undefined,
80
142
  }
81
143
  })
82
144
  const cancelSchedule = () => {
83
145
  for (const cancel of cancellations) cancel()
84
146
  cancellations.clear()
85
- clock?.cancel()
147
+ for (const wait of waits) wait.cancel()
148
+ waits.clear()
149
+ if (!controlled || stopped || settled) clock?.cancel()
86
150
  }
87
151
  const fail = (error: unknown) => {
88
152
  if (stopped) return
89
153
  stopped = true
154
+ settled = true
90
155
  cancelSchedule()
91
156
  states.forEach((state) => state.onStart.clear())
92
157
  started.forEach((child) => child.stop())
158
+ changed()
93
159
  rejectScheduled(error)
94
160
  }
95
161
  const launch = (index: number) => {
96
162
  const state = states[index]!
97
163
  if (stopped || state.animation) return
164
+ if (paused) {
165
+ wait(0, () => launch(index))
166
+ return
167
+ }
98
168
  try {
99
169
  const step = state.step
100
- const child = typeof step === 'function' ? step() : animate(...step)
170
+ const child =
171
+ typeof step === 'function'
172
+ ? step()
173
+ : (animate as (...args: any[]) => Animation)(...(step as any[]))
174
+ if (controlled && !isPlayable(child)) {
175
+ child?.stop?.()
176
+ throw new TypeError(
177
+ 'A timeline participant must expose pause(), play() and paused; use sequence() for non-playable effects.',
178
+ )
179
+ }
101
180
  state.animation = child
181
+ state.start = time()
182
+ if (controlled) {
183
+ try {
184
+ const playback = playbackSource(child).read()
185
+ if (playback.ready) state.start = Math.max(0, time() - playback.time)
186
+ } catch {
187
+ /* A custom playable participant can omit a clock. */
188
+ }
189
+ }
102
190
  started.push(child)
103
191
  // Observe setup failures immediately, even while other steps are still
104
192
  // scheduled. Nested sequences can reject before this one reaches arrival.
@@ -110,25 +198,51 @@ export function sequence(steps: readonly Step[]): Animation {
110
198
  cancelSchedule()
111
199
  resolveScheduled()
112
200
  }
201
+ changed()
113
202
  } catch (error) {
114
203
  fail(error)
115
204
  }
116
205
  }
117
206
  const wait = (ms: number, callback: () => void) => {
118
207
  if (stopped) return
119
- if (ms <= 0) {
120
- callback()
208
+ if (!controlled) {
209
+ if (ms <= 0) {
210
+ callback()
211
+ return
212
+ }
213
+ let cancel = () => {}
214
+ let fired = false
215
+ const off = driver.after(ms, () => {
216
+ fired = true
217
+ cancellations.delete(cancel)
218
+ if (!stopped) callback()
219
+ })
220
+ cancel = off
221
+ if (!fired) cancellations.add(cancel)
121
222
  return
122
223
  }
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)
224
+ let off = () => {}
225
+ const waiting = {
226
+ at: time() + Math.max(0, ms),
227
+ fire() {
228
+ off()
229
+ waits.delete(waiting)
230
+ if (!stopped) callback()
231
+ },
232
+ cancel() {
233
+ off()
234
+ off = () => {}
235
+ },
236
+ schedule() {
237
+ waiting.cancel()
238
+ if (paused || stopped) return
239
+ const remaining = waiting.at - time()
240
+ if (remaining <= 0) waiting.fire()
241
+ else off = driver.after(remaining, waiting.fire)
242
+ },
243
+ }
244
+ waits.add(waiting)
245
+ waiting.schedule()
132
246
  }
133
247
 
134
248
  for (let index = 0; index < states.length; index++) {
@@ -214,7 +328,11 @@ export function sequence(steps: readonly Step[]): Animation {
214
328
  const arrived = scheduled.then(() => Promise.all(started.map(arrivalOf)))
215
329
  const finished = arrived.then(async () => {
216
330
  const results = await Promise.all(started.map((animation) => animation.finished))
217
- return !stopped && results.length === steps.length && results.every(Boolean)
331
+ completed = !stopped && results.length === steps.length && results.every(Boolean)
332
+ settled = true
333
+ clock?.cancel()
334
+ changed()
335
+ return completed
218
336
  })
219
337
 
220
338
  const animation: Animation = {
@@ -222,13 +340,69 @@ export function sequence(steps: readonly Step[]): Animation {
222
340
  stop() {
223
341
  if (stopped) return
224
342
  stopped = true
343
+ settled = true
225
344
  cancelSchedule()
226
345
  states.forEach((state) => state.onStart.clear())
227
346
  resolveScheduled()
228
347
  started.forEach((animation) => animation.stop())
348
+ resumeChildren.clear()
349
+ changed()
229
350
  },
230
351
  then: (onfulfilled, onrejected) => finished.then(onfulfilled, onrejected),
231
352
  }
353
+ if (controlled) {
354
+ const duration = () => {
355
+ if (pending) return undefined
356
+ let total = 0
357
+ for (const state of states) {
358
+ const child = state.animation as PlayableAnimation | undefined
359
+ if (!child || child.duration === undefined) return undefined
360
+ try {
361
+ if (!playbackSource(child).read().ready) return undefined
362
+ } catch {
363
+ /* custom playable */
364
+ }
365
+ total = Math.max(total, (state.start ?? 0) + child.duration)
366
+ }
367
+ return total
368
+ }
369
+ Object.defineProperties(animation, {
370
+ paused: { get: () => paused },
371
+ duration: { get: duration },
372
+ })
373
+ Object.assign(animation, {
374
+ pause() {
375
+ if (paused || settled) return
376
+ paused = true
377
+ clock?.pause()
378
+ for (const wait of waits) wait.cancel()
379
+ for (const child of started as PlayableAnimation[]) {
380
+ if (!child.paused) {
381
+ resumeChildren.add(child)
382
+ child.pause()
383
+ }
384
+ }
385
+ changed()
386
+ },
387
+ play() {
388
+ if (!paused || settled) return
389
+ paused = false
390
+ clock?.play()
391
+ for (const child of resumeChildren) child.play()
392
+ resumeChildren.clear()
393
+ for (const wait of [...waits]) wait.schedule()
394
+ changed()
395
+ },
396
+ })
397
+ changed = registerPlayback(animation, () => ({
398
+ time: time(),
399
+ duration: duration() ?? Infinity,
400
+ paused,
401
+ ready: duration() !== undefined,
402
+ settled,
403
+ completed,
404
+ }))
405
+ }
232
406
  setArrival(animation, arrived)
233
407
  return animation
234
408
  }
@@ -276,3 +450,18 @@ function applyStartingValues(steps: readonly Step[]) {
276
450
  }
277
451
  }
278
452
  }
453
+
454
+ function isPlayable(animation: Animation): animation is PlayableAnimation {
455
+ const handle = animation as PlayableAnimation | undefined
456
+ return (
457
+ !!handle &&
458
+ typeof handle.pause === 'function' &&
459
+ typeof handle.play === 'function' &&
460
+ typeof handle.paused === 'boolean' &&
461
+ typeof handle.stop === 'function' &&
462
+ typeof handle.finished?.then === 'function' &&
463
+ typeof handle.finished?.catch === 'function' &&
464
+ (handle.duration === undefined ||
465
+ (typeof handle.duration === 'number' && handle.duration >= 0))
466
+ )
467
+ }
@@ -0,0 +1,192 @@
1
+ import { getDriver } from './driver/index.js'
2
+ import { Trace } from '@nativescript/core'
3
+ import { setArrival, setArrivalTiming } from './engine.js'
4
+ import {
5
+ createClock,
6
+ registerPlayback,
7
+ validateProgress,
8
+ type PlaybackAnimation,
9
+ type PlaybackCommand,
10
+ } from './playback.js'
11
+
12
+ /** A sampled resource's control clock. Native sinks play frames; JS runs only at boundaries. */
13
+ export class TimedAnimation implements PlaybackAnimation {
14
+ readonly finished: Promise<boolean>
15
+ private resolve!: (value: boolean) => void
16
+ private arrive!: () => void
17
+ private readonly clock
18
+ private readonly notifyPlayback: () => void
19
+ private readonly listeners = new Set<() => void>()
20
+ private cancelEnd?: () => void
21
+ private cancelArrival?: () => void
22
+ private _paused = false
23
+ private settled = false
24
+ private completed = false
25
+ private arrived = false
26
+ private active = false
27
+
28
+ constructor(
29
+ readonly duration: number,
30
+ private readonly arrival: number,
31
+ private readonly update: (
32
+ time: number,
33
+ paused: boolean,
34
+ command: PlaybackCommand,
35
+ ) => void,
36
+ private readonly finish: (completed: boolean) => void,
37
+ ) {
38
+ const driver = getDriver()
39
+ this.clock = driver.clock?.(duration) ?? createClock(() => driver.now())
40
+ this.finished = new Promise((resolve) => {
41
+ this.resolve = resolve
42
+ })
43
+ setArrival(
44
+ this,
45
+ new Promise<void>((resolve) => {
46
+ this.arrive = resolve
47
+ }),
48
+ )
49
+ this.notifyPlayback = registerPlayback(this, () => ({
50
+ time: this.time,
51
+ duration,
52
+ paused: this._paused,
53
+ ready: this.active,
54
+ settled: this.settled,
55
+ completed: this.completed,
56
+ }))
57
+ setArrivalTiming(this, {
58
+ read: () => ({
59
+ remaining: Math.max(0, arrival - this.time),
60
+ paused: this.paused,
61
+ arrived: this.arrived,
62
+ }),
63
+ subscribe: (callback) => {
64
+ this.listeners.add(callback)
65
+ return () => {
66
+ this.listeners.delete(callback)
67
+ }
68
+ },
69
+ })
70
+ }
71
+
72
+ get time() {
73
+ return Math.max(0, Math.min(this.duration, this.clock.time()))
74
+ }
75
+ get paused() {
76
+ return this._paused
77
+ }
78
+ get done() {
79
+ return this.settled
80
+ }
81
+ get progress() {
82
+ if (!Number.isFinite(this.duration))
83
+ throw new RangeError('Progress requires a finite animation.')
84
+ return this.completed ? 1 : this.duration ? this.time / this.duration : 0
85
+ }
86
+ set progress(value: number) {
87
+ validateProgress(value, this.duration)
88
+ this.seekTime(value * this.duration)
89
+ }
90
+
91
+ start() {
92
+ if (this.active || this.settled) return
93
+ this.active = true
94
+ this.schedule()
95
+ this.notify()
96
+ }
97
+
98
+ pause() {
99
+ if (this._paused || this.settled) return
100
+ this.clock.pause()
101
+ this._paused = true
102
+ this.cancelTimers()
103
+ this.update(this.time, true, 'pause')
104
+ this.notify()
105
+ }
106
+
107
+ play() {
108
+ if (!this._paused || this.settled) return
109
+ this._paused = false
110
+ this.clock.play()
111
+ this.update(this.time, false, 'play')
112
+ this.schedule()
113
+ this.notify()
114
+ }
115
+
116
+ seekTime(time: number) {
117
+ if (!Number.isFinite(time) || time < 0)
118
+ throw new RangeError('Seek time must be finite and nonnegative.')
119
+ if (this.settled) return
120
+ this.clock.seek(Math.min(this.duration, time))
121
+ this.update(this.time, this._paused, 'seek')
122
+ this.schedule()
123
+ this.notify()
124
+ }
125
+
126
+ stop() {
127
+ this.settle(false)
128
+ }
129
+
130
+ then<A = boolean, B = never>(
131
+ onfulfilled?: ((value: boolean) => A | PromiseLike<A>) | null,
132
+ onrejected?: ((reason: unknown) => B | PromiseLike<B>) | null,
133
+ ): Promise<A | B> {
134
+ return this.finished.then(onfulfilled, onrejected)
135
+ }
136
+
137
+ private notify() {
138
+ this.notifyPlayback()
139
+ for (const callback of [...this.listeners]) callback()
140
+ }
141
+ private cancelTimers() {
142
+ this.cancelEnd?.()
143
+ this.cancelArrival?.()
144
+ this.cancelEnd = this.cancelArrival = undefined
145
+ }
146
+ private schedule() {
147
+ this.cancelTimers()
148
+ if (!this.active || this._paused || this.settled) return
149
+ const driver = getDriver()
150
+ if (Number.isFinite(this.duration)) {
151
+ const remaining = this.duration - this.time
152
+ if (remaining <= 0) {
153
+ this.settle(true)
154
+ return
155
+ }
156
+ this.cancelEnd = driver.after(remaining, () => this.settle(true))
157
+ }
158
+ if (!this.arrived && Number.isFinite(this.arrival)) {
159
+ const remaining = this.arrival - this.time
160
+ const arrive = () => {
161
+ this.arrived = true
162
+ this.arrive()
163
+ this.notify()
164
+ }
165
+ if (remaining <= 0) arrive()
166
+ else this.cancelArrival = driver.after(remaining, arrive)
167
+ }
168
+ }
169
+ private settle(completed: boolean) {
170
+ if (this.settled) return
171
+ this.settled = true
172
+ this.completed = completed
173
+ this.cancelTimers()
174
+ if (completed) this.clock.seek(this.duration)
175
+ this.clock.cancel()
176
+ try {
177
+ this.finish(completed)
178
+ } catch (error) {
179
+ this.completed = false
180
+ Trace.write(
181
+ `Jolt sampled playback failed: ${String(error)}`,
182
+ Trace.categories.Error,
183
+ Trace.messageType.error,
184
+ )
185
+ }
186
+ this.arrived = true
187
+ this.arrive()
188
+ this.notify()
189
+ this.resolve(this.completed)
190
+ this.listeners.clear()
191
+ }
192
+ }