@rafaelaugusto/jolt 0.1.0-beta.13 → 0.1.0-beta.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/driver/android-gradient.d.ts.map +1 -1
- package/dist/driver/android-gradient.js +11 -1
- package/dist/driver/android-gradient.js.map +1 -1
- package/dist/driver/android-particles.d.ts +6 -0
- package/dist/driver/android-particles.d.ts.map +1 -0
- package/dist/driver/android-particles.js +93 -0
- package/dist/driver/android-particles.js.map +1 -0
- package/dist/driver/ios-gradient.d.ts.map +1 -1
- package/dist/driver/ios-gradient.js +24 -2
- package/dist/driver/ios-gradient.js.map +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +9 -3
- package/dist/engine.js.map +1 -1
- package/dist/gradient-binding.d.ts +38 -0
- package/dist/gradient-binding.d.ts.map +1 -0
- package/dist/gradient-binding.js +128 -0
- package/dist/gradient-binding.js.map +1 -0
- package/dist/gradient.d.ts +19 -4
- package/dist/gradient.d.ts.map +1 -1
- package/dist/gradient.js +331 -70
- package/dist/gradient.js.map +1 -1
- package/dist/haptics.d.ts.map +1 -1
- package/dist/haptics.js +101 -28
- package/dist/haptics.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/motion-scope.d.ts +1 -1
- package/dist/motion-scope.d.ts.map +1 -1
- package/dist/motion-scope.js +8 -1
- package/dist/motion-scope.js.map +1 -1
- package/dist/motion-value.d.ts +17 -0
- package/dist/motion-value.d.ts.map +1 -1
- package/dist/motion-value.js +38 -0
- package/dist/motion-value.js.map +1 -1
- package/dist/native/index.d.ts +1 -0
- package/dist/native/index.d.ts.map +1 -1
- package/dist/native/index.js +1 -0
- package/dist/native/index.js.map +1 -1
- package/dist/particles.d.ts +5 -2
- package/dist/particles.d.ts.map +1 -1
- package/dist/particles.js +16 -10
- package/dist/particles.js.map +1 -1
- package/dist/scene-appearance.d.ts +14 -0
- package/dist/scene-appearance.d.ts.map +1 -0
- package/dist/scene-appearance.js +68 -0
- package/dist/scene-appearance.js.map +1 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/vue/index.d.ts.map +1 -1
- package/dist/vue/index.js +28 -5
- package/dist/vue/index.js.map +1 -1
- package/package.json +1 -1
- package/platforms/android/java/org/rafaelaugusto/jolt/GradientLayer.java +30 -1
- package/platforms/android/java/org/rafaelaugusto/jolt/ParticleEmitter.java +192 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/ParticleSimulation.java +114 -0
- package/platforms/android/proguard-rules.pro +2 -0
- package/src/android-plugin.d.ts +56 -0
- package/src/driver/android-gradient.ts +8 -1
- package/src/driver/android-particles.ts +118 -0
- package/src/driver/ios-gradient.ts +24 -2
- package/src/engine.ts +10 -4
- package/src/gradient-binding.ts +158 -0
- package/src/gradient.ts +376 -91
- package/src/haptics.ts +106 -28
- package/src/index.ts +1 -0
- package/src/ios-plugin.d.ts +7 -0
- package/src/motion-scope.ts +9 -2
- package/src/motion-value.ts +43 -0
- package/src/native/index.ts +7 -0
- package/src/particles.ts +18 -17
- package/src/scene-appearance.ts +92 -0
- package/src/types.ts +1 -1
- package/src/vue/index.ts +29 -5
package/src/haptics.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isIOS, type View } from '@nativescript/core'
|
|
1
|
+
import { isAndroid, isIOS, type View } from '@nativescript/core'
|
|
2
2
|
import type { Driver } from './driver/index.js'
|
|
3
3
|
import type { Track } from './track.js'
|
|
4
4
|
|
|
@@ -39,7 +39,69 @@ export function resolveHaptic(haptic: Haptic | undefined): HapticOptions | undef
|
|
|
39
39
|
|
|
40
40
|
const PREPARE_AHEAD = 100
|
|
41
41
|
const MAX_LATENESS = 100
|
|
42
|
-
|
|
42
|
+
|
|
43
|
+
interface Feedback {
|
|
44
|
+
active(): boolean
|
|
45
|
+
enabled(): boolean
|
|
46
|
+
prepare?(): void
|
|
47
|
+
fire(): void
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function feedbackFor(
|
|
51
|
+
view: View,
|
|
52
|
+
style: HapticStyle,
|
|
53
|
+
driver: Driver,
|
|
54
|
+
): Feedback | undefined {
|
|
55
|
+
if (isAndroid) {
|
|
56
|
+
if (typeof android === 'undefined' || driver.reducedMotion?.()) return
|
|
57
|
+
const native = view.nativeViewProtected as android.view.View | undefined
|
|
58
|
+
if (!native) return
|
|
59
|
+
const sdk = android.os.Build.VERSION.SDK_INT
|
|
60
|
+
const constants = android.view.HapticFeedbackConstants
|
|
61
|
+
const effect =
|
|
62
|
+
style === 'heavy'
|
|
63
|
+
? constants.LONG_PRESS
|
|
64
|
+
: style === 'medium' && sdk >= 23
|
|
65
|
+
? constants.CONTEXT_CLICK
|
|
66
|
+
: style === 'light' && sdk >= 21
|
|
67
|
+
? constants.CLOCK_TICK
|
|
68
|
+
: constants.KEYBOARD_TAP
|
|
69
|
+
return {
|
|
70
|
+
// Animator scale zero must also silence opacity/color feedback when
|
|
71
|
+
// configure({ reducedMotion: 'never' }) keeps a logical track alive.
|
|
72
|
+
enabled: () => !driver.reducedMotion?.(),
|
|
73
|
+
active: () =>
|
|
74
|
+
view.isLoaded &&
|
|
75
|
+
view.nativeViewProtected === native &&
|
|
76
|
+
!!native.getWindowToken() &&
|
|
77
|
+
native.getWindowVisibility() === 0 &&
|
|
78
|
+
native.isShown() &&
|
|
79
|
+
native.hasWindowFocus(),
|
|
80
|
+
// No ignore-setting flags, forced vibration or permission. A false result
|
|
81
|
+
// means the system declined this request; do not retry with a stronger effect.
|
|
82
|
+
fire: () => {
|
|
83
|
+
native.performHapticFeedback(effect)
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (!isIOS || typeof UIImpactFeedbackGenerator === 'undefined') return
|
|
88
|
+
const styles = { light: 0, medium: 1, heavy: 2 } satisfies Record<
|
|
89
|
+
HapticStyle,
|
|
90
|
+
UIImpactFeedbackStyle
|
|
91
|
+
>
|
|
92
|
+
const generator = UIImpactFeedbackGenerator.alloc().initWithStyle(styles[style])
|
|
93
|
+
const native = view.nativeViewProtected as UIView
|
|
94
|
+
return {
|
|
95
|
+
enabled: () => true,
|
|
96
|
+
active: () =>
|
|
97
|
+
view.isLoaded &&
|
|
98
|
+
view.nativeViewProtected === native &&
|
|
99
|
+
!!native.window &&
|
|
100
|
+
UIApplication.sharedApplication.applicationState === 0,
|
|
101
|
+
prepare: () => generator.prepare(),
|
|
102
|
+
fire: () => generator.impactOccurred(),
|
|
103
|
+
}
|
|
104
|
+
}
|
|
43
105
|
|
|
44
106
|
/** Discrete feedback only: animation playback remains entirely native. */
|
|
45
107
|
export function playHaptics(
|
|
@@ -50,7 +112,7 @@ export function playHaptics(
|
|
|
50
112
|
startedAt: number,
|
|
51
113
|
elapsedTime = () => driver.now() - startedAt,
|
|
52
114
|
) {
|
|
53
|
-
if (!
|
|
115
|
+
if (!options) return
|
|
54
116
|
const moving = tracks.filter(
|
|
55
117
|
(track) =>
|
|
56
118
|
track.duration > 0 && track.values.some((value) => value !== track.values[0]),
|
|
@@ -62,25 +124,13 @@ export function playHaptics(
|
|
|
62
124
|
const offsets = options.times
|
|
63
125
|
? options.times.map((time) => delay + (end - delay) * time)
|
|
64
126
|
: [Math.max(...tracks.map((track) => track.arrival))]
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
medium: 1,
|
|
68
|
-
heavy: 2,
|
|
69
|
-
} satisfies Record<HapticStyle, UIImpactFeedbackStyle>
|
|
70
|
-
let generator: UIImpactFeedbackGenerator | undefined =
|
|
71
|
-
UIImpactFeedbackGenerator.alloc().initWithStyle(styles[options.style])
|
|
72
|
-
const native = view.nativeViewProtected as UIView
|
|
127
|
+
let feedback = feedbackFor(view, options.style, driver)
|
|
128
|
+
if (!feedback) return
|
|
73
129
|
let paused = false
|
|
74
130
|
let next = 0
|
|
75
131
|
let cancelPrepare: (() => void) | undefined
|
|
76
132
|
let cancelImpact: (() => void) | undefined
|
|
77
133
|
|
|
78
|
-
const active = () =>
|
|
79
|
-
view.isLoaded &&
|
|
80
|
-
view.nativeViewProtected === native &&
|
|
81
|
-
!!native.window &&
|
|
82
|
-
UIApplication.sharedApplication.applicationState === 0 // UIApplicationState.Active
|
|
83
|
-
|
|
84
134
|
const cancelTimers = () => {
|
|
85
135
|
cancelPrepare?.()
|
|
86
136
|
cancelImpact?.()
|
|
@@ -90,28 +140,54 @@ export function playHaptics(
|
|
|
90
140
|
cancelPrepare?.()
|
|
91
141
|
cancelImpact?.()
|
|
92
142
|
cancelPrepare = cancelImpact = undefined
|
|
93
|
-
|
|
143
|
+
feedback = undefined
|
|
94
144
|
view.off('unloaded', cancel)
|
|
95
145
|
}
|
|
96
146
|
|
|
97
|
-
const fire = (
|
|
98
|
-
const elapsed = elapsedTime()
|
|
147
|
+
const fire = (elapsed = elapsedTime()) => {
|
|
99
148
|
// A busy UI thread can delay timers. Drop stale impacts instead of replaying
|
|
100
149
|
// several old taps at once when it becomes responsive again.
|
|
101
|
-
while (next < offsets.length && offsets[next]! <= elapsed
|
|
150
|
+
while (next < offsets.length && offsets[next]! <= elapsed) {
|
|
102
151
|
const offset = offsets[next++]!
|
|
103
|
-
if (active() && elapsed - offset <= MAX_LATENESS)
|
|
152
|
+
if (feedback?.enabled() && feedback.active() && elapsed - offset <= MAX_LATENESS)
|
|
153
|
+
feedback.fire()
|
|
104
154
|
}
|
|
105
155
|
}
|
|
106
156
|
|
|
107
157
|
const schedule = () => {
|
|
108
158
|
if (paused) return
|
|
109
|
-
if (!
|
|
159
|
+
if (!feedback?.enabled() || next === offsets.length) return cancel()
|
|
160
|
+
if (isAndroid) {
|
|
161
|
+
// Start pending markers together with the visual animators. Chaining a
|
|
162
|
+
// new ValueAnimator from each impact adds another first-frame startup
|
|
163
|
+
// delay, which can wrongly make subsequent points stale at fast scales.
|
|
164
|
+
const markers = new Set<() => void>()
|
|
165
|
+
cancelImpact = () => {
|
|
166
|
+
for (const release of markers) release()
|
|
167
|
+
markers.clear()
|
|
168
|
+
}
|
|
169
|
+
const arm = (index: number) => {
|
|
170
|
+
if (!feedback?.enabled()) return cancel()
|
|
171
|
+
// Native durations are integer milliseconds; zero completes synchronously.
|
|
172
|
+
const remaining = Math.max(1, offsets[index]! - elapsedTime())
|
|
173
|
+
const release = driver.after(remaining, () => {
|
|
174
|
+
markers.delete(release)
|
|
175
|
+
if (!feedback || paused) return
|
|
176
|
+
if (!feedback.enabled()) return cancel()
|
|
177
|
+
fire()
|
|
178
|
+
if (next === offsets.length) cancel()
|
|
179
|
+
else if (next <= index) arm(index)
|
|
180
|
+
})
|
|
181
|
+
markers.add(release)
|
|
182
|
+
}
|
|
183
|
+
for (let index = next; index < offsets.length; index++) arm(index)
|
|
184
|
+
return
|
|
185
|
+
}
|
|
110
186
|
const remaining = offsets[next]! - elapsedTime()
|
|
111
|
-
if (active())
|
|
112
|
-
if (remaining > PREPARE_AHEAD) {
|
|
187
|
+
if (feedback.active()) feedback.prepare?.()
|
|
188
|
+
if (feedback.prepare && remaining > PREPARE_AHEAD) {
|
|
113
189
|
cancelPrepare = driver.after(remaining - PREPARE_AHEAD, () => {
|
|
114
|
-
if (active())
|
|
190
|
+
if (feedback?.active()) feedback.prepare?.()
|
|
115
191
|
})
|
|
116
192
|
}
|
|
117
193
|
cancelImpact = driver.after(Math.max(0, remaining), () => {
|
|
@@ -140,8 +216,10 @@ export function playHaptics(
|
|
|
140
216
|
schedule()
|
|
141
217
|
},
|
|
142
218
|
finish() {
|
|
143
|
-
// Native completion
|
|
144
|
-
|
|
219
|
+
// Native completion proves that the final frame was reached, even when
|
|
220
|
+
// a scaled/resumed clock sample is one frame behind. Earlier impacts are
|
|
221
|
+
// still judged against the end position, so stale points are not replayed.
|
|
222
|
+
fire(Math.max(end, elapsedTime()))
|
|
145
223
|
cancel()
|
|
146
224
|
},
|
|
147
225
|
}
|
package/src/index.ts
CHANGED
package/src/ios-plugin.d.ts
CHANGED
|
@@ -111,4 +111,11 @@ declare class JoltReferenceProperties extends NSObject {
|
|
|
111
111
|
static mapCoordinate(target: NSObject): JoltPropertySink
|
|
112
112
|
static mapCamera(target: NSObject): JoltPropertySink
|
|
113
113
|
static scenePose(target: NSObject): JoltPropertySink
|
|
114
|
+
static sceneLightIntensity(target: NSObject): JoltPropertySink
|
|
115
|
+
static sceneLightColor(target: NSObject): JoltPropertySink
|
|
116
|
+
static sceneMaterialTransparency(target: NSObject): JoltPropertySink
|
|
117
|
+
static sceneMaterialDiffuse(target: NSObject): JoltPropertySink
|
|
118
|
+
static sceneMaterialEmission(target: NSObject): JoltPropertySink
|
|
119
|
+
static sceneAppearanceSinkCount(): number
|
|
120
|
+
static sceneAppearanceWriteCount(): number
|
|
114
121
|
}
|
package/src/motion-scope.ts
CHANGED
|
@@ -41,7 +41,7 @@ export interface MotionScope {
|
|
|
41
41
|
timeline: typeof timeline
|
|
42
42
|
press: typeof press
|
|
43
43
|
drag: typeof drag
|
|
44
|
-
/** Removed with the scope;
|
|
44
|
+
/** Removed with the scope; suspension holds bindings until resume and stops concrete playback. */
|
|
45
45
|
gradient: typeof gradient
|
|
46
46
|
states<
|
|
47
47
|
T extends Record<string, Target | null>,
|
|
@@ -213,7 +213,14 @@ export function motionScope(): MotionScope {
|
|
|
213
213
|
gradient(target, options) {
|
|
214
214
|
assertActive()
|
|
215
215
|
const made = gradient(target, options)
|
|
216
|
-
scope.add({
|
|
216
|
+
scope.add({
|
|
217
|
+
get paused() {
|
|
218
|
+
return made.suspended
|
|
219
|
+
},
|
|
220
|
+
pause: () => made.suspend(),
|
|
221
|
+
play: () => made.resume(),
|
|
222
|
+
dispose: () => made.remove(),
|
|
223
|
+
})
|
|
217
224
|
return made
|
|
218
225
|
},
|
|
219
226
|
states(targets, definitions, options) {
|
package/src/motion-value.ts
CHANGED
|
@@ -409,6 +409,12 @@ class Root extends Derived {
|
|
|
409
409
|
return { property, keyframes: [], transition, track: mapped }
|
|
410
410
|
}
|
|
411
411
|
|
|
412
|
+
/** Capture the current native presentation clock, even if a later command replaces it. */
|
|
413
|
+
captureTime(): () => number {
|
|
414
|
+
const run = this.presentation
|
|
415
|
+
return run ? () => this.runTime(run) : () => 0
|
|
416
|
+
}
|
|
417
|
+
|
|
412
418
|
elapsed(): number {
|
|
413
419
|
return this.presentation ? this.runTime(this.presentation) : 0
|
|
414
420
|
}
|
|
@@ -595,6 +601,9 @@ class PlaybackValue extends Root {
|
|
|
595
601
|
override get paused() {
|
|
596
602
|
return this.playback.read().paused
|
|
597
603
|
}
|
|
604
|
+
override captureTime() {
|
|
605
|
+
return () => this.playback.read().time
|
|
606
|
+
}
|
|
598
607
|
override elapsed() {
|
|
599
608
|
return this.playback.read().time
|
|
600
609
|
}
|
|
@@ -642,6 +651,40 @@ class PlaybackValue extends Root {
|
|
|
642
651
|
}
|
|
643
652
|
}
|
|
644
653
|
|
|
654
|
+
/** Internal compound-binding bridge. One raw snapshot per root keeps sibling mappings coherent. */
|
|
655
|
+
export function valueSource(source: DerivedValue | DerivedColor, color: boolean) {
|
|
656
|
+
if (!(source instanceof Derived) || source.color !== color)
|
|
657
|
+
throw new TypeError(
|
|
658
|
+
`Gradient component needs a ${color ? 'color' : 'numeric'} Jolt value.`,
|
|
659
|
+
)
|
|
660
|
+
const { root, read } = source
|
|
661
|
+
return {
|
|
662
|
+
identity: root,
|
|
663
|
+
read,
|
|
664
|
+
snapshot() {
|
|
665
|
+
return {
|
|
666
|
+
track: root.plan({ property: 'opacity', read: (value) => value } as Binding)
|
|
667
|
+
.track!(),
|
|
668
|
+
elapsed: root.elapsed(),
|
|
669
|
+
time: root.captureTime(),
|
|
670
|
+
running: root.running,
|
|
671
|
+
value: root.raw(),
|
|
672
|
+
}
|
|
673
|
+
},
|
|
674
|
+
subscribe(listener: () => void) {
|
|
675
|
+
root.listeners.add(listener)
|
|
676
|
+
return () => {
|
|
677
|
+
root.listeners.delete(listener)
|
|
678
|
+
}
|
|
679
|
+
},
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/** Treat values as opaque identities in declarative option comparisons. */
|
|
684
|
+
export function isDerived(value: unknown): value is DerivedValue | DerivedColor {
|
|
685
|
+
return value instanceof Derived
|
|
686
|
+
}
|
|
687
|
+
|
|
645
688
|
/** Internal native-adapter bridge. Mappings are sampled at boundaries, never per frame. */
|
|
646
689
|
export function valuePlayback(source: DerivedValue) {
|
|
647
690
|
if (!(source instanceof Derived) || source.color)
|
package/src/native/index.ts
CHANGED
|
@@ -14,3 +14,10 @@ export type {
|
|
|
14
14
|
MapLibreCamera,
|
|
15
15
|
ScenePose,
|
|
16
16
|
} from '../native-properties.js'
|
|
17
|
+
export {
|
|
18
|
+
sceneKitLightIntensity,
|
|
19
|
+
sceneKitLightColor,
|
|
20
|
+
sceneKitMaterialTransparency,
|
|
21
|
+
sceneKitMaterialDiffuse,
|
|
22
|
+
sceneKitMaterialEmission,
|
|
23
|
+
} from '../scene-appearance.js'
|
package/src/particles.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { Application, Color, isIOS, type View } from '@nativescript/core'
|
|
1
|
+
import { Application, Color, isAndroid, isIOS, type View } from '@nativescript/core'
|
|
2
2
|
import { toView } from './animate.js'
|
|
3
3
|
import type { Target } from './types.js'
|
|
4
|
-
import { reducedMotion } from './config.js'
|
|
4
|
+
import { onReducedMotion, reducedMotion } from './config.js'
|
|
5
|
+
import { androidParticles } from './driver/android-particles.js'
|
|
5
6
|
|
|
6
7
|
export interface ConfettiOptions {
|
|
7
8
|
/** Approximate number of particles in the burst. Defaults to 80. */
|
|
@@ -38,7 +39,10 @@ export interface ParticleOptions {
|
|
|
38
39
|
duration?: number
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
type
|
|
42
|
+
export type ParticleSettings = Required<Omit<ParticleOptions, 'duration'>> & {
|
|
43
|
+
duration?: number
|
|
44
|
+
}
|
|
45
|
+
type Settings = ParticleSettings
|
|
42
46
|
const bursts = new WeakMap<View, () => void>()
|
|
43
47
|
const emitters = new WeakMap<View, () => void>()
|
|
44
48
|
const palette = ['#facc15', '#ef4444', '#3b82f6', '#22c55e', '#a855f7']
|
|
@@ -46,7 +50,7 @@ const textures = new Map<Settings['shape'], UIImage>()
|
|
|
46
50
|
const radians = (degrees: number) => (degrees * Math.PI) / 180
|
|
47
51
|
const BURST_DURATION = 200
|
|
48
52
|
|
|
49
|
-
/** Bursts from the target's center into its window.
|
|
53
|
+
/** Bursts from the target's center into its window. Returns immediate cleanup. */
|
|
50
54
|
export function confetti(target: Target, options: ConfettiOptions = {}): () => void {
|
|
51
55
|
const count = number('count', options.count ?? 80, 0, 10000)
|
|
52
56
|
if (!Number.isInteger(count)) throw new RangeError('Particle count must be an integer.')
|
|
@@ -70,7 +74,7 @@ export function confetti(target: Target, options: ConfettiOptions = {}): () => v
|
|
|
70
74
|
return start(target, settings, true)
|
|
71
75
|
}
|
|
72
76
|
|
|
73
|
-
/** Adds a native particle emitter to the target.
|
|
77
|
+
/** Adds a native particle emitter to the target. Returns immediate cleanup. */
|
|
74
78
|
export function particles(target: Target, options: ParticleOptions = {}): () => void {
|
|
75
79
|
return start(target, resolve(options), false)
|
|
76
80
|
}
|
|
@@ -79,7 +83,9 @@ function start(target: Target, settings: Settings, overlay: boolean): () => void
|
|
|
79
83
|
const view = toView(target)
|
|
80
84
|
const running = overlay ? bursts : emitters
|
|
81
85
|
running.get(view)?.()
|
|
82
|
-
if (
|
|
86
|
+
if (settings.rate === 0 || reducedMotion()) return () => {}
|
|
87
|
+
if (isAndroid) return androidParticles(view, settings, overlay, running)
|
|
88
|
+
if (!isIOS) return () => {}
|
|
83
89
|
return new Emitter(view, settings, overlay, running).stop
|
|
84
90
|
}
|
|
85
91
|
|
|
@@ -141,7 +147,7 @@ class Emitter {
|
|
|
141
147
|
private native: UIView | undefined
|
|
142
148
|
private window: UIWindow | undefined
|
|
143
149
|
private timer: ReturnType<typeof setTimeout> | undefined
|
|
144
|
-
private
|
|
150
|
+
private unwatch: (() => void) | undefined
|
|
145
151
|
|
|
146
152
|
constructor(
|
|
147
153
|
private readonly view: View,
|
|
@@ -153,6 +159,8 @@ class Emitter {
|
|
|
153
159
|
view.on('loaded', this.schedule)
|
|
154
160
|
view.on('layoutChanged', this.schedule)
|
|
155
161
|
view.on('unloaded', this.stop)
|
|
162
|
+
Application.on(Application.suspendEvent, this.stop)
|
|
163
|
+
this.unwatch = onReducedMotion(this.motionChanged)
|
|
156
164
|
this.schedule()
|
|
157
165
|
}
|
|
158
166
|
|
|
@@ -162,17 +170,14 @@ class Emitter {
|
|
|
162
170
|
this.view.off('loaded', this.schedule)
|
|
163
171
|
this.view.off('layoutChanged', this.schedule)
|
|
164
172
|
this.view.off('unloaded', this.stop)
|
|
173
|
+
Application.off(Application.suspendEvent, this.stop)
|
|
165
174
|
if (this.running.get(this.view) === this.stop) this.running.delete(this.view)
|
|
166
175
|
if (this.timer !== undefined) clearTimeout(this.timer)
|
|
167
|
-
|
|
168
|
-
Application.ios.removeNotificationObserver(
|
|
169
|
-
this.observer,
|
|
170
|
-
UIAccessibilityReduceMotionStatusDidChangeNotification,
|
|
171
|
-
)
|
|
176
|
+
this.unwatch?.()
|
|
172
177
|
this.layer?.removeAllAnimations()
|
|
173
178
|
this.layer?.removeFromSuperlayer()
|
|
174
179
|
this.layer = this.native = this.window = undefined
|
|
175
|
-
this.timer = this.
|
|
180
|
+
this.timer = this.unwatch = undefined
|
|
176
181
|
}
|
|
177
182
|
|
|
178
183
|
private schedule = () => {
|
|
@@ -232,10 +237,6 @@ class Emitter {
|
|
|
232
237
|
),
|
|
233
238
|
window,
|
|
234
239
|
)
|
|
235
|
-
this.observer = Application.ios.addNotificationObserver(
|
|
236
|
-
UIAccessibilityReduceMotionStatusDidChangeNotification,
|
|
237
|
-
this.motionChanged,
|
|
238
|
-
)
|
|
239
240
|
if (this.settings.duration !== undefined) {
|
|
240
241
|
// The render server stops emission even if JS is blocked. The timer
|
|
241
242
|
// only removes the empty layer after the last particles have expired.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/// <reference types="@nativescript/types-ios/lib/ios/objc-x86_64/objc!SceneKit" preserve="true" />
|
|
2
|
+
import { Color } from '@nativescript/core'
|
|
3
|
+
import { colorMixer, parseColor, type ColorValue } from './color.js'
|
|
4
|
+
import { registerNativeProperty, type NativeProperty } from './native-property.js'
|
|
5
|
+
|
|
6
|
+
const clamp = (n: number) => Math.max(0, Math.min(1, n))
|
|
7
|
+
function scalar(name: string, maximum: number, create: (target: object) => object) {
|
|
8
|
+
return registerNativeProperty<number>({
|
|
9
|
+
name: `jolt.scenekit/${name}`,
|
|
10
|
+
units: [
|
|
11
|
+
name === 'light-intensity'
|
|
12
|
+
? 'lumens (PBR); 1000 = unit multiplier otherwise'
|
|
13
|
+
: 'opacity fraction',
|
|
14
|
+
],
|
|
15
|
+
platforms: ['ios'],
|
|
16
|
+
encode: (value) => [value],
|
|
17
|
+
decode: (values) => values[0]!,
|
|
18
|
+
validate(value) {
|
|
19
|
+
if (!Number.isFinite(value) || value < 0 || value > maximum)
|
|
20
|
+
throw new RangeError(`${name} needs a finite value in 0..${maximum}.`)
|
|
21
|
+
},
|
|
22
|
+
interpolate: (a, b, t) => Math.max(0, Math.min(maximum, a + (b - a) * t)),
|
|
23
|
+
create,
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
function color(name: string, create: (target: object) => object) {
|
|
27
|
+
return registerNativeProperty<ColorValue>({
|
|
28
|
+
name: `jolt.scenekit/${name}`,
|
|
29
|
+
// Premultiply the sampled sRGB channels, so native linear interpolation also handles zero alpha.
|
|
30
|
+
units: [
|
|
31
|
+
'premultiplied sRGB red',
|
|
32
|
+
'premultiplied sRGB green',
|
|
33
|
+
'premultiplied sRGB blue',
|
|
34
|
+
'alpha',
|
|
35
|
+
],
|
|
36
|
+
platforms: ['ios'],
|
|
37
|
+
encode(value) {
|
|
38
|
+
const argb = parseColor(value),
|
|
39
|
+
alpha = (argb >>> 24) / 255
|
|
40
|
+
return [
|
|
41
|
+
(((argb >>> 16) & 255) / 255) * alpha,
|
|
42
|
+
(((argb >>> 8) & 255) / 255) * alpha,
|
|
43
|
+
((argb & 255) / 255) * alpha,
|
|
44
|
+
alpha,
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
decode(values) {
|
|
48
|
+
const alpha = clamp(values[3]!)
|
|
49
|
+
const channel = (i: number) =>
|
|
50
|
+
Math.round(clamp(alpha ? values[i]! / alpha : 0) * 255)
|
|
51
|
+
return new Color(
|
|
52
|
+
((Math.round(alpha * 255) << 24) |
|
|
53
|
+
(channel(0) << 16) |
|
|
54
|
+
(channel(1) << 8) |
|
|
55
|
+
channel(2)) >>>
|
|
56
|
+
0,
|
|
57
|
+
)
|
|
58
|
+
},
|
|
59
|
+
interpolate: (a, b, t) => new Color(colorMixer(parseColor(a), parseColor(b))(t)),
|
|
60
|
+
create,
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** SCNLight intensity, finite and nonnegative. Non-spatial: retains timing under reduced motion. */
|
|
65
|
+
export const sceneKitLightIntensity: (light: SCNLight) => NativeProperty<number> = scalar(
|
|
66
|
+
'light-intensity',
|
|
67
|
+
Infinity,
|
|
68
|
+
(target) => JoltReferenceProperties.sceneLightIntensity(target as NSObject),
|
|
69
|
+
)
|
|
70
|
+
/** Solid UIColor/CGColor light color. Returns a normalized sRGB Color from get(). */
|
|
71
|
+
export const sceneKitLightColor: (light: SCNLight) => NativeProperty<ColorValue> = color(
|
|
72
|
+
'light-color',
|
|
73
|
+
(target) => JoltReferenceProperties.sceneLightColor(target as NSObject),
|
|
74
|
+
)
|
|
75
|
+
/** SCNMaterial transparency in 0..1; does not change transparencyMode/transparent contents. */
|
|
76
|
+
export const sceneKitMaterialTransparency: (
|
|
77
|
+
material: SCNMaterial,
|
|
78
|
+
) => NativeProperty<number> = scalar('material-transparency', 1, (target) =>
|
|
79
|
+
JoltReferenceProperties.sceneMaterialTransparency(target as NSObject),
|
|
80
|
+
)
|
|
81
|
+
/** Solid color diffuse contents only; never converts a texture into a color. */
|
|
82
|
+
export const sceneKitMaterialDiffuse: (
|
|
83
|
+
material: SCNMaterial,
|
|
84
|
+
) => NativeProperty<ColorValue> = color('material-diffuse', (target) =>
|
|
85
|
+
JoltReferenceProperties.sceneMaterialDiffuse(target as NSObject),
|
|
86
|
+
)
|
|
87
|
+
/** Solid color emission contents only; does not make the material illuminate nearby geometry. */
|
|
88
|
+
export const sceneKitMaterialEmission: (
|
|
89
|
+
material: SCNMaterial,
|
|
90
|
+
) => NativeProperty<ColorValue> = color('material-emission', (target) =>
|
|
91
|
+
JoltReferenceProperties.sceneMaterialEmission(target as NSObject),
|
|
92
|
+
)
|
package/src/types.ts
CHANGED
|
@@ -84,7 +84,7 @@ export type AnimateOptions = Transition & {
|
|
|
84
84
|
relative?: boolean
|
|
85
85
|
/** With several targets, delays each one by this much more than the previous. */
|
|
86
86
|
stagger?: number | Stagger
|
|
87
|
-
/**
|
|
87
|
+
/** Native haptic feedback at arrival, or at explicit points in a finite animation. */
|
|
88
88
|
haptic?: Haptic
|
|
89
89
|
/**
|
|
90
90
|
* Plays on a named layer, on top of the property's other animations instead of replacing
|
package/src/vue/index.ts
CHANGED
|
@@ -40,6 +40,8 @@ import {
|
|
|
40
40
|
type GradientOptions,
|
|
41
41
|
type Transition,
|
|
42
42
|
} from '../index.js'
|
|
43
|
+
import { isDerived } from '../motion-value.js'
|
|
44
|
+
import { prepareGradient, type GradientValues } from '../gradient.js'
|
|
43
45
|
import { currentKeyboard } from '../keyboard.js'
|
|
44
46
|
|
|
45
47
|
type Host = NSVElement
|
|
@@ -70,7 +72,10 @@ export interface TransitionProps {
|
|
|
70
72
|
const animations = new WeakMap<object, PlaybackAnimation>()
|
|
71
73
|
const scopes = new WeakMap<object, MotionScope>()
|
|
72
74
|
const removers = new WeakMap<object, () => void>()
|
|
73
|
-
const gradients = new WeakMap<
|
|
75
|
+
const gradients = new WeakMap<
|
|
76
|
+
object,
|
|
77
|
+
{ gradient: Gradient; colors: number; release?: () => void }
|
|
78
|
+
>()
|
|
74
79
|
|
|
75
80
|
function split<T>(
|
|
76
81
|
value: T | readonly [T, Options?],
|
|
@@ -82,6 +87,7 @@ function split<T>(
|
|
|
82
87
|
|
|
83
88
|
function same(a: unknown, b: unknown): boolean {
|
|
84
89
|
if (Object.is(a, b)) return true
|
|
90
|
+
if (isDerived(a) || isDerived(b)) return false
|
|
85
91
|
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null)
|
|
86
92
|
return false
|
|
87
93
|
if (Array.isArray(a) !== Array.isArray(b)) return false
|
|
@@ -161,17 +167,35 @@ function paint(el: Host, value: GradientValue, animated: boolean) {
|
|
|
161
167
|
(Transition | SpringName)?,
|
|
162
168
|
]
|
|
163
169
|
const current = gradients.get(el)
|
|
170
|
+
prepareGradient(options)
|
|
164
171
|
const colors = options.colors.length
|
|
172
|
+
const hasBindings = [
|
|
173
|
+
options.angle,
|
|
174
|
+
options.radius,
|
|
175
|
+
options.center?.x,
|
|
176
|
+
options.center?.y,
|
|
177
|
+
...options.colors,
|
|
178
|
+
...(options.stops ?? []),
|
|
179
|
+
].some(isDerived)
|
|
165
180
|
if (current && current.gradient.type === options.type) {
|
|
166
181
|
const { type, ...values } = options
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
else
|
|
182
|
+
const previous = current.release
|
|
183
|
+
if (hasBindings) current.release = current.gradient.bind(values)
|
|
184
|
+
else {
|
|
185
|
+
if (animated && current.colors === colors)
|
|
186
|
+
current.gradient.animate(values as GradientValues, transition)
|
|
187
|
+
else current.gradient.set(values as GradientValues)
|
|
188
|
+
current.release = undefined
|
|
189
|
+
}
|
|
190
|
+
previous?.()
|
|
170
191
|
current.colors = colors
|
|
171
192
|
return
|
|
172
193
|
}
|
|
173
194
|
current?.gradient.remove()
|
|
174
|
-
|
|
195
|
+
// Track directive ownership explicitly, including bindings omitted on a later update.
|
|
196
|
+
const made = gradient(el, options)
|
|
197
|
+
const release = hasBindings ? made.bind(options) : undefined
|
|
198
|
+
gradients.set(el, { gradient: made, colors, release })
|
|
175
199
|
}
|
|
176
200
|
|
|
177
201
|
/**
|