@rafaelaugusto/jolt 0.1.0-beta.13 → 0.1.0-beta.15
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 +14 -9
- 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 +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/motion-scope.d.ts +3 -1
- package/dist/motion-scope.d.ts.map +1 -1
- package/dist/motion-scope.js +14 -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 +2 -0
- package/dist/native/index.d.ts.map +1 -1
- package/dist/native/index.js +2 -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/scene-camera.d.ts +5 -0
- package/dist/scene-camera.d.ts.map +1 -0
- package/dist/scene-camera.js +23 -0
- package/dist/scene-camera.js.map +1 -0
- package/dist/size-layout.d.ts +19 -0
- package/dist/size-layout.d.ts.map +1 -0
- package/dist/size-layout.js +216 -0
- package/dist/size-layout.js.map +1 -0
- package/dist/size.d.ts +19 -0
- package/dist/size.d.ts.map +1 -0
- package/dist/size.js +251 -0
- package/dist/size.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 +3 -0
- package/src/ios-plugin.d.ts +10 -0
- package/src/motion-scope.ts +23 -2
- package/src/motion-value.ts +43 -0
- package/src/native/index.ts +8 -0
- package/src/particles.ts +18 -17
- package/src/scene-appearance.ts +92 -0
- package/src/scene-camera.ts +30 -0
- package/src/size-layout.ts +237 -0
- package/src/size.ts +302 -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
|
@@ -69,6 +69,7 @@ export type {
|
|
|
69
69
|
GradientOptions,
|
|
70
70
|
GradientType,
|
|
71
71
|
GradientValues,
|
|
72
|
+
GradientBindings,
|
|
72
73
|
} from './gradient.js'
|
|
73
74
|
export { configure, onReducedMotion, reducedMotion } from './config.js'
|
|
74
75
|
export type { JoltConfig } from './config.js'
|
|
@@ -103,3 +104,5 @@ export type {
|
|
|
103
104
|
|
|
104
105
|
export { animateLayout } from './layout.js'
|
|
105
106
|
export type { LayoutOptions } from './layout.js'
|
|
107
|
+
export { animateSize } from './size.js'
|
|
108
|
+
export type { SizeAnimation, SizeDimensions, SizeOptions } from './size.js'
|
package/src/ios-plugin.d.ts
CHANGED
|
@@ -108,7 +108,17 @@ declare class JoltPropertyPlayer extends NSObject {
|
|
|
108
108
|
cancel(): void
|
|
109
109
|
}
|
|
110
110
|
declare class JoltReferenceProperties extends NSObject {
|
|
111
|
+
static sceneFieldOfView(camera: NSObject): JoltPropertySink
|
|
112
|
+
static sceneCameraSinkCount(): number
|
|
113
|
+
static sceneCameraWriteCount(): number
|
|
111
114
|
static mapCoordinate(target: NSObject): JoltPropertySink
|
|
112
115
|
static mapCamera(target: NSObject): JoltPropertySink
|
|
113
116
|
static scenePose(target: NSObject): JoltPropertySink
|
|
117
|
+
static sceneLightIntensity(target: NSObject): JoltPropertySink
|
|
118
|
+
static sceneLightColor(target: NSObject): JoltPropertySink
|
|
119
|
+
static sceneMaterialTransparency(target: NSObject): JoltPropertySink
|
|
120
|
+
static sceneMaterialDiffuse(target: NSObject): JoltPropertySink
|
|
121
|
+
static sceneMaterialEmission(target: NSObject): JoltPropertySink
|
|
122
|
+
static sceneAppearanceSinkCount(): number
|
|
123
|
+
static sceneAppearanceWriteCount(): number
|
|
114
124
|
}
|
package/src/motion-scope.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { press } from './press.js'
|
|
|
3
3
|
import { drag } from './drag.js'
|
|
4
4
|
import { gradient } from './gradient.js'
|
|
5
5
|
import { animateLayout, type LayoutOptions } from './layout.js'
|
|
6
|
+
import { animateSize, type SizeDimensions, type SizeOptions } from './size.js'
|
|
6
7
|
import type { SpringName } from './transition.js'
|
|
7
8
|
import { sequence, timeline } from './sequence.js'
|
|
8
9
|
import {
|
|
@@ -35,13 +36,14 @@ export interface MotionScope {
|
|
|
35
36
|
/** Owned handles/subscriptions, including suspended subscription factories. */
|
|
36
37
|
readonly size: number
|
|
37
38
|
animateLayout: typeof animateLayout
|
|
39
|
+
animateSize: typeof animateSize
|
|
38
40
|
animate: typeof animate
|
|
39
41
|
decay: typeof decay
|
|
40
42
|
sequence: typeof sequence
|
|
41
43
|
timeline: typeof timeline
|
|
42
44
|
press: typeof press
|
|
43
45
|
drag: typeof drag
|
|
44
|
-
/** Removed with the scope;
|
|
46
|
+
/** Removed with the scope; suspension holds bindings until resume and stops concrete playback. */
|
|
45
47
|
gradient: typeof gradient
|
|
46
48
|
states<
|
|
47
49
|
T extends Record<string, Target | null>,
|
|
@@ -195,6 +197,18 @@ export function motionScope(): MotionScope {
|
|
|
195
197
|
},
|
|
196
198
|
animate: ((...args: Parameters<typeof animate>) =>
|
|
197
199
|
startOwned(() => animate(...args))) as typeof animate,
|
|
200
|
+
animateSize(
|
|
201
|
+
target: Target,
|
|
202
|
+
dimensions: SizeDimensions,
|
|
203
|
+
changeOrOptions?: (() => void) | SizeOptions | SpringName,
|
|
204
|
+
options?: SizeOptions | SpringName,
|
|
205
|
+
) {
|
|
206
|
+
return startOwned(() =>
|
|
207
|
+
typeof changeOrOptions === 'function'
|
|
208
|
+
? animateSize(target, dimensions, changeOrOptions, options)
|
|
209
|
+
: animateSize(target, dimensions, changeOrOptions),
|
|
210
|
+
)
|
|
211
|
+
},
|
|
198
212
|
decay(...args) {
|
|
199
213
|
return startOwned(() => decay(...args))
|
|
200
214
|
},
|
|
@@ -213,7 +227,14 @@ export function motionScope(): MotionScope {
|
|
|
213
227
|
gradient(target, options) {
|
|
214
228
|
assertActive()
|
|
215
229
|
const made = gradient(target, options)
|
|
216
|
-
scope.add({
|
|
230
|
+
scope.add({
|
|
231
|
+
get paused() {
|
|
232
|
+
return made.suspended
|
|
233
|
+
},
|
|
234
|
+
pause: () => made.suspend(),
|
|
235
|
+
play: () => made.resume(),
|
|
236
|
+
dispose: () => made.remove(),
|
|
237
|
+
})
|
|
217
238
|
return made
|
|
218
239
|
},
|
|
219
240
|
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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { registerNativeProperty, checkNativeProperty } from '../native-property.js'
|
|
2
2
|
export type { NativeProperty, NativePropertyDefinition } from '../native-property.js'
|
|
3
3
|
export type { NativeSink } from '../native-property-driver.js'
|
|
4
|
+
export { sceneKitFieldOfView } from '../scene-camera.js'
|
|
4
5
|
export {
|
|
5
6
|
mapKitCoordinate,
|
|
6
7
|
mapKitCamera,
|
|
@@ -14,3 +15,10 @@ export type {
|
|
|
14
15
|
MapLibreCamera,
|
|
15
16
|
ScenePose,
|
|
16
17
|
} from '../native-properties.js'
|
|
18
|
+
export {
|
|
19
|
+
sceneKitLightIntensity,
|
|
20
|
+
sceneKitLightColor,
|
|
21
|
+
sceneKitMaterialTransparency,
|
|
22
|
+
sceneKitMaterialDiffuse,
|
|
23
|
+
sceneKitMaterialEmission,
|
|
24
|
+
} 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
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/// <reference types="@nativescript/types-ios/lib/ios/objc-x86_64/objc!SceneKit" preserve="true" />
|
|
2
|
+
import { registerNativeProperty, type NativeProperty } from './native-property.js'
|
|
3
|
+
|
|
4
|
+
/** Perspective SCNCamera field of view in degrees (1..179), on its projectionDirection axis. */
|
|
5
|
+
export const sceneKitFieldOfView: (camera: SCNCamera) => NativeProperty<number> =
|
|
6
|
+
registerNativeProperty<number>({
|
|
7
|
+
name: 'jolt.scenekit/field-of-view',
|
|
8
|
+
units: ['degrees on the camera projectionDirection axis'],
|
|
9
|
+
platforms: ['ios'],
|
|
10
|
+
spatial: true,
|
|
11
|
+
encode: (value) => [value],
|
|
12
|
+
decode: (values) => values[0]!,
|
|
13
|
+
validate(value) {
|
|
14
|
+
if (!Number.isFinite(value) || value < 1 || value > 179)
|
|
15
|
+
throw new RangeError(
|
|
16
|
+
'SceneKit field of view needs a finite angle in 1..179 degrees.',
|
|
17
|
+
)
|
|
18
|
+
},
|
|
19
|
+
interpolate: (a, b, t) => Math.max(1, Math.min(179, a + (b - a) * t)),
|
|
20
|
+
create(target) {
|
|
21
|
+
if (
|
|
22
|
+
typeof JoltReferenceProperties === 'undefined' ||
|
|
23
|
+
typeof JoltReferenceProperties.sceneFieldOfView !== 'function'
|
|
24
|
+
)
|
|
25
|
+
throw new Error(
|
|
26
|
+
'SceneKit field of view requires an updated @rafaelaugusto/jolt-native plugin. Update both packages and rebuild the native app.',
|
|
27
|
+
)
|
|
28
|
+
return JoltReferenceProperties.sceneFieldOfView(target as NSObject)
|
|
29
|
+
},
|
|
30
|
+
})
|