@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
@@ -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
+ }
package/src/types.ts CHANGED
@@ -84,7 +84,7 @@ export type AnimateOptions = Transition & {
84
84
  relative?: boolean
85
85
  /** With several targets, delays each one by this much more than the previous. */
86
86
  stagger?: number | Stagger
87
- /** iOS impact feedback at arrival, or at explicit points in a finite animation. */
87
+ /** Native haptic feedback at arrival, or at explicit points in a finite animation. */
88
88
  haptic?: Haptic
89
89
  /**
90
90
  * Plays on a named layer, on top of the property's other animations instead of replacing
@@ -0,0 +1,133 @@
1
+ import { Application, Color, Trace } from '@nativescript/core'
2
+ import { getDriver } from './driver/index.js'
3
+
4
+ export interface SubscribeOptions {
5
+ /** Deliver the current value synchronously when subscribing. Defaults to true. */
6
+ immediate?: boolean
7
+ }
8
+
9
+ /** `time` is milliseconds on the native animation clock, shared by a notification batch. */
10
+ export type ValueListener<T> = (value: T, time: number) => void
11
+
12
+ interface Observer {
13
+ read(value: number): number
14
+ color: boolean
15
+ listener: ValueListener<number | Color>
16
+ previous: number
17
+ }
18
+
19
+ // Only sources that need a frame live here. Idle subscriptions retain no global root reference.
20
+ const pending = new Set<ValueObservers>()
21
+ let cancelFrame: (() => void) | undefined
22
+ let listening = false
23
+ let suspended = false
24
+
25
+ function suspend() {
26
+ suspended = true
27
+ cancelFrame?.()
28
+ cancelFrame = undefined
29
+ }
30
+
31
+ function resume() {
32
+ suspended = false
33
+ schedule()
34
+ }
35
+
36
+ function schedule() {
37
+ if (!pending.size) {
38
+ cancelFrame?.()
39
+ cancelFrame = undefined
40
+ if (listening && !suspended) {
41
+ Application.off(Application.suspendEvent, suspend)
42
+ Application.off(Application.resumeEvent, resume)
43
+ listening = false
44
+ }
45
+ return
46
+ }
47
+ if (!listening) {
48
+ Application.on(Application.suspendEvent, suspend)
49
+ Application.on(Application.resumeEvent, resume)
50
+ listening = true
51
+ }
52
+ if (cancelFrame || suspended) return
53
+ cancelFrame = getDriver().frame(() => {
54
+ cancelFrame = undefined
55
+ const time = getDriver().now()
56
+ const sources = [...pending]
57
+ pending.clear()
58
+ // Capture every source before calling any listener: reentrant writes belong to the next batch.
59
+ const deliveries = sources.map((source) => source.snapshot(time))
60
+ for (const source of sources) if (source.active) pending.add(source)
61
+ for (const deliver of deliveries) deliver()
62
+ schedule()
63
+ })
64
+ }
65
+
66
+ function deliver(observer: Observer, value: number, time: number) {
67
+ try {
68
+ observer.listener(observer.color ? new Color(value) : value, time)
69
+ } catch (error) {
70
+ // A consumer failure must not interrupt other consumers or strand the shared frame source.
71
+ Trace.write(
72
+ `Jolt motion value subscriber failed: ${error instanceof Error ? error.stack : String(error)}`,
73
+ Trace.categories.Error,
74
+ Trace.messageType.error,
75
+ )
76
+ }
77
+ }
78
+
79
+ /** Root-owned subscriptions; mappings are evaluated only for consumers that opted in. */
80
+ export class ValueObservers {
81
+ private readonly observers = new Set<Observer>()
82
+ private running = false
83
+
84
+ constructor(private readonly read: (time: number) => number) {}
85
+
86
+ get active() {
87
+ return this.running && this.observers.size > 0
88
+ }
89
+
90
+ subscribe(
91
+ read: (value: number) => number,
92
+ color: boolean,
93
+ listener: ValueListener<number | Color>,
94
+ options: SubscribeOptions = {},
95
+ ): () => void {
96
+ if (typeof listener !== 'function')
97
+ throw new TypeError('subscribe() needs a listener function.')
98
+ const time = getDriver().now()
99
+ const observer = { read, color, listener, previous: read(this.read(time)) }
100
+ this.observers.add(observer)
101
+ if (options.immediate !== false) deliver(observer, observer.previous, time)
102
+ if (this.active) this.changed(true)
103
+ return () => {
104
+ this.observers.delete(observer)
105
+ if (!this.observers.size) {
106
+ pending.delete(this)
107
+ schedule()
108
+ }
109
+ }
110
+ }
111
+
112
+ changed(running: boolean) {
113
+ this.running = running
114
+ if (this.observers.size) {
115
+ pending.add(this)
116
+ schedule()
117
+ }
118
+ }
119
+
120
+ snapshot(time: number): () => void {
121
+ const value = this.read(time)
122
+ const deliveries = [...this.observers].flatMap((observer) => {
123
+ const next = observer.read(value)
124
+ if (Object.is(next, observer.previous)) return []
125
+ observer.previous = next
126
+ return [{ observer, next }]
127
+ })
128
+ return () => {
129
+ for (const { observer, next } of deliveries)
130
+ if (this.observers.has(observer)) deliver(observer, next, time)
131
+ }
132
+ }
133
+ }
@@ -0,0 +1,71 @@
1
+ import { instantTrack, type Track } from './track.js'
2
+
3
+ export interface ValuePart {
4
+ readonly track: Track
5
+ readonly elapsed: number
6
+ readonly base: number
7
+ readonly span: number
8
+ }
9
+
10
+ /** Compile scalar addition before any binding's mapping. The snapshot is independent of later edits. */
11
+ export function valueTrack(parts: readonly ValuePart[], rest: number) {
12
+ const value = (time: number) =>
13
+ parts.reduce(
14
+ (sum, part) =>
15
+ sum + part.base + part.span * part.track.valueAt(part.elapsed + time),
16
+ rest,
17
+ )
18
+ let finite = 0
19
+ const periods: number[] = []
20
+ for (const { track, elapsed } of parts) {
21
+ if (Number.isFinite(track.end)) finite = Math.max(finite, track.end - elapsed)
22
+ else if (elapsed < track.delay) finite = Math.max(finite, track.delay - elapsed)
23
+ else periods.push(Math.max(1, track.duration * (track.reverse ? 2 : 1)))
24
+ }
25
+ if (finite <= 0 && !periods.length)
26
+ return { track: instantTrack('x', value(0)), windowed: false }
27
+ const loop = finite <= 0 && periods.length ? commonPeriod(periods) : undefined
28
+ // Bound finite compositions too: a minutes-long base must not undersample a short pulse.
29
+ const duration = finite > 0 ? Math.min(finite, 4000) : (loop ?? 4000)
30
+ const steps = Math.max(1, Math.min(6000, Math.ceil((duration * 60) / 1000)))
31
+ const times = Array.from({ length: steps + 1 }, (_, i) => i / steps)
32
+ const values = times.map((time) => value(time * duration))
33
+ const endless = loop !== undefined
34
+ const at = (time: number) =>
35
+ value(
36
+ endless
37
+ ? ((time % duration) + duration) % duration
38
+ : Math.min(duration, Math.max(0, time)),
39
+ )
40
+ const track: Track = {
41
+ property: 'x',
42
+ delay: 0,
43
+ duration,
44
+ repeat: endless ? Infinity : 0,
45
+ reverse: false,
46
+ times,
47
+ values,
48
+ to: endless ? values[0]! : values[steps]!,
49
+ end: endless ? Infinity : duration,
50
+ arrival: endless ? Infinity : duration,
51
+ valueAt: at,
52
+ velocityAt: (time) => (at(time + 0.5) - at(time - 0.5)) * 1000,
53
+ }
54
+ return {
55
+ track,
56
+ windowed:
57
+ finite > duration ||
58
+ (!endless && parts.some(({ track }) => !Number.isFinite(track.end))),
59
+ }
60
+ }
61
+
62
+ function commonPeriod(periods: readonly number[]) {
63
+ const gcd = (a: number, b: number): number => (b ? gcd(b, a % b) : a)
64
+ let total = 1
65
+ for (const period of periods) {
66
+ if (!Number.isInteger(period)) return undefined
67
+ total = (total / gcd(total, period)) * period
68
+ if (total > 30000) return undefined
69
+ }
70
+ return total
71
+ }
package/src/vue/index.ts CHANGED
@@ -40,6 +40,8 @@ import {
40
40
  type GradientOptions,
41
41
  type Transition,
42
42
  } from '../index.js'
