@rafaelaugusto/jolt 0.0.0-stage → 0.1.0-beta.0

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 (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +256 -2
  3. package/dist/animate.d.ts +26 -0
  4. package/dist/animate.d.ts.map +1 -0
  5. package/dist/animate.js +153 -0
  6. package/dist/animate.js.map +1 -0
  7. package/dist/driver/android.d.ts +3 -0
  8. package/dist/driver/android.d.ts.map +1 -0
  9. package/dist/driver/android.js +73 -0
  10. package/dist/driver/android.js.map +1 -0
  11. package/dist/driver/index.d.ts +18 -0
  12. package/dist/driver/index.d.ts.map +1 -0
  13. package/dist/driver/index.js +15 -0
  14. package/dist/driver/index.js.map +1 -0
  15. package/dist/driver/ios.d.ts +3 -0
  16. package/dist/driver/ios.d.ts.map +1 -0
  17. package/dist/driver/ios.js +68 -0
  18. package/dist/driver/ios.js.map +1 -0
  19. package/dist/easing.d.ts +8 -0
  20. package/dist/easing.d.ts.map +1 -0
  21. package/dist/easing.js +71 -0
  22. package/dist/easing.js.map +1 -0
  23. package/dist/engine.d.ts +47 -0
  24. package/dist/engine.d.ts.map +1 -0
  25. package/dist/engine.js +168 -0
  26. package/dist/engine.js.map +1 -0
  27. package/dist/index.d.ts +13 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +7 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/presets.d.ts +295 -0
  32. package/dist/presets.d.ts.map +1 -0
  33. package/dist/presets.js +123 -0
  34. package/dist/presets.js.map +1 -0
  35. package/dist/press.d.ts +13 -0
  36. package/dist/press.d.ts.map +1 -0
  37. package/dist/press.js +32 -0
  38. package/dist/press.js.map +1 -0
  39. package/dist/properties.d.ts +8 -0
  40. package/dist/properties.d.ts.map +1 -0
  41. package/dist/properties.js +42 -0
  42. package/dist/properties.js.map +1 -0
  43. package/dist/sequence.d.ts +25 -0
  44. package/dist/sequence.d.ts.map +1 -0
  45. package/dist/sequence.js +68 -0
  46. package/dist/sequence.js.map +1 -0
  47. package/dist/spring.d.ts +22 -0
  48. package/dist/spring.d.ts.map +1 -0
  49. package/dist/spring.js +54 -0
  50. package/dist/spring.js.map +1 -0
  51. package/dist/track.d.ts +28 -0
  52. package/dist/track.d.ts.map +1 -0
  53. package/dist/track.js +103 -0
  54. package/dist/track.js.map +1 -0
  55. package/dist/transition.d.ts +58 -0
  56. package/dist/transition.d.ts.map +1 -0
  57. package/dist/transition.js +82 -0
  58. package/dist/transition.js.map +1 -0
  59. package/dist/types.d.ts +38 -0
  60. package/dist/types.d.ts.map +1 -0
  61. package/dist/types.js +2 -0
  62. package/dist/types.js.map +1 -0
  63. package/package.json +63 -4
  64. package/src/animate.ts +219 -0
  65. package/src/driver/android.ts +101 -0
  66. package/src/driver/index.ts +32 -0
  67. package/src/driver/ios.ts +88 -0
  68. package/src/easing.ts +100 -0
  69. package/src/engine.ts +208 -0
  70. package/src/index.ts +12 -0
  71. package/src/presets.ts +134 -0
  72. package/src/press.ts +43 -0
  73. package/src/properties.ts +51 -0
  74. package/src/sequence.ts +85 -0
  75. package/src/spring.ts +95 -0
  76. package/src/track.ts +163 -0
  77. package/src/transition.ts +160 -0
  78. package/src/types.ts +39 -0
package/src/track.ts ADDED
@@ -0,0 +1,163 @@
1
+ import { linear } from './easing.js'
2
+ import { precision, type Property } from './properties.js'
3
+ import { solveSpring } from './spring.js'
4
+ import type { Curve, ResolvedTransition } from './transition.js'
5
+
6
+ /**
7
+ * One property animated over time. The curve is computed here and sampled,
8
+ * so the platform plays plain keyframes and nothing runs in JavaScript per frame.
9
+ */
10
+ export interface Track {
11
+ readonly property: Property
12
+ readonly delay: number
13
+ /** One iteration, in milliseconds. */
14
+ readonly duration: number
15
+ readonly repeat: number
16
+ readonly reverse: boolean
17
+ /** Offsets from 0 to 1 within one iteration, paired with `values`. */
18
+ readonly times: readonly number[]
19
+ readonly values: readonly number[]
20
+ /** Where the property is left once the track is over. */
21
+ readonly to: number
22
+ /** Milliseconds from the start until the end of the last iteration. */
23
+ readonly end: number
24
+ /** When it looks done: a spring is close to its target and stays there, long before it settles. */
25
+ readonly arrival: number
26
+ valueAt(elapsed: number): number
27
+ /** Units per second. */
28
+ velocityAt(elapsed: number): number
29
+ }
30
+
31
+ interface Motion {
32
+ duration: number
33
+ position(time: number): number
34
+ velocity(time: number): number
35
+ times: number[]
36
+ values: number[]
37
+ }
38
+
39
+ const SAMPLES_PER_SECOND = 60
40
+ const ARRIVAL_TOLERANCE = 0.05
41
+
42
+ export function createTrack(
43
+ property: Property,
44
+ keyframes: readonly number[],
45
+ { curve, delay, repeat, reverse }: ResolvedTransition,
46
+ velocity: number,
47
+ ): Track {
48
+ const motion =
49
+ curve.type === 'spring'
50
+ ? spring(property, keyframes, curve, velocity)
51
+ : tween(keyframes, curve)
52
+ const { duration } = motion
53
+ const first = motion.values[0]!
54
+ const last = motion.values.at(-1)!
55
+ const finite = Number.isFinite(repeat)
56
+ const endsBackwards = reverse && finite && repeat % 2 === 1
57
+ const end = delay + (duration === 0 ? 0 : duration * (repeat + 1))
58
+
59
+ const localTime = (elapsed: number): [time: number, direction: number] => {
60
+ const time = elapsed - delay
61
+ if (time <= 0) return [0, 1]
62
+ if (time >= end - delay) return [endsBackwards ? 0 : duration, 1]
63
+ const iteration = Math.floor(time / duration)
64
+ const local = time - iteration * duration
65
+ return reverse && iteration % 2 === 1 ? [duration - local, -1] : [local, 1]
66
+ }
67
+
68
+ return {
69
+ property,
70
+ delay,
71
+ duration,
72
+ repeat,
73
+ reverse,
74
+ times: motion.times,
75
+ values: motion.values,
76
+ to: !finite || endsBackwards ? first : last,
77
+ end,
78
+ arrival: curve.type === 'spring' && repeat === 0 ? delay + arrival(motion) : end,
79
+ valueAt: (elapsed) => motion.position(localTime(elapsed)[0]),
80
+ velocityAt: (elapsed) => {
81
+ if (elapsed <= delay || elapsed >= end) return 0
82
+ const [time, direction] = localTime(elapsed)
83
+ return motion.velocity(time) * direction
84
+ },
85
+ }
86
+ }
87
+
88
+ function arrival({ times, values, duration }: Motion) {
89
+ const target = values.at(-1)!
90
+ const tolerance =
91
+ Math.max(...values.map((value) => Math.abs(value - target))) * ARRIVAL_TOLERANCE
92
+ let i = values.length - 1
93
+ while (i > 0 && Math.abs(values[i - 1]! - target) <= tolerance) i--
94
+ return times[i]! * duration
95
+ }
96
+
97
+ function spring(
98
+ property: Property,
99
+ [from, to]: readonly number[],
100
+ curve: Extract<Curve, { type: 'spring' }>,
101
+ velocity: number,
102
+ ): Motion {
103
+ const motion = solveSpring(
104
+ curve.physics,
105
+ from!,
106
+ to!,
107
+ curve.velocity ?? velocity,
108
+ precision[property],
109
+ )
110
+ const steps = Math.max(1, Math.ceil((motion.duration / 1000) * SAMPLES_PER_SECOND))
111
+ const times: number[] = []
112
+ const values: number[] = []
113
+ for (let i = 0; i <= steps; i++) {
114
+ times.push(i / steps)
115
+ values.push(i === steps ? to! : motion.position((i / steps) * motion.duration))
116
+ }
117
+ return { ...motion, times, values }
118
+ }
119
+
120
+ function tween(
121
+ keyframes: readonly number[],
122
+ curve: Extract<Curve, { type: 'tween' }>,
123
+ ): Motion {
124
+ const { duration, ease } = curve
125
+ const offsets = curve.times ?? keyframes.map((_, i) => i / (keyframes.length - 1))
126
+
127
+ const position = (time: number) => {
128
+ const progress = duration > 0 ? Math.min(Math.max(time / duration, 0), 1) : 1
129
+ let i = 0
130
+ while (i < offsets.length - 2 && progress > offsets[i + 1]!) i++
131
+ const start = offsets[i]!
132
+ const span = offsets[i + 1]! - start
133
+ const local = span > 0 ? (progress - start) / span : 1
134
+ const from = keyframes[i]!
135
+ return from + (keyframes[i + 1]! - from) * ease[i]!(local)
136
+ }
137
+
138
+ const times = [0]
139
+ const values = [keyframes[0]!]
140
+ for (let i = 0; i < keyframes.length - 1; i++) {
141
+ const start = offsets[i]!
142
+ const span = offsets[i + 1]! - start
143
+ const from = keyframes[i]!
144
+ const to = keyframes[i + 1]!
145
+ const easing = ease[i]!
146
+ const steps =
147
+ easing === linear || from === to
148
+ ? 1
149
+ : Math.max(1, Math.ceil(((span * duration) / 1000) * SAMPLES_PER_SECOND))
150
+ for (let step = 1; step <= steps; step++) {
151
+ times.push(start + (span * step) / steps)
152
+ values.push(step === steps ? to : from + (to - from) * easing(step / steps))
153
+ }
154
+ }
155
+
156
+ return {
157
+ duration,
158
+ position,
159
+ velocity: (time) => (position(time + 0.5) - position(time - 0.5)) * 1000,
160
+ times,
161
+ values,
162
+ }
163
+ }
@@ -0,0 +1,160 @@
1
+ import { resolveEasing, type Easing, type EasingFunction } from './easing.js'
2
+ import type { Property } from './properties.js'
3
+ import { springPhysics, type SpringPhysics } from './spring.js'
4
+
5
+ export interface Timing {
6
+ /** Milliseconds to wait before starting. The first keyframe is applied right away. */
7
+ delay?: number
8
+ /** How many more times to play it. `Infinity` keeps going until stopped. */
9
+ repeat?: number
10
+ /** `reverse` plays every other repeat backwards. */
11
+ repeatType?: 'loop' | 'reverse'
12
+ }
13
+
14
+ export interface Tween extends Timing {
15
+ type?: 'tween'
16
+ /** Milliseconds. */
17
+ duration?: number
18
+ /** One easing, or one per segment between keyframes. */
19
+ ease?: Easing | readonly Easing[]
20
+ /** Where each keyframe sits, from 0 to 1. Evenly spaced by default. */
21
+ times?: readonly number[]
22
+ }
23
+
24
+ export interface Spring extends Timing {
25
+ type?: 'spring'
26
+ stiffness?: number
27
+ damping?: number
28
+ mass?: number
29
+ /** From -1 to 1. 0 settles without overshooting, higher values bounce more. */
30
+ bounce?: number
31
+ /** How long the spring feels, in milliseconds. Goes with `bounce`, ignored with `stiffness`, `damping` or `mass`. */
32
+ duration?: number
33
+ /** Units per second. Defaults to the velocity of the animation it interrupts. */
34
+ velocity?: number
35
+ }
36
+
37
+ export type Transition = Tween | Spring
38
+
39
+ export type SpringName = 'smooth' | 'snappy' | 'bouncy'
40
+
41
+ export const springs: Record<SpringName, Spring> = {
42
+ smooth: { type: 'spring', duration: 500, bounce: 0 },
43
+ snappy: { type: 'spring', duration: 400, bounce: 0.15 },
44
+ bouncy: { type: 'spring', duration: 550, bounce: 0.4 },
45
+ }
46
+
47
+ export type Curve =
48
+ | {
49
+ type: 'tween'
50
+ duration: number
51
+ ease: readonly EasingFunction[]
52
+ times: readonly number[] | undefined
53
+ }
54
+ | { type: 'spring'; physics: SpringPhysics; velocity: number | undefined }
55
+
56
+ export interface ResolvedTransition {
57
+ curve: Curve
58
+ delay: number
59
+ repeat: number
60
+ reverse: boolean
61
+ }
62
+
63
+ const springKeys = ['stiffness', 'damping', 'mass', 'bounce', 'velocity'] as const
64
+ const curveKeys = ['type', 'duration', 'ease', 'times', ...springKeys] as const
65
+ const timingKeys = ['delay', 'repeat', 'repeatType'] as const
66
+
67
+ export const DEFAULT_DURATION = 300
68
+
69
+ export function hasCurve(transition: Transition): boolean {
70
+ return curveKeys.some(
71
+ (key) => (transition as Record<string, unknown>)[key] !== undefined,
72
+ )
73
+ }
74
+
75
+ export function timingOf(transition: Transition): Timing {
76
+ const timing: Record<string, unknown> = {}
77
+ for (const key of timingKeys) {
78
+ if (transition[key] !== undefined) timing[key] = transition[key]
79
+ }
80
+ return timing
81
+ }
82
+
83
+ /** Timing merges key by key, the curve is taken whole from whichever side has one. */
84
+ export function mergeTransitions(base: Transition, override: Transition): Transition {
85
+ return hasCurve(override)
86
+ ? { ...timingOf(base), ...override }
87
+ : { ...base, ...timingOf(override) }
88
+ }
89
+
90
+ export function resolveTransition(
91
+ transition: Transition,
92
+ property: Property,
93
+ keyframes: number,
94
+ ): ResolvedTransition {
95
+ const type =
96
+ transition.type ??
97
+ (springKeys.some((key) => (transition as Spring)[key] !== undefined)
98
+ ? 'spring'
99
+ : hasCurve(transition) || property === 'opacity' || keyframes > 2
100
+ ? 'tween'
101
+ : 'spring')
102
+
103
+ const delay = transition.delay ?? 0
104
+ const repeat = transition.repeat ?? 0
105
+ if (delay < 0 || Number.isNaN(delay)) throw new RangeError(`Invalid delay: ${delay}.`)
106
+ if (repeat < 0 || Number.isNaN(repeat))
107
+ throw new RangeError(`Invalid repeat: ${repeat}.`)
108
+ const timing = { delay, repeat, reverse: transition.repeatType === 'reverse' }
109
+
110
+ if (type === 'spring') {
111
+ if (keyframes > 2) {
112
+ throw new Error(
113
+ `A spring goes from one value to another, but "${property}" has ${keyframes} keyframes. Use a tween for keyframes.`,
114
+ )
115
+ }
116
+ const spring = transition as Spring
117
+ return {
118
+ ...timing,
119
+ curve: { type, physics: springPhysics(spring), velocity: spring.velocity },
120
+ }
121
+ }
122
+
123
+ const tween = transition as Tween
124
+ const duration = tween.duration ?? DEFAULT_DURATION
125
+ if (duration < 0 || Number.isNaN(duration)) {
126
+ throw new RangeError(`Invalid duration: ${duration}.`)
127
+ }
128
+
129
+ const segments = keyframes - 1
130
+ const ease = tween.ease ?? 'easeOut'
131
+ const eases = isEasingList(ease) ? ease : Array.from({ length: segments }, () => ease)
132
+ if (eases.length !== segments) {
133
+ throw new Error(
134
+ `"${property}" has ${segments} segments between keyframes but ${eases.length} easings.`,
135
+ )
136
+ }
137
+
138
+ const times = tween.times
139
+ if (times) {
140
+ const valid =
141
+ times.length === keyframes &&
142
+ times[0] === 0 &&
143
+ times.at(-1) === 1 &&
144
+ times.every((time, i) => i === 0 || time >= times[i - 1]!)
145
+ if (!valid) {
146
+ throw new Error(
147
+ `"times" needs one value per keyframe (${keyframes}), going from 0 to 1 without going back.`,
148
+ )
149
+ }
150
+ }
151
+
152
+ return {
153
+ ...timing,
154
+ curve: { type, duration, ease: eases.map(resolveEasing), times },
155
+ }
156
+ }
157
+
158
+ function isEasingList(ease: Easing | readonly Easing[]): ease is readonly Easing[] {
159
+ return Array.isArray(ease) && ease.length > 0 && typeof ease[0] !== 'number'
160
+ }
package/src/types.ts ADDED
@@ -0,0 +1,39 @@
1
+ import type { View } from '@nativescript/core'
2
+ import type { SpringName, Transition } from './transition.js'
3
+
4
+ /** A value to animate to, or keyframes. `null` stands for the value the view has when the animation starts. */
5
+ export type KeyframeValue = number | readonly (number | null)[]
6
+
7
+ export interface Keyframes {
8
+ opacity?: KeyframeValue
9
+ /** Same as `translateX`, in DIPs. */
10
+ x?: KeyframeValue
11
+ /** Same as `translateY`, in DIPs. */
12
+ y?: KeyframeValue
13
+ /** Sets `scaleX` and `scaleY` together. */
14
+ scale?: KeyframeValue
15
+ scaleX?: KeyframeValue
16
+ scaleY?: KeyframeValue
17
+ /** Degrees. */
18
+ rotate?: KeyframeValue
19
+ rotateX?: KeyframeValue
20
+ rotateY?: KeyframeValue
21
+ }
22
+
23
+ /** A view, or anything that holds one: a NativeScript-Vue element, an Angular `ElementRef`. */
24
+ export type Target =
25
+ View | { readonly nativeView: View } | { readonly nativeElement: View }
26
+
27
+ export interface Stagger {
28
+ /** Milliseconds between one target and the next. */
29
+ each: number
30
+ /** Which target starts first. */
31
+ from?: 'first' | 'last' | 'center' | number
32
+ }
33
+
34
+ export type AnimateOptions = Transition & {
35
+ [K in keyof Keyframes]?: Transition | SpringName
36
+ } & {
37
+ /** With several targets, delays each one by this much more than the previous. */
38
+ stagger?: number | Stagger
39
+ }