@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.
Files changed (86) hide show
  1. package/README.md +2 -2
  2. package/dist/core/drop.d.ts +34 -1
  3. package/dist/core/drop.d.ts.map +1 -1
  4. package/dist/core/drop.js +93 -8
  5. package/dist/core/drop.js.map +1 -1
  6. package/dist/core/loading.d.ts +125 -0
  7. package/dist/core/loading.d.ts.map +1 -0
  8. package/dist/core/loading.js +119 -0
  9. package/dist/core/loading.js.map +1 -0
  10. package/dist/core/preset-recipe.d.ts +2 -2
  11. package/dist/core/preset-recipe.d.ts.map +1 -1
  12. package/dist/core/preset-recipe.js +3 -1
  13. package/dist/core/preset-recipe.js.map +1 -1
  14. package/dist/core/text-pieces.d.ts +18 -0
  15. package/dist/core/text-pieces.d.ts.map +1 -0
  16. package/dist/core/text-pieces.js +43 -0
  17. package/dist/core/text-pieces.js.map +1 -0
  18. package/dist/drag.d.ts +7 -1
  19. package/dist/drag.d.ts.map +1 -1
  20. package/dist/drag.js +72 -37
  21. package/dist/drag.js.map +1 -1
  22. package/dist/drop-zones.d.ts +1 -1
  23. package/dist/drop-zones.d.ts.map +1 -1
  24. package/dist/drop-zones.js +2 -0
  25. package/dist/drop-zones.js.map +1 -1
  26. package/dist/engine.d.ts.map +1 -1
  27. package/dist/engine.js +7 -2
  28. package/dist/engine.js.map +1 -1
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js.map +1 -1
  32. package/dist/loader.d.ts +2 -1
  33. package/dist/loader.d.ts.map +1 -1
  34. package/dist/loader.js +17 -15
  35. package/dist/loader.js.map +1 -1
  36. package/dist/reveal.d.ts.map +1 -1
  37. package/dist/reveal.js +4 -3
  38. package/dist/reveal.js.map +1 -1
  39. package/dist/shimmer.d.ts.map +1 -1
  40. package/dist/shimmer.js +6 -4
  41. package/dist/shimmer.js.map +1 -1
  42. package/dist/web/drag.d.ts +8 -0
  43. package/dist/web/drag.d.ts.map +1 -1
  44. package/dist/web/drag.js +68 -14
  45. package/dist/web/drag.js.map +1 -1
  46. package/dist/web/drop-zones.d.ts +2 -2
  47. package/dist/web/drop-zones.d.ts.map +1 -1
  48. package/dist/web/drop-zones.js +21 -4
  49. package/dist/web/drop-zones.js.map +1 -1
  50. package/dist/web/loading.d.ts +2 -1
  51. package/dist/web/loading.d.ts.map +1 -1
  52. package/dist/web/loading.js +19 -15
  53. package/dist/web/loading.js.map +1 -1
  54. package/dist/web/pointer.d.ts +2 -0
  55. package/dist/web/pointer.d.ts.map +1 -1
  56. package/dist/web/pointer.js +13 -0
  57. package/dist/web/pointer.js.map +1 -1
  58. package/dist/web/public.d.ts +1 -1
  59. package/dist/web/public.d.ts.map +1 -1
  60. package/dist/web/public.js.map +1 -1
  61. package/dist/web/reveal.d.ts.map +1 -1
  62. package/dist/web/reveal.js +4 -3
  63. package/dist/web/reveal.js.map +1 -1
  64. package/dist/web/text-effect.d.ts.map +1 -1
  65. package/dist/web/text-effect.js +10 -16
  66. package/dist/web/text-effect.js.map +1 -1
  67. package/package.json +12 -3
  68. package/platforms/android/java/org/rafaelaugusto/jolt/PointerInput.java +26 -4
  69. package/src/core/drop.ts +123 -5
  70. package/src/core/loading.ts +172 -0
  71. package/src/core/preset-recipe.ts +6 -2
  72. package/src/core/text-pieces.ts +52 -0
  73. package/src/drag.ts +81 -38
  74. package/src/drop-zones.ts +13 -3
  75. package/src/engine.ts +8 -3
  76. package/src/index.ts +3 -0
  77. package/src/loader.ts +23 -20
  78. package/src/reveal.ts +4 -3
  79. package/src/shimmer.ts +6 -4
  80. package/src/web/drag.ts +70 -12
  81. package/src/web/drop-zones.ts +31 -6
  82. package/src/web/loading.ts +38 -17
  83. package/src/web/pointer.ts +14 -0
  84. package/src/web/public.ts +3 -0
  85. package/src/web/reveal.ts +7 -8
  86. 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
