@rafaelaugusto/jolt 0.1.0-beta.1 → 0.1.0-beta.11

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 (318) hide show
  1. package/README.md +385 -18
  2. package/dist/animate.d.ts +19 -3
  3. package/dist/animate.d.ts.map +1 -1
  4. package/dist/animate.js +186 -14
  5. package/dist/animate.js.map +1 -1
  6. package/dist/color.d.ts +11 -0
  7. package/dist/color.d.ts.map +1 -0
  8. package/dist/color.js +73 -0
  9. package/dist/color.js.map +1 -0
  10. package/dist/config.d.ts +31 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +73 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/decay.d.ts +11 -0
  15. package/dist/decay.d.ts.map +1 -0
  16. package/dist/decay.js +22 -0
  17. package/dist/decay.js.map +1 -0
  18. package/dist/decoration.d.ts +18 -0
  19. package/dist/decoration.d.ts.map +1 -0
  20. package/dist/decoration.js +155 -0
  21. package/dist/decoration.js.map +1 -0
  22. package/dist/drag-input.d.ts +15 -0
  23. package/dist/drag-input.d.ts.map +1 -0
  24. package/dist/drag-input.js +66 -0
  25. package/dist/drag-input.js.map +1 -0
  26. package/dist/drag-physics.d.ts +20 -0
  27. package/dist/drag-physics.d.ts.map +1 -0
  28. package/dist/drag-physics.js +75 -0
  29. package/dist/drag-physics.js.map +1 -0
  30. package/dist/drag.d.ts +58 -0
  31. package/dist/drag.d.ts.map +1 -0
  32. package/dist/drag.js +324 -0
  33. package/dist/drag.js.map +1 -0
  34. package/dist/driver/android-color.d.ts +20 -0
  35. package/dist/driver/android-color.d.ts.map +1 -0
  36. package/dist/driver/android-color.js +136 -0
  37. package/dist/driver/android-color.js.map +1 -0
  38. package/dist/driver/android-decoration.d.ts +22 -0
  39. package/dist/driver/android-decoration.d.ts.map +1 -0
  40. package/dist/driver/android-decoration.js +139 -0
  41. package/dist/driver/android-decoration.js.map +1 -0
  42. package/dist/driver/android-gradient.d.ts +5 -0
  43. package/dist/driver/android-gradient.d.ts.map +1 -0
  44. package/dist/driver/android-gradient.js +89 -0
  45. package/dist/driver/android-gradient.js.map +1 -0
  46. package/dist/driver/android-playback.d.ts +13 -0
  47. package/dist/driver/android-playback.d.ts.map +1 -0
  48. package/dist/driver/android-playback.js +141 -0
  49. package/dist/driver/android-playback.js.map +1 -0
  50. package/dist/driver/android.d.ts +1 -1
  51. package/dist/driver/android.d.ts.map +1 -1
  52. package/dist/driver/android.js +165 -13
  53. package/dist/driver/android.js.map +1 -1
  54. package/dist/driver/index.d.ts +25 -2
  55. package/dist/driver/index.d.ts.map +1 -1
  56. package/dist/driver/index.js.map +1 -1
  57. package/dist/driver/ios-color.d.ts +16 -0
  58. package/dist/driver/ios-color.d.ts.map +1 -0
  59. package/dist/driver/ios-color.js +392 -0
  60. package/dist/driver/ios-color.js.map +1 -0
  61. package/dist/driver/ios-decoration.d.ts +17 -0
  62. package/dist/driver/ios-decoration.d.ts.map +1 -0
  63. package/dist/driver/ios-decoration.js +271 -0
  64. package/dist/driver/ios-decoration.js.map +1 -0
  65. package/dist/driver/ios-gradient.d.ts +5 -0
  66. package/dist/driver/ios-gradient.d.ts.map +1 -0
  67. package/dist/driver/ios-gradient.js +109 -0
  68. package/dist/driver/ios-gradient.js.map +1 -0
  69. package/dist/driver/ios-layout.d.ts +9 -0
  70. package/dist/driver/ios-layout.d.ts.map +1 -0
  71. package/dist/driver/ios-layout.js +52 -0
  72. package/dist/driver/ios-layout.js.map +1 -0
  73. package/dist/driver/ios-playback.d.ts +10 -0
  74. package/dist/driver/ios-playback.d.ts.map +1 -0
  75. package/dist/driver/ios-playback.js +47 -0
  76. package/dist/driver/ios-playback.js.map +1 -0
  77. package/dist/driver/ios-shadow.d.ts +6 -0
  78. package/dist/driver/ios-shadow.d.ts.map +1 -0
  79. package/dist/driver/ios-shadow.js +52 -0
  80. package/dist/driver/ios-shadow.js.map +1 -0
  81. package/dist/driver/ios.d.ts.map +1 -1
  82. package/dist/driver/ios.js +163 -28
  83. package/dist/driver/ios.js.map +1 -1
  84. package/dist/engine.d.ts +75 -5
  85. package/dist/engine.d.ts.map +1 -1
  86. package/dist/engine.js +293 -23
  87. package/dist/engine.js.map +1 -1
  88. package/dist/gradient.d.ts +90 -0
  89. package/dist/gradient.d.ts.map +1 -0
  90. package/dist/gradient.js +247 -0
  91. package/dist/gradient.js.map +1 -0
  92. package/dist/haptics.d.ts +21 -0
  93. package/dist/haptics.d.ts.map +1 -0
  94. package/dist/haptics.js +120 -0
  95. package/dist/haptics.js.map +1 -0
  96. package/dist/image-effect-native.d.ts +26 -0
  97. package/dist/image-effect-native.d.ts.map +1 -0
  98. package/dist/image-effect-native.js +73 -0
  99. package/dist/image-effect-native.js.map +1 -0
  100. package/dist/image-effect.d.ts +50 -0
  101. package/dist/image-effect.d.ts.map +1 -0
  102. package/dist/image-effect.js +290 -0
  103. package/dist/image-effect.js.map +1 -0
  104. package/dist/image-owner.d.ts +3 -0
  105. package/dist/image-owner.d.ts.map +1 -0
  106. package/dist/image-owner.js +11 -0
  107. package/dist/image-owner.js.map +1 -0
  108. package/dist/index.d.ts +47 -4
  109. package/dist/index.d.ts.map +1 -1
  110. package/dist/index.js +22 -2
  111. package/dist/index.js.map +1 -1
  112. package/dist/interaction-surface.d.ts +12 -0
  113. package/dist/interaction-surface.d.ts.map +1 -0
  114. package/dist/interaction-surface.js +36 -0
  115. package/dist/interaction-surface.js.map +1 -0
  116. package/dist/items.d.ts +20 -0
  117. package/dist/items.d.ts.map +1 -0
  118. package/dist/items.js +109 -0
  119. package/dist/items.js.map +1 -0
  120. package/dist/keyboard.d.ts +41 -0
  121. package/dist/keyboard.d.ts.map +1 -0
  122. package/dist/keyboard.js +256 -0
  123. package/dist/keyboard.js.map +1 -0
  124. package/dist/label.d.ts +7 -0
  125. package/dist/label.d.ts.map +1 -0
  126. package/dist/label.js +60 -0
  127. package/dist/label.js.map +1 -0
  128. package/dist/layout.d.ts +13 -0
  129. package/dist/layout.d.ts.map +1 -0
  130. package/dist/layout.js +348 -0
  131. package/dist/layout.js.map +1 -0
  132. package/dist/liquid-grid.d.ts +3 -0
  133. package/dist/liquid-grid.d.ts.map +1 -0
  134. package/dist/liquid-grid.js +24 -0
  135. package/dist/liquid-grid.js.map +1 -0
  136. package/dist/liquid.d.ts +18 -0
  137. package/dist/liquid.d.ts.map +1 -0
  138. package/dist/liquid.js +234 -0
  139. package/dist/liquid.js.map +1 -0
  140. package/dist/loader.d.ts +19 -0
  141. package/dist/loader.d.ts.map +1 -0
  142. package/dist/loader.js +173 -0
  143. package/dist/loader.js.map +1 -0
  144. package/dist/motion-scope.d.ts +39 -0
  145. package/dist/motion-scope.d.ts.map +1 -0
  146. package/dist/motion-scope.js +238 -0
  147. package/dist/motion-scope.js.map +1 -0
  148. package/dist/motion-states.d.ts +43 -0
  149. package/dist/motion-states.d.ts.map +1 -0
  150. package/dist/motion-states.js +220 -0
  151. package/dist/motion-states.js.map +1 -0
  152. package/dist/particles.d.ts +39 -0
  153. package/dist/particles.d.ts.map +1 -0
  154. package/dist/particles.js +265 -0
  155. package/dist/particles.js.map +1 -0
  156. package/dist/path.d.ts +12 -0
  157. package/dist/path.d.ts.map +1 -0
  158. package/dist/path.js +90 -0
  159. package/dist/path.js.map +1 -0
  160. package/dist/playback-group.d.ts +6 -0
  161. package/dist/playback-group.d.ts.map +1 -0
  162. package/dist/playback-group.js +141 -0
  163. package/dist/playback-group.js.map +1 -0
  164. package/dist/playback.d.ts +25 -0
  165. package/dist/playback.d.ts.map +1 -0
  166. package/dist/playback.js +35 -0
  167. package/dist/playback.js.map +1 -0
  168. package/dist/press.d.ts.map +1 -1
  169. package/dist/press.js +23 -7
  170. package/dist/press.js.map +1 -1
  171. package/dist/properties.d.ts +6 -3
  172. package/dist/properties.d.ts.map +1 -1
  173. package/dist/properties.js +26 -2
  174. package/dist/properties.js.map +1 -1
  175. package/dist/reveal.d.ts +25 -0
  176. package/dist/reveal.d.ts.map +1 -0
  177. package/dist/reveal.js +294 -0
  178. package/dist/reveal.js.map +1 -0
  179. package/dist/roll-diff.d.ts +22 -0
  180. package/dist/roll-diff.d.ts.map +1 -0
  181. package/dist/roll-diff.js +36 -0
  182. package/dist/roll-diff.js.map +1 -0
  183. package/dist/roll.d.ts +29 -0
  184. package/dist/roll.d.ts.map +1 -0
  185. package/dist/roll.js +442 -0
  186. package/dist/roll.js.map +1 -0
  187. package/dist/scroll-linked.d.ts +10 -0
  188. package/dist/scroll-linked.d.ts.map +1 -0
  189. package/dist/scroll-linked.js +58 -0
  190. package/dist/scroll-linked.js.map +1 -0
  191. package/dist/sequence.d.ts +10 -5
  192. package/dist/sequence.d.ts.map +1 -1
  193. package/dist/sequence.js +208 -14
  194. package/dist/sequence.js.map +1 -1
  195. package/dist/shimmer.d.ts +12 -0
  196. package/dist/shimmer.d.ts.map +1 -0
  197. package/dist/shimmer.js +161 -0
  198. package/dist/shimmer.js.map +1 -0
  199. package/dist/svg-path.d.ts +14 -0
  200. package/dist/svg-path.d.ts.map +1 -0
  201. package/dist/svg-path.js +281 -0
  202. package/dist/svg-path.js.map +1 -0
  203. package/dist/symbol-effect.d.ts +23 -0
  204. package/dist/symbol-effect.d.ts.map +1 -0
  205. package/dist/symbol-effect.js +187 -0
  206. package/dist/symbol-effect.js.map +1 -0
  207. package/dist/text-layout.d.ts +30 -0
  208. package/dist/text-layout.d.ts.map +1 -0
  209. package/dist/text-layout.js +185 -0
  210. package/dist/text-layout.js.map +1 -0
  211. package/dist/text.d.ts +17 -0
  212. package/dist/text.d.ts.map +1 -0
  213. package/dist/text.js +263 -0
  214. package/dist/text.js.map +1 -0
  215. package/dist/track.d.ts +3 -1
  216. package/dist/track.d.ts.map +1 -1
  217. package/dist/track.js +266 -13
  218. package/dist/track.js.map +1 -1
  219. package/dist/transition.d.ts +32 -1
  220. package/dist/transition.d.ts.map +1 -1
  221. package/dist/transition.js +58 -6
  222. package/dist/transition.js.map +1 -1
  223. package/dist/types.d.ts +48 -1
  224. package/dist/types.d.ts.map +1 -1
  225. package/dist/vue/index.d.ts +95 -0
  226. package/dist/vue/index.d.ts.map +1 -0
  227. package/dist/vue/index.js +163 -0
  228. package/dist/vue/index.js.map +1 -0
  229. package/dist/zoom-native.d.ts +30 -0
  230. package/dist/zoom-native.d.ts.map +1 -0
  231. package/dist/zoom-native.js +56 -0
  232. package/dist/zoom-native.js.map +1 -0
  233. package/dist/zoomable.d.ts +48 -0
  234. package/dist/zoomable.d.ts.map +1 -0
  235. package/dist/zoomable.js +249 -0
  236. package/dist/zoomable.js.map +1 -0
  237. package/package.json +29 -5
  238. package/platforms/android/include.gradle +7 -0
  239. package/platforms/android/java/org/rafaelaugusto/jolt/ColorTarget.java +102 -0
  240. package/platforms/android/java/org/rafaelaugusto/jolt/DecorationTarget.java +154 -0
  241. package/platforms/android/java/org/rafaelaugusto/jolt/GradientLayer.java +195 -0
  242. package/platforms/android/java/org/rafaelaugusto/jolt/ImageEffect.java +288 -0
  243. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFollower.java +91 -0
  244. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardObserver.java +97 -0
  245. package/platforms/android/java/org/rafaelaugusto/jolt/NumberRoll.java +190 -0
  246. package/platforms/android/java/org/rafaelaugusto/jolt/PhotoZoom.java +250 -0
  247. package/platforms/android/java/org/rafaelaugusto/jolt/TextReveal.java +180 -0
  248. package/platforms/android/proguard-rules.pro +21 -0
  249. package/platforms/ios/build.xcconfig +1 -0
  250. package/platforms/ios/src/JoltImageEffect.h +25 -0
  251. package/platforms/ios/src/JoltImageEffect.mm +242 -0
  252. package/platforms/ios/src/JoltInteractiveAnimation.h +18 -0
  253. package/platforms/ios/src/JoltInteractiveAnimation.m +123 -0
  254. package/platforms/ios/src/JoltPhotoZoom.h +23 -0
  255. package/platforms/ios/src/JoltPhotoZoom.m +229 -0
  256. package/platforms/ios/src/module.modulemap +6 -0
  257. package/src/android-plugin.d.ts +184 -0
  258. package/src/animate.ts +249 -36
  259. package/src/color.ts +101 -0
  260. package/src/config.ts +84 -0
  261. package/src/decay.ts +45 -0
  262. package/src/decoration.ts +180 -0
  263. package/src/drag-input.ts +74 -0
  264. package/src/drag-physics.ts +110 -0
  265. package/src/drag.ts +395 -0
  266. package/src/driver/android-color.ts +170 -0
  267. package/src/driver/android-decoration.ts +182 -0
  268. package/src/driver/android-gradient.ts +109 -0
  269. package/src/driver/android-playback.ts +157 -0
  270. package/src/driver/android.ts +208 -20
  271. package/src/driver/index.ts +37 -2
  272. package/src/driver/ios-color.ts +441 -0
  273. package/src/driver/ios-decoration.ts +332 -0
  274. package/src/driver/ios-gradient.ts +114 -0
  275. package/src/driver/ios-layout.ts +63 -0
  276. package/src/driver/ios-playback.ts +51 -0
  277. package/src/driver/ios-shadow.ts +50 -0
  278. package/src/driver/ios.ts +193 -33
  279. package/src/engine.ts +321 -27
  280. package/src/gradient.ts +372 -0
  281. package/src/haptics.ts +148 -0
  282. package/src/image-effect-native.ts +129 -0
  283. package/src/image-effect.ts +336 -0
  284. package/src/image-owner.ts +12 -0
  285. package/src/index.ts +75 -4
  286. package/src/interaction-surface.ts +41 -0
  287. package/src/ios-plugin.d.ts +81 -0
  288. package/src/items.ts +140 -0
  289. package/src/keyboard.ts +291 -0
  290. package/src/label.ts +76 -0
  291. package/src/layout.ts +408 -0
  292. package/src/liquid-grid.ts +28 -0
  293. package/src/liquid.ts +287 -0
  294. package/src/loader.ts +202 -0
  295. package/src/motion-scope.ts +279 -0
  296. package/src/motion-states.ts +269 -0
  297. package/src/particles.ts +323 -0
  298. package/src/path.ts +124 -0
  299. package/src/playback-group.ts +143 -0
  300. package/src/playback.ts +62 -0
  301. package/src/press.ts +26 -10
  302. package/src/properties.ts +32 -6
  303. package/src/reveal.ts +374 -0
  304. package/src/roll-diff.ts +57 -0
  305. package/src/roll.ts +515 -0
  306. package/src/scroll-linked.ts +73 -0
  307. package/src/sequence.ts +204 -14
  308. package/src/shimmer.ts +180 -0
  309. package/src/svg-path.ts +355 -0
  310. package/src/symbol-effect.ts +226 -0
  311. package/src/text-layout.ts +228 -0
  312. package/src/text.ts +279 -0
  313. package/src/track.ts +347 -12
  314. package/src/transition.ts +93 -6
  315. package/src/types.ts +49 -1
  316. package/src/vue/index.ts +231 -0
  317. package/src/zoom-native.ts +124 -0
  318. package/src/zoomable.ts +330 -0
