@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.
- package/README.md +9 -6
- package/dist/animate.d.ts +6 -4
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +3 -1
- package/dist/animate.js.map +1 -1
- package/dist/drag.js +3 -3
- package/dist/drag.js.map +1 -1
- package/dist/driver/android-gradient.d.ts.map +1 -1
- package/dist/driver/android-gradient.js +11 -1
- package/dist/driver/android-gradient.js.map +1 -1
- package/dist/driver/android-particles.d.ts +6 -0
- package/dist/driver/android-particles.d.ts.map +1 -0
- package/dist/driver/android-particles.js +93 -0
- package/dist/driver/android-particles.js.map +1 -0
- package/dist/driver/android.d.ts.map +1 -1
- package/dist/driver/android.js +2 -0
- package/dist/driver/android.js.map +1 -1
- package/dist/driver/frame.d.ts +3 -0
- package/dist/driver/frame.d.ts.map +1 -0
- package/dist/driver/frame.js +7 -0
- package/dist/driver/frame.js.map +1 -0
- package/dist/driver/index.d.ts +2 -0
- package/dist/driver/index.d.ts.map +1 -1
- package/dist/driver/index.js.map +1 -1
- package/dist/driver/ios-gradient.d.ts.map +1 -1
- package/dist/driver/ios-gradient.js +24 -2
- package/dist/driver/ios-gradient.js.map +1 -1
- package/dist/driver/ios.d.ts.map +1 -1
- package/dist/driver/ios.js +2 -0
- package/dist/driver/ios.js.map +1 -1
- package/dist/engine.d.ts +2 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +32 -5
- package/dist/engine.js.map +1 -1
- package/dist/gradient-binding.d.ts +38 -0
- package/dist/gradient-binding.d.ts.map +1 -0
- package/dist/gradient-binding.js +128 -0
- package/dist/gradient-binding.js.map +1 -0
- package/dist/gradient.d.ts +19 -4
- package/dist/gradient.d.ts.map +1 -1
- package/dist/gradient.js +331 -70
- package/dist/gradient.js.map +1 -1
- package/dist/haptics.d.ts.map +1 -1
- package/dist/haptics.js +101 -28
- package/dist/haptics.js.map +1 -1
- package/dist/image-effect.d.ts +1 -0
- package/dist/image-effect.d.ts.map +1 -1
- package/dist/image-effect.js +10 -2
- package/dist/image-effect.js.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/motion-scope.d.ts +3 -2
- package/dist/motion-scope.d.ts.map +1 -1
- package/dist/motion-scope.js +12 -2
- package/dist/motion-scope.js.map +1 -1
- package/dist/motion-value.d.ts +62 -8
- package/dist/motion-value.d.ts.map +1 -1
- package/dist/motion-value.js +327 -91
- package/dist/motion-value.js.map +1 -1
- package/dist/native/index.d.ts +7 -0
- package/dist/native/index.d.ts.map +1 -0
- package/dist/native/index.js +4 -0
- package/dist/native/index.js.map +1 -0
- package/dist/native-properties.d.ts +28 -0
- package/dist/native-properties.d.ts.map +1 -0
- package/dist/native-properties.js +154 -0
- package/dist/native-properties.js.map +1 -0
- package/dist/native-property-driver.d.ts +19 -0
- package/dist/native-property-driver.d.ts.map +1 -0
- package/dist/native-property-driver.js +69 -0
- package/dist/native-property-driver.js.map +1 -0
- package/dist/native-property.d.ts +35 -0
- package/dist/native-property.d.ts.map +1 -0
- package/dist/native-property.js +277 -0
- package/dist/native-property.js.map +1 -0
- package/dist/particles.d.ts +5 -2
- package/dist/particles.d.ts.map +1 -1
- package/dist/particles.js +16 -10
- package/dist/particles.js.map +1 -1
- package/dist/playback-group.d.ts.map +1 -1
- package/dist/playback-group.js +17 -1
- package/dist/playback-group.js.map +1 -1
- package/dist/playback.d.ts +15 -0
- package/dist/playback.d.ts.map +1 -1
- package/dist/playback.js +23 -0
- package/dist/playback.js.map +1 -1
- package/dist/scene-appearance.d.ts +14 -0
- package/dist/scene-appearance.d.ts.map +1 -0
- package/dist/scene-appearance.js +68 -0
- package/dist/scene-appearance.js.map +1 -0
- package/dist/sequence.d.ts +24 -0
- package/dist/sequence.d.ts.map +1 -1
- package/dist/sequence.js +181 -19
- package/dist/sequence.js.map +1 -1
- package/dist/timed-animation.d.ts +38 -0
- package/dist/timed-animation.d.ts.map +1 -0
- package/dist/timed-animation.js +177 -0
- package/dist/timed-animation.js.map +1 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/value-observers.d.ts +19 -0
- package/dist/value-observers.d.ts.map +1 -0
- package/dist/value-observers.js +111 -0
- package/dist/value-observers.js.map +1 -0
- package/dist/value-track.d.ts +13 -0
- package/dist/value-track.d.ts.map +1 -0
- package/dist/value-track.js +59 -0
- package/dist/value-track.js.map +1 -0
- package/dist/vue/index.d.ts.map +1 -1
- package/dist/vue/index.js +28 -5
- package/dist/vue/index.js.map +1 -1
- package/package.json +6 -2
- package/platforms/android/java/org/rafaelaugusto/jolt/GradientLayer.java +30 -1
- package/platforms/android/java/org/rafaelaugusto/jolt/ParticleEmitter.java +192 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/ParticleSimulation.java +114 -0
- package/platforms/android/proguard-rules.pro +2 -0
- package/src/android-plugin.d.ts +93 -0
- package/src/animate.ts +13 -7
- package/src/drag.ts +3 -3
- package/src/driver/android-gradient.ts +8 -1
- package/src/driver/android-particles.ts +118 -0
- package/src/driver/android.ts +2 -0
- package/src/driver/frame.ts +10 -0
- package/src/driver/index.ts +2 -0
- package/src/driver/ios-gradient.ts +24 -2
- package/src/driver/ios.ts +2 -0
- package/src/engine.ts +33 -6
- package/src/gradient-binding.ts +158 -0
- package/src/gradient.ts +376 -91
- package/src/haptics.ts +106 -28
- package/src/image-effect.ts +10 -2
- package/src/index.ts +6 -0
- package/src/ios-plugin.d.ts +40 -0
- package/src/motion-scope.ts +14 -3
- package/src/motion-value.ts +374 -99
- package/src/native/index.ts +23 -0
- package/src/native-properties.ts +188 -0
- package/src/native-property-driver.ts +123 -0
- package/src/native-property.ts +353 -0
- package/src/particles.ts +18 -17
- package/src/playback-group.ts +17 -1
- package/src/playback.ts +41 -0
- package/src/scene-appearance.ts +92 -0
- package/src/sequence.ts +209 -20
- package/src/timed-animation.ts +192 -0
- package/src/types.ts +1 -1
- package/src/value-observers.ts +133 -0
- package/src/value-track.ts +71 -0
- 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
|
-
/**
|
|
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<
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
else
|
|
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
|
-
|
|
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
|
/**
|