@rafaelaugusto/jolt 0.1.0-beta.0 → 0.1.0-beta.2

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 (58) hide show
  1. package/README.md +134 -14
  2. package/dist/animate.d.ts +3 -3
  3. package/dist/animate.d.ts.map +1 -1
  4. package/dist/animate.js +40 -19
  5. package/dist/animate.js.map +1 -1
  6. package/dist/driver/ios.d.ts.map +1 -1
  7. package/dist/driver/ios.js +19 -3
  8. package/dist/driver/ios.js.map +1 -1
  9. package/dist/engine.d.ts +7 -3
  10. package/dist/engine.d.ts.map +1 -1
  11. package/dist/engine.js +28 -5
  12. package/dist/engine.js.map +1 -1
  13. package/dist/haptics.d.ts +18 -0
  14. package/dist/haptics.d.ts.map +1 -0
  15. package/dist/haptics.js +96 -0
  16. package/dist/haptics.js.map +1 -0
  17. package/dist/index.d.ts +9 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +4 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/particles.d.ts +39 -0
  22. package/dist/particles.d.ts.map +1 -0
  23. package/dist/particles.js +264 -0
  24. package/dist/particles.js.map +1 -0
  25. package/dist/presets.d.ts +16 -1
  26. package/dist/presets.d.ts.map +1 -1
  27. package/dist/presets.js +24 -6
  28. package/dist/presets.js.map +1 -1
  29. package/dist/press.d.ts.map +1 -1
  30. package/dist/press.js +3 -1
  31. package/dist/press.js.map +1 -1
  32. package/dist/sequence.d.ts +3 -2
  33. package/dist/sequence.d.ts.map +1 -1
  34. package/dist/sequence.js +6 -5
  35. package/dist/sequence.js.map +1 -1
  36. package/dist/shimmer.d.ts +12 -0
  37. package/dist/shimmer.d.ts.map +1 -0
  38. package/dist/shimmer.js +160 -0
  39. package/dist/shimmer.js.map +1 -0
  40. package/dist/symbol-effect.d.ts +23 -0
  41. package/dist/symbol-effect.d.ts.map +1 -0
  42. package/dist/symbol-effect.js +186 -0
  43. package/dist/symbol-effect.js.map +1 -0
  44. package/dist/types.d.ts +5 -0
  45. package/dist/types.d.ts.map +1 -1
  46. package/package.json +6 -1
  47. package/src/animate.ts +64 -19
  48. package/src/driver/ios.ts +23 -3
  49. package/src/engine.ts +26 -6
  50. package/src/haptics.ts +125 -0
  51. package/src/index.ts +13 -2
  52. package/src/particles.ts +323 -0
  53. package/src/presets.ts +31 -7
  54. package/src/press.ts +7 -1
  55. package/src/sequence.ts +8 -5
  56. package/src/shimmer.ts +179 -0
  57. package/src/symbol-effect.ts +225 -0
  58. package/src/types.ts +5 -0
