@rafaelaugusto/jolt 0.1.0-beta.12 → 0.1.0-beta.14

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 +9 -6
  2. package/dist/animate.d.ts +6 -4
  3. package/dist/animate.d.ts.map +1 -1
  4. package/dist/animate.js +3 -1
  5. package/dist/animate.js.map +1 -1
  6. package/dist/drag.js +3 -3
  7. package/dist/drag.js.map +1 -1
  8. package/dist/driver/android-gradient.d.ts.map +1 -1
  9. package/dist/driver/android-gradient.js +11 -1
  10. package/dist/driver/android-gradient.js.map +1 -1
  11. package/dist/driver/android-particles.d.ts +6 -0
  12. package/dist/driver/android-particles.d.ts.map +1 -0
  13. package/dist/driver/android-particles.js +93 -0
  14. package/dist/driver/android-particles.js.map +1 -0
  15. package/dist/driver/android.d.ts.map +1 -1
  16. package/dist/driver/android.js +2 -0
  17. package/dist/driver/android.js.map +1 -1
  18. package/dist/driver/frame.d.ts +3 -0
  19. package/dist/driver/frame.d.ts.map +1 -0
  20. package/dist/driver/frame.js +7 -0
  21. package/dist/driver/frame.js.map +1 -0
  22. package/dist/driver/index.d.ts +2 -0
  23. package/dist/driver/index.d.ts.map +1 -1
  24. package/dist/driver/index.js.map +1 -1
  25. package/dist/driver/ios-gradient.d.ts.map +1 -1
  26. package/dist/driver/ios-gradient.js +24 -2
  27. package/dist/driver/ios-gradient.js.map +1 -1
  28. package/dist/driver/ios.d.ts.map +1 -1
  29. package/dist/driver/ios.js +2 -0
  30. package/dist/driver/ios.js.map +1 -1
  31. package/dist/engine.d.ts +2 -1
  32. package/dist/engine.d.ts.map +1 -1
  33. package/dist/engine.js +32 -5
  34. package/dist/engine.js.map +1 -1
  35. package/dist/gradient-binding.d.ts +38 -0
  36. package/dist/gradient-binding.d.ts.map +1 -0
  37. package/dist/gradient-binding.js +128 -0
  38. package/dist/gradient-binding.js.map +1 -0
  39. package/dist/gradient.d.ts +19 -4
  40. package/dist/gradient.d.ts.map +1 -1
  41. package/dist/gradient.js +331 -70
  42. package/dist/gradient.js.map +1 -1
  43. package/dist/haptics.d.ts.map +1 -1
  44. package/dist/haptics.js +101 -28
  45. package/dist/haptics.js.map +1 -1
  46. package/dist/image-effect.d.ts +1 -0
  47. package/dist/image-effect.d.ts.map +1 -1
  48. package/dist/image-effect.js +10 -2
  49. package/dist/image-effect.js.map +1 -1
  50. package/dist/index.d.ts +6 -2
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +2 -0
  53. package/dist/index.js.map +1 -1
  54. package/dist/motion-scope.d.ts +3 -2
  55. package/dist/motion-scope.d.ts.map +1 -1
  56. package/dist/motion-scope.js +12 -2
  57. package/dist/motion-scope.js.map +1 -1
  58. package/dist/motion-value.d.ts +62 -8
  59. package/dist/motion-value.d.ts.map +1 -1
  60. package/dist/motion-value.js +327 -91
  61. package/dist/motion-value.js.map +1 -1
  62. package/dist/native/index.d.ts +7 -0
  63. package/dist/native/index.d.ts.map +1 -0
  64. package/dist/native/index.js +4 -0
  65. package/dist/native/index.js.map +1 -0
  66. package/dist/native-properties.d.ts +28 -0
  67. package/dist/native-properties.d.ts.map +1 -0
  68. package/dist/native-properties.js +154 -0
  69. package/dist/native-properties.js.map +1 -0
  70. package/dist/native-property-driver.d.ts +19 -0
  71. package/dist/native-property-driver.d.ts.map +1 -0
  72. package/dist/native-property-driver.js +69 -0
  73. package/dist/native-property-driver.js.map +1 -0
  74. package/dist/native-property.d.ts +35 -0
  75. package/dist/native-property.d.ts.map +1 -0
  76. package/dist/native-property.js +277 -0
  77. package/dist/native-property.js.map +1 -0
  78. package/dist/particles.d.ts +5 -2
  79. package/dist/particles.d.ts.map +1 -1
  80. package/dist/particles.js +16 -10
  81. package/dist/particles.js.map +1 -1
  82. package/dist/playback-group.d.ts.map +1 -1
  83. package/dist/playback-group.js +17 -1
  84. package/dist/playback-group.js.map +1 -1
  85. package/dist/playback.d.ts +15 -0
  86. package/dist/playback.d.ts.map +1 -1
  87. package/dist/playback.js +23 -0
  88. package/dist/playback.js.map +1 -1
  89. package/dist/scene-appearance.d.ts +14 -0
  90. package/dist/scene-appearance.d.ts.map +1 -0
  91. package/dist/scene-appearance.js +68 -0
  92. package/dist/scene-appearance.js.map +1 -0
  93. package/dist/sequence.d.ts +24 -0
  94. package/dist/sequence.d.ts.map +1 -1
  95. package/dist/sequence.js +181 -19
  96. package/dist/sequence.js.map +1 -1
  97. package/dist/timed-animation.d.ts +38 -0
  98. package/dist/timed-animation.d.ts.map +1 -0
  99. package/dist/timed-animation.js +177 -0
  100. package/dist/timed-animation.js.map +1 -0
  101. package/dist/types.d.ts +1 -1
  102. package/dist/types.d.ts.map +1 -1
  103. package/dist/value-observers.d.ts +19 -0
  104. package/dist/value-observers.d.ts.map +1 -0
  105. package/dist/value-observers.js +111 -0
  106. package/dist/value-observers.js.map +1 -0
  107. package/dist/value-track.d.ts +13 -0
  108. package/dist/value-track.d.ts.map +1 -0
  109. package/dist/value-track.js +59 -0
  110. package/dist/value-track.js.map +1 -0
  111. package/dist/vue/index.d.ts.map +1 -1
  112. package/dist/vue/index.js +28 -5
  113. package/dist/vue/index.js.map +1 -1
  114. package/package.json +6 -2
  115. package/platforms/android/java/org/rafaelaugusto/jolt/GradientLayer.java +30 -1
  116. package/platforms/android/java/org/rafaelaugusto/jolt/ParticleEmitter.java +192 -0
  117. package/platforms/android/java/org/rafaelaugusto/jolt/ParticleSimulation.java +114 -0
  118. package/platforms/android/proguard-rules.pro +2 -0
  119. package/src/android-plugin.d.ts +93 -0
  120. package/src/animate.ts +13 -7
  121. package/src/drag.ts +3 -3
  122. package/src/driver/android-gradient.ts +8 -1
  123. package/src/driver/android-particles.ts +118 -0
  124. package/src/driver/android.ts +2 -0
  125. package/src/driver/frame.ts +10 -0
  126. package/src/driver/index.ts +2 -0
  127. package/src/driver/ios-gradient.ts +24 -2
  128. package/src/driver/ios.ts +2 -0
  129. package/src/engine.ts +33 -6
  130. package/src/gradient-binding.ts +158 -0
  131. package/src/gradient.ts +376 -91
  132. package/src/haptics.ts +106 -28
  133. package/src/image-effect.ts +10 -2
  134. package/src/index.ts +6 -0
  135. package/src/ios-plugin.d.ts +40 -0
  136. package/src/motion-scope.ts +14 -3
  137. package/src/motion-value.ts +374 -99
  138. package/src/native/index.ts +23 -0
  139. package/src/native-properties.ts +188 -0
  140. package/src/native-property-driver.ts +123 -0
  141. package/src/native-property.ts +353 -0
  142. package/src/particles.ts +18 -17
  143. package/src/playback-group.ts +17 -1
  144. package/src/playback.ts +41 -0
  145. package/src/scene-appearance.ts +92 -0
  146. package/src/sequence.ts +209 -20
  147. package/src/timed-animation.ts +192 -0
  148. package/src/types.ts +1 -1
  149. package/src/value-observers.ts +133 -0
  150. package/src/value-track.ts +71 -0
  151. package/src/vue/index.ts +29 -5
