@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.
Files changed (79) hide show
  1. package/README.md +38 -7
  2. package/dist/animate.d.ts +14 -4
  3. package/dist/animate.d.ts.map +1 -1
  4. package/dist/animate.js +50 -34
  5. package/dist/animate.js.map +1 -1
  6. package/dist/drag.d.ts +17 -3
  7. package/dist/drag.d.ts.map +1 -1
  8. package/dist/drag.js +34 -11
  9. package/dist/drag.js.map +1 -1
  10. package/dist/easing.d.ts +1 -1
  11. package/dist/easing.d.ts.map +1 -1
  12. package/dist/easing.js +11 -0
  13. package/dist/easing.js.map +1 -1
  14. package/dist/engine.d.ts +18 -1
  15. package/dist/engine.d.ts.map +1 -1
  16. package/dist/engine.js +143 -36
  17. package/dist/engine.js.map +1 -1
  18. package/dist/gradient.d.ts.map +1 -1
  19. package/dist/gradient.js +10 -4
  20. package/dist/gradient.js.map +1 -1
  21. package/dist/index.d.ts +4 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +1 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/keyboard.d.ts.map +1 -1
  26. package/dist/keyboard.js +63 -10
  27. package/dist/keyboard.js.map +1 -1
  28. package/dist/mixer.d.ts +49 -0
  29. package/dist/mixer.d.ts.map +1 -0
  30. package/dist/mixer.js +238 -0
  31. package/dist/mixer.js.map +1 -0
  32. package/dist/motion-scope.d.ts +4 -1
  33. package/dist/motion-scope.d.ts.map +1 -1
  34. package/dist/motion-scope.js +8 -3
  35. package/dist/motion-scope.js.map +1 -1
  36. package/dist/motion-states.d.ts +10 -5
  37. package/dist/motion-states.d.ts.map +1 -1
  38. package/dist/motion-states.js +55 -22
  39. package/dist/motion-states.js.map +1 -1
  40. package/dist/motion-value.d.ts +115 -0
  41. package/dist/motion-value.d.ts.map +1 -0
  42. package/dist/motion-value.js +437 -0
  43. package/dist/motion-value.js.map +1 -0
  44. package/dist/path.d.ts.map +1 -1
  45. package/dist/path.js +2 -2
  46. package/dist/path.js.map +1 -1
  47. package/dist/track.d.ts.map +1 -1
  48. package/dist/track.js +31 -12
  49. package/dist/track.js.map +1 -1
  50. package/dist/transition.d.ts +15 -4
  51. package/dist/transition.d.ts.map +1 -1
  52. package/dist/transition.js +46 -7
  53. package/dist/transition.js.map +1 -1
  54. package/dist/types.d.ts +5 -0
  55. package/dist/types.d.ts.map +1 -1
  56. package/dist/vue/index.d.ts +28 -2
  57. package/dist/vue/index.d.ts.map +1 -1
  58. package/dist/vue/index.js +78 -2
  59. package/dist/vue/index.js.map +1 -1
  60. package/package.json +1 -1
  61. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFrame.java +53 -0
  62. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFrameFollower.java +57 -0
  63. package/src/android-plugin.d.ts +27 -0
  64. package/src/animate.ts +99 -16
  65. package/src/drag.ts +67 -21
  66. package/src/easing.ts +14 -0
  67. package/src/engine.ts +158 -53
  68. package/src/gradient.ts +10 -4
  69. package/src/index.ts +18 -1
  70. package/src/keyboard.ts +83 -18
  71. package/src/mixer.ts +264 -0
  72. package/src/motion-scope.ts +12 -4
  73. package/src/motion-states.ts +79 -29
  74. package/src/motion-value.ts +563 -0
  75. package/src/path.ts +2 -1
  76. package/src/track.ts +37 -12
  77. package/src/transition.ts +69 -10
  78. package/src/types.ts +5 -0
  79. 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
+ }
@@ -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
- return startOwned(() => animate(...args))
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
  },
@@ -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
- export type StateTransition =
25
+ type StateCurve =
25
26
  | SpringName
26
27
  | Omit<Spring, 'repeat' | 'repeatType'>
27
28
  | Omit<Tween, 'repeat' | 'repeatType'>
28
- export interface MotionStatesOptions {
29
- transition?: StateTransition
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
- | { handle: Animation; children: PlaybackAnimation[]; timing: StateTransition }
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 === undefined ? defaults : normalize(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) prepareAnimation(view, frames, defaults)
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
- function normalize(transition: StateTransition | undefined): StateTransition {
235
- if (transition === undefined) return 'snappy'
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 (!allowed.has(key))
306
+ if (!curveKeys.has(key))
257
307
  throw new TypeError(`State transitions do not support "${key}".`)
258
308
  if (
259
309
  transition.duration !== undefined &&