@rafaelaugusto/jolt 0.1.0-rc.13 → 0.1.0-rc.14
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 +2 -2
- package/dist/core/drop.d.ts +34 -1
- package/dist/core/drop.d.ts.map +1 -1
- package/dist/core/drop.js +93 -8
- package/dist/core/drop.js.map +1 -1
- package/dist/core/loading.d.ts +125 -0
- package/dist/core/loading.d.ts.map +1 -0
- package/dist/core/loading.js +119 -0
- package/dist/core/loading.js.map +1 -0
- package/dist/core/preset-recipe.d.ts +2 -2
- package/dist/core/preset-recipe.d.ts.map +1 -1
- package/dist/core/preset-recipe.js +3 -1
- package/dist/core/preset-recipe.js.map +1 -1
- package/dist/core/text-pieces.d.ts +18 -0
- package/dist/core/text-pieces.d.ts.map +1 -0
- package/dist/core/text-pieces.js +43 -0
- package/dist/core/text-pieces.js.map +1 -0
- package/dist/drag.d.ts +7 -1
- package/dist/drag.d.ts.map +1 -1
- package/dist/drag.js +72 -37
- package/dist/drag.js.map +1 -1
- package/dist/drop-zones.d.ts +1 -1
- package/dist/drop-zones.d.ts.map +1 -1
- package/dist/drop-zones.js +2 -0
- package/dist/drop-zones.js.map +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +7 -2
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts +2 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +17 -15
- package/dist/loader.js.map +1 -1
- package/dist/reveal.d.ts.map +1 -1
- package/dist/reveal.js +4 -3
- package/dist/reveal.js.map +1 -1
- package/dist/shimmer.d.ts.map +1 -1
- package/dist/shimmer.js +6 -4
- package/dist/shimmer.js.map +1 -1
- package/dist/web/drag.d.ts +8 -0
- package/dist/web/drag.d.ts.map +1 -1
- package/dist/web/drag.js +68 -14
- package/dist/web/drag.js.map +1 -1
- package/dist/web/drop-zones.d.ts +2 -2
- package/dist/web/drop-zones.d.ts.map +1 -1
- package/dist/web/drop-zones.js +21 -4
- package/dist/web/drop-zones.js.map +1 -1
- package/dist/web/loading.d.ts +2 -1
- package/dist/web/loading.d.ts.map +1 -1
- package/dist/web/loading.js +19 -15
- package/dist/web/loading.js.map +1 -1
- package/dist/web/pointer.d.ts +2 -0
- package/dist/web/pointer.d.ts.map +1 -1
- package/dist/web/pointer.js +13 -0
- package/dist/web/pointer.js.map +1 -1
- package/dist/web/public.d.ts +1 -1
- package/dist/web/public.d.ts.map +1 -1
- package/dist/web/public.js.map +1 -1
- package/dist/web/reveal.d.ts.map +1 -1
- package/dist/web/reveal.js +4 -3
- package/dist/web/reveal.js.map +1 -1
- package/dist/web/text-effect.d.ts.map +1 -1
- package/dist/web/text-effect.js +10 -16
- package/dist/web/text-effect.js.map +1 -1
- package/package.json +12 -3
- package/platforms/android/java/org/rafaelaugusto/jolt/PointerInput.java +26 -4
- package/src/core/drop.ts +123 -5
- package/src/core/loading.ts +172 -0
- package/src/core/preset-recipe.ts +6 -2
- package/src/core/text-pieces.ts +52 -0
- package/src/drag.ts +81 -38
- package/src/drop-zones.ts +13 -3
- package/src/engine.ts +8 -3
- package/src/index.ts +3 -0
- package/src/loader.ts +23 -20
- package/src/reveal.ts +4 -3
- package/src/shimmer.ts +6 -4
- package/src/web/drag.ts +70 -12
- package/src/web/drop-zones.ts +31 -6
- package/src/web/loading.ts +38 -17
- package/src/web/pointer.ts +14 -0
- package/src/web/public.ts +3 -0
- package/src/web/reveal.ts +7 -8
- package/src/web/text-effect.ts +20 -17
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { graphemes, type Grapheme } from './roll-diff.js'
|
|
2
|
+
|
|
3
|
+
export type TextGrouping = 'letter' | 'word'
|
|
4
|
+
|
|
5
|
+
/** A grapheme or word segment, with its position in text order. */
|
|
6
|
+
export interface TextPiece extends Grapheme {
|
|
7
|
+
group: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Text effects split at most this many graphemes. */
|
|
11
|
+
export const MAX_TEXT_GRAPHEMES = 256
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Letters are extended grapheme clusters. Words are the Unicode word segments the
|
|
15
|
+
* platform reports, with spaces and punctuation as pieces of their own.
|
|
16
|
+
*/
|
|
17
|
+
export function textPieces(text: string, by: TextGrouping): TextPiece[] {
|
|
18
|
+
if (by === 'letter') return graphemes(text).map((part, group) => ({ ...part, group }))
|
|
19
|
+
const pieces: TextPiece[] = []
|
|
20
|
+
if (typeof Intl === 'undefined' || !('Segmenter' in Intl)) {
|
|
21
|
+
let start = 0
|
|
22
|
+
for (const [segment] of text.matchAll(/\s+|\S+/gu)) {
|
|
23
|
+
pieces.push({
|
|
24
|
+
text: segment,
|
|
25
|
+
start,
|
|
26
|
+
end: start + segment.length,
|
|
27
|
+
group: pieces.length,
|
|
28
|
+
})
|
|
29
|
+
start += segment.length
|
|
30
|
+
}
|
|
31
|
+
return pieces
|
|
32
|
+
}
|
|
33
|
+
const segmenter = new Intl.Segmenter(undefined, { granularity: 'word' })
|
|
34
|
+
for (const { segment, index } of segmenter.segment(text))
|
|
35
|
+
pieces.push({
|
|
36
|
+
text: segment,
|
|
37
|
+
start: index,
|
|
38
|
+
end: index + segment.length,
|
|
39
|
+
group: pieces.length,
|
|
40
|
+
})
|
|
41
|
+
return pieces
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Whitespace keeps its place in the order but draws nothing. */
|
|
45
|
+
export function isVisiblePiece(piece: Pick<TextPiece, 'text'>): boolean {
|
|
46
|
+
return piece.text.trim() !== ''
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Milliseconds from the start of the effect until the piece at `index` begins. */
|
|
50
|
+
export function staggeredStart(index: number, stagger: number, delay: number): number {
|
|
51
|
+
return delay + index * stagger
|
|
52
|
+
}
|
package/src/drag.ts
CHANGED
|
@@ -54,6 +54,8 @@ export interface DragOptions {
|
|
|
54
54
|
rubberband?: boolean
|
|
55
55
|
/** Decay retention per millisecond. Defaults to 0.998. */
|
|
56
56
|
friction?: number
|
|
57
|
+
/** A descendant view that alone receives the pan; the dragged view still moves. */
|
|
58
|
+
handle?: Target
|
|
57
59
|
onStart?: (event: DragEvent) => void
|
|
58
60
|
onMove?: (event: DragEvent) => void
|
|
59
61
|
onRelease?: (event: DragRelease) => void
|
|
@@ -92,6 +94,7 @@ interface Settings {
|
|
|
92
94
|
axes: Axis[]
|
|
93
95
|
curves: Record<Axis, DragCurve>
|
|
94
96
|
value: Root | undefined
|
|
97
|
+
handle: View | undefined
|
|
95
98
|
callbacks: Pick<DragOptions, 'onStart' | 'onMove' | 'onCancel' | 'onSettle'> & {
|
|
96
99
|
onRelease?: (event: DragRelease<Animation>) => void
|
|
97
100
|
}
|
|
@@ -185,7 +188,13 @@ function settings(options: DragOptions | DragValueOptions): Settings {
|
|
|
185
188
|
for (const callback of Object.values(callbacks))
|
|
186
189
|
if (callback !== undefined && typeof callback !== 'function')
|
|
187
190
|
throw new TypeError('Drag callbacks must be functions.')
|
|
188
|
-
|
|
191
|
+
const handle = options.handle === undefined ? undefined : toView(options.handle)
|
|
192
|
+
return { axes, curves, value: value && asRoot(value), handle, callbacks }
|
|
193
|
+
}
|
|
194
|
+
function inside(view: View, handle: View) {
|
|
195
|
+
for (let node = handle.parent as View | undefined; node; node = node.parent as View)
|
|
196
|
+
if (node === view) return true
|
|
197
|
+
return false
|
|
189
198
|
}
|
|
190
199
|
|
|
191
200
|
/**
|
|
@@ -198,12 +207,16 @@ export function drag(target: Target, options: DragOptions | DragValueOptions = {
|
|
|
198
207
|
return createDrag(target, options)
|
|
199
208
|
}
|
|
200
209
|
|
|
210
|
+
export interface AnchoredDrag extends Drag {
|
|
211
|
+
/** Offset an active contact's translation in DIPs without moving it under the finger. */
|
|
212
|
+
shift(x: number, y: number): void
|
|
213
|
+
}
|
|
201
214
|
/** Internal adapter hook: inspect the final input before native release motion changes model values. */
|
|
202
215
|
export function dragBeforeRelease(
|
|
203
216
|
target: Target,
|
|
204
217
|
options: DragOptions,
|
|
205
218
|
beforeRelease: () => void,
|
|
206
|
-
):
|
|
219
|
+
): AnchoredDrag {
|
|
207
220
|
return createDrag(target, options, beforeRelease)
|
|
208
221
|
}
|
|
209
222
|
|
|
@@ -211,9 +224,11 @@ function createDrag(
|
|
|
211
224
|
target: Target,
|
|
212
225
|
options: DragOptions | DragValueOptions,
|
|
213
226
|
beforeRelease?: () => void,
|
|
214
|
-
):
|
|
227
|
+
): AnchoredDrag {
|
|
215
228
|
const view = toView(target)
|
|
216
229
|
const config = settings(options) // Invalid replacements leave the existing owner intact.
|
|
230
|
+
if (config.handle && !inside(view, config.handle))
|
|
231
|
+
throw new TypeError('A drag handle must be a descendant of the dragged view.')
|
|
217
232
|
const previous = owners.get(view)
|
|
218
233
|
const controller = new DragController(view, config, beforeRelease)
|
|
219
234
|
owners.set(view, controller)
|
|
@@ -227,7 +242,7 @@ function createDrag(
|
|
|
227
242
|
return controller
|
|
228
243
|
}
|
|
229
244
|
|
|
230
|
-
class DragController implements
|
|
245
|
+
class DragController implements AnchoredDrag {
|
|
231
246
|
private active = false
|
|
232
247
|
private disabled = false
|
|
233
248
|
private destroyed = false
|
|
@@ -238,15 +253,20 @@ class DragController implements Drag {
|
|
|
238
253
|
private release: Animation | undefined
|
|
239
254
|
private input = new DragInput()
|
|
240
255
|
private origin = { x: 0, y: 0 }
|
|
256
|
+
private delta = { x: 0, y: 0, velocityX: 0, velocityY: 0 }
|
|
241
257
|
private last: DragEvent = { x: 0, y: 0, velocityX: 0, velocityY: 0 }
|
|
242
258
|
private lengths = { x: 200, y: 200 }
|
|
259
|
+
private surface: View
|
|
243
260
|
constructor(
|
|
244
261
|
private view: View,
|
|
245
262
|
private config: Settings,
|
|
246
263
|
private beforeRelease?: () => void,
|
|
247
264
|
) {
|
|
248
|
-
|
|
249
|
-
view
|
|
265
|
+
this.surface = config.handle ?? view
|
|
266
|
+
for (const node of new Set([view, this.surface])) {
|
|
267
|
+
node.on('loaded', this.attach)
|
|
268
|
+
node.on('unloaded', this.unload)
|
|
269
|
+
}
|
|
250
270
|
Application.on(Application.suspendEvent, this.suspend)
|
|
251
271
|
Application.on(Application.resumeEvent, this.resume)
|
|
252
272
|
this.attach()
|
|
@@ -293,8 +313,10 @@ class DragController implements Drag {
|
|
|
293
313
|
dispose = () => {
|
|
294
314
|
if (this.destroyed) return
|
|
295
315
|
this.destroyed = true
|
|
296
|
-
this.view
|
|
297
|
-
|
|
316
|
+
for (const node of new Set([this.view, this.surface])) {
|
|
317
|
+
node.off('loaded', this.attach)
|
|
318
|
+
node.off('unloaded', this.unload)
|
|
319
|
+
}
|
|
298
320
|
Application.off(Application.suspendEvent, this.suspend)
|
|
299
321
|
Application.off(Application.resumeEvent, this.resume)
|
|
300
322
|
if (owners.get(this.view) === this) owners.delete(this.view)
|
|
@@ -306,20 +328,27 @@ class DragController implements Drag {
|
|
|
306
328
|
this.destroyed ||
|
|
307
329
|
this.disabled ||
|
|
308
330
|
this.background ||
|
|
309
|
-
!this.view.isLoaded
|
|
331
|
+
!this.view.isLoaded ||
|
|
332
|
+
!this.surface.isLoaded
|
|
310
333
|
)
|
|
311
334
|
return
|
|
312
335
|
this.blocked = false
|
|
313
336
|
this.attached = true
|
|
314
|
-
this.
|
|
315
|
-
this.
|
|
337
|
+
this.surface.on('touch', this.touch)
|
|
338
|
+
this.surface.on('pan', this.pan)
|
|
316
339
|
}
|
|
317
340
|
private detach = () => {
|
|
318
341
|
this.attached = false
|
|
319
|
-
this.
|
|
320
|
-
this.
|
|
342
|
+
this.surface.off('touch', this.touch)
|
|
343
|
+
this.surface.off('pan', this.pan)
|
|
321
344
|
this.cancel()
|
|
322
345
|
}
|
|
346
|
+
shift = (x: number, y: number) => {
|
|
347
|
+
if (!this.active || this.destroyed || !Number.isFinite(x) || !Number.isFinite(y))
|
|
348
|
+
return
|
|
349
|
+
this.origin = { x: this.origin.x + x, y: this.origin.y + y }
|
|
350
|
+
this.place()
|
|
351
|
+
}
|
|
323
352
|
private unload = () => this.detach()
|
|
324
353
|
private suspend = () => {
|
|
325
354
|
this.background = true
|
|
@@ -339,7 +368,11 @@ class DragController implements Drag {
|
|
|
339
368
|
private pan = (data: EventData) => {
|
|
340
369
|
const event = data as PanGestureEventData
|
|
341
370
|
if (!this.attached || this.destroyed || this.disabled || this.background) return
|
|
342
|
-
if (
|
|
371
|
+
if (
|
|
372
|
+
this.view.isEnabled === false ||
|
|
373
|
+
this.surface.isEnabled === false ||
|
|
374
|
+
(this.surface !== this.view && !inside(this.view, this.surface))
|
|
375
|
+
) {
|
|
343
376
|
this.cancel()
|
|
344
377
|
return
|
|
345
378
|
}
|
|
@@ -385,31 +418,13 @@ class DragController implements Drag {
|
|
|
385
418
|
if (!this.active) return
|
|
386
419
|
const generation = this.generation
|
|
387
420
|
if (Number.isFinite(input.x) && Number.isFinite(input.y)) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
const raw = this.origin[axis] + input[axis]
|
|
394
|
-
const distance = raw - clampDrag(raw, curve.bounds)
|
|
395
|
-
const resistance =
|
|
396
|
-
distance === 0
|
|
397
|
-
? 1
|
|
398
|
-
: curve.rubberband
|
|
399
|
-
? 0.55 / (1 + Math.abs(distance) / this.lengths[axis]) ** 2
|
|
400
|
-
: 0
|
|
401
|
-
position[axis] = curve.rubberband
|
|
402
|
-
? resistDrag(raw, curve.bounds, this.lengths[axis])
|
|
403
|
-
: clampDrag(raw, curve.bounds)
|
|
404
|
-
if (axis === 'x') vx = input.velocityX * resistance
|
|
405
|
-
else vy = input.velocityY * resistance
|
|
406
|
-
if (this.config.value) this.config.value.setBase(position[axis])
|
|
407
|
-
else {
|
|
408
|
-
stopBase(this.view, [axis])
|
|
409
|
-
setBase(this.view, axis, position[axis])
|
|
410
|
-
}
|
|
421
|
+
this.delta = {
|
|
422
|
+
x: input.x,
|
|
423
|
+
y: input.y,
|
|
424
|
+
velocityX: input.velocityX,
|
|
425
|
+
velocityY: input.velocityY,
|
|
411
426
|
}
|
|
412
|
-
this.
|
|
427
|
+
this.place()
|
|
413
428
|
if (event.state !== 3)
|
|
414
429
|
this.emit(() => this.config.callbacks.onMove?.({ ...this.last }))
|
|
415
430
|
}
|
|
@@ -420,6 +435,34 @@ class DragController implements Drag {
|
|
|
420
435
|
if (this.active && generation === this.generation) this.finish()
|
|
421
436
|
})
|
|
422
437
|
}
|
|
438
|
+
private place() {
|
|
439
|
+
const input = this.delta
|
|
440
|
+
const position = { x: this.last.x, y: this.last.y }
|
|
441
|
+
let vx = 0,
|
|
442
|
+
vy = 0
|
|
443
|
+
for (const axis of this.config.axes) {
|
|
444
|
+
const curve = this.config.curves[axis]
|
|
445
|
+
const raw = this.origin[axis] + input[axis]
|
|
446
|
+
const distance = raw - clampDrag(raw, curve.bounds)
|
|
447
|
+
const resistance =
|
|
448
|
+
distance === 0
|
|
449
|
+
? 1
|
|
450
|
+
: curve.rubberband
|
|
451
|
+
? 0.55 / (1 + Math.abs(distance) / this.lengths[axis]) ** 2
|
|
452
|
+
: 0
|
|
453
|
+
position[axis] = curve.rubberband
|
|
454
|
+
? resistDrag(raw, curve.bounds, this.lengths[axis])
|
|
455
|
+
: clampDrag(raw, curve.bounds)
|
|
456
|
+
if (axis === 'x') vx = input.velocityX * resistance
|
|
457
|
+
else vy = input.velocityY * resistance
|
|
458
|
+
if (this.config.value) this.config.value.setBase(position[axis])
|
|
459
|
+
else {
|
|
460
|
+
stopBase(this.view, [axis])
|
|
461
|
+
setBase(this.view, axis, position[axis])
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
this.last = { ...position, velocityX: vx, velocityY: vy }
|
|
465
|
+
}
|
|
423
466
|
private emit(callback: () => void) {
|
|
424
467
|
try {
|
|
425
468
|
callback()
|
package/src/drop-zones.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Application, Trace, isIOS, type View } from '@nativescript/core'
|
|
2
2
|
import { toView } from './animate.js'
|
|
3
|
-
import { dragBeforeRelease, type
|
|
3
|
+
import { dragBeforeRelease, type AnchoredDrag, type DragOptions } from './drag.js'
|
|
4
4
|
import {
|
|
5
5
|
createDropPolicy,
|
|
6
6
|
type DropOptions,
|
|
@@ -9,7 +9,15 @@ import {
|
|
|
9
9
|
} from './core/drop.js'
|
|
10
10
|
import type { Target } from './types.js'
|
|
11
11
|
|
|
12
|
-
export type {
|
|
12
|
+
export type {
|
|
13
|
+
DropEvent,
|
|
14
|
+
DropMoveEvent,
|
|
15
|
+
DropPadding,
|
|
16
|
+
DropRect,
|
|
17
|
+
DropTarget,
|
|
18
|
+
DropZones,
|
|
19
|
+
DropEffect,
|
|
20
|
+
} from './core/drop.js'
|
|
13
21
|
export interface DropZoneOptions extends DropOptions<View> {
|
|
14
22
|
drag?: Omit<DragOptions, 'onStart' | 'onMove' | 'onRelease' | 'onCancel' | 'onSettle'>
|
|
15
23
|
}
|
|
@@ -27,7 +35,7 @@ export function dropZones(target: Target, options: DropZoneOptions): DropZones {
|
|
|
27
35
|
'value' in inputOptions
|
|
28
36
|
)
|
|
29
37
|
throw new TypeError('Drop zones own drag callbacks and move the source view itself.')
|
|
30
|
-
let input:
|
|
38
|
+
let input: AnchoredDrag | undefined,
|
|
31
39
|
handoff = false,
|
|
32
40
|
background = Application.suspended
|
|
33
41
|
const geometry = isIOS ? undefined : new org.rafaelaugusto.jolt.DropGeometry()
|
|
@@ -80,6 +88,8 @@ export function dropZones(target: Target, options: DropZoneOptions): DropZones {
|
|
|
80
88
|
bounds,
|
|
81
89
|
enabled: () => !background,
|
|
82
90
|
cancelDrag: cancelInput,
|
|
91
|
+
translation: () => input?.position ?? { x: 0, y: 0 },
|
|
92
|
+
shift: (x, y) => input?.shift(x, y),
|
|
83
93
|
error: (error) =>
|
|
84
94
|
Trace.write(String(error), Trace.categories.Error, Trace.messageType.error),
|
|
85
95
|
},
|
package/src/engine.ts
CHANGED
|
@@ -393,13 +393,18 @@ export class ViewAnimation implements PlaybackAnimation {
|
|
|
393
393
|
for (const { previous } of made) if (previous) release(previous, now, false)
|
|
394
394
|
const tracks = made.map(({ track }) => track)
|
|
395
395
|
|
|
396
|
-
for (const { track, layer } of made) {
|
|
396
|
+
for (const { track, layer, previous } of made) {
|
|
397
397
|
const mix =
|
|
398
398
|
layer !== undefined
|
|
399
399
|
? mixing(this.view, track.property)
|
|
400
400
|
: mixOf(this.view, track.property)
|
|
401
|
-
if (!mix)
|
|
402
|
-
|
|
401
|
+
if (!mix) {
|
|
402
|
+
writeValue(this.view, track.property, track.to)
|
|
403
|
+
// Core skips the native setter when the model already holds the value, which
|
|
404
|
+
// leaves the cancelled animator's last frame on screen.
|
|
405
|
+
if (previous && track.end === 0)
|
|
406
|
+
driver.commit?.(this.view, track.property, track.to)
|
|
407
|
+
} else if (track.end === 0) mix.setRest(layer ?? BASE, track.to)
|
|
403
408
|
}
|
|
404
409
|
driver.sync?.(this.view)
|
|
405
410
|
|
package/src/index.ts
CHANGED
package/src/loader.ts
CHANGED
|
@@ -2,8 +2,9 @@ import { Application, Color, isIOS, type View } from '@nativescript/core'
|
|
|
2
2
|
import { toView } from './animate.js'
|
|
3
3
|
import type { Target } from './types.js'
|
|
4
4
|
import { reducedMotion } from './config.js'
|
|
5
|
+
import { loaderDefinition, type LoaderStyle } from './core/loading.js'
|
|
5
6
|
|
|
6
|
-
export type LoaderStyle
|
|
7
|
+
export type { LoaderStyle }
|
|
7
8
|
|
|
8
9
|
export interface LoaderOptions {
|
|
9
10
|
/** Defaults to 'dots'. */
|
|
@@ -26,10 +27,10 @@ const key = 'jolt:loader'
|
|
|
26
27
|
*/
|
|
27
28
|
export function loader(target: Target, options: LoaderOptions = {}): () => void {
|
|
28
29
|
const settings: Required<LoaderOptions> = {
|
|
29
|
-
style: options.style ??
|
|
30
|
-
color: options.color ??
|
|
31
|
-
size: options.size ??
|
|
32
|
-
duration: options.duration ??
|
|
30
|
+
style: options.style ?? loaderDefinition.style,
|
|
31
|
+
color: options.color ?? loaderDefinition.color,
|
|
32
|
+
size: options.size ?? loaderDefinition.size,
|
|
33
|
+
duration: options.duration ?? loaderDefinition.duration,
|
|
33
34
|
}
|
|
34
35
|
if (!['dots', 'bars', 'ring'].includes(settings.style))
|
|
35
36
|
throw new TypeError('Unknown loader style.')
|
|
@@ -136,22 +137,28 @@ class Loader {
|
|
|
136
137
|
const layer = this.layer
|
|
137
138
|
const item = this.item!
|
|
138
139
|
const { style, duration } = this.settings
|
|
140
|
+
const kind = loaderDefinition.kinds[style]
|
|
139
141
|
layer.bounds = CGRectMake(0, 0, size, size)
|
|
140
142
|
layer.position = CGPointMake(
|
|
141
143
|
bounds.origin.x + bounds.size.width / 2,
|
|
142
144
|
bounds.origin.y + bounds.size.height / 2,
|
|
143
145
|
)
|
|
144
|
-
layer.instanceCount =
|
|
145
|
-
layer.instanceDelay = duration / 1000 /
|
|
146
|
+
layer.instanceCount = kind.count
|
|
147
|
+
layer.instanceDelay = duration / 1000 / kind.stagger
|
|
146
148
|
if (style === 'ring') {
|
|
147
|
-
const diameter = size *
|
|
149
|
+
const diameter = size * kind.width
|
|
148
150
|
item.bounds = CGRectMake(0, 0, diameter, diameter)
|
|
149
151
|
item.position = CGPointMake(size / 2, diameter / 2)
|
|
150
152
|
item.cornerRadius = diameter / 2
|
|
151
|
-
layer.instanceTransform = CATransform3DMakeRotation(
|
|
153
|
+
layer.instanceTransform = CATransform3DMakeRotation(
|
|
154
|
+
(2 * Math.PI) / kind.count,
|
|
155
|
+
0,
|
|
156
|
+
0,
|
|
157
|
+
1,
|
|
158
|
+
)
|
|
152
159
|
} else {
|
|
153
|
-
const width = size *
|
|
154
|
-
const height =
|
|
160
|
+
const width = size * kind.width
|
|
161
|
+
const height = size * kind.height
|
|
155
162
|
item.bounds = CGRectMake(0, 0, width, height)
|
|
156
163
|
item.position = CGPointMake(width / 2, size / 2)
|
|
157
164
|
item.cornerRadius = width / 2
|
|
@@ -162,24 +169,20 @@ class Loader {
|
|
|
162
169
|
)
|
|
163
170
|
}
|
|
164
171
|
const reduced = reducedMotion()
|
|
165
|
-
item.opacity =
|
|
172
|
+
item.opacity = kind.rest
|
|
166
173
|
if (reduced && this.animating) {
|
|
167
174
|
item.removeAnimationForKey(key)
|
|
168
175
|
this.animating = false
|
|
169
176
|
} else if (!reduced && !this.animating) {
|
|
170
177
|
const animation = CAKeyframeAnimation.animationWithKeyPath(
|
|
171
|
-
|
|
178
|
+
kind.property === 'opacity'
|
|
172
179
|
? 'opacity'
|
|
173
|
-
:
|
|
180
|
+
: kind.property === 'scaleY'
|
|
174
181
|
? 'transform.scale.y'
|
|
175
182
|
: 'transform.scale',
|
|
176
183
|
)
|
|
177
|
-
animation.values = NSArray.arrayWithArray(
|
|
178
|
-
|
|
179
|
-
)
|
|
180
|
-
animation.keyTimes = NSArray.arrayWithArray(
|
|
181
|
-
style === 'ring' ? [0, 1] : [0, 0.3, 0.6, 1],
|
|
182
|
-
)
|
|
184
|
+
animation.values = NSArray.arrayWithArray([...kind.values])
|
|
185
|
+
animation.keyTimes = NSArray.arrayWithArray([...kind.times])
|
|
183
186
|
animation.duration = duration / 1000
|
|
184
187
|
animation.repeatCount = Infinity
|
|
185
188
|
// Start one cycle in so every delayed copy is already participating.
|
package/src/reveal.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { resolveEasing } from './easing.js'
|
|
|
4
4
|
import type { Animation } from './engine.js'
|
|
5
5
|
import type { Target } from './types.js'
|
|
6
6
|
import { reducedMotion } from './config.js'
|
|
7
|
+
import { revealDefinition } from './core/loading.js'
|
|
7
8
|
|
|
8
9
|
export interface RevealOptions {
|
|
9
10
|
/** Circle origin in DIPs from the target's top-left corner. Defaults to its center. */
|
|
@@ -24,18 +25,18 @@ type MaskedView = UIView & { originalMask?: CALayer | null; maskType?: unknown }
|
|
|
24
25
|
type Snapshot = { circle: Circle; native: UIView; bounds: CGRect }
|
|
25
26
|
const running = new WeakMap<View, CircularAnimation>()
|
|
26
27
|
const callbacks = new WeakMap<NSObject, (finished: boolean) => void>()
|
|
27
|
-
const ease = resolveEasing(
|
|
28
|
+
const ease = resolveEasing(revealDefinition.ease)
|
|
28
29
|
const key = 'jolt:reveal'
|
|
29
30
|
let Delegate: typeof NSObject | undefined
|
|
30
31
|
|
|
31
32
|
/** Reveals an iOS view through an expanding circle, then removes the temporary mask. */
|
|
32
33
|
export function reveal(target: Target, options: RevealOptions = {}): Animation {
|
|
33
|
-
return start(target, true, options.from, options.duration ??
|
|
34
|
+
return start(target, true, options.from, options.duration ?? revealDefinition.duration)
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
/** Contracts an iOS view to a point, then sets visibility to 'hidden' without collapsing layout. */
|
|
37
38
|
export function conceal(target: Target, options: ConcealOptions = {}): Animation {
|
|
38
|
-
return start(target, false, options.to, options.duration ??
|
|
39
|
+
return start(target, false, options.to, options.duration ?? revealDefinition.duration)
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
function start(
|
package/src/shimmer.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Application, isIOS, type View } from '@nativescript/core'
|
|
|
2
2
|
import { toView } from './animate.js'
|
|
3
3
|
import type { Target } from './types.js'
|
|
4
4
|
import { reducedMotion } from './config.js'
|
|
5
|
+
import { shimmerDefinition } from './core/loading.js'
|
|
5
6
|
|
|
6
7
|
export interface ShimmerOptions {
|
|
7
8
|
/** Milliseconds per sweep. Defaults to 1500. */
|
|
@@ -21,7 +22,7 @@ type MaskedView = UIView & { originalMask?: CALayer | null }
|
|
|
21
22
|
*/
|
|
22
23
|
export function shimmer(target: Target, options: ShimmerOptions = {}): () => void {
|
|
23
24
|
const view = toView(target)
|
|
24
|
-
const duration = options.duration ??
|
|
25
|
+
const duration = options.duration ?? shimmerDefinition.duration
|
|
25
26
|
if (!Number.isFinite(duration) || duration <= 0)
|
|
26
27
|
throw new RangeError('Shimmer duration must be a positive finite number.')
|
|
27
28
|
running.get(view)?.()
|
|
@@ -138,15 +139,16 @@ class Shimmer {
|
|
|
138
139
|
|
|
139
140
|
private createGradient(): CAGradientLayer {
|
|
140
141
|
const gradient = CAGradientLayer.new()
|
|
141
|
-
const
|
|
142
|
+
const { mask } = shimmerDefinition
|
|
143
|
+
const dim = UIColor.colorWithWhiteAlpha(1, mask.opacity).CGColor
|
|
142
144
|
gradient.colors = NSArray.arrayWithArray([dim, UIColor.whiteColor.CGColor, dim])
|
|
143
145
|
gradient.startPoint = CGPointMake(0, 0.5)
|
|
144
146
|
gradient.endPoint = CGPointMake(1, 0.5)
|
|
145
147
|
|
|
146
148
|
// A three-width mask lets the band enter and leave while every stop stays
|
|
147
149
|
// within the documented 0...1 range for CAGradientLayer.locations.
|
|
148
|
-
const from = NSArray.arrayWithArray([
|
|
149
|
-
const to = NSArray.arrayWithArray([
|
|
150
|
+
const from = NSArray.arrayWithArray([...mask.from])
|
|
151
|
+
const to = NSArray.arrayWithArray([...mask.to])
|
|
150
152
|
gradient.locations = from
|
|
151
153
|
const animation = CABasicAnimation.animationWithKeyPath('locations')
|
|
152
154
|
animation.fromValue = from
|