@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.
- package/README.md +134 -14
- package/dist/animate.d.ts +3 -3
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +40 -19
- package/dist/animate.js.map +1 -1
- package/dist/driver/ios.d.ts.map +1 -1
- package/dist/driver/ios.js +19 -3
- package/dist/driver/ios.js.map +1 -1
- package/dist/engine.d.ts +7 -3
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +28 -5
- package/dist/engine.js.map +1 -1
- package/dist/haptics.d.ts +18 -0
- package/dist/haptics.d.ts.map +1 -0
- package/dist/haptics.js +96 -0
- package/dist/haptics.js.map +1 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/particles.d.ts +39 -0
- package/dist/particles.d.ts.map +1 -0
- package/dist/particles.js +264 -0
- package/dist/particles.js.map +1 -0
- package/dist/presets.d.ts +16 -1
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +24 -6
- package/dist/presets.js.map +1 -1
- package/dist/press.d.ts.map +1 -1
- package/dist/press.js +3 -1
- package/dist/press.js.map +1 -1
- package/dist/sequence.d.ts +3 -2
- package/dist/sequence.d.ts.map +1 -1
- package/dist/sequence.js +6 -5
- package/dist/sequence.js.map +1 -1
- package/dist/shimmer.d.ts +12 -0
- package/dist/shimmer.d.ts.map +1 -0
- package/dist/shimmer.js +160 -0
- package/dist/shimmer.js.map +1 -0
- package/dist/symbol-effect.d.ts +23 -0
- package/dist/symbol-effect.d.ts.map +1 -0
- package/dist/symbol-effect.js +186 -0
- package/dist/symbol-effect.js.map +1 -0
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -1
- package/src/animate.ts +64 -19
- package/src/driver/ios.ts +23 -3
- package/src/engine.ts +26 -6
- package/src/haptics.ts +125 -0
- package/src/index.ts +13 -2
- package/src/particles.ts +323 -0
- package/src/presets.ts +31 -7
- package/src/press.ts +7 -1
- package/src/sequence.ts +8 -5
- package/src/shimmer.ts +179 -0
- package/src/symbol-effect.ts +225 -0
- 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
|
package/dist/types.d.ts.map
CHANGED
|
@@ -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;
|
|
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.
|
|
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 {
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
68
|
-
export function stop(
|
|
69
|
-
|
|
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 =
|
|
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
|
|
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)
|
|
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 {
|
|
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(
|
|
162
|
-
|
|
163
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
11
|
+
* if it was stopped, superseded or skipped.
|
|
11
12
|
* Never rejects.
|
|
12
13
|
*/
|
|
13
14
|
readonly finished: Promise<boolean>
|
|
14
|
-
/** Stops
|
|
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)
|
|
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) =>
|
|
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
|
}
|