@@ -0,0 +1,186 @@
1
+ import { Image, ImageSource, isIOS } from '@nativescript/core';
2
+ import { toView } from './animate.js';
3
+ const running = new WeakMap();
4
+ const names = ['bounce', 'pulse', 'scale', 'variableColor'];
5
+ /**
6
+ * Plays an iOS 17+ SF Symbol effect on a NativeScript Image.
7
+ * Scale holds until stopped; replacements also update the Image's src.
8
+ *
9
+ * ```ts
10
+ * symbolEffect(icon, 'bounce')
11
+ * symbolEffect(icon, { replace: 'sys://heart.fill' })
12
+ * ```
13
+ */
14
+ export function symbolEffect(target, effect, options = {}) {
15
+ const view = toView(target);
16
+ if (!(view instanceof Image))
17
+ throw new TypeError('symbolEffect expects an Image.');
18
+ if (typeof effect === 'string'
19
+ ? !names.includes(effect)
20
+ : !effect ||
21
+ typeof effect.replace !== 'string' ||
22
+ !/^sys:\/\/[^\s/]+$/.test(effect.replace)) {
23
+ throw new TypeError('Expected a symbol effect name or { replace: "sys://name" }.');
24
+ }
25
+ const speed = options.speed ?? 1;
26
+ if (!Number.isFinite(speed) || speed <= 0)
27
+ throw new RangeError('Symbol effect speed must be a positive finite number.');
28
+ if (options.repeat !== undefined && typeof options.repeat !== 'boolean')
29
+ throw new TypeError('Symbol effect repeat must be a boolean.');
30
+ if (options.repeat && (typeof effect !== 'string' || effect === 'scale'))
31
+ throw new TypeError('Only bounce, pulse and variableColor can repeat.');
32
+ return new SymbolAnimation(view, effect, speed, options.repeat ?? false);
33
+ }
34
+ class SymbolAnimation {
35
+ view;
36
+ effect;
37
+ speed;
38
+ repeat;
39
+ finished;
40
+ resolve;
41
+ settled = false;
42
+ started = false;
43
+ changingSource = false;
44
+ waitingForLoad;
45
+ native;
46
+ nativeEffect;
47
+ replacement;
48
+ constructor(view, effect, speed, repeat) {
49
+ this.view = view;
50
+ this.effect = effect;
51
+ this.speed = speed;
52
+ this.repeat = repeat;
53
+ this.finished = new Promise((resolve) => (this.resolve = resolve));
54
+ this.waitingForLoad = !view.isLoaded;
55
+ running.get(view)?.stop();
56
+ running.set(view, this);
57
+ view.on('unloaded', this.stop);
58
+ view.on('srcChange', this.sourceChanged);
59
+ view.on('imageSourceChange', this.imageChanged);
60
+ view.on('loaded', this.loaded);
61
+ view.on('isLoadingChange', this.start);
62
+ this.start();
63
+ }
64
+ then(onfulfilled, onrejected) {
65
+ return this.finished.then(onfulfilled, onrejected);
66
+ }
67
+ stop = () => this.end(false, true);
68
+ sourceChanged = () => {
69
+ if (!this.changingSource)
70
+ this.stop();
71
+ };
72
+ imageChanged = () => {
73
+ if (this.changingSource)
74
+ return;
75
+ if (this.started)
76
+ this.stop();
77
+ else
78
+ this.start();
79
+ };
80
+ loaded = () => {
81
+ this.waitingForLoad = false;
82
+ this.start();
83
+ };
84
+ start = () => {
85
+ if (this.settled || this.started)
86
+ return;
87
+ if (!isIOS)
88
+ return this.end(false);
89
+ const { view, effect } = this;
90
+ // isLoaded becomes true before NativeScript resumes native property updates.
91
+ // Wait for the loaded event so intermediate image resets don't start playback.
92
+ if (this.waitingForLoad ||
93
+ !view.isLoaded ||
94
+ !view.nativeViewProtected ||
95
+ view.isLoading)
96
+ return;
97
+ this.started = true;
98
+ const native = (this.native = view.nativeViewProtected);
99
+ const reducedMotion = UIAccessibilityIsReduceMotionEnabled();
100
+ if (typeof effect !== 'string') {
101
+ // Resolve first so a symbol absent on this OS doesn't clear the current image.
102
+ if (!ImageSource.fromSystemImageSync(effect.replace.slice(6))?.ios)
103
+ return this.end(false);
104
+ const previous = native.image;
105
+ const loadMode = view.loadMode;
106
+ this.changingSource = true;
107
+ try {
108
+ // Let NativeScript update its source, tint, configuration and pending loads.
109
+ // Both assignments happen in this turn, before UIKit draws the next frame.
110
+ view.loadMode = 'sync';
111
+ view.src = effect.replace;
112
+ }
113
+ finally {
114
+ view.loadMode = loadMode;
115
+ this.changingSource = false;
116
+ }
117
+ // A source-change listener may have started another effect or changed the src.
118
+ if (this.settled)
119
+ return;
120
+ if (view.src !== effect.replace)
121
+ return this.end(false);
122
+ const image = native.image;
123
+ if (!image)
124
+ return this.end(false);
125
+ if (reducedMotion ||
126
+ !previous?.symbolImage ||
127
+ !native.respondsToSelector('setSymbolImage:withContentTransition:options:completion:'))
128
+ return this.end(true);
129
+ this.replacement = image;
130
+ native.image = previous;
131
+ native.setSymbolImageWithContentTransitionOptionsCompletion(image, NSSymbolReplaceContentTransition.transition(), this.options(), this.completed);
132
+ return;
133
+ }
134
+ if (reducedMotion ||
135
+ !native.image?.symbolImage ||
136
+ !native.respondsToSelector('addSymbolEffect:options:animated:completion:'))
137
+ return this.end(false);
138
+ const factories = {
139
+ bounce: NSSymbolBounceEffect,
140
+ pulse: NSSymbolPulseEffect,
141
+ scale: NSSymbolScaleEffect,
142
+ variableColor: NSSymbolVariableColorEffect,
143
+ };
144
+ this.nativeEffect = factories[effect].effect();
145
+ native.addSymbolEffectOptionsAnimatedCompletion(this.nativeEffect, this.options(), true, this.completed);
146
+ };
147
+ options() {
148
+ const options = this.repeat
149
+ ? NSSymbolEffectOptions.optionsWithRepeating()
150
+ : NSSymbolEffectOptions.optionsWithNonRepeating();
151
+ return options.optionsWithSpeed(this.speed);
152
+ }
153
+ completed = (context) => {
154
+ // Scale's entrance can complete while the effect is still holding its state.
155
+ if (context.finished && (this.effect === 'scale' || this.repeat))
156
+ return;
157
+ this.end(context.finished);
158
+ };
159
+ end(finished, remove = false) {
160
+ if (this.settled)
161
+ return;
162
+ this.settled = true;
163
+ const { view, native, nativeEffect, replacement } = this;
164
+ view.off('loaded', this.loaded);
165
+ view.off('isLoadingChange', this.start);
166
+ view.off('unloaded', this.stop);
167
+ view.off('srcChange', this.sourceChanged);
168
+ view.off('imageSourceChange', this.imageChanged);
169
+ if (running.get(view) === this)
170
+ running.delete(view);
171
+ if (remove && native) {
172
+ if (nativeEffect)
173
+ native.removeSymbolEffectOfType(nativeEffect);
174
+ // Content transitions have no removal API. Keep the destination icon.
175
+ if (replacement && native.image === replacement) {
176
+ native.image = null;
177
+ native.image = replacement;
178
+ }
179
+ }
180
+ this.native = undefined;
181
+ this.nativeEffect = undefined;
182
+ this.replacement = undefined;
183
+ this.resolve(finished);
184
+ }
185
+ }
186
+ //# sourceMappingURL=symbol-effect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"symbol-effect.js","sourceRoot":"","sources":["../src/symbol-effect.ts"],"names":[],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { View } from '@nativescript/core';
2
+ import type { Haptic } from './haptics.js';
2
3
  import type { SpringName, Transition } from './transition.js';