43
+ import { isDerived } from '../motion-value.js'
44
+ import { prepareGradient, type GradientValues } from '../gradient.js'
43
45
  import { currentKeyboard } from '../keyboard.js'
44
46
 
45
47
  type Host = NSVElement
@@ -70,7 +72,10 @@ export interface TransitionProps {
70
72
  const animations = new WeakMap<object, PlaybackAnimation>()
71
73
  const scopes = new WeakMap<object, MotionScope>()
72
74
  const removers = new WeakMap<object, () => void>()
73
- const gradients = new WeakMap<object, { gradient: Gradient; colors: number }>()
75
+ const gradients = new WeakMap<
76
+ object,
77
+ { gradient: Gradient; colors: number; release?: () => void }
78
+ >()
74
79
 
75
80
  function split<T>(
76
81
  value: T | readonly [T, Options?],
@@ -82,6 +87,7 @@ function split<T>(
82
87
 
83
88
  function same(a: unknown, b: unknown): boolean {
84
89
  if (Object.is(a, b)) return true
90
+ if (isDerived(a) || isDerived(b)) return false
85
91
  if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null)
86
92
  return false
87
93
  if (Array.isArray(a) !== Array.isArray(b)) return false
@@ -161,17 +167,35 @@ function paint(el: Host, value: GradientValue, animated: boolean) {
161
167
  (Transition | SpringName)?,
162
168
  ]
163
169
  const current = gradients.get(el)
170
+ prepareGradient(options)
164
171
  const colors = options.colors.length
172
+ const hasBindings = [
173
+ options.angle,
174
+ options.radius,
175
+ options.center?.x,
176
+ options.center?.y,
177
+ ...options.colors,
178
+ ...(options.stops ?? []),
179
+ ].some(isDerived)
165
180
  if (current && current.gradient.type === options.type) {
166
181
  const { type, ...values } = options
167
- if (animated && current.colors === colors)
168
- current.gradient.animate(values, transition)
169
- else current.gradient.set(values)
182
+ const previous = current.release
183
+ if (hasBindings) current.release = current.gradient.bind(values)
184
+ else {
185
+ if (animated && current.colors === colors)
186
+ current.gradient.animate(values as GradientValues, transition)
187
+ else current.gradient.set(values as GradientValues)
188
+ current.release = undefined
189
+ }
190
+ previous?.()
170
191
  current.colors = colors
171
192
  return
172
193
  }
173
194
  current?.gradient.remove()
174
- gradients.set(el, { gradient: gradient(el, options), colors })
195
+ // Track directive ownership explicitly, including bindings omitted on a later update.
196
+ const made = gradient(el, options)
197
+ const release = hasBindings ? made.bind(options) : undefined
198
+ gradients.set(el, { gradient: made, colors, release })
175
199
  }
176
200
 
177
201
  /**