package/src/gradient.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { isAndroid, isIOS, type View } from '@nativescript/core'
1
+ import { Application, Trace, isAndroid, isIOS, type View } from '@nativescript/core'
2
2
  import { toView } from './animate.js'
3
3
  import { colorMixer, parseColor, type ColorValue } from './color.js'
4
4
  import { reducedMotion } from './config.js'
@@ -14,6 +14,19 @@ import {
14
14
  type Transition,
15
15
  } from './transition.js'
16
16
  import type { Target } from './types.js'
17
+ import { createClock, type PlaybackClock } from './playback.js'
18
+ import {
19
+ cloneFrame,
20
+ gradientPatch,
21
+ gradientProgram,
22
+ put,
23
+ take,
24
+ type GradientBindings,
25
+ type GradientComponent,
26
+ type GradientPatch,
27
+ type GradientSource,
28
+ } from './gradient-binding.js'
29
+ export type { GradientBindings } from './gradient-binding.js'
17
30
 
18
31
  export type GradientType = 'linear' | 'radial' | 'conic'
19
32
 
@@ -30,9 +43,9 @@ export interface GradientValues {
30
43
  radius?: number
31
44
  }
32
45
 
33
- export interface GradientOptions extends GradientValues {
46
+ export interface GradientOptions extends GradientBindings {
34
47
  type: GradientType
35
- colors: readonly ColorValue[]
48
+ colors: NonNullable<GradientBindings['colors']>
36
49
  }
37
50
 
38
51
  export interface Gradient {
@@ -41,8 +54,16 @@ export interface Gradient {
41
54
  animate(values: GradientValues, transition?: Transition | SpringName): Animation
42
55
  /** Applies values at once, stopping the animation in progress. */
43
56
  set(values: GradientValues): void
44
- /** Stops the animation in progress where it is. */
57
+ /** Holds the current frame and detaches all gradient bindings; sources keep running. */
45
58
  stop(): void
59
+ /** Attach mixed static/value components. Cleanup holds their current values; newer owners survive. */
60
+ bind(values: GradientBindings): () => void
61
+ /** Whether this gradient is explicitly suspended (independent of app/view lifecycle). */
62
+ readonly suspended: boolean
63
+ /** Hold and detach source subscriptions until resume; source playback is not paused. */
64
+ suspend(): void
65
+ /** Catch up to current source positions and reattach once. */
66
+ resume(): void
46
67
  /** Takes the gradient off the view. */
47
68
  remove(): void
48
69
  }
@@ -72,7 +93,7 @@ export interface GradientPainter {
72
93
  times: readonly number[],
73
94
  timing: GradientTiming,
74
95
  onEnd: (finished: boolean) => void,
75
- ): { cancel(): void }
96
+ ): { cancel(): void; control?(elapsed: number, paused: boolean): void }
76
97
  remove(): void
77
98
  }
