@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
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Application,
|
|
3
|
+
Image,
|
|
4
|
+
ImageSource,
|
|
5
|
+
isIOS,
|
|
6
|
+
type EventData,
|
|
7
|
+
} from '@nativescript/core'
|
|
8
|
+
import { toView } from './animate.js'
|
|
9
|
+
import { reducedMotion, timeScale } from './config.js'
|
|
10
|
+
import type { Animation } from './engine.js'
|
|
11
|
+
import { ownImage } from './image-owner.js'
|
|
12
|
+
import { createImageRenderer, type ImageRenderer } from './image-effect-native.js'
|
|
13
|
+
import type { Target } from './types.js'
|
|
14
|
+
|
|
15
|
+
export type ImageEffectName = 'ripple' | 'distortion' | 'dissolve'
|
|
16
|
+
export interface ImageEffectOptions {
|
|
17
|
+
/** Origin in target-local DIPs; defaults to the center. */
|
|
18
|
+
origin?: { x: number; y: number }
|
|
19
|
+
/** Maximum displacement in DIPs, 0..64. Defaults to 8. */
|
|
20
|
+
amplitude?: number
|
|
21
|
+
/** Influence radius in DIPs, greater than zero. Defaults to 96. */
|
|
22
|
+
radius?: number
|
|
23
|
+
/** Milliseconds for a finite effect (or drag release). Defaults to 650. */
|
|
24
|
+
duration?: number
|
|
25
|
+
/** Deterministic dissolve noise seed, 0..65535. Defaults to 1. */
|
|
26
|
+
seed?: number
|
|
27
|
+
/** Loaded destination raster for dissolve. */
|
|
28
|
+
to?: ImageSource
|
|
29
|
+
/** Prepare at progress zero without starting. */
|
|
30
|
+
paused?: boolean
|
|
31
|
+
}
|
|
32
|
+
export interface ImageEffectDiagnostics {
|
|
33
|
+
backend: 'waiting' | 'metal' | 'agsl' | 'fallback'
|
|
34
|
+
reason?: 'reduced-motion' | 'unsupported' | 'not-visible'
|
|
35
|
+
/** Native setup error, when available. Intended for developer diagnostics. */
|
|
36
|
+
detail?: string
|
|
37
|
+
frames: number
|
|
38
|
+
textureBytes: number
|
|
39
|
+
peakTextureBytes: number
|
|
40
|
+
preparationMs: number
|
|
41
|
+
longestFrameMs: number
|
|
42
|
+
}
|
|
43
|
+
export interface ImageEffect extends Animation {
|
|
44
|
+
readonly effect: ImageEffectName
|
|
45
|
+
readonly paused: boolean
|
|
46
|
+
/** Finite progress; distortion rejects seeking because it follows native touch input. */
|
|
47
|
+
progress: number
|
|
48
|
+
readonly diagnostics: Readonly<ImageEffectDiagnostics>
|
|
49
|
+
pause(): void
|
|
50
|
+
play(): void
|
|
51
|
+
/** Reverse a finite effect from its current pixels. Terminal handles remain terminal. */
|
|
52
|
+
reverse(): void
|
|
53
|
+
/** Complete immediately at the current direction's endpoint. */
|
|
54
|
+
finish(): void
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const names: readonly ImageEffectName[] = ['ripple', 'distortion', 'dissolve']
|
|
58
|
+
const surfaceChanges = [
|
|
59
|
+
'tintColorChange',
|
|
60
|
+
...['TopLeft', 'TopRight', 'BottomRight', 'BottomLeft'].map(
|
|
61
|
+
(corner) => `border${corner}RadiusChange`,
|
|
62
|
+
),
|
|
63
|
+
...['Top', 'Right', 'Bottom', 'Left'].flatMap((edge) => [
|
|
64
|
+
`padding${edge}Change`,
|
|
65
|
+
`border${edge}WidthChange`,
|
|
66
|
+
]),
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
/** Optional native image effects. Stop restores the source; dissolve completion commits its destination. */
|
|
70
|
+
export function imageEffect(
|
|
71
|
+
target: Target,
|
|
72
|
+
effect: ImageEffectName,
|
|
73
|
+
options: ImageEffectOptions = {},
|
|
74
|
+
): ImageEffect {
|
|
75
|
+
const image = toView(target)
|
|
76
|
+
if (!(image instanceof Image))
|
|
77
|
+
throw new TypeError('imageEffect() expects a NativeScript Image.')
|
|
78
|
+
if (!names.includes(effect))
|
|
79
|
+
throw new TypeError(`Unknown image effect: ${String(effect)}.`)
|
|
80
|
+
if (!options || typeof options !== 'object')
|
|
81
|
+
throw new TypeError('Image effect options must be an object.')
|
|
82
|
+
const amplitude = options.amplitude ?? 8,
|
|
83
|
+
radius = options.radius ?? 96
|
|
84
|
+
const duration = options.duration ?? 650,
|
|
85
|
+
seed = options.seed ?? 1
|
|
86
|
+
if (!Number.isFinite(amplitude) || amplitude < 0 || amplitude > 64)
|
|
87
|
+
throw new RangeError('Image displacement must be between 0 and 64 DIPs.')
|
|
88
|
+
if (
|
|
89
|
+
!Number.isFinite(radius) ||
|
|
90
|
+
radius <= 0 ||
|
|
91
|
+
!Number.isFinite(duration) ||
|
|
92
|
+
duration <= 0
|
|
93
|
+
)
|
|
94
|
+
throw new RangeError('Image radius and duration must be positive finite numbers.')
|
|
95
|
+
if (!Number.isInteger(seed) || seed < 0 || seed > 65535)
|
|
96
|
+
throw new RangeError('Image seed must be an integer between 0 and 65535.')
|
|
97
|
+
if (
|
|
98
|
+
options.origin &&
|
|
99
|
+
(!Number.isFinite(options.origin.x) || !Number.isFinite(options.origin.y))
|
|
100
|
+
)
|
|
101
|
+
throw new RangeError('Image origin must contain finite local coordinates.')
|
|
102
|
+
if (options.paused !== undefined && typeof options.paused !== 'boolean')
|
|
103
|
+
throw new TypeError('paused must be a boolean.')
|
|
104
|
+
if (
|
|
105
|
+
effect === 'dissolve' &&
|
|
106
|
+
(!(options.to instanceof ImageSource) ||
|
|
107
|
+
!Number.isFinite(options.to.width) ||
|
|
108
|
+
!Number.isFinite(options.to.height) ||
|
|
109
|
+
options.to.width <= 0 ||
|
|
110
|
+
options.to.height <= 0)
|
|
111
|
+
)
|
|
112
|
+
throw new TypeError('Dissolve requires a loaded destination ImageSource.')
|
|
113
|
+
return new Effect(image, effect, {
|
|
114
|
+
...options,
|
|
115
|
+
amplitude,
|
|
116
|
+
radius,
|
|
117
|
+
duration: duration / timeScale(),
|
|
118
|
+
seed,
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
class Effect implements ImageEffect {
|
|
123
|
+
readonly finished: Promise<boolean>
|
|
124
|
+
private resolve!: (completed: boolean) => void
|
|
125
|
+
private renderer: ImageRenderer | undefined
|
|
126
|
+
private releaseOwnership: () => void = () => {}
|
|
127
|
+
private terminal = false
|
|
128
|
+
private queued = false
|
|
129
|
+
private committing = false
|
|
130
|
+
private userPaused: boolean
|
|
131
|
+
private suspended = false
|
|
132
|
+
private position = 0
|
|
133
|
+
private direction: 0 | 1 = 1
|
|
134
|
+
private dimensions = ''
|
|
135
|
+
private stats: ImageEffectDiagnostics = {
|
|
136
|
+
backend: 'waiting',
|
|
137
|
+
frames: 0,
|
|
138
|
+
textureBytes: 0,
|
|
139
|
+
peakTextureBytes: 0,
|
|
140
|
+
preparationMs: 0,
|
|
141
|
+
longestFrameMs: 0,
|
|
142
|
+
}
|
|
143
|
+
private reduceObserver:
|
|
144
|
+
ReturnType<typeof Application.ios.addNotificationObserver> | undefined
|
|
145
|
+
|
|
146
|
+
constructor(
|
|
147
|
+
private readonly image: Image,
|
|
148
|
+
readonly effect: ImageEffectName,
|
|
149
|
+
private readonly options: ImageEffectOptions &
|
|
150
|
+
Required<Pick<ImageEffectOptions, 'amplitude' | 'radius' | 'duration' | 'seed'>>,
|
|
151
|
+
) {
|
|
152
|
+
this.userPaused = options.paused ?? false
|
|
153
|
+
this.finished = new Promise((resolve) => {
|
|
154
|
+
this.resolve = resolve
|
|
155
|
+
})
|
|
156
|
+
this.releaseOwnership = ownImage(image, this.stop)
|
|
157
|
+
for (const event of ['loaded', 'layoutChanged']) image.on(event, this.schedule)
|
|
158
|
+
for (const event of ['imageSourceChange', 'stretchChange'])
|
|
159
|
+
image.on(event, this.changed)
|
|
160
|
+
image.on('unloaded', this.stop)
|
|
161
|
+
image.style.on('visibilityChange', this.visible)
|
|
162
|
+
for (const event of surfaceChanges) image.style.on(event, this.changed)
|
|
163
|
+
Application.on(Application.suspendEvent, this.suspend)
|
|
164
|
+
Application.on(Application.resumeEvent, this.resume)
|
|
165
|
+
if (isIOS)
|
|
166
|
+
this.reduceObserver = Application.ios.addNotificationObserver(
|
|
167
|
+
UIAccessibilityReduceMotionStatusDidChangeNotification,
|
|
168
|
+
this.preferences,
|
|
169
|
+
)
|
|
170
|
+
this.schedule()
|
|
171
|
+
}
|
|
172
|
+
then<TResult1 = boolean, TResult2 = never>(
|
|
173
|
+
fulfilled?: ((value: boolean) => TResult1 | PromiseLike<TResult1>) | null,
|
|
174
|
+
rejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
|
|
175
|
+
): PromiseLike<TResult1 | TResult2> {
|
|
176
|
+
return this.finished.then(fulfilled, rejected)
|
|
177
|
+
}
|
|
178
|
+
get paused() {
|
|
179
|
+
return this.userPaused
|
|
180
|
+
}
|
|
181
|
+
get progress() {
|
|
182
|
+
return this.renderer?.progress ?? this.position
|
|
183
|
+
}
|
|
184
|
+
set progress(value: number) {
|
|
185
|
+
if (this.effect === 'distortion')
|
|
186
|
+
throw new TypeError('Distortion follows touch input and cannot be scrubbed.')
|
|
187
|
+
if (!Number.isFinite(value) || value < 0 || value > 1)
|
|
188
|
+
throw new RangeError('Image progress must be between 0 and 1.')
|
|
189
|
+
if (this.terminal) return
|
|
190
|
+
this.position = value
|
|
191
|
+
this.renderer?.seek(value)
|
|
192
|
+
}
|
|
193
|
+
get diagnostics(): Readonly<ImageEffectDiagnostics> {
|
|
194
|
+
return {
|
|
195
|
+
...this.stats,
|
|
196
|
+
...(this.renderer
|
|
197
|
+
? {
|
|
198
|
+
frames: this.renderer.frames,
|
|
199
|
+
textureBytes: this.renderer.textureBytes,
|
|
200
|
+
longestFrameMs: this.renderer.longestFrameMs,
|
|
201
|
+
}
|
|
202
|
+
: {}),
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
pause = () => {
|
|
206
|
+
if (this.terminal) return
|
|
207
|
+
this.userPaused = true
|
|
208
|
+
this.renderer?.pause()
|
|
209
|
+
}
|
|
210
|
+
play = () => {
|
|
211
|
+
if (this.terminal) return
|
|
212
|
+
this.userPaused = false
|
|
213
|
+
if (this.suspended) return
|
|
214
|
+
if (this.renderer) {
|
|
215
|
+
if (this.effect === 'distortion') this.renderer.resume()
|
|
216
|
+
else this.renderer.play(this.direction)
|
|
217
|
+
} else this.schedule()
|
|
218
|
+
}
|
|
219
|
+
reverse = () => {
|
|
220
|
+
if (this.effect === 'distortion')
|
|
221
|
+
throw new TypeError('Distortion has no finite playback direction.')
|
|
222
|
+
if (this.terminal) return
|
|
223
|
+
this.direction = this.direction === 1 ? 0 : 1
|
|
224
|
+
this.play()
|
|
225
|
+
}
|
|
226
|
+
finish = () => {
|
|
227
|
+
if (this.terminal) return
|
|
228
|
+
if (this.effect === 'distortion') return this.stop()
|
|
229
|
+
this.complete(true, this.direction)
|
|
230
|
+
}
|
|
231
|
+
stop = () => this.complete(false, this.progress)
|
|
232
|
+
private complete = (completed: boolean, position: number, preserveImage = false) => {
|
|
233
|
+
if (this.terminal) return
|
|
234
|
+
this.terminal = true
|
|
235
|
+
this.position = position
|
|
236
|
+
this.stats = { ...this.diagnostics, textureBytes: 0 }
|
|
237
|
+
this.renderer?.stop(preserveImage)
|
|
238
|
+
this.renderer = undefined
|
|
239
|
+
this.releaseOwnership()
|
|
240
|
+
for (const event of ['loaded', 'layoutChanged']) this.image.off(event, this.schedule)
|
|
241
|
+
for (const event of ['imageSourceChange', 'stretchChange'])
|
|
242
|
+
this.image.off(event, this.changed)
|
|
243
|
+
this.image.off('unloaded', this.stop)
|
|
244
|
+
this.image.style.off('visibilityChange', this.visible)
|
|
245
|
+
for (const event of surfaceChanges) this.image.style.off(event, this.changed)
|
|
246
|
+
Application.off(Application.suspendEvent, this.suspend)
|
|
247
|
+
Application.off(Application.resumeEvent, this.resume)
|
|
248
|
+
if (this.reduceObserver)
|
|
249
|
+
Application.ios.removeNotificationObserver(
|
|
250
|
+
this.reduceObserver,
|
|
251
|
+
UIAccessibilityReduceMotionStatusDidChangeNotification,
|
|
252
|
+
)
|
|
253
|
+
this.reduceObserver = undefined
|
|
254
|
+
if (completed && this.effect === 'dissolve' && position === 1) {
|
|
255
|
+
this.committing = true
|
|
256
|
+
// The view may clear its ImageSource on disposal. Keep the caller's
|
|
257
|
+
// destination reusable by giving the view its own wrapper.
|
|
258
|
+
const destination = new ImageSource(this.options.to!.getNativeSource())
|
|
259
|
+
destination.rotationAngle = this.options.to!.rotationAngle
|
|
260
|
+
this.image.imageSource = destination
|
|
261
|
+
this.committing = false
|
|
262
|
+
}
|
|
263
|
+
this.resolve(completed)
|
|
264
|
+
}
|
|
265
|
+
private changed = (event?: EventData) => {
|
|
266
|
+
if (this.committing) return
|
|
267
|
+
if (this.renderer)
|
|
268
|
+
this.complete(false, this.progress, event?.eventName === 'imageSourceChange')
|
|
269
|
+
else this.schedule()
|
|
270
|
+
}
|
|
271
|
+
private suspend = () => {
|
|
272
|
+
this.suspended = true
|
|
273
|
+
this.renderer?.pause()
|
|
274
|
+
}
|
|
275
|
+
private resume = () => {
|
|
276
|
+
this.suspended = false
|
|
277
|
+
this.schedule()
|
|
278
|
+
if (!this.userPaused) this.play()
|
|
279
|
+
}
|
|
280
|
+
private visible = () => {
|
|
281
|
+
if (this.image.visibility !== 'visible') this.stop()
|
|
282
|
+
}
|
|
283
|
+
private preferences = () => {
|
|
284
|
+
if (reducedMotion()) this.fallback('reduced-motion')
|
|
285
|
+
}
|
|
286
|
+
private fallback(reason: ImageEffectDiagnostics['reason']) {
|
|
287
|
+
this.stats = { ...this.stats, backend: 'fallback', reason }
|
|
288
|
+
// Reduced/unsupported rendering still performs a requested content change.
|
|
289
|
+
if (this.effect === 'dissolve' && this.direction === 1) {
|
|
290
|
+
this.complete(true, 1)
|
|
291
|
+
} else this.complete(false, 0)
|
|
292
|
+
}
|
|
293
|
+
private schedule = () => {
|
|
294
|
+
if (this.queued || this.terminal) return
|
|
295
|
+
this.queued = true
|
|
296
|
+
void Promise.resolve().then(() => {
|
|
297
|
+
this.queued = false
|
|
298
|
+
if (this.terminal || this.suspended || !this.image.isLoaded) return
|
|
299
|
+
if (reducedMotion()) return this.fallback('reduced-motion')
|
|
300
|
+
if (this.image.visibility !== 'visible') return this.fallback('not-visible')
|
|
301
|
+
const { width, height } = this.image.getActualSize()
|
|
302
|
+
if (width <= 0 || height <= 0) return
|
|
303
|
+
const dimensions = `${width}:${height}`
|
|
304
|
+
if (this.renderer) {
|
|
305
|
+
if (this.dimensions !== dimensions) this.stop()
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
if (!this.image.imageSource) return
|
|
309
|
+
this.dimensions = dimensions
|
|
310
|
+
const started = Date.now()
|
|
311
|
+
try {
|
|
312
|
+
this.renderer = createImageRenderer(
|
|
313
|
+
this.image,
|
|
314
|
+
{
|
|
315
|
+
...this.options,
|
|
316
|
+
mode: names.indexOf(this.effect),
|
|
317
|
+
x: this.options.origin?.x ?? width / 2,
|
|
318
|
+
y: this.options.origin?.y ?? height / 2,
|
|
319
|
+
},
|
|
320
|
+
this.complete,
|
|
321
|
+
)
|
|
322
|
+
} catch (error) {
|
|
323
|
+
this.stats.preparationMs = Date.now() - started
|
|
324
|
+
this.stats.detail = String(error)
|
|
325
|
+
return this.fallback('unsupported')
|
|
326
|
+
}
|
|
327
|
+
if (!this.renderer) return this.fallback('unsupported')
|
|
328
|
+
this.stats.backend = this.renderer.backend
|
|
329
|
+
this.stats.preparationMs = Date.now() - started
|
|
330
|
+
this.stats.peakTextureBytes = this.renderer.textureBytes
|
|
331
|
+
this.renderer.pause()
|
|
332
|
+
if (this.position) this.renderer.seek(this.position)
|
|
333
|
+
if (!this.userPaused && !this.suspended) this.play()
|
|
334
|
+
})
|
|
335
|
+
}
|
|
336
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Image } from '@nativescript/core'
|
|
2
|
+
|
|
3
|
+
/** Rendering effects share one image surface; ordinary view transforms stay independent. */
|
|
4
|
+
const owners = new WeakMap<Image, () => void>()
|
|
5
|
+
|
|
6
|
+
export function ownImage(image: Image, stop: () => void): () => void {
|
|
7
|
+
owners.get(image)?.()
|
|
8
|
+
owners.set(image, stop)
|
|
9
|
+
return () => {
|
|
10
|
+
if (owners.get(image) === stop) owners.delete(image)
|
|
11
|
+
}
|
|
12
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,83 @@
|
|
|
1
|
-
export { animate, stop } from './animate.js'
|
|
1
|
+
export { animate, decay, set, stop } from './animate.js'
|
|
2
2
|
export { sequence } from './sequence.js'
|
|
3
|
-
export type { Step } from './sequence.js'
|
|
3
|
+
export type { Step, SequenceOptions, SequencePosition } from './sequence.js'
|
|
4
4
|
export { press } from './press.js'
|
|
5
|
+
export { interactionSurface } from './interaction-surface.js'
|
|
6
|
+
export type { InteractionSurface } from './interaction-surface.js'
|
|
7
|
+
export { drag } from './drag.js'
|
|
8
|
+
export type { Drag, DragOptions, DragPoint, DragEvent, DragRelease } from './drag.js'
|
|
9
|
+
export { scrollLinked } from './scroll-linked.js'
|
|
10
|
+
export type { ScrollLinkedOptions } from './scroll-linked.js'
|
|
11
|
+
export { animateItems } from './items.js'
|
|
12
|
+
export type { ItemsOptions } from './items.js'
|
|
13
|
+
export { followKeyboard, onKeyboard } from './keyboard.js'
|
|
14
|
+
export type { KeyboardOptions, KeyboardState } from './keyboard.js'
|
|
15
|
+
export type { Haptic, HapticOptions, HapticStyle } from './haptics.js'
|
|
16
|
+
export { confetti, particles } from './particles.js'
|
|
17
|
+
export type { ConfettiOptions, ParticleOptions } from './particles.js'
|
|
18
|
+
export { shimmer } from './shimmer.js'
|
|
19
|
+
export type { ShimmerOptions } from './shimmer.js'
|
|
20
|
+
export { liquid } from './liquid.js'
|
|
21
|
+
export type { LiquidOptions, LiquidEffect } from './liquid.js'
|
|
22
|
+
export { imageEffect } from './image-effect.js'
|
|
23
|
+
export type {
|
|
24
|
+
ImageEffect,
|
|
25
|
+
ImageEffectName,
|
|
26
|
+
ImageEffectOptions,
|
|
27
|
+
ImageEffectDiagnostics,
|
|
28
|
+
} from './image-effect.js'
|
|
29
|
+
export { zoomable } from './zoomable.js'
|
|
30
|
+
export type { Zoomable, ZoomableOptions, ZoomState, PhotoDismissal } from './zoomable.js'
|
|
31
|
+
export { loader } from './loader.js'
|
|
32
|
+
export type { LoaderOptions, LoaderStyle } from './loader.js'
|
|
33
|
+
export { reveal, conceal } from './reveal.js'
|
|
34
|
+
export type { RevealOptions, ConcealOptions } from './reveal.js'
|
|
35
|
+
export { animateText } from './text.js'
|
|
36
|
+
export type { TextOptions } from './text.js'
|
|
37
|
+
export { rollTo } from './roll.js'
|
|
38
|
+
export type { RollOptions } from './roll.js'
|
|
39
|
+
export { symbolEffect } from './symbol-effect.js'
|
|
40
|
+
export type {
|
|
41
|
+
SymbolEffect,
|
|
42
|
+
SymbolEffectName,
|
|
43
|
+
SymbolEffectOptions,
|
|
44
|
+
} from './symbol-effect.js'
|
|
45
|
+
export { gradient } from './gradient.js'
|
|
46
|
+
export type {
|
|
47
|
+
Gradient,
|
|
48
|
+
GradientOptions,
|
|
49
|
+
GradientType,
|
|
50
|
+
GradientValues,
|
|
51
|
+
} from './gradient.js'
|
|
52
|
+
export { configure, onReducedMotion, reducedMotion } from './config.js'
|
|
53
|
+
export type { JoltConfig } from './config.js'
|
|
5
54
|
export { definePreset, presets } from './presets.js'
|
|
6
55
|
export type { Preset, PresetName, Presets } from './presets.js'
|
|
7
|
-
export { springs } from './transition.js'
|
|
8
|
-
export type {
|
|
56
|
+
export { origamiSpring, springs } from './transition.js'
|
|
57
|
+
export type {
|
|
58
|
+
OrigamiSpring,
|
|
59
|
+
Spring,
|
|
60
|
+
SpringName,
|
|
61
|
+
Timing,
|
|
62
|
+
Transition,
|
|
63
|
+
Tween,
|
|
64
|
+
} from './transition.js'
|
|
9
65
|
export { cubicBezier } from './easing.js'
|
|
10
66
|
export type { CubicBezier, Easing, EasingFunction, EasingName } from './easing.js'
|
|
67
|
+
export type { PlaybackAnimation } from './playback.js'
|
|
11
68
|
export type { Animation } from './engine.js'
|
|
69
|
+
export type { ColorValue, ColorKeyframes } from './color.js'
|
|
12
70
|
export type * from './types.js'
|
|
71
|
+
export { motionScope } from './motion-scope.js'
|
|
72
|
+
export type { MotionScope, MotionResource } from './motion-scope.js'
|
|
73
|
+
export { motionStates } from './motion-states.js'
|
|
74
|
+
export type {
|
|
75
|
+
MotionStates,
|
|
76
|
+
MotionStatesOptions,
|
|
77
|
+
MotionState,
|
|
78
|
+
MotionValues,
|
|
79
|
+
StateTransition,
|
|
80
|
+
} from './motion-states.js'
|
|
81
|
+
|
|
82
|
+
export { animateLayout } from './layout.js'
|
|
83
|
+
export type { LayoutOptions } from './layout.js'
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { View, ViewBase } from '@nativescript/core'
|
|
2
|
+
import { toView } from './animate.js'
|
|
3
|
+
import type { Target } from './types.js'
|
|
4
|
+
|
|
5
|
+
const surfaces = new WeakMap<ViewBase, Set<object>>()
|
|
6
|
+
export interface InteractionSurface {
|
|
7
|
+
readonly disposed: boolean
|
|
8
|
+
/** Existing animations retain their driver until they end. Disables future opt-in. */
|
|
9
|
+
dispose(): void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Opt a view/subtree into native geometry updates before starting its animations. */
|
|
13
|
+
export function interactionSurface(target: Target): InteractionSurface {
|
|
14
|
+
const view = toView(target)
|
|
15
|
+
let tokens = surfaces.get(view)
|
|
16
|
+
if (!tokens) surfaces.set(view, (tokens = new Set()))
|
|
17
|
+
const token = {}
|
|
18
|
+
tokens.add(token)
|
|
19
|
+
let disposed = false
|
|
20
|
+
return {
|
|
21
|
+
get disposed() {
|
|
22
|
+
return disposed
|
|
23
|
+
},
|
|
24
|
+
dispose() {
|
|
25
|
+
if (disposed) return
|
|
26
|
+
disposed = true
|
|
27
|
+
tokens.delete(token)
|
|
28
|
+
if (!tokens.size) surfaces.delete(view)
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Evaluated at animation start; no per-frame JS traversal. */
|
|
34
|
+
export function hasInteractionSurface(view: View): boolean {
|
|
35
|
+
let current: ViewBase | undefined = view
|
|
36
|
+
while (current) {
|
|
37
|
+
if (surfaces.get(current)?.size) return true
|
|
38
|
+
current = current.parent
|
|
39
|
+
}
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
declare class JoltInteractiveAnimation extends NSObject {
|
|
2
|
+
static startViewLayersKeyPathValuesTimesDurationDelayRepeatReverseFinalValueStartedAtCompletion(
|
|
3
|
+
view: UIView,
|
|
4
|
+
layers: NSArray<CALayer>,
|
|
5
|
+
keyPath: string,
|
|
6
|
+
values: NSArray<number>,
|
|
7
|
+
times: NSArray<number>,
|
|
8
|
+
duration: number,
|
|
9
|
+
delay: number,
|
|
10
|
+
repeat: number,
|
|
11
|
+
reverse: boolean,
|
|
12
|
+
finalValue: number,
|
|
13
|
+
startedAt: number,
|
|
14
|
+
completion: (completed: boolean) => void,
|
|
15
|
+
): JoltInteractiveAnimation
|
|
16
|
+
static syncViewExcluding(view: UIView, keyPath: string | null): void
|
|
17
|
+
static valueForViewKeyPath(view: UIView, keyPath: string): number | null
|
|
18
|
+
static activeCount(): number
|
|
19
|
+
readonly elapsed: number
|
|
20
|
+
controlPaused(elapsed: number, paused: boolean): void
|
|
21
|
+
cancel(): void
|
|
22
|
+
}
|
|
23
|
+
declare class JoltImageEffect extends NSObject {
|
|
24
|
+
stopPreservingImage(): void
|
|
25
|
+
static createWithViewDestinationModeOriginXOriginYAmplitudeRadiusDurationSeedListener(
|
|
26
|
+
image: UIImageView,
|
|
27
|
+
destination: UIImage | null,
|
|
28
|
+
mode: number,
|
|
29
|
+
x: number,
|
|
30
|
+
y: number,
|
|
31
|
+
amplitude: number,
|
|
32
|
+
radius: number,
|
|
33
|
+
duration: number,
|
|
34
|
+
seed: number,
|
|
35
|
+
listener: (completed: boolean, progress: number) => void,
|
|
36
|
+
): JoltImageEffect | null
|
|
37
|
+
readonly progress: number
|
|
38
|
+
readonly paused: boolean
|
|
39
|
+
readonly frames: number
|
|
40
|
+
readonly longestFrame: number
|
|
41
|
+
readonly textureBytes: number
|
|
42
|
+
play(endpoint: number): void
|
|
43
|
+
pause(): void
|
|
44
|
+
resume(): void
|
|
45
|
+
seek(progress: number): void
|
|
46
|
+
finish(endpoint: number): void
|
|
47
|
+
stop(): void
|
|
48
|
+
}
|
|
49
|
+
declare class JoltPhotoZoom extends NSObject {
|
|
50
|
+
static attachImageViewportMinimumMaximumDoubleTapDismissReducedBackdropChromeOnDismissOnSettled(
|
|
51
|
+
image: UIImageView,
|
|
52
|
+
viewport: UIView,
|
|
53
|
+
minimum: number,
|
|
54
|
+
maximum: number,
|
|
55
|
+
doubleTap: number,
|
|
56
|
+
dismiss: boolean,
|
|
57
|
+
reduced: boolean,
|
|
58
|
+
backdrop: UIView | null,
|
|
59
|
+
chrome: UIView | null,
|
|
60
|
+
onDismiss: (
|
|
61
|
+
x: number,
|
|
62
|
+
y: number,
|
|
63
|
+
scale: number,
|
|
64
|
+
velocity: number,
|
|
65
|
+
progress: number,
|
|
66
|
+
) => void,
|
|
67
|
+
onSettled: (x: number, y: number, scale: number) => void,
|
|
68
|
+
): JoltPhotoZoom
|
|
69
|
+
readonly x: number
|
|
70
|
+
readonly y: number
|
|
71
|
+
readonly scale: number
|
|
72
|
+
readonly dismissProgress: number
|
|
73
|
+
readonly dismissPending: boolean
|
|
74
|
+
zoomFocusXFocusYAnimated(scale: number, x: number, y: number, animated: boolean): void
|
|
75
|
+
reset(animated: boolean): void
|
|
76
|
+
requestDismiss(): void
|
|
77
|
+
setReduced(value: boolean): void
|
|
78
|
+
setActive(value: boolean): void
|
|
79
|
+
layout(): void
|
|
80
|
+
stop(): void
|
|
81
|
+
}
|
package/src/items.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { ListView, View, type EventData, type ItemEventData } from '@nativescript/core'
|
|
2
|
+
import { animate, endingValues, startingValues, toView } from './animate.js'
|
|
3
|
+
import { getDriver } from './driver/index.js'
|
|
4
|
+
import { stopView } from './engine.js'
|
|
5
|
+
import type { PresetName } from './presets.js'
|
|
6
|
+
import { writeValue } from './properties.js'
|
|
7
|
+
import type { SpringName } from './transition.js'
|
|
8
|
+
import type { AnimateOptions, Keyframes, Target } from './types.js'
|
|
9
|
+
|
|
10
|
+
export type ItemsOptions = AnimateOptions & {
|
|
11
|
+
/** Animate each data item only the first time it shows up. Defaults to false. */
|
|
12
|
+
once?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Pending {
|
|
16
|
+
index: number
|
|
17
|
+
section: number | undefined
|
|
18
|
+
item: unknown
|
|
19
|
+
key: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type Row = View & {
|
|
23
|
+
bindingContext?: unknown
|
|
24
|
+
_listViewItemIndex?: number
|
|
25
|
+
_listViewSectionIndex?: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface Source extends View {
|
|
29
|
+
items?: unknown
|
|
30
|
+
_getDataItem?(index: number): unknown
|
|
31
|
+
_getDataItemInSection?(section: number, index: number): unknown
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Plays an animation on each row of a list as it comes on screen. Rows are
|
|
36
|
+
* recycled, so a row that is reused for another item starts over, or settles
|
|
37
|
+
* at the end values when that item was already shown and `once` is set.
|
|
38
|
+
* Returns a function that removes the effect.
|
|
39
|
+
*
|
|
40
|
+
* ```ts
|
|
41
|
+
* animateItems(listView, 'fadeInUp')
|
|
42
|
+
* animateItems(listView, 'fadeInUp', { once: true })
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export function animateItems(
|
|
46
|
+
list: Target,
|
|
47
|
+
animation: Keyframes | PresetName,
|
|
48
|
+
options?: ItemsOptions | SpringName,
|
|
49
|
+
): () => void {
|
|
50
|
+
const source = toView(list) as Source
|
|
51
|
+
const { once = false, ...rest } = typeof options === 'object' ? options : {}
|
|
52
|
+
if (rest.stagger !== undefined)
|
|
53
|
+
throw new TypeError('animateItems animates one row at a time; stagger has no effect.')
|
|
54
|
+
const transition: AnimateOptions | SpringName | undefined =
|
|
55
|
+
typeof options === 'string' ? options : rest
|
|
56
|
+
const properties = [...startingValues(animation, transition).keys()]
|
|
57
|
+
const ends = endingValues(animation, transition)
|
|
58
|
+
|
|
59
|
+
const rows = new Set<Row>()
|
|
60
|
+
const seenItems = new WeakSet<object>()
|
|
61
|
+
const seenKeys = new Set<string>()
|
|
62
|
+
const pending: Pending[] = []
|
|
63
|
+
let queued = false
|
|
64
|
+
let disposed = false
|
|
65
|
+
|
|
66
|
+
const settle = (row: Row) => {
|
|
67
|
+
stopView(row, properties)
|
|
68
|
+
for (const [property, value] of ends)
|
|
69
|
+
if (value !== null) writeValue(row, property, value)
|
|
70
|
+
getDriver().sync?.(row)
|
|
71
|
+
}
|
|
72
|
+
const show = (row: Row, { item, key }: Pending) => {
|
|
73
|
+
rows.add(row)
|
|
74
|
+
const tracked = typeof item === 'object' && item !== null
|
|
75
|
+
const seen = tracked ? seenItems.has(item) : seenKeys.has(key)
|
|
76
|
+
if (tracked) seenItems.add(item)
|
|
77
|
+
else seenKeys.add(key)
|
|
78
|
+
if (once && seen) settle(row)
|
|
79
|
+
else {
|
|
80
|
+
stopView(row, properties)
|
|
81
|
+
animate(row, animation, transition)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const flush = () => {
|
|
85
|
+
queued = false
|
|
86
|
+
if (disposed) return
|
|
87
|
+
const current = new Set<Row>()
|
|
88
|
+
const fresh: Row[] = []
|
|
89
|
+
source.eachChildView((child: Row) => {
|
|
90
|
+
current.add(child)
|
|
91
|
+
if (!rows.has(child)) fresh.push(child)
|
|
92
|
+
return true
|
|
93
|
+
})
|
|
94
|
+
for (const row of rows) if (!current.has(row)) rows.delete(row)
|
|
95
|
+
for (const entry of pending.splice(0)) {
|
|
96
|
+
const row =
|
|
97
|
+
fresh.find(
|
|
98
|
+
(candidate) =>
|
|
99
|
+
candidate._listViewItemIndex === entry.index &&
|
|
100
|
+
candidate._listViewSectionIndex === entry.section,
|
|
101
|
+
) ??
|
|
102
|
+
fresh.find(
|
|
103
|
+
(candidate) =>
|
|
104
|
+
entry.item !== undefined && candidate.bindingContext === entry.item,
|
|
105
|
+
)
|
|
106
|
+
if (!row) continue
|
|
107
|
+
fresh.splice(fresh.indexOf(row), 1)
|
|
108
|
+
show(row, entry)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const onItemLoading = (event: EventData) => {
|
|
112
|
+
const { index, section, view } = event as ItemEventData
|
|
113
|
+
const item =
|
|
114
|
+
section === undefined
|
|
115
|
+
? source._getDataItem?.(index)
|
|
116
|
+
: source._getDataItemInSection?.(section, index)
|
|
117
|
+
const entry: Pending = { index, section, item, key: `${section ?? ''}:${index}` }
|
|
118
|
+
if (view instanceof View) {
|
|
119
|
+
show(view, entry)
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
pending.push(entry)
|
|
123
|
+
if (queued) return
|
|
124
|
+
queued = true
|
|
125
|
+
void Promise.resolve().then(flush)
|
|
126
|
+
}
|
|
127
|
+
const cleanup = () => {
|
|
128
|
+
if (disposed) return
|
|
129
|
+
disposed = true
|
|
130
|
+
source.off(ListView.itemLoadingEvent, onItemLoading)
|
|
131
|
+
source.off('unloaded', cleanup)
|
|
132
|
+
pending.length = 0
|
|
133
|
+
for (const row of rows) settle(row)
|
|
134
|
+
rows.clear()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
source.on(ListView.itemLoadingEvent, onItemLoading)
|
|
138
|
+
source.on('unloaded', cleanup)
|
|
139
|
+
return cleanup
|
|
140
|
+
}
|