@rafaelaugusto/jolt 0.1.0-rc.2 → 0.1.0-rc.4
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 -4
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +20 -5
- package/dist/animate.js.map +1 -1
- package/dist/bottom-sheet/geometry.d.ts +7 -2
- package/dist/bottom-sheet/geometry.d.ts.map +1 -1
- package/dist/bottom-sheet/geometry.js +7 -3
- package/dist/bottom-sheet/geometry.js.map +1 -1
- package/dist/bottom-sheet/index.d.ts +10 -0
- package/dist/bottom-sheet/index.d.ts.map +1 -1
- package/dist/bottom-sheet/index.js +72 -22
- package/dist/bottom-sheet/index.js.map +1 -1
- package/dist/bottom-sheet/native.d.ts +5 -0
- package/dist/bottom-sheet/native.d.ts.map +1 -1
- package/dist/bottom-sheet/native.js +6 -0
- package/dist/bottom-sheet/native.js.map +1 -1
- package/dist/driver/android.d.ts.map +1 -1
- package/dist/driver/android.js +15 -7
- package/dist/driver/android.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/interactive-navigation/index.d.ts +2 -0
- package/dist/interactive-navigation/index.d.ts.map +1 -1
- package/dist/interactive-navigation/index.js +1 -0
- package/dist/interactive-navigation/index.js.map +1 -1
- package/dist/interactive-navigation/modal.d.ts +34 -0
- package/dist/interactive-navigation/modal.d.ts.map +1 -0
- package/dist/interactive-navigation/modal.js +253 -0
- package/dist/interactive-navigation/modal.js.map +1 -0
- package/dist/live-spring-driver.d.ts +5 -0
- package/dist/live-spring-driver.d.ts.map +1 -1
- package/dist/live-spring-driver.js +13 -0
- package/dist/live-spring-driver.js.map +1 -1
- package/dist/mixer.d.ts +7 -1
- package/dist/mixer.d.ts.map +1 -1
- package/dist/mixer.js +57 -22
- package/dist/mixer.js.map +1 -1
- package/dist/motion-value.d.ts +12 -1
- package/dist/motion-value.d.ts.map +1 -1
- package/dist/motion-value.js +115 -7
- package/dist/motion-value.js.map +1 -1
- package/dist/scroll-header/geometry.d.ts +1 -1
- package/dist/scroll-header/geometry.d.ts.map +1 -1
- package/dist/scroll-header/geometry.js +7 -2
- package/dist/scroll-header/geometry.js.map +1 -1
- package/dist/scroll-header/index.d.ts.map +1 -1
- package/dist/scroll-header/index.js +13 -0
- package/dist/scroll-header/index.js.map +1 -1
- package/dist/shapes/index.d.ts +1 -1
- package/dist/shapes/index.d.ts.map +1 -1
- package/dist/shapes/index.js +23 -9
- package/dist/shapes/index.js.map +1 -1
- package/dist/types.d.ts +5 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/value-track.d.ts +9 -0
- package/dist/value-track.d.ts.map +1 -1
- package/dist/value-track.js +28 -16
- package/dist/value-track.js.map +1 -1
- package/package.json +1 -1
- package/platforms/android/java/org/rafaelaugusto/jolt/BlurEffect.java +14 -2
- package/platforms/android/java/org/rafaelaugusto/jolt/GradientLayer.java +30 -8
- package/platforms/android/java/org/rafaelaugusto/jolt/KeyframeFactory.java +26 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/LiveSpring.java +36 -9
- package/platforms/android/java/org/rafaelaugusto/jolt/ScrollHeader.java +3 -2
- package/platforms/android/java/org/rafaelaugusto/jolt/ShapeLayer.java +17 -7
- package/platforms/android/proguard-rules.pro +1 -0
- package/platforms/ios/src/JoltInteractiveModal.h +16 -0
- package/platforms/ios/src/JoltInteractiveModal.m +178 -0
- package/platforms/ios/src/JoltLiveSpring.h +1 -0
- package/platforms/ios/src/JoltLiveSpring.m +183 -67
- package/platforms/ios/src/module.modulemap +1 -0
- package/src/android-plugin.d.ts +10 -0
- package/src/animate.ts +23 -5
- package/src/bottom-sheet/geometry.ts +9 -2
- package/src/bottom-sheet/index.ts +94 -20
- package/src/bottom-sheet/native.ts +6 -0
- package/src/driver/android.ts +31 -14
- package/src/index.ts +1 -1
- package/src/interactive-navigation/index.ts +7 -0
- package/src/interactive-navigation/modal.ts +306 -0
- package/src/ios-plugin.d.ts +23 -0
- package/src/live-spring-driver.ts +15 -0
- package/src/mixer.ts +62 -22
- package/src/motion-value.ts +121 -9
- package/src/scroll-header/geometry.ts +7 -2
- package/src/scroll-header/index.ts +12 -0
- package/src/shapes/index.ts +20 -9
- package/src/types.ts +5 -2
- package/src/value-track.ts +25 -14
|
@@ -37,6 +37,8 @@ export interface LiveNode {
|
|
|
37
37
|
moving(): boolean
|
|
38
38
|
follow(source: LiveNode, maps: readonly LiveMapping[]): void
|
|
39
39
|
update(input: LiveInput): void
|
|
40
|
+
/** Several sampled inputs over a rest value, each on its own clock, summed natively. */
|
|
41
|
+
inputs(rest: number, inputs: readonly LiveInput[]): void
|
|
40
42
|
reduced(value: boolean): void
|
|
41
43
|
paused(value: boolean): void
|
|
42
44
|
bind(view: View, property: NumericProperty, maps: readonly LiveMapping[]): LiveBinding
|
|
@@ -58,6 +60,17 @@ export function liveSpringDriver(): LiveSpringDriver {
|
|
|
58
60
|
)
|
|
59
61
|
return nativeDriver
|
|
60
62
|
}
|
|
63
|
+
/** Native composition is optional: an app binary without the classes keeps sampled playback. */
|
|
64
|
+
export function liveComposition(): LiveSpringDriver | undefined {
|
|
65
|
+
return override ?? (available() ? nativeDriver : undefined)
|
|
66
|
+
}
|
|
67
|
+
export const stillPhysics: LiveSpringPhysics = {
|
|
68
|
+
stiffness: 100,
|
|
69
|
+
damping: 20,
|
|
70
|
+
mass: 1,
|
|
71
|
+
restDelta: 0.001,
|
|
72
|
+
restSpeed: 0.01,
|
|
73
|
+
}
|
|
61
74
|
function available() {
|
|
62
75
|
return isAndroid
|
|
63
76
|
? typeof org !== 'undefined' &&
|
|
@@ -104,6 +117,7 @@ const nativeDriver: LiveSpringDriver = {
|
|
|
104
117
|
input.running
|
|
105
118
|
? native.updateInput(JSON.stringify(input))
|
|
106
119
|
: native.setTarget(input.value),
|
|
120
|
+
inputs: (rest, inputs) => native.setInputs(rest, JSON.stringify(inputs)),
|
|
107
121
|
reduced: (value) => native.setReduced(value),
|
|
108
122
|
paused: (value) => native.setPaused(value),
|
|
109
123
|
bind(view, property, maps) {
|
|
@@ -145,6 +159,7 @@ const nativeDriver: LiveSpringDriver = {
|
|
|
145
159
|
input.running
|
|
146
160
|
? native.updateInput(JSON.stringify(input))
|
|
147
161
|
: native.setTarget(input.value),
|
|
162
|
+
inputs: (rest, inputs) => native.setInputsRest(JSON.stringify(inputs), rest),
|
|
148
163
|
reduced: (value) => native.setReduced(value),
|
|
149
164
|
paused: (value) => native.setPaused(value),
|
|
150
165
|
bind(view, property, maps) {
|
package/src/mixer.ts
CHANGED
|
@@ -2,7 +2,13 @@ import type { View } from '@nativescript/core'
|
|
|
2
2
|
import { getDriver } from './driver/index.js'
|
|
3
3
|
import { readValue, writeValue, type Property } from './properties.js'
|
|
4
4
|
import type { Track } from './track.js'
|
|
5
|
-
import
|
|
5
|
+
import {
|
|
6
|
+
liveComposition,
|
|
7
|
+
stillPhysics,
|
|
8
|
+
type LiveBinding,
|
|
9
|
+
type LiveInput,
|
|
10
|
+
type LiveNode,
|
|
11
|
+
} from './live-spring-driver.js'
|
|
6
12
|
|
|
7
13
|
/** The channel of regular animations; layers are named next to it. */
|
|
8
14
|
export const BASE = ''
|
|
@@ -11,7 +17,10 @@ const SAMPLES_PER_SECOND = 60
|
|
|
11
17
|
const MAX_SAMPLES = 6000
|
|
12
18
|
/** Endless channels loop natively over the least common multiple of their periods, up to this. */
|
|
13
19
|
const MAX_LOOP = 30000
|
|
14
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Beyond it, a native composer plays the layers without rebuilding. An app binary
|
|
22
|
+
* without the native classes falls back to windows sampled again every few seconds.
|
|
23
|
+
*/
|
|
15
24
|
const WINDOW = 4000
|
|
16
25
|
|
|
17
26
|
export const multiplies = (property: Property) =>
|
|
@@ -69,6 +78,7 @@ export function mixFor(view: View, property: Property): Mix {
|
|
|
69
78
|
/** One property of one view: a base and named layers, played natively as one composite track. */
|
|
70
79
|
export class Mix {
|
|
71
80
|
private live: { read(): number; layers(inputs: readonly LiveInput[]): void } | undefined
|
|
81
|
+
private composer: { node: LiveNode; binding: LiveBinding } | undefined
|
|
72
82
|
private readonly rests = new Map<string, number>()
|
|
73
83
|
private readonly channels = new Map<string, Channel>()
|
|
74
84
|
private native: unknown
|
|
@@ -93,6 +103,7 @@ export class Mix {
|
|
|
93
103
|
read(): number
|
|
94
104
|
layers(inputs: readonly LiveInput[]): void
|
|
95
105
|
}): () => void {
|
|
106
|
+
this.release()
|
|
96
107
|
this.live = base
|
|
97
108
|
this.compose()
|
|
98
109
|
return () => {
|
|
@@ -201,6 +212,30 @@ export class Mix {
|
|
|
201
212
|
})
|
|
202
213
|
}
|
|
203
214
|
|
|
215
|
+
/** Every channel and rest as a native layer; the base is one of them unless something else owns it. */
|
|
216
|
+
private inputs(now: number, withBase: boolean): LiveInput[] {
|
|
217
|
+
const layers: LiveInput[] = []
|
|
218
|
+
for (const key of new Set([...this.rests.keys(), ...this.channels.keys()])) {
|
|
219
|
+
if (key === BASE && !withBase) continue
|
|
220
|
+
const channel = this.channels.get(key)
|
|
221
|
+
const value = this.part(key, now, now)
|
|
222
|
+
const track = channel?.track
|
|
223
|
+
layers.push({
|
|
224
|
+
value,
|
|
225
|
+
running: !!channel && !channel.paused,
|
|
226
|
+
elapsed: this.elapsed(key, now),
|
|
227
|
+
duration: track?.duration ?? 0,
|
|
228
|
+
delay: track?.delay ?? 0,
|
|
229
|
+
repeat: track ? (Number.isFinite(track.repeat) ? track.repeat : -1) : 0,
|
|
230
|
+
reverse: track?.reverse ?? false,
|
|
231
|
+
to: track?.to ?? value,
|
|
232
|
+
times: track?.times ?? [],
|
|
233
|
+
values: track?.values ?? [],
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
return layers
|
|
237
|
+
}
|
|
238
|
+
|
|
204
239
|
/** Samples the composition from now and plays it as one native track. */
|
|
205
240
|
private compose() {
|
|
206
241
|
const driver = getDriver()
|
|
@@ -211,26 +246,7 @@ export class Mix {
|
|
|
211
246
|
|
|
212
247
|
if (this.live) {
|
|
213
248
|
if (previous !== undefined) driver.stop(this.view, previous)
|
|
214
|
-
|
|
215
|
-
for (const key of new Set([...this.rests.keys(), ...this.channels.keys()])) {
|
|
216
|
-
if (key === BASE) continue
|
|
217
|
-
const channel = this.channels.get(key)
|
|
218
|
-
const value = this.part(key, now, now)
|
|
219
|
-
const track = channel?.track
|
|
220
|
-
layers.push({
|
|
221
|
-
value,
|
|
222
|
-
running: !!channel && !channel.paused,
|
|
223
|
-
elapsed: this.elapsed(key, now),
|
|
224
|
-
duration: track?.duration ?? 0,
|
|
225
|
-
delay: track?.delay ?? 0,
|
|
226
|
-
repeat: track ? (Number.isFinite(track.repeat) ? track.repeat : -1) : 0,
|
|
227
|
-
reverse: track?.reverse ?? false,
|
|
228
|
-
to: track?.to ?? value,
|
|
229
|
-
times: track?.times ?? [],
|
|
230
|
-
values: track?.values ?? [],
|
|
231
|
-
})
|
|
232
|
-
}
|
|
233
|
-
this.live.layers(layers)
|
|
249
|
+
this.live.layers(this.inputs(now, false))
|
|
234
250
|
return
|
|
235
251
|
}
|
|
236
252
|
|
|
@@ -248,11 +264,14 @@ export class Mix {
|
|
|
248
264
|
if (previous !== undefined) driver.stop(this.view, previous)
|
|
249
265
|
const loop = finite <= 0 && periods.length ? leastCommonMultiple(periods) : undefined
|
|
250
266
|
if (finite <= 0 && !periods.length) {
|
|
267
|
+
this.release()
|
|
251
268
|
writeValue(this.view, this.property, this.value(now))
|
|
252
269
|
driver.sync?.(this.view)
|
|
253
270
|
this.dissolve()
|
|
254
271
|
return
|
|
255
272
|
}
|
|
273
|
+
if (finite <= 0 && loop === undefined && this.composeNatively(now)) return
|
|
274
|
+
this.release()
|
|
256
275
|
|
|
257
276
|
const span = finite > 0 ? finite : (loop ?? WINDOW)
|
|
258
277
|
const steps = Math.max(
|
|
@@ -292,6 +311,27 @@ export class Mix {
|
|
|
292
311
|
if (elapsed > 1) driver.control?.(this.view, native, elapsed, false)
|
|
293
312
|
}
|
|
294
313
|
|
|
314
|
+
/** Endless layers with no common period play on a native composer instead of rolling windows. */
|
|
315
|
+
private composeNatively(now: number): boolean {
|
|
316
|
+
const live = liveComposition()
|
|
317
|
+
if (!live || !layerable(this.property) || !this.view.nativeViewProtected) return false
|
|
318
|
+
if (!this.composer) {
|
|
319
|
+
const node = live.create(identity(this.property), stillPhysics, true)
|
|
320
|
+
const binding = node.bind(this.view, this.property, [])
|
|
321
|
+
this.composer = { node, binding }
|
|
322
|
+
}
|
|
323
|
+
this.composer.binding.layers(this.inputs(now, true))
|
|
324
|
+
return true
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private release() {
|
|
328
|
+
const composer = this.composer
|
|
329
|
+
if (!composer) return
|
|
330
|
+
this.composer = undefined
|
|
331
|
+
composer.binding.cancel()
|
|
332
|
+
composer.node.dispose()
|
|
333
|
+
}
|
|
334
|
+
|
|
295
335
|
private dissolve() {
|
|
296
336
|
if (this.live || this.channels.size) return
|
|
297
337
|
for (const [key, value] of this.rests)
|
package/src/motion-value.ts
CHANGED
|
@@ -16,13 +16,16 @@ import {
|
|
|
16
16
|
} from './color.js'
|
|
17
17
|
import { onReducedMotion, reducedMotion, timeScale } from './config.js'
|
|
18
18
|
import { springPhysics, type SpringParams } from './spring.js'
|
|
19
|
-
import { mixFor } from './mixer.js'
|
|
19
|
+
import { layerable, mixFor } from './mixer.js'
|
|
20
20
|
import {
|
|
21
|
+
type LiveBinding,
|
|
22
|
+
type LiveInput,
|
|
23
|
+
type LiveMapping,
|
|
24
|
+
type LiveNode,
|
|
25
|
+
liveComposition,
|
|
21
26
|
liveSpringDriver,
|
|
27
|
+
stillPhysics,
|
|
22
28
|
syncLiveSprings,
|
|
23
|
-
type LiveNode,
|
|
24
|
-
type LiveMapping,
|
|
25
|
-
type LiveBinding,
|
|
26
29
|
} from './live-spring-driver.js'
|
|
27
30
|
import {
|
|
28
31
|
clampDecoration,
|
|
@@ -44,7 +47,7 @@ import {
|
|
|
44
47
|
import type { KeyframeProperty, Target } from './types.js'
|
|
45
48
|
import { playbackSource, type PlaybackAnimation, type PlaybackClock } from './playback.js'
|
|
46
49
|
import { TimedAnimation } from './timed-animation.js'
|
|
47
|
-
import { valueTrack } from './value-track.js'
|
|
50
|
+
import { compositionSpan, valueTrack } from './value-track.js'
|
|
48
51
|
import {
|
|
49
52
|
ValueObservers,
|
|
50
53
|
type SubscribeOptions,
|
|
@@ -108,6 +111,8 @@ interface Run {
|
|
|
108
111
|
readonly track: Track
|
|
109
112
|
readonly base: number
|
|
110
113
|
readonly span: number
|
|
114
|
+
/** The track's samples in the value's units, ready for a native composer. */
|
|
115
|
+
readonly scaled: readonly number[]
|
|
111
116
|
readonly animation: TimedAnimation
|
|
112
117
|
readonly startedAt: number
|
|
113
118
|
readonly clock?: Pick<PlaybackClock, 'time'>
|
|
@@ -213,7 +218,10 @@ class Root extends Derived {
|
|
|
213
218
|
private readonly rests = new Map<string, number>()
|
|
214
219
|
private readonly runs = new Map<string, Run>()
|
|
215
220
|
private presentation: Presentation | undefined
|
|
221
|
+
private presented = false
|
|
216
222
|
private cancelPresentation: (() => void) | undefined
|
|
223
|
+
private composer: LiveNode | undefined
|
|
224
|
+
private composition: { rest: number; inputs: LiveInput[] } | undefined
|
|
217
225
|
|
|
218
226
|
constructor(initial: number) {
|
|
219
227
|
super(undefined, (value) => value, false)
|
|
@@ -345,6 +353,7 @@ class Root extends Derived {
|
|
|
345
353
|
track,
|
|
346
354
|
base,
|
|
347
355
|
span,
|
|
356
|
+
scaled: track.values.map((value) => base + span * value),
|
|
348
357
|
animation,
|
|
349
358
|
startedAt: driver.now(),
|
|
350
359
|
clock: { time: () => animation.time },
|
|
@@ -398,6 +407,74 @@ class Root extends Derived {
|
|
|
398
407
|
private sync() {
|
|
399
408
|
this.cancelPresentation?.()
|
|
400
409
|
this.cancelPresentation = undefined
|
|
410
|
+
this.presentation = undefined
|
|
411
|
+
this.presented = false
|
|
412
|
+
const now = getDriver().now()
|
|
413
|
+
const runs = [...this.runs.values()].filter((run) => !run.animation.paused)
|
|
414
|
+
const span =
|
|
415
|
+
runs.length > 1
|
|
416
|
+
? compositionSpan(
|
|
417
|
+
runs.map((run) => ({ track: run.track, elapsed: this.runTime(run, now) })),
|
|
418
|
+
)
|
|
419
|
+
: undefined
|
|
420
|
+
this.composition = span?.windowed && liveComposition() ? this.inputs(now) : undefined
|
|
421
|
+
if (this.composition)
|
|
422
|
+
this.composer?.inputs(this.composition.rest, this.composition.inputs)
|
|
423
|
+
else {
|
|
424
|
+
this.settle()
|
|
425
|
+
this.present()
|
|
426
|
+
}
|
|
427
|
+
this.changed()
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** Every run as a native input on its own clock; rests and paused runs stand still. */
|
|
431
|
+
private inputs(now: number) {
|
|
432
|
+
let rest = 0
|
|
433
|
+
for (const [key, value] of this.rests) if (!this.runs.has(key)) rest += value
|
|
434
|
+
const inputs: LiveInput[] = []
|
|
435
|
+
for (const run of this.runs.values()) {
|
|
436
|
+
const elapsed = this.runTime(run, now)
|
|
437
|
+
const { track } = run
|
|
438
|
+
inputs.push({
|
|
439
|
+
value: run.base + run.span * track.valueAt(elapsed),
|
|
440
|
+
running: !run.animation.paused,
|
|
441
|
+
elapsed,
|
|
442
|
+
duration: track.duration,
|
|
443
|
+
delay: track.delay,
|
|
444
|
+
repeat: Number.isFinite(track.repeat) ? track.repeat : -1,
|
|
445
|
+
reverse: track.reverse,
|
|
446
|
+
to: run.base + run.span * track.to,
|
|
447
|
+
times: track.times,
|
|
448
|
+
values: run.scaled,
|
|
449
|
+
})
|
|
450
|
+
}
|
|
451
|
+
return { rest, inputs }
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** The native composer a binding joins while this value would otherwise roll windows. */
|
|
455
|
+
composerNode(): LiveNode | undefined {
|
|
456
|
+
const composition = this.composition
|
|
457
|
+
if (!composition) return undefined
|
|
458
|
+
if (!this.composer) {
|
|
459
|
+
const live = liveComposition()
|
|
460
|
+
if (!live) return undefined
|
|
461
|
+
this.composer = live.create(this.raw(), stillPhysics, true)
|
|
462
|
+
this.composer.inputs(composition.rest, composition.inputs)
|
|
463
|
+
}
|
|
464
|
+
return this.composer
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** Drops the composer once no binding needs it or the composition stops rolling. */
|
|
468
|
+
settle() {
|
|
469
|
+
if (this.composition && this.bindings.size) return
|
|
470
|
+
this.composer?.dispose()
|
|
471
|
+
this.composer = undefined
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** Sampled at once for tracks, on demand while bindings sit on the composer. */
|
|
475
|
+
private present(): Presentation | undefined {
|
|
476
|
+
if (this.presented) return this.presentation
|
|
477
|
+
this.presented = true
|
|
401
478
|
const now = getDriver().now()
|
|
402
479
|
let rest = 0
|
|
403
480
|
for (const [key, value] of this.rests) if (!this.runs.has(key)) rest += value
|
|
@@ -429,7 +506,7 @@ class Root extends Derived {
|
|
|
429
506
|
clock?.cancel()
|
|
430
507
|
}
|
|
431
508
|
}
|
|
432
|
-
this.
|
|
509
|
+
return this.presentation
|
|
433
510
|
}
|
|
434
511
|
|
|
435
512
|
protected changed() {
|
|
@@ -440,7 +517,7 @@ class Root extends Derived {
|
|
|
440
517
|
|
|
441
518
|
/** The plan that plays this value's run, mapped, on one bound property. */
|
|
442
519
|
plan(binding: Binding): Plan {
|
|
443
|
-
const run = this.
|
|
520
|
+
const run = this.present()
|
|
444
521
|
const { property, read } = binding
|
|
445
522
|
const write = (value: number) =>
|
|
446
523
|
isDecorationProperty(property)
|
|
@@ -495,12 +572,13 @@ class Root extends Derived {
|
|
|
495
572
|
|
|
496
573
|
/** Capture the current native presentation clock, even if a later command replaces it. */
|
|
497
574
|
captureTime(): () => number {
|
|
498
|
-
const run = this.
|
|
575
|
+
const run = this.present()
|
|
499
576
|
return run ? () => this.runTime(run) : () => 0
|
|
500
577
|
}
|
|
501
578
|
|
|
502
579
|
elapsed(): number {
|
|
503
|
-
|
|
580
|
+
const run = this.present()
|
|
581
|
+
return run ? this.runTime(run) : 0
|
|
504
582
|
}
|
|
505
583
|
|
|
506
584
|
get paused(): boolean {
|
|
@@ -862,6 +940,16 @@ function attach(view: View, property: Property, source: Derived) {
|
|
|
862
940
|
const root = source.root
|
|
863
941
|
if (root instanceof SpringRoot)
|
|
864
942
|
return attachSpring(view, property as NumericProperty, source, root)
|
|
943
|
+
let native: LiveBinding | undefined, composer: LiveNode | undefined
|
|
944
|
+
let releaseMix: (() => void) | undefined
|
|
945
|
+
const detach = () => {
|
|
946
|
+
if (!native) return
|
|
947
|
+
native.cancel()
|
|
948
|
+
releaseMix?.()
|
|
949
|
+
releaseMix = undefined
|
|
950
|
+
native = composer = undefined
|
|
951
|
+
syncLiveSprings(view)
|
|
952
|
+
}
|
|
865
953
|
const binding: Binding = {
|
|
866
954
|
view,
|
|
867
955
|
property,
|
|
@@ -870,6 +958,24 @@ function attach(view: View, property: Property, source: Derived) {
|
|
|
870
958
|
read: source.read,
|
|
871
959
|
sync() {
|
|
872
960
|
if (!view.isLoaded || !view.nativeViewProtected) return
|
|
961
|
+
const node = layerable(property) && !source.color ? root.composerNode() : undefined
|
|
962
|
+
if (node) {
|
|
963
|
+
if (composer === node) return
|
|
964
|
+
detach()
|
|
965
|
+
binding.animation?.stop()
|
|
966
|
+
binding.animation = undefined
|
|
967
|
+
stopBase(view, [property])
|
|
968
|
+
const handle = node.bind(view, property as NumericProperty, source.mappings)
|
|
969
|
+
native = handle
|
|
970
|
+
composer = node
|
|
971
|
+
releaseMix = mixFor(view, property).attachLive({
|
|
972
|
+
read: () => handle.value(),
|
|
973
|
+
layers: (layers) => handle.layers(layers),
|
|
974
|
+
})
|
|
975
|
+
syncLiveSprings(view)
|
|
976
|
+
return
|
|
977
|
+
}
|
|
978
|
+
detach()
|
|
873
979
|
if (!root.running) {
|
|
874
980
|
// A value at rest writes straight through, so scroll offsets cost one write per event.
|
|
875
981
|
binding.animation?.stop()
|
|
@@ -894,12 +1000,17 @@ function attach(view: View, property: Property, source: Derived) {
|
|
|
894
1000
|
animation.seekTime(elapsed)
|
|
895
1001
|
binding.animation = animation
|
|
896
1002
|
},
|
|
1003
|
+
cleanup() {
|
|
1004
|
+
view.off('unloaded', detach)
|
|
1005
|
+
detach()
|
|
1006
|
+
},
|
|
897
1007
|
}
|
|
898
1008
|
let properties = bound.get(view)
|
|
899
1009
|
if (!properties) bound.set(view, (properties = new Map()))
|
|
900
1010
|
properties.set(property, binding)
|
|
901
1011
|
root.bindings.add(binding)
|
|
902
1012
|
view.on('loaded', binding.sync)
|
|
1013
|
+
view.on('unloaded', detach)
|
|
903
1014
|
try {
|
|
904
1015
|
binding.sync()
|
|
905
1016
|
} catch (error) {
|
|
@@ -1134,6 +1245,7 @@ function release(binding: Binding) {
|
|
|
1134
1245
|
const properties = bound.get(binding.view)
|
|
1135
1246
|
if (properties?.get(binding.property) === binding) properties.delete(binding.property)
|
|
1136
1247
|
binding.cleanup?.()
|
|
1248
|
+
binding.root.settle()
|
|
1137
1249
|
}
|
|
1138
1250
|
|
|
1139
1251
|
/** A regular animation on a bound property takes it over; the value keeps its other bindings. */
|
|
@@ -61,13 +61,18 @@ export function headerGeometry(
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
/** Keep
|
|
64
|
+
/** Keep collapsed rows relative to the pinned edge; keep expanded content in place. */
|
|
65
65
|
export function resizedOffset(
|
|
66
66
|
offset: number,
|
|
67
67
|
before: ScrollHeaderGeometry,
|
|
68
68
|
after: ScrollHeaderGeometry,
|
|
69
69
|
) {
|
|
70
|
-
|
|
70
|
+
if (offset <= 0) return 0
|
|
71
|
+
// Growing the compact title/section must not cover the previously visible row.
|
|
72
|
+
// Its position relative to the pinned bottom is range + row position - offset.
|
|
73
|
+
const delta =
|
|
74
|
+
offset >= before.range ? after.range - before.range : after.spacer - before.spacer
|
|
75
|
+
return Math.max(0, offset + delta)
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
export function restoredOffset(
|
|
@@ -137,6 +137,7 @@ export function scrollHeader(list: ListView, options: ScrollHeaderOptions): Scro
|
|
|
137
137
|
const host = list.parent
|
|
138
138
|
const saved = {
|
|
139
139
|
clip: host.clipToBounds,
|
|
140
|
+
nativeClip: isIOS ? (host.nativeViewProtected as UIView)?.clipsToBounds : undefined,
|
|
140
141
|
headerClip: header.clipToBounds,
|
|
141
142
|
headerY: header.translateY,
|
|
142
143
|
titleY: title.translateY,
|
|
@@ -284,6 +285,13 @@ export function scrollHeader(list: ListView, options: ScrollHeaderOptions): Scro
|
|
|
284
285
|
}
|
|
285
286
|
const measure = async () => {
|
|
286
287
|
if (ended || !list.isLoaded || !header.isLoaded) return
|
|
288
|
+
// Core's default-valued property can be true while a plain UIView still
|
|
289
|
+
// has clipping disabled. Apply it when the native host exists, too.
|
|
290
|
+
if (isIOS && host.nativeViewProtected) {
|
|
291
|
+
const container = host.nativeViewProtected as UIView
|
|
292
|
+
saved.nativeClip ??= container.clipsToBounds
|
|
293
|
+
container.clipsToBounds = true
|
|
294
|
+
}
|
|
287
295
|
const generation = revision
|
|
288
296
|
await layout(host)
|
|
289
297
|
if (ended || generation !== revision) return
|
|
@@ -509,6 +517,10 @@ export function scrollHeader(list: ListView, options: ScrollHeaderOptions): Scro
|
|
|
509
517
|
set(artwork, { y: saved.artworkY, scaleX: saved.scaleX, scaleY: saved.scaleY })
|
|
510
518
|
artwork.originY = saved.originY
|
|
511
519
|
host.clipToBounds = saved.clip
|
|
520
|
+
if (isIOS && host.nativeViewProtected && saved.nativeClip !== undefined) {
|
|
521
|
+
const container = host.nativeViewProtected as UIView
|
|
522
|
+
if (container.clipsToBounds) container.clipsToBounds = saved.nativeClip
|
|
523
|
+
}
|
|
512
524
|
header.clipToBounds = saved.headerClip
|
|
513
525
|
options.setSpacerHeight(options.originalSpacerHeight ?? 0)
|
|
514
526
|
if (owners.get(list) === control) owners.delete(list)
|
package/src/shapes/index.ts
CHANGED
|
@@ -44,7 +44,7 @@ function resolve(base: ShapeFrame, values: ShapeValues): ShapeFrame {
|
|
|
44
44
|
)
|
|
45
45
|
return { path, start, end }
|
|
46
46
|
}
|
|
47
|
-
/** Draw an owned native vector surface over a view.
|
|
47
|
+
/** Draw an owned native vector surface over a view. Attaches once the view is loaded. */
|
|
48
48
|
export function shape(target: Target, options: ShapeOptions): Shape {
|
|
49
49
|
if (!options || typeof options !== 'object')
|
|
50
50
|
throw new TypeError('Shape options are required.')
|
|
@@ -77,8 +77,6 @@ export function shape(target: Target, options: ShapeOptions): Shape {
|
|
|
77
77
|
)
|
|
78
78
|
throw new TypeError('Invalid shape cap, join or fill rule.')
|
|
79
79
|
const view = toView(target)
|
|
80
|
-
if (!view.isLoaded || !view.nativeViewProtected)
|
|
81
|
-
throw new Error('Create shapes after native layout.')
|
|
82
80
|
if (owners.has(view))
|
|
83
81
|
throw new Error('This view already owns a shape. Dispose it first.')
|
|
84
82
|
return new ShapeOwner(view, style, frame)
|
|
@@ -92,6 +90,7 @@ class ShapeOwner implements Shape {
|
|
|
92
90
|
track: Track
|
|
93
91
|
from: ShapeFrame
|
|
94
92
|
to: ShapeFrame
|
|
93
|
+
started: boolean
|
|
95
94
|
}
|
|
96
95
|
private suspended = false
|
|
97
96
|
private backgrounded = false
|
|
@@ -101,7 +100,7 @@ class ShapeOwner implements Shape {
|
|
|
101
100
|
private readonly style: ShapeStyle,
|
|
102
101
|
private frame: ShapeFrame,
|
|
103
102
|
) {
|
|
104
|
-
this.attach()
|
|
103
|
+
if (view.isLoaded && view.nativeViewProtected) this.attach()
|
|
105
104
|
owners.set(view, this)
|
|
106
105
|
view.on('loaded', this.layout)
|
|
107
106
|
view.on('layoutChanged', this.layout)
|
|
@@ -128,7 +127,8 @@ class ShapeOwner implements Shape {
|
|
|
128
127
|
if (this.native && this.native !== this.view.nativeViewProtected) this.unload()
|
|
129
128
|
if (!this.painter) {
|
|
130
129
|
this.attach()
|
|
131
|
-
if (this.active)
|
|
130
|
+
if (this.active && !this.active.started) this.begin()
|
|
131
|
+
else if (this.active) {
|
|
132
132
|
const { from, to, track, animation } = this.active
|
|
133
133
|
this.painter!.play(from, to, track)
|
|
134
134
|
this.painter!.control(animation.time, true)
|
|
@@ -140,6 +140,18 @@ class ShapeOwner implements Shape {
|
|
|
140
140
|
this.suspended = false
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
|
+
private begin() {
|
|
144
|
+
const { from, to, track, animation } = this.active!
|
|
145
|
+
this.active!.started = true
|
|
146
|
+
if (track.end > 0) {
|
|
147
|
+
this.painter!.play(from, to, track)
|
|
148
|
+
animation.seekTime(0)
|
|
149
|
+
} else {
|
|
150
|
+
this.frame = to
|
|
151
|
+
this.painter!.paint(to)
|
|
152
|
+
}
|
|
153
|
+
animation.start()
|
|
154
|
+
}
|
|
143
155
|
private unload = () => {
|
|
144
156
|
if (this.backgrounded && this.active) this.frame = this.painter?.read() ?? this.frame
|
|
145
157
|
else this.stop()
|
|
@@ -180,9 +192,7 @@ class ShapeOwner implements Shape {
|
|
|
180
192
|
}
|
|
181
193
|
animate(values: ShapeValues, options: Transition | SpringName = {}) {
|
|
182
194
|
this.alive()
|
|
183
|
-
|
|
184
|
-
throw new Error('Load the shape before animating.')
|
|
185
|
-
const from = this.painter.read(),
|
|
195
|
+
const from = this.painter?.read() ?? this.frame,
|
|
186
196
|
to = resolve(from, values)
|
|
187
197
|
compatible(from.path, to.path)
|
|
188
198
|
if (typeof options === 'string' && !Object.hasOwn(springs, options))
|
|
@@ -229,8 +239,9 @@ class ShapeOwner implements Shape {
|
|
|
229
239
|
if (this.active?.animation === animation) this.active = undefined
|
|
230
240
|
},
|
|
231
241
|
)
|
|
232
|
-
this.active = { animation, track, from, to }
|
|
242
|
+
this.active = { animation, track, from, to, started: !!this.painter }
|
|
233
243
|
try {
|
|
244
|
+
if (!this.painter) return animation
|
|
234
245
|
if (track.end > 0) this.painter.play(from, to, track)
|
|
235
246
|
else {
|
|
236
247
|
this.frame = to
|
package/src/types.ts
CHANGED
|
@@ -53,9 +53,12 @@ export type Values = {
|
|
|
53
53
|
[K in KeyframeProperty]?: Exclude<Keyframes[K], readonly unknown[]>
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
/** A view, or anything that holds one: a NativeScript-Vue element, an Angular `ElementRef`. */
|
|
56
|
+
/** A view, or anything that holds one: a NativeScript-Vue element or component instance, an Angular `ElementRef`. */
|
|
57
57
|
export type Target =
|
|
58
|
-
|
|
58
|
+
| View
|
|
59
|
+
| { readonly nativeView: View }
|
|
60
|
+
| { readonly nativeElement: View }
|
|
61
|
+
| { readonly $el: View | { readonly nativeView: View } }
|
|
59
62
|
|
|
60
63
|
/** Translation velocities in DIPs per second. Omitted axes keep their current motion. */
|
|
61
64
|
export interface DecayVelocity {
|
package/src/value-track.ts
CHANGED
|
@@ -15,22 +15,12 @@ export function valueTrack(parts: readonly ValuePart[], rest: number) {
|
|
|
15
15
|
sum + part.base + part.span * part.track.valueAt(part.elapsed + time),
|
|
16
16
|
rest,
|
|
17
17
|
)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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)
|
|
18
|
+
const span = compositionSpan(parts)
|
|
19
|
+
if (!span) return { track: instantTrack('x', value(0)), windowed: false }
|
|
20
|
+
const { duration, endless, windowed } = span
|
|
30
21
|
const steps = Math.max(1, Math.min(6000, Math.ceil((duration * 60) / 1000)))
|
|
31
22
|
const times = Array.from({ length: steps + 1 }, (_, i) => i / steps)
|
|
32
23
|
const values = times.map((time) => value(time * duration))
|
|
33
|
-
const endless = loop !== undefined
|
|
34
24
|
const at = (time: number) =>
|
|
35
25
|
value(
|
|
36
26
|
endless
|
|
@@ -51,8 +41,29 @@ export function valueTrack(parts: readonly ValuePart[], rest: number) {
|
|
|
51
41
|
valueAt: at,
|
|
52
42
|
velocityAt: (time) => (at(time + 0.5) - at(time - 0.5)) * 1000,
|
|
53
43
|
}
|
|
44
|
+
return { track, windowed }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* How long one sampled composition can play before it needs rebuilding. A rolling
|
|
49
|
+
* window repeats every four seconds; native composition avoids it entirely.
|
|
50
|
+
*/
|
|
51
|
+
export function compositionSpan(parts: readonly Pick<ValuePart, 'track' | 'elapsed'>[]) {
|
|
52
|
+
let finite = 0
|
|
53
|
+
const periods: number[] = []
|
|
54
|
+
for (const { track, elapsed } of parts) {
|
|
55
|
+
if (Number.isFinite(track.end)) finite = Math.max(finite, track.end - elapsed)
|
|
56
|
+
else if (elapsed < track.delay) finite = Math.max(finite, track.delay - elapsed)
|
|
57
|
+
else periods.push(Math.max(1, track.duration * (track.reverse ? 2 : 1)))
|
|
58
|
+
}
|
|
59
|
+
if (finite <= 0 && !periods.length) return undefined
|
|
60
|
+
const loop = finite <= 0 && periods.length ? commonPeriod(periods) : undefined
|
|
61
|
+
// Bound finite compositions too: a minutes-long base must not undersample a short pulse.
|
|
62
|
+
const duration = finite > 0 ? Math.min(finite, 4000) : (loop ?? 4000)
|
|
63
|
+
const endless = loop !== undefined
|
|
54
64
|
return {
|
|
55
|
-
|
|
65
|
+
duration,
|
|
66
|
+
endless,
|
|
56
67
|
windowed:
|
|
57
68
|
finite > duration ||
|
|
58
69
|
(!endless && parts.some(({ track }) => !Number.isFinite(track.end))),
|