@rafaelaugusto/jolt 0.1.0-beta.1 → 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 +58 -5
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +18 -5
- package/dist/animate.js.map +1 -1
- package/dist/driver/ios.d.ts.map +1 -1
- package/dist/driver/ios.js +7 -0
- package/dist/driver/ios.js.map +1 -1
- package/dist/engine.d.ts +6 -3
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +15 -2
- 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 +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- 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/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 +3 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/animate.ts +21 -3
- package/src/driver/ios.ts +11 -0
- package/src/engine.ts +15 -3
- package/src/haptics.ts +125 -0
- package/src/index.ts +11 -0
- package/src/particles.ts +323 -0
- package/src/shimmer.ts +179 -0
- package/src/symbol-effect.ts +225 -0
- package/src/types.ts +3 -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)[];
|
|
@@ -36,5 +37,7 @@ export type AnimateOptions = Transition & {
|
|
|
36
37
|
relative?: boolean;
|
|
37
38
|
/** With several targets, delays each one by this much more than the previous. */
|
|
38
39
|
stagger?: number | Stagger;
|
|
40
|
+
/** iOS impact feedback at arrival, or at explicit points in a finite animation. */
|
|
41
|
+
haptic?: Haptic;
|
|
39
42
|
};
|
|
40
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,6FAA6F;IAC7F,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,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,6 +39,7 @@
|
|
|
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 .",
|
package/src/animate.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type Plan,
|
|
9
9
|
} from './engine.js'
|
|
10
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,
|
|
@@ -30,6 +31,7 @@ interface Options {
|
|
|
30
31
|
overrides: Partial<Record<keyof Keyframes, Transition>>
|
|
31
32
|
stagger: number | Stagger | undefined
|
|
32
33
|
relative: boolean | undefined
|
|
34
|
+
haptic: Haptic | undefined
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
/**
|
|
@@ -47,12 +49,17 @@ export function animate(
|
|
|
47
49
|
animation: Keyframes | PresetName,
|
|
48
50
|
options?: AnimateOptions | SpringName,
|
|
49
51
|
): Animation {
|
|
50
|
-
const { keyframes, base, overrides, stagger, relative } = prepare(
|
|
52
|
+
const { keyframes, base, overrides, stagger, relative, haptic } = prepare(
|
|
53
|
+
animation,
|
|
54
|
+
options,
|
|
55
|
+
)
|
|
56
|
+
const feedback = resolveHaptic(haptic)
|
|
51
57
|
|
|
52
58
|
if (!isList(target)) {
|
|
53
59
|
return new ViewAnimation(
|
|
54
60
|
toView(target),
|
|
55
61
|
plan(keyframes, base, overrides, 0, relative),
|
|
62
|
+
feedback,
|
|
56
63
|
)
|
|
57
64
|
}
|
|
58
65
|
|
|
@@ -69,6 +76,7 @@ export function animate(
|
|
|
69
76
|
staggerDelay(stagger, index, views.length),
|
|
70
77
|
relative,
|
|
71
78
|
),
|
|
79
|
+
feedback,
|
|
72
80
|
),
|
|
73
81
|
),
|
|
74
82
|
)
|
|
@@ -133,33 +141,43 @@ function prepare(
|
|
|
133
141
|
: { ...own.overrides, ...user.overrides },
|
|
134
142
|
stagger: user.stagger ?? own.stagger,
|
|
135
143
|
relative: user.relative ?? own.relative,
|
|
144
|
+
haptic: user.haptic ?? own.haptic,
|
|
136
145
|
}
|
|
137
146
|
}
|
|
138
147
|
|
|
139
148
|
function normalize(options: AnimateOptions | SpringName | undefined): Options {
|
|
140
149
|
if (options === undefined)
|
|
141
|
-
return {
|
|
150
|
+
return {
|
|
151
|
+
base: {},
|
|
152
|
+
overrides: {},
|
|
153
|
+
stagger: undefined,
|
|
154
|
+
relative: undefined,
|
|
155
|
+
haptic: undefined,
|
|
156
|
+
}
|
|
142
157
|
if (typeof options === 'string')
|
|
143
158
|
return {
|
|
144
159
|
base: spring(options),
|
|
145
160
|
overrides: {},
|
|
146
161
|
stagger: undefined,
|
|
147
162
|
relative: undefined,
|
|
163
|
+
haptic: undefined,
|
|
148
164
|
}
|
|
149
165
|
|
|
150
166
|
const base: Record<string, unknown> = {}
|
|
151
167
|
const overrides: Options['overrides'] = {}
|
|
152
168
|
let stagger: Options['stagger']
|
|
153
169
|
let relative: Options['relative']
|
|
170
|
+
let haptic: Options['haptic']
|
|
154
171
|
for (const [key, value] of Object.entries(options)) {
|
|
155
172
|
if (value === undefined) continue
|
|
156
173
|
if (key === 'stagger') stagger = value as Options['stagger']
|
|
157
174
|
else if (key === 'relative') relative = value as Options['relative']
|
|
175
|
+
else if (key === 'haptic') haptic = value as Options['haptic']
|
|
158
176
|
else if (key === 'scale' || isProperty(key)) {
|
|
159
177
|
overrides[key] = typeof value === 'string' ? spring(value as SpringName) : value
|
|
160
178
|
} else base[key] = value
|
|
161
179
|
}
|
|
162
|
-
return { base, overrides, stagger, relative }
|
|
180
|
+
return { base, overrides, stagger, relative, haptic }
|
|
163
181
|
}
|
|
164
182
|
|
|
165
183
|
function plan(
|
package/src/driver/ios.ts
CHANGED
|
@@ -58,6 +58,17 @@ export function createIOSDriver(): Driver {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
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
|
+
}
|
|
61
72
|
animation.values = NSArray.arrayWithArray(
|
|
62
73
|
track.values.map((value) => (rotation ? radians(value) : value)),
|
|
63
74
|
)
|
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
|
|
|
@@ -56,10 +57,12 @@ export class ViewAnimation implements Animation {
|
|
|
56
57
|
private cancelWait: (() => void) | undefined
|
|
57
58
|
private arrive!: () => void
|
|
58
59
|
private cancelArrival: (() => void) | undefined
|
|
60
|
+
private haptics: ReturnType<typeof playHaptics>
|
|
59
61
|
|
|
60
62
|
constructor(
|
|
61
63
|
readonly view: View,
|
|
62
64
|
plans: readonly Plan[],
|
|
65
|
+
private readonly haptic?: HapticOptions,
|
|
63
66
|
) {
|
|
64
67
|
this.finished = new Promise((resolve) => (this.resolve = resolve))
|
|
65
68
|
setArrival(this, new Promise<void>((resolve) => (this.arrive = resolve)))
|
|
@@ -77,6 +80,7 @@ export class ViewAnimation implements Animation {
|
|
|
77
80
|
stop() {
|
|
78
81
|
if (this.settled) return
|
|
79
82
|
this.cancelWait?.()
|
|
83
|
+
this.haptics?.cancel()
|
|
80
84
|
const now = getDriver().now()
|
|
81
85
|
for (const entry of this.entries) {
|
|
82
86
|
if (!entry.done) release(entry, now, true)
|
|
@@ -86,7 +90,10 @@ export class ViewAnimation implements Animation {
|
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
ended(finished: boolean) {
|
|
89
|
-
if (!finished)
|
|
93
|
+
if (!finished) {
|
|
94
|
+
this.complete = false
|
|
95
|
+
this.haptics?.cancel()
|
|
96
|
+
}
|
|
90
97
|
if (--this.pending === 0) this.settle()
|
|
91
98
|
}
|
|
92
99
|
|
|
@@ -137,6 +144,8 @@ export class ViewAnimation implements Animation {
|
|
|
137
144
|
this.pending++
|
|
138
145
|
}
|
|
139
146
|
|
|
147
|
+
this.haptics = playHaptics(this.view, tracks, this.haptic, driver, now)
|
|
148
|
+
|
|
140
149
|
for (const entry of this.entries) {
|
|
141
150
|
entry.handle = driver.play(this.view, entry.track, now, () => {
|
|
142
151
|
if (entry.done) return
|
|
@@ -174,6 +183,9 @@ export class ViewAnimation implements Animation {
|
|
|
174
183
|
if (this.settled) return
|
|
175
184
|
this.settled = true
|
|
176
185
|
this.cancelArrival?.()
|
|
186
|
+
if (this.complete) this.haptics?.finish()
|
|
187
|
+
else this.haptics?.cancel()
|
|
188
|
+
this.haptics = undefined
|
|
177
189
|
this.arrive()
|
|
178
190
|
this.resolve(this.complete)
|
|
179
191
|
}
|
package/src/haptics.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { isIOS, type View } from '@nativescript/core'
|
|
2
|
+
import type { Driver } from './driver/index.js'
|
|
3
|
+
import type { Track } from './track.js'
|
|
4
|
+
|
|
5
|
+
export type HapticStyle = 'light' | 'medium' | 'heavy'
|
|
6
|
+
|
|
7
|
+
export interface HapticOptions {
|
|
8
|
+
style: HapticStyle
|
|
9
|
+
/** Points from 0 to 1 over the finite animation, excluding its initial delay. Defaults to arrival. */
|
|
10
|
+
times?: readonly number[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type Haptic = HapticStyle | HapticOptions | false
|
|
14
|
+
|
|
15
|
+
/** Validate before any running animation is interrupted. */
|
|
16
|
+
export function resolveHaptic(haptic: Haptic | undefined): HapticOptions | undefined {
|
|
17
|
+
if (haptic === undefined || haptic === false) return undefined
|
|
18
|
+
const options = typeof haptic === 'string' ? { style: haptic } : haptic
|
|
19
|
+
if (!options || !['light', 'medium', 'heavy'].includes(options.style))
|
|
20
|
+
throw new TypeError('Haptic style must be light, medium or heavy.')
|
|
21
|
+
const times = options.times
|
|
22
|
+
if (
|
|
23
|
+
times !== undefined &&
|
|
24
|
+
(!Array.isArray(times) ||
|
|
25
|
+
times.length === 0 ||
|
|
26
|
+
Array.from(times).some(
|
|
27
|
+
(time, i) =>
|
|
28
|
+
!Number.isFinite(time) ||
|
|
29
|
+
time <= 0 ||
|
|
30
|
+
time > 1 ||
|
|
31
|
+
(i > 0 && time <= times[i - 1]!),
|
|
32
|
+
))
|
|
33
|
+
)
|
|
34
|
+
throw new RangeError(
|
|
35
|
+
'Haptic times must be increasing points greater than 0 and at most 1.',
|
|
36
|
+
)
|
|
37
|
+
return { style: options.style, times: times && [...times] }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const PREPARE_AHEAD = 100
|
|
41
|
+
const MAX_LATENESS = 100
|
|
42
|
+
const COMPLETION_TOLERANCE = 20
|
|
43
|
+
|
|
44
|
+
/** Discrete feedback only: animation playback remains entirely native. */
|
|
45
|
+
export function playHaptics(
|
|
46
|
+
view: View,
|
|
47
|
+
tracks: readonly Track[],
|
|
48
|
+
options: HapticOptions | undefined,
|
|
49
|
+
driver: Driver,
|
|
50
|
+
startedAt: number,
|
|
51
|
+
) {
|
|
52
|
+
if (!isIOS || !options || typeof UIImpactFeedbackGenerator === 'undefined') return
|
|
53
|
+
const moving = tracks.filter(
|
|
54
|
+
(track) =>
|
|
55
|
+
track.duration > 0 && track.values.some((value) => value !== track.values[0]),
|
|
56
|
+
)
|
|
57
|
+
if (!moving.length) return
|
|
58
|
+
const end = Math.max(...tracks.map((track) => track.end))
|
|
59
|
+
if (!Number.isFinite(end)) return
|
|
60
|
+
const delay = Math.min(...moving.map((track) => track.delay))
|
|
61
|
+
const offsets = options.times
|
|
62
|
+
? options.times.map((time) => delay + (end - delay) * time)
|
|
63
|
+
: [Math.max(...tracks.map((track) => track.arrival))]
|
|
64
|
+
const styles = {
|
|
65
|
+
light: 0,
|
|
66
|
+
medium: 1,
|
|
67
|
+
heavy: 2,
|
|
68
|
+
} satisfies Record<HapticStyle, UIImpactFeedbackStyle>
|
|
69
|
+
let generator: UIImpactFeedbackGenerator | undefined =
|
|
70
|
+
UIImpactFeedbackGenerator.alloc().initWithStyle(styles[options.style])
|
|
71
|
+
const native = view.nativeViewProtected as UIView
|
|
72
|
+
let next = 0
|
|
73
|
+
let cancelPrepare: (() => void) | undefined
|
|
74
|
+
let cancelImpact: (() => void) | undefined
|
|
75
|
+
|
|
76
|
+
const active = () =>
|
|
77
|
+
view.isLoaded &&
|
|
78
|
+
view.nativeViewProtected === native &&
|
|
79
|
+
!!native.window &&
|
|
80
|
+
UIApplication.sharedApplication.applicationState === 0 // UIApplicationState.Active
|
|
81
|
+
|
|
82
|
+
const cancel = () => {
|
|
83
|
+
cancelPrepare?.()
|
|
84
|
+
cancelImpact?.()
|
|
85
|
+
cancelPrepare = cancelImpact = undefined
|
|
86
|
+
generator = undefined
|
|
87
|
+
view.off('unloaded', cancel)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const fire = (tolerance = 0) => {
|
|
91
|
+
const elapsed = driver.now() - startedAt
|
|
92
|
+
// A busy UI thread can delay timers. Drop stale impacts instead of replaying
|
|
93
|
+
// several old taps at once when it becomes responsive again.
|
|
94
|
+
while (next < offsets.length && offsets[next]! <= elapsed + tolerance) {
|
|
95
|
+
const offset = offsets[next++]!
|
|
96
|
+
if (active() && elapsed - offset <= MAX_LATENESS) generator?.impactOccurred()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const schedule = () => {
|
|
101
|
+
if (!generator || next === offsets.length) return cancel()
|
|
102
|
+
const remaining = startedAt + offsets[next]! - driver.now()
|
|
103
|
+
if (active()) generator.prepare()
|
|
104
|
+
if (remaining > PREPARE_AHEAD) {
|
|
105
|
+
cancelPrepare = driver.after(remaining - PREPARE_AHEAD, () => {
|
|
106
|
+
if (active()) generator?.prepare()
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
cancelImpact = driver.after(Math.max(0, remaining), () => {
|
|
110
|
+
fire()
|
|
111
|
+
schedule()
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
view.on('unloaded', cancel)
|
|
116
|
+
schedule()
|
|
117
|
+
return {
|
|
118
|
+
cancel,
|
|
119
|
+
finish() {
|
|
120
|
+
// Native completion may beat the timer at the final keyframe by a frame.
|
|
121
|
+
fire(COMPLETION_TOLERANCE)
|
|
122
|
+
cancel()
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,17 @@ export { animate, stop } from './animate.js'
|
|
|
2
2
|
export { sequence } from './sequence.js'
|
|
3
3
|
export type { Step } from './sequence.js'
|
|
4
4
|
export { press } from './press.js'
|
|
5
|
+
export type { Haptic, HapticOptions, HapticStyle } from './haptics.js'
|
|
6
|
+
export { confetti, particles } from './particles.js'
|
|
7
|
+
export type { ConfettiOptions, ParticleOptions } from './particles.js'
|
|
8
|
+
export { shimmer } from './shimmer.js'
|
|
9
|
+
export type { ShimmerOptions } from './shimmer.js'
|
|
10
|
+
export { symbolEffect } from './symbol-effect.js'
|
|
11
|
+
export type {
|
|
12
|
+
SymbolEffect,
|
|
13
|
+
SymbolEffectName,
|
|
14
|
+
SymbolEffectOptions,
|
|
15
|
+
} from './symbol-effect.js'
|
|
5
16
|
export { definePreset, presets } from './presets.js'
|
|
6
17
|
export type { Preset, PresetName, Presets } from './presets.js'
|
|
7
18
|
export { springs } from './transition.js'
|