@rafaelaugusto/jolt 0.1.0-rc.2 → 0.1.0-rc.3
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/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/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/package.json +1 -1
- package/platforms/android/java/org/rafaelaugusto/jolt/KeyframeFactory.java +26 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/ScrollHeader.java +3 -2
- 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/module.modulemap +1 -0
- package/src/android-plugin.d.ts +9 -0
- package/src/animate.ts +23 -5
- 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 +22 -0
- 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
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Application,
|
|
3
|
+
isIOS,
|
|
4
|
+
SharedTransition,
|
|
5
|
+
Trace,
|
|
6
|
+
Transition,
|
|
7
|
+
View,
|
|
8
|
+
type NavigationTransition,
|
|
9
|
+
} from '@nativescript/core'
|
|
10
|
+
import { onReducedMotion, reducedMotion, timeScale } from '../config.js'
|
|
11
|
+
import { springPhysics } from '../spring.js'
|
|
12
|
+
import { springs, type Spring } from '../transition.js'
|
|
13
|
+
|
|
14
|
+
export type InteractiveModalState =
|
|
15
|
+
| 'ready'
|
|
16
|
+
| 'presenting'
|
|
17
|
+
| 'idle'
|
|
18
|
+
| 'dragging'
|
|
19
|
+
| 'finishing'
|
|
20
|
+
| 'cancelling'
|
|
21
|
+
| 'closed'
|
|
22
|
+
| 'disposed'
|
|
23
|
+
export interface InteractiveModalEvent {
|
|
24
|
+
state: InteractiveModalState
|
|
25
|
+
outcome?: 'completed' | 'cancelled'
|
|
26
|
+
/** Modal heights per second; supplied only at release. */
|
|
27
|
+
release?: Readonly<{ progress: number; velocity: number }>
|
|
28
|
+
}
|
|
29
|
+
export interface InteractiveModalOptions {
|
|
30
|
+
spring?:
|
|
31
|
+
| keyof typeof springs
|
|
32
|
+
| Pick<Spring, 'duration' | 'bounce' | 'stiffness' | 'damping' | 'mass'>
|
|
33
|
+
/** Synchronous gesture gate. Core's cancelable:false is also respected. */
|
|
34
|
+
canBegin?: () => boolean
|
|
35
|
+
/** Boundaries only; gesture updates and settling execute in Objective-C. */
|
|
36
|
+
onState?: (event: InteractiveModalEvent) => void
|
|
37
|
+
}
|
|
38
|
+
export interface InteractiveModal {
|
|
39
|
+
readonly supported: boolean
|
|
40
|
+
/** Use for one Core showModal / NativeScript-Vue $showModal presentation. */
|
|
41
|
+
readonly transition: NavigationTransition | undefined
|
|
42
|
+
readonly state: InteractiveModalState
|
|
43
|
+
/** Close once, forwarding Core's callback arguments. Queues during opening. */
|
|
44
|
+
close(...args: unknown[]): boolean
|
|
45
|
+
/** Roll back a held drag; an already committed release finishes normally. */
|
|
46
|
+
cancel(): void
|
|
47
|
+
/** Detach without closing the modal. Core closeModal remains available. */
|
|
48
|
+
dispose(): void
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A full-window iOS modal transition using NativeScript's modal owner/delegate. */
|
|
52
|
+
export function interactiveModal(
|
|
53
|
+
options: InteractiveModalOptions = {},
|
|
54
|
+
): InteractiveModal {
|
|
55
|
+
const input =
|
|
56
|
+
typeof options.spring === 'string' ? springs[options.spring] : options.spring
|
|
57
|
+
if (options.spring !== undefined && !input) throw new TypeError('Unknown modal spring.')
|
|
58
|
+
for (const [key, value] of Object.entries(input ?? {}) as [string, unknown][]) {
|
|
59
|
+
if (!['type', 'duration', 'bounce', 'stiffness', 'damping', 'mass'].includes(key))
|
|
60
|
+
throw new TypeError(`Unsupported modal spring option: ${key}.`)
|
|
61
|
+
if (key === 'type' && value !== 'spring')
|
|
62
|
+
throw new TypeError('Modal motion requires a spring.')
|
|
63
|
+
if (key !== 'type' && (typeof value !== 'number' || !Number.isFinite(value)))
|
|
64
|
+
throw new RangeError(`${key} must be finite.`)
|
|
65
|
+
if (['duration', 'stiffness', 'mass'].includes(key) && (value as number) <= 0)
|
|
66
|
+
throw new RangeError(`${key} must be positive.`)
|
|
67
|
+
if (key === 'damping' && (value as number) < 0)
|
|
68
|
+
throw new RangeError('damping must be nonnegative.')
|
|
69
|
+
}
|
|
70
|
+
const physics = springPhysics(input ?? springs.smooth)
|
|
71
|
+
if (Object.values(physics).some((value) => !Number.isFinite(value)))
|
|
72
|
+
throw new RangeError('Modal spring physics must be finite.')
|
|
73
|
+
const supported =
|
|
74
|
+
isIOS &&
|
|
75
|
+
typeof JoltInteractiveModal !== 'undefined' &&
|
|
76
|
+
typeof View.closedModallyEvent === 'string'
|
|
77
|
+
let state: InteractiveModalState = 'ready'
|
|
78
|
+
let transition: NavigationTransition | undefined
|
|
79
|
+
let view: View | undefined, native: JoltInteractiveModal | undefined
|
|
80
|
+
let start: (() => void) | undefined, offMotion: (() => void) | undefined
|
|
81
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
82
|
+
let pendingClose: unknown[] | undefined
|
|
83
|
+
let disposing = false,
|
|
84
|
+
suspended = false,
|
|
85
|
+
shown = false,
|
|
86
|
+
returning = false
|
|
87
|
+
const update = (value: {
|
|
88
|
+
interactiveBegan?: boolean
|
|
89
|
+
interactiveCancelled?: boolean
|
|
90
|
+
}) => {
|
|
91
|
+
if (transition?.instance) SharedTransition.updateState(transition.instance.id, value)
|
|
92
|
+
}
|
|
93
|
+
const emit = (
|
|
94
|
+
next: InteractiveModalState,
|
|
95
|
+
outcome?: InteractiveModalEvent['outcome'],
|
|
96
|
+
release?: InteractiveModalEvent['release'],
|
|
97
|
+
) => {
|
|
98
|
+
if (state === 'disposed') return
|
|
99
|
+
state = next
|
|
100
|
+
try {
|
|
101
|
+
options.onState?.({
|
|
102
|
+
state,
|
|
103
|
+
...(outcome ? { outcome } : {}),
|
|
104
|
+
...(release ? { release } : {}),
|
|
105
|
+
})
|
|
106
|
+
} catch (error) {
|
|
107
|
+
Trace.write(
|
|
108
|
+
`Interactive modal observer failed: ${String(error)}`,
|
|
109
|
+
'Jolt',
|
|
110
|
+
Trace.messageType.error,
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const release = () => {
|
|
115
|
+
if (state === 'disposed') return
|
|
116
|
+
if (timer !== undefined) clearTimeout(timer)
|
|
117
|
+
timer = undefined
|
|
118
|
+
view?.off('shownModally', arrived)
|
|
119
|
+
view?.off('closedModally', closed)
|
|
120
|
+
view?.off('unloaded', unloaded)
|
|
121
|
+
Application.off(Application.suspendEvent, suspend)
|
|
122
|
+
Application.off(Application.resumeEvent, resume)
|
|
123
|
+
offMotion?.()
|
|
124
|
+
offMotion = undefined
|
|
125
|
+
native?.dispose()
|
|
126
|
+
native = undefined
|
|
127
|
+
if (transition?.instance) SharedTransition.finishState(transition.instance.id)
|
|
128
|
+
view = undefined
|
|
129
|
+
start = undefined
|
|
130
|
+
pendingClose = undefined
|
|
131
|
+
emit('disposed')
|
|
132
|
+
}
|
|
133
|
+
const cancelled = () => {
|
|
134
|
+
if (state === 'disposed') return
|
|
135
|
+
// UIKit's dismissal completion must observe interactiveCancelled before we
|
|
136
|
+
// clear it. Core clears interactiveBegan after preserving the modal's owner.
|
|
137
|
+
const core = transition?.instance && SharedTransition.getState(transition.instance.id)
|
|
138
|
+
if (core?.interactiveBegan) {
|
|
139
|
+
timer = setTimeout(cancelled, 10)
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
timer = undefined
|
|
143
|
+
update({ interactiveCancelled: false })
|
|
144
|
+
emit('idle', 'cancelled')
|
|
145
|
+
if (disposing) release()
|
|
146
|
+
}
|
|
147
|
+
const arrived = () => {
|
|
148
|
+
shown = true
|
|
149
|
+
// UIKit skips animator creation for Core's animated:false. Keep ordinary
|
|
150
|
+
// close usable; no native interactive recognizer is attached in that mode.
|
|
151
|
+
if (state === 'ready') emit('idle')
|
|
152
|
+
if (pendingClose && state === 'idle') {
|
|
153
|
+
const args = pendingClose
|
|
154
|
+
pendingClose = undefined
|
|
155
|
+
control.close(...args)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const closed = () => {
|
|
159
|
+
if (state === 'disposed' || state === 'closed') return
|
|
160
|
+
emit('closed', 'completed')
|
|
161
|
+
release()
|
|
162
|
+
}
|
|
163
|
+
const checkDetached = () => {
|
|
164
|
+
timer = undefined
|
|
165
|
+
if (
|
|
166
|
+
!suspended &&
|
|
167
|
+
view &&
|
|
168
|
+
!view.isLoaded &&
|
|
169
|
+
!view.viewController?.presentingViewController
|
|
170
|
+
)
|
|
171
|
+
control.dispose()
|
|
172
|
+
}
|
|
173
|
+
const unloaded = () => {
|
|
174
|
+
if (state === 'cancelling') return
|
|
175
|
+
if (timer !== undefined) clearTimeout(timer)
|
|
176
|
+
timer = setTimeout(checkDetached, 0)
|
|
177
|
+
}
|
|
178
|
+
const suspend = () => {
|
|
179
|
+
suspended = true
|
|
180
|
+
native?.cancel()
|
|
181
|
+
}
|
|
182
|
+
const resume = () => {
|
|
183
|
+
suspended = false
|
|
184
|
+
if (state !== 'cancelling') unloaded()
|
|
185
|
+
}
|
|
186
|
+
class ModalMotion extends Transition {
|
|
187
|
+
override setupInteractiveGesture(callback: () => void, modal: View) {
|
|
188
|
+
if (view || state === 'disposed')
|
|
189
|
+
throw new Error('Use a fresh interactiveModal for each presentation.')
|
|
190
|
+
view = modal
|
|
191
|
+
start = callback
|
|
192
|
+
view.on('shownModally', arrived)
|
|
193
|
+
view.on('closedModally', closed)
|
|
194
|
+
view.on('unloaded', unloaded)
|
|
195
|
+
}
|
|
196
|
+
override iosPresentedController(presented: UIViewController) {
|
|
197
|
+
if (native || state === 'disposed' || !view) return undefined
|
|
198
|
+
returning = false
|
|
199
|
+
native = JoltInteractiveModal.attachStiffnessDampingMassShouldBeginOnEvent(
|
|
200
|
+
presented,
|
|
201
|
+
physics.stiffness,
|
|
202
|
+
physics.damping,
|
|
203
|
+
physics.mass,
|
|
204
|
+
() => {
|
|
205
|
+
try {
|
|
206
|
+
return (
|
|
207
|
+
!disposing &&
|
|
208
|
+
shown &&
|
|
209
|
+
state === 'idle' &&
|
|
210
|
+
!suspended &&
|
|
211
|
+
(options.canBegin?.() ?? true) === true
|
|
212
|
+
)
|
|
213
|
+
} catch {
|
|
214
|
+
return false
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
(event) => {
|
|
218
|
+
if (state === 'disposed') return
|
|
219
|
+
if (event === 2) {
|
|
220
|
+
update({ interactiveBegan: true, interactiveCancelled: false })
|
|
221
|
+
emit('dragging')
|
|
222
|
+
start?.()
|
|
223
|
+
} else if (event === 3 || event === 4) {
|
|
224
|
+
update({ interactiveCancelled: event === 4 })
|
|
225
|
+
emit(
|
|
226
|
+
event === 3 ? 'finishing' : 'cancelling',
|
|
227
|
+
undefined,
|
|
228
|
+
native && { progress: native.progress, velocity: native.velocity },
|
|
229
|
+
)
|
|
230
|
+
} else if (event === 0) emit(returning ? 'finishing' : 'presenting')
|
|
231
|
+
else if (event === 1) {
|
|
232
|
+
emit('idle')
|
|
233
|
+
if (disposing) release()
|
|
234
|
+
else if (shown) arrived()
|
|
235
|
+
} else if (event === 6) cancelled()
|
|
236
|
+
// Core's closedModally event owns final disposal and callback delivery.
|
|
237
|
+
},
|
|
238
|
+
)
|
|
239
|
+
native.preparePresentingReducedTimeScale(true, reducedMotion(), timeScale())
|
|
240
|
+
return native
|
|
241
|
+
}
|
|
242
|
+
override iosDismissedController() {
|
|
243
|
+
if (!native || state === 'disposed') return undefined
|
|
244
|
+
returning = true
|
|
245
|
+
native.preparePresentingReducedTimeScale(false, reducedMotion(), timeScale())
|
|
246
|
+
return native
|
|
247
|
+
}
|
|
248
|
+
override iosInteractionDismiss() {
|
|
249
|
+
return native?.interactive ? native : undefined
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const control: InteractiveModal = {
|
|
253
|
+
supported,
|
|
254
|
+
get transition() {
|
|
255
|
+
return transition
|
|
256
|
+
},
|
|
257
|
+
get state() {
|
|
258
|
+
return state
|
|
259
|
+
},
|
|
260
|
+
close(...args) {
|
|
261
|
+
if (
|
|
262
|
+
!supported ||
|
|
263
|
+
disposing ||
|
|
264
|
+
['closed', 'disposed', 'dragging', 'finishing', 'cancelling'].includes(state)
|
|
265
|
+
)
|
|
266
|
+
return false
|
|
267
|
+
if (!shown || state === 'presenting' || state === 'ready') {
|
|
268
|
+
if (pendingClose) return false
|
|
269
|
+
pendingClose = args
|
|
270
|
+
return true
|
|
271
|
+
}
|
|
272
|
+
if (!view) return false
|
|
273
|
+
update({ interactiveBegan: false, interactiveCancelled: false })
|
|
274
|
+
emit('finishing')
|
|
275
|
+
view.closeModal(...args)
|
|
276
|
+
return true
|
|
277
|
+
},
|
|
278
|
+
cancel() {
|
|
279
|
+
native?.cancel()
|
|
280
|
+
},
|
|
281
|
+
dispose() {
|
|
282
|
+
if (state === 'disposed' || disposing) return
|
|
283
|
+
disposing = true
|
|
284
|
+
pendingClose = undefined
|
|
285
|
+
native?.cancel()
|
|
286
|
+
if (
|
|
287
|
+
state !== 'dragging' &&
|
|
288
|
+
state !== 'cancelling' &&
|
|
289
|
+
state !== 'finishing' &&
|
|
290
|
+
state !== 'presenting'
|
|
291
|
+
)
|
|
292
|
+
release()
|
|
293
|
+
},
|
|
294
|
+
}
|
|
295
|
+
if (supported) {
|
|
296
|
+
transition = SharedTransition.custom(new ModalMotion(), {
|
|
297
|
+
interactive: { dismiss: {} },
|
|
298
|
+
})
|
|
299
|
+
Application.on(Application.suspendEvent, suspend)
|
|
300
|
+
Application.on(Application.resumeEvent, resume)
|
|
301
|
+
offMotion = onReducedMotion((value) => {
|
|
302
|
+
if (value) control.cancel()
|
|
303
|
+
})
|
|
304
|
+
}
|
|
305
|
+
return control
|
|
306
|
+
}
|
package/src/ios-plugin.d.ts
CHANGED
|
@@ -343,6 +343,28 @@ declare class JoltInteractiveNavigation extends NSObject {
|
|
|
343
343
|
cancel(): void
|
|
344
344
|
disposeRestoringSwipe(enabled: boolean): void
|
|
345
345
|
}
|
|
346
|
+
declare class JoltInteractiveModal extends NSObject {
|
|
347
|
+
static attachStiffnessDampingMassShouldBeginOnEvent(
|
|
348
|
+
controller: UIViewController,
|
|
349
|
+
stiffness: number,
|
|
350
|
+
damping: number,
|
|
351
|
+
mass: number,
|
|
352
|
+
shouldBegin: () => boolean,
|
|
353
|
+
onEvent: (event: number) => void,
|
|
354
|
+
): JoltInteractiveModal
|
|
355
|
+
static activeCount(): number
|
|
356
|
+
static recognizerCount(): number
|
|
357
|
+
readonly interactive: boolean
|
|
358
|
+
readonly progress: number
|
|
359
|
+
readonly velocity: number
|
|
360
|
+
preparePresentingReducedTimeScale(
|
|
361
|
+
presenting: boolean,
|
|
362
|
+
reduced: boolean,
|
|
363
|
+
timeScale: number,
|
|
364
|
+
): void
|
|
365
|
+
cancel(): void
|
|
366
|
+
dispose(): void
|
|
367
|
+
}
|
|
346
368
|
|
|
347
369
|
declare class JoltCircleWorld extends NSObject {
|
|
348
370
|
static createOptions(container: UIView, options: string): JoltCircleWorld
|
|
@@ -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 {
|