@rafaelaugusto/jolt 0.1.0-beta.11 → 0.1.0-beta.12
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 +38 -7
- package/dist/animate.d.ts +14 -4
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +50 -34
- package/dist/animate.js.map +1 -1
- package/dist/drag.d.ts +17 -3
- package/dist/drag.d.ts.map +1 -1
- package/dist/drag.js +34 -11
- package/dist/drag.js.map +1 -1
- package/dist/easing.d.ts +1 -1
- package/dist/easing.d.ts.map +1 -1
- package/dist/easing.js +11 -0
- package/dist/easing.js.map +1 -1
- package/dist/engine.d.ts +18 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +143 -36
- package/dist/engine.js.map +1 -1
- package/dist/gradient.d.ts.map +1 -1
- package/dist/gradient.js +10 -4
- package/dist/gradient.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/keyboard.d.ts.map +1 -1
- package/dist/keyboard.js +63 -10
- package/dist/keyboard.js.map +1 -1
- package/dist/mixer.d.ts +49 -0
- package/dist/mixer.d.ts.map +1 -0
- package/dist/mixer.js +238 -0
- package/dist/mixer.js.map +1 -0
- package/dist/motion-scope.d.ts +4 -1
- package/dist/motion-scope.d.ts.map +1 -1
- package/dist/motion-scope.js +8 -3
- package/dist/motion-scope.js.map +1 -1
- package/dist/motion-states.d.ts +10 -5
- package/dist/motion-states.d.ts.map +1 -1
- package/dist/motion-states.js +55 -22
- package/dist/motion-states.js.map +1 -1
- package/dist/motion-value.d.ts +115 -0
- package/dist/motion-value.d.ts.map +1 -0
- package/dist/motion-value.js +437 -0
- package/dist/motion-value.js.map +1 -0
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +2 -2
- package/dist/path.js.map +1 -1
- package/dist/track.d.ts.map +1 -1
- package/dist/track.js +31 -12
- package/dist/track.js.map +1 -1
- package/dist/transition.d.ts +15 -4
- package/dist/transition.d.ts.map +1 -1
- package/dist/transition.js +46 -7
- package/dist/transition.js.map +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/vue/index.d.ts +28 -2
- package/dist/vue/index.d.ts.map +1 -1
- package/dist/vue/index.js +78 -2
- package/dist/vue/index.js.map +1 -1
- package/package.json +1 -1
- package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFrame.java +53 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFrameFollower.java +57 -0
- package/src/android-plugin.d.ts +27 -0
- package/src/animate.ts +99 -16
- package/src/drag.ts +67 -21
- package/src/easing.ts +14 -0
- package/src/engine.ts +158 -53
- package/src/gradient.ts +10 -4
- package/src/index.ts +18 -1
- package/src/keyboard.ts +83 -18
- package/src/mixer.ts +264 -0
- package/src/motion-scope.ts +12 -4
- package/src/motion-states.ts +79 -29
- package/src/motion-value.ts +563 -0
- package/src/path.ts +2 -1
- package/src/track.ts +37 -12
- package/src/transition.ts +69 -10
- package/src/types.ts +5 -0
- package/src/vue/index.ts +98 -1
package/src/mixer.ts
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import type { View } from '@nativescript/core'
|
|
2
|
+
import { getDriver } from './driver/index.js'
|
|
3
|
+
import { readValue, writeValue, type Property } from './properties.js'
|
|
4
|
+
import type { Track } from './track.js'
|
|
5
|
+
|
|
6
|
+
/** The channel of regular animations; layers are named next to it. */
|
|
7
|
+
export const BASE = ''
|
|
8
|
+
|
|
9
|
+
const SAMPLES_PER_SECOND = 60
|
|
10
|
+
const MAX_SAMPLES = 6000
|
|
11
|
+
/** Endless channels loop natively over the least common multiple of their periods, up to this. */
|
|
12
|
+
const MAX_LOOP = 30000
|
|
13
|
+
/** Beyond it, the composition is played in windows and sampled again at each one. */
|
|
14
|
+
const WINDOW = 4000
|
|
15
|
+
|
|
16
|
+
export const multiplies = (property: Property) =>
|
|
17
|
+
property === 'opacity' || property === 'scaleX' || property === 'scaleY'
|
|
18
|
+
export const identity = (property: Property) => (multiplies(property) ? 1 : 0)
|
|
19
|
+
export const layerable = (property: Property) =>
|
|
20
|
+
multiplies(property) ||
|
|
21
|
+
property === 'x' ||
|
|
22
|
+
property === 'y' ||
|
|
23
|
+
property === 'rotate' ||
|
|
24
|
+
property === 'rotateX' ||
|
|
25
|
+
property === 'rotateY'
|
|
26
|
+
|
|
27
|
+
interface Channel {
|
|
28
|
+
track: Track
|
|
29
|
+
anchor: number
|
|
30
|
+
offset: number
|
|
31
|
+
paused: boolean
|
|
32
|
+
onEnd: (finished: boolean) => void
|
|
33
|
+
timer?: () => void
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** What the engine holds instead of a driver handle while a property is mixed. */
|
|
37
|
+
export class MixHandle {
|
|
38
|
+
constructor(
|
|
39
|
+
readonly mix: Mix,
|
|
40
|
+
readonly key: string,
|
|
41
|
+
) {}
|
|
42
|
+
elapsed() {
|
|
43
|
+
return this.mix.elapsed(this.key)
|
|
44
|
+
}
|
|
45
|
+
control(position: number, paused: boolean) {
|
|
46
|
+
this.mix.control(this.key, position, paused)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const mixes = new WeakMap<View, Map<Property, Mix>>()
|
|
51
|
+
|
|
52
|
+
export function mixOf(view: View, property: Property): Mix | undefined {
|
|
53
|
+
return mixes.get(view)?.get(property)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function mixesOf(view: View): Mix[] {
|
|
57
|
+
return [...(mixes.get(view)?.values() ?? [])]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function mixFor(view: View, property: Property): Mix {
|
|
61
|
+
let properties = mixes.get(view)
|
|
62
|
+
if (!properties) mixes.set(view, (properties = new Map()))
|
|
63
|
+
let mix = properties.get(property)
|
|
64
|
+
if (!mix) properties.set(property, (mix = new Mix(view, property)))
|
|
65
|
+
return mix
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** One property of one view: a base and named layers, played natively as one composite track. */
|
|
69
|
+
export class Mix {
|
|
70
|
+
private readonly rests = new Map<string, number>()
|
|
71
|
+
private readonly channels = new Map<string, Channel>()
|
|
72
|
+
private native: unknown
|
|
73
|
+
private generation = 0
|
|
74
|
+
|
|
75
|
+
constructor(
|
|
76
|
+
readonly view: View,
|
|
77
|
+
readonly property: Property,
|
|
78
|
+
) {
|
|
79
|
+
this.rests.set(BASE, readValue(view, property))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
rest(key: string): number {
|
|
83
|
+
return (
|
|
84
|
+
this.rests.get(key) ??
|
|
85
|
+
(key === BASE ? readValue(this.view, this.property) : identity(this.property))
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
setRest(key: string, value: number) {
|
|
90
|
+
this.rests.set(key, value)
|
|
91
|
+
this.compose()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
adopt(
|
|
95
|
+
key: string,
|
|
96
|
+
track: Track,
|
|
97
|
+
startedAt: number,
|
|
98
|
+
onEnd: (finished: boolean) => void,
|
|
99
|
+
) {
|
|
100
|
+
this.channels.get(key)?.timer?.()
|
|
101
|
+
const channel: Channel = { track, anchor: startedAt, offset: 0, paused: false, onEnd }
|
|
102
|
+
this.channels.set(key, channel)
|
|
103
|
+
this.schedule(key, channel)
|
|
104
|
+
this.compose()
|
|
105
|
+
return new MixHandle(this, key)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
elapsed(key: string, now = getDriver().now()): number {
|
|
109
|
+
const channel = this.channels.get(key)
|
|
110
|
+
if (!channel) return 0
|
|
111
|
+
return channel.paused ? channel.offset : channel.offset + now - channel.anchor
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
control(key: string, position: number, paused: boolean) {
|
|
115
|
+
const channel = this.channels.get(key)
|
|
116
|
+
if (!channel) return
|
|
117
|
+
channel.timer?.()
|
|
118
|
+
channel.timer = undefined
|
|
119
|
+
channel.offset = position
|
|
120
|
+
channel.anchor = getDriver().now()
|
|
121
|
+
channel.paused = paused
|
|
122
|
+
if (!paused) this.schedule(key, channel)
|
|
123
|
+
this.compose()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* A channel leaves: replaced, stopped or done. A stopped layer takes its contribution
|
|
128
|
+
* with it; anything else rests where it is.
|
|
129
|
+
*/
|
|
130
|
+
detach(key: string, stopped: boolean) {
|
|
131
|
+
const channel = this.channels.get(key)
|
|
132
|
+
if (!channel) return
|
|
133
|
+
const value = channel.track.valueAt(this.elapsed(key))
|
|
134
|
+
channel.timer?.()
|
|
135
|
+
this.channels.delete(key)
|
|
136
|
+
this.rests.set(key, stopped && key !== BASE ? identity(this.property) : value)
|
|
137
|
+
this.compose()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Everything stops: what is on screen becomes the base, and layers are cleared. */
|
|
141
|
+
freeze() {
|
|
142
|
+
const value = this.value()
|
|
143
|
+
for (const [key, channel] of [...this.channels]) {
|
|
144
|
+
channel.timer?.()
|
|
145
|
+
this.channels.delete(key)
|
|
146
|
+
channel.onEnd(false)
|
|
147
|
+
}
|
|
148
|
+
this.rests.clear()
|
|
149
|
+
this.rests.set(BASE, value)
|
|
150
|
+
this.compose()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The composed value on screen at `time`. */
|
|
154
|
+
value(time = getDriver().now()): number {
|
|
155
|
+
const now = getDriver().now()
|
|
156
|
+
let result = this.part(BASE, time, now)
|
|
157
|
+
const keys = new Set([...this.rests.keys(), ...this.channels.keys()])
|
|
158
|
+
for (const key of keys) {
|
|
159
|
+
if (key === BASE) continue
|
|
160
|
+
const part = this.part(key, time, now)
|
|
161
|
+
result = multiplies(this.property) ? result * part : result + part
|
|
162
|
+
}
|
|
163
|
+
return result
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private part(key: string, time: number, now: number) {
|
|
167
|
+
const channel = this.channels.get(key)
|
|
168
|
+
if (!channel) return this.rest(key)
|
|
169
|
+
const elapsed = channel.paused
|
|
170
|
+
? channel.offset
|
|
171
|
+
: channel.offset + now - channel.anchor + (time - now)
|
|
172
|
+
return channel.track.valueAt(elapsed)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private schedule(key: string, channel: Channel) {
|
|
176
|
+
const remaining = channel.track.end - this.elapsed(key)
|
|
177
|
+
if (!Number.isFinite(remaining)) return
|
|
178
|
+
channel.timer = getDriver().after(Math.max(0, remaining), () => {
|
|
179
|
+
if (this.channels.get(key) !== channel) return
|
|
180
|
+
this.channels.delete(key)
|
|
181
|
+
this.rests.set(key, channel.track.valueAt(channel.track.end))
|
|
182
|
+
channel.onEnd(true)
|
|
183
|
+
this.compose()
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Samples the composition from now and plays it as one native track. */
|
|
188
|
+
private compose() {
|
|
189
|
+
const driver = getDriver()
|
|
190
|
+
const now = driver.now()
|
|
191
|
+
const instance = ++this.generation
|
|
192
|
+
const previous = this.native
|
|
193
|
+
this.native = undefined
|
|
194
|
+
|
|
195
|
+
let finite = 0
|
|
196
|
+
const periods: number[] = []
|
|
197
|
+
for (const [key, channel] of this.channels) {
|
|
198
|
+
if (channel.paused) continue
|
|
199
|
+
const elapsed = this.elapsed(key, now)
|
|
200
|
+
const { track } = channel
|
|
201
|
+
if (Number.isFinite(track.end)) finite = Math.max(finite, track.end - elapsed)
|
|
202
|
+
else if (elapsed < track.delay) finite = Math.max(finite, track.delay - elapsed)
|
|
203
|
+
else periods.push(Math.max(1, Math.round(track.duration * (track.reverse ? 2 : 1))))
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (previous !== undefined) driver.stop(this.view, previous)
|
|
207
|
+
const loop = finite <= 0 && periods.length ? leastCommonMultiple(periods) : undefined
|
|
208
|
+
if (finite <= 0 && !periods.length) {
|
|
209
|
+
writeValue(this.view, this.property, this.value(now))
|
|
210
|
+
driver.sync?.(this.view)
|
|
211
|
+
this.dissolve()
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const span = finite > 0 ? finite : (loop ?? WINDOW)
|
|
216
|
+
const steps = Math.max(
|
|
217
|
+
1,
|
|
218
|
+
Math.min(MAX_SAMPLES, Math.ceil((span / 1000) * SAMPLES_PER_SECOND)),
|
|
219
|
+
)
|
|
220
|
+
const times = Array.from({ length: steps + 1 }, (_, i) => i / steps)
|
|
221
|
+
const values = times.map((time) => this.value(now + time * span))
|
|
222
|
+
const endless = loop !== undefined
|
|
223
|
+
const at = (elapsed: number) =>
|
|
224
|
+
this.value(now + (endless ? elapsed % span : Math.min(Math.max(elapsed, 0), span)))
|
|
225
|
+
const track: Track = {
|
|
226
|
+
property: this.property,
|
|
227
|
+
delay: 0,
|
|
228
|
+
duration: span,
|
|
229
|
+
repeat: endless ? Infinity : 0,
|
|
230
|
+
reverse: false,
|
|
231
|
+
times,
|
|
232
|
+
values,
|
|
233
|
+
to: endless ? values[0]! : values[steps]!,
|
|
234
|
+
end: endless ? Infinity : span,
|
|
235
|
+
arrival: endless ? Infinity : span,
|
|
236
|
+
valueAt: at,
|
|
237
|
+
velocityAt: (elapsed) => (at(elapsed + 0.5) - at(elapsed - 0.5)) * 1000,
|
|
238
|
+
}
|
|
239
|
+
writeValue(this.view, this.property, track.to)
|
|
240
|
+
this.native = driver.play(this.view, track, now, (finished = true) => {
|
|
241
|
+
if (instance !== this.generation || !finished) return
|
|
242
|
+
this.native = undefined
|
|
243
|
+
this.compose()
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private dissolve() {
|
|
248
|
+
if (this.channels.size) return
|
|
249
|
+
for (const [key, value] of this.rests)
|
|
250
|
+
if (key !== BASE && value !== identity(this.property)) return
|
|
251
|
+
const properties = mixes.get(this.view)
|
|
252
|
+
if (properties?.get(this.property) === this) properties.delete(this.property)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function leastCommonMultiple(periods: readonly number[]): number | undefined {
|
|
257
|
+
const gcd = (a: number, b: number): number => (b ? gcd(b, a % b) : a)
|
|
258
|
+
let result = periods[0]!
|
|
259
|
+
for (const period of periods.slice(1)) {
|
|
260
|
+
result = (result / gcd(result, period)) * period
|
|
261
|
+
if (result > MAX_LOOP) return undefined
|
|
262
|
+
}
|
|
263
|
+
return result
|
|
264
|
+
}
|
package/src/motion-scope.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { animate, decay } from './animate.js'
|
|
2
2
|
import { press } from './press.js'
|
|
3
3
|
import { drag } from './drag.js'
|
|
4
|
+
import { gradient } from './gradient.js'
|
|
4
5
|
import { animateLayout, type LayoutOptions } from './layout.js'
|
|
5
6
|
import type { SpringName } from './transition.js'
|
|
6
7
|
import { sequence } from './sequence.js'
|
|
@@ -39,13 +40,15 @@ export interface MotionScope {
|
|
|
39
40
|
sequence: typeof sequence
|
|
40
41
|
press: typeof press
|
|
41
42
|
drag: typeof drag
|
|
43
|
+
/** Removed with the scope; suspending it stops a gradient animation where it is. */
|
|
44
|
+
gradient: typeof gradient
|
|
42
45
|
states<
|
|
43
46
|
T extends Record<string, Target | null>,
|
|
44
47
|
const S extends Record<string, MotionState<keyof T>>,
|
|
45
48
|
>(
|
|
46
49
|
targets: T,
|
|
47
50
|
definitions: MotionDefinitions<T, S>,
|
|
48
|
-
options?: Omit<MotionStatesOptions, 'scope'>,
|
|
51
|
+
options?: Omit<MotionStatesOptions<Extract<keyof T, string>>, 'scope'>,
|
|
49
52
|
): MotionStates<Extract<keyof S, string>, Extract<keyof T, string>>
|
|
50
53
|
/** Adopt an existing handle. Completed animations are released automatically. */
|
|
51
54
|
add<T extends MotionResource>(resource: T): T
|
|
@@ -189,9 +192,8 @@ export function motionScope(): MotionScope {
|
|
|
189
192
|
: animateLayout(container, changeOrOptions),
|
|
190
193
|
)
|
|
191
194
|
},
|
|
192
|
-
animate(...args)
|
|
193
|
-
|
|
194
|
-
},
|
|
195
|
+
animate: ((...args: Parameters<typeof animate>) =>
|
|
196
|
+
startOwned(() => animate(...args))) as typeof animate,
|
|
195
197
|
decay(...args) {
|
|
196
198
|
return startOwned(() => decay(...args))
|
|
197
199
|
},
|
|
@@ -204,6 +206,12 @@ export function motionScope(): MotionScope {
|
|
|
204
206
|
drag(...args) {
|
|
205
207
|
return startOwned(() => drag(...args))
|
|
206
208
|
},
|
|
209
|
+
gradient(target, options) {
|
|
210
|
+
assertActive()
|
|
211
|
+
const made = gradient(target, options)
|
|
212
|
+
scope.add({ pause: () => made.stop(), play() {}, dispose: () => made.remove() })
|
|
213
|
+
return made
|
|
214
|
+
},
|
|
207
215
|
states(targets, definitions, options) {
|
|
208
216
|
return startOwned(() => motionStates(targets, definitions, options))
|
|
209
217
|
},
|
package/src/motion-states.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { Animation } from './engine.js'
|
|
|
5
5
|
import type { PlaybackAnimation } from './playback.js'
|
|
6
6
|
import type { KeyframeProperty, Keyframes, Target, Values } from './types.js'
|
|
7
7
|
import type { View } from '@nativescript/core'
|
|
8
|
+
import { mergeTransitions, springs } from './transition.js'
|
|
8
9
|
import type { Spring, Tween, SpringName } from './transition.js'
|
|
9
10
|
import type { MotionScope } from './motion-scope.js'
|
|
10
11
|
|
|
@@ -21,12 +22,17 @@ export type MotionDefinitions<T, S> = S & {
|
|
|
21
22
|
[K in keyof S[N]]: Record<Exclude<keyof S[N][K], KeyframeProperty>, never>
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
+
type StateCurve =
|
|
25
26
|
| SpringName
|
|
26
27
|
| Omit<Spring, 'repeat' | 'repeatType'>
|
|
27
28
|
| Omit<Tween, 'repeat' | 'repeatType'>
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
/** One transition for every target, or a main one with some targets set apart by name. */
|
|
30
|
+
export type StateTransition<T extends string = string> =
|
|
31
|
+
| StateCurve
|
|
32
|
+
| (Exclude<StateCurve, SpringName> &
|
|
33
|
+
(string extends T ? unknown : { [K in T]?: StateCurve }))
|
|
34
|
+
export interface MotionStatesOptions<T extends string = string> {
|
|
35
|
+
transition?: StateTransition<T>
|
|
30
36
|
scope?: MotionScope
|
|
31
37
|
}
|
|
32
38
|
export interface MotionStates<S extends string = string, T extends string = string> {
|
|
@@ -35,7 +41,7 @@ export interface MotionStates<S extends string = string, T extends string = stri
|
|
|
35
41
|
readonly settled: S | undefined
|
|
36
42
|
readonly disposed: boolean
|
|
37
43
|
readonly paused: boolean
|
|
38
|
-
to(state: S, transition?: StateTransition): Animation
|
|
44
|
+
to(state: S, transition?: StateTransition<T>): Animation
|
|
39
45
|
/**
|
|
40
46
|
* Point a target at a new view, or at none with `null`. The new view takes the current
|
|
41
47
|
* state at once, or joins the transition in progress from where it is.
|
|
@@ -56,7 +62,7 @@ export function motionStates<
|
|
|
56
62
|
>(
|
|
57
63
|
targets: T,
|
|
58
64
|
definitions: MotionDefinitions<T, S>,
|
|
59
|
-
options: MotionStatesOptions = {},
|
|
65
|
+
options: MotionStatesOptions<Extract<keyof T, string>> = {},
|
|
60
66
|
): MotionStates<Extract<keyof S, string>, Extract<keyof T, string>> {
|
|
61
67
|
type Name = Extract<keyof S, string>
|
|
62
68
|
type Slot = Extract<keyof T, string>
|
|
@@ -69,8 +75,13 @@ export function motionStates<
|
|
|
69
75
|
const assigned = [...views.values()].filter(Boolean)
|
|
70
76
|
if (new Set(assigned).size !== assigned.length)
|
|
71
77
|
throw new TypeError('Motion state targets must refer to distinct views.')
|
|
78
|
+
for (const name of views.keys())
|
|
79
|
+
if (curveKeys.has(name))
|
|
80
|
+
throw new TypeError(
|
|
81
|
+
`A motion target can't be named "${name}", like a transition option.`,
|
|
82
|
+
)
|
|
72
83
|
const states = new Map<string, [string, Keyframes][]>()
|
|
73
|
-
const defaults = normalize(options.transition)
|
|
84
|
+
const defaults = normalize(options.transition, views)
|
|
74
85
|
for (const [name, state] of Object.entries(definitions)) {
|
|
75
86
|
if (!state || typeof state !== 'object' || Array.isArray(state))
|
|
76
87
|
throw new TypeError(`Invalid motion state "${name}".`)
|
|
@@ -95,8 +106,8 @@ export function motionStates<
|
|
|
95
106
|
Object.assign(frames, { [property]: value })
|
|
96
107
|
}
|
|
97
108
|
// Validate every definition up front without claiming any native property.
|
|
98
|
-
if (view) prepareAnimation(view, frames, defaults)
|
|
99
|
-
else endingValues(frames, defaults)
|
|
109
|
+
if (view) prepareAnimation(view, frames, timingFor(defaults, target))
|
|
110
|
+
else endingValues(frames, timingFor(defaults, target))
|
|
100
111
|
rows.push([target, frames])
|
|
101
112
|
}
|
|
102
113
|
states.set(name, rows)
|
|
@@ -106,8 +117,7 @@ export function motionStates<
|
|
|
106
117
|
let disposed = false
|
|
107
118
|
let paused = false
|
|
108
119
|
let active:
|
|
109
|
-
|
|
110
|
-
| undefined
|
|
120
|
+
{ handle: Animation; children: PlaybackAnimation[]; timing: Timing } | undefined
|
|
111
121
|
const refs = new Map<string, (view: Target | null) => void>()
|
|
112
122
|
const framesFor = (state: Name | undefined, target: string) =>
|
|
113
123
|
state === undefined
|
|
@@ -130,10 +140,10 @@ export function motionStates<
|
|
|
130
140
|
if (disposed) throw new Error('Motion states controller is disposed.')
|
|
131
141
|
const rows = states.get(name)
|
|
132
142
|
if (!rows) throw new TypeError(`Unknown motion state "${name}".`)
|
|
133
|
-
const timing = transition
|
|
143
|
+
const timing = normalize(transition, views, defaults)
|
|
134
144
|
const prepared = rows.flatMap(([target, frames]) => {
|
|
135
145
|
const view = views.get(target)
|
|
136
|
-
return view ? [prepareAnimation(view, frames, timing)] : []
|
|
146
|
+
return view ? [prepareAnimation(view, frames, timingFor(timing, target))] : []
|
|
137
147
|
})
|
|
138
148
|
const previous = active
|
|
139
149
|
const children: PlaybackAnimation[] = []
|
|
@@ -188,12 +198,13 @@ export function motionStates<
|
|
|
188
198
|
if (view)
|
|
189
199
|
for (const [, rows] of states)
|
|
190
200
|
for (const [slot, frames] of rows)
|
|
191
|
-
if (slot === target)
|
|
201
|
+
if (slot === target)
|
|
202
|
+
prepareAnimation(view, frames, timingFor(defaults, target))
|
|
192
203
|
views.set(target, view)
|
|
193
204
|
const frames = view ? framesFor(current, target) : undefined
|
|
194
205
|
if (!view || !frames) return
|
|
195
206
|
if (active) {
|
|
196
|
-
const child = prepareAnimation(view, frames, active.timing)()
|
|
207
|
+
const child = prepareAnimation(view, frames, timingFor(active.timing, target))()
|
|
197
208
|
active.children.push(child)
|
|
198
209
|
if (paused) child.pause()
|
|
199
210
|
} else set(view, frames as Values)
|
|
@@ -231,8 +242,59 @@ export function motionStates<
|
|
|
231
242
|
return options.scope ? options.scope.add(controller) : controller
|
|
232
243
|
}
|
|
233
244
|
|
|
234
|
-
|
|
235
|
-
|
|
245
|
+
const curveKeys = new Set([
|
|
246
|
+
'type',
|
|
247
|
+
'delay',
|
|
248
|
+
'duration',
|
|
249
|
+
'ease',
|
|
250
|
+
'times',
|
|
251
|
+
'stiffness',
|
|
252
|
+
'damping',
|
|
253
|
+
'mass',
|
|
254
|
+
'bounce',
|
|
255
|
+
'velocity',
|
|
256
|
+
])
|
|
257
|
+
|
|
258
|
+
type Curve = Exclude<StateCurve, SpringName>
|
|
259
|
+
interface Timing {
|
|
260
|
+
readonly main: StateCurve
|
|
261
|
+
readonly targets: ReadonlyMap<string, StateCurve>
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** The transition one target plays: its own merged over the main one, or the main one. */
|
|
265
|
+
function timingFor(timing: Timing, target: string): StateCurve {
|
|
266
|
+
const own = timing.targets.get(target)
|
|
267
|
+
if (own === undefined) return timing.main
|
|
268
|
+
const main = typeof timing.main === 'string' ? springs[timing.main] : timing.main
|
|
269
|
+
return mergeTransitions(main, typeof own === 'string' ? springs[own] : own) as Curve
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const snappy: Timing = { main: 'snappy', targets: new Map() }
|
|
273
|
+
|
|
274
|
+
/** A transition that only names targets keeps the fallback for everything else. */
|
|
275
|
+
function normalize(
|
|
276
|
+
transition: StateTransition | undefined,
|
|
277
|
+
targets: ReadonlyMap<string, unknown>,
|
|
278
|
+
fallback = snappy,
|
|
279
|
+
): Timing {
|
|
280
|
+
if (transition === undefined) return fallback
|
|
281
|
+
if (typeof transition === 'string')
|
|
282
|
+
return { main: curve(transition), targets: new Map() }
|
|
283
|
+
if (!transition || typeof transition !== 'object')
|
|
284
|
+
throw new TypeError('Invalid state transition.')
|
|
285
|
+
const main: Record<string, unknown> = {}
|
|
286
|
+
const own = new Map<string, StateCurve>()
|
|
287
|
+
for (const [key, value] of Object.entries(transition)) {
|
|
288
|
+
if (targets.has(key)) {
|
|
289
|
+
if (value !== undefined) own.set(key, curve(value as StateCurve))
|
|
290
|
+
} else main[key] = value
|
|
291
|
+
}
|
|
292
|
+
if (own.size && !Object.keys(main).length)
|
|
293
|
+
return { main: fallback.main, targets: new Map([...fallback.targets, ...own]) }
|
|
294
|
+
return { main: curve(main as Curve), targets: own }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function curve(transition: StateCurve): StateCurve {
|
|
236
298
|
if (typeof transition === 'string') {
|
|
237
299
|
if (!['smooth', 'snappy', 'bouncy'].includes(transition))
|
|
238
300
|
throw new TypeError('Unknown state spring.')
|
|
@@ -240,20 +302,8 @@ function normalize(transition: StateTransition | undefined): StateTransition {
|
|
|
240
302
|
}
|
|
241
303
|
if (!transition || typeof transition !== 'object')
|
|
242
304
|
throw new TypeError('Invalid state transition.')
|
|
243
|
-
const allowed = new Set([
|
|
244
|
-
'type',
|
|
245
|
-
'delay',
|
|
246
|
-
'duration',
|
|
247
|
-
'ease',
|
|
248
|
-
'times',
|
|
249
|
-
'stiffness',
|
|
250
|
-
'damping',
|
|
251
|
-
'mass',
|
|
252
|
-
'bounce',
|
|
253
|
-
'velocity',
|
|
254
|
-
])
|
|
255
305
|
for (const key of Object.keys(transition))
|
|
256
|
-
if (!
|
|
306
|
+
if (!curveKeys.has(key))
|
|
257
307
|
throw new TypeError(`State transitions do not support "${key}".`)
|
|
258
308
|
if (
|
|
259
309
|
transition.duration !== undefined &&
|