78
99
 
@@ -103,56 +124,211 @@ const types: readonly GradientType[] = ['linear', 'radial', 'conic']
103
124
  */
104
125
  export function gradient(target: Target, options: GradientOptions): Gradient {
105
126
  const view = toView(target)
127
+ const { frame, patch } = prepareGradient(options)
128
+ return new GradientController(view, options.type, frame, patch)
129
+ }
130
+
131
+ /** Validate declarative replacements before removing their current native layer. */
132
+ export function prepareGradient(options: GradientOptions) {
106
133
  if (!options || !types.includes(options.type))
107
134
  throw new TypeError(`Gradient type must be 'linear', 'radial' or 'conic'.`)
108
- const first = resolve(
109
- { colors: [], stops: [], angle: 180, x: 0.5, y: 0.5, radius: 0.5 },
110
- options,
111
- false,
135
+ if (!options.colors)
136
+ throw new RangeError('A gradient needs between two and eight colors.')
137
+ const base: GradientFrame = {
138
+ colors: [],
139
+ stops: [],
140
+ angle: 180,
141
+ x: 0.5,
142
+ y: 0.5,
143
+ radius: 0.5,
144
+ }
145
+ const patch = gradientPatch(options, 0)
146
+ const prepared = gradientProgram(
147
+ { at: () => base, remaining: 0, elapsed: () => 0 },
148
+ patch,
112
149
  )
113
- return new GradientController(view, options.type, first)
150
+ return { frame: prepared.current, patch }
151
+ }
152
+
153
+ interface GradientRun {
154
+ from: GradientFrame
155
+ to: GradientFrame
156
+ track: Track
157
+ keys: Set<GradientComponent>
158
+ masked: Set<GradientComponent>
159
+ clock: PlaybackClock
160
+ cancelEnd?: () => void
161
+ settle(finished: boolean): void
114
162
  }
115
163
 
116
164
  class GradientController implements Gradient {
117
165
  private painter: GradientPainter | undefined
118
- private pending: GradientFrame | undefined
119
- private removed = false
166
+ private player: ReturnType<GradientPainter['play']> | undefined
167
+ private program: ReturnType<typeof gradientProgram> | undefined
120
168
  private model: GradientFrame
121
- private running:
122
- | {
123
- from: GradientFrame
124
- to: GradientFrame
125
- track: Track
126
- startedAt: number
127
- handle: { cancel(): void }
128
- settle: (finished: boolean) => void
129
- }
130
- | undefined
169
+ private removed = false
170
+ private _suspended = false
171
+ private background = false
172
+ private unloadedView = false
173
+ private running: GradientRun | undefined
174
+ private bindings = new Map<
175
+ GradientComponent,
176
+ { source: GradientSource; owner: object }
177
+ >()
178
+ private releases: (() => void)[] = []
179
+ private cancelWindow?: () => void
180
+ private generation = 0
131
181
 
132
182
  constructor(
133
183
  private readonly view: View,
134
184
  readonly type: GradientType,
135
185
  frame: GradientFrame,
186
+ patch: GradientPatch,
136
187
  ) {
137
188
  this.model = frame
138
- this.attach((factory ?? platformPainter)(view, type))
189
+ const owner = {}
190
+ for (const [key, source] of patch)
191
+ if (typeof source !== 'number') this.bindings.set(key, { source, owner })
192
+ this.painter = (factory ?? platformPainter)(view, type)
139
193
  view.on('unloaded', this.unloaded)
194
+ view.on('loaded', this.loaded)
195
+ view.on('layoutChanged', this.resized)
196
+ Application.on(Application.suspendEvent, this.appSuspended)
197
+ Application.on(Application.resumeEvent, this.appResumed)
198
+ this.subscribe()
199
+ this.render()
140
200
  }
141
201
 
142
- private unloaded = () => this.stop()
143
-
144
- private attach(painter: GradientPainter | undefined) {
202
+ private get inactive() {
203
+ return this.removed || this._suspended || this.background || this.unloadedView
204
+ }
205
+ private patch() {
206
+ return new Map([...this.bindings].map(([key, entry]) => [key, entry.source]))
207
+ }
208
+ private base() {
209
+ const run = this.running
210
+ const elapsed = run?.clock.time() ?? 0
211
+ const model = cloneFrame(this.model)
212
+ return {
213
+ remaining: run ? Math.max(0, run.track.end - elapsed) : 0,
214
+ elapsed: () => (run ? Math.max(0, run.clock.time() - elapsed) : 0),
215
+ at: (delta: number) => {
216
+ const frame = cloneFrame(model)
217
+ if (run) {
218
+ const sample = mix(
219
+ run.from,
220
+ run.to,
221
+ run.track.valueAt(elapsed + delta),
222
+ reducedMotion(),
223
+ )
224
+ for (const key of run.keys)
225
+ if (!run.masked.has(key)) put(frame, key, take(sample, key))
226
+ }
227
+ return frame
228
+ },
229
+ }
230
+ }
231
+ private presented(): GradientFrame {
232
+ if (this.inactive) return cloneFrame(this.model)
233
+ try {
234
+ return gradientProgram(this.base(), this.patch()).current
235
+ } catch {
236
+ return (
237
+ this.program?.at(Math.min(this.program.duration, this.program.elapsed())) ??
238
+ cloneFrame(this.model)
239
+ )
240
+ }
241
+ }
242
+ private clearPlayer() {
243
+ this.generation++
244
+ this.cancelWindow?.()
245
+ this.cancelWindow = undefined
246
+ const player = this.player
247
+ this.player = undefined
248
+ player?.cancel()
249
+ }
250
+ private unsubscribe() {
251
+ for (const release of this.releases) release()
252
+ this.releases = []
253
+ }
254
+ private subscribe() {
255
+ this.unsubscribe()
256
+ if (this.inactive) return
257
+ const sources = new Map<object, GradientSource>()
258
+ for (const { source } of this.bindings.values()) sources.set(source.identity, source)
259
+ this.releases = [...sources.values()].map((source) => source.subscribe(this.changed))
260
+ }
261
+ private changed = () => {
262
+ if (this.inactive) return
263
+ try {
264
+ this.render()
265
+ } catch (error) {
266
+ // A later invalid source must not throw through its other consumers or leave
267
+ // half of a compound frame painted. Hold the last compiled frame and detach.
268
+ this.stop()
269
+ Trace.write(
270
+ `Jolt gradient binding stopped: ${String(error)}`,
271
+ Trace.categories.Error,
272
+ Trace.messageType.error,
273
+ )
274
+ }
275
+ }
276
+ private render() {
277
+ if (this.inactive) return
278
+ const prepared = gradientProgram(this.base(), this.patch())
279
+ this.clearPlayer()
280
+ this.program = prepared
281
+ const painter = this.painter
145
282
  if (!painter) return
146
- if (this.removed) return painter.remove()
147
- this.painter = painter
148
- painter.paint(this.pending ?? this.model)
149
- this.pending = undefined
283
+ const run = this.running
284
+ if (run && !this.bindings.size && !run.masked.size) {
285
+ const generation = this.generation
286
+ this.player = painter.play(
287
+ run.track.values.map((p) => mix(run.from, run.to, p, reducedMotion())),
288
+ run.track.times,
289
+ {
290
+ duration: run.track.duration,
291
+ delay: run.track.delay,
292
+ repeat: run.track.repeat,
293
+ reverse: run.track.reverse,
294
+ },
295
+ (completed) => {
296
+ if (generation === this.generation && this.running === run)
297
+ this.finish(run, completed)
298
+ },
299
+ )
300
+ // Concrete playback retains its existing API; bindings always use native control.
301
+ if (run.clock.time() > 1) this.player.control?.(run.clock.time(), false)
302
+ return
303
+ }
304
+ if (prepared.duration <= 0) {
305
+ painter.paint(prepared.current)
306
+ return
307
+ }
308
+ const generation = this.generation
309
+ this.player = painter.play(
310
+ prepared.frames,
311
+ prepared.times,
312
+ { duration: prepared.duration, delay: 0, repeat: 0, reverse: false },
313
+ () => {},
314
+ )
315
+ this.player.control?.(prepared.elapsed(), false)
316
+ // Only long compound windows need a boundary callback; each source sends its
317
+ // own completion/control notifications. There is no observer frame subscription.
318
+ if (prepared.remaining > prepared.duration)
319
+ this.cancelWindow = getDriver().after(
320
+ Math.max(1, prepared.duration - prepared.elapsed()),
321
+ () => {
322
+ if (generation === this.generation) this.changed()
323
+ },
324
+ )
150
325
  }
151
326
 
152
327
  animate(values: GradientValues, transition?: Transition | SpringName): Animation {
153
328
  this.assertActive()
154
329
  const from = this.presented()
155
330
  const to = resolve(from, values, true)
331
+ const patch = gradientPatch(values, from.colors.length)
156
332
  const resolved =
157
333
  typeof transition === 'string' ? springOf(transition) : (transition ?? {})
158
334
  const track = createTrack(
@@ -161,96 +337,205 @@ class GradientController implements Gradient {
161
337
  resolveTransition(resolved, 'opacity', 2),
162
338
  0,
163
339
  )
164
- const reduced = reducedMotion()
165
- const frameAt = (progress: number) => mix(from, to, progress, reduced)
166
340
  this.cancelRunning()
167
- this.model = frameAt(track.to)
168
-
341
+ this.model = from
342
+ for (const key of patch.keys()) this.bindings.delete(key)
343
+ this.subscribe()
169
344
  let settle!: (finished: boolean) => void
170
- const finished = new Promise<boolean>((resolve) => (settle = resolve))
345
+ const finished = new Promise<boolean>((resolve) => {
346
+ settle = resolve
347
+ })
348
+ const driver = getDriver()
349
+ const run: GradientRun = {
350
+ from,
351
+ to,
352
+ track,
353
+ keys: new Set(patch.keys()),
354
+ masked: new Set(),
355
+ clock: driver.clock?.(track.end) ?? createClock(() => driver.now()),
356
+ settle,
357
+ }
171
358
  const animation: Animation = {
172
359
  finished,
173
360
  stop: () => {
174
- if (this.running?.settle === settle) this.stop()
361
+ if (this.running !== run) return
362
+ this.model = this.presented()
363
+ this.cancelRunning()
364
+ this.render()
175
365
  },
176
366
  then: (onfulfilled, onrejected) => finished.then(onfulfilled, onrejected),
177
367
  }
178
- if (track.end === 0 || !this.painter) {
179
- this.paint(this.model)
180
- settle(true)
181
- return animation
368
+ this.running = run
369
+ if (track.end === 0 || !this.painter) this.finish(run, true)
370
+ else {
371
+ this.render()
372
+ if (Number.isFinite(track.end))
373
+ run.cancelEnd = driver.after(Math.max(1, track.end - run.clock.time()), () =>
374
+ this.finish(run, true),
375
+ )
182
376
  }
183
- const handle = this.painter.play(
184
- track.values.map(frameAt),
185
- track.times,
186
- {
187
- duration: track.duration,
188
- delay: track.delay,
189
- repeat: track.repeat,
190
- reverse: track.reverse,
191
- },
192
- (completed) => {
193
- if (this.running?.settle !== settle) return
194
- this.running = undefined
195
- settle(completed)
196
- },
377
+ return animation
378
+ }
379
+
380
+ private finish(run: GradientRun, completed: boolean) {
381
+ if (this.running !== run) return
382
+ const frame = completed
383
+ ? mix(run.from, run.to, run.track.to, reducedMotion())
384
+ : this.presented()
385
+ for (const key of run.keys)
386
+ if (!run.masked.has(key)) put(this.model, key, take(frame, key))
387
+ this.running = undefined
388
+ run.cancelEnd?.()
389
+ run.clock.cancel()
390
+ this.clearPlayer()
391
+ run.settle(completed)
392
+ this.render()
393
+ }
394
+ private cancelRunning() {
395
+ const run = this.running
396
+ this.running = undefined
397
+ run?.cancelEnd?.()
398
+ run?.clock.cancel()
399
+ this.clearPlayer()
400
+ run?.settle(false)
401
+ }
402
+
403
+ bind(values: GradientBindings): () => void {
404
+ this.assertActive()
405
+ const held = this.presented()
406
+ const patch = gradientPatch(values, held.colors.length)
407
+ const candidate: GradientPatch = this.patch()
408
+ const count = values.colors?.length ?? held.colors.length
409
+ const structural = count !== held.colors.length
410
+ if (structural) {
411
+ held.colors.length = held.stops.length = count
412
+ for (const key of candidate.keys())
413
+ if (key.startsWith('color:') || key.startsWith('stop:')) candidate.delete(key)
414
+ }
415
+ for (const [key, value] of patch) candidate.set(key, value)
416
+ const prepared = gradientProgram(
417
+ { at: () => held, remaining: 0, elapsed: () => 0 },
418
+ candidate,
197
419
  )
198
- this.running = {
199
- from,
200
- to,
201
- track,
202
- startedAt: getDriver().now(),
203
- handle,
204
- settle,
420
+ // Validation/sampling above is transactional. Only now may ownership change.
421
+ const owner = {}
422
+ if (structural) {
423
+ for (const key of this.bindings.keys())
424
+ if (key.startsWith('color:') || key.startsWith('stop:')) this.bindings.delete(key)
425
+ for (const key of this.running?.keys ?? [])
426
+ if (key.startsWith('color:') || key.startsWith('stop:'))
427
+ this.running!.masked.add(key)
428
+ }
429
+ this.model = prepared.current
430
+ for (const [key, source] of patch) {
431
+ this.running?.masked.add(key)
432
+ if (typeof source === 'number') this.bindings.delete(key)
433
+ else this.bindings.set(key, { source, owner })
434
+ }
435
+ this.subscribe()
436
+ this.render()
437
+ return () => {
438
+ if (this.removed) return
439
+ const keys = [...this.bindings]
440
+ .filter(([, entry]) => entry.owner === owner)
441
+ .map(([key]) => key)
442
+ if (!keys.length) return
443
+ const current = this.presented()
444
+ for (const key of keys) {
445
+ put(this.model, key, take(current, key))
446
+ this.bindings.delete(key)
447
+ }
448
+ this.subscribe()
449
+ this.render()
205
450
  }
206
- return animation
207
451
  }
208
452
 
209
453
  set(values: GradientValues) {
210
454
  this.assertActive()
211
- const next = resolve(this.presented(), values, false)
455
+ const from = this.presented()
456
+ resolve(from, values, false)
457
+ gradientPatch(values, from.colors.length)
212
458
  this.cancelRunning()
213
- this.model = next
214
- this.paint(next)
459
+ this.model = from
460
+ this.bind(values)
215
461
  }
216
-
217
462
  stop() {
218
- if (!this.running) return
219
- const current = this.presented()
463
+ if (this.removed) return
464
+ const frame = this.presented()
465
+ this.bindings.clear()
466
+ this.unsubscribe()
467
+ this.cancelRunning()
468
+ this.model = frame
469
+ this.program = undefined
470
+ this.painter?.paint(frame)
471
+ }
472
+ private hold() {
473
+ this.model = this.presented()
220
474
  this.cancelRunning()
221
- this.model = current
222
- this.paint(current)
475
+ this.unsubscribe()
476
+ this.painter?.paint(this.model)
477
+ }
478
+ get suspended() {
479
+ return this._suspended
223
480
  }
224
481
 
482
+ suspend() {
483
+ if (!this._suspended && !this.removed) {
484
+ this.hold()
485
+ this._suspended = true
486
+ }
487
+ }
488
+ resume() {
489
+ if (this._suspended && !this.removed) {
490
+ this._suspended = false
491
+ this.subscribe()
492
+ this.changed()
493
+ }
494
+ }
495
+ private unloaded = () => {
496
+ if (this.removed) return
497
+ this.hold()
498
+ this.unloadedView = true
499
+ this.painter?.remove()
500
+ this.painter = undefined
501
+ }
502
+ private loaded = () => {
503
+ if (this.removed) return
504
+ this.unloadedView = false
505
+ this.painter ??= (factory ?? platformPainter)(this.view, this.type)
506
+ this.subscribe()
507
+ this.changed()
508
+ }
509
+ private resized = () => {
510
+ if (this.bindings.size || this.running) this.changed()
511
+ }
512
+ private appSuspended = () => {
513
+ if (!this.background && !this.removed) {
514
+ this.hold()
515
+ this.background = true
516
+ }
517
+ }
518
+ private appResumed = () => {
519
+ if (this.background && !this.removed) {
520
+ this.background = false
521
+ this.subscribe()
522
+ this.changed()
523
+ }
524
+ }
225
525
  remove() {
226
526
  if (this.removed) return
527
+ this.unsubscribe()
227
528
  this.cancelRunning()
529
+ this.bindings.clear()
228
530
  this.removed = true
229
531
  this.view.off('unloaded', this.unloaded)
532
+ this.view.off('loaded', this.loaded)
533
+ this.view.off('layoutChanged', this.resized)
534
+ Application.off(Application.suspendEvent, this.appSuspended)
535
+ Application.off(Application.resumeEvent, this.appResumed)
230
536
  this.painter?.remove()
231
537
  this.painter = undefined
232
538
  }
233
-
234
- private paint(frame: GradientFrame) {
235
- if (this.painter) this.painter.paint(frame)
236
- else this.pending = frame
237
- }
238
-
239
- private presented(): GradientFrame {
240
- const running = this.running
241
- if (!running) return this.model
242
- const elapsed = getDriver().now() - running.startedAt
243
- return mix(running.from, running.to, running.track.valueAt(elapsed), reducedMotion())
244
- }
245
-
246
- private cancelRunning() {
247
- const running = this.running
248
- if (!running) return
249
- this.running = undefined
250
- running.handle.cancel()
251
- running.settle(false)
252
- }
253
-
254
539
  private assertActive() {
255
540
  if (this.removed) throw new Error('This gradient was removed.')
256
541
  }