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