3
4
  /** A value to animate to, or keyframes. `null` stands for the value the view has when the animation starts. */
4
5
  export type KeyframeValue = number | readonly (number | null)[];
@@ -32,7 +33,11 @@ export interface Stagger {
32
33
  export type AnimateOptions = Transition & {
33
34
  [K in keyof Keyframes]?: Transition | SpringName;
34
35
  } & {
36
+ /** Offset translations/rotations and multiply scale/opacity by their values at the start. */
37
+ relative?: boolean;
35
38
  /** With several targets, delays each one by this much more than the previous. */
36
39
  stagger?: number | Stagger;
40
+ /** iOS impact feedback at arrival, or at explicit points in a finite animation. */
41
+ haptic?: Haptic;
37
42
  };
38
43
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAA;AAC9C,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAE7D,+GAA+G;AAC/G,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAA;AAE/D,MAAM,WAAW,SAAS;IACxB,OAAO,CAAC,EAAE,aAAa,CAAA;IACvB,qCAAqC;IACrC,CAAC,CAAC,EAAE,aAAa,CAAA;IACjB,qCAAqC;IACrC,CAAC,CAAC,EAAE,aAAa,CAAA;IACjB,2CAA2C;IAC3C,KAAK,CAAC,EAAE,aAAa,CAAA;IACrB,MAAM,CAAC,EAAE,aAAa,CAAA;IACtB,MAAM,CAAC,EAAE,aAAa,CAAA;IACtB,eAAe;IACf,MAAM,CAAC,EAAE,aAAa,CAAA;IACtB,OAAO,CAAC,EAAE,aAAa,CAAA;IACvB,OAAO,CAAC,EAAE,aAAa,CAAA;CACxB;AAED,+FAA+F;AAC/F,MAAM,MAAM,MAAM,GAChB,IAAI,GAAG;IAAE,QAAQ,CAAC,UAAU,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,aAAa,EAAE,IAAI,CAAA;CAAE,CAAA;AAEzE,MAAM,WAAW,OAAO;IACtB,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAA;IACZ,iCAAiC;IACjC,IAAI,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAA;CAC5C;AAED,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG;KACvC,CAAC,IAAI,MAAM,SAAS,CAAC,CAAC,EAAE,UAAU,GAAG,UAAU;CACjD,GAAG;IACF,iFAAiF;IACjF,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CAC3B,CAAA"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAA;AAC9C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAE7D,+GAA+G;AAC/G,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAA;AAE/D,MAAM,WAAW,SAAS;IACxB,OAAO,CAAC,EAAE,aAAa,CAAA;IACvB,qCAAqC;IACrC,CAAC,CAAC,EAAE,aAAa,CAAA;IACjB,qCAAqC;IACrC,CAAC,CAAC,EAAE,aAAa,CAAA;IACjB,2CAA2C;IAC3C,KAAK,CAAC,EAAE,aAAa,CAAA;IACrB,MAAM,CAAC,EAAE,aAAa,CAAA;IACtB,MAAM,CAAC,EAAE,aAAa,CAAA;IACtB,eAAe;IACf,MAAM,CAAC,EAAE,aAAa,CAAA;IACtB,OAAO,CAAC,EAAE,aAAa,CAAA;IACvB,OAAO,CAAC,EAAE,aAAa,CAAA;CACxB;AAED,+FAA+F;AAC/F,MAAM,MAAM,MAAM,GAChB,IAAI,GAAG;IAAE,QAAQ,CAAC,UAAU,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,aAAa,EAAE,IAAI,CAAA;CAAE,CAAA;AAEzE,MAAM,WAAW,OAAO;IACtB,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAA;IACZ,iCAAiC;IACjC,IAAI,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAA;CAC5C;AAED,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG;KACvC,CAAC,IAAI,MAAM,SAAS,CAAC,CAAC,EAAE,UAAU,GAAG,UAAU;CACjD,GAAG;IACF,6FAA6F;IAC7F,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,iFAAiF;IACjF,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IAC1B,mFAAmF;IACnF,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rafaelaugusto/jolt",
3
- "version": "0.1.0-beta.0",
3
+ "version": "0.1.0-beta.2",
4
4
  "description": "Native animations for NativeScript: presets, springs, keyframes and stagger in one call.",
5
5
  "keywords": [
6
6
  "nativescript",
@@ -39,10 +39,14 @@
39
39
  "build": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.build.json",
40
40
  "dev": "tsc -p tsconfig.build.json --watch",
41
41
  "typecheck": "tsc --noEmit",
42
+ "benchmark:typecheck": "tsc -p benchmarks/tsconfig.json",
42
43
  "test": "vitest run",
43
44
  "test:watch": "vitest",
44
45
  "format": "prettier --write .",
45
46
  "format:check": "prettier --check .",
47
+ "docs:dev": "vitepress dev docs",
48
+ "docs:build": "vitepress build docs",
49
+ "docs:preview": "vitepress preview docs",
46
50
  "prepack": "npm run build"
47
51
  },
48
52
  "peerDependencies": {
@@ -54,6 +58,7 @@
54
58
  "@nativescript/types-ios": "^9.1.3",
55
59
  "prettier": "^3.9.10",
56
60
  "typescript": "^7.0.2",
61
+ "vitepress": "^1.6.4",
57
62
  "vitest": "^5.0.4"
58
63
  },
59
64
  "engines": {
package/src/animate.ts CHANGED
@@ -7,7 +7,8 @@ import {
7
7
  type Animation,
8
8
  type Plan,
9
9
  } from './engine.js'
10
- import { presets, type PresetName } from './presets.js'
10
+ import { getPreset, type PresetName } from './presets.js'
11
+ import { resolveHaptic, type Haptic } from './haptics.js'
11
12
  import { isProperty, type Property } from './properties.js'
12
13
  import {
13
14
  hasCurve,
@@ -29,6 +30,8 @@ interface Options {
29
30
  base: Transition
30
31
  overrides: Partial<Record<keyof Keyframes, Transition>>
31
32
  stagger: number | Stagger | undefined
33
+ relative: boolean | undefined
34
+ haptic: Haptic | undefined
32
35
  }
33
36
 
34
37
  /**
@@ -46,10 +49,18 @@ export function animate(
46
49
  animation: Keyframes | PresetName,
47
50
  options?: AnimateOptions | SpringName,
48
51
  ): Animation {
49
- const { keyframes, base, overrides, stagger } = prepare(animation, options)
52
+ const { keyframes, base, overrides, stagger, relative, haptic } = prepare(
53
+ animation,
54
+ options,
55
+ )
56
+ const feedback = resolveHaptic(haptic)
50
57
 
51
58
  if (!isList(target)) {
52
- return new ViewAnimation(toView(target), plan(keyframes, base, overrides, 0))
59
+ return new ViewAnimation(
60
+ toView(target),
61
+ plan(keyframes, base, overrides, 0, relative),
62
+ feedback,
63
+ )
53
64
  }
54
65
 
55
66
  const views = target.map(toView)
@@ -58,15 +69,28 @@ export function animate(
58
69
  (view, index) =>
59
70
  new ViewAnimation(
60
71
  view,
61
- plan(keyframes, base, overrides, staggerDelay(stagger, index, views.length)),
72
+ plan(
73
+ keyframes,
74
+ base,
75
+ overrides,
76
+ staggerDelay(stagger, index, views.length),
77
+ relative,
78
+ ),
79
+ feedback,
62
80
  ),
63
81
  ),
64
82
  )
65
83
  }
66
84
 
67
- /** Stops every animation running on the view, leaving it where it is. */
68
- export function stop(target: Target | readonly Target[]) {
69
- for (const view of toViews(target)) stopView(view)
85
+ /** Stops all or selected properties at their current values. `scale` stops both axes. */
86
+ export function stop(
87
+ target: Target | readonly Target[],
88
+ properties?: readonly (keyof Keyframes)[],
89
+ ) {
90
+ const expanded = properties?.flatMap<Property>((property) =>
91
+ property === 'scale' ? ['scaleX', 'scaleY'] : [property],
92
+ )
93
+ for (const view of toViews(target)) stopView(view, expanded)
70
94
  }
71
95
 
72
96
  /** Plays several animations as one: finished once they all are, and stopped together. */
@@ -107,35 +131,53 @@ function prepare(
107
131
  const user = normalize(options)
108
132
  if (typeof animation !== 'string') return { keyframes: animation, ...user }
109
133
 
110
- const preset = presets[animation]
111
- if (!preset) throw new Error(`Unknown preset "${animation}".`)
134
+ const preset = getPreset(animation)
112
135
  const own = normalize(preset.transition)
113
136
  return {
114
- keyframes: preset.keyframes as Keyframes,
137
+ keyframes: preset.keyframes,
115
138
  base: mergeTransitions(own.base, user.base),
116
139
  overrides: hasCurve(user.base)
117
140
  ? user.overrides
118
141
  : { ...own.overrides, ...user.overrides },
119
- stagger: user.stagger,
142
+ stagger: user.stagger ?? own.stagger,
143
+ relative: user.relative ?? own.relative,
144
+ haptic: user.haptic ?? own.haptic,
120
145
  }
121
146
  }
122
147
 
123
148
  function normalize(options: AnimateOptions | SpringName | undefined): Options {
124
- if (options === undefined) return { base: {}, overrides: {}, stagger: undefined }
149
+ if (options === undefined)
150
+ return {
151
+ base: {},
152
+ overrides: {},
153
+ stagger: undefined,
154
+ relative: undefined,
155
+ haptic: undefined,
156
+ }
125
157
  if (typeof options === 'string')
126
- return { base: spring(options), overrides: {}, stagger: undefined }
158
+ return {
159
+ base: spring(options),
160
+ overrides: {},
161
+ stagger: undefined,
162
+ relative: undefined,
163
+ haptic: undefined,
164
+ }
127
165
 
128
166
  const base: Record<string, unknown> = {}
129
167
  const overrides: Options['overrides'] = {}
130
168
  let stagger: Options['stagger']
169
+ let relative: Options['relative']
170
+ let haptic: Options['haptic']
131
171
  for (const [key, value] of Object.entries(options)) {
132
172
  if (value === undefined) continue
133
173
  if (key === 'stagger') stagger = value as Options['stagger']
174
+ else if (key === 'relative') relative = value as Options['relative']
175
+ else if (key === 'haptic') haptic = value as Options['haptic']
134
176
  else if (key === 'scale' || isProperty(key)) {
135
177
  overrides[key] = typeof value === 'string' ? spring(value as SpringName) : value
136
178
  } else base[key] = value
137
179
  }
138
- return { base, overrides, stagger }
180
+ return { base, overrides, stagger, relative, haptic }
139
181
  }
140
182
 
141
183
  function plan(
@@ -143,6 +185,7 @@ function plan(
143
185
  base: Transition,
144
186
  overrides: Options['overrides'],
145
187
  offset: number,
188
+ relative = false,
146
189
  ): Plan[] {
147
190
  return expand(keyframes).map(([property, frames]) => {
148
191
  const override =
@@ -153,17 +196,19 @@ function plan(
153
196
  frames.length,
154
197
  )
155
198
  transition.delay += offset
156
- return { property, keyframes: frames, transition }
199
+ return { property, keyframes: frames, transition, relative }
157
200
  })
158
201
  }
159
202
 
160
203
  /** Each property the animation touches, with the value it starts from when it sets one. */
161
- export function startingValues(animation: Keyframes | PresetName) {
162
- const keyframes =
163
- typeof animation === 'string' ? presets[animation]?.keyframes : animation
204
+ export function startingValues(
205
+ animation: Keyframes | PresetName,
206
+ options?: AnimateOptions | SpringName,
207
+ ) {
208
+ const { keyframes, relative } = prepare(animation, options)
164
209
  if (!keyframes) throw new Error(`Unknown preset "${animation}".`)
165
210
  return new Map(
166
- expand(keyframes as Keyframes).map(([property, [first]]) => [property, first!]),
211
+ expand(keyframes).map(([property, [first]]) => [property, relative ? null : first!]),
167
212
  )
168
213
  }
169
214
 
package/src/driver/ios.ts CHANGED
@@ -5,6 +5,7 @@ import { after, type Driver } from './index.js'
5
5
  interface Handle {
6
6
  layers: CALayer[]
7
7
  key: string
8
+ onStop?: () => void
8
9
  }
9
10
 
10
11
  const keyPaths: Record<Property, string> = {
@@ -51,11 +52,23 @@ export function createIOSDriver(): Driver {
51
52
  const layers = layersOf(view)
52
53
  const key = `jolt:${track.property}`
53
54
  const rotation = track.property.startsWith('rotate')
55
+ const infinite = track.repeat === Infinity
54
56
  if (track.property === 'rotateX' || track.property === 'rotateY') {
55
57
  ensurePerspective(view, layers)
56
58
  }
57
59
 
58
60
  const animation = CAKeyframeAnimation.animationWithKeyPath(keyPaths[track.property])
61
+ // iOS 15+. The app must opt in through CADisableMinimumFrameDurationOnPhone.
62
+ if (animation.respondsToSelector('setPreferredFrameRateRange:')) {
63
+ const nativeView = view.nativeViewProtected as UIView
64
+ const screen = nativeView.window?.screen ?? UIScreen.mainScreen
65
+ const maximum = Math.min(120, screen.maximumFramesPerSecond)
66
+ animation.preferredFrameRateRange = CAFrameRateRangeMake(
67
+ Math.min(80, maximum),
68
+ maximum,
69
+ maximum,
70
+ )
71
+ }
59
72
  animation.values = NSArray.arrayWithArray(
60
73
  track.values.map((value) => (rotation ? radians(value) : value)),
61
74
  )
@@ -63,22 +76,29 @@ export function createIOSDriver(): Driver {
63
76
  animation.duration = Math.max(track.duration, 1) / 1000
64
77
  animation.beginTime = (startedAt + track.delay) / 1000
65
78
  animation.fillMode = kCAFillModeBackwards
79
+ // Keep infinite loops when iOS removes animations on backgrounding.
80
+ // They are removed explicitly when stopped or replaced.
81
+ animation.removedOnCompletion = !infinite
66
82
  if (track.repeat > 0) {
67
83
  animation.autoreverses = track.reverse
68
84
  animation.repeatCount = track.reverse ? (track.repeat + 1) / 2 : track.repeat + 1
69
85
  }
70
86
 
71
87
  CATransaction.begin()
72
- CATransaction.setCompletionBlock(onEnd)
88
+ // iOS completes the transaction on backgrounding even if the loop is retained.
89
+ if (!infinite) CATransaction.setCompletionBlock(onEnd)
73
90
  for (const layer of layers) layer.addAnimationForKey(animation, key)
74
91
  CATransaction.commit()
75
92
 
76
- return { layers, key }
93
+ return { layers, key, onStop: infinite ? onEnd : undefined }
77
94
  },
78
95
 
79
96
  stop(_view, handle) {
80
- const { layers, key } = handle as Handle
97
+ const playing = handle as Handle
98
+ const { layers, key, onStop } = playing
99
+ playing.onStop = undefined
81
100
  for (const layer of layers) layer.removeAnimationForKey(key)
101
+ onStop?.()
82
102
  },
83
103
 
84
104
  presented: () => undefined,
package/src/engine.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { View } from '@nativescript/core'
2
2
  import { getDriver } from './driver/index.js'
3
+ import { playHaptics, type HapticOptions } from './haptics.js'
3
4
  import { readValue, writeValue, type Property } from './properties.js'
4
5
  import { createTrack, type Track } from './track.js'
5
6
  import type { ResolvedTransition } from './transition.js'
@@ -7,11 +8,11 @@ import type { ResolvedTransition } from './transition.js'
7
8
  export interface Animation extends PromiseLike<boolean> {
8
9
  /**
9
10
  * Resolves with `true` once the animation played to the end, or with `false`
10
- * if it was stopped or another animation took over one of its properties.
11
+ * if it was stopped, superseded or skipped.
11
12
  * Never rejects.
12
13
  */
13
14
  readonly finished: Promise<boolean>
14
- /** Stops where it is. The view keeps the values it had at that moment. */
15
+ /** Stops playback. Property animations keep their current values; symbol effects are removed. */
15
16
  stop(): void
16
17
  }
17
18
 
@@ -20,6 +21,7 @@ export interface Plan {
20
21
  /** `null` stands for the value the property has when the animation starts. */
21
22
  keyframes: readonly (number | null)[]
22
23
  transition: ResolvedTransition
24
+ relative?: boolean
23
25
  }
24
26
 
25
27
  interface Entry {
@@ -55,10 +57,12 @@ export class ViewAnimation implements Animation {
55
57
  private cancelWait: (() => void) | undefined
56
58
  private arrive!: () => void
57
59
  private cancelArrival: (() => void) | undefined
60
+ private haptics: ReturnType<typeof playHaptics>
58
61
 
59
62
  constructor(
60
63
  readonly view: View,
61
64
  plans: readonly Plan[],
65
+ private readonly haptic?: HapticOptions,
62
66
  ) {
63
67
  this.finished = new Promise((resolve) => (this.resolve = resolve))
64
68
  setArrival(this, new Promise<void>((resolve) => (this.arrive = resolve)))
@@ -76,6 +80,7 @@ export class ViewAnimation implements Animation {
76
80
  stop() {
77
81
  if (this.settled) return
78
82
  this.cancelWait?.()
83
+ this.haptics?.cancel()
79
84
  const now = getDriver().now()
80
85
  for (const entry of this.entries) {
81
86
  if (!entry.done) release(entry, now, true)
@@ -85,7 +90,10 @@ export class ViewAnimation implements Animation {
85
90
  }
86
91
 
87
92
  ended(finished: boolean) {
88
- if (!finished) this.complete = false
93
+ if (!finished) {
94
+ this.complete = false
95
+ this.haptics?.cancel()
96
+ }
89
97
  if (--this.pending === 0) this.settle()
90
98
  }
91
99
 
@@ -94,7 +102,7 @@ export class ViewAnimation implements Animation {
94
102
  const now = driver.now()
95
103
  const entries = entriesOf(this.view)
96
104
 
97
- const tracks = plans.map(({ property, keyframes, transition }) => {
105
+ const tracks = plans.map(({ property, keyframes, transition, relative }) => {
98
106
  const previous = entries.get(property)
99
107
  let current: number
100
108
  let velocity = 0
@@ -108,7 +116,13 @@ export class ViewAnimation implements Animation {
108
116
  }
109
117
  return createTrack(
110
118
  property,
111
- keyframes.map((value) => value ?? current),
119
+ keyframes.map((value) => {
120
+ if (value === null) return current
121
+ if (!relative) return value
122
+ return property === 'opacity' || property.startsWith('scale')
123
+ ? current * value
124
+ : current + value
125
+ }),
112
126
  transition,
113
127
  velocity,
114
128
  )
@@ -130,6 +144,8 @@ export class ViewAnimation implements Animation {
130
144
  this.pending++
131
145
  }
132
146
 
147
+ this.haptics = playHaptics(this.view, tracks, this.haptic, driver, now)
148
+
133
149
  for (const entry of this.entries) {
134
150
  entry.handle = driver.play(this.view, entry.track, now, () => {
135
151
  if (entry.done) return
@@ -147,7 +163,8 @@ export class ViewAnimation implements Animation {
147
163
  }
148
164
 
149
165
  private waitForLoad(plans: readonly Plan[]) {
150
- for (const { property, keyframes } of plans) {
166
+ for (const { property, keyframes, relative } of plans) {
167
+ if (relative) continue
151
168
  const first = keyframes[0]
152
169
  if (first !== null && first !== undefined) writeValue(this.view, property, first)
153
170
  }
@@ -166,6 +183,9 @@ export class ViewAnimation implements Animation {
166
183
  if (this.settled) return
167
184
  this.settled = true
168
185
  this.cancelArrival?.()
186
+ if (this.complete) this.haptics?.finish()
187
+ else this.haptics?.cancel()
188
+ this.haptics = undefined
169
189
  this.arrive()
170
190
  this.resolve(this.complete)
171
191
  }