package/src/track.ts CHANGED
@@ -1,7 +1,12 @@
1
+ import { clampDecoration, isDecorationProperty } from './decoration.js'
1
2
  import { linear } from './easing.js'
3
+ import { solveDecay } from './decay.js'
4
+ import { solveDragRelease } from './drag-physics.js'
2
5
  import { precision, type Property } from './properties.js'
3
- import { solveSpring } from './spring.js'
6
+ import { solveSpring, springPhysics } from './spring.js'
4
7
  import type { Curve, ResolvedTransition } from './transition.js'
8
+ import { colorMixer, isColorProperty, type ColorProperty } from './color.js'
9
+ import { reducedMotion, timeScale } from './config.js'
5
10
 
6
11
  /**
7
12
  * One property animated over time. The curve is computed here and sampled,
@@ -37,18 +42,40 @@ interface Motion {
37
42
  }
38
43
 
39
44
  const SAMPLES_PER_SECOND = 60
45
+ const MAX_DECAY_SAMPLES = 6000
40
46
  const ARRIVAL_TOLERANCE = 0.05
41
47
 
42
48
  export function createTrack(
43
49
  property: Property,
44
50
  keyframes: readonly number[],
45
- { curve, delay, repeat, reverse }: ResolvedTransition,
51
+ transition: ResolvedTransition,
46
52
  velocity: number,
47
53
  ): Track {
48
- const motion =
54
+ if (isColorProperty(property)) return colorTrack(property, keyframes, transition)
55
+ const { curve } = transition
56
+ if (property !== 'opacity' && reducedMotion())
57
+ return instantTrack(
58
+ property,
59
+ restingValue(property, keyframes, curve, transition.repeat, transition.reverse),
60
+ )
61
+ const slowdown = 1 / timeScale()
62
+ const delay = transition.delay * slowdown
63
+ const played = slow(
49
64
  curve.type === 'spring'
50
- ? spring(property, keyframes, curve, velocity)
51
- : tween(keyframes, curve)
65
+ ? spring(property, keyframes, curve, velocity * slowdown, slowdown)
66
+ : curve.type === 'drag'
67
+ ? dragMotion(keyframes[0]!, curve, slowdown)
68
+ : curve.type === 'decay'
69
+ ? decay(property, keyframes[0]!, curve, slowdown)
70
+ : tween(keyframes, curve, slowdown),
71
+ slowdown,
72
+ )
73
+ const { motion, repeat, reverse } = pauseBetweenRepeats(
74
+ played,
75
+ transition.repeat,
76
+ transition.reverse,
77
+ (transition.repeatDelay ?? 0) * slowdown,
78
+ )
52
79
  const { duration } = motion
53
80
  const first = motion.values[0]!
54
81
  const last = motion.values.at(-1)!
@@ -65,7 +92,7 @@ export function createTrack(
65
92
  return reverse && iteration % 2 === 1 ? [duration - local, -1] : [local, 1]
66
93
  }
67
94
 
68
- return {
95
+ const track: Track = {
69
96
  property,
70
97
  delay,
71
98
  duration,
@@ -75,7 +102,13 @@ export function createTrack(
75
102
  values: motion.values,
76
103
  to: !finite || endsBackwards ? first : last,
77
104
  end,
78
- arrival: curve.type === 'spring' && repeat === 0 ? delay + arrival(motion) : end,
105
+ arrival:
106
+ (curve.type === 'spring' ||
107
+ curve.type === 'drag' ||
108
+ (curve.type === 'decay' && curve.snap !== undefined)) &&
109
+ transition.repeat === 0
110
+ ? delay + arrival(motion)
111
+ : end,
79
112
  valueAt: (elapsed) => motion.position(localTime(elapsed)[0]),
80
113
  velocityAt: (elapsed) => {
81
114
  if (elapsed <= delay || elapsed >= end) return 0
@@ -83,6 +116,271 @@ export function createTrack(
83
116
  return motion.velocity(time) * direction
84
117
  },
85
118
  }
119
+ if (!isDecorationProperty(property)) return track
120
+ return {
121
+ ...track,
122
+ values: track.values.map((value) => clampDecoration(property, value)),
123
+ to: clampDecoration(property, track.to),
124
+ valueAt: (elapsed) => clampDecoration(property, track.valueAt(elapsed)),
125
+ }
126
+ }
127
+
128
+ /** Already at its end: the engine writes the value and plays nothing. */
129
+ export function instantTrack(property: Property, value: number): Track {
130
+ const to = isDecorationProperty(property) ? clampDecoration(property, value) : value
131
+ return {
132
+ property,
133
+ delay: 0,
134
+ duration: 0,
135
+ repeat: 0,
136
+ reverse: false,
137
+ times: [0, 1],
138
+ values: [to, to],
139
+ to,
140
+ end: 0,
141
+ arrival: 0,
142
+ valueAt: () => to,
143
+ velocityAt: () => 0,
144
+ }
145
+ }
146
+
147
+ function restingValue(
148
+ property: Property,
149
+ keyframes: readonly number[],
150
+ curve: Curve,
151
+ repeat: number,
152
+ reverse: boolean,
153
+ ) {
154
+ const first = keyframes[0]!
155
+ if (!Number.isFinite(repeat) || (reverse && repeat % 2 === 1)) return first
156
+ if (curve.type === 'drag') return solveDragRelease(first, curve).to
157
+ if (curve.type !== 'decay') return keyframes.at(-1)!
158
+ const landing = solveDecay(
159
+ first,
160
+ curve.velocity,
161
+ curve.friction,
162
+ precision[property],
163
+ ).to
164
+ return curve.snap ? closest(curve.snap, landing) : landing
165
+ }
166
+
167
+ function closest(stops: readonly number[], target: number) {
168
+ return stops.reduce((best, stop) =>
169
+ Math.abs(stop - target) < Math.abs(best - target) ? stop : best,
170
+ )
171
+ }
172
+
173
+ function colorTrack(
174
+ property: ColorProperty,
175
+ colors: readonly number[],
176
+ transition: ResolvedTransition,
177
+ ): Track {
178
+ const progress = createTrack(
179
+ 'opacity',
180
+ colors.map((_, i) => i),
181
+ transition,
182
+ 0,
183
+ )
184
+ const mixes = colors.slice(1).map((color, i) => colorMixer(colors[i]!, color))
185
+ const colorAt = (position: number) => {
186
+ const p = Math.max(0, Math.min(colors.length - 1, position))
187
+ const segment = Math.min(mixes.length - 1, Math.floor(p))
188
+ return mixes[segment]!(p - segment)
189
+ }
190
+ // Linear timing still needs intermediate colors: native RGB interpolation is
191
+ // only the short bridge between these perceptually spaced samples.
192
+ const steps = Math.max(
193
+ 1,
194
+ Math.min(
195
+ MAX_DECAY_SAMPLES,
196
+ Math.ceil((progress.duration / 1000) * SAMPLES_PER_SECOND),
197
+ ),
198
+ )
199
+ const times = [
200
+ ...new Set([
201
+ ...progress.times,
202
+ ...Array.from({ length: steps + 1 }, (_, i) => i / steps),
203
+ ]),
204
+ ].sort((a, b) => a - b)
205
+ return {
206
+ ...progress,
207
+ property,
208
+ times,
209
+ values: times.map((time) =>
210
+ colorAt(
211
+ time === 0
212
+ ? progress.values[0]!
213
+ : time === 1
214
+ ? progress.values.at(-1)!
215
+ : progress.valueAt(progress.delay + time * progress.duration),
216
+ ),
217
+ ),
218
+ to: colorAt(progress.to),
219
+ valueAt: (elapsed) => colorAt(progress.valueAt(elapsed)),
220
+ // Packed colors have no scalar velocity to carry into another spring.
221
+ velocityAt: () => 0,
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Holds the end value for `pause` ms before each repeat, inside the keyframes, so both
227
+ * platforms play it natively. Finite repeats are laid out one after another, with no
228
+ * wait after the last; endless ones loop a cycle that waits at each turn.
229
+ */
230
+ function pauseBetweenRepeats(
231
+ motion: Motion,
232
+ repeat: number,
233
+ reverse: boolean,
234
+ pause: number,
235
+ ): { motion: Motion; repeat: number; reverse: boolean } {
236
+ if (pause <= 0 || repeat <= 0 || motion.duration === 0)
237
+ return { motion, repeat, reverse }
238
+ type Segment = { kind: 'forward' | 'backward' | 'hold'; duration: number }
239
+ const play = (backward: boolean): Segment => ({
240
+ kind: backward ? 'backward' : 'forward',
241
+ duration: motion.duration,
242
+ })
243
+ const hold: Segment = { kind: 'hold', duration: pause }
244
+ const segments: Segment[] = []
245
+ if (Number.isFinite(repeat))
246
+ for (let i = 0; i <= repeat; i++) {
247
+ if (i > 0) segments.push(hold)
248
+ segments.push(play(reverse && i % 2 === 1))
249
+ }
250
+ else segments.push(play(false), hold, ...(reverse ? [play(true), hold] : []))
251
+
252
+ const total = segments.reduce((sum, segment) => sum + segment.duration, 0)
253
+ const starts = segments.map((_, i) =>
254
+ segments.slice(0, i).reduce((sum, segment) => sum + segment.duration, 0),
255
+ )
256
+ const last = motion.values.length - 1
257
+ const endOf = (i: number) => {
258
+ const segment = segments[i]!
259
+ if (segment.kind === 'forward') return motion.values[last]!
260
+ if (segment.kind === 'backward') return motion.values[0]!
261
+ return endOf(i - 1)
262
+ }
263
+ // A loop jumps back to its start at the end of the hold, not across it.
264
+ const jump = Math.min(1, pause / 10)
265
+
266
+ const times: number[] = []
267
+ const values: number[] = []
268
+ segments.forEach((segment, i) => {
269
+ const start = starts[i]!
270
+ if (segment.kind === 'hold') {
271
+ const held = endOf(i - 1)
272
+ const next = segments[i + 1]
273
+ const jumps =
274
+ next !== undefined &&
275
+ (next.kind === 'forward' ? motion.values[0] : motion.values[last]) !== held
276
+ times.push((start + segment.duration - (jumps ? jump : 0)) / total)
277
+ values.push(held)
278
+ return
279
+ }
280
+ for (let k = 0; k <= last; k++) {
281
+ const index = segment.kind === 'forward' ? k : last - k
282
+ const offset = motion.times[index]!
283
+ const time = segment.kind === 'forward' ? offset : 1 - offset
284
+ if (k === 0 && i > 0 && values.at(-1) === motion.values[index]) continue
285
+ times.push((start + time * motion.duration) / total)
286
+ values.push(motion.values[index]!)
287
+ }
288
+ })
289
+
290
+ const locate = (time: number) => {
291
+ let i = 0
292
+ while (i < segments.length - 1 && time >= starts[i + 1]!) i++
293
+ return [i, time - starts[i]!] as const
294
+ }
295
+ return {
296
+ motion: {
297
+ duration: total,
298
+ times,
299
+ values,
300
+ position: (time) => {
301
+ const [i, local] = locate(Math.max(0, Math.min(total, time)))
302
+ const segment = segments[i]!
303
+ if (segment.kind === 'hold') return endOf(i - 1)
304
+ return motion.position(
305
+ segment.kind === 'forward' ? local : motion.duration - local,
306
+ )
307
+ },
308
+ velocity: (time) => {
309
+ const [i, local] = locate(Math.max(0, Math.min(total, time)))
310
+ const segment = segments[i]!
311
+ if (segment.kind === 'hold') return 0
312
+ return segment.kind === 'forward'
313
+ ? motion.velocity(local)
314
+ : -motion.velocity(motion.duration - local)
315
+ },
316
+ },
317
+ repeat: Number.isFinite(repeat) ? 0 : Infinity,
318
+ reverse: false,
319
+ }
320
+ }
321
+
322
+ /** Plays the same trajectory over a longer or shorter time. */
323
+ function slow(motion: Motion, factor: number): Motion {
324
+ if (factor === 1) return motion
325
+ return {
326
+ ...motion,
327
+ duration: motion.duration * factor,
328
+ position: (time) => motion.position(time / factor),
329
+ velocity: (time) => motion.velocity(time / factor) / factor,
330
+ }
331
+ }
332
+
333
+ /** Keyframes per second of playback, so slowed curves stay smooth. */
334
+ function samples(duration: number, density: number) {
335
+ return Math.max(
336
+ 1,
337
+ Math.min(
338
+ MAX_DECAY_SAMPLES,
339
+ Math.ceil(((duration * density) / 1000) * SAMPLES_PER_SECOND),
340
+ ),
341
+ )
342
+ }
343
+
344
+ function dragMotion(
345
+ from: number,
346
+ curve: Extract<Curve, { type: 'drag' }>,
347
+ density: number,
348
+ ): Motion {
349
+ const motion = solveDragRelease(from, curve)
350
+ const steps = samples(motion.duration, density)
351
+ const times = Array.from({ length: steps + 1 }, (_, i) => i / steps)
352
+ return {
353
+ ...motion,
354
+ times,
355
+ values: times.map((time) => motion.position(time * motion.duration)),
356
+ }
357
+ }
358
+
359
+ function decay(
360
+ property: Property,
361
+ from: number,
362
+ curve: Extract<Curve, { type: 'decay' }>,
363
+ density: number,
364
+ ): Motion {
365
+ const motion = solveDecay(from, curve.velocity, curve.friction, precision[property])
366
+ if (curve.snap) {
367
+ return spring(
368
+ property,
369
+ [from, closest(curve.snap, motion.to)],
370
+ { type: 'spring', physics: springPhysics({}), velocity: curve.velocity },
371
+ 0,
372
+ density,
373
+ )
374
+ }
375
+
376
+ const steps = samples(motion.duration, density)
377
+ const times: number[] = []
378
+ const values: number[] = []
379
+ for (let i = 0; i <= steps; i++) {
380
+ times.push(i / steps)
381
+ values.push(motion.position((i / steps) * motion.duration))
382
+ }
383
+ return { ...motion, times, values }
86
384
  }
87
385
 
88
386
  function arrival({ times, values, duration }: Motion) {
@@ -99,6 +397,7 @@ function spring(
99
397
  [from, to]: readonly number[],
100
398
  curve: Extract<Curve, { type: 'spring' }>,
101
399
  velocity: number,
400
+ density: number,
102
401
  ): Motion {
103
402
  const motion = solveSpring(
104
403
  curve.physics,
@@ -107,7 +406,7 @@ function spring(
107
406
  curve.velocity ?? velocity,
108
407
  precision[property],
109
408
  )
110
- const steps = Math.max(1, Math.ceil((motion.duration / 1000) * SAMPLES_PER_SECOND))
409
+ const steps = samples(motion.duration, density)
111
410
  const times: number[] = []
112
411
  const values: number[] = []
113
412
  for (let i = 0; i <= steps; i++) {
@@ -120,7 +419,9 @@ function spring(
120
419
  function tween(
121
420
  keyframes: readonly number[],
122
421
  curve: Extract<Curve, { type: 'tween' }>,
422
+ density: number,
123
423
  ): Motion {
424
+ if (curve.track && curve.track !== linear) return easedTrack(keyframes, curve, density)
124
425
  const { duration, ease } = curve
125
426
  const offsets = curve.times ?? keyframes.map((_, i) => i / (keyframes.length - 1))
126
427
 
@@ -143,10 +444,7 @@ function tween(
143
444
  const from = keyframes[i]!
144
445
  const to = keyframes[i + 1]!
145
446
  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))
447
+ const steps = easing === linear || from === to ? 1 : samples(span * duration, density)
150
448
  for (let step = 1; step <= steps; step++) {
151
449
  times.push(start + (span * step) / steps)
152
450
  values.push(step === steps ? to : from + (to - from) * easing(step / steps))
@@ -161,3 +459,40 @@ function tween(
161
459
  values,
162
460
  }
163
461
  }
462
+
463
+ /** One easing over the whole timeline, then straight lines between keyframes, sampled evenly in time. */
464
+ function easedTrack(
465
+ keyframes: readonly number[],
466
+ curve: Extract<Curve, { type: 'tween' }>,
467
+ density: number,
468
+ ): Motion {
469
+ const { duration, track: ease = linear } = curve
470
+ const offsets = curve.times ?? keyframes.map((_, i) => i / (keyframes.length - 1))
471
+ const last = keyframes.length - 1
472
+
473
+ // Like `interpolate`, an easing past 0 or 1 extends the first or last segment.
474
+ const at = (progress: number) => {
475
+ let i = 0
476
+ while (i < last - 1 && progress > offsets[i + 1]!) i++
477
+ const start = offsets[i]!
478
+ const span = offsets[i + 1]! - start
479
+ const from = keyframes[i]!
480
+ const to = keyframes[i + 1]!
481
+ if (span <= 0) return progress < start ? from : to
482
+ return from + ((to - from) * (progress - start)) / span
483
+ }
484
+ const position = (time: number) =>
485
+ at(ease(duration > 0 ? Math.min(Math.max(time / duration, 0), 1) : 1))
486
+
487
+ const steps = samples(duration, density)
488
+ const times = Array.from({ length: steps + 1 }, (_, i) => i / steps)
489
+ return {
490
+ duration,
491
+ position,
492
+ velocity: (time) => (position(time + 0.5) - position(time - 0.5)) * 1000,
493
+ times,
494
+ values: times.map((time, i) =>
495
+ i === steps ? keyframes[last]! : position(time * duration),
496
+ ),
497
+ }
498
+ }
package/src/transition.ts CHANGED
@@ -1,6 +1,9 @@
1
- import { resolveEasing, type Easing, type EasingFunction } from './easing.js'
1
+ import { linear, resolveEasing, type Easing, type EasingFunction } from './easing.js'
2
2
  import type { Property } from './properties.js'
3
+ import { isDecorationProperty } from './decoration.js'
4
+ import { isColorProperty } from './color.js'
3
5
  import { springPhysics, type SpringPhysics } from './spring.js'
6
+ import type { DragCurve } from './drag-physics.js'
4
7
 
5
8
  export interface Timing {
6
9
  /** Milliseconds to wait before starting. The first keyframe is applied right away. */
@@ -9,6 +12,8 @@ export interface Timing {
9
12
  repeat?: number
10
13
  /** `reverse` plays every other repeat backwards. */
11
14
  repeatType?: 'loop' | 'reverse'
15
+ /** Milliseconds to hold the end value before each repeat. The first play doesn't wait. */
16
+ repeatDelay?: number
12
17
  }
13
18
 
14
19
  export interface Tween extends Timing {
@@ -19,6 +24,11 @@ export interface Tween extends Timing {
19
24
  ease?: Easing | readonly Easing[]
20
25
  /** Where each keyframe sits, from 0 to 1. Evenly spaced by default. */
21
26
  times?: readonly number[]
27
+ /**
28
+ * Run one `ease` over the whole track instead of each segment, with straight lines
29
+ * between keyframes, like React Native's `interpolate`.
30
+ */
31
+ easeTrack?: boolean
22
32
  }
23
33
 
24
34
  export interface Spring extends Timing {
@@ -44,25 +54,72 @@ export const springs: Record<SpringName, Spring> = {
44
54
  bouncy: { type: 'spring', duration: 550, bounce: 0.4 },
45
55
  }
46
56
 
57
+ export interface OrigamiSpring {
58
+ /** React Native's default is 7. */
59
+ friction?: number
60
+ /** React Native's default is 40. */
61
+ tension?: number
62
+ }
63
+
64
+ /**
65
+ * The spring React Native's `Animated.spring` plays for `friction` and `tension`,
66
+ * converted with the same Origami formulas.
67
+ *
68
+ * ```ts
69
+ * animate(card, { x: 100 }, origamiSpring({ friction: 7, tension: 40 }))
70
+ * ```
71
+ */
72
+ export function origamiSpring({
73
+ friction = 7,
74
+ tension = 40,
75
+ }: OrigamiSpring = {}): Spring {
76
+ if (!Number.isFinite(friction) || !Number.isFinite(tension))
77
+ throw new RangeError('Origami friction and tension must be finite.')
78
+ const stiffness = (tension - 30) * 3.62 + 194
79
+ const damping = (friction - 8) * 3 + 25
80
+ if (stiffness <= 0 || damping < 0)
81
+ throw new RangeError(
82
+ `Origami tension ${tension} and friction ${friction} give no spring (stiffness ${stiffness}, damping ${damping}).`,
83
+ )
84
+ return { type: 'spring', stiffness, damping, mass: 1 }
85
+ }
86
+
47
87
  export type Curve =
88
+ | DragCurve
48
89
  | {
49
90
  type: 'tween'
50
91
  duration: number
51
92
  ease: readonly EasingFunction[]
52
93
  times: readonly number[] | undefined
94
+ /** One easing over the whole track; `ease` is then linear per segment. */
95
+ track?: EasingFunction
53
96
  }
54
97
  | { type: 'spring'; physics: SpringPhysics; velocity: number | undefined }
98
+ | {
99
+ type: 'decay'
100
+ friction: number
101
+ velocity: number
102
+ snap: readonly number[] | undefined
103
+ }
55
104
 
56
105
  export interface ResolvedTransition {
57
106
  curve: Curve
58
107
  delay: number
59
108
  repeat: number
60
109
  reverse: boolean
110
+ repeatDelay?: number
61
111
  }
62
112
 
63
113
  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
114
+ const curveKeys = [
115
+ 'type',
116
+ 'duration',
117
+ 'ease',
118
+ 'times',
119
+ 'easeTrack',
120
+ ...springKeys,
121
+ ] as const
122
+ const timingKeys = ['delay', 'repeat', 'repeatType', 'repeatDelay'] as const
66
123
 
67
124
  export const DEFAULT_DURATION = 300
68
125
 
@@ -96,7 +153,11 @@ export function resolveTransition(
96
153
  transition.type ??
97
154
  (springKeys.some((key) => (transition as Spring)[key] !== undefined)
98
155
  ? 'spring'
99
- : hasCurve(transition) || property === 'opacity' || keyframes > 2
156
+ : hasCurve(transition) ||
157
+ property === 'opacity' ||
158
+ isColorProperty(property) ||
159
+ isDecorationProperty(property) ||
160
+ keyframes > 2
100
161
  ? 'tween'
101
162
  : 'spring')
102
163
 
@@ -105,9 +166,19 @@ export function resolveTransition(
105
166
  if (delay < 0 || Number.isNaN(delay)) throw new RangeError(`Invalid delay: ${delay}.`)
106
167
  if (repeat < 0 || Number.isNaN(repeat))
107
168
  throw new RangeError(`Invalid repeat: ${repeat}.`)
108
- const timing = { delay, repeat, reverse: transition.repeatType === 'reverse' }
169
+ const repeatDelay = transition.repeatDelay ?? 0
170
+ if (!Number.isFinite(repeatDelay) || repeatDelay < 0)
171
+ throw new RangeError(`Invalid repeat delay: ${repeatDelay}.`)
172
+ const timing = {
173
+ delay,
174
+ repeat,
175
+ reverse: transition.repeatType === 'reverse',
176
+ ...(repeatDelay > 0 && repeat > 0 && { repeatDelay }),
177
+ }
109
178
 
110
179
  if (type === 'spring') {
180
+ if ((transition as Tween).easeTrack)
181
+ throw new TypeError('easeTrack applies to tweens, not springs.')
111
182
  if (keyframes > 2) {
112
183
  throw new Error(
113
184
  `A spring goes from one value to another, but "${property}" has ${keyframes} keyframes. Use a tween for keyframes.`,
@@ -122,12 +193,21 @@ export function resolveTransition(
122
193
 
123
194
  const tween = transition as Tween
124
195
  const duration = tween.duration ?? DEFAULT_DURATION
125
- if (duration < 0 || Number.isNaN(duration)) {
196
+ if (
197
+ duration < 0 ||
198
+ Number.isNaN(duration) ||
199
+ ((isColorProperty(property) || isDecorationProperty(property)) &&
200
+ !Number.isFinite(duration))
201
+ ) {
126
202
  throw new RangeError(`Invalid duration: ${duration}.`)
127
203
  }
128
204
 
129
205
  const segments = keyframes - 1
130
206
  const ease = tween.ease ?? 'easeOut'
207
+ if (tween.easeTrack && isEasingList(ease))
208
+ throw new TypeError(
209
+ 'easeTrack runs one easing over the whole track; pass a single ease.',
210
+ )
131
211
  const eases = isEasingList(ease) ? ease : Array.from({ length: segments }, () => ease)
132
212
  if (eases.length !== segments) {
133
213
  throw new Error(
@@ -149,6 +229,13 @@ export function resolveTransition(
149
229
  }
150
230
  }
151
231
 
232
+ if (tween.easeTrack) {
233
+ const track = resolveEasing(ease as Easing)
234
+ return {
235
+ ...timing,
236
+ curve: { type, duration, ease: eases.map(() => linear), times, track },
237
+ }
238
+ }
152
239
  return {
153
240
  ...timing,
154
241
  curve: { type, duration, ease: eases.map(resolveEasing), times },
package/src/types.ts CHANGED
@@ -1,10 +1,35 @@
1
1
  import type { View } from '@nativescript/core'
2
+ import type { Haptic } from './haptics.js'
2
3
  import type { SpringName, Transition } from './transition.js'
4
+ import type { ColorKeyframes } from './color.js'
3
5
 
4
6
  /** A value to animate to, or keyframes. `null` stands for the value the view has when the animation starts. */
5
7
  export type KeyframeValue = number | readonly (number | null)[]
6
8
 
7
9
  export interface Keyframes {
10
+ /** One continuous SVG path, expressed as translation coordinates in DIPs. */
11
+ path?: string
12
+ /** Face the path's forward tangent (+X artwork). Requires path. */
13
+ orient?: boolean
14
+ backgroundColor?: ColorKeyframes
15
+ /** Uniform starting edge colors; widths and radii may differ. */
16
+ borderColor?: ColorKeyframes
17
+ color?: ColorKeyframes
18
+ /** Uniform circular corners, in DIPs. */
19
+ borderRadius?: KeyframeValue
20
+ /** Uniform visual border width, in DIPs. Content layout uses the final width. */
21
+ borderWidth?: KeyframeValue
22
+ /** iOS: opacity of one existing boxShadow, from 0 to 1. */
23
+ shadowOpacity?: KeyframeValue
24
+ /** iOS: native blur radius in DIPs (half Core's boxShadow blurRadius). */
25
+ shadowRadius?: KeyframeValue
26
+ /** iOS: offset of one existing boxShadow, in DIPs. */
27
+ shadowOffsetX?: KeyframeValue
28
+ shadowOffsetY?: KeyframeValue
29
+ /** Android: base elevation in DIPs. */
30
+ elevation?: KeyframeValue
31
+ /** Android: dynamic Z offset in DIPs. Native state changes may replace it. */
32
+ translationZ?: KeyframeValue
8
33
  opacity?: KeyframeValue
9
34
  /** Same as `translateX`, in DIPs. */
10
35
  x?: KeyframeValue
@@ -20,10 +45,31 @@ export interface Keyframes {
20
45
  rotateY?: KeyframeValue
21
46
  }
22
47
 
48
+ /** Properties that can be timed or stopped independently. A path owns x/y and optionally rotate. */
49
+ export type KeyframeProperty = Exclude<keyof Keyframes, 'path' | 'orient'>
50
+
51
+ /** One value per property, as `set()` applies them. */
52
+ export type Values = {
53
+ [K in KeyframeProperty]?: Exclude<Keyframes[K], readonly unknown[]>
54
+ }
55
+
23
56
  /** A view, or anything that holds one: a NativeScript-Vue element, an Angular `ElementRef`. */
24
57
  export type Target =
25
58
  View | { readonly nativeView: View } | { readonly nativeElement: View }
26
59
 
60
+ /** Translation velocities in DIPs per second. Omitted axes keep their current motion. */
61
+ export interface DecayVelocity {
62
+ x?: number
63
+ y?: number
64
+ }
65
+
66
+ export interface DecayOptions {
67
+ /** Velocity multiplier per millisecond, between 0 and 1 exclusively. Defaults to 0.998. */
68
+ friction?: number
69
+ /** Spring to the closest stop to the projected landing, shared or set per axis. */
70
+ snap?: readonly number[] | { x?: readonly number[]; y?: readonly number[] }
71
+ }
72
+
27
73
  export interface Stagger {
28
74
  /** Milliseconds between one target and the next. */
29
75
  each: number
@@ -32,10 +78,12 @@ export interface Stagger {
32
78
  }
33
79
 
34
80
  export type AnimateOptions = Transition & {
35
- [K in keyof Keyframes]?: Transition | SpringName
81
+ [K in KeyframeProperty | 'path']?: Transition | SpringName
36
82
  } & {
37
83
  /** Offset translations/rotations and multiply scale/opacity by their values at the start. */
38
84
  relative?: boolean
39
85
  /** With several targets, delays each one by this much more than the previous. */
40
86
  stagger?: number | Stagger
87
+ /** iOS impact feedback at arrival, or at explicit points in a finite animation. */
88
+ haptic?: Haptic
41
89
  }