@rafaelaugusto/jolt 0.1.0-beta.3 → 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 +55 -13
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +24 -2
- 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/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.map +1 -1
- package/dist/driver/android.js +6 -0
- package/dist/driver/android.js.map +1 -1
- package/dist/driver/index.d.ts +1 -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 +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.map +1 -1
- package/dist/engine.js +9 -5
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +3 -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/press.d.ts.map +1 -1
- package/dist/press.js +6 -1
- package/dist/press.js.map +1 -1
- package/dist/properties.d.ts +4 -2
- package/dist/properties.d.ts.map +1 -1
- package/dist/properties.js +12 -1
- package/dist/properties.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 +38 -2
- package/dist/track.js.map +1 -1
- 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 +5 -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 +30 -4
- package/src/color.ts +101 -0
- package/src/driver/android-color.ts +168 -0
- package/src/driver/android.ts +8 -4
- package/src/driver/index.ts +6 -1
- package/src/driver/ios-color.ts +432 -0
- package/src/driver/ios.ts +13 -3
- package/src/engine.ts +9 -5
- package/src/index.ts +3 -0
- package/src/liquid-grid.ts +28 -0
- package/src/liquid.ts +282 -0
- package/src/press.ts +8 -2
- package/src/properties.ts +15 -5
- package/src/text.ts +2 -0
- package/src/track.ts +63 -2
- package/src/transition.ts +10 -2
- package/src/types.ts +5 -0
|
@@ -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
|
}
|
package/src/engine.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { playHaptics, type HapticOptions } from './haptics.js'
|
|
|
4
4
|
import { readValue, writeValue, type Property } from './properties.js'
|
|
5
5
|
import { createTrack, type Track } from './track.js'
|
|
6
6
|
import type { ResolvedTransition } from './transition.js'
|
|
7
|
+
import { isColorProperty } from './color.js'
|
|
7
8
|
|
|
8
9
|
export interface Animation extends PromiseLike<boolean> {
|
|
9
10
|
/**
|
|
@@ -112,13 +113,16 @@ export class ViewAnimation implements Animation {
|
|
|
112
113
|
velocity = previous.track.velocityAt(elapsed)
|
|
113
114
|
release(previous, now, false)
|
|
114
115
|
} else {
|
|
115
|
-
current =
|
|
116
|
+
current =
|
|
117
|
+
(isColorProperty(property)
|
|
118
|
+
? driver.presented(this.view, property)
|
|
119
|
+
: undefined) ?? readValue(this.view, property)
|
|
116
120
|
}
|
|
117
121
|
return createTrack(
|
|
118
122
|
property,
|
|
119
123
|
keyframes.map((value) => {
|
|
120
124
|
if (value === null) return current
|
|
121
|
-
if (!relative) return value
|
|
125
|
+
if (!relative || isColorProperty(property)) return value
|
|
122
126
|
return property === 'opacity' || property.startsWith('scale')
|
|
123
127
|
? current * value
|
|
124
128
|
: current + value
|
|
@@ -147,11 +151,11 @@ export class ViewAnimation implements Animation {
|
|
|
147
151
|
this.haptics = playHaptics(this.view, tracks, this.haptic, driver, now)
|
|
148
152
|
|
|
149
153
|
for (const entry of this.entries) {
|
|
150
|
-
entry.handle = driver.play(this.view, entry.track, now, () => {
|
|
154
|
+
entry.handle = driver.play(this.view, entry.track, now, (finished = true) => {
|
|
151
155
|
if (entry.done) return
|
|
152
156
|
entry.done = true
|
|
153
157
|
forget(entry)
|
|
154
|
-
this.ended(
|
|
158
|
+
this.ended(finished)
|
|
155
159
|
})
|
|
156
160
|
}
|
|
157
161
|
|
|
@@ -164,7 +168,7 @@ export class ViewAnimation implements Animation {
|
|
|
164
168
|
|
|
165
169
|
private waitForLoad(plans: readonly Plan[]) {
|
|
166
170
|
for (const { property, keyframes, relative } of plans) {
|
|
167
|
-
if (relative) continue
|
|
171
|
+
if (relative && !isColorProperty(property)) continue
|
|
168
172
|
const first = keyframes[0]
|
|
169
173
|
if (first !== null && first !== undefined) writeValue(this.view, property, first)
|
|
170
174
|
}
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,8 @@ export { confetti, particles } from './particles.js'
|
|
|
7
7
|
export type { ConfettiOptions, ParticleOptions } from './particles.js'
|
|
8
8
|
export { shimmer } from './shimmer.js'
|
|
9
9
|
export type { ShimmerOptions } from './shimmer.js'
|
|
10
|
+
export { liquid } from './liquid.js'
|
|
11
|
+
export type { LiquidOptions, LiquidEffect } from './liquid.js'
|
|
10
12
|
export { loader } from './loader.js'
|
|
11
13
|
export type { LoaderOptions, LoaderStyle } from './loader.js'
|
|
12
14
|
export { reveal, conceal } from './reveal.js'
|
|
@@ -26,4 +28,5 @@ export type { Spring, SpringName, Timing, Transition, Tween } from './transition
|
|
|
26
28
|
export { cubicBezier } from './easing.js'
|
|
27
29
|
export type { CubicBezier, Easing, EasingFunction, EasingName } from './easing.js'
|
|
28
30
|
export type { Animation } from './engine.js'
|
|
31
|
+
export type { ColorValue, ColorKeyframes } from './color.js'
|
|
29
32
|
export type * from './types.js'
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** A periodic displacement field. The fixed boundary keeps transparent edges in bounds. */
|
|
2
|
+
export function liquidGrid(
|
|
3
|
+
columns: number,
|
|
4
|
+
rows: number,
|
|
5
|
+
phase: number,
|
|
6
|
+
intensity: number,
|
|
7
|
+
) {
|
|
8
|
+
const positions = new Float32Array((columns + 1) * (rows + 1) * 2)
|
|
9
|
+
const t = phase * Math.PI * 2
|
|
10
|
+
let index = 0
|
|
11
|
+
for (let row = 0; row <= rows; row++) {
|
|
12
|
+
const y = row / rows
|
|
13
|
+
for (let column = 0; column <= columns; column++) {
|
|
14
|
+
const x = column / columns
|
|
15
|
+
const edge = Math.sin(Math.PI * x) ** 2 * Math.sin(Math.PI * y) ** 2
|
|
16
|
+
const amount = 0.055 * intensity * edge
|
|
17
|
+
positions[index++] =
|
|
18
|
+
x +
|
|
19
|
+
amount *
|
|
20
|
+
(Math.sin(Math.PI * 2 * y + t) + 0.4 * Math.sin(Math.PI * 2 * (x + y) - 2 * t))
|
|
21
|
+
positions[index++] =
|
|
22
|
+
y +
|
|
23
|
+
amount *
|
|
24
|
+
(Math.cos(Math.PI * 2 * x - t) + 0.4 * Math.cos(Math.PI * 2 * (x - y) + 2 * t))
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return positions
|
|
28
|
+
}
|