- return { axes, curves, value: value && asRoot(value), callbacks }
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
- ): Drag {
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
- ): Drag {
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 Drag {
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
- view.on('loaded', this.attach)
249
- view.on('unloaded', this.unload)
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.off('loaded', this.attach)
297
- this.view.off('unloaded', this.unload)
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.view.on('touch', this.touch)
315
- this.view.on('pan', this.pan)
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.view.off('touch', this.touch)
320
- this.view.off('pan', this.pan)
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 (this.view.isEnabled === false) {
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
- const position = { x: this.last.x, y: this.last.y }
389
- let vx = 0,
390
- vy = 0
391
- for (const axis of this.config.axes) {
392
- const curve = this.config.curves[axis]
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.last = { ...position, velocityX: vx, velocityY: vy }
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 Drag, type DragOptions } from './drag.js'
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 { DropEvent, DropTarget, DropZones, DropEffect } from './core/drop.js'
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: Drag | undefined,
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) writeValue(this.view, track.property, track.to)
402
- else if (track.end === 0) mix.setRest(layer ?? BASE, track.to)
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
@@ -29,6 +29,9 @@ export type {
29
29
  DropZones,
30
30
  DropTarget,
31
31
  DropEvent,
32
+ DropMoveEvent,
33
+ DropPadding,
34
+ DropRect,
32
35
  DropEffect,
33
36
  } from './drop-zones.js'
34
37
  export type {
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 = 'dots' | 'bars' | 'ring'
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 ?? 'dots',
30
- color: options.color ?? '#71717a',
31
- size: options.size ?? 48,
32
- duration: options.duration ?? 1200,
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 = style === 'dots' ? 3 : style === 'bars' ? 5 : 12
145
- layer.instanceDelay = duration / 1000 / (style === 'ring' ? 12 : 5)
146
+ layer.instanceCount = kind.count
147
+ layer.instanceDelay = duration / 1000 / kind.stagger
146
148
  if (style === 'ring') {
147
- const diameter = size * 0.16
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((2 * Math.PI) / 12, 0, 0, 1)
153
+ layer.instanceTransform = CATransform3DMakeRotation(
154
+ (2 * Math.PI) / kind.count,
155
+ 0,
156
+ 0,
157
+ 1,
158
+ )
152
159
  } else {
153
- const width = size * (style === 'dots' ? 0.2 : 0.12)
154
- const height = style === 'dots' ? width : size * 0.75
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 = style === 'ring' ? 0.35 : 1
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
- style === 'ring'
178
+ kind.property === 'opacity'
172
179
  ? 'opacity'
173
- : style === 'bars'
180
+ : kind.property === 'scaleY'
174
181
  ? 'transform.scale.y'
175
182
  : 'transform.scale',
176
183
  )
177
- animation.values = NSArray.arrayWithArray(
178
- style === 'ring' ? [1, 0.2] : [0.35, 1, 0.35, 0.35],
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('easeInOut')
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 ?? 400)
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 ?? 400)
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 ?? 1500
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 dim = UIColor.colorWithWhiteAlpha(1, 0.35).CGColor
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([0, 0.125, 0.25])
149
- const to = NSArray.arrayWithArray([0.75, 0.875, 1])
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