@rafaelaugusto/jolt 0.1.0-beta.1 → 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 +367 -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-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 +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/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 +43 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -1
- 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/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 +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 +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/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 +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-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 +193 -33
- package/src/engine.ts +321 -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 +59 -2
- 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/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 +63 -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,332 @@
|
|
|
1
|
+
import type { View } from '@nativescript/core'
|
|
2
|
+
import {
|
|
3
|
+
dip,
|
|
4
|
+
isWritingDecoration,
|
|
5
|
+
radiusKeys,
|
|
6
|
+
widthKeys,
|
|
7
|
+
validateDecorationTarget,
|
|
8
|
+
type DecorationProperty,
|
|
9
|
+
} from '../decoration.js'
|
|
10
|
+
import { iosPlayback } from './ios-playback.js'
|
|
11
|
+
import type { Track } from '../track.js'
|
|
12
|
+
|
|
13
|
+
type NativeView = UIView & { outerShadowContainerLayer?: CALayer }
|
|
14
|
+
type Binding = { layer: CALayer; path: string; values: unknown[] }
|
|
15
|
+
interface Handle {
|
|
16
|
+
elapsed(): number
|
|
17
|
+
control(elapsed: number, paused: boolean): void
|
|
18
|
+
track: Track
|
|
19
|
+
stop(): void
|
|
20
|
+
presented(): number | undefined
|
|
21
|
+
attach(): void
|
|
22
|
+
}
|
|
23
|
+
const playing = new WeakMap<View, Map<DecorationProperty, Handle>>()
|
|
24
|
+
const paths = {
|
|
25
|
+
borderRadius: 'cornerRadius',
|
|
26
|
+
borderWidth: 'borderWidth',
|
|
27
|
+
shadowOpacity: 'shadowOpacity',
|
|
28
|
+
shadowRadius: 'shadowRadius',
|
|
29
|
+
shadowOffsetX: 'shadowOffset.width',
|
|
30
|
+
shadowOffsetY: 'shadowOffset.height',
|
|
31
|
+
} as const
|
|
32
|
+
|
|
33
|
+
export function syncIOSDecoration(view: View, excluding?: string) {
|
|
34
|
+
for (const [property, handle] of playing.get(view) ?? [])
|
|
35
|
+
if (property !== excluding) handle.attach()
|
|
36
|
+
}
|
|
37
|
+
export const presentedIOSDecoration = (view: View, property: DecorationProperty) =>
|
|
38
|
+
playing.get(view)?.get(property)?.presented()
|
|
39
|
+
|
|
40
|
+
/** Animate Core's uniform border and its separate outer-shadow layers. */
|
|
41
|
+
export function playIOSDecoration(
|
|
42
|
+
view: View,
|
|
43
|
+
track: Track,
|
|
44
|
+
startedAt: number,
|
|
45
|
+
onEnd: (finished?: boolean) => void,
|
|
46
|
+
): Handle {
|
|
47
|
+
const timing = iosPlayback(track, startedAt)
|
|
48
|
+
const property = track.property as Exclude<
|
|
49
|
+
DecorationProperty,
|
|
50
|
+
'elevation' | 'translationZ'
|
|
51
|
+
>
|
|
52
|
+
const native = view.nativeViewProtected as NativeView
|
|
53
|
+
const key = `jolt:${property}`
|
|
54
|
+
let bindings: Binding[] = []
|
|
55
|
+
let stopped = false
|
|
56
|
+
let queued = false
|
|
57
|
+
let generation = 0
|
|
58
|
+
const entries = playing.get(view) ?? new Map<DecorationProperty, Handle>()
|
|
59
|
+
playing.set(view, entries)
|
|
60
|
+
const cleanup = () => {
|
|
61
|
+
if (stopped) return
|
|
62
|
+
stopped = true
|
|
63
|
+
generation++
|
|
64
|
+
view.off('unloaded', cancel)
|
|
65
|
+
view.off('layoutChanged', refresh)
|
|
66
|
+
view.style.off('backgroundInternalChange', refresh)
|
|
67
|
+
for (const event of changes) view.style.off(event, external)
|
|
68
|
+
for (const binding of bindings) binding.layer.removeAnimationForKey(key)
|
|
69
|
+
if (entries.get(property) === handle) entries.delete(property)
|
|
70
|
+
}
|
|
71
|
+
const cancel = () => {
|
|
72
|
+
if (!stopped) {
|
|
73
|
+
cleanup()
|
|
74
|
+
onEnd(false)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const refresh = () => {
|
|
78
|
+
if (stopped || queued) return
|
|
79
|
+
queued = true
|
|
80
|
+
void Promise.resolve().then(() => {
|
|
81
|
+
queued = false
|
|
82
|
+
if (!stopped && view.nativeViewProtected === native) handle.attach()
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
const external = (event: { eventName: string }) => {
|
|
86
|
+
if (isWritingDecoration(view)) return
|
|
87
|
+
const own =
|
|
88
|
+
property === 'borderRadius'
|
|
89
|
+
? radiusKeys
|
|
90
|
+
: property === 'borderWidth'
|
|
91
|
+
? widthKeys
|
|
92
|
+
: ['boxShadow']
|
|
93
|
+
void Promise.resolve().then(() => {
|
|
94
|
+
if (stopped) return
|
|
95
|
+
if (own.some((name) => event.eventName === `${name}Change`)) return cancel()
|
|
96
|
+
try {
|
|
97
|
+
validateDecorationTarget(view, property)
|
|
98
|
+
} catch {
|
|
99
|
+
cancel()
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
const changes = [
|
|
104
|
+
...radiusKeys,
|
|
105
|
+
...widthKeys,
|
|
106
|
+
'borderTopColor',
|
|
107
|
+
'borderRightColor',
|
|
108
|
+
'borderBottomColor',
|
|
109
|
+
'borderLeftColor',
|
|
110
|
+
'clipPath',
|
|
111
|
+
'cornerShape',
|
|
112
|
+
'boxShadow',
|
|
113
|
+
].map((name) => `${name}Change`)
|
|
114
|
+
const handle: Handle = {
|
|
115
|
+
track,
|
|
116
|
+
stop: cancel,
|
|
117
|
+
elapsed: timing.elapsed,
|
|
118
|
+
control(time, paused) {
|
|
119
|
+
if (!stopped) {
|
|
120
|
+
timing.control(time, paused)
|
|
121
|
+
handle.attach()
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
presented() {
|
|
125
|
+
if (timing.controlled) return undefined
|
|
126
|
+
const binding = bindings[0]
|
|
127
|
+
const layer = binding?.layer.presentationLayer()
|
|
128
|
+
if (!layer) return track.valueAt(timing.elapsed())
|
|
129
|
+
if (property === 'shadowOffsetX') return layer.shadowOffset.width
|
|
130
|
+
if (property === 'shadowOffsetY') return layer.shadowOffset.height
|
|
131
|
+
return (layer as unknown as Record<string, number>)[paths[property]]!
|
|
132
|
+
},
|
|
133
|
+
attach() {
|
|
134
|
+
if (stopped) return
|
|
135
|
+
const instance = ++generation
|
|
136
|
+
for (const binding of bindings) binding.layer.removeAnimationForKey(key)
|
|
137
|
+
bindings = []
|
|
138
|
+
// Path clipping and spread are nonlinear. Sample even a linear radius
|
|
139
|
+
// tween so its shadow and inner cutout follow the capped native corner.
|
|
140
|
+
const steps = Math.max(1, Math.min(6000, Math.ceil((track.duration * 60) / 1000)))
|
|
141
|
+
const times =
|
|
142
|
+
property === 'borderRadius'
|
|
143
|
+
? [
|
|
144
|
+
...new Set([
|
|
145
|
+
...track.times,
|
|
146
|
+
...Array.from({ length: steps + 1 }, (_, i) => i / steps),
|
|
147
|
+
]),
|
|
148
|
+
].sort((a, b) => a - b)
|
|
149
|
+
: [...track.times]
|
|
150
|
+
const values =
|
|
151
|
+
property === 'borderRadius'
|
|
152
|
+
? times.map((time) =>
|
|
153
|
+
time === 1
|
|
154
|
+
? track.values.at(-1)!
|
|
155
|
+
: track.valueAt(track.delay + time * track.duration),
|
|
156
|
+
)
|
|
157
|
+
: [...track.values]
|
|
158
|
+
if (property === 'borderRadius' || property === 'borderWidth') {
|
|
159
|
+
bindings.push({
|
|
160
|
+
layer: native.layer,
|
|
161
|
+
path: paths[property],
|
|
162
|
+
values:
|
|
163
|
+
property === 'borderRadius' && native.bounds?.size
|
|
164
|
+
? values.map((value) =>
|
|
165
|
+
Math.min(
|
|
166
|
+
value,
|
|
167
|
+
native.bounds.size.width / 2,
|
|
168
|
+
native.bounds.size.height / 2,
|
|
169
|
+
),
|
|
170
|
+
)
|
|
171
|
+
: values,
|
|
172
|
+
})
|
|
173
|
+
if (property === 'borderRadius') {
|
|
174
|
+
const shadows = view.boxShadow
|
|
175
|
+
const layers = native.outerShadowContainerLayer?.sublayers
|
|
176
|
+
if (Array.isArray(shadows) && layers) {
|
|
177
|
+
for (let i = 0; i < Math.min(shadows.length, layers.count); i++) {
|
|
178
|
+
const layer = layers.objectAtIndex(i)
|
|
179
|
+
const spread = dip(shadows[i]!.spreadRadius)
|
|
180
|
+
bindings.push({
|
|
181
|
+
layer,
|
|
182
|
+
path: 'shadowPath',
|
|
183
|
+
values: values.map((radius) =>
|
|
184
|
+
roundedPath(native.bounds, radius, spread),
|
|
185
|
+
),
|
|
186
|
+
})
|
|
187
|
+
if (layer.mask)
|
|
188
|
+
bindings.push({
|
|
189
|
+
layer: layer.mask,
|
|
190
|
+
path: 'path',
|
|
191
|
+
values: values.map((radius) =>
|
|
192
|
+
shadowMask(native.bounds, radius, maskExtent(view, entries)),
|
|
193
|
+
),
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
const layer = native.outerShadowContainerLayer?.sublayers?.firstObject
|
|
200
|
+
if (!layer) return cancel()
|
|
201
|
+
bindings.push({ layer, path: paths[property], values: [...track.values] })
|
|
202
|
+
// Core's mask bounds are sized for the final blur/offset. Give intermediate
|
|
203
|
+
// samples room too. Radius owns the animated inner edge when it is active.
|
|
204
|
+
if (layer.mask && !entries.has('borderRadius')) {
|
|
205
|
+
CATransaction.begin()
|
|
206
|
+
CATransaction.setDisableActions(true)
|
|
207
|
+
;(layer.mask as CAShapeLayer).path = shadowMask(
|
|
208
|
+
native.bounds,
|
|
209
|
+
native.layer.cornerRadius,
|
|
210
|
+
maskExtent(view, entries),
|
|
211
|
+
)
|
|
212
|
+
CATransaction.commit()
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
CATransaction.begin()
|
|
216
|
+
CATransaction.setDisableActions(true)
|
|
217
|
+
if (!timing.paused && track.repeat !== Infinity)
|
|
218
|
+
CATransaction.setCompletionBlock(() => {
|
|
219
|
+
if (stopped || generation !== instance) return
|
|
220
|
+
cleanup()
|
|
221
|
+
onEnd()
|
|
222
|
+
})
|
|
223
|
+
// Core treats a zero-alpha shadow as opaque. Commit the native endpoint
|
|
224
|
+
// explicitly too; otherwise a fade to zero would flash back on completion.
|
|
225
|
+
const paint = bindings[0]!.layer
|
|
226
|
+
if (property === 'shadowOpacity') paint.shadowOpacity = track.to
|
|
227
|
+
else if (property === 'shadowRadius') paint.shadowRadius = track.to
|
|
228
|
+
else if (property === 'shadowOffsetX')
|
|
229
|
+
paint.shadowOffset = CGSizeMake(track.to, paint.shadowOffset.height)
|
|
230
|
+
else if (property === 'shadowOffsetY')
|
|
231
|
+
paint.shadowOffset = CGSizeMake(paint.shadowOffset.width, track.to)
|
|
232
|
+
for (const { layer, path, values } of bindings) {
|
|
233
|
+
const animation = CAKeyframeAnimation.animationWithKeyPath(path)
|
|
234
|
+
animation.values = NSArray.arrayWithArray(values)
|
|
235
|
+
animation.keyTimes = NSArray.arrayWithArray(times)
|
|
236
|
+
animation.duration = Math.max(1, track.duration) / 1000
|
|
237
|
+
if (track.repeat > 0) {
|
|
238
|
+
animation.autoreverses = track.reverse
|
|
239
|
+
animation.repeatCount = track.reverse
|
|
240
|
+
? (track.repeat + 1) / 2
|
|
241
|
+
: track.repeat + 1
|
|
242
|
+
}
|
|
243
|
+
timing.configure(animation, layer)
|
|
244
|
+
if (animation.respondsToSelector('setPreferredFrameRateRange:')) {
|
|
245
|
+
const max = Math.min(
|
|
246
|
+
120,
|
|
247
|
+
native.window?.screen.maximumFramesPerSecond ??
|
|
248
|
+
UIScreen.mainScreen.maximumFramesPerSecond,
|
|
249
|
+
)
|
|
250
|
+
animation.preferredFrameRateRange = CAFrameRateRangeMake(
|
|
251
|
+
Math.min(80, max),
|
|
252
|
+
max,
|
|
253
|
+
max,
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
layer.addAnimationForKey(animation, key)
|
|
257
|
+
}
|
|
258
|
+
CATransaction.commit()
|
|
259
|
+
},
|
|
260
|
+
}
|
|
261
|
+
entries.set(property, handle)
|
|
262
|
+
view.on('unloaded', cancel)
|
|
263
|
+
view.on('layoutChanged', refresh)
|
|
264
|
+
view.style.on('backgroundInternalChange', refresh)
|
|
265
|
+
for (const event of changes) view.style.on(event, external)
|
|
266
|
+
// All model endpoints have been committed before driver.play(). Each handle
|
|
267
|
+
// follows the same timeline even if Core redraws its layers during layout.
|
|
268
|
+
for (const active of entries.values()) active.attach()
|
|
269
|
+
return handle
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function maskExtent(view: View, entries: Map<DecorationProperty, Handle>): number {
|
|
273
|
+
const shadows = Array.isArray(view.boxShadow) ? view.boxShadow : []
|
|
274
|
+
const maximum = (property: DecorationProperty, fallback: number) =>
|
|
275
|
+
Math.max(
|
|
276
|
+
Math.abs(fallback),
|
|
277
|
+
...(entries.get(property)?.track.values.map(Math.abs) ?? []),
|
|
278
|
+
)
|
|
279
|
+
return Math.max(
|
|
280
|
+
1,
|
|
281
|
+
...shadows.map(
|
|
282
|
+
(shadow) =>
|
|
283
|
+
maximum('shadowRadius', dip(shadow.blurRadius) / 2) * 6 +
|
|
284
|
+
Math.abs(dip(shadow.spreadRadius)) +
|
|
285
|
+
maximum('shadowOffsetX', dip(shadow.offsetX)) +
|
|
286
|
+
maximum('shadowOffsetY', dip(shadow.offsetY)) +
|
|
287
|
+
1,
|
|
288
|
+
),
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Keep identical path topology at radius zero so CA can interpolate it. */
|
|
293
|
+
function roundedPath(bounds: CGRect, radius: number, spread = 0): interop.Pointer {
|
|
294
|
+
const rect = CGRectInset(bounds, -spread, -spread)
|
|
295
|
+
const r = Math.min(
|
|
296
|
+
Math.max(0, Math.min(radius, bounds.size.width / 2, bounds.size.height / 2) + spread),
|
|
297
|
+
Math.max(0, rect.size.width / 2),
|
|
298
|
+
Math.max(0, rect.size.height / 2),
|
|
299
|
+
)
|
|
300
|
+
const x = rect.origin.x,
|
|
301
|
+
y = rect.origin.y,
|
|
302
|
+
right = x + rect.size.width,
|
|
303
|
+
bottom = y + rect.size.height
|
|
304
|
+
const k = r * 0.5522847498307936
|
|
305
|
+
const path = CGPathCreateMutable()
|
|
306
|
+
CGPathMoveToPoint(path, null, x + r, y)
|
|
307
|
+
CGPathAddLineToPoint(path, null, right - r, y)
|
|
308
|
+
CGPathAddCurveToPoint(path, null, right - r + k, y, right, y + r - k, right, y + r)
|
|
309
|
+
CGPathAddLineToPoint(path, null, right, bottom - r)
|
|
310
|
+
CGPathAddCurveToPoint(
|
|
311
|
+
path,
|
|
312
|
+
null,
|
|
313
|
+
right,
|
|
314
|
+
bottom - r + k,
|
|
315
|
+
right - r + k,
|
|
316
|
+
bottom,
|
|
317
|
+
right - r,
|
|
318
|
+
bottom,
|
|
319
|
+
)
|
|
320
|
+
CGPathAddLineToPoint(path, null, x + r, bottom)
|
|
321
|
+
CGPathAddCurveToPoint(path, null, x + r - k, bottom, x, bottom - r + k, x, bottom - r)
|
|
322
|
+
CGPathAddLineToPoint(path, null, x, y + r)
|
|
323
|
+
CGPathAddCurveToPoint(path, null, x, y + r - k, x + r - k, y, x + r, y)
|
|
324
|
+
CGPathCloseSubpath(path)
|
|
325
|
+
return path
|
|
326
|
+
}
|
|
327
|
+
function shadowMask(bounds: CGRect, radius: number, extent: number): interop.Pointer {
|
|
328
|
+
const path = CGPathCreateMutable()
|
|
329
|
+
CGPathAddPath(path, null, roundedPath(bounds, radius))
|
|
330
|
+
CGPathAddRect(path, null, CGRectInset(bounds, -extent, -extent))
|
|
331
|
+
return path
|
|
332
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { View } from '@nativescript/core'
|
|
2
|
+
|
|
3
|
+
type LayoutView = View & {
|
|
4
|
+
_isTransformed: boolean
|
|
5
|
+
_modifyNativeViewFrame(native: UIView, frame: CGRect): void
|
|
6
|
+
}
|
|
7
|
+
const retained = new WeakMap<View, { count: number; restore(): void }>()
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* NativeScript caches whether its JS transform is nonidentity. Interactive native
|
|
11
|
+
* playback can move the actual model while that cached destination is identity.
|
|
12
|
+
* Let Core reset/restore the real transform when it next assigns a layout frame.
|
|
13
|
+
* This hook belongs only to the opted-in instance and only while tracks own it.
|
|
14
|
+
*/
|
|
15
|
+
export function retainInteractiveLayout(view: View): () => void {
|
|
16
|
+
const host = view as LayoutView
|
|
17
|
+
let entry = retained.get(view)
|
|
18
|
+
if (!entry) {
|
|
19
|
+
const original = host._modifyNativeViewFrame
|
|
20
|
+
if (typeof original !== 'function') return () => {}
|
|
21
|
+
const descriptor = Object.getOwnPropertyDescriptor(host, '_modifyNativeViewFrame')
|
|
22
|
+
let active = true
|
|
23
|
+
const wrapper: LayoutView['_modifyNativeViewFrame'] = function (
|
|
24
|
+
this: LayoutView,
|
|
25
|
+
native,
|
|
26
|
+
frame,
|
|
27
|
+
) {
|
|
28
|
+
if (active)
|
|
29
|
+
this._isTransformed = !CATransform3DEqualToTransform(
|
|
30
|
+
native.layer.transform,
|
|
31
|
+
CATransform3DIdentity,
|
|
32
|
+
)
|
|
33
|
+
original.call(this, native, frame)
|
|
34
|
+
}
|
|
35
|
+
host._modifyNativeViewFrame = wrapper
|
|
36
|
+
entry = {
|
|
37
|
+
count: 0,
|
|
38
|
+
restore() {
|
|
39
|
+
active = false
|
|
40
|
+
const native = host.nativeViewProtected as UIView | undefined
|
|
41
|
+
if (native)
|
|
42
|
+
host._isTransformed = !CATransform3DEqualToTransform(
|
|
43
|
+
native.layer.transform,
|
|
44
|
+
CATransform3DIdentity,
|
|
45
|
+
)
|
|
46
|
+
if (host._modifyNativeViewFrame !== wrapper) return
|
|
47
|
+
if (descriptor) Object.defineProperty(host, '_modifyNativeViewFrame', descriptor)
|
|
48
|
+
else delete (host as Partial<LayoutView>)._modifyNativeViewFrame
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
retained.set(view, entry)
|
|
52
|
+
}
|
|
53
|
+
entry.count++
|
|
54
|
+
let released = false
|
|
55
|
+
return () => {
|
|
56
|
+
if (released) return
|
|
57
|
+
released = true
|
|
58
|
+
if (--entry.count === 0) {
|
|
59
|
+
entry.restore()
|
|
60
|
+
retained.delete(view)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Track } from '../track.js'
|
|
2
|
+
|
|
3
|
+
/** Each CAAnimation owns its clock; the view and its children keep their speed. */
|
|
4
|
+
export function iosPlayback(track: Track, startedAt: number) {
|
|
5
|
+
let anchor = startedAt
|
|
6
|
+
let offset = 0
|
|
7
|
+
let paused = false
|
|
8
|
+
let controlled = false
|
|
9
|
+
const elapsed = () => offset + (paused ? 0 : CACurrentMediaTime() * 1000 - anchor)
|
|
10
|
+
return {
|
|
11
|
+
elapsed,
|
|
12
|
+
get controlled() {
|
|
13
|
+
return controlled
|
|
14
|
+
},
|
|
15
|
+
get paused() {
|
|
16
|
+
return paused
|
|
17
|
+
},
|
|
18
|
+
control(time: number, hold: boolean) {
|
|
19
|
+
controlled = true
|
|
20
|
+
offset = time
|
|
21
|
+
anchor = CACurrentMediaTime() * 1000
|
|
22
|
+
paused = hold
|
|
23
|
+
},
|
|
24
|
+
configure(animation: CAKeyframeAnimation, layer: CALayer) {
|
|
25
|
+
const now = CACurrentMediaTime()
|
|
26
|
+
const position = elapsed()
|
|
27
|
+
animation.speed = paused ? 0 : 1
|
|
28
|
+
animation.timeOffset = paused ? Math.max(0, position - track.delay) / 1000 : 0
|
|
29
|
+
animation.beginTime = layer.convertTimeFromLayer(
|
|
30
|
+
paused ? now : now - position / 1000 + track.delay / 1000,
|
|
31
|
+
null,
|
|
32
|
+
)
|
|
33
|
+
animation.fillMode = paused ? 'both' : kCAFillModeBackwards
|
|
34
|
+
animation.removedOnCompletion = !paused && track.repeat !== Infinity
|
|
35
|
+
// With speed 0, CA folds an exact duration boundary back to the first
|
|
36
|
+
// frame. Hold the actual endpoint with a constant copy, also when a
|
|
37
|
+
// shorter property waits at its end while another one is being scrubbed.
|
|
38
|
+
if (paused && Number.isFinite(track.end) && position >= track.end) {
|
|
39
|
+
const values = animation.values!
|
|
40
|
+
const index = track.reverse && track.repeat % 2 === 1 ? 0 : values.count - 1
|
|
41
|
+
const value = values.objectAtIndex(index)
|
|
42
|
+
animation.values = NSArray.arrayWithArray([value, value])
|
|
43
|
+
animation.keyTimes = NSArray.arrayWithArray([0, 1])
|
|
44
|
+
animation.duration = 1
|
|
45
|
+
animation.repeatCount = 0
|
|
46
|
+
animation.autoreverses = false
|
|
47
|
+
animation.timeOffset = 0
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { View } from '@nativescript/core'
|
|
2
|
+
|
|
3
|
+
type BackgroundView = { _redrawNativeBackground?: (...args: unknown[]) => void }
|
|
4
|
+
const repairs = new WeakSet<View>()
|
|
5
|
+
|
|
6
|
+
/** Core treats alpha 0 as 1. Repair its model layer after background redraws,
|
|
7
|
+
* not on animation frames. The wrapper belongs only to this view and is
|
|
8
|
+
* removed when the shadow color changes. */
|
|
9
|
+
export function preserveZeroShadow(view: View) {
|
|
10
|
+
const target = view as unknown as BackgroundView
|
|
11
|
+
const hasZero = () =>
|
|
12
|
+
Array.isArray(view.boxShadow) &&
|
|
13
|
+
view.boxShadow.some((shadow) => shadow.color.argb >>> 24 === 0)
|
|
14
|
+
if (!hasZero() || repairs.has(view)) return
|
|
15
|
+
const descriptor = Object.getOwnPropertyDescriptor(view, '_redrawNativeBackground')
|
|
16
|
+
const original = target._redrawNativeBackground
|
|
17
|
+
const apply = () => {
|
|
18
|
+
if (!hasZero()) return dispose()
|
|
19
|
+
if (!view.isLoaded) return
|
|
20
|
+
const layers = view.nativeViewProtected?.outerShadowContainerLayer?.sublayers as
|
|
21
|
+
NSArray<CALayer> | undefined
|
|
22
|
+
if (!layers || !Array.isArray(view.boxShadow)) return
|
|
23
|
+
CATransaction.begin()
|
|
24
|
+
CATransaction.setDisableActions(true)
|
|
25
|
+
for (let i = 0; i < Math.min(layers.count, view.boxShadow.length); i++)
|
|
26
|
+
if (view.boxShadow[i]!.color.argb >>> 24 === 0)
|
|
27
|
+
layers.objectAtIndex(i).shadowOpacity = 0
|
|
28
|
+
CATransaction.commit()
|
|
29
|
+
}
|
|
30
|
+
const redraw = function (this: BackgroundView, ...args: unknown[]) {
|
|
31
|
+
original!.apply(this, args)
|
|
32
|
+
apply()
|
|
33
|
+
}
|
|
34
|
+
const dispose = () => {
|
|
35
|
+
view.style.off('boxShadowChange', apply)
|
|
36
|
+
view.off('loaded', apply)
|
|
37
|
+
view.off('layoutChanged', apply)
|
|
38
|
+
if (original && target._redrawNativeBackground === redraw) {
|
|
39
|
+
if (descriptor) Object.defineProperty(view, '_redrawNativeBackground', descriptor)
|
|
40
|
+
else delete target._redrawNativeBackground
|
|
41
|
+
}
|
|
42
|
+
repairs.delete(view)
|
|
43
|
+
}
|
|
44
|
+
repairs.add(view)
|
|
45
|
+
if (original) target._redrawNativeBackground = redraw
|
|
46
|
+
view.style.on('boxShadowChange', apply)
|
|
47
|
+
view.on('loaded', apply)
|
|
48
|
+
view.on('layoutChanged', apply)
|
|
49
|
+
apply()
|
|
50
|
+
}
|