@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.
- package/README.md +385 -18
- package/dist/animate.d.ts +19 -3
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +186 -14
- package/dist/animate.js.map +1 -1
- package/dist/color.d.ts +11 -0
- package/dist/color.d.ts.map +1 -0
- package/dist/color.js +73 -0
- package/dist/color.js.map +1 -0
- package/dist/config.d.ts +31 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +73 -0
- package/dist/config.js.map +1 -0
- package/dist/decay.d.ts +11 -0
- package/dist/decay.d.ts.map +1 -0
- package/dist/decay.js +22 -0
- package/dist/decay.js.map +1 -0
- package/dist/decoration.d.ts +18 -0
- package/dist/decoration.d.ts.map +1 -0
- package/dist/decoration.js +155 -0
- package/dist/decoration.js.map +1 -0
- package/dist/drag-input.d.ts +15 -0
- package/dist/drag-input.d.ts.map +1 -0
- package/dist/drag-input.js +66 -0
- package/dist/drag-input.js.map +1 -0
- package/dist/drag-physics.d.ts +20 -0
- package/dist/drag-physics.d.ts.map +1 -0
- package/dist/drag-physics.js +75 -0
- package/dist/drag-physics.js.map +1 -0
- package/dist/drag.d.ts +58 -0
- package/dist/drag.d.ts.map +1 -0
- package/dist/drag.js +324 -0
- package/dist/drag.js.map +1 -0
- package/dist/driver/android-color.d.ts +20 -0
- package/dist/driver/android-color.d.ts.map +1 -0
- package/dist/driver/android-color.js +136 -0
- package/dist/driver/android-color.js.map +1 -0
- package/dist/driver/android-decoration.d.ts +22 -0
- package/dist/driver/android-decoration.d.ts.map +1 -0
- package/dist/driver/android-decoration.js +139 -0
- package/dist/driver/android-decoration.js.map +1 -0
- package/dist/driver/android-gradient.d.ts +5 -0
- package/dist/driver/android-gradient.d.ts.map +1 -0
- package/dist/driver/android-gradient.js +89 -0
- package/dist/driver/android-gradient.js.map +1 -0
- package/dist/driver/android-playback.d.ts +13 -0
- package/dist/driver/android-playback.d.ts.map +1 -0
- package/dist/driver/android-playback.js +141 -0
- package/dist/driver/android-playback.js.map +1 -0
- package/dist/driver/android.d.ts +1 -1
- package/dist/driver/android.d.ts.map +1 -1
- package/dist/driver/android.js +165 -13
- package/dist/driver/android.js.map +1 -1
- package/dist/driver/index.d.ts +25 -2
- package/dist/driver/index.d.ts.map +1 -1
- package/dist/driver/index.js.map +1 -1
- package/dist/driver/ios-color.d.ts +16 -0
- package/dist/driver/ios-color.d.ts.map +1 -0
- package/dist/driver/ios-color.js +392 -0
- package/dist/driver/ios-color.js.map +1 -0
- package/dist/driver/ios-decoration.d.ts +17 -0
- package/dist/driver/ios-decoration.d.ts.map +1 -0
- package/dist/driver/ios-decoration.js +271 -0
- package/dist/driver/ios-decoration.js.map +1 -0
- package/dist/driver/ios-gradient.d.ts +5 -0
- package/dist/driver/ios-gradient.d.ts.map +1 -0
- package/dist/driver/ios-gradient.js +109 -0
- package/dist/driver/ios-gradient.js.map +1 -0
- package/dist/driver/ios-layout.d.ts +9 -0
- package/dist/driver/ios-layout.d.ts.map +1 -0
- package/dist/driver/ios-layout.js +52 -0
- package/dist/driver/ios-layout.js.map +1 -0
- package/dist/driver/ios-playback.d.ts +10 -0
- package/dist/driver/ios-playback.d.ts.map +1 -0
- package/dist/driver/ios-playback.js +47 -0
- package/dist/driver/ios-playback.js.map +1 -0
- package/dist/driver/ios-shadow.d.ts +6 -0
- package/dist/driver/ios-shadow.d.ts.map +1 -0
- package/dist/driver/ios-shadow.js +52 -0
- package/dist/driver/ios-shadow.js.map +1 -0
- package/dist/driver/ios.d.ts.map +1 -1
- package/dist/driver/ios.js +163 -28
- package/dist/driver/ios.js.map +1 -1
- package/dist/engine.d.ts +75 -5
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +293 -23
- package/dist/engine.js.map +1 -1
- package/dist/gradient.d.ts +90 -0
- package/dist/gradient.d.ts.map +1 -0
- package/dist/gradient.js +247 -0
- package/dist/gradient.js.map +1 -0
- package/dist/haptics.d.ts +21 -0
- package/dist/haptics.d.ts.map +1 -0
- package/dist/haptics.js +120 -0
- package/dist/haptics.js.map +1 -0
- package/dist/image-effect-native.d.ts +26 -0
- package/dist/image-effect-native.d.ts.map +1 -0
- package/dist/image-effect-native.js +73 -0
- package/dist/image-effect-native.js.map +1 -0
- package/dist/image-effect.d.ts +50 -0
- package/dist/image-effect.d.ts.map +1 -0
- package/dist/image-effect.js +290 -0
- package/dist/image-effect.js.map +1 -0
- package/dist/image-owner.d.ts +3 -0
- package/dist/image-owner.d.ts.map +1 -0
- package/dist/image-owner.js +11 -0
- package/dist/image-owner.js.map +1 -0
- package/dist/index.d.ts +47 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -2
- package/dist/index.js.map +1 -1
- package/dist/interaction-surface.d.ts +12 -0
- package/dist/interaction-surface.d.ts.map +1 -0
- package/dist/interaction-surface.js +36 -0
- package/dist/interaction-surface.js.map +1 -0
- package/dist/items.d.ts +20 -0
- package/dist/items.d.ts.map +1 -0
- package/dist/items.js +109 -0
- package/dist/items.js.map +1 -0
- package/dist/keyboard.d.ts +41 -0
- package/dist/keyboard.d.ts.map +1 -0
- package/dist/keyboard.js +256 -0
- package/dist/keyboard.js.map +1 -0
- package/dist/label.d.ts +7 -0
- package/dist/label.d.ts.map +1 -0
- package/dist/label.js +60 -0
- package/dist/label.js.map +1 -0
- package/dist/layout.d.ts +13 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +348 -0
- package/dist/layout.js.map +1 -0
- package/dist/liquid-grid.d.ts +3 -0
- package/dist/liquid-grid.d.ts.map +1 -0
- package/dist/liquid-grid.js +24 -0
- package/dist/liquid-grid.js.map +1 -0
- package/dist/liquid.d.ts +18 -0
- package/dist/liquid.d.ts.map +1 -0
- package/dist/liquid.js +234 -0
- package/dist/liquid.js.map +1 -0
- package/dist/loader.d.ts +19 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +173 -0
- package/dist/loader.js.map +1 -0
- package/dist/motion-scope.d.ts +39 -0
- package/dist/motion-scope.d.ts.map +1 -0
- package/dist/motion-scope.js +238 -0
- package/dist/motion-scope.js.map +1 -0
- package/dist/motion-states.d.ts +43 -0
- package/dist/motion-states.d.ts.map +1 -0
- package/dist/motion-states.js +220 -0
- package/dist/motion-states.js.map +1 -0
- package/dist/particles.d.ts +39 -0
- package/dist/particles.d.ts.map +1 -0
- package/dist/particles.js +265 -0
- package/dist/particles.js.map +1 -0
- package/dist/path.d.ts +12 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +90 -0
- package/dist/path.js.map +1 -0
- package/dist/playback-group.d.ts +6 -0
- package/dist/playback-group.d.ts.map +1 -0
- package/dist/playback-group.js +141 -0
- package/dist/playback-group.js.map +1 -0
- package/dist/playback.d.ts +25 -0
- package/dist/playback.d.ts.map +1 -0
- package/dist/playback.js +35 -0
- package/dist/playback.js.map +1 -0
- package/dist/press.d.ts.map +1 -1
- package/dist/press.js +23 -7
- package/dist/press.js.map +1 -1
- package/dist/properties.d.ts +6 -3
- package/dist/properties.d.ts.map +1 -1
- package/dist/properties.js +26 -2
- package/dist/properties.js.map +1 -1
- package/dist/reveal.d.ts +25 -0
- package/dist/reveal.d.ts.map +1 -0
- package/dist/reveal.js +294 -0
- package/dist/reveal.js.map +1 -0
- package/dist/roll-diff.d.ts +22 -0
- package/dist/roll-diff.d.ts.map +1 -0
- package/dist/roll-diff.js +36 -0
- package/dist/roll-diff.js.map +1 -0
- package/dist/roll.d.ts +29 -0
- package/dist/roll.d.ts.map +1 -0
- package/dist/roll.js +442 -0
- package/dist/roll.js.map +1 -0
- package/dist/scroll-linked.d.ts +10 -0
- package/dist/scroll-linked.d.ts.map +1 -0
- package/dist/scroll-linked.js +58 -0
- package/dist/scroll-linked.js.map +1 -0
- package/dist/sequence.d.ts +10 -5
- package/dist/sequence.d.ts.map +1 -1
- package/dist/sequence.js +208 -14
- package/dist/sequence.js.map +1 -1
- package/dist/shimmer.d.ts +12 -0
- package/dist/shimmer.d.ts.map +1 -0
- package/dist/shimmer.js +161 -0
- package/dist/shimmer.js.map +1 -0
- package/dist/svg-path.d.ts +14 -0
- package/dist/svg-path.d.ts.map +1 -0
- package/dist/svg-path.js +281 -0
- package/dist/svg-path.js.map +1 -0
- package/dist/symbol-effect.d.ts +23 -0
- package/dist/symbol-effect.d.ts.map +1 -0
- package/dist/symbol-effect.js +187 -0
- package/dist/symbol-effect.js.map +1 -0
- package/dist/text-layout.d.ts +30 -0
- package/dist/text-layout.d.ts.map +1 -0
- package/dist/text-layout.js +185 -0
- package/dist/text-layout.js.map +1 -0
- package/dist/text.d.ts +17 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +263 -0
- package/dist/text.js.map +1 -0
- package/dist/track.d.ts +3 -1
- package/dist/track.d.ts.map +1 -1
- package/dist/track.js +266 -13
- package/dist/track.js.map +1 -1
- package/dist/transition.d.ts +32 -1
- package/dist/transition.d.ts.map +1 -1
- package/dist/transition.js +58 -6
- package/dist/transition.js.map +1 -1
- package/dist/types.d.ts +48 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/vue/index.d.ts +95 -0
- package/dist/vue/index.d.ts.map +1 -0
- package/dist/vue/index.js +163 -0
- package/dist/vue/index.js.map +1 -0
- package/dist/zoom-native.d.ts +30 -0
- package/dist/zoom-native.d.ts.map +1 -0
- package/dist/zoom-native.js +56 -0
- package/dist/zoom-native.js.map +1 -0
- package/dist/zoomable.d.ts +48 -0
- package/dist/zoomable.d.ts.map +1 -0
- package/dist/zoomable.js +249 -0
- package/dist/zoomable.js.map +1 -0
- package/package.json +29 -5
- package/platforms/android/include.gradle +7 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/ColorTarget.java +102 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/DecorationTarget.java +154 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/GradientLayer.java +195 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/ImageEffect.java +288 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFollower.java +91 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardObserver.java +97 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/NumberRoll.java +190 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/PhotoZoom.java +250 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/TextReveal.java +180 -0
- package/platforms/android/proguard-rules.pro +21 -0
- package/platforms/ios/build.xcconfig +1 -0
- package/platforms/ios/src/JoltImageEffect.h +25 -0
- package/platforms/ios/src/JoltImageEffect.mm +242 -0
- package/platforms/ios/src/JoltInteractiveAnimation.h +18 -0
- package/platforms/ios/src/JoltInteractiveAnimation.m +123 -0
- package/platforms/ios/src/JoltPhotoZoom.h +23 -0
- package/platforms/ios/src/JoltPhotoZoom.m +229 -0
- package/platforms/ios/src/module.modulemap +6 -0
- package/src/android-plugin.d.ts +184 -0
- package/src/animate.ts +249 -36
- package/src/color.ts +101 -0
- package/src/config.ts +84 -0
- package/src/decay.ts +45 -0
- package/src/decoration.ts +180 -0
- package/src/drag-input.ts +74 -0
- package/src/drag-physics.ts +110 -0
- package/src/drag.ts +395 -0
- package/src/driver/android-color.ts +170 -0
- package/src/driver/android-decoration.ts +182 -0
- package/src/driver/android-gradient.ts +109 -0
- package/src/driver/android-playback.ts +157 -0
- package/src/driver/android.ts +208 -20
- package/src/driver/index.ts +37 -2
- package/src/driver/ios-color.ts +441 -0
- package/src/driver/ios-decoration.ts +332 -0
- package/src/driver/ios-gradient.ts +114 -0
- package/src/driver/ios-layout.ts +63 -0
- package/src/driver/ios-playback.ts +51 -0
- package/src/driver/ios-shadow.ts +50 -0
- package/src/driver/ios.ts +193 -33
- package/src/engine.ts +321 -27
- package/src/gradient.ts +372 -0
- package/src/haptics.ts +148 -0
- package/src/image-effect-native.ts +129 -0
- package/src/image-effect.ts +336 -0
- package/src/image-owner.ts +12 -0
- package/src/index.ts +75 -4
- package/src/interaction-surface.ts +41 -0
- package/src/ios-plugin.d.ts +81 -0
- package/src/items.ts +140 -0
- package/src/keyboard.ts +291 -0
- package/src/label.ts +76 -0
- package/src/layout.ts +408 -0
- package/src/liquid-grid.ts +28 -0
- package/src/liquid.ts +287 -0
- package/src/loader.ts +202 -0
- package/src/motion-scope.ts +279 -0
- package/src/motion-states.ts +269 -0
- package/src/particles.ts +323 -0
- package/src/path.ts +124 -0
- package/src/playback-group.ts +143 -0
- package/src/playback.ts +62 -0
- package/src/press.ts +26 -10
- package/src/properties.ts +32 -6
- package/src/reveal.ts +374 -0
- package/src/roll-diff.ts +57 -0
- package/src/roll.ts +515 -0
- package/src/scroll-linked.ts +73 -0
- package/src/sequence.ts +204 -14
- package/src/shimmer.ts +180 -0
- package/src/svg-path.ts +355 -0
- package/src/symbol-effect.ts +226 -0
- package/src/text-layout.ts +228 -0
- package/src/text.ts +279 -0
- package/src/track.ts +347 -12
- package/src/transition.ts +93 -6
- package/src/types.ts +49 -1
- package/src/vue/index.ts +231 -0
- package/src/zoom-native.ts +124 -0
- package/src/zoomable.ts +330 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { getDriver } from './driver/index.js'
|
|
2
|
+
|
|
3
|
+
export interface JoltConfig {
|
|
4
|
+
/** Playback speed for every engine animation. `0.2` plays everything five times slower. */
|
|
5
|
+
timeScale?: number
|
|
6
|
+
/**
|
|
7
|
+
* When transforms, paths, decorations and decay jump to their end instead of animating.
|
|
8
|
+
* `user` follows the platform setting. Opacity and colors still animate.
|
|
9
|
+
*/
|
|
10
|
+
reducedMotion?: 'user' | 'always' | 'never'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const settings: Required<JoltConfig> = { timeScale: 1, reducedMotion: 'user' }
|
|
14
|
+
const listeners = new Set<(reduced: boolean) => void>()
|
|
15
|
+
let reported = false
|
|
16
|
+
let unwatch: (() => void) | undefined
|
|
17
|
+
const preferences: readonly JoltConfig['reducedMotion'][] = ['user', 'always', 'never']
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Global settings, applied to animations that start after the call.
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* configure({ timeScale: 0.2 }) // tune a spring in slow motion
|
|
24
|
+
* configure({ reducedMotion: 'always' }) // check the reduced layout
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function configure(options: JoltConfig): void {
|
|
28
|
+
if (options === null || typeof options !== 'object')
|
|
29
|
+
throw new TypeError('configure() takes an options object.')
|
|
30
|
+
const { timeScale, reducedMotion } = options
|
|
31
|
+
if (timeScale !== undefined && (!Number.isFinite(timeScale) || timeScale <= 0))
|
|
32
|
+
throw new RangeError(`Time scale must be a positive finite number, got ${timeScale}.`)
|
|
33
|
+
if (reducedMotion !== undefined && !preferences.includes(reducedMotion))
|
|
34
|
+
throw new TypeError(
|
|
35
|
+
`Reduced motion must be 'user', 'always' or 'never', got ${String(reducedMotion)}.`,
|
|
36
|
+
)
|
|
37
|
+
if (timeScale !== undefined) settings.timeScale = timeScale
|
|
38
|
+
if (reducedMotion !== undefined) {
|
|
39
|
+
settings.reducedMotion = reducedMotion
|
|
40
|
+
changed()
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function timeScale(): number {
|
|
45
|
+
return settings.timeScale
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Whether motion is reduced right now. `user` asks the platform each time. */
|
|
49
|
+
export function reducedMotion(): boolean {
|
|
50
|
+
if (settings.reducedMotion === 'user') return getDriver().reducedMotion?.() ?? false
|
|
51
|
+
return settings.reducedMotion === 'always'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Calls back with the new value whenever `reducedMotion()` changes, from `configure()`
|
|
56
|
+
* or the platform setting. Returns a cleanup function.
|
|
57
|
+
*
|
|
58
|
+
* ```ts
|
|
59
|
+
* const off = onReducedMotion((reduced) => (illustration.visibility = reduced ? 'visible' : 'collapse'))
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export function onReducedMotion(callback: (reduced: boolean) => void): () => void {
|
|
63
|
+
if (typeof callback !== 'function')
|
|
64
|
+
throw new TypeError('onReducedMotion() takes a callback.')
|
|
65
|
+
const listener = (reduced: boolean) => callback(reduced)
|
|
66
|
+
if (listeners.size === 0) {
|
|
67
|
+
reported = reducedMotion()
|
|
68
|
+
unwatch = getDriver().watchReducedMotion?.(changed)
|
|
69
|
+
}
|
|
70
|
+
listeners.add(listener)
|
|
71
|
+
return () => {
|
|
72
|
+
if (!listeners.delete(listener) || listeners.size > 0) return
|
|
73
|
+
unwatch?.()
|
|
74
|
+
unwatch = undefined
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function changed() {
|
|
79
|
+
if (listeners.size === 0) return
|
|
80
|
+
const reduced = reducedMotion()
|
|
81
|
+
if (reduced === reported) return
|
|
82
|
+
reported = reduced
|
|
83
|
+
for (const listener of [...listeners]) listener(reduced)
|
|
84
|
+
}
|
package/src/decay.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface DecayMotion {
|
|
2
|
+
duration: number
|
|
3
|
+
to: number
|
|
4
|
+
position(time: number): number
|
|
5
|
+
velocity(time: number): number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Velocity retains this fraction every millisecond, independently of the display's refresh rate. */
|
|
9
|
+
export const DEFAULT_FRICTION = 0.998
|
|
10
|
+
|
|
11
|
+
/** Exponential motion, with velocity in units per second and time in milliseconds. */
|
|
12
|
+
export function solveDecay(
|
|
13
|
+
from: number,
|
|
14
|
+
velocity: number,
|
|
15
|
+
friction: number,
|
|
16
|
+
precision: number,
|
|
17
|
+
): DecayMotion {
|
|
18
|
+
const rate = -Math.log(friction)
|
|
19
|
+
const distance = velocity / (1000 * rate)
|
|
20
|
+
const to = from + distance
|
|
21
|
+
// Stop only when both the remaining distance and speed are imperceptible.
|
|
22
|
+
const duration =
|
|
23
|
+
velocity === 0
|
|
24
|
+
? 0
|
|
25
|
+
: Math.ceil(
|
|
26
|
+
Math.max(
|
|
27
|
+
0,
|
|
28
|
+
Math.log(Math.abs(distance) / precision) / rate,
|
|
29
|
+
Math.log(Math.abs(velocity) / (precision * 10)) / rate,
|
|
30
|
+
),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if (!Number.isFinite(to) || !Number.isFinite(duration) || duration > 2_147_483_647) {
|
|
34
|
+
throw new RangeError('Decay velocity and friction produce an unplayable motion.')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
duration,
|
|
39
|
+
to,
|
|
40
|
+
position: (time) =>
|
|
41
|
+
time >= duration ? to : from - distance * Math.expm1(-rate * Math.max(0, time)),
|
|
42
|
+
velocity: (time) =>
|
|
43
|
+
time >= duration ? 0 : velocity * Math.exp(-rate * Math.max(0, time)),
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { preserveZeroShadow } from './driver/ios-shadow.js'
|
|
2
|
+
import { Color, Utils, isAndroid, type View } from '@nativescript/core'
|
|
3
|
+
|
|
4
|
+
type Shadow = Exclude<View['boxShadow'], string>[number]
|
|
5
|
+
export type DecorationProperty =
|
|
6
|
+
| 'borderRadius'
|
|
7
|
+
| 'borderWidth'
|
|
8
|
+
| 'shadowOpacity'
|
|
9
|
+
| 'shadowRadius'
|
|
10
|
+
| 'shadowOffsetX'
|
|
11
|
+
| 'shadowOffsetY'
|
|
12
|
+
| 'elevation'
|
|
13
|
+
| 'translationZ'
|
|
14
|
+
|
|
15
|
+
export const decorationProperties: readonly DecorationProperty[] = [
|
|
16
|
+
'borderRadius',
|
|
17
|
+
'borderWidth',
|
|
18
|
+
'shadowOpacity',
|
|
19
|
+
'shadowRadius',
|
|
20
|
+
'shadowOffsetX',
|
|
21
|
+
'shadowOffsetY',
|
|
22
|
+
'elevation',
|
|
23
|
+
'translationZ',
|
|
24
|
+
]
|
|
25
|
+
export const radiusKeys = [
|
|
26
|
+
'borderTopLeftRadius',
|
|
27
|
+
'borderTopRightRadius',
|
|
28
|
+
'borderBottomRightRadius',
|
|
29
|
+
'borderBottomLeftRadius',
|
|
30
|
+
] as const
|
|
31
|
+
export const widthKeys = [
|
|
32
|
+
'borderTopWidth',
|
|
33
|
+
'borderRightWidth',
|
|
34
|
+
'borderBottomWidth',
|
|
35
|
+
'borderLeftWidth',
|
|
36
|
+
] as const
|
|
37
|
+
const writing = new WeakSet<View>()
|
|
38
|
+
export const isWritingDecoration = (view: View) => writing.has(view)
|
|
39
|
+
export const isDecorationProperty = (key: string): key is DecorationProperty =>
|
|
40
|
+
decorationProperties.includes(key as DecorationProperty)
|
|
41
|
+
|
|
42
|
+
/** Core lengths may be numbers, px strings or parsed { value, unit } lengths. */
|
|
43
|
+
export function dip(value: unknown): number {
|
|
44
|
+
if (value === undefined || value === null) return 0
|
|
45
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
46
|
+
if (typeof value === 'string') {
|
|
47
|
+
const match = /^(-?(?:\d+(?:\.\d*)?|\.\d+))(dip|px)?$/.exec(value.trim())
|
|
48
|
+
if (match)
|
|
49
|
+
return Number(match[1]) / (match[2] === 'px' ? Utils.layout.getDisplayDensity() : 1)
|
|
50
|
+
}
|
|
51
|
+
if (typeof value === 'object' && 'value' in value && 'unit' in value) {
|
|
52
|
+
const length = value as { value: number; unit: string }
|
|
53
|
+
if (Number.isFinite(length.value) && (length.unit === 'dip' || length.unit === 'px'))
|
|
54
|
+
return length.value / (length.unit === 'px' ? Utils.layout.getDisplayDensity() : 1)
|
|
55
|
+
}
|
|
56
|
+
throw new TypeError('Decoration animations require finite DIP or px lengths.')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function singleShadow(view: View): Shadow {
|
|
60
|
+
const shadows = view.boxShadow
|
|
61
|
+
if (!Array.isArray(shadows) || shadows.length !== 1 || shadows[0]!.inset)
|
|
62
|
+
throw new TypeError('Shadow animation requires one existing outer boxShadow.')
|
|
63
|
+
return shadows[0]!
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function validateDecorationTarget(view: View, property: DecorationProperty) {
|
|
67
|
+
const shadow = property.startsWith('shadow')
|
|
68
|
+
const z = property === 'elevation' || property === 'translationZ'
|
|
69
|
+
if ((shadow && isAndroid) || (z && !isAndroid))
|
|
70
|
+
throw new TypeError(`"${property}" is only supported on ${z ? 'Android' : 'iOS'}.`)
|
|
71
|
+
if (z) return
|
|
72
|
+
if (view.style.clipPath || view.style.cornerShape === 'squircle')
|
|
73
|
+
throw new TypeError(
|
|
74
|
+
'Decoration animations require circular corners without a clipPath.',
|
|
75
|
+
)
|
|
76
|
+
for (const keys of [radiusKeys, widthKeys]) {
|
|
77
|
+
const values = keys.map((key) => dip(view[key]))
|
|
78
|
+
if (values.some((value) => value !== values[0]))
|
|
79
|
+
throw new TypeError(
|
|
80
|
+
'Decoration animations require uniform border widths and radii.',
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
const colors = [
|
|
84
|
+
view.borderTopColor,
|
|
85
|
+
view.borderRightColor,
|
|
86
|
+
view.borderBottomColor,
|
|
87
|
+
view.borderLeftColor,
|
|
88
|
+
].map((color) => color?.argb ?? 0)
|
|
89
|
+
if (colors.some((color) => color !== colors[0]))
|
|
90
|
+
throw new TypeError('Decoration animations require uniform border colors.')
|
|
91
|
+
if (shadow) {
|
|
92
|
+
const value = singleShadow(view)
|
|
93
|
+
for (const length of [
|
|
94
|
+
value.offsetX,
|
|
95
|
+
value.offsetY,
|
|
96
|
+
value.blurRadius,
|
|
97
|
+
value.spreadRadius,
|
|
98
|
+
])
|
|
99
|
+
dip(length)
|
|
100
|
+
}
|
|
101
|
+
if (
|
|
102
|
+
property === 'borderRadius' &&
|
|
103
|
+
isAndroid &&
|
|
104
|
+
view.boxShadow &&
|
|
105
|
+
(!Array.isArray(view.boxShadow) || view.boxShadow.length)
|
|
106
|
+
)
|
|
107
|
+
throw new TypeError(
|
|
108
|
+
'Animate Android radius with elevation shadows; CSS boxShadow caches its shape.',
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function validateDecorationFrames(
|
|
113
|
+
property: DecorationProperty,
|
|
114
|
+
frames: readonly (number | null)[],
|
|
115
|
+
) {
|
|
116
|
+
if (
|
|
117
|
+
frames.some((value) => value !== null && clampDecoration(property, value) !== value)
|
|
118
|
+
)
|
|
119
|
+
throw new RangeError(`Invalid keyframes for "${property}".`)
|
|
120
|
+
}
|
|
121
|
+
export function clampDecoration(property: DecorationProperty, value: number): number {
|
|
122
|
+
if (property === 'shadowOpacity') return Math.max(0, Math.min(1, value))
|
|
123
|
+
return property === 'borderRadius' ||
|
|
124
|
+
property === 'borderWidth' ||
|
|
125
|
+
property === 'shadowRadius' ||
|
|
126
|
+
property === 'elevation'
|
|
127
|
+
? Math.max(0, value)
|
|
128
|
+
: value
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function readDecoration(view: View, property: DecorationProperty): number {
|
|
132
|
+
if (property === 'borderRadius') {
|
|
133
|
+
const radius = !isAndroid ? view.nativeViewProtected?.layer?.cornerRadius : undefined
|
|
134
|
+
return typeof radius === 'number' ? radius : dip(view.borderTopLeftRadius)
|
|
135
|
+
}
|
|
136
|
+
if (property === 'borderWidth') return dip(view.borderTopWidth)
|
|
137
|
+
if (property === 'elevation')
|
|
138
|
+
return view.nativeViewProtected?.getElevation
|
|
139
|
+
? view.nativeViewProtected.getElevation() / Utils.layout.getDisplayDensity()
|
|
140
|
+
: view.androidElevation || 0
|
|
141
|
+
if (property === 'translationZ')
|
|
142
|
+
return (
|
|
143
|
+
view.nativeViewProtected?.getTranslationZ?.() / Utils.layout.getDisplayDensity() ||
|
|
144
|
+
0
|
|
145
|
+
)
|
|
146
|
+
const shadow = singleShadow(view)
|
|
147
|
+
if (property === 'shadowOpacity')
|
|
148
|
+
return ((shadow.color?.argb ?? 0xff000000) >>> 24) / 255
|
|
149
|
+
return property === 'shadowRadius'
|
|
150
|
+
? dip(shadow.blurRadius) / 2
|
|
151
|
+
: dip(property === 'shadowOffsetX' ? shadow.offsetX : shadow.offsetY)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function writeDecoration(view: View, property: DecorationProperty, raw: number) {
|
|
155
|
+
const value = clampDecoration(property, raw)
|
|
156
|
+
writing.add(view)
|
|
157
|
+
try {
|
|
158
|
+
if (property === 'borderRadius' || property === 'borderWidth') view[property] = value
|
|
159
|
+
else if (property === 'elevation') view.androidElevation = value
|
|
160
|
+
else if (property === 'translationZ')
|
|
161
|
+
view.nativeViewProtected?.setTranslationZ?.(
|
|
162
|
+
value * Utils.layout.getDisplayDensity(),
|
|
163
|
+
)
|
|
164
|
+
else {
|
|
165
|
+
const shadow = { ...singleShadow(view) }
|
|
166
|
+
if (property === 'shadowOpacity')
|
|
167
|
+
shadow.color = new Color(
|
|
168
|
+
((Math.round(value * 255) << 24) | ((shadow.color?.argb ?? 0) & 0xffffff)) >>>
|
|
169
|
+
0,
|
|
170
|
+
)
|
|
171
|
+
else if (property === 'shadowRadius') shadow.blurRadius = value * 2
|
|
172
|
+
else if (property === 'shadowOffsetX') shadow.offsetX = value
|
|
173
|
+
else shadow.offsetY = value
|
|
174
|
+
view.boxShadow = [shadow]
|
|
175
|
+
if (!isAndroid) preserveZeroShadow(view)
|
|
176
|
+
}
|
|
177
|
+
} finally {
|
|
178
|
+
writing.delete(view)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Utils, type PanGestureEventData, type View } from '@nativescript/core'
|
|
2
|
+
import { getDriver } from './driver/index.js'
|
|
3
|
+
|
|
4
|
+
interface Sample {
|
|
5
|
+
x: number
|
|
6
|
+
y: number
|
|
7
|
+
time: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Android's raw coordinates avoid feedback from translating the touched view. */
|
|
11
|
+
export class DragInput {
|
|
12
|
+
private samples: Sample[] = []
|
|
13
|
+
private lastMoved = 0
|
|
14
|
+
reset() {
|
|
15
|
+
this.samples = []
|
|
16
|
+
this.lastMoved = 0
|
|
17
|
+
}
|
|
18
|
+
read(event: PanGestureEventData, view: View) {
|
|
19
|
+
const ios = event.ios as UIPanGestureRecognizer | undefined
|
|
20
|
+
const android = event.android?.current as android.view.MotionEvent | undefined
|
|
21
|
+
const parent = view.parent?.nativeViewProtected ?? view.nativeViewProtected
|
|
22
|
+
const delta = ios
|
|
23
|
+
? ios.translationInView(parent)
|
|
24
|
+
: { x: event.deltaX, y: event.deltaY }
|
|
25
|
+
const density = Utils.layout.getDisplayDensity()
|
|
26
|
+
const sample: Sample = android
|
|
27
|
+
? {
|
|
28
|
+
x: android.getRawX() / density,
|
|
29
|
+
y: android.getRawY() / density,
|
|
30
|
+
time: Number(android.getEventTime()),
|
|
31
|
+
}
|
|
32
|
+
: { ...delta, time: getDriver().now() }
|
|
33
|
+
if (
|
|
34
|
+
Number.isFinite(sample.x) &&
|
|
35
|
+
Number.isFinite(sample.y) &&
|
|
36
|
+
Number.isFinite(sample.time)
|
|
37
|
+
) {
|
|
38
|
+
const previous = this.samples.at(-1)
|
|
39
|
+
if (!previous || sample.x !== previous.x || sample.y !== previous.y)
|
|
40
|
+
this.lastMoved = sample.time
|
|
41
|
+
// Replace duplicate times (Android sends began + changed for the same event).
|
|
42
|
+
if (this.samples.at(-1)?.time === sample.time) this.samples.pop()
|
|
43
|
+
this.samples.push(sample)
|
|
44
|
+
while (this.samples.length > 2 && this.samples[1]!.time < sample.time - 100)
|
|
45
|
+
this.samples.shift()
|
|
46
|
+
}
|
|
47
|
+
const first = this.samples[0]
|
|
48
|
+
const elapsed = first ? sample.time - first.time : 0
|
|
49
|
+
// UIKit may retain its last movement velocity when the finger holds still
|
|
50
|
+
// before lifting. A stale velocity must not turn that hold into a fling.
|
|
51
|
+
const nativeVelocity = ios
|
|
52
|
+
? sample.time - this.lastMoved > 100
|
|
53
|
+
? { x: 0, y: 0 }
|
|
54
|
+
: ios.velocityInView(parent)
|
|
55
|
+
: undefined
|
|
56
|
+
const velocity = nativeVelocity ?? {
|
|
57
|
+
x: elapsed > 0 && elapsed <= 150 ? ((sample.x - first!.x) * 1000) / elapsed : 0,
|
|
58
|
+
y: elapsed > 0 && elapsed <= 150 ? ((sample.y - first!.y) * 1000) / elapsed : 0,
|
|
59
|
+
}
|
|
60
|
+
const action = android?.getActionMasked()
|
|
61
|
+
return {
|
|
62
|
+
x: delta.x,
|
|
63
|
+
y: delta.y,
|
|
64
|
+
velocityX: Number.isFinite(velocity.x) ? velocity.x : 0,
|
|
65
|
+
velocityY: Number.isFinite(velocity.y) ? velocity.y : 0,
|
|
66
|
+
cancelled:
|
|
67
|
+
event.state === 0 ||
|
|
68
|
+
action === 3 ||
|
|
69
|
+
action === 5 ||
|
|
70
|
+
action === 6 ||
|
|
71
|
+
(android ? android.getPointerCount() > 1 : (ios?.numberOfTouches ?? 1) > 1),
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { solveDecay } from './decay.js'
|
|
2
|
+
import { solveSpring, springPhysics } from './spring.js'
|
|
3
|
+
|
|
4
|
+
export interface DragCurve {
|
|
5
|
+
type: 'drag'
|
|
6
|
+
velocity: number
|
|
7
|
+
friction: number
|
|
8
|
+
bounds: readonly [number, number]
|
|
9
|
+
snap?: readonly number[]
|
|
10
|
+
rubberband: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const clampDrag = (value: number, [min, max]: readonly [number, number]) =>
|
|
14
|
+
Math.max(min, Math.min(max, value))
|
|
15
|
+
|
|
16
|
+
/** Bounded resistance; coordinates and the resistance length are DIPs. */
|
|
17
|
+
export function resistDrag(
|
|
18
|
+
value: number,
|
|
19
|
+
bounds: readonly [number, number],
|
|
20
|
+
length: number,
|
|
21
|
+
) {
|
|
22
|
+
const edge = clampDrag(value, bounds)
|
|
23
|
+
const distance = value - edge
|
|
24
|
+
return edge + (distance / (1 + Math.abs(distance) / length)) * 0.55
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Recover the finger-space origin when grabbing an already rubberbanded pose. */
|
|
28
|
+
export function resumeDrag(
|
|
29
|
+
value: number,
|
|
30
|
+
bounds: readonly [number, number],
|
|
31
|
+
length: number,
|
|
32
|
+
) {
|
|
33
|
+
const edge = clampDrag(value, bounds)
|
|
34
|
+
const distance = value - edge
|
|
35
|
+
// A native edge spring or external owner can exceed the usual resistance range.
|
|
36
|
+
// Expand it for this gesture so every finite visible pose has a stable inverse.
|
|
37
|
+
length = Math.max(length, Math.abs(distance) * 4)
|
|
38
|
+
return { origin: edge + distance / (0.55 - Math.abs(distance) / length), length }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function dragDestination(from: number, curve: DragCurve) {
|
|
42
|
+
const projected = solveDecay(from, curve.velocity, curve.friction, 0.25).to
|
|
43
|
+
if (curve.snap)
|
|
44
|
+
return curve.snap.reduce((best, point) =>
|
|
45
|
+
Math.abs(point - projected) < Math.abs(best - projected) ? point : best,
|
|
46
|
+
)
|
|
47
|
+
return clampDrag(projected, curve.bounds)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** One precomputed motion. Native keyframes play both the coast and edge recovery. */
|
|
51
|
+
export function solveDragRelease(from: number, curve: DragCurve) {
|
|
52
|
+
const to = dragDestination(from, curve)
|
|
53
|
+
const coast = solveDecay(from, curve.velocity, curve.friction, 0.25)
|
|
54
|
+
const edge = clampDrag(from, curve.bounds)
|
|
55
|
+
const springTo = (start: number, target: number, velocity: number) => {
|
|
56
|
+
const motion = solveSpring(
|
|
57
|
+
springPhysics({ duration: 500, bounce: 0 }),
|
|
58
|
+
start,
|
|
59
|
+
target,
|
|
60
|
+
velocity,
|
|
61
|
+
0.25,
|
|
62
|
+
)
|
|
63
|
+
const position = (time: number) => {
|
|
64
|
+
const value = time >= motion.duration ? target : motion.position(time)
|
|
65
|
+
return curve.rubberband ? value : clampDrag(value, curve.bounds)
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
...motion,
|
|
69
|
+
position,
|
|
70
|
+
velocity: (time: number) => {
|
|
71
|
+
const raw = motion.position(time)
|
|
72
|
+
return time >= motion.duration ||
|
|
73
|
+
(!curve.rubberband && raw !== clampDrag(raw, curve.bounds))
|
|
74
|
+
? 0
|
|
75
|
+
: motion.velocity(time)
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (curve.snap || from !== edge)
|
|
80
|
+
return {
|
|
81
|
+
...springTo(from, curve.snap ? to : edge, curve.velocity),
|
|
82
|
+
to: curve.snap ? to : edge,
|
|
83
|
+
}
|
|
84
|
+
if (to === coast.to) return coast
|
|
85
|
+
|
|
86
|
+
// Reach the boundary with the decay's actual velocity, then absorb that velocity.
|
|
87
|
+
const rate = -Math.log(curve.friction)
|
|
88
|
+
const collision = Math.max(
|
|
89
|
+
0,
|
|
90
|
+
-Math.log1p((-(to - from) * rate * 1000) / curve.velocity) / rate,
|
|
91
|
+
)
|
|
92
|
+
const recovery = springTo(to, to, coast.velocity(collision))
|
|
93
|
+
const duration = collision + (curve.rubberband ? recovery.duration : 0)
|
|
94
|
+
return {
|
|
95
|
+
to,
|
|
96
|
+
duration,
|
|
97
|
+
position: (time: number) =>
|
|
98
|
+
time >= duration
|
|
99
|
+
? to
|
|
100
|
+
: time < collision
|
|
101
|
+
? coast.position(time)
|
|
102
|
+
: recovery.position(time - collision),
|
|
103
|
+
velocity: (time: number) =>
|
|
104
|
+
time >= duration
|
|
105
|
+
? 0
|
|
106
|
+
: time < collision
|
|
107
|
+
? coast.velocity(time)
|
|
108
|
+
: recovery.velocity(time - collision),
|
|
109
|
+
}
|
|
110
|
+
}
|