@rafaelaugusto/jolt 0.1.0-beta.11 → 0.1.0-beta.13
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 +42 -8
- package/dist/animate.d.ts +15 -3
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +52 -34
- package/dist/animate.js.map +1 -1
- package/dist/drag.d.ts +17 -3
- package/dist/drag.d.ts.map +1 -1
- package/dist/drag.js +34 -11
- package/dist/drag.js.map +1 -1
- package/dist/driver/android.d.ts.map +1 -1
- package/dist/driver/android.js +2 -0
- package/dist/driver/android.js.map +1 -1
- package/dist/driver/frame.d.ts +3 -0
- package/dist/driver/frame.d.ts.map +1 -0
- package/dist/driver/frame.js +7 -0
- package/dist/driver/frame.js.map +1 -0
- package/dist/driver/index.d.ts +2 -0
- package/dist/driver/index.d.ts.map +1 -1
- package/dist/driver/index.js.map +1 -1
- package/dist/driver/ios.d.ts.map +1 -1
- package/dist/driver/ios.js +2 -0
- package/dist/driver/ios.js.map +1 -1
- package/dist/easing.d.ts +1 -1
- package/dist/easing.d.ts.map +1 -1
- package/dist/easing.js +11 -0
- package/dist/easing.js.map +1 -1
- package/dist/engine.d.ts +20 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +166 -38
- package/dist/engine.js.map +1 -1
- package/dist/gradient.d.ts.map +1 -1
- package/dist/gradient.js +10 -4
- package/dist/gradient.js.map +1 -1
- package/dist/image-effect.d.ts +1 -0
- package/dist/image-effect.d.ts.map +1 -1
- package/dist/image-effect.js +10 -2
- package/dist/image-effect.js.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/keyboard.d.ts.map +1 -1
- package/dist/keyboard.js +63 -10
- package/dist/keyboard.js.map +1 -1
- package/dist/mixer.d.ts +49 -0
- package/dist/mixer.d.ts.map +1 -0
- package/dist/mixer.js +238 -0
- package/dist/mixer.js.map +1 -0
- package/dist/motion-scope.d.ts +6 -2
- package/dist/motion-scope.d.ts.map +1 -1
- package/dist/motion-scope.js +12 -4
- package/dist/motion-scope.js.map +1 -1
- package/dist/motion-states.d.ts +10 -5
- package/dist/motion-states.d.ts.map +1 -1
- package/dist/motion-states.js +55 -22
- package/dist/motion-states.js.map +1 -1
- package/dist/motion-value.d.ts +152 -0
- package/dist/motion-value.d.ts.map +1 -0
- package/dist/motion-value.js +635 -0
- package/dist/motion-value.js.map +1 -0
- package/dist/native/index.d.ts +6 -0
- package/dist/native/index.d.ts.map +1 -0
- package/dist/native/index.js +3 -0
- package/dist/native/index.js.map +1 -0
- package/dist/native-properties.d.ts +28 -0
- package/dist/native-properties.d.ts.map +1 -0
- package/dist/native-properties.js +154 -0
- package/dist/native-properties.js.map +1 -0
- package/dist/native-property-driver.d.ts +19 -0
- package/dist/native-property-driver.d.ts.map +1 -0
- package/dist/native-property-driver.js +69 -0
- package/dist/native-property-driver.js.map +1 -0
- package/dist/native-property.d.ts +35 -0
- package/dist/native-property.d.ts.map +1 -0
- package/dist/native-property.js +277 -0
- package/dist/native-property.js.map +1 -0
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +2 -2
- package/dist/path.js.map +1 -1
- package/dist/playback-group.d.ts.map +1 -1
- package/dist/playback-group.js +17 -1
- package/dist/playback-group.js.map +1 -1
- package/dist/playback.d.ts +15 -0
- package/dist/playback.d.ts.map +1 -1
- package/dist/playback.js +23 -0
- package/dist/playback.js.map +1 -1
- package/dist/sequence.d.ts +24 -0
- package/dist/sequence.d.ts.map +1 -1
- package/dist/sequence.js +181 -19
- package/dist/sequence.js.map +1 -1
- package/dist/timed-animation.d.ts +38 -0
- package/dist/timed-animation.d.ts.map +1 -0
- package/dist/timed-animation.js +177 -0
- package/dist/timed-animation.js.map +1 -0
- package/dist/track.d.ts.map +1 -1
- package/dist/track.js +31 -12
- package/dist/track.js.map +1 -1
- package/dist/transition.d.ts +15 -4
- package/dist/transition.d.ts.map +1 -1
- package/dist/transition.js +46 -7
- package/dist/transition.js.map +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/value-observers.d.ts +19 -0
- package/dist/value-observers.d.ts.map +1 -0
- package/dist/value-observers.js +111 -0
- package/dist/value-observers.js.map +1 -0
- package/dist/value-track.d.ts +13 -0
- package/dist/value-track.d.ts.map +1 -0
- package/dist/value-track.js +59 -0
- package/dist/value-track.js.map +1 -0
- package/dist/vue/index.d.ts +28 -2
- package/dist/vue/index.d.ts.map +1 -1
- package/dist/vue/index.js +78 -2
- package/dist/vue/index.js.map +1 -1
- package/package.json +6 -2
- package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFrame.java +53 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFrameFollower.java +57 -0
- package/src/android-plugin.d.ts +64 -0
- package/src/animate.ts +106 -17
- package/src/drag.ts +67 -21
- package/src/driver/android.ts +2 -0
- package/src/driver/frame.ts +10 -0
- package/src/driver/index.ts +2 -0
- package/src/driver/ios.ts +2 -0
- package/src/easing.ts +14 -0
- package/src/engine.ts +181 -55
- package/src/gradient.ts +10 -4
- package/src/image-effect.ts +10 -2
- package/src/index.ts +23 -1
- package/src/ios-plugin.d.ts +33 -0
- package/src/keyboard.ts +83 -18
- package/src/mixer.ts +264 -0
- package/src/motion-scope.ts +17 -5
- package/src/motion-states.ts +79 -29
- package/src/motion-value.ts +795 -0
- package/src/native/index.ts +16 -0
- package/src/native-properties.ts +188 -0
- package/src/native-property-driver.ts +123 -0
- package/src/native-property.ts +353 -0
- package/src/path.ts +2 -1
- package/src/playback-group.ts +17 -1
- package/src/playback.ts +41 -0
- package/src/sequence.ts +209 -20
- package/src/timed-animation.ts +192 -0
- package/src/track.ts +37 -12
- package/src/transition.ts +69 -10
- package/src/types.ts +5 -0
- package/src/value-observers.ts +133 -0
- package/src/value-track.ts +71 -0
- package/src/vue/index.ts +98 -1
package/src/track.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { solveDecay } from './decay.js'
|
|
|
4
4
|
import { solveDragRelease } from './drag-physics.js'
|
|
5
5
|
import { precision, type Property } from './properties.js'
|
|
6
6
|
import { solveSpring, springPhysics } from './spring.js'
|
|
7
|
-
import type { Curve, ResolvedTransition } from './transition.js'
|
|
7
|
+
import type { Curve, RepeatDelayRange, ResolvedTransition } from './transition.js'
|
|
8
8
|
import { colorMixer, isColorProperty, type ColorProperty } from './color.js'
|
|
9
9
|
import { reducedMotion, timeScale } from './config.js'
|
|
10
10
|
|
|
@@ -74,7 +74,9 @@ export function createTrack(
|
|
|
74
74
|
played,
|
|
75
75
|
transition.repeat,
|
|
76
76
|
transition.reverse,
|
|
77
|
-
|
|
77
|
+
transition.repeatDelay ?? 0,
|
|
78
|
+
slowdown,
|
|
79
|
+
transition.seed,
|
|
78
80
|
)
|
|
79
81
|
const { duration } = motion
|
|
80
82
|
const first = motion.values[0]!
|
|
@@ -222,32 +224,44 @@ function colorTrack(
|
|
|
222
224
|
}
|
|
223
225
|
}
|
|
224
226
|
|
|
227
|
+
/** An endless loop with drawn pauses plays this many of them, then starts over. */
|
|
228
|
+
const DRAWN_CYCLES = 16
|
|
229
|
+
|
|
225
230
|
/**
|
|
226
|
-
* Holds the end value for `
|
|
231
|
+
* Holds the end value for `delay` ms before each repeat, inside the keyframes, so both
|
|
227
232
|
* platforms play it natively. Finite repeats are laid out one after another, with no
|
|
228
|
-
* wait after the last; endless ones loop a cycle that waits at each turn
|
|
233
|
+
* wait after the last; endless ones loop a cycle that waits at each turn,
|
|
234
|
+
* or DRAWN_CYCLES cycles when the waits are drawn from a range.
|
|
229
235
|
*/
|
|
230
236
|
function pauseBetweenRepeats(
|
|
231
237
|
motion: Motion,
|
|
232
238
|
repeat: number,
|
|
233
239
|
reverse: boolean,
|
|
234
|
-
|
|
240
|
+
delay: number | RepeatDelayRange,
|
|
241
|
+
slowdown: number,
|
|
242
|
+
seed = Math.random(),
|
|
235
243
|
): { motion: Motion; repeat: number; reverse: boolean } {
|
|
236
|
-
|
|
244
|
+
const [shortest, longest] = typeof delay === 'number' ? [delay, delay] : delay
|
|
245
|
+
if (longest <= 0 || repeat <= 0 || motion.duration === 0)
|
|
237
246
|
return { motion, repeat, reverse }
|
|
247
|
+
const draw = random(seed)
|
|
248
|
+
const pause = () => (shortest + draw() * (longest - shortest)) * slowdown
|
|
238
249
|
type Segment = { kind: 'forward' | 'backward' | 'hold'; duration: number }
|
|
239
250
|
const play = (backward: boolean): Segment => ({
|
|
240
251
|
kind: backward ? 'backward' : 'forward',
|
|
241
252
|
duration: motion.duration,
|
|
242
253
|
})
|
|
243
|
-
const hold: Segment
|
|
254
|
+
const hold = (): Segment => ({ kind: 'hold', duration: pause() })
|
|
244
255
|
const segments: Segment[] = []
|
|
245
256
|
if (Number.isFinite(repeat))
|
|
246
257
|
for (let i = 0; i <= repeat; i++) {
|
|
247
|
-
if (i > 0) segments.push(hold)
|
|
258
|
+
if (i > 0) segments.push(hold())
|
|
248
259
|
segments.push(play(reverse && i % 2 === 1))
|
|
249
260
|
}
|
|
250
|
-
else
|
|
261
|
+
else {
|
|
262
|
+
const cycles = shortest === longest ? (reverse ? 2 : 1) : DRAWN_CYCLES
|
|
263
|
+
for (let i = 0; i < cycles; i++) segments.push(play(reverse && i % 2 === 1), hold())
|
|
264
|
+
}
|
|
251
265
|
|
|
252
266
|
const total = segments.reduce((sum, segment) => sum + segment.duration, 0)
|
|
253
267
|
const starts = segments.map((_, i) =>
|
|
@@ -260,8 +274,6 @@ function pauseBetweenRepeats(
|
|
|
260
274
|
if (segment.kind === 'backward') return motion.values[0]!
|
|
261
275
|
return endOf(i - 1)
|
|
262
276
|
}
|
|
263
|
-
// A loop jumps back to its start at the end of the hold, not across it.
|
|
264
|
-
const jump = Math.min(1, pause / 10)
|
|
265
277
|
|
|
266
278
|
const times: number[] = []
|
|
267
279
|
const values: number[] = []
|
|
@@ -273,7 +285,9 @@ function pauseBetweenRepeats(
|
|
|
273
285
|
const jumps =
|
|
274
286
|
next !== undefined &&
|
|
275
287
|
(next.kind === 'forward' ? motion.values[0] : motion.values[last]) !== held
|
|
276
|
-
|
|
288
|
+
// A loop jumps back to its start at the end of the hold, not across it.
|
|
289
|
+
const jump = jumps ? Math.min(1, segment.duration / 10) : 0
|
|
290
|
+
times.push((start + segment.duration - jump) / total)
|
|
277
291
|
values.push(held)
|
|
278
292
|
return
|
|
279
293
|
}
|
|
@@ -319,6 +333,17 @@ function pauseBetweenRepeats(
|
|
|
319
333
|
}
|
|
320
334
|
}
|
|
321
335
|
|
|
336
|
+
/** Mulberry32: the same seed draws the same pauses for every track of one animation. */
|
|
337
|
+
function random(seed: number) {
|
|
338
|
+
let state = Math.floor(seed * 2 ** 32) >>> 0
|
|
339
|
+
return () => {
|
|
340
|
+
state = (state + 0x6d2b79f5) >>> 0
|
|
341
|
+
let t = Math.imul(state ^ (state >>> 15), state | 1)
|
|
342
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
|
|
343
|
+
return ((t ^ (t >>> 14)) >>> 0) / 2 ** 32
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
322
347
|
/** Plays the same trajectory over a longer or shorter time. */
|
|
323
348
|
function slow(motion: Motion, factor: number): Motion {
|
|
324
349
|
if (factor === 1) return motion
|
package/src/transition.ts
CHANGED
|
@@ -12,10 +12,15 @@ export interface Timing {
|
|
|
12
12
|
repeat?: number
|
|
13
13
|
/** `reverse` plays every other repeat backwards. */
|
|
14
14
|
repeatType?: 'loop' | 'reverse'
|
|
15
|
-
/**
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Milliseconds to hold the end value before each repeat. The first play doesn't wait.
|
|
17
|
+
* `[min, max]` draws a new pause for each repeat.
|
|
18
|
+
*/
|
|
19
|
+
repeatDelay?: number | RepeatDelayRange
|
|
17
20
|
}
|
|
18
21
|
|
|
22
|
+
export type RepeatDelayRange = readonly [min: number, max: number]
|
|
23
|
+
|
|
19
24
|
export interface Tween extends Timing {
|
|
20
25
|
type?: 'tween'
|
|
21
26
|
/** Milliseconds. */
|
|
@@ -107,7 +112,9 @@ export interface ResolvedTransition {
|
|
|
107
112
|
delay: number
|
|
108
113
|
repeat: number
|
|
109
114
|
reverse: boolean
|
|
110
|
-
repeatDelay?: number
|
|
115
|
+
repeatDelay?: number | RepeatDelayRange
|
|
116
|
+
/** Draws the pauses of a `repeatDelay` range; tracks of one animation share it. */
|
|
117
|
+
seed?: number
|
|
111
118
|
}
|
|
112
119
|
|
|
113
120
|
const springKeys = ['stiffness', 'damping', 'mass', 'bounce', 'velocity'] as const
|
|
@@ -137,11 +144,53 @@ export function timingOf(transition: Transition): Timing {
|
|
|
137
144
|
return timing
|
|
138
145
|
}
|
|
139
146
|
|
|
140
|
-
|
|
147
|
+
const isSpring = (transition: Transition) =>
|
|
148
|
+
transition.type === 'spring' ||
|
|
149
|
+
springKeys.some((key) => (transition as Spring)[key] !== undefined)
|
|
150
|
+
|
|
151
|
+
/** Curve keys a merged transition took from the main one rather than from its override. */
|
|
152
|
+
const inherited = new WeakMap<Transition, ReadonlySet<string>>()
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Timing merges key by key. Over a tween, a curve that stays a tween merges key by key too;
|
|
156
|
+
* otherwise the curve is taken whole from whichever side has one.
|
|
157
|
+
*/
|
|
141
158
|
export function mergeTransitions(base: Transition, override: Transition): Transition {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
159
|
+
if (!hasCurve(override)) {
|
|
160
|
+
const merged = { ...base, ...timingOf(override) }
|
|
161
|
+
const taken = inherited.get(base)
|
|
162
|
+
if (taken) inherited.set(merged, taken)
|
|
163
|
+
return merged
|
|
164
|
+
}
|
|
165
|
+
if (!hasCurve(base) || isSpring(base) || isSpring(override))
|
|
166
|
+
return { ...timingOf(base), ...override }
|
|
167
|
+
const merged = { ...base, ...override } as Transition
|
|
168
|
+
const own = override as Record<string, unknown>
|
|
169
|
+
inherited.set(
|
|
170
|
+
merged,
|
|
171
|
+
new Set(
|
|
172
|
+
curveKeys.filter(
|
|
173
|
+
(key) =>
|
|
174
|
+
(base as Record<string, unknown>)[key] !== undefined && own[key] === undefined,
|
|
175
|
+
),
|
|
176
|
+
),
|
|
177
|
+
)
|
|
178
|
+
return merged
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Drops inherited `times` and per-segment easings that don't fit this property's keyframes. */
|
|
182
|
+
export function fitTransition(transition: Transition, keyframes: number): Transition {
|
|
183
|
+
const taken = inherited.get(transition)
|
|
184
|
+
if (!taken) return transition
|
|
185
|
+
const tween = { ...transition } as Tween
|
|
186
|
+
const segments = (ease: Tween['ease']) =>
|
|
187
|
+
ease !== undefined && isEasingList(ease) ? ease.length : undefined
|
|
188
|
+
if (taken.has('times') && tween.times && tween.times.length !== keyframes)
|
|
189
|
+
delete tween.times
|
|
190
|
+
if (taken.has('ease') && (segments(tween.ease) ?? keyframes - 1) !== keyframes - 1)
|
|
191
|
+
delete tween.ease
|
|
192
|
+
if (taken.has('easeTrack') && segments(tween.ease) !== undefined) delete tween.easeTrack
|
|
193
|
+
return tween
|
|
145
194
|
}
|
|
146
195
|
|
|
147
196
|
export function resolveTransition(
|
|
@@ -167,13 +216,23 @@ export function resolveTransition(
|
|
|
167
216
|
if (repeat < 0 || Number.isNaN(repeat))
|
|
168
217
|
throw new RangeError(`Invalid repeat: ${repeat}.`)
|
|
169
218
|
const repeatDelay = transition.repeatDelay ?? 0
|
|
170
|
-
|
|
171
|
-
|
|
219
|
+
const [shortest, longest] =
|
|
220
|
+
typeof repeatDelay === 'number' ? [repeatDelay, repeatDelay] : repeatDelay
|
|
221
|
+
if (
|
|
222
|
+
(typeof repeatDelay !== 'number' &&
|
|
223
|
+
(!Array.isArray(repeatDelay) || repeatDelay.length !== 2)) ||
|
|
224
|
+
!Number.isFinite(shortest) ||
|
|
225
|
+
!Number.isFinite(longest) ||
|
|
226
|
+
shortest! < 0 ||
|
|
227
|
+
shortest! > longest!
|
|
228
|
+
)
|
|
229
|
+
throw new RangeError(`Invalid repeat delay: ${JSON.stringify(repeatDelay)}.`)
|
|
172
230
|
const timing = {
|
|
173
231
|
delay,
|
|
174
232
|
repeat,
|
|
175
233
|
reverse: transition.repeatType === 'reverse',
|
|
176
|
-
...(
|
|
234
|
+
...(longest! > 0 && repeat > 0 && { repeatDelay }),
|
|
235
|
+
...(longest! > shortest! && repeat > 0 && { seed: Math.random() }),
|
|
177
236
|
}
|
|
178
237
|
|
|
179
238
|
if (type === 'spring') {
|
package/src/types.ts
CHANGED
|
@@ -86,4 +86,9 @@ export type AnimateOptions = Transition & {
|
|
|
86
86
|
stagger?: number | Stagger
|
|
87
87
|
/** iOS impact feedback at arrival, or at explicit points in a finite animation. */
|
|
88
88
|
haptic?: Haptic
|
|
89
|
+
/**
|
|
90
|
+
* Plays on a named layer, on top of the property's other animations instead of replacing
|
|
91
|
+
* them. Translations and rotations add; scale and opacity multiply.
|
|
92
|
+
*/
|
|
93
|
+
layer?: string
|
|
89
94
|
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { Application, Color, Trace } from '@nativescript/core'
|
|
2
|
+
import { getDriver } from './driver/index.js'
|
|
3
|
+
|
|
4
|
+
export interface SubscribeOptions {
|
|
5
|
+
/** Deliver the current value synchronously when subscribing. Defaults to true. */
|
|
6
|
+
immediate?: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** `time` is milliseconds on the native animation clock, shared by a notification batch. */
|
|
10
|
+
export type ValueListener<T> = (value: T, time: number) => void
|
|
11
|
+
|
|
12
|
+
interface Observer {
|
|
13
|
+
read(value: number): number
|
|
14
|
+
color: boolean
|
|
15
|
+
listener: ValueListener<number | Color>
|
|
16
|
+
previous: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Only sources that need a frame live here. Idle subscriptions retain no global root reference.
|
|
20
|
+
const pending = new Set<ValueObservers>()
|
|
21
|
+
let cancelFrame: (() => void) | undefined
|
|
22
|
+
let listening = false
|
|
23
|
+
let suspended = false
|
|
24
|
+
|
|
25
|
+
function suspend() {
|
|
26
|
+
suspended = true
|
|
27
|
+
cancelFrame?.()
|
|
28
|
+
cancelFrame = undefined
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resume() {
|
|
32
|
+
suspended = false
|
|
33
|
+
schedule()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function schedule() {
|
|
37
|
+
if (!pending.size) {
|
|
38
|
+
cancelFrame?.()
|
|
39
|
+
cancelFrame = undefined
|
|
40
|
+
if (listening && !suspended) {
|
|
41
|
+
Application.off(Application.suspendEvent, suspend)
|
|
42
|
+
Application.off(Application.resumeEvent, resume)
|
|
43
|
+
listening = false
|
|
44
|
+
}
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
if (!listening) {
|
|
48
|
+
Application.on(Application.suspendEvent, suspend)
|
|
49
|
+
Application.on(Application.resumeEvent, resume)
|
|
50
|
+
listening = true
|
|
51
|
+
}
|
|
52
|
+
if (cancelFrame || suspended) return
|
|
53
|
+
cancelFrame = getDriver().frame(() => {
|
|
54
|
+
cancelFrame = undefined
|
|
55
|
+
const time = getDriver().now()
|
|
56
|
+
const sources = [...pending]
|
|
57
|
+
pending.clear()
|
|
58
|
+
// Capture every source before calling any listener: reentrant writes belong to the next batch.
|
|
59
|
+
const deliveries = sources.map((source) => source.snapshot(time))
|
|
60
|
+
for (const source of sources) if (source.active) pending.add(source)
|
|
61
|
+
for (const deliver of deliveries) deliver()
|
|
62
|
+
schedule()
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function deliver(observer: Observer, value: number, time: number) {
|
|
67
|
+
try {
|
|
68
|
+
observer.listener(observer.color ? new Color(value) : value, time)
|
|
69
|
+
} catch (error) {
|
|
70
|
+
// A consumer failure must not interrupt other consumers or strand the shared frame source.
|
|
71
|
+
Trace.write(
|
|
72
|
+
`Jolt motion value subscriber failed: ${error instanceof Error ? error.stack : String(error)}`,
|
|
73
|
+
Trace.categories.Error,
|
|
74
|
+
Trace.messageType.error,
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Root-owned subscriptions; mappings are evaluated only for consumers that opted in. */
|
|
80
|
+
export class ValueObservers {
|
|
81
|
+
private readonly observers = new Set<Observer>()
|
|
82
|
+
private running = false
|
|
83
|
+
|
|
84
|
+
constructor(private readonly read: (time: number) => number) {}
|
|
85
|
+
|
|
86
|
+
get active() {
|
|
87
|
+
return this.running && this.observers.size > 0
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
subscribe(
|
|
91
|
+
read: (value: number) => number,
|
|
92
|
+
color: boolean,
|
|
93
|
+
listener: ValueListener<number | Color>,
|
|
94
|
+
options: SubscribeOptions = {},
|
|
95
|
+
): () => void {
|
|
96
|
+
if (typeof listener !== 'function')
|
|
97
|
+
throw new TypeError('subscribe() needs a listener function.')
|
|
98
|
+
const time = getDriver().now()
|
|
99
|
+
const observer = { read, color, listener, previous: read(this.read(time)) }
|
|
100
|
+
this.observers.add(observer)
|
|
101
|
+
if (options.immediate !== false) deliver(observer, observer.previous, time)
|
|
102
|
+
if (this.active) this.changed(true)
|
|
103
|
+
return () => {
|
|
104
|
+
this.observers.delete(observer)
|
|
105
|
+
if (!this.observers.size) {
|
|
106
|
+
pending.delete(this)
|
|
107
|
+
schedule()
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
changed(running: boolean) {
|
|
113
|
+
this.running = running
|
|
114
|
+
if (this.observers.size) {
|
|
115
|
+
pending.add(this)
|
|
116
|
+
schedule()
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
snapshot(time: number): () => void {
|
|
121
|
+
const value = this.read(time)
|
|
122
|
+
const deliveries = [...this.observers].flatMap((observer) => {
|
|
123
|
+
const next = observer.read(value)
|
|
124
|
+
if (Object.is(next, observer.previous)) return []
|
|
125
|
+
observer.previous = next
|
|
126
|
+
return [{ observer, next }]
|
|
127
|
+
})
|
|
128
|
+
return () => {
|
|
129
|
+
for (const { observer, next } of deliveries)
|
|
130
|
+
if (this.observers.has(observer)) deliver(observer, next, time)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { instantTrack, type Track } from './track.js'
|
|
2
|
+
|
|
3
|
+
export interface ValuePart {
|
|
4
|
+
readonly track: Track
|
|
5
|
+
readonly elapsed: number
|
|
6
|
+
readonly base: number
|
|
7
|
+
readonly span: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Compile scalar addition before any binding's mapping. The snapshot is independent of later edits. */
|
|
11
|
+
export function valueTrack(parts: readonly ValuePart[], rest: number) {
|
|
12
|
+
const value = (time: number) =>
|
|
13
|
+
parts.reduce(
|
|
14
|
+
(sum, part) =>
|
|
15
|
+
sum + part.base + part.span * part.track.valueAt(part.elapsed + time),
|
|
16
|
+
rest,
|
|
17
|
+
)
|
|
18
|
+
let finite = 0
|
|
19
|
+
const periods: number[] = []
|
|
20
|
+
for (const { track, elapsed } of parts) {
|
|
21
|
+
if (Number.isFinite(track.end)) finite = Math.max(finite, track.end - elapsed)
|
|
22
|
+
else if (elapsed < track.delay) finite = Math.max(finite, track.delay - elapsed)
|
|
23
|
+
else periods.push(Math.max(1, track.duration * (track.reverse ? 2 : 1)))
|
|
24
|
+
}
|
|
25
|
+
if (finite <= 0 && !periods.length)
|
|
26
|
+
return { track: instantTrack('x', value(0)), windowed: false }
|
|
27
|
+
const loop = finite <= 0 && periods.length ? commonPeriod(periods) : undefined
|
|
28
|
+
// Bound finite compositions too: a minutes-long base must not undersample a short pulse.
|
|
29
|
+
const duration = finite > 0 ? Math.min(finite, 4000) : (loop ?? 4000)
|
|
30
|
+
const steps = Math.max(1, Math.min(6000, Math.ceil((duration * 60) / 1000)))
|
|
31
|
+
const times = Array.from({ length: steps + 1 }, (_, i) => i / steps)
|
|
32
|
+
const values = times.map((time) => value(time * duration))
|
|
33
|
+
const endless = loop !== undefined
|
|
34
|
+
const at = (time: number) =>
|
|
35
|
+
value(
|
|
36
|
+
endless
|
|
37
|
+
? ((time % duration) + duration) % duration
|
|
38
|
+
: Math.min(duration, Math.max(0, time)),
|
|
39
|
+
)
|
|
40
|
+
const track: Track = {
|
|
41
|
+
property: 'x',
|
|
42
|
+
delay: 0,
|
|
43
|
+
duration,
|
|
44
|
+
repeat: endless ? Infinity : 0,
|
|
45
|
+
reverse: false,
|
|
46
|
+
times,
|
|
47
|
+
values,
|
|
48
|
+
to: endless ? values[0]! : values[steps]!,
|
|
49
|
+
end: endless ? Infinity : duration,
|
|
50
|
+
arrival: endless ? Infinity : duration,
|
|
51
|
+
valueAt: at,
|
|
52
|
+
velocityAt: (time) => (at(time + 0.5) - at(time - 0.5)) * 1000,
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
track,
|
|
56
|
+
windowed:
|
|
57
|
+
finite > duration ||
|
|
58
|
+
(!endless && parts.some(({ track }) => !Number.isFinite(track.end))),
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function commonPeriod(periods: readonly number[]) {
|
|
63
|
+
const gcd = (a: number, b: number): number => (b ? gcd(b, a % b) : a)
|
|
64
|
+
let total = 1
|
|
65
|
+
for (const period of periods) {
|
|
66
|
+
if (!Number.isInteger(period)) return undefined
|
|
67
|
+
total = (total / gcd(total, period)) * period
|
|
68
|
+
if (total > 30000) return undefined
|
|
69
|
+
}
|
|
70
|
+
return total
|
|
71
|
+
}
|
package/src/vue/index.ts
CHANGED
|
@@ -15,10 +15,19 @@ import {
|
|
|
15
15
|
type Ref,
|
|
16
16
|
} from 'nativescript-vue'
|
|
17
17
|
import {
|
|
18
|
+
gradient,
|
|
18
19
|
press,
|
|
19
20
|
motionScope,
|
|
21
|
+
bind,
|
|
22
|
+
motionValue,
|
|
23
|
+
scrollValue,
|
|
20
24
|
onKeyboard,
|
|
21
25
|
onReducedMotion,
|
|
26
|
+
stop,
|
|
27
|
+
type Bindings,
|
|
28
|
+
type MotionValue,
|
|
29
|
+
type ScrollValue,
|
|
30
|
+
type Target,
|
|
22
31
|
reducedMotion,
|
|
23
32
|
type KeyboardState,
|
|
24
33
|
type MotionScope,
|
|
@@ -27,6 +36,9 @@ import {
|
|
|
27
36
|
type PlaybackAnimation,
|
|
28
37
|
type PresetName,
|
|
29
38
|
type SpringName,
|
|
39
|
+
type Gradient,
|
|
40
|
+
type GradientOptions,
|
|
41
|
+
type Transition,
|
|
30
42
|
} from '../index.js'
|
|
31
43
|
import { currentKeyboard } from '../keyboard.js'
|
|
32
44
|
|
|
@@ -37,6 +49,11 @@ type Options = AnimateOptions | SpringName
|
|
|
37
49
|
export type AnimateValue =
|
|
38
50
|
PresetName | Keyframes | readonly [animation: PresetName | Keyframes, options?: Options]
|
|
39
51
|
|
|
52
|
+
/** The gradient, alone or paired with the transition for later changes. */
|
|
53
|
+
export type GradientValue =
|
|
54
|
+
| GradientOptions
|
|
55
|
+
| readonly [gradient: GradientOptions, transition?: Transition | SpringName]
|
|
56
|
+
|
|
40
57
|
/** The pressed keyframes, alone or paired with a transition. */
|
|
41
58
|
export type PressValue = Keyframes | readonly [pressed: Keyframes, options?: Options]
|
|
42
59
|
|
|
@@ -53,6 +70,7 @@ export interface TransitionProps {
|
|
|
53
70
|
const animations = new WeakMap<object, PlaybackAnimation>()
|
|
54
71
|
const scopes = new WeakMap<object, MotionScope>()
|
|
55
72
|
const removers = new WeakMap<object, () => void>()
|
|
73
|
+
const gradients = new WeakMap<object, { gradient: Gradient; colors: number }>()
|
|
56
74
|
|
|
57
75
|
function split<T>(
|
|
58
76
|
value: T | readonly [T, Options?],
|
|
@@ -137,6 +155,47 @@ export const vPress: ObjectDirective<Host, PressValue | undefined> = {
|
|
|
137
155
|
},
|
|
138
156
|
}
|
|
139
157
|
|
|
158
|
+
function paint(el: Host, value: GradientValue, animated: boolean) {
|
|
159
|
+
const [options, transition] = (Array.isArray(value) ? value : [value]) as [
|
|
160
|
+
GradientOptions,
|
|
161
|
+
(Transition | SpringName)?,
|
|
162
|
+
]
|
|
163
|
+
const current = gradients.get(el)
|
|
164
|
+
const colors = options.colors.length
|
|
165
|
+
if (current && current.gradient.type === options.type) {
|
|
166
|
+
const { type, ...values } = options
|
|
167
|
+
if (animated && current.colors === colors)
|
|
168
|
+
current.gradient.animate(values, transition)
|
|
169
|
+
else current.gradient.set(values)
|
|
170
|
+
current.colors = colors
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
current?.gradient.remove()
|
|
174
|
+
gradients.set(el, { gradient: gradient(el, options), colors })
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Paints a native gradient behind the element, and animates to the new values when they change.
|
|
179
|
+
* A change of type, or of the number of colors, applies at once.
|
|
180
|
+
*
|
|
181
|
+
* ```vue
|
|
182
|
+
* <GridLayout v-gradient="{ type: 'linear', colors: ['#18181B', '#3F3F46'], angle: 135 }" />
|
|
183
|
+
* <GridLayout v-gradient="[{ type: 'linear', colors: active ? hot : cold }, 'snappy']" />
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
export const vGradient: ObjectDirective<Host, GradientValue> = {
|
|
187
|
+
mounted(el, { value }) {
|
|
188
|
+
paint(el, value, false)
|
|
189
|
+
},
|
|
190
|
+
updated(el, { value, oldValue }) {
|
|
191
|
+
if (!same(value, oldValue)) paint(el, value, true)
|
|
192
|
+
},
|
|
193
|
+
unmounted(el) {
|
|
194
|
+
gradients.get(el)?.gradient.remove()
|
|
195
|
+
gradients.delete(el)
|
|
196
|
+
},
|
|
197
|
+
}
|
|
198
|
+
|
|
140
199
|
/** The hooks `JoltTransition` hands to Vue's `BaseTransition`. */
|
|
141
200
|
export function transitionHooks(props: TransitionProps): BaseTransitionProps<Host> {
|
|
142
201
|
const run = (value: AnimateValue | undefined) => (el: Host, done: () => void) => {
|
|
@@ -195,10 +254,11 @@ export const JoltTransition = defineComponent({
|
|
|
195
254
|
},
|
|
196
255
|
})
|
|
197
256
|
|
|
198
|
-
/** Registers `v-animate`, `v-press` and `JoltTransition` on the app. */
|
|
257
|
+
/** Registers `v-animate`, `v-press`, `v-gradient` and `JoltTransition` on the app. */
|
|
199
258
|
export function install(app: App) {
|
|
200
259
|
app.directive('animate', vAnimate)
|
|
201
260
|
app.directive('press', vPress)
|
|
261
|
+
app.directive('gradient', vGradient)
|
|
202
262
|
app.component('JoltTransition', JoltTransition)
|
|
203
263
|
}
|
|
204
264
|
|
|
@@ -229,3 +289,40 @@ export function useKeyboard(): Readonly<Ref<KeyboardState>> {
|
|
|
229
289
|
onUnmounted(off)
|
|
230
290
|
return readonly(keyboard) as Readonly<Ref<KeyboardState>>
|
|
231
291
|
}
|
|
292
|
+
|
|
293
|
+
/** Call in setup(): a motion value that stops when the component unmounts. */
|
|
294
|
+
export function useMotionValue(initial = 0): MotionValue {
|
|
295
|
+
const value = motionValue(initial)
|
|
296
|
+
onUnmounted(() => stop(value))
|
|
297
|
+
return value
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Call in setup(): a template ref that binds motion values to the element on mount
|
|
302
|
+
* and releases them on unmount. A recreated element picks the values up again.
|
|
303
|
+
*/
|
|
304
|
+
export function useBinding(bindings: Bindings): (element: Target | null) => void {
|
|
305
|
+
let element: Target | null = null
|
|
306
|
+
let release: (() => void) | undefined
|
|
307
|
+
onUnmounted(() => {
|
|
308
|
+
release?.()
|
|
309
|
+
release = undefined
|
|
310
|
+
})
|
|
311
|
+
return (next) => {
|
|
312
|
+
if (next === element) return
|
|
313
|
+
release?.()
|
|
314
|
+
release = undefined
|
|
315
|
+
element = next
|
|
316
|
+
if (next) release = bind(next, bindings)
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Call in setup(): a scroll offset as a value. Pass its `ref` to the ScrollView's
|
|
322
|
+
* template ref; it stops following when the component unmounts.
|
|
323
|
+
*/
|
|
324
|
+
export function useScrollValue(options?: { axis?: 'x' | 'y' }): ScrollValue {
|
|
325
|
+
const value = scrollValue(null, options)
|
|
326
|
+
onUnmounted(() => value.dispose())
|
|
327
|
+
return value
|
|
328
|
+
}
|