@rafaelaugusto/jolt 0.1.0-beta.2 → 0.1.0-beta.4
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 +112 -16
- package/dist/animate.d.ts +3 -1
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +89 -3
- 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/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/driver/android-color.d.ts +17 -0
- package/dist/driver/android-color.d.ts.map +1 -0
- package/dist/driver/android-color.js +134 -0
- package/dist/driver/android-color.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 +101 -5
- package/dist/driver/android.js.map +1 -1
- package/dist/driver/index.d.ts +4 -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 +14 -0
- package/dist/driver/ios-color.d.ts.map +1 -0
- package/dist/driver/ios-color.js +381 -0
- package/dist/driver/ios-color.js.map +1 -0
- package/dist/driver/ios.d.ts.map +1 -1
- package/dist/driver/ios.js +9 -1
- package/dist/driver/ios.js.map +1 -1
- package/dist/engine.d.ts +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +15 -8
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- 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 +229 -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 +172 -0
- package/dist/loader.js.map +1 -0
- package/dist/press.d.ts.map +1 -1
- package/dist/press.js +6 -1
- package/dist/press.js.map +1 -1
- package/dist/properties.d.ts +5 -3
- package/dist/properties.d.ts.map +1 -1
- package/dist/properties.js +13 -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 +293 -0
- package/dist/reveal.js.map +1 -0
- package/dist/text-layout.d.ts +22 -0
- package/dist/text-layout.d.ts.map +1 -0
- package/dist/text-layout.js +179 -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 +267 -0
- package/dist/text.js.map +1 -0
- package/dist/track.d.ts +1 -1
- package/dist/track.d.ts.map +1 -1
- package/dist/track.js +63 -5
- package/dist/track.js.map +1 -1
- package/dist/transition.d.ts +5 -0
- package/dist/transition.d.ts.map +1 -1
- package/dist/transition.js +8 -2
- package/dist/transition.js.map +1 -1
- package/dist/types.d.ts +19 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +11 -2
- package/platforms/android/include.gradle +7 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/ColorTarget.java +102 -0
- package/platforms/android/proguard-rules.pro +6 -0
- package/src/animate.ts +105 -5
- package/src/color.ts +101 -0
- package/src/decay.ts +45 -0
- package/src/driver/android-color.ts +168 -0
- package/src/driver/android.ts +120 -10
- package/src/driver/index.ts +9 -2
- package/src/driver/ios-color.ts +432 -0
- package/src/driver/ios.ts +13 -3
- package/src/engine.ts +17 -9
- package/src/index.ts +10 -1
- package/src/liquid-grid.ts +28 -0
- package/src/liquid.ts +282 -0
- package/src/loader.ts +201 -0
- package/src/press.ts +8 -2
- package/src/properties.ts +16 -6
- package/src/reveal.ts +374 -0
- package/src/text-layout.ts +216 -0
- package/src/text.ts +298 -0
- package/src/track.ts +105 -5
- package/src/transition.ts +16 -2
- package/src/types.ts +18 -0
package/src/driver/android.ts
CHANGED
|
@@ -1,11 +1,52 @@
|
|
|
1
1
|
import { Utils, type View } from '@nativescript/core'
|
|
2
|
-
import type {
|
|
3
|
-
import {
|
|
2
|
+
import type { NumericProperty } from '../properties.js'
|
|
3
|
+
import type { Driver } from './index.js'
|
|
4
|
+
import { isColorProperty } from '../color.js'
|
|
5
|
+
import { playAndroidColor, presentedAndroidColor } from './android-color.js'
|
|
4
6
|
|
|
5
7
|
type NativeView = android.view.View
|
|
6
8
|
|
|
9
|
+
interface Handle {
|
|
10
|
+
cancel(): void
|
|
11
|
+
elapsed(): number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Own only layers we create; keep app-configured hardware/software layers and paints intact. */
|
|
15
|
+
function fadeLayer(view: View, nativeView: NativeView): (() => void) | undefined {
|
|
16
|
+
const { View, ViewGroup } = android.view
|
|
17
|
+
if (
|
|
18
|
+
!view.isLoaded ||
|
|
19
|
+
view.nativeViewProtected !== nativeView ||
|
|
20
|
+
!(nativeView instanceof ViewGroup) ||
|
|
21
|
+
nativeView.getChildCount() === 0 ||
|
|
22
|
+
!nativeView.isHardwareAccelerated() ||
|
|
23
|
+
nativeView.getLayerType() !== View.LAYER_TYPE_NONE
|
|
24
|
+
)
|
|
25
|
+
return undefined
|
|
26
|
+
|
|
27
|
+
// Do not cache an entire oversized scrolling/content tree into a GPU texture.
|
|
28
|
+
const { widthPixels, heightPixels } = nativeView.getResources().getDisplayMetrics()
|
|
29
|
+
const width = nativeView.getWidth()
|
|
30
|
+
const height = nativeView.getHeight()
|
|
31
|
+
if (width <= 0 || height <= 0 || width > widthPixels || height > heightPixels)
|
|
32
|
+
return undefined
|
|
33
|
+
|
|
34
|
+
nativeView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
|
|
35
|
+
let active = true
|
|
36
|
+
const restore = () => {
|
|
37
|
+
if (!active) return
|
|
38
|
+
active = false
|
|
39
|
+
view.off('unloaded', restore)
|
|
40
|
+
if (nativeView.getLayerType() === View.LAYER_TYPE_HARDWARE) {
|
|
41
|
+
nativeView.setLayerType(View.LAYER_TYPE_NONE, null)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
view.on('unloaded', restore)
|
|
45
|
+
return restore
|
|
46
|
+
}
|
|
47
|
+
|
|
7
48
|
const accessors: Record<
|
|
8
|
-
|
|
49
|
+
NumericProperty,
|
|
9
50
|
{ get(view: NativeView): number; set(view: NativeView, value: number): void }
|
|
10
51
|
> = {
|
|
11
52
|
opacity: { get: (v) => v.getAlpha(), set: (v, value) => v.setAlpha(value) },
|
|
@@ -18,7 +59,7 @@ const accessors: Record<
|
|
|
18
59
|
rotateY: { get: (v) => v.getRotationY(), set: (v, value) => v.setRotationY(value) },
|
|
19
60
|
}
|
|
20
61
|
|
|
21
|
-
const unit = (property:
|
|
62
|
+
const unit = (property: NumericProperty) =>
|
|
22
63
|
property === 'x' || property === 'y' ? Utils.layout.getDisplayDensity() : 1
|
|
23
64
|
|
|
24
65
|
export function createAndroidDriver(): Driver {
|
|
@@ -32,16 +73,23 @@ export function createAndroidDriver(): Driver {
|
|
|
32
73
|
rotate: View.ROTATION,
|
|
33
74
|
rotateX: View.ROTATION_X,
|
|
34
75
|
rotateY: View.ROTATION_Y,
|
|
35
|
-
} satisfies Record<
|
|
76
|
+
} satisfies Record<NumericProperty, unknown>
|
|
36
77
|
const interpolator = new android.view.animation.LinearInterpolator()
|
|
37
78
|
|
|
38
79
|
return {
|
|
39
80
|
now: () => android.os.SystemClock.uptimeMillis(),
|
|
40
81
|
|
|
41
82
|
play(view, track, _startedAt, onEnd) {
|
|
83
|
+
if (isColorProperty(track.property)) return playAndroidColor(view, track, onEnd)
|
|
42
84
|
const nativeView = view.nativeViewProtected as NativeView
|
|
43
85
|
const scale = unit(track.property)
|
|
44
86
|
|
|
87
|
+
if (track.property === 'rotateX' || track.property === 'rotateY') {
|
|
88
|
+
nativeView.setCameraDistance(
|
|
89
|
+
(view.perspective || 300) * Utils.layout.getDisplayDensity(),
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
45
93
|
const keyframes = Array.create(android.animation.Keyframe, track.values.length)
|
|
46
94
|
track.values.forEach((value, i) => {
|
|
47
95
|
keyframes[i] = android.animation.Keyframe.ofFloat(track.times[i]!, value * scale)
|
|
@@ -56,6 +104,14 @@ export function createAndroidDriver(): Driver {
|
|
|
56
104
|
nativeView,
|
|
57
105
|
holders,
|
|
58
106
|
)
|
|
107
|
+
let cancelled = false
|
|
108
|
+
let ended = false
|
|
109
|
+
let started = false
|
|
110
|
+
let restoreLayer: (() => void) | undefined
|
|
111
|
+
const releaseLayer = () => {
|
|
112
|
+
restoreLayer?.()
|
|
113
|
+
restoreLayer = undefined
|
|
114
|
+
}
|
|
59
115
|
animator.setDuration(Math.max(track.duration, 1))
|
|
60
116
|
animator.setStartDelay(track.delay)
|
|
61
117
|
animator.setInterpolator(interpolator)
|
|
@@ -73,10 +129,25 @@ export function createAndroidDriver(): Driver {
|
|
|
73
129
|
}
|
|
74
130
|
animator.addListener(
|
|
75
131
|
new android.animation.Animator.AnimatorListener({
|
|
76
|
-
onAnimationStart() {
|
|
132
|
+
onAnimationStart() {
|
|
133
|
+
started = true
|
|
134
|
+
if (
|
|
135
|
+
!cancelled &&
|
|
136
|
+
!ended &&
|
|
137
|
+
track.property === 'opacity' &&
|
|
138
|
+
track.values.some((value) => value !== track.values[0])
|
|
139
|
+
)
|
|
140
|
+
restoreLayer = fadeLayer(view, nativeView)
|
|
141
|
+
},
|
|
77
142
|
onAnimationRepeat() {},
|
|
78
|
-
onAnimationCancel() {
|
|
143
|
+
onAnimationCancel() {
|
|
144
|
+
cancelled = true
|
|
145
|
+
releaseLayer()
|
|
146
|
+
},
|
|
79
147
|
onAnimationEnd() {
|
|
148
|
+
if (ended) return
|
|
149
|
+
ended = true
|
|
150
|
+
releaseLayer()
|
|
80
151
|
onEnd()
|
|
81
152
|
},
|
|
82
153
|
}),
|
|
@@ -84,18 +155,57 @@ export function createAndroidDriver(): Driver {
|
|
|
84
155
|
|
|
85
156
|
accessors[track.property].set(nativeView, track.values[0]! * scale)
|
|
86
157
|
animator.start()
|
|
87
|
-
return
|
|
158
|
+
return {
|
|
159
|
+
elapsed: () => (started ? track.delay + animator.getCurrentPlayTime() : 0),
|
|
160
|
+
cancel() {
|
|
161
|
+
// Cancel before native callbacks: Android can send onStart while cancelling a delay.
|
|
162
|
+
cancelled = true
|
|
163
|
+
releaseLayer()
|
|
164
|
+
animator.cancel()
|
|
165
|
+
},
|
|
166
|
+
} satisfies Handle
|
|
88
167
|
},
|
|
89
168
|
|
|
90
169
|
stop(_view, handle) {
|
|
91
|
-
;(handle as
|
|
170
|
+
;(handle as Handle).cancel()
|
|
92
171
|
},
|
|
93
172
|
|
|
173
|
+
elapsed: (handle) => (handle as Handle).elapsed(),
|
|
174
|
+
|
|
94
175
|
presented(view, property) {
|
|
176
|
+
if (isColorProperty(property)) return presentedAndroidColor(view, property)
|
|
95
177
|
const nativeView = view.nativeViewProtected as NativeView | undefined
|
|
96
178
|
return nativeView ? accessors[property].get(nativeView) / unit(property) : undefined
|
|
97
179
|
},
|
|
98
180
|
|
|
99
|
-
after,
|
|
181
|
+
after(ms, callback) {
|
|
182
|
+
// A native marker follows animator duration scale, including changes during playback.
|
|
183
|
+
// Only completion crosses the bridge; there is no JavaScript update listener.
|
|
184
|
+
const values = Array.create('float', 2)
|
|
185
|
+
values[0] = 0
|
|
186
|
+
values[1] = 1
|
|
187
|
+
const marker = android.animation.ValueAnimator.ofFloat(values)
|
|
188
|
+
marker.setDuration(ms)
|
|
189
|
+
let active = true
|
|
190
|
+
marker.addListener(
|
|
191
|
+
new android.animation.Animator.AnimatorListener({
|
|
192
|
+
onAnimationStart() {},
|
|
193
|
+
onAnimationRepeat() {},
|
|
194
|
+
onAnimationCancel() {
|
|
195
|
+
active = false
|
|
196
|
+
},
|
|
197
|
+
onAnimationEnd() {
|
|
198
|
+
if (!active) return
|
|
199
|
+
active = false
|
|
200
|
+
callback()
|
|
201
|
+
},
|
|
202
|
+
}),
|
|
203
|
+
)
|
|
204
|
+
marker.start()
|
|
205
|
+
return () => {
|
|
206
|
+
active = false
|
|
207
|
+
marker.cancel()
|
|
208
|
+
}
|
|
209
|
+
},
|
|
100
210
|
}
|
|
101
211
|
}
|
package/src/driver/index.ts
CHANGED
|
@@ -8,11 +8,18 @@ export interface Driver {
|
|
|
8
8
|
/** Milliseconds, on the clock the platform's animations run on. */
|
|
9
9
|
now(): number
|
|
10
10
|
/** Starts the track at `startedAt + track.delay` and calls `onEnd` once, however it ends. */
|
|
11
|
-
play(
|
|
11
|
+
play(
|
|
12
|
+
view: View,
|
|
13
|
+
track: Track,
|
|
14
|
+
startedAt: number,
|
|
15
|
+
onEnd: (finished?: boolean) => void,
|
|
16
|
+
): unknown
|
|
12
17
|
stop(view: View, handle: unknown): void
|
|
18
|
+
/** Track time, including its delay, when the native clock differs from `now()`. */
|
|
19
|
+
elapsed?(handle: unknown): number
|
|
13
20
|
/** The value on screen, when the platform knows it better than the track does. */
|
|
14
21
|
presented(view: View, property: Property): number | undefined
|
|
15
|
-
/** Calls back after `ms`
|
|
22
|
+
/** Calls back after `ms` of animation time, respecting platform speed settings. */
|
|
16
23
|
after(ms: number, callback: () => void): () => void
|
|
17
24
|
}
|
|
18
25
|
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import { Color, type View } from '@nativescript/core'
|
|
2
|
+
import { colorProperty } from '@nativescript/core/ui/styling/style-properties'
|
|
3
|
+
import { formattedTextProperty, textProperty } from '@nativescript/core/ui/text-base'
|
|
4
|
+
import { readColor, type ColorProperty } from '../color.js'
|
|
5
|
+
import type { Track } from '../track.js'
|
|
6
|
+
|
|
7
|
+
type TextView = View & {
|
|
8
|
+
text?: string
|
|
9
|
+
formattedText?: {
|
|
10
|
+
toString(): string
|
|
11
|
+
style: object
|
|
12
|
+
spans: { length: number; getItem(index: number): { text: string; style: object } }
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
type PaintLayer = {
|
|
16
|
+
layer: CALayer
|
|
17
|
+
path: 'backgroundColor' | 'borderColor' | 'fillColor'
|
|
18
|
+
}
|
|
19
|
+
type NativeView = UIView & { borderLayer?: CAShapeLayer }
|
|
20
|
+
interface ColorHandle {
|
|
21
|
+
stop(): void
|
|
22
|
+
presented(): number | undefined
|
|
23
|
+
}
|
|
24
|
+
const playing = new WeakMap<View, Map<string, ColorHandle>>()
|
|
25
|
+
const labelOf = (view: View) => {
|
|
26
|
+
const native = view.nativeViewProtected
|
|
27
|
+
return native instanceof UILabel
|
|
28
|
+
? native
|
|
29
|
+
: native instanceof UIButton
|
|
30
|
+
? native.titleLabel
|
|
31
|
+
: undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function presentedIOSColor(
|
|
35
|
+
view: View,
|
|
36
|
+
property: ColorProperty,
|
|
37
|
+
): number | undefined {
|
|
38
|
+
const current = playing.get(view)?.get(property)?.presented()
|
|
39
|
+
if (current !== undefined) return current
|
|
40
|
+
if (property === 'color' && !view.color) {
|
|
41
|
+
const label = labelOf(view)
|
|
42
|
+
if (label?.textColor) return Color.fromIosColor(label.textColor).argb >>> 0
|
|
43
|
+
}
|
|
44
|
+
return readColor(view, property)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Glyph entrances and color masks cannot own the same UILabel paint at once. */
|
|
48
|
+
export function stopIOSColorText(view: View) {
|
|
49
|
+
playing.get(view)?.get('color')?.stop()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Core Animation paints a solid layer through an exact UILabel alpha mask. */
|
|
53
|
+
export function playIOSColor(
|
|
54
|
+
view: View,
|
|
55
|
+
track: Track,
|
|
56
|
+
startedAt: number,
|
|
57
|
+
onEnd: (finished?: boolean) => void,
|
|
58
|
+
): ColorHandle {
|
|
59
|
+
const property = track.property as ColorProperty
|
|
60
|
+
let target: PaintLayer | undefined
|
|
61
|
+
let restore: (() => void) | undefined
|
|
62
|
+
let stopped = false
|
|
63
|
+
let queued = false
|
|
64
|
+
let attached = false
|
|
65
|
+
let generation = 0
|
|
66
|
+
let textGeometry: string | undefined
|
|
67
|
+
const native = view.nativeViewProtected as NativeView
|
|
68
|
+
const label = property === 'color' ? labelOf(view) : undefined
|
|
69
|
+
const key = `jolt:${property}`
|
|
70
|
+
const listeners: [
|
|
71
|
+
object & {
|
|
72
|
+
on(name: string, callback: () => void): void
|
|
73
|
+
off(name: string, callback: () => void): void
|
|
74
|
+
},
|
|
75
|
+
string,
|
|
76
|
+
() => void,
|
|
77
|
+
][] = []
|
|
78
|
+
const listen = (
|
|
79
|
+
object: typeof view | typeof view.style,
|
|
80
|
+
name: string,
|
|
81
|
+
callback: () => void,
|
|
82
|
+
) => {
|
|
83
|
+
object.on(name, callback)
|
|
84
|
+
listeners.push([object, name, callback])
|
|
85
|
+
}
|
|
86
|
+
const cleanup = () => {
|
|
87
|
+
if (stopped) return
|
|
88
|
+
stopped = true
|
|
89
|
+
generation++
|
|
90
|
+
for (const [object, name, callback] of listeners) object.off(name, callback)
|
|
91
|
+
target?.layer.removeAnimationForKey(key)
|
|
92
|
+
restore?.()
|
|
93
|
+
if (playing.get(view)?.get(property) === handle) playing.get(view)?.delete(property)
|
|
94
|
+
}
|
|
95
|
+
const cancel = () => {
|
|
96
|
+
if (stopped) return
|
|
97
|
+
cleanup()
|
|
98
|
+
onEnd(false)
|
|
99
|
+
}
|
|
100
|
+
const scheduleCancel = () => {
|
|
101
|
+
if (stopped || queued) return
|
|
102
|
+
queued = true
|
|
103
|
+
void Promise.resolve().then(() => {
|
|
104
|
+
queued = false
|
|
105
|
+
if (!stopped) cancel()
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
const handle: ColorHandle = {
|
|
109
|
+
stop: cancel,
|
|
110
|
+
presented() {
|
|
111
|
+
if (!target) return track.values[0]
|
|
112
|
+
const layer = target.layer.presentationLayer()
|
|
113
|
+
// Before CA commits its first frame, the model already contains the target.
|
|
114
|
+
if (!layer) return track.valueAt(CACurrentMediaTime() * 1000 - startedAt)
|
|
115
|
+
const color = (layer as unknown as Record<string, interop.Pointer | null>)[
|
|
116
|
+
target.path
|
|
117
|
+
]
|
|
118
|
+
return color
|
|
119
|
+
? Color.fromIosColor(UIColor.colorWithCGColor(color)).argb >>> 0
|
|
120
|
+
: undefined
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
let entries = playing.get(view)
|
|
124
|
+
if (!entries) playing.set(view, (entries = new Map()))
|
|
125
|
+
entries.set(property, handle)
|
|
126
|
+
listen(view, 'unloaded', cancel)
|
|
127
|
+
for (const name of property === 'borderColor'
|
|
128
|
+
? [
|
|
129
|
+
'borderTopColorChange',
|
|
130
|
+
'borderRightColorChange',
|
|
131
|
+
'borderBottomColorChange',
|
|
132
|
+
'borderLeftColorChange',
|
|
133
|
+
]
|
|
134
|
+
: [`${property}Change`])
|
|
135
|
+
listen(view.style, name, scheduleCancel)
|
|
136
|
+
if (property === 'backgroundColor')
|
|
137
|
+
listen(view.style, 'backgroundImageChange', scheduleCancel)
|
|
138
|
+
|
|
139
|
+
const start = () => {
|
|
140
|
+
if (stopped || attached) return
|
|
141
|
+
if (property === 'color') {
|
|
142
|
+
if (
|
|
143
|
+
!label ||
|
|
144
|
+
!label.window ||
|
|
145
|
+
label.bounds.size.width <= 0 ||
|
|
146
|
+
label.bounds.size.height <= 0
|
|
147
|
+
)
|
|
148
|
+
return
|
|
149
|
+
let paint: ReturnType<typeof textPaint>
|
|
150
|
+
try {
|
|
151
|
+
paint = textPaint(view as TextView, label)
|
|
152
|
+
} catch {
|
|
153
|
+
return cancel()
|
|
154
|
+
}
|
|
155
|
+
if (!paint) return cancel()
|
|
156
|
+
target = { layer: paint.layer, path: 'backgroundColor' }
|
|
157
|
+
restore = paint.restore
|
|
158
|
+
} else {
|
|
159
|
+
// Core rasterizes an image together with its background into a pattern.
|
|
160
|
+
// Replacing that CGColor would erase the image.
|
|
161
|
+
if (
|
|
162
|
+
property === 'backgroundColor' &&
|
|
163
|
+
typeof view.backgroundImage === 'string' &&
|
|
164
|
+
view.backgroundImage
|
|
165
|
+
)
|
|
166
|
+
return cancel()
|
|
167
|
+
target =
|
|
168
|
+
property === 'borderColor' && native.borderLayer
|
|
169
|
+
? { layer: native.borderLayer, path: 'fillColor' }
|
|
170
|
+
: { layer: native.layer, path: property }
|
|
171
|
+
}
|
|
172
|
+
attached = true
|
|
173
|
+
if (label) textGeometry = geometry(label)
|
|
174
|
+
attach()
|
|
175
|
+
}
|
|
176
|
+
const attach = () => {
|
|
177
|
+
if (!target || stopped) return
|
|
178
|
+
const instance = ++generation
|
|
179
|
+
const { layer, path } = target
|
|
180
|
+
CATransaction.begin()
|
|
181
|
+
CATransaction.setDisableActions(true)
|
|
182
|
+
;(layer as unknown as Record<string, interop.Pointer>)[path] = new Color(
|
|
183
|
+
track.to,
|
|
184
|
+
).ios.CGColor
|
|
185
|
+
CATransaction.commit()
|
|
186
|
+
const animation = CAKeyframeAnimation.animationWithKeyPath(path)
|
|
187
|
+
animation.values = NSArray.arrayWithArray(
|
|
188
|
+
track.values.map((color) => new Color(color).ios.CGColor),
|
|
189
|
+
)
|
|
190
|
+
animation.keyTimes = NSArray.arrayWithArray(track.times as number[])
|
|
191
|
+
animation.duration = Math.max(track.duration, 1) / 1000
|
|
192
|
+
animation.beginTime = layer.convertTimeFromLayer(
|
|
193
|
+
(startedAt + track.delay) / 1000,
|
|
194
|
+
null,
|
|
195
|
+
)
|
|
196
|
+
animation.fillMode = kCAFillModeBackwards
|
|
197
|
+
animation.removedOnCompletion = track.repeat !== Infinity
|
|
198
|
+
if (animation.respondsToSelector('setPreferredFrameRateRange:')) {
|
|
199
|
+
const maximum = Math.min(
|
|
200
|
+
120,
|
|
201
|
+
native.window?.screen.maximumFramesPerSecond ??
|
|
202
|
+
UIScreen.mainScreen.maximumFramesPerSecond,
|
|
203
|
+
)
|
|
204
|
+
animation.preferredFrameRateRange = CAFrameRateRangeMake(
|
|
205
|
+
Math.min(80, maximum),
|
|
206
|
+
maximum,
|
|
207
|
+
maximum,
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
if (track.repeat > 0) {
|
|
211
|
+
animation.autoreverses = track.reverse
|
|
212
|
+
animation.repeatCount = track.reverse ? (track.repeat + 1) / 2 : track.repeat + 1
|
|
213
|
+
}
|
|
214
|
+
CATransaction.begin()
|
|
215
|
+
if (track.repeat !== Infinity)
|
|
216
|
+
CATransaction.setCompletionBlock(() => {
|
|
217
|
+
if (!stopped && instance === generation) {
|
|
218
|
+
cleanup()
|
|
219
|
+
onEnd()
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
layer.addAnimationForKey(animation, key)
|
|
223
|
+
CATransaction.commit()
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (property === 'color') {
|
|
227
|
+
for (const name of ['textChange', 'formattedTextChange', 'isEnabledChange'])
|
|
228
|
+
listen(view, name, scheduleCancel)
|
|
229
|
+
for (const name of [
|
|
230
|
+
'fontInternalChange',
|
|
231
|
+
'textTransformChange',
|
|
232
|
+
'whiteSpaceChange',
|
|
233
|
+
'textOverflowChange',
|
|
234
|
+
'maxLinesChange',
|
|
235
|
+
'textStrokeChange',
|
|
236
|
+
'directionChange',
|
|
237
|
+
'visibilityChange',
|
|
238
|
+
'paddingLeftChange',
|
|
239
|
+
'paddingRightChange',
|
|
240
|
+
'paddingTopChange',
|
|
241
|
+
'paddingBottomChange',
|
|
242
|
+
'textAlignmentChange',
|
|
243
|
+
'letterSpacingChange',
|
|
244
|
+
'textDecorationChange',
|
|
245
|
+
'textShadowChange',
|
|
246
|
+
'lineHeightChange',
|
|
247
|
+
])
|
|
248
|
+
listen(view.style, name, scheduleCancel)
|
|
249
|
+
listen(view, 'layoutChanged', () => {
|
|
250
|
+
if (attached) {
|
|
251
|
+
// Core can relayout after rebuilding an identical attributed string.
|
|
252
|
+
// Only changed glyph geometry invalidates the captured mask.
|
|
253
|
+
if (label && geometry(label) !== textGeometry) scheduleCancel()
|
|
254
|
+
} else start()
|
|
255
|
+
})
|
|
256
|
+
const formatted = (view as TextView).formattedText
|
|
257
|
+
if (formatted) {
|
|
258
|
+
const object = formatted as unknown as typeof view
|
|
259
|
+
listen(object, 'propertyChange', scheduleCancel)
|
|
260
|
+
}
|
|
261
|
+
} else if (property === 'borderColor') {
|
|
262
|
+
// Core may rebuild its border layer after a background or geometry change.
|
|
263
|
+
// Reattach on the same native timeline, without cancelling sibling colors.
|
|
264
|
+
let pending = false
|
|
265
|
+
const followBorder = () => {
|
|
266
|
+
if (stopped || pending) return
|
|
267
|
+
pending = true
|
|
268
|
+
void Promise.resolve().then(() => {
|
|
269
|
+
pending = false
|
|
270
|
+
if (stopped || !attached || !target) return
|
|
271
|
+
const next: PaintLayer = native.borderLayer
|
|
272
|
+
? { layer: native.borderLayer, path: 'fillColor' }
|
|
273
|
+
: { layer: native.layer, path: 'borderColor' }
|
|
274
|
+
if (next.layer === target.layer && next.path === target.path) return
|
|
275
|
+
const previous = target
|
|
276
|
+
target = next
|
|
277
|
+
generation++
|
|
278
|
+
previous.layer.removeAnimationForKey(key)
|
|
279
|
+
attach()
|
|
280
|
+
})
|
|
281
|
+
}
|
|
282
|
+
listen(view, 'layoutChanged', followBorder)
|
|
283
|
+
listen(view.style, 'backgroundInternalChange', followBorder)
|
|
284
|
+
}
|
|
285
|
+
start()
|
|
286
|
+
return handle
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function textPaint(
|
|
290
|
+
view: TextView,
|
|
291
|
+
native: UILabel,
|
|
292
|
+
): { layer: CALayer; restore(): void } | undefined {
|
|
293
|
+
const owner = view.nativeViewProtected
|
|
294
|
+
const original = native.attributedText
|
|
295
|
+
const text = original?.string ?? native.text ?? ''
|
|
296
|
+
const bounds = native.bounds
|
|
297
|
+
const scale = native.window?.screen.scale ?? UIScreen.mainScreen.scale
|
|
298
|
+
if (
|
|
299
|
+
!text ||
|
|
300
|
+
Array.from(text).some((char) => {
|
|
301
|
+
const cp = char.codePointAt(0)!
|
|
302
|
+
return (
|
|
303
|
+
(cp >= 0x1f000 && cp <= 0x1faff) ||
|
|
304
|
+
(cp >= 0x2600 && cp <= 0x27bf) ||
|
|
305
|
+
cp === 0x200d ||
|
|
306
|
+
cp === 0x20e3 ||
|
|
307
|
+
cp === 0xfe0f
|
|
308
|
+
)
|
|
309
|
+
}) ||
|
|
310
|
+
(native.shadowColor && CGColorGetAlpha(native.shadowColor.CGColor) > 0) ||
|
|
311
|
+
bounds.size.width * bounds.size.height * scale * scale > 4_000_000
|
|
312
|
+
)
|
|
313
|
+
return undefined
|
|
314
|
+
const source = original
|
|
315
|
+
? NSMutableAttributedString.alloc().initWithAttributedString(original)
|
|
316
|
+
: NSMutableAttributedString.alloc().initWithString(text)
|
|
317
|
+
const full = { location: 0, length: source.length }
|
|
318
|
+
let unsupported = false
|
|
319
|
+
for (const attribute of [NSAttachmentAttributeName, NSShadowAttributeName])
|
|
320
|
+
source.enumerateAttributeInRangeOptionsUsingBlock(
|
|
321
|
+
attribute,
|
|
322
|
+
full,
|
|
323
|
+
0 as NSAttributedStringEnumerationOptions,
|
|
324
|
+
(value) => {
|
|
325
|
+
if (attribute === NSShadowAttributeName) {
|
|
326
|
+
const color = (value as NSShadow | null)?.shadowColor as UIColor | null
|
|
327
|
+
if (color && CGColorGetAlpha(color.CGColor) > 0) unsupported = true
|
|
328
|
+
} else if (value != null) unsupported = true
|
|
329
|
+
},
|
|
330
|
+
)
|
|
331
|
+
if (unsupported) return undefined
|
|
332
|
+
const ranges: NSRange[] = []
|
|
333
|
+
const ownColor = (style: object) =>
|
|
334
|
+
((style as Record<symbol, number>)[colorProperty.sourceKey] ?? 0) >= 2
|
|
335
|
+
const formatted = view.formattedText
|
|
336
|
+
if (formatted) {
|
|
337
|
+
let offset = 0
|
|
338
|
+
for (let i = 0; i < formatted.spans.length; i++) {
|
|
339
|
+
const span = formatted.spans.getItem(i)
|
|
340
|
+
if (!ownColor(formatted.style) && !ownColor(span.style))
|
|
341
|
+
ranges.push({ location: offset, length: span.text.length })
|
|
342
|
+
offset += span.text.length
|
|
343
|
+
}
|
|
344
|
+
} else ranges.push(full)
|
|
345
|
+
const maskText = NSMutableAttributedString.alloc().initWithAttributedString(source)
|
|
346
|
+
const hidden = NSMutableAttributedString.alloc().initWithAttributedString(source)
|
|
347
|
+
maskText.addAttributeValueRange(NSFontAttributeName, native.font, full)
|
|
348
|
+
// Keep per-span fonts, weights and paragraph metrics exactly as UILabel uses them.
|
|
349
|
+
if (original)
|
|
350
|
+
original.enumerateAttributeInRangeOptionsUsingBlock(
|
|
351
|
+
NSFontAttributeName,
|
|
352
|
+
full,
|
|
353
|
+
0 as NSAttributedStringEnumerationOptions,
|
|
354
|
+
(font, range) => {
|
|
355
|
+
if (font) maskText.addAttributeValueRange(NSFontAttributeName, font, range)
|
|
356
|
+
},
|
|
357
|
+
)
|
|
358
|
+
for (const name of [
|
|
359
|
+
NSForegroundColorAttributeName,
|
|
360
|
+
NSBackgroundColorAttributeName,
|
|
361
|
+
NSUnderlineColorAttributeName,
|
|
362
|
+
NSStrikethroughColorAttributeName,
|
|
363
|
+
NSStrokeColorAttributeName,
|
|
364
|
+
])
|
|
365
|
+
maskText.addAttributeValueRange(name, UIColor.clearColor, full)
|
|
366
|
+
for (const range of ranges)
|
|
367
|
+
for (const name of [
|
|
368
|
+
NSForegroundColorAttributeName,
|
|
369
|
+
NSUnderlineColorAttributeName,
|
|
370
|
+
NSStrikethroughColorAttributeName,
|
|
371
|
+
NSStrokeColorAttributeName,
|
|
372
|
+
]) {
|
|
373
|
+
maskText.addAttributeValueRange(name, UIColor.whiteColor, range)
|
|
374
|
+
hidden.addAttributeValueRange(name, UIColor.clearColor, range)
|
|
375
|
+
}
|
|
376
|
+
let image: UIImage | null
|
|
377
|
+
try {
|
|
378
|
+
native.attributedText = maskText
|
|
379
|
+
UIGraphicsBeginImageContextWithOptions(bounds.size, false, scale)
|
|
380
|
+
try {
|
|
381
|
+
CGContextTranslateCTM(
|
|
382
|
+
UIGraphicsGetCurrentContext()!,
|
|
383
|
+
-bounds.origin.x,
|
|
384
|
+
-bounds.origin.y,
|
|
385
|
+
)
|
|
386
|
+
native.drawTextInRect(bounds)
|
|
387
|
+
image = UIGraphicsGetImageFromCurrentImageContext()
|
|
388
|
+
} finally {
|
|
389
|
+
UIGraphicsEndImageContext()
|
|
390
|
+
}
|
|
391
|
+
} finally {
|
|
392
|
+
if (original) native.attributedText = original
|
|
393
|
+
else native.text = text
|
|
394
|
+
}
|
|
395
|
+
if (!image?.CGImage) return undefined
|
|
396
|
+
const mask = CALayer.new()
|
|
397
|
+
mask.frame = CGRectMake(0, 0, bounds.size.width, bounds.size.height)
|
|
398
|
+
mask.contents = image.CGImage
|
|
399
|
+
mask.contentsScale = scale
|
|
400
|
+
const layer = CALayer.new()
|
|
401
|
+
layer.frame = bounds
|
|
402
|
+
layer.name = 'jolt:color'
|
|
403
|
+
layer.mask = mask
|
|
404
|
+
native.attributedText = hidden
|
|
405
|
+
native.layer.addSublayer(layer)
|
|
406
|
+
return {
|
|
407
|
+
layer,
|
|
408
|
+
restore() {
|
|
409
|
+
layer.removeFromSuperlayer()
|
|
410
|
+
if (view.nativeViewProtected !== owner) return
|
|
411
|
+
const property = view.formattedText ? formattedTextProperty : textProperty
|
|
412
|
+
;(view as unknown as Record<symbol, (value: unknown) => void>)[property.setNative]!(
|
|
413
|
+
view.formattedText ?? view.text,
|
|
414
|
+
)
|
|
415
|
+
},
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function geometry(label: UILabel): string {
|
|
420
|
+
const bounds = label.bounds
|
|
421
|
+
const text = label.textRectForBoundsLimitedToNumberOfLines(bounds, label.numberOfLines)
|
|
422
|
+
return [
|
|
423
|
+
bounds.origin.x,
|
|
424
|
+
bounds.origin.y,
|
|
425
|
+
bounds.size.width,
|
|
426
|
+
bounds.size.height,
|
|
427
|
+
text.origin.x,
|
|
428
|
+
text.origin.y,
|
|
429
|
+
text.size.width,
|
|
430
|
+
text.size.height,
|
|
431
|
+
].join(',')
|
|
432
|
+
}
|
package/src/driver/ios.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import type { View } from '@nativescript/core'
|
|
2
|
-
import type {
|
|
2
|
+
import type { NumericProperty } from '../properties.js'
|
|
3
|
+
import { isColorProperty } from '../color.js'
|
|
4
|
+
import { playIOSColor, presentedIOSColor } from './ios-color.js'
|
|
3
5
|
import { after, type Driver } from './index.js'
|
|
4
6
|
|
|
5
7
|
interface Handle {
|
|
6
8
|
layers: CALayer[]
|
|
7
9
|
key: string
|
|
8
10
|
onStop?: () => void
|
|
11
|
+
color?: ReturnType<typeof playIOSColor>
|
|
9
12
|
}
|
|
10
13
|
|
|
11
|
-
const keyPaths: Record<
|
|
14
|
+
const keyPaths: Record<NumericProperty, string> = {
|
|
12
15
|
opacity: 'opacity',
|
|
13
16
|
x: 'transform.translation.x',
|
|
14
17
|
y: 'transform.translation.y',
|
|
@@ -49,6 +52,8 @@ export function createIOSDriver(): Driver {
|
|
|
49
52
|
now: () => CACurrentMediaTime() * 1000,
|
|
50
53
|
|
|
51
54
|
play(view, track, startedAt, onEnd): Handle {
|
|
55
|
+
if (isColorProperty(track.property))
|
|
56
|
+
return { layers: [], key: '', color: playIOSColor(view, track, startedAt, onEnd) }
|
|
52
57
|
const layers = layersOf(view)
|
|
53
58
|
const key = `jolt:${track.property}`
|
|
54
59
|
const rotation = track.property.startsWith('rotate')
|
|
@@ -96,12 +101,17 @@ export function createIOSDriver(): Driver {
|
|
|
96
101
|
stop(_view, handle) {
|
|
97
102
|
const playing = handle as Handle
|
|
98
103
|
const { layers, key, onStop } = playing
|
|
104
|
+
if (playing.color) {
|
|
105
|
+
playing.color.stop()
|
|
106
|
+
return
|
|
107
|
+
}
|
|
99
108
|
playing.onStop = undefined
|
|
100
109
|
for (const layer of layers) layer.removeAnimationForKey(key)
|
|
101
110
|
onStop?.()
|
|
102
111
|
},
|
|
103
112
|
|
|
104
|
-
presented: () =>
|
|
113
|
+
presented: (view, property) =>
|
|
114
|
+
isColorProperty(property) ? presentedIOSColor(view, property) : undefined,
|
|
105
115
|
|
|
106
116
|
after,
|
|
107
117
|
}
|