@rafaelaugusto/jolt 0.1.0-beta.5 → 0.1.0-beta.7
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 +122 -5
- package/dist/animate.d.ts +4 -2
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +48 -11
- package/dist/animate.js.map +1 -1
- package/dist/config.d.ts +22 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +34 -0
- package/dist/config.js.map +1 -0
- package/dist/driver/android.d.ts.map +1 -1
- package/dist/driver/android.js +7 -0
- package/dist/driver/android.js.map +1 -1
- package/dist/driver/index.d.ts +2 -0
- package/dist/driver/index.d.ts.map +1 -1
- package/dist/driver/index.js.map +1 -1
- package/dist/driver/ios-color.js +1 -1
- package/dist/driver/ios-color.js.map +1 -1
- package/dist/driver/ios-decoration.js +1 -1
- package/dist/driver/ios-decoration.js.map +1 -1
- package/dist/driver/ios-playback.d.ts +1 -1
- package/dist/driver/ios-playback.d.ts.map +1 -1
- package/dist/driver/ios-playback.js +14 -0
- package/dist/driver/ios-playback.js.map +1 -1
- package/dist/driver/ios.d.ts.map +1 -1
- package/dist/driver/ios.js +1 -0
- package/dist/driver/ios.js.map +1 -1
- package/dist/engine.d.ts +16 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +33 -1
- package/dist/engine.js.map +1 -1
- 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 +15 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- 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 +17 -0
- package/dist/keyboard.d.ts.map +1 -0
- package/dist/keyboard.js +116 -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/liquid.d.ts.map +1 -1
- package/dist/liquid.js +6 -1
- package/dist/liquid.js.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +2 -1
- package/dist/loader.js.map +1 -1
- package/dist/particles.d.ts.map +1 -1
- package/dist/particles.js +4 -3
- package/dist/particles.js.map +1 -1
- 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.map +1 -1
- package/dist/playback-group.js +29 -1
- package/dist/playback-group.js.map +1 -1
- package/dist/press.d.ts.map +1 -1
- package/dist/press.js +3 -3
- package/dist/press.js.map +1 -1
- package/dist/reveal.d.ts.map +1 -1
- package/dist/reveal.js +3 -2
- package/dist/reveal.js.map +1 -1
- 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 +202 -14
- package/dist/sequence.js.map +1 -1
- package/dist/shimmer.d.ts.map +1 -1
- package/dist/shimmer.js +2 -1
- package/dist/shimmer.js.map +1 -1
- 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.map +1 -1
- package/dist/symbol-effect.js +4 -3
- package/dist/symbol-effect.js.map +1 -1
- package/dist/text-layout.d.ts +8 -0
- package/dist/text-layout.d.ts.map +1 -1
- package/dist/text-layout.js +8 -2
- package/dist/text-layout.js.map +1 -1
- package/dist/text.d.ts +1 -1
- package/dist/text.d.ts.map +1 -1
- package/dist/text.js +60 -64
- package/dist/text.js.map +1 -1
- package/dist/track.d.ts +2 -0
- package/dist/track.d.ts.map +1 -1
- package/dist/track.js +62 -15
- package/dist/track.js.map +1 -1
- package/dist/types.d.ts +7 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/vue/index.d.ts +89 -0
- package/dist/vue/index.d.ts.map +1 -0
- package/dist/vue/index.js +134 -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 +16 -3
- 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/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 +10 -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/JoltPhotoZoom.h +23 -0
- package/platforms/ios/src/JoltPhotoZoom.m +229 -0
- package/platforms/ios/src/module.modulemap +5 -0
- package/src/android-plugin.d.ts +142 -0
- package/src/animate.ts +80 -30
- package/src/config.ts +46 -0
- package/src/driver/android.ts +14 -0
- package/src/driver/index.ts +2 -0
- package/src/driver/ios-color.ts +1 -1
- package/src/driver/ios-decoration.ts +1 -1
- package/src/driver/ios-playback.ts +15 -1
- package/src/driver/ios.ts +2 -0
- package/src/engine.ts +47 -2
- 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 +20 -1
- package/src/ios-plugin.d.ts +59 -0
- package/src/items.ts +140 -0
- package/src/keyboard.ts +132 -0
- package/src/label.ts +76 -0
- package/src/liquid.ts +6 -1
- package/src/loader.ts +2 -1
- package/src/particles.ts +4 -4
- package/src/path.ts +124 -0
- package/src/playback-group.ts +34 -1
- package/src/press.ts +3 -5
- package/src/reveal.ts +3 -3
- package/src/roll-diff.ts +57 -0
- package/src/roll.ts +515 -0
- package/src/scroll-linked.ts +73 -0
- package/src/sequence.ts +199 -14
- package/src/shimmer.ts +2 -1
- package/src/svg-path.ts +355 -0
- package/src/symbol-effect.ts +4 -3
- package/src/text-layout.ts +14 -2
- package/src/text.ts +62 -81
- package/src/track.ts +89 -24
- package/src/types.ts +8 -1
- package/src/vue/index.ts +189 -0
- package/src/zoom-native.ts +124 -0
- package/src/zoomable.ts +330 -0
package/src/index.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
export { animate, decay, 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 { scrollLinked } from './scroll-linked.js'
|
|
6
|
+
export type { ScrollLinkedOptions } from './scroll-linked.js'
|
|
7
|
+
export { animateItems } from './items.js'
|
|
8
|
+
export type { ItemsOptions } from './items.js'
|
|
9
|
+
export { followKeyboard } from './keyboard.js'
|
|
10
|
+
export type { KeyboardOptions } from './keyboard.js'
|
|
5
11
|
export type { Haptic, HapticOptions, HapticStyle } from './haptics.js'
|
|
6
12
|
export { confetti, particles } from './particles.js'
|
|
7
13
|
export type { ConfettiOptions, ParticleOptions } from './particles.js'
|
|
@@ -9,18 +15,31 @@ export { shimmer } from './shimmer.js'
|
|
|
9
15
|
export type { ShimmerOptions } from './shimmer.js'
|
|
10
16
|
export { liquid } from './liquid.js'
|
|
11
17
|
export type { LiquidOptions, LiquidEffect } from './liquid.js'
|
|
18
|
+
export { imageEffect } from './image-effect.js'
|
|
19
|
+
export type {
|
|
20
|
+
ImageEffect,
|
|
21
|
+
ImageEffectName,
|
|
22
|
+
ImageEffectOptions,
|
|
23
|
+
ImageEffectDiagnostics,
|
|
24
|
+
} from './image-effect.js'
|
|
25
|
+
export { zoomable } from './zoomable.js'
|
|
26
|
+
export type { Zoomable, ZoomableOptions, ZoomState, PhotoDismissal } from './zoomable.js'
|
|
12
27
|
export { loader } from './loader.js'
|
|
13
28
|
export type { LoaderOptions, LoaderStyle } from './loader.js'
|
|
14
29
|
export { reveal, conceal } from './reveal.js'
|
|
15
30
|
export type { RevealOptions, ConcealOptions } from './reveal.js'
|
|
16
31
|
export { animateText } from './text.js'
|
|
17
32
|
export type { TextOptions } from './text.js'
|
|
33
|
+
export { rollTo } from './roll.js'
|
|
34
|
+
export type { RollOptions } from './roll.js'
|
|
18
35
|
export { symbolEffect } from './symbol-effect.js'
|
|
19
36
|
export type {
|
|
20
37
|
SymbolEffect,
|
|
21
38
|
SymbolEffectName,
|
|
22
39
|
SymbolEffectOptions,
|
|
23
40
|
} from './symbol-effect.js'
|
|
41
|
+
export { configure } from './config.js'
|
|
42
|
+
export type { JoltConfig } from './config.js'
|
|
24
43
|
export { definePreset, presets } from './presets.js'
|
|
25
44
|
export type { Preset, PresetName, Presets } from './presets.js'
|
|
26
45
|
export { springs } from './transition.js'
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
declare class JoltImageEffect extends NSObject {
|
|
2
|
+
stopPreservingImage(): void
|
|
3
|
+
static createWithViewDestinationModeOriginXOriginYAmplitudeRadiusDurationSeedListener(
|
|
4
|
+
image: UIImageView,
|
|
5
|
+
destination: UIImage | null,
|
|
6
|
+
mode: number,
|
|
7
|
+
x: number,
|
|
8
|
+
y: number,
|
|
9
|
+
amplitude: number,
|
|
10
|
+
radius: number,
|
|
11
|
+
duration: number,
|
|
12
|
+
seed: number,
|
|
13
|
+
listener: (completed: boolean, progress: number) => void,
|
|
14
|
+
): JoltImageEffect | null
|
|
15
|
+
readonly progress: number
|
|
16
|
+
readonly paused: boolean
|
|
17
|
+
readonly frames: number
|
|
18
|
+
readonly longestFrame: number
|
|
19
|
+
readonly textureBytes: number
|
|
20
|
+
play(endpoint: number): void
|
|
21
|
+
pause(): void
|
|
22
|
+
resume(): void
|
|
23
|
+
seek(progress: number): void
|
|
24
|
+
finish(endpoint: number): void
|
|
25
|
+
stop(): void
|
|
26
|
+
}
|
|
27
|
+
declare class JoltPhotoZoom extends NSObject {
|
|
28
|
+
static attachImageViewportMinimumMaximumDoubleTapDismissReducedBackdropChromeOnDismissOnSettled(
|
|
29
|
+
image: UIImageView,
|
|
30
|
+
viewport: UIView,
|
|
31
|
+
minimum: number,
|
|
32
|
+
maximum: number,
|
|
33
|
+
doubleTap: number,
|
|
34
|
+
dismiss: boolean,
|
|
35
|
+
reduced: boolean,
|
|
36
|
+
backdrop: UIView | null,
|
|
37
|
+
chrome: UIView | null,
|
|
38
|
+
onDismiss: (
|
|
39
|
+
x: number,
|
|
40
|
+
y: number,
|
|
41
|
+
scale: number,
|
|
42
|
+
velocity: number,
|
|
43
|
+
progress: number,
|
|
44
|
+
) => void,
|
|
45
|
+
onSettled: (x: number, y: number, scale: number) => void,
|
|
46
|
+
): JoltPhotoZoom
|
|
47
|
+
readonly x: number
|
|
48
|
+
readonly y: number
|
|
49
|
+
readonly scale: number
|
|
50
|
+
readonly dismissProgress: number
|
|
51
|
+
readonly dismissPending: boolean
|
|
52
|
+
zoomFocusXFocusYAnimated(scale: number, x: number, y: number, animated: boolean): void
|
|
53
|
+
reset(animated: boolean): void
|
|
54
|
+
requestDismiss(): void
|
|
55
|
+
setReduced(value: boolean): void
|
|
56
|
+
setActive(value: boolean): void
|
|
57
|
+
layout(): void
|
|
58
|
+
stop(): void
|
|
59
|
+
}
|
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
|
+
}
|
package/src/keyboard.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { Application, Utils, isAndroid, isIOS, type View } from '@nativescript/core'
|
|
2
|
+
import { stop, toView } from './animate.js'
|
|
3
|
+
import type { Target } from './types.js'
|
|
4
|
+
|
|
5
|
+
export interface KeyboardOptions {
|
|
6
|
+
/** DIPs kept between the view's bottom edge and the keyboard. Defaults to zero. */
|
|
7
|
+
offset?: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const BEGIN_FROM_CURRENT_STATE = 4
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Lifts the view by its overlap with the software keyboard, at the keyboard's
|
|
14
|
+
* own speed and curve, and lets it back down when the keyboard hides.
|
|
15
|
+
* Returns a function that removes the effect.
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* const release = followKeyboard(composer)
|
|
19
|
+
* followKeyboard(sendButton, { offset: 12 })
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function followKeyboard(
|
|
23
|
+
target: Target,
|
|
24
|
+
options: KeyboardOptions = {},
|
|
25
|
+
): () => void {
|
|
26
|
+
const view = toView(target)
|
|
27
|
+
const offset = options.offset ?? 0
|
|
28
|
+
if (!Number.isFinite(offset)) throw new RangeError('Keyboard offset must be finite.')
|
|
29
|
+
if (isIOS) return followIOS(view, offset)
|
|
30
|
+
if (isAndroid) return followAndroid(view, offset)
|
|
31
|
+
return () => {}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function followIOS(view: View, offset: number) {
|
|
35
|
+
let disposed = false
|
|
36
|
+
let current = 0
|
|
37
|
+
const onKeyboard = (notification: NSNotification) => {
|
|
38
|
+
if (disposed || !view.isLoaded) return
|
|
39
|
+
const native = view.nativeViewProtected as UIView | undefined
|
|
40
|
+
const window = native?.window
|
|
41
|
+
const info = notification.userInfo
|
|
42
|
+
if (!native || !window || !info) return
|
|
43
|
+
const end = info.objectForKey(UIKeyboardFrameEndUserInfoKey)?.CGRectValue as
|
|
44
|
+
CGRect | undefined
|
|
45
|
+
if (!end) return
|
|
46
|
+
const keyboard = window.convertRectFromWindow(end, null)
|
|
47
|
+
const hidden =
|
|
48
|
+
keyboard.size.height <= 0 || keyboard.origin.y >= window.bounds.size.height
|
|
49
|
+
const frame = native.convertRectToView(native.bounds, null)
|
|
50
|
+
const bottom = frame.origin.y + frame.size.height - view.translateY
|
|
51
|
+
const overlap = hidden ? 0 : Math.max(0, bottom + offset - keyboard.origin.y)
|
|
52
|
+
if (overlap === current) return
|
|
53
|
+
current = overlap
|
|
54
|
+
stop(view, ['y'])
|
|
55
|
+
const apply = () => {
|
|
56
|
+
view.translateY = overlap ? -overlap : 0
|
|
57
|
+
}
|
|
58
|
+
const duration = info.objectForKey(UIKeyboardAnimationDurationUserInfoKey)
|
|
59
|
+
?.doubleValue as number | undefined
|
|
60
|
+
if (!duration) return apply()
|
|
61
|
+
const curve =
|
|
62
|
+
(info.objectForKey(UIKeyboardAnimationCurveUserInfoKey)?.integerValue as
|
|
63
|
+
number | undefined) ?? 0
|
|
64
|
+
// The keyboard's curve is private; shifted into the options it still applies.
|
|
65
|
+
UIView.animateWithDurationDelayOptionsAnimationsCompletion(
|
|
66
|
+
duration,
|
|
67
|
+
0,
|
|
68
|
+
((curve << 16) | BEGIN_FROM_CURRENT_STATE) as UIViewAnimationOptions,
|
|
69
|
+
apply,
|
|
70
|
+
() => {},
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
const observer = Application.ios.addNotificationObserver(
|
|
74
|
+
UIKeyboardWillChangeFrameNotification,
|
|
75
|
+
onKeyboard,
|
|
76
|
+
)
|
|
77
|
+
const cleanup = () => {
|
|
78
|
+
if (disposed) return
|
|
79
|
+
disposed = true
|
|
80
|
+
Application.ios.removeNotificationObserver(
|
|
81
|
+
observer,
|
|
82
|
+
UIKeyboardWillChangeFrameNotification,
|
|
83
|
+
)
|
|
84
|
+
view.off('unloaded', cleanup)
|
|
85
|
+
if (current) view.translateY = 0
|
|
86
|
+
}
|
|
87
|
+
view.on('unloaded', cleanup)
|
|
88
|
+
return cleanup
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function followAndroid(view: View, offset: number) {
|
|
92
|
+
if (android.os.Build.VERSION.SDK_INT < 30) return () => {}
|
|
93
|
+
const density = Utils.layout.getDisplayDensity()
|
|
94
|
+
let disposed = false
|
|
95
|
+
let follower: org.rafaelaugusto.jolt.KeyboardFollower | undefined
|
|
96
|
+
const attach = () => {
|
|
97
|
+
if (disposed || follower) return
|
|
98
|
+
const native = view.nativeViewProtected as android.view.View | undefined
|
|
99
|
+
if (!native) return
|
|
100
|
+
try {
|
|
101
|
+
follower = new org.rafaelaugusto.jolt.KeyboardFollower(
|
|
102
|
+
native,
|
|
103
|
+
offset * density,
|
|
104
|
+
new org.rafaelaugusto.jolt.KeyboardFollower.Listener({
|
|
105
|
+
onStart: () => stop(view, ['y']),
|
|
106
|
+
onSettled: (translation) => {
|
|
107
|
+
view.translateY = translation / density
|
|
108
|
+
},
|
|
109
|
+
}),
|
|
110
|
+
)
|
|
111
|
+
} catch {
|
|
112
|
+
// A JS-only update cannot install the native plugin. Leave the platform's resize in place.
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
follower.attach()
|
|
116
|
+
}
|
|
117
|
+
const cleanup = () => {
|
|
118
|
+
if (disposed) return
|
|
119
|
+
disposed = true
|
|
120
|
+
view.off('loaded', attach)
|
|
121
|
+
view.off('unloaded', cleanup)
|
|
122
|
+
if (follower) {
|
|
123
|
+
follower.detach()
|
|
124
|
+
follower = undefined
|
|
125
|
+
view.translateY = 0
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
view.on('loaded', attach)
|
|
129
|
+
view.on('unloaded', cleanup)
|
|
130
|
+
if (view.isLoaded) attach()
|
|
131
|
+
return cleanup
|
|
132
|
+
}
|
package/src/label.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Label } from '@nativescript/core'
|
|
2
|
+
|
|
3
|
+
const callbacks = new WeakMap<NSObject, (finished: boolean) => void>()
|
|
4
|
+
let Delegate: typeof NSObject | undefined
|
|
5
|
+
const styleEvents = [
|
|
6
|
+
'fontInternalChange',
|
|
7
|
+
'colorChange',
|
|
8
|
+
'textAlignmentChange',
|
|
9
|
+
'textDecorationChange',
|
|
10
|
+
'textTransformChange',
|
|
11
|
+
'letterSpacingChange',
|
|
12
|
+
'lineHeightChange',
|
|
13
|
+
'textShadowChange',
|
|
14
|
+
'textStrokeChange',
|
|
15
|
+
'whiteSpaceChange',
|
|
16
|
+
'textOverflowChange',
|
|
17
|
+
'maxLinesChange',
|
|
18
|
+
'paddingLeftChange',
|
|
19
|
+
'paddingRightChange',
|
|
20
|
+
'paddingTopChange',
|
|
21
|
+
'paddingBottomChange',
|
|
22
|
+
'visibilityChange',
|
|
23
|
+
'directionChange',
|
|
24
|
+
'borderLeftWidthChange',
|
|
25
|
+
'borderRightWidthChange',
|
|
26
|
+
'borderTopWidthChange',
|
|
27
|
+
'borderBottomWidthChange',
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
/** Reports text, formatted text and style changes of a Label until the returned function is called. */
|
|
31
|
+
export function watchLabel(view: Label, onChange: () => void): () => void {
|
|
32
|
+
const formatted = view.formattedText
|
|
33
|
+
view.on('textChange', onChange)
|
|
34
|
+
view.on('formattedTextChange', onChange)
|
|
35
|
+
formatted?.on('propertyChange', onChange)
|
|
36
|
+
for (const event of styleEvents) view.style.on(event, onChange)
|
|
37
|
+
return () => {
|
|
38
|
+
view.off('textChange', onChange)
|
|
39
|
+
view.off('formattedTextChange', onChange)
|
|
40
|
+
formatted?.off('propertyChange', onChange)
|
|
41
|
+
for (const event of styleEvents) view.style.off(event, onChange)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A CAAnimationDelegate that reports when its animation stops. Release it with `forgetCompletion`. */
|
|
46
|
+
export function completion(
|
|
47
|
+
callback: (finished: boolean) => void,
|
|
48
|
+
): NSObject & CAAnimationDelegate {
|
|
49
|
+
if (!Delegate) {
|
|
50
|
+
const base = NSObject as typeof NSObject & {
|
|
51
|
+
extend(
|
|
52
|
+
methods: object,
|
|
53
|
+
options: { protocols: unknown[]; name: string },
|
|
54
|
+
): typeof NSObject
|
|
55
|
+
}
|
|
56
|
+
Delegate = base.extend(
|
|
57
|
+
{
|
|
58
|
+
animationDidStopFinished(
|
|
59
|
+
this: NSObject,
|
|
60
|
+
_animation: CAAnimation,
|
|
61
|
+
finished: boolean,
|
|
62
|
+
) {
|
|
63
|
+
callbacks.get(this)?.(finished)
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{ protocols: [CAAnimationDelegate], name: 'JoltLabelAnimationDelegate' },
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
const delegate = Delegate.new()
|
|
70
|
+
callbacks.set(delegate, callback)
|
|
71
|
+
return delegate
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function forgetCompletion(delegate: NSObject) {
|
|
75
|
+
callbacks.delete(delegate)
|
|
76
|
+
}
|
package/src/liquid.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { tintColorProperty } from '@nativescript/core/ui/image'
|
|
|
3
3
|
import { toView } from './animate.js'
|
|
4
4
|
import { liquidGrid } from './liquid-grid.js'
|
|
5
5
|
import type { Target } from './types.js'
|
|
6
|
+
import { reducedMotion } from './config.js'
|
|
7
|
+
import { ownImage } from './image-owner.js'
|
|
6
8
|
|
|
7
9
|
export interface LiquidOptions {
|
|
8
10
|
/** Amount of internal deformation, from 0 to 1. Defaults to 0.7. */
|
|
@@ -49,6 +51,7 @@ export function liquid(target: Target, options: LiquidOptions = {}): LiquidEffec
|
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
class Liquid implements LiquidEffect {
|
|
54
|
+
private releaseOwnership: (() => void) | undefined
|
|
52
55
|
private stopped = false
|
|
53
56
|
private queued = false
|
|
54
57
|
private pausedByUser = false
|
|
@@ -68,6 +71,7 @@ class Liquid implements LiquidEffect {
|
|
|
68
71
|
private readonly image: Image,
|
|
69
72
|
private readonly settings: Required<LiquidOptions>,
|
|
70
73
|
) {
|
|
74
|
+
this.releaseOwnership = ownImage(image, this.stop)
|
|
71
75
|
running.set(image, this)
|
|
72
76
|
for (const event of ['loaded', 'layoutChanged', 'imageSourceChange', 'stretchChange'])
|
|
73
77
|
image.on(event, this.schedule)
|
|
@@ -92,6 +96,7 @@ class Liquid implements LiquidEffect {
|
|
|
92
96
|
stop = () => {
|
|
93
97
|
if (this.stopped) return
|
|
94
98
|
this.stopped = true
|
|
99
|
+
this.releaseOwnership?.()
|
|
95
100
|
for (const event of ['loaded', 'layoutChanged', 'imageSourceChange', 'stretchChange'])
|
|
96
101
|
this.image.off(event, this.schedule)
|
|
97
102
|
this.image.off('unloaded', this.unloaded)
|
|
@@ -134,7 +139,7 @@ class Liquid implements LiquidEffect {
|
|
|
134
139
|
!this.image.isLoaded ||
|
|
135
140
|
this.image.visibility !== 'visible' ||
|
|
136
141
|
this.image.tintColor ||
|
|
137
|
-
|
|
142
|
+
reducedMotion()
|
|
138
143
|
)
|
|
139
144
|
return this.remove()
|
|
140
145
|
const native = this.image.nativeViewProtected as UIImageView | undefined
|
package/src/loader.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Application, Color, isIOS, type View } from '@nativescript/core'
|
|
2
2
|
import { toView } from './animate.js'
|
|
3
3
|
import type { Target } from './types.js'
|
|
4
|
+
import { reducedMotion } from './config.js'
|
|
4
5
|
|
|
5
6
|
export type LoaderStyle = 'dots' | 'bars' | 'ring'
|
|
6
7
|
|
|
@@ -160,7 +161,7 @@ class Loader {
|
|
|
160
161
|
0,
|
|
161
162
|
)
|
|
162
163
|
}
|
|
163
|
-
const reduced =
|
|
164
|
+
const reduced = reducedMotion()
|
|
164
165
|
item.opacity = style === 'ring' ? 0.35 : 1
|
|
165
166
|
if (reduced && this.animating) {
|
|
166
167
|
item.removeAnimationForKey(key)
|
package/src/particles.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Application, Color, isIOS, type View } from '@nativescript/core'
|
|
2
2
|
import { toView } from './animate.js'
|
|
3
3
|
import type { Target } from './types.js'
|
|
4
|
+
import { reducedMotion } from './config.js'
|
|
4
5
|
|
|
5
6
|
export interface ConfettiOptions {
|
|
6
7
|
/** Approximate number of particles in the burst. Defaults to 80. */
|
|
@@ -78,8 +79,7 @@ function start(target: Target, settings: Settings, overlay: boolean): () => void
|
|
|
78
79
|
const view = toView(target)
|
|
79
80
|
const running = overlay ? bursts : emitters
|
|
80
81
|
running.get(view)?.()
|
|
81
|
-
if (!isIOS || settings.rate === 0 ||
|
|
82
|
-
return () => {}
|
|
82
|
+
if (!isIOS || settings.rate === 0 || reducedMotion()) return () => {}
|
|
83
83
|
return new Emitter(view, settings, overlay, running).stop
|
|
84
84
|
}
|
|
85
85
|
|
|
@@ -191,12 +191,12 @@ class Emitter {
|
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
private motionChanged = () => {
|
|
194
|
-
if (
|
|
194
|
+
if (reducedMotion()) this.stop()
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
private update() {
|
|
198
198
|
if (!this.view.isLoaded) return
|
|
199
|
-
if (
|
|
199
|
+
if (reducedMotion()) return this.stop()
|
|
200
200
|
const native = this.view.nativeViewProtected as UIView | undefined
|
|
201
201
|
const window = native?.window
|
|
202
202
|
if (this.layer && (!native || !window)) return this.stop()
|
package/src/path.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { parsePath, type MotionPath } from './svg-path.js'
|
|
2
|
+
import { reducedMotion, timeScale } from './config.js'
|
|
3
|
+
import { createTrack, instantTrack, type Track } from './track.js'
|
|
4
|
+
import {
|
|
5
|
+
mergeTransitions,
|
|
6
|
+
resolveTransition,
|
|
7
|
+
type Transition,
|
|
8
|
+
type ResolvedTransition,
|
|
9
|
+
} from './transition.js'
|
|
10
|
+
import type { Keyframes, KeyframeProperty } from './types.js'
|
|
11
|
+
|
|
12
|
+
export type PathProperty = 'x' | 'y' | 'rotate'
|
|
13
|
+
export interface PreparedPath {
|
|
14
|
+
readonly properties: readonly PathProperty[]
|
|
15
|
+
readonly starts: ReadonlyMap<PathProperty, number>
|
|
16
|
+
readonly transition: ResolvedTransition
|
|
17
|
+
track(property: PathProperty, offset: number, delay: number): Track
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function preparePath(
|
|
21
|
+
keyframes: Keyframes,
|
|
22
|
+
base: Transition,
|
|
23
|
+
overrides: Partial<Record<KeyframeProperty | 'path', Transition>>,
|
|
24
|
+
): PreparedPath | undefined {
|
|
25
|
+
if (keyframes.orient !== undefined && typeof keyframes.orient !== 'boolean')
|
|
26
|
+
throw new TypeError('Path orient must be a boolean.')
|
|
27
|
+
if (keyframes.path === undefined) {
|
|
28
|
+
if (keyframes.orient !== undefined) throw new TypeError('orient requires a path.')
|
|
29
|
+
return undefined
|
|
30
|
+
}
|
|
31
|
+
if (keyframes.x !== undefined || keyframes.y !== undefined)
|
|
32
|
+
throw new TypeError('A path owns x and y; omit explicit x/y keyframes.')
|
|
33
|
+
if (keyframes.orient && keyframes.rotate !== undefined)
|
|
34
|
+
throw new TypeError('An oriented path owns rotate; omit explicit rotate keyframes.')
|
|
35
|
+
if (overrides.x || overrides.y || (keyframes.orient && overrides.rotate))
|
|
36
|
+
throw new TypeError('Use the path transition to time its coordinates together.')
|
|
37
|
+
const geometry = parsePath(keyframes.path)
|
|
38
|
+
const orient = keyframes.orient === true
|
|
39
|
+
const transition = resolveTransition(
|
|
40
|
+
overrides.path ? mergeTransitions(base, overrides.path) : base,
|
|
41
|
+
'opacity',
|
|
42
|
+
2,
|
|
43
|
+
)
|
|
44
|
+
if (
|
|
45
|
+
!Number.isFinite(transition.curve.type === 'tween' ? transition.curve.duration : 0) ||
|
|
46
|
+
!Number.isFinite(transition.delay)
|
|
47
|
+
)
|
|
48
|
+
throw new RangeError('Path duration and delay must be finite.')
|
|
49
|
+
const properties: readonly PathProperty[] = orient ? ['x', 'y', 'rotate'] : ['x', 'y']
|
|
50
|
+
const first = geometry.at(0)
|
|
51
|
+
let motion: ReturnType<typeof sample> | undefined
|
|
52
|
+
return {
|
|
53
|
+
properties,
|
|
54
|
+
starts: new Map(properties.map((property) => [property, first[property]])),
|
|
55
|
+
transition,
|
|
56
|
+
track(property, offset, requested) {
|
|
57
|
+
motion ??= sample(geometry, transition, orient)
|
|
58
|
+
const { progress, times, frames } = motion
|
|
59
|
+
const to = geometry.at(progress.to)[property] + offset
|
|
60
|
+
if (reducedMotion()) return instantTrack(property, to)
|
|
61
|
+
const delay = requested / timeScale()
|
|
62
|
+
const shift = delay - progress.delay
|
|
63
|
+
const pose = (time: number) =>
|
|
64
|
+
geometry.at(progress.valueAt(time - shift))[property] + offset
|
|
65
|
+
return {
|
|
66
|
+
...progress,
|
|
67
|
+
property,
|
|
68
|
+
delay,
|
|
69
|
+
end: progress.end + shift,
|
|
70
|
+
arrival: progress.arrival + shift,
|
|
71
|
+
times,
|
|
72
|
+
values: offset
|
|
73
|
+
? frames[property].map((value) => value + offset)
|
|
74
|
+
: frames[property],
|
|
75
|
+
to,
|
|
76
|
+
valueAt: pose,
|
|
77
|
+
velocityAt(time) {
|
|
78
|
+
if (time <= delay || time >= progress.end + shift) return 0
|
|
79
|
+
const position = progress.valueAt(time - shift)
|
|
80
|
+
if (position < 0 || position > 1) return 0
|
|
81
|
+
const epsilon = Math.min(1e-5, 0.01 / Math.max(1, geometry.length))
|
|
82
|
+
const a = Math.max(0, position - epsilon),
|
|
83
|
+
b = Math.min(1, position + epsilon)
|
|
84
|
+
return (
|
|
85
|
+
((geometry.at(b)[property] - geometry.at(a)[property]) / (b - a)) *
|
|
86
|
+
progress.velocityAt(time - shift)
|
|
87
|
+
)
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Sample scalar timing once, then project all coordinates through the same path. */
|
|
95
|
+
function sample(geometry: MotionPath, transition: ResolvedTransition, orient: boolean) {
|
|
96
|
+
const progress = createTrack('opacity', [0, 1], transition, 0)
|
|
97
|
+
const count = Math.max(
|
|
98
|
+
1,
|
|
99
|
+
Math.min(
|
|
100
|
+
6000,
|
|
101
|
+
Math.max(
|
|
102
|
+
Math.ceil((progress.duration / 1000) * 60),
|
|
103
|
+
Math.ceil(geometry.length / 2),
|
|
104
|
+
),
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
const times = Array.from({ length: count + 1 }, (_, i) => i / count)
|
|
108
|
+
const frames: Record<PathProperty, number[]> = { x: [], y: [], rotate: [] }
|
|
109
|
+
for (const time of times) {
|
|
110
|
+
const position =
|
|
111
|
+
time === 0
|
|
112
|
+
? 0
|
|
113
|
+
: time === 1
|
|
114
|
+
? 1
|
|
115
|
+
: progress.valueAt(progress.delay + time * progress.duration)
|
|
116
|
+
if (!Number.isFinite(position))
|
|
117
|
+
throw new RangeError('Path easing must return finite progress.')
|
|
118
|
+
const pose = geometry.at(position)
|
|
119
|
+
frames.x.push(pose.x)
|
|
120
|
+
frames.y.push(pose.y)
|
|
121
|
+
if (orient) frames.rotate.push(pose.rotate)
|
|
122
|
+
}
|
|
123
|
+
return { progress, times, frames }
|
|
124
|
+
}
|