@rafaelaugusto/jolt 0.1.0-beta.12 → 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 +4 -1
- package/dist/animate.d.ts +6 -4
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +3 -1
- package/dist/animate.js.map +1 -1
- package/dist/drag.js +3 -3
- 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/engine.d.ts +2 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +23 -2
- package/dist/engine.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 +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/motion-scope.d.ts +2 -1
- package/dist/motion-scope.d.ts.map +1 -1
- package/dist/motion-scope.js +4 -1
- package/dist/motion-scope.js.map +1 -1
- package/dist/motion-value.d.ts +45 -8
- package/dist/motion-value.d.ts.map +1 -1
- package/dist/motion-value.js +289 -91
- package/dist/motion-value.js.map +1 -1
- 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/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/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/package.json +6 -2
- package/src/android-plugin.d.ts +37 -0
- package/src/animate.ts +13 -7
- package/src/drag.ts +3 -3
- 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/engine.ts +23 -2
- package/src/image-effect.ts +10 -2
- package/src/index.ts +5 -0
- package/src/ios-plugin.d.ts +33 -0
- package/src/motion-scope.ts +5 -1
- package/src/motion-value.ts +331 -99
- 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/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/value-observers.ts +133 -0
- package/src/value-track.ts +71 -0
|
@@ -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
|
+
}
|