@rafaelaugusto/jolt 0.1.0-beta.10 → 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 (92) hide show
  1. package/README.md +55 -6
  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/driver/android-gradient.d.ts +5 -0
  11. package/dist/driver/android-gradient.d.ts.map +1 -0
  12. package/dist/driver/android-gradient.js +89 -0
  13. package/dist/driver/android-gradient.js.map +1 -0
  14. package/dist/driver/ios-gradient.d.ts +5 -0
  15. package/dist/driver/ios-gradient.d.ts.map +1 -0
  16. package/dist/driver/ios-gradient.js +109 -0
  17. package/dist/driver/ios-gradient.js.map +1 -0
  18. package/dist/easing.d.ts +1 -1
  19. package/dist/easing.d.ts.map +1 -1
  20. package/dist/easing.js +11 -0
  21. package/dist/easing.js.map +1 -1
  22. package/dist/engine.d.ts +18 -1
  23. package/dist/engine.d.ts.map +1 -1
  24. package/dist/engine.js +143 -36
  25. package/dist/engine.js.map +1 -1
  26. package/dist/gradient.d.ts +90 -0
  27. package/dist/gradient.d.ts.map +1 -0
  28. package/dist/gradient.js +253 -0
  29. package/dist/gradient.js.map +1 -0
  30. package/dist/index.d.ts +7 -3
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +3 -1
  33. package/dist/index.js.map +1 -1
  34. package/dist/keyboard.d.ts.map +1 -1
  35. package/dist/keyboard.js +63 -10
  36. package/dist/keyboard.js.map +1 -1
  37. package/dist/mixer.d.ts +49 -0
  38. package/dist/mixer.d.ts.map +1 -0
  39. package/dist/mixer.js +238 -0
  40. package/dist/mixer.js.map +1 -0
  41. package/dist/motion-scope.d.ts +4 -1
  42. package/dist/motion-scope.d.ts.map +1 -1
  43. package/dist/motion-scope.js +8 -3
  44. package/dist/motion-scope.js.map +1 -1
  45. package/dist/motion-states.d.ts +18 -6
  46. package/dist/motion-states.d.ts.map +1 -1
  47. package/dist/motion-states.js +114 -26
  48. package/dist/motion-states.js.map +1 -1
  49. package/dist/motion-value.d.ts +115 -0
  50. package/dist/motion-value.d.ts.map +1 -0
  51. package/dist/motion-value.js +437 -0
  52. package/dist/motion-value.js.map +1 -0
  53. package/dist/path.d.ts.map +1 -1
  54. package/dist/path.js +2 -2
  55. package/dist/path.js.map +1 -1
  56. package/dist/track.d.ts.map +1 -1
  57. package/dist/track.js +31 -12
  58. package/dist/track.js.map +1 -1
  59. package/dist/transition.d.ts +30 -4
  60. package/dist/transition.d.ts.map +1 -1
  61. package/dist/transition.js +63 -7
  62. package/dist/transition.js.map +1 -1
  63. package/dist/types.d.ts +5 -0
  64. package/dist/types.d.ts.map +1 -1
  65. package/dist/vue/index.d.ts +28 -2
  66. package/dist/vue/index.d.ts.map +1 -1
  67. package/dist/vue/index.js +78 -2
  68. package/dist/vue/index.js.map +1 -1
  69. package/package.json +1 -1
  70. package/platforms/android/java/org/rafaelaugusto/jolt/GradientLayer.java +195 -0
  71. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFrame.java +53 -0
  72. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFrameFollower.java +57 -0
  73. package/platforms/android/proguard-rules.pro +2 -0
  74. package/src/android-plugin.d.ts +56 -0
  75. package/src/animate.ts +99 -16
  76. package/src/drag.ts +67 -21
  77. package/src/driver/android-gradient.ts +109 -0
  78. package/src/driver/ios-gradient.ts +114 -0
  79. package/src/easing.ts +14 -0
  80. package/src/engine.ts +158 -53
  81. package/src/gradient.ts +378 -0
  82. package/src/index.ts +34 -3
  83. package/src/keyboard.ts +83 -18
  84. package/src/mixer.ts +264 -0
  85. package/src/motion-scope.ts +14 -6
  86. package/src/motion-states.ts +145 -41
  87. package/src/motion-value.ts +563 -0
  88. package/src/path.ts +2 -1
  89. package/src/track.ts +37 -12
  90. package/src/transition.ts +99 -10
  91. package/src/types.ts +5 -0
  92. package/src/vue/index.ts +98 -1
package/src/transition.ts CHANGED
@@ -12,10 +12,15 @@ export interface Timing {
12
12
  repeat?: number
13
13
  /** `reverse` plays every other repeat backwards. */
14
14
  repeatType?: 'loop' | 'reverse'
15
- /** Milliseconds to hold the end value before each repeat. The first play doesn't wait. */
16
- repeatDelay?: number
15
+ /**
16
+ * Milliseconds to hold the end value before each repeat. The first play doesn't wait.
17
+ * `[min, max]` draws a new pause for each repeat.
18
+ */
19
+ repeatDelay?: number | RepeatDelayRange
17
20
  }
18
21
 
22
+ export type RepeatDelayRange = readonly [min: number, max: number]
23
+
19
24
  export interface Tween extends Timing {
20
25
  type?: 'tween'
21
26
  /** Milliseconds. */
@@ -54,6 +59,36 @@ export const springs: Record<SpringName, Spring> = {
54
59
  bouncy: { type: 'spring', duration: 550, bounce: 0.4 },
55
60
  }
56
61
 
62
+ export interface OrigamiSpring {
63
+ /** React Native's default is 7. */
64
+ friction?: number
65
+ /** React Native's default is 40. */
66
+ tension?: number
67
+ }
68
+
69
+ /**
70
+ * The spring React Native's `Animated.spring` plays for `friction` and `tension`,
71
+ * converted with the same Origami formulas.
72
+ *
73
+ * ```ts
74
+ * animate(card, { x: 100 }, origamiSpring({ friction: 7, tension: 40 }))
75
+ * ```
76
+ */
77
+ export function origamiSpring({
78
+ friction = 7,
79
+ tension = 40,
80
+ }: OrigamiSpring = {}): Spring {
81
+ if (!Number.isFinite(friction) || !Number.isFinite(tension))
82
+ throw new RangeError('Origami friction and tension must be finite.')
83
+ const stiffness = (tension - 30) * 3.62 + 194
84
+ const damping = (friction - 8) * 3 + 25
85
+ if (stiffness <= 0 || damping < 0)
86
+ throw new RangeError(
87
+ `Origami tension ${tension} and friction ${friction} give no spring (stiffness ${stiffness}, damping ${damping}).`,
88
+ )
89
+ return { type: 'spring', stiffness, damping, mass: 1 }
90
+ }
91
+
57
92
  export type Curve =
58
93
  | DragCurve
59
94
  | {
@@ -77,7 +112,9 @@ export interface ResolvedTransition {
77
112
  delay: number
78
113
  repeat: number
79
114
  reverse: boolean
80
- repeatDelay?: number
115
+ repeatDelay?: number | RepeatDelayRange
116
+ /** Draws the pauses of a `repeatDelay` range; tracks of one animation share it. */
117
+ seed?: number
81
118
  }
82
119
 
83
120
  const springKeys = ['stiffness', 'damping', 'mass', 'bounce', 'velocity'] as const
@@ -107,11 +144,53 @@ export function timingOf(transition: Transition): Timing {
107
144
  return timing
108
145
  }
109
146
 
110
- /** Timing merges key by key, the curve is taken whole from whichever side has one. */
147
+ const isSpring = (transition: Transition) =>
148
+ transition.type === 'spring' ||
149
+ springKeys.some((key) => (transition as Spring)[key] !== undefined)
150
+
151
+ /** Curve keys a merged transition took from the main one rather than from its override. */
152
+ const inherited = new WeakMap<Transition, ReadonlySet<string>>()
153
+
154
+ /**
155
+ * Timing merges key by key. Over a tween, a curve that stays a tween merges key by key too;
156
+ * otherwise the curve is taken whole from whichever side has one.
157
+ */
111
158
  export function mergeTransitions(base: Transition, override: Transition): Transition {
112
- return hasCurve(override)
113
- ? { ...timingOf(base), ...override }
114
- : { ...base, ...timingOf(override) }
159
+ if (!hasCurve(override)) {
160
+ const merged = { ...base, ...timingOf(override) }
161
+ const taken = inherited.get(base)
162
+ if (taken) inherited.set(merged, taken)
163
+ return merged
164
+ }
165
+ if (!hasCurve(base) || isSpring(base) || isSpring(override))
166
+ return { ...timingOf(base), ...override }
167
+ const merged = { ...base, ...override } as Transition
168
+ const own = override as Record<string, unknown>
169
+ inherited.set(
170
+ merged,
171
+ new Set(
172
+ curveKeys.filter(
173
+ (key) =>
174
+ (base as Record<string, unknown>)[key] !== undefined && own[key] === undefined,
175
+ ),
176
+ ),
177
+ )
178
+ return merged
179
+ }
180
+
181
+ /** Drops inherited `times` and per-segment easings that don't fit this property's keyframes. */
182
+ export function fitTransition(transition: Transition, keyframes: number): Transition {
183
+ const taken = inherited.get(transition)
184
+ if (!taken) return transition
185
+ const tween = { ...transition } as Tween
186
+ const segments = (ease: Tween['ease']) =>
187
+ ease !== undefined && isEasingList(ease) ? ease.length : undefined
188
+ if (taken.has('times') && tween.times && tween.times.length !== keyframes)
189
+ delete tween.times
190
+ if (taken.has('ease') && (segments(tween.ease) ?? keyframes - 1) !== keyframes - 1)
191
+ delete tween.ease
192
+ if (taken.has('easeTrack') && segments(tween.ease) !== undefined) delete tween.easeTrack
193
+ return tween
115
194
  }
116
195
 
117
196
  export function resolveTransition(
@@ -137,13 +216,23 @@ export function resolveTransition(
137
216
  if (repeat < 0 || Number.isNaN(repeat))
138
217
  throw new RangeError(`Invalid repeat: ${repeat}.`)
139
218
  const repeatDelay = transition.repeatDelay ?? 0
140
- if (!Number.isFinite(repeatDelay) || repeatDelay < 0)
141
- throw new RangeError(`Invalid repeat delay: ${repeatDelay}.`)
219
+ const [shortest, longest] =
220
+ typeof repeatDelay === 'number' ? [repeatDelay, repeatDelay] : repeatDelay
221
+ if (
222
+ (typeof repeatDelay !== 'number' &&
223
+ (!Array.isArray(repeatDelay) || repeatDelay.length !== 2)) ||
224
+ !Number.isFinite(shortest) ||
225
+ !Number.isFinite(longest) ||
226
+ shortest! < 0 ||
227
+ shortest! > longest!
228
+ )
229
+ throw new RangeError(`Invalid repeat delay: ${JSON.stringify(repeatDelay)}.`)
142
230
  const timing = {
143
231
  delay,
144
232
  repeat,
145
233
  reverse: transition.repeatType === 'reverse',
146
- ...(repeatDelay > 0 && repeat > 0 && { repeatDelay }),
234
+ ...(longest! > 0 && repeat > 0 && { repeatDelay }),
235
+ ...(longest! > shortest! && repeat > 0 && { seed: Math.random() }),
147
236
  }
148
237
 
149
238
  if (type === 'spring') {
package/src/types.ts CHANGED
@@ -86,4 +86,9 @@ export type AnimateOptions = Transition & {
86
86
  stagger?: number | Stagger
87
87
  /** iOS impact feedback at arrival, or at explicit points in a finite animation. */
88
88
  haptic?: Haptic
89
+ /**
90
+ * Plays on a named layer, on top of the property's other animations instead of replacing
91
+ * them. Translations and rotations add; scale and opacity multiply.
92
+ */
93
+ layer?: string
89
94
  }
package/src/vue/index.ts CHANGED
@@ -15,10 +15,19 @@ import {
15
15
  type Ref,
16
16
  } from 'nativescript-vue'
17
17
  import {
18
+ gradient,
18
19
  press,
19
20
  motionScope,
21
+ bind,
22
+ motionValue,
23
+ scrollValue,
20
24
  onKeyboard,
21
25
  onReducedMotion,
26
+ stop,
27
+ type Bindings,
28
+ type MotionValue,
29
+ type ScrollValue,
30
+ type Target,
22
31
  reducedMotion,
23
32
  type KeyboardState,
24
33
  type MotionScope,
@@ -27,6 +36,9 @@ import {
27
36
  type PlaybackAnimation,
28
37
  type PresetName,
29
38
  type SpringName,
39
+ type Gradient,
40
+ type GradientOptions,
41
+ type Transition,
30
42
  } from '../index.js'
31
43
  import { currentKeyboard } from '../keyboard.js'
32
44
 
@@ -37,6 +49,11 @@ type Options = AnimateOptions | SpringName
37
49
  export type AnimateValue =
38
50
  PresetName | Keyframes | readonly [animation: PresetName | Keyframes, options?: Options]
39
51
 
52
+ /** The gradient, alone or paired with the transition for later changes. */
53
+ export type GradientValue =
54
+ | GradientOptions
55
+ | readonly [gradient: GradientOptions, transition?: Transition | SpringName]
56
+
40
57
  /** The pressed keyframes, alone or paired with a transition. */
41
58
  export type PressValue = Keyframes | readonly [pressed: Keyframes, options?: Options]
42
59
 
@@ -53,6 +70,7 @@ export interface TransitionProps {
53
70
  const animations = new WeakMap<object, PlaybackAnimation>()
54
71
  const scopes = new WeakMap<object, MotionScope>()
55
72
  const removers = new WeakMap<object, () => void>()
73
+ const gradients = new WeakMap<object, { gradient: Gradient; colors: number }>()
56
74
 
57
75
  function split<T>(
58
76
  value: T | readonly [T, Options?],
@@ -137,6 +155,47 @@ export const vPress: ObjectDirective<Host, PressValue | undefined> = {
137
155
  },
138
156
  }
139
157
 
158
+ function paint(el: Host, value: GradientValue, animated: boolean) {
159
+ const [options, transition] = (Array.isArray(value) ? value : [value]) as [
160
+ GradientOptions,
161
+ (Transition | SpringName)?,
162
+ ]
163
+ const current = gradients.get(el)
164
+ const colors = options.colors.length
165
+ if (current && current.gradient.type === options.type) {
166
+ const { type, ...values } = options
167
+ if (animated && current.colors === colors)
168
+ current.gradient.animate(values, transition)
169
+ else current.gradient.set(values)
170
+ current.colors = colors
171
+ return
172
+ }
173
+ current?.gradient.remove()
174
+ gradients.set(el, { gradient: gradient(el, options), colors })
175
+ }
176
+
177
+ /**
178
+ * Paints a native gradient behind the element, and animates to the new values when they change.
179
+ * A change of type, or of the number of colors, applies at once.
180
+ *
181
+ * ```vue
182
+ * <GridLayout v-gradient="{ type: 'linear', colors: ['#18181B', '#3F3F46'], angle: 135 }" />
183
+ * <GridLayout v-gradient="[{ type: 'linear', colors: active ? hot : cold }, 'snappy']" />
184
+ * ```
185
+ */
186
+ export const vGradient: ObjectDirective<Host, GradientValue> = {
187
+ mounted(el, { value }) {
188
+ paint(el, value, false)
189
+ },
190
+ updated(el, { value, oldValue }) {
191
+ if (!same(value, oldValue)) paint(el, value, true)
192
+ },
193
+ unmounted(el) {
194
+ gradients.get(el)?.gradient.remove()
195
+ gradients.delete(el)
196
+ },
197
+ }
198
+
140
199
  /** The hooks `JoltTransition` hands to Vue's `BaseTransition`. */
141
200
  export function transitionHooks(props: TransitionProps): BaseTransitionProps<Host> {
142
201
  const run = (value: AnimateValue | undefined) => (el: Host, done: () => void) => {
@@ -195,10 +254,11 @@ export const JoltTransition = defineComponent({
195
254
  },
196
255
  })
197
256
 
198
- /** Registers `v-animate`, `v-press` and `JoltTransition` on the app. */
257
+ /** Registers `v-animate`, `v-press`, `v-gradient` and `JoltTransition` on the app. */
199
258
  export function install(app: App) {
200
259
  app.directive('animate', vAnimate)
201
260
  app.directive('press', vPress)
261
+ app.directive('gradient', vGradient)
202
262
  app.component('JoltTransition', JoltTransition)
203
263
  }
204
264
 
@@ -229,3 +289,40 @@ export function useKeyboard(): Readonly<Ref<KeyboardState>> {
229
289
  onUnmounted(off)
230
290
  return readonly(keyboard) as Readonly<Ref<KeyboardState>>
231
291
  }
292
+
293
+ /** Call in setup(): a motion value that stops when the component unmounts. */
294
+ export function useMotionValue(initial = 0): MotionValue {
295
+ const value = motionValue(initial)
296
+ onUnmounted(() => stop(value))
297
+ return value
298
+ }
299
+
300
+ /**
301
+ * Call in setup(): a template ref that binds motion values to the element on mount
302
+ * and releases them on unmount. A recreated element picks the values up again.
303
+ */
304
+ export function useBinding(bindings: Bindings): (element: Target | null) => void {
305
+ let element: Target | null = null
306
+ let release: (() => void) | undefined
307
+ onUnmounted(() => {
308
+ release?.()
309
+ release = undefined
310
+ })
311
+ return (next) => {
312
+ if (next === element) return
313
+ release?.()
314
+ release = undefined
315
+ element = next
316
+ if (next) release = bind(next, bindings)
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Call in setup(): a scroll offset as a value. Pass its `ref` to the ScrollView's
322
+ * template ref; it stops following when the component unmounts.
323
+ */
324
+ export function useScrollValue(options?: { axis?: 'x' | 'y' }): ScrollValue {
325
+ const value = scrollValue(null, options)
326
+ onUnmounted(() => value.dispose())
327
+ return value
328
+ }