@rafaelaugusto/jolt 0.1.0-beta.0 → 0.1.0-beta.10
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 +441 -25
- package/dist/animate.d.ts +21 -5
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +205 -25
- 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-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-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 +164 -20
- package/dist/driver/ios.js.map +1 -1
- package/dist/engine.d.ts +76 -5
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +303 -23
- package/dist/engine.js.map +1 -1
- 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 +45 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -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 +36 -0
- package/dist/motion-states.d.ts.map +1 -0
- package/dist/motion-states.js +165 -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/presets.d.ts +16 -1
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +24 -6
- package/dist/presets.js.map +1 -1
- package/dist/press.d.ts.map +1 -1
- package/dist/press.js +25 -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 +13 -7
- package/dist/sequence.d.ts.map +1 -1
- package/dist/sequence.js +214 -19
- 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 +17 -1
- package/dist/transition.d.ts.map +1 -1
- package/dist/transition.js +41 -6
- package/dist/transition.js.map +1 -1
- package/dist/types.d.ts +50 -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 +33 -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/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 +19 -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 +155 -0
- package/src/animate.ts +278 -38
- 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-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-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 +194 -25
- package/src/engine.ts +329 -27
- 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 +61 -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 +215 -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/presets.ts +31 -7
- package/src/press.ts +32 -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 +212 -19
- 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 +63 -6
- package/src/types.ts +51 -1
- package/src/vue/index.ts +231 -0
- package/src/zoom-native.ts +124 -0
- package/src/zoomable.ts +330 -0
package/src/liquid.ts
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { Application, Image, isIOS } from '@nativescript/core'
|
|
2
|
+
import { tintColorProperty } from '@nativescript/core/ui/image'
|
|
3
|
+
import { toView } from './animate.js'
|
|
4
|
+
import { liquidGrid } from './liquid-grid.js'
|
|
5
|
+
import type { Target } from './types.js'
|
|
6
|
+
import { reducedMotion } from './config.js'
|
|
7
|
+
import { ownImage } from './image-owner.js'
|
|
8
|
+
|
|
9
|
+
export interface LiquidOptions {
|
|
10
|
+
/** Amount of internal deformation, from 0 to 1. Defaults to 0.7. */
|
|
11
|
+
intensity?: number
|
|
12
|
+
/** Milliseconds per seamless cycle. Defaults to 8000. */
|
|
13
|
+
duration?: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface LiquidEffect {
|
|
17
|
+
/** Freeze the current deformation. */
|
|
18
|
+
pause(): void
|
|
19
|
+
/** Continue from the paused deformation. */
|
|
20
|
+
resume(): void
|
|
21
|
+
/** Remove the effect and restore the image. Idempotent. */
|
|
22
|
+
stop(): void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const running = new WeakMap<Image, LiquidEffect>()
|
|
26
|
+
const key = 'jolt:liquid'
|
|
27
|
+
const columns = 24
|
|
28
|
+
const rows = 24
|
|
29
|
+
const steps = 64
|
|
30
|
+
const noop = () => {}
|
|
31
|
+
|
|
32
|
+
/** Deforms a raster image with native iOS texture warps. Android leaves it unchanged. */
|
|
33
|
+
export function liquid(target: Target, options: LiquidOptions = {}): LiquidEffect {
|
|
34
|
+
const settings = {
|
|
35
|
+
intensity: options.intensity ?? 0.7,
|
|
36
|
+
duration: options.duration ?? 8000,
|
|
37
|
+
}
|
|
38
|
+
if (
|
|
39
|
+
!Number.isFinite(settings.intensity) ||
|
|
40
|
+
settings.intensity < 0 ||
|
|
41
|
+
settings.intensity > 1
|
|
42
|
+
)
|
|
43
|
+
throw new RangeError('Liquid intensity must be between zero and one.')
|
|
44
|
+
if (!Number.isFinite(settings.duration) || settings.duration <= 0)
|
|
45
|
+
throw new RangeError('Liquid duration must be a positive finite number.')
|
|
46
|
+
const image = toView(target)
|
|
47
|
+
if (!(image instanceof Image)) throw new TypeError('Expected a NativeScript Image.')
|
|
48
|
+
running.get(image)?.stop()
|
|
49
|
+
if (!isIOS || settings.intensity === 0) return { pause: noop, resume: noop, stop: noop }
|
|
50
|
+
return new Liquid(image, settings)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
class Liquid implements LiquidEffect {
|
|
54
|
+
private releaseOwnership: (() => void) | undefined
|
|
55
|
+
private stopped = false
|
|
56
|
+
private queued = false
|
|
57
|
+
private pausedByUser = false
|
|
58
|
+
private active = UIApplication.sharedApplication.applicationState === 0 // UIApplicationStateActive
|
|
59
|
+
private native: UIImageView | undefined
|
|
60
|
+
private original: UIImage | undefined
|
|
61
|
+
private source: Image['imageSource'] | undefined
|
|
62
|
+
private renderer: SKView | undefined
|
|
63
|
+
private scene: SKScene | undefined
|
|
64
|
+
private sprite: SKSpriteNode | undefined
|
|
65
|
+
private observers: [
|
|
66
|
+
string,
|
|
67
|
+
ReturnType<typeof Application.ios.addNotificationObserver>,
|
|
68
|
+
][] = []
|
|
69
|
+
|
|
70
|
+
constructor(
|
|
71
|
+
private readonly image: Image,
|
|
72
|
+
private readonly settings: Required<LiquidOptions>,
|
|
73
|
+
) {
|
|
74
|
+
this.releaseOwnership = ownImage(image, this.stop)
|
|
75
|
+
running.set(image, this)
|
|
76
|
+
for (const event of ['loaded', 'layoutChanged', 'imageSourceChange', 'stretchChange'])
|
|
77
|
+
image.on(event, this.schedule)
|
|
78
|
+
image.on('unloaded', this.unloaded)
|
|
79
|
+
image.style.on('visibilityChange', this.schedule)
|
|
80
|
+
image.style.on('tintColorChange', this.schedule)
|
|
81
|
+
this.observe(UIAccessibilityReduceMotionStatusDidChangeNotification, this.schedule)
|
|
82
|
+
this.observe(UIApplicationWillResignActiveNotification, this.suspend)
|
|
83
|
+
this.observe(UIApplicationDidBecomeActiveNotification, this.activate)
|
|
84
|
+
this.schedule()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
pause = () => {
|
|
88
|
+
this.pausedByUser = true
|
|
89
|
+
if (this.renderer) this.renderer.paused = true
|
|
90
|
+
}
|
|
91
|
+
resume = () => {
|
|
92
|
+
if (this.stopped) return
|
|
93
|
+
this.pausedByUser = false
|
|
94
|
+
this.schedule()
|
|
95
|
+
}
|
|
96
|
+
stop = () => {
|
|
97
|
+
if (this.stopped) return
|
|
98
|
+
this.stopped = true
|
|
99
|
+
this.releaseOwnership?.()
|
|
100
|
+
for (const event of ['loaded', 'layoutChanged', 'imageSourceChange', 'stretchChange'])
|
|
101
|
+
this.image.off(event, this.schedule)
|
|
102
|
+
this.image.off('unloaded', this.unloaded)
|
|
103
|
+
this.image.style.off('visibilityChange', this.schedule)
|
|
104
|
+
this.image.style.off('tintColorChange', this.schedule)
|
|
105
|
+
for (const [name, observer] of this.observers)
|
|
106
|
+
Application.ios.removeNotificationObserver(observer, name)
|
|
107
|
+
this.observers = []
|
|
108
|
+
if (running.get(this.image) === this) running.delete(this.image)
|
|
109
|
+
this.remove()
|
|
110
|
+
}
|
|
111
|
+
private observe(name: string, callback: () => void) {
|
|
112
|
+
this.observers.push([name, Application.ios.addNotificationObserver(name, callback)])
|
|
113
|
+
}
|
|
114
|
+
private suspend = () => {
|
|
115
|
+
this.active = false
|
|
116
|
+
if (this.renderer) this.renderer.paused = true
|
|
117
|
+
}
|
|
118
|
+
private activate = () => {
|
|
119
|
+
this.active = true
|
|
120
|
+
this.schedule()
|
|
121
|
+
}
|
|
122
|
+
private unloaded = () => this.remove()
|
|
123
|
+
private schedule = () => {
|
|
124
|
+
if (this.stopped || this.queued) return
|
|
125
|
+
this.queued = true
|
|
126
|
+
void Promise.resolve().then(() => {
|
|
127
|
+
this.queued = false
|
|
128
|
+
if (this.stopped) return
|
|
129
|
+
try {
|
|
130
|
+
this.update()
|
|
131
|
+
} catch (error) {
|
|
132
|
+
this.stop()
|
|
133
|
+
throw error
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
private update() {
|
|
138
|
+
if (
|
|
139
|
+
!this.image.isLoaded ||
|
|
140
|
+
this.image.visibility !== 'visible' ||
|
|
141
|
+
this.image.tintColor ||
|
|
142
|
+
reducedMotion()
|
|
143
|
+
)
|
|
144
|
+
return this.remove()
|
|
145
|
+
const native = this.image.nativeViewProtected as UIImageView | undefined
|
|
146
|
+
if (!(native instanceof UIImageView) || !native.window) return this.remove()
|
|
147
|
+
if (
|
|
148
|
+
this.native &&
|
|
149
|
+
(this.native !== native ||
|
|
150
|
+
native.image !== null ||
|
|
151
|
+
this.image.imageSource !== this.source)
|
|
152
|
+
)
|
|
153
|
+
this.remove()
|
|
154
|
+
const source = this.original ?? native.image
|
|
155
|
+
if (
|
|
156
|
+
!source?.CGImage ||
|
|
157
|
+
source.images?.count ||
|
|
158
|
+
source.renderingMode === 2 /* AlwaysTemplate */
|
|
159
|
+
)
|
|
160
|
+
return this.remove()
|
|
161
|
+
if (source.size.width * source.size.height * source.scale ** 2 > 16_777_216)
|
|
162
|
+
return this.remove()
|
|
163
|
+
const bounds = native.bounds
|
|
164
|
+
if (
|
|
165
|
+
bounds.size.width <= 0 ||
|
|
166
|
+
bounds.size.height <= 0 ||
|
|
167
|
+
source.size.width <= 0 ||
|
|
168
|
+
source.size.height <= 0
|
|
169
|
+
)
|
|
170
|
+
return this.remove()
|
|
171
|
+
if (!this.renderer) {
|
|
172
|
+
// A paused SKView cannot draw its first frame. Keep the original visible
|
|
173
|
+
// until rendering can start, including source changes while paused.
|
|
174
|
+
if (this.pausedByUser || !this.active) return
|
|
175
|
+
this.create(native, source)
|
|
176
|
+
}
|
|
177
|
+
const renderer = this.renderer!
|
|
178
|
+
renderer.frame = CGRectMake(
|
|
179
|
+
bounds.origin.x,
|
|
180
|
+
bounds.origin.y,
|
|
181
|
+
bounds.size.width,
|
|
182
|
+
bounds.size.height,
|
|
183
|
+
)
|
|
184
|
+
this.scene!.size = CGSizeMake(bounds.size.width, bounds.size.height)
|
|
185
|
+
const iw = source.size.width
|
|
186
|
+
const ih = source.size.height
|
|
187
|
+
const { width, height } = bounds.size
|
|
188
|
+
const stretch = this.image.stretch
|
|
189
|
+
const scale =
|
|
190
|
+
stretch === 'aspectFill'
|
|
191
|
+
? Math.max(width / iw, height / ih)
|
|
192
|
+
: Math.min(width / iw, height / ih)
|
|
193
|
+
this.sprite!.size = CGSizeMake(
|
|
194
|
+
stretch === 'fill' ? width : stretch === 'none' ? iw : iw * scale,
|
|
195
|
+
stretch === 'fill' ? height : stretch === 'none' ? ih : ih * scale,
|
|
196
|
+
)
|
|
197
|
+
this.sprite!.position = CGPointMake(width / 2, height / 2)
|
|
198
|
+
renderer.paused = this.pausedByUser || !this.active
|
|
199
|
+
}
|
|
200
|
+
private create(native: UIImageView, source: UIImage) {
|
|
201
|
+
const renderer = SKView.alloc().initWithFrame(native.bounds)
|
|
202
|
+
this.renderer = renderer
|
|
203
|
+
this.native = native
|
|
204
|
+
this.original = source
|
|
205
|
+
this.source = this.image.imageSource
|
|
206
|
+
renderer.opaque = false
|
|
207
|
+
renderer.backgroundColor = UIColor.clearColor
|
|
208
|
+
renderer.allowsTransparency = true
|
|
209
|
+
renderer.asynchronous = true
|
|
210
|
+
renderer.userInteractionEnabled = false
|
|
211
|
+
renderer.accessibilityElementsHidden = true
|
|
212
|
+
renderer.clipsToBounds = true
|
|
213
|
+
renderer.preferredFramesPerSecond = Math.min(
|
|
214
|
+
120,
|
|
215
|
+
native.window?.screen?.maximumFramesPerSecond ?? 60,
|
|
216
|
+
)
|
|
217
|
+
renderer.paused = true
|
|
218
|
+
const scene = SKScene.sceneWithSize(native.bounds.size)
|
|
219
|
+
this.scene = scene
|
|
220
|
+
scene.backgroundColor = UIColor.clearColor
|
|
221
|
+
scene.scaleMode = 3 // SKSceneScaleModeResizeFill
|
|
222
|
+
const texture = SKTexture.textureWithImage(source)
|
|
223
|
+
const sprite = SKSpriteNode.spriteNodeWithTexture(texture)
|
|
224
|
+
this.sprite = sprite
|
|
225
|
+
const warps: SKWarpGeometryGrid[] = []
|
|
226
|
+
const times: number[] = []
|
|
227
|
+
for (let step = 0; step <= steps; step++) {
|
|
228
|
+
const positions = liquidGrid(
|
|
229
|
+
columns,
|
|
230
|
+
rows,
|
|
231
|
+
step === steps ? 0 : step / steps,
|
|
232
|
+
this.settings.intensity,
|
|
233
|
+
)
|
|
234
|
+
warps.push(
|
|
235
|
+
SKWarpGeometryGrid.gridWithColumnsRowsSourcePositionsDestPositions(
|
|
236
|
+
columns,
|
|
237
|
+
rows,
|
|
238
|
+
null,
|
|
239
|
+
positions,
|
|
240
|
+
),
|
|
241
|
+
)
|
|
242
|
+
times.push(((step / steps) * this.settings.duration) / 1000)
|
|
243
|
+
}
|
|
244
|
+
const loop = SKAction.animateWithWarpsTimes(warps, times)
|
|
245
|
+
if (!loop) throw new Error('Unable to create the liquid warp action.')
|
|
246
|
+
sprite.warpGeometry = SKWarpGeometryGrid.gridWithColumnsRows(columns, rows)
|
|
247
|
+
const entrance = SKAction.warpToDuration(
|
|
248
|
+
warps[0]!,
|
|
249
|
+
Math.min(0.35, this.settings.duration / 10000),
|
|
250
|
+
)
|
|
251
|
+
if (!entrance) throw new Error('Unable to create the liquid entrance.')
|
|
252
|
+
sprite.runActionWithKey(
|
|
253
|
+
SKAction.sequence([entrance, SKAction.repeatActionForever(loop)]),
|
|
254
|
+
key,
|
|
255
|
+
)
|
|
256
|
+
scene.addChild(sprite)
|
|
257
|
+
renderer.presentScene(scene)
|
|
258
|
+
native.addSubview(renderer)
|
|
259
|
+
native.image = null
|
|
260
|
+
}
|
|
261
|
+
private remove() {
|
|
262
|
+
const { renderer, sprite, native, original } = this
|
|
263
|
+
if (renderer) {
|
|
264
|
+
renderer.paused = true
|
|
265
|
+
sprite?.removeAllActions()
|
|
266
|
+
sprite?.removeFromParent()
|
|
267
|
+
renderer.presentScene(null)
|
|
268
|
+
renderer.removeFromSuperview()
|
|
269
|
+
}
|
|
270
|
+
if (
|
|
271
|
+
native &&
|
|
272
|
+
native.image === null &&
|
|
273
|
+
original &&
|
|
274
|
+
this.image.imageSource === this.source
|
|
275
|
+
) {
|
|
276
|
+
native.image = original
|
|
277
|
+
// Core couldn't turn the suppressed UIImage into a template while it was
|
|
278
|
+
// hidden. Reapply its public native setter when a tint takes over.
|
|
279
|
+
if (this.image.tintColor)
|
|
280
|
+
(this.image as unknown as Record<symbol, (value: unknown) => void>)[
|
|
281
|
+
tintColorProperty.setNative
|
|
282
|
+
]!(this.image.tintColor)
|
|
283
|
+
}
|
|
284
|
+
this.renderer = this.scene = this.sprite = this.native = this.original = undefined
|
|
285
|
+
this.source = undefined
|
|
286
|
+
}
|
|
287
|
+
}
|
package/src/loader.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { Application, Color, isIOS, type View } from '@nativescript/core'
|
|
2
|
+
import { toView } from './animate.js'
|
|
3
|
+
import type { Target } from './types.js'
|
|
4
|
+
import { reducedMotion } from './config.js'
|
|
5
|
+
|
|
6
|
+
export type LoaderStyle = 'dots' | 'bars' | 'ring'
|
|
7
|
+
|
|
8
|
+
export interface LoaderOptions {
|
|
9
|
+
/** Defaults to 'dots'. */
|
|
10
|
+
style?: LoaderStyle
|
|
11
|
+
/** A NativeScript color string. Defaults to '#71717a'. */
|
|
12
|
+
color?: string
|
|
13
|
+
/** Maximum width and height in DIPs, fitted inside the target. Defaults to 48. */
|
|
14
|
+
size?: number
|
|
15
|
+
/** Milliseconds per cycle. Defaults to 1200. */
|
|
16
|
+
duration?: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const running = new WeakMap<View, () => void>()
|
|
20
|
+
const key = 'jolt:loader'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Adds a centered native iOS loading indicator. Returns an idempotent cleanup.
|
|
24
|
+
* Unloading releases native resources; loading resumes until explicitly stopped.
|
|
25
|
+
* Reduce Motion keeps the indicator static. Android is a no-op.
|
|
26
|
+
*/
|
|
27
|
+
export function loader(target: Target, options: LoaderOptions = {}): () => void {
|
|
28
|
+
const settings: Required<LoaderOptions> = {
|
|
29
|
+
style: options.style ?? 'dots',
|
|
30
|
+
color: options.color ?? '#71717a',
|
|
31
|
+
size: options.size ?? 48,
|
|
32
|
+
duration: options.duration ?? 1200,
|
|
33
|
+
}
|
|
34
|
+
if (!['dots', 'bars', 'ring'].includes(settings.style))
|
|
35
|
+
throw new TypeError('Unknown loader style.')
|
|
36
|
+
if (typeof settings.color !== 'string' || !Color.isValid(settings.color))
|
|
37
|
+
throw new TypeError('Loader color must be a valid color string.')
|
|
38
|
+
for (const name of ['size', 'duration'] as const)
|
|
39
|
+
if (!Number.isFinite(settings[name]) || settings[name] <= 0)
|
|
40
|
+
throw new RangeError(`Loader ${name} must be a positive finite number.`)
|
|
41
|
+
const view = toView(target)
|
|
42
|
+
running.get(view)?.()
|
|
43
|
+
if (!isIOS) return () => {}
|
|
44
|
+
return new Loader(view, settings).stop
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class Loader {
|
|
48
|
+
private stopped = false
|
|
49
|
+
private queued = false
|
|
50
|
+
private waitingForLoad: boolean
|
|
51
|
+
private native: UIView | undefined
|
|
52
|
+
private layer: CAReplicatorLayer | undefined
|
|
53
|
+
private item: CALayer | undefined
|
|
54
|
+
private animating = false
|
|
55
|
+
private observer: ReturnType<typeof Application.ios.addNotificationObserver> | undefined
|
|
56
|
+
|
|
57
|
+
constructor(
|
|
58
|
+
private readonly view: View,
|
|
59
|
+
private readonly settings: Required<LoaderOptions>,
|
|
60
|
+
) {
|
|
61
|
+
this.waitingForLoad = !view.isLoaded
|
|
62
|
+
running.set(view, this.stop)
|
|
63
|
+
view.on('loaded', this.loaded)
|
|
64
|
+
view.on('layoutChanged', this.schedule)
|
|
65
|
+
view.on('unloaded', this.unloaded)
|
|
66
|
+
this.schedule()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
stop = () => {
|
|
70
|
+
if (this.stopped) return
|
|
71
|
+
this.stopped = true
|
|
72
|
+
this.view.off('loaded', this.loaded)
|
|
73
|
+
this.view.off('layoutChanged', this.schedule)
|
|
74
|
+
this.view.off('unloaded', this.unloaded)
|
|
75
|
+
if (running.get(this.view) === this.stop) running.delete(this.view)
|
|
76
|
+
this.unloaded()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private loaded = () => {
|
|
80
|
+
this.waitingForLoad = false
|
|
81
|
+
this.schedule()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private unloaded = () => {
|
|
85
|
+
this.waitingForLoad = true
|
|
86
|
+
if (this.observer) {
|
|
87
|
+
Application.ios.removeNotificationObserver(
|
|
88
|
+
this.observer,
|
|
89
|
+
UIAccessibilityReduceMotionStatusDidChangeNotification,
|
|
90
|
+
)
|
|
91
|
+
this.observer = undefined
|
|
92
|
+
}
|
|
93
|
+
this.remove()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private schedule = () => {
|
|
97
|
+
if (this.stopped || this.queued) return
|
|
98
|
+
this.queued = true
|
|
99
|
+
void Promise.resolve().then(() => {
|
|
100
|
+
this.queued = false
|
|
101
|
+
if (this.stopped) return
|
|
102
|
+
try {
|
|
103
|
+
this.update()
|
|
104
|
+
} catch (error) {
|
|
105
|
+
this.stop()
|
|
106
|
+
throw error
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private update() {
|
|
112
|
+
if (this.waitingForLoad || !this.view.isLoaded) return
|
|
113
|
+
const native = this.view.nativeViewProtected as UIView | undefined
|
|
114
|
+
if (!native?.window) return this.remove()
|
|
115
|
+
const bounds = native.layer.bounds
|
|
116
|
+
const size = Math.min(this.settings.size, bounds.size.width, bounds.size.height)
|
|
117
|
+
if (size <= 0) return this.remove()
|
|
118
|
+
if (this.native && this.native !== native) this.remove()
|
|
119
|
+
this.observer ??= Application.ios.addNotificationObserver(
|
|
120
|
+
UIAccessibilityReduceMotionStatusDidChangeNotification,
|
|
121
|
+
this.schedule,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
CATransaction.begin()
|
|
125
|
+
CATransaction.setDisableActions(true)
|
|
126
|
+
try {
|
|
127
|
+
if (!this.layer) {
|
|
128
|
+
this.native = native
|
|
129
|
+
this.layer = CAReplicatorLayer.new()
|
|
130
|
+
this.layer.name = key
|
|
131
|
+
this.item = CALayer.new()
|
|
132
|
+
this.item.backgroundColor = new Color(this.settings.color).ios.CGColor
|
|
133
|
+
this.layer.addSublayer(this.item)
|
|
134
|
+
native.layer.addSublayer(this.layer)
|
|
135
|
+
}
|
|
136
|
+
const layer = this.layer
|
|
137
|
+
const item = this.item!
|
|
138
|
+
const { style, duration } = this.settings
|
|
139
|
+
layer.bounds = CGRectMake(0, 0, size, size)
|
|
140
|
+
layer.position = CGPointMake(
|
|
141
|
+
bounds.origin.x + bounds.size.width / 2,
|
|
142
|
+
bounds.origin.y + bounds.size.height / 2,
|
|
143
|
+
)
|
|
144
|
+
layer.instanceCount = style === 'dots' ? 3 : style === 'bars' ? 5 : 12
|
|
145
|
+
layer.instanceDelay = duration / 1000 / (style === 'ring' ? 12 : 5)
|
|
146
|
+
if (style === 'ring') {
|
|
147
|
+
const diameter = size * 0.16
|
|
148
|
+
item.bounds = CGRectMake(0, 0, diameter, diameter)
|
|
149
|
+
item.position = CGPointMake(size / 2, diameter / 2)
|
|
150
|
+
item.cornerRadius = diameter / 2
|
|
151
|
+
layer.instanceTransform = CATransform3DMakeRotation((2 * Math.PI) / 12, 0, 0, 1)
|
|
152
|
+
} else {
|
|
153
|
+
const width = size * (style === 'dots' ? 0.2 : 0.12)
|
|
154
|
+
const height = style === 'dots' ? width : size * 0.75
|
|
155
|
+
item.bounds = CGRectMake(0, 0, width, height)
|
|
156
|
+
item.position = CGPointMake(width / 2, size / 2)
|
|
157
|
+
item.cornerRadius = width / 2
|
|
158
|
+
layer.instanceTransform = CATransform3DMakeTranslation(
|
|
159
|
+
(size - width) / (layer.instanceCount - 1),
|
|
160
|
+
0,
|
|
161
|
+
0,
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
const reduced = reducedMotion()
|
|
165
|
+
item.opacity = style === 'ring' ? 0.35 : 1
|
|
166
|
+
if (reduced && this.animating) {
|
|
167
|
+
item.removeAnimationForKey(key)
|
|
168
|
+
this.animating = false
|
|
169
|
+
} else if (!reduced && !this.animating) {
|
|
170
|
+
const animation = CAKeyframeAnimation.animationWithKeyPath(
|
|
171
|
+
style === 'ring'
|
|
172
|
+
? 'opacity'
|
|
173
|
+
: style === 'bars'
|
|
174
|
+
? 'transform.scale.y'
|
|
175
|
+
: 'transform.scale',
|
|
176
|
+
)
|
|
177
|
+
animation.values = NSArray.arrayWithArray(
|
|
178
|
+
style === 'ring' ? [1, 0.2] : [0.35, 1, 0.35, 0.35],
|
|
179
|
+
)
|
|
180
|
+
animation.keyTimes = NSArray.arrayWithArray(
|
|
181
|
+
style === 'ring' ? [0, 1] : [0, 0.3, 0.6, 1],
|
|
182
|
+
)
|
|
183
|
+
animation.duration = duration / 1000
|
|
184
|
+
animation.repeatCount = Infinity
|
|
185
|
+
// Start one cycle in so every delayed copy is already participating.
|
|
186
|
+
animation.beginTime =
|
|
187
|
+
item.convertTimeFromLayer(CACurrentMediaTime(), null) - animation.duration
|
|
188
|
+
item.addAnimationForKey(animation, key)
|
|
189
|
+
this.animating = true
|
|
190
|
+
}
|
|
191
|
+
} finally {
|
|
192
|
+
CATransaction.commit()
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private remove() {
|
|
197
|
+
this.item?.removeAnimationForKey(key)
|
|
198
|
+
this.layer?.removeFromSuperlayer()
|
|
199
|
+
this.item = this.layer = this.native = undefined
|
|
200
|
+
this.animating = false
|
|
201
|
+
}
|
|
202
|
+
}
|