@rafaelaugusto/jolt 0.0.0-stage → 0.1.0-beta.1
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/LICENSE +21 -0
- package/README.md +323 -2
- package/dist/animate.d.ts +26 -0
- package/dist/animate.d.ts.map +1 -0
- package/dist/animate.js +161 -0
- package/dist/animate.js.map +1 -0
- package/dist/driver/android.d.ts +3 -0
- package/dist/driver/android.d.ts.map +1 -0
- package/dist/driver/android.js +73 -0
- package/dist/driver/android.js.map +1 -0
- package/dist/driver/index.d.ts +18 -0
- package/dist/driver/index.d.ts.map +1 -0
- package/dist/driver/index.js +15 -0
- package/dist/driver/index.js.map +1 -0
- package/dist/driver/ios.d.ts +3 -0
- package/dist/driver/ios.d.ts.map +1 -0
- package/dist/driver/ios.js +77 -0
- package/dist/driver/ios.js.map +1 -0
- package/dist/easing.d.ts +8 -0
- package/dist/easing.d.ts.map +1 -0
- package/dist/easing.js +71 -0
- package/dist/easing.js.map +1 -0
- package/dist/engine.d.ts +48 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +178 -0
- package/dist/engine.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/presets.d.ts +310 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +141 -0
- package/dist/presets.js.map +1 -0
- package/dist/press.d.ts +13 -0
- package/dist/press.d.ts.map +1 -0
- package/dist/press.js +34 -0
- package/dist/press.js.map +1 -0
- package/dist/properties.d.ts +8 -0
- package/dist/properties.d.ts.map +1 -0
- package/dist/properties.js +42 -0
- package/dist/properties.js.map +1 -0
- package/dist/sequence.d.ts +26 -0
- package/dist/sequence.d.ts.map +1 -0
- package/dist/sequence.js +69 -0
- package/dist/sequence.js.map +1 -0
- package/dist/spring.d.ts +22 -0
- package/dist/spring.d.ts.map +1 -0
- package/dist/spring.js +54 -0
- package/dist/spring.js.map +1 -0
- package/dist/track.d.ts +28 -0
- package/dist/track.d.ts.map +1 -0
- package/dist/track.js +103 -0
- package/dist/track.js.map +1 -0
- package/dist/transition.d.ts +58 -0
- package/dist/transition.d.ts.map +1 -0
- package/dist/transition.js +82 -0
- package/dist/transition.js.map +1 -0
- package/dist/types.d.ts +40 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +67 -4
- package/src/animate.ts +246 -0
- package/src/driver/android.ts +101 -0
- package/src/driver/index.ts +32 -0
- package/src/driver/ios.ts +97 -0
- package/src/easing.ts +100 -0
- package/src/engine.ts +216 -0
- package/src/index.ts +12 -0
- package/src/presets.ts +158 -0
- package/src/press.ts +49 -0
- package/src/properties.ts +51 -0
- package/src/sequence.ts +88 -0
- package/src/spring.ts +95 -0
- package/src/track.ts +163 -0
- package/src/transition.ts +160 -0
- package/src/types.ts +41 -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,41 @@
|
|
|
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
|
+
/** Offset translations/rotations and multiply scale/opacity by their values at the start. */
|
|
38
|
+
relative?: boolean
|
|
39
|
+
/** With several targets, delays each one by this much more than the previous. */
|
|
40
|
+
stagger?: number | Stagger
|
|
41
|
+
}
|