@rafaelaugusto/jolt 0.1.0-beta.13 → 0.1.0-beta.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -9
- package/dist/driver/android-gradient.d.ts.map +1 -1
- package/dist/driver/android-gradient.js +11 -1
- package/dist/driver/android-gradient.js.map +1 -1
- package/dist/driver/android-particles.d.ts +6 -0
- package/dist/driver/android-particles.d.ts.map +1 -0
- package/dist/driver/android-particles.js +93 -0
- package/dist/driver/android-particles.js.map +1 -0
- package/dist/driver/ios-gradient.d.ts.map +1 -1
- package/dist/driver/ios-gradient.js +24 -2
- package/dist/driver/ios-gradient.js.map +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +9 -3
- package/dist/engine.js.map +1 -1
- package/dist/gradient-binding.d.ts +38 -0
- package/dist/gradient-binding.d.ts.map +1 -0
- package/dist/gradient-binding.js +128 -0
- package/dist/gradient-binding.js.map +1 -0
- package/dist/gradient.d.ts +19 -4
- package/dist/gradient.d.ts.map +1 -1
- package/dist/gradient.js +331 -70
- package/dist/gradient.js.map +1 -1
- package/dist/haptics.d.ts.map +1 -1
- package/dist/haptics.js +101 -28
- package/dist/haptics.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/motion-scope.d.ts +3 -1
- package/dist/motion-scope.d.ts.map +1 -1
- package/dist/motion-scope.js +14 -1
- package/dist/motion-scope.js.map +1 -1
- package/dist/motion-value.d.ts +17 -0
- package/dist/motion-value.d.ts.map +1 -1
- package/dist/motion-value.js +38 -0
- package/dist/motion-value.js.map +1 -1
- package/dist/native/index.d.ts +2 -0
- package/dist/native/index.d.ts.map +1 -1
- package/dist/native/index.js +2 -0
- package/dist/native/index.js.map +1 -1
- package/dist/particles.d.ts +5 -2
- package/dist/particles.d.ts.map +1 -1
- package/dist/particles.js +16 -10
- package/dist/particles.js.map +1 -1
- package/dist/scene-appearance.d.ts +14 -0
- package/dist/scene-appearance.d.ts.map +1 -0
- package/dist/scene-appearance.js +68 -0
- package/dist/scene-appearance.js.map +1 -0
- package/dist/scene-camera.d.ts +5 -0
- package/dist/scene-camera.d.ts.map +1 -0
- package/dist/scene-camera.js +23 -0
- package/dist/scene-camera.js.map +1 -0
- package/dist/size-layout.d.ts +19 -0
- package/dist/size-layout.d.ts.map +1 -0
- package/dist/size-layout.js +216 -0
- package/dist/size-layout.js.map +1 -0
- package/dist/size.d.ts +19 -0
- package/dist/size.d.ts.map +1 -0
- package/dist/size.js +251 -0
- package/dist/size.js.map +1 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/vue/index.d.ts.map +1 -1
- package/dist/vue/index.js +28 -5
- package/dist/vue/index.js.map +1 -1
- package/package.json +1 -1
- package/platforms/android/java/org/rafaelaugusto/jolt/GradientLayer.java +30 -1
- package/platforms/android/java/org/rafaelaugusto/jolt/ParticleEmitter.java +192 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/ParticleSimulation.java +114 -0
- package/platforms/android/proguard-rules.pro +2 -0
- package/src/android-plugin.d.ts +56 -0
- package/src/driver/android-gradient.ts +8 -1
- package/src/driver/android-particles.ts +118 -0
- package/src/driver/ios-gradient.ts +24 -2
- package/src/engine.ts +10 -4
- package/src/gradient-binding.ts +158 -0
- package/src/gradient.ts +376 -91
- package/src/haptics.ts +106 -28
- package/src/index.ts +3 -0
- package/src/ios-plugin.d.ts +10 -0
- package/src/motion-scope.ts +23 -2
- package/src/motion-value.ts +43 -0
- package/src/native/index.ts +8 -0
- package/src/particles.ts +18 -17
- package/src/scene-appearance.ts +92 -0
- package/src/scene-camera.ts +30 -0
- package/src/size-layout.ts +237 -0
- package/src/size.ts +302 -0
- package/src/types.ts +1 -1
- package/src/vue/index.ts +29 -5
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { GridLayout, StackLayout, Utils, View, unsetValue } from '@nativescript/core'
|
|
2
|
+
|
|
3
|
+
export type SizeAxis = 'width' | 'height'
|
|
4
|
+
export type SizeValue = number | 'auto'
|
|
5
|
+
export type SizeValues = Partial<Record<SizeAxis, SizeValue>>
|
|
6
|
+
export type SizePixels = Record<SizeAxis, number>
|
|
7
|
+
|
|
8
|
+
type MeasuredView = View & {
|
|
9
|
+
_currentWidthMeasureSpec?: number
|
|
10
|
+
_currentHeightMeasureSpec?: number
|
|
11
|
+
_oldLeft?: number
|
|
12
|
+
_oldTop?: number
|
|
13
|
+
_oldRight?: number
|
|
14
|
+
_oldBottom?: number
|
|
15
|
+
}
|
|
16
|
+
const overlays = new WeakMap<View, Map<SizeAxis, object>>()
|
|
17
|
+
|
|
18
|
+
/** One live tree: speculative measurement never lays out or duplicates its views. */
|
|
19
|
+
export function sizeLayout(
|
|
20
|
+
view: View,
|
|
21
|
+
axes: readonly SizeAxis[],
|
|
22
|
+
replacing: readonly SizeAxis[] = [],
|
|
23
|
+
read: () => SizePixels = () => view.getActualSize(),
|
|
24
|
+
) {
|
|
25
|
+
const parent = view.parent
|
|
26
|
+
if (
|
|
27
|
+
view.isLoaded &&
|
|
28
|
+
!(parent instanceof StackLayout) &&
|
|
29
|
+
!(parent instanceof GridLayout)
|
|
30
|
+
)
|
|
31
|
+
throw new TypeError('animateSize needs a child of StackLayout or GridLayout.')
|
|
32
|
+
const style = view.style as unknown as Record<string, unknown>
|
|
33
|
+
for (const axis of axes) {
|
|
34
|
+
const initial = view[axis]
|
|
35
|
+
if (
|
|
36
|
+
typeof initial === 'object' &&
|
|
37
|
+
initial !== null &&
|
|
38
|
+
(initial.unit === 'dip' || initial.unit === 'px')
|
|
39
|
+
)
|
|
40
|
+
dimension(initial.value, `initial ${axis}`)
|
|
41
|
+
else dimension(initial, `initial ${axis}`)
|
|
42
|
+
const key = `keyframe:${axis}`
|
|
43
|
+
if (
|
|
44
|
+
!replacing.includes(axis) &&
|
|
45
|
+
style[key] !== unsetValue &&
|
|
46
|
+
style[key] !== undefined
|
|
47
|
+
)
|
|
48
|
+
throw new Error(`animateSize cannot own ${axis} during a CSS keyframe animation.`)
|
|
49
|
+
}
|
|
50
|
+
let writing = false
|
|
51
|
+
let disposed = false
|
|
52
|
+
let claimed = false
|
|
53
|
+
const identity = {}
|
|
54
|
+
const claim = () => {
|
|
55
|
+
if (claimed || disposed) return
|
|
56
|
+
claimed = true
|
|
57
|
+
const current = overlays.get(view) ?? new Map<SizeAxis, object>()
|
|
58
|
+
for (const axis of axes) current.set(axis, identity)
|
|
59
|
+
overlays.set(view, current)
|
|
60
|
+
}
|
|
61
|
+
const owns = (axis: SizeAxis) => overlays.get(view)?.get(axis) === identity
|
|
62
|
+
let releaseHooks = () => {}
|
|
63
|
+
const write = (values: SizeValues) => {
|
|
64
|
+
if (disposed) return
|
|
65
|
+
claim()
|
|
66
|
+
writing = true
|
|
67
|
+
try {
|
|
68
|
+
for (const axis of axes)
|
|
69
|
+
if (owns(axis) && values[axis] !== undefined)
|
|
70
|
+
style[`keyframe:${axis}`] = bounded(view, axis, values[axis]!)
|
|
71
|
+
} finally {
|
|
72
|
+
writing = false
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const clear = () => {
|
|
76
|
+
writing = true
|
|
77
|
+
try {
|
|
78
|
+
for (const axis of axes) if (owns(axis)) style[`keyframe:${axis}`] = unsetValue
|
|
79
|
+
} finally {
|
|
80
|
+
writing = false
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const root = () => {
|
|
84
|
+
let node = view as MeasuredView
|
|
85
|
+
while (node.parent instanceof View && node.parent.typeName !== 'Page')
|
|
86
|
+
node = node.parent as MeasuredView
|
|
87
|
+
return node
|
|
88
|
+
}
|
|
89
|
+
const specs = (node: MeasuredView) => {
|
|
90
|
+
// Core's last layout allocation excludes transforms; UIView.frame does not.
|
|
91
|
+
const cached = [node._oldLeft, node._oldTop, node._oldRight, node._oldBottom]
|
|
92
|
+
const bounds =
|
|
93
|
+
cached.every((value) => Number.isFinite(value)) &&
|
|
94
|
+
cached[2]! > cached[0]! &&
|
|
95
|
+
cached[3]! > cached[1]!
|
|
96
|
+
? { left: cached[0]!, top: cached[1]!, right: cached[2]!, bottom: cached[3]! }
|
|
97
|
+
: node._getCurrentLayoutBounds()
|
|
98
|
+
const layout = Utils.layout
|
|
99
|
+
return {
|
|
100
|
+
bounds,
|
|
101
|
+
width:
|
|
102
|
+
node._currentWidthMeasureSpec ??
|
|
103
|
+
layout.makeMeasureSpec(Math.max(0, bounds.right - bounds.left), layout.EXACTLY),
|
|
104
|
+
height:
|
|
105
|
+
node._currentHeightMeasureSpec ??
|
|
106
|
+
layout.makeMeasureSpec(Math.max(0, bounds.bottom - bounds.top), layout.EXACTLY),
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const measure = () => {
|
|
110
|
+
const node = root()
|
|
111
|
+
const spec = specs(node)
|
|
112
|
+
view.requestLayout()
|
|
113
|
+
node.measure(spec.width, spec.height)
|
|
114
|
+
return { node, ...spec }
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
read,
|
|
118
|
+
write,
|
|
119
|
+
ready() {
|
|
120
|
+
const { bounds } = specs(root())
|
|
121
|
+
return bounds.right > bounds.left && bounds.bottom > bounds.top
|
|
122
|
+
},
|
|
123
|
+
measure(destination: SizeValues): SizePixels {
|
|
124
|
+
const held = Object.fromEntries(
|
|
125
|
+
axes.map((axis) => [axis, style[`keyframe:${axis}`]]),
|
|
126
|
+
)
|
|
127
|
+
try {
|
|
128
|
+
write(destination)
|
|
129
|
+
measure()
|
|
130
|
+
const dip = Utils.layout.toDeviceIndependentPixels
|
|
131
|
+
return {
|
|
132
|
+
width: dip(view.getMeasuredWidth()),
|
|
133
|
+
height: dip(view.getMeasuredHeight()),
|
|
134
|
+
}
|
|
135
|
+
} finally {
|
|
136
|
+
writing = true
|
|
137
|
+
try {
|
|
138
|
+
for (const axis of axes)
|
|
139
|
+
if (!disposed && owns(axis)) style[`keyframe:${axis}`] = held[axis]
|
|
140
|
+
} finally {
|
|
141
|
+
writing = false
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
/** Flush the restored/current declarations before any draw can see probe measurements. */
|
|
146
|
+
layout() {
|
|
147
|
+
const { node, bounds } = measure()
|
|
148
|
+
node.layout(bounds.left, bounds.top, bounds.right, bounds.bottom)
|
|
149
|
+
},
|
|
150
|
+
commit(values?: SizeValues) {
|
|
151
|
+
writing = true
|
|
152
|
+
try {
|
|
153
|
+
if (values)
|
|
154
|
+
for (const axis of axes)
|
|
155
|
+
if (owns(axis) && values[axis] !== undefined)
|
|
156
|
+
view[axis] = bounded(view, axis, values[axis]!)
|
|
157
|
+
} finally {
|
|
158
|
+
writing = false
|
|
159
|
+
clear()
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
watch(takeover: () => void) {
|
|
163
|
+
claim()
|
|
164
|
+
const undo: (() => void)[] = []
|
|
165
|
+
releaseHooks = () =>
|
|
166
|
+
undo
|
|
167
|
+
.splice(0)
|
|
168
|
+
.reverse()
|
|
169
|
+
.forEach((off) => off())
|
|
170
|
+
// A local/CSS write hidden under the animation override still takes ownership.
|
|
171
|
+
for (const axis of axes)
|
|
172
|
+
for (const name of [axis, `css:${axis}`, `keyframe:${axis}`]) {
|
|
173
|
+
const own = Object.getOwnPropertyDescriptor(style, name)
|
|
174
|
+
let source: object | null = style
|
|
175
|
+
let descriptor: PropertyDescriptor | undefined
|
|
176
|
+
while (source && !descriptor) {
|
|
177
|
+
descriptor = Object.getOwnPropertyDescriptor(source, name)
|
|
178
|
+
source = Object.getPrototypeOf(source)
|
|
179
|
+
}
|
|
180
|
+
if (!descriptor?.set || !descriptor.get || own?.configurable === false)
|
|
181
|
+
throw new Error(`animateSize cannot observe ownership of ${name}.`)
|
|
182
|
+
const original = descriptor
|
|
183
|
+
const set = function (this: object, value: unknown) {
|
|
184
|
+
if (!writing) takeover()
|
|
185
|
+
original.set!.call(this, value)
|
|
186
|
+
}
|
|
187
|
+
Object.defineProperty(style, name, {
|
|
188
|
+
configurable: true,
|
|
189
|
+
get: original.get,
|
|
190
|
+
set,
|
|
191
|
+
})
|
|
192
|
+
undo.push(() => {
|
|
193
|
+
if (Object.getOwnPropertyDescriptor(style, name)?.set !== set) return
|
|
194
|
+
if (own) Object.defineProperty(style, name, own)
|
|
195
|
+
else delete style[name]
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
dispose() {
|
|
200
|
+
disposed = true
|
|
201
|
+
releaseHooks()
|
|
202
|
+
clear()
|
|
203
|
+
const current = overlays.get(view)
|
|
204
|
+
for (const axis of axes) if (owns(axis)) current!.delete(axis)
|
|
205
|
+
if (!current?.size) overlays.delete(view)
|
|
206
|
+
},
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function bounded(view: View, axis: SizeAxis, value: SizeValue): SizeValue {
|
|
211
|
+
if (value === 'auto') return value
|
|
212
|
+
const minimum = axis === 'width' ? view.minWidth : view.minHeight
|
|
213
|
+
const maximum = axis === 'width' ? view.maxWidth : view.maxHeight
|
|
214
|
+
const dip = Utils.layout.toDeviceIndependentPixels
|
|
215
|
+
const min =
|
|
216
|
+
typeof minimum === 'number'
|
|
217
|
+
? minimum
|
|
218
|
+
: dip((axis === 'width' ? view.effectiveMinWidth : view.effectiveMinHeight) ?? 0)
|
|
219
|
+
const max =
|
|
220
|
+
typeof maximum === 'number'
|
|
221
|
+
? maximum
|
|
222
|
+
: dip(
|
|
223
|
+
(axis === 'width' ? view.effectiveMaxWidth : view.effectiveMaxHeight) ??
|
|
224
|
+
Infinity,
|
|
225
|
+
)
|
|
226
|
+
return Math.min(max, Math.max(0, min, value))
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function dimension(value: unknown, label: string): asserts value is SizeValue {
|
|
230
|
+
if (
|
|
231
|
+
value !== 'auto' &&
|
|
232
|
+
(typeof value !== 'number' || !Number.isFinite(value) || value < 0)
|
|
233
|
+
)
|
|
234
|
+
throw new RangeError(
|
|
235
|
+
`animateSize ${label} must be a nonnegative finite DIP number or "auto"; percentages and unit objects are unsupported.`,
|
|
236
|
+
)
|
|
237
|
+
}
|
package/src/size.ts
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { Application } from '@nativescript/core'
|
|
2
|
+
import type { View } from '@nativescript/core'
|
|
3
|
+
import { toView } from './animate.js'
|
|
4
|
+
import { onReducedMotion, reducedMotion } from './config.js'
|
|
5
|
+
import { getDriver } from './driver/index.js'
|
|
6
|
+
import type { Animation } from './engine.js'
|
|
7
|
+
import { createClock, type PlaybackClock } from './playback.js'
|
|
8
|
+
import { createTrack, type Track } from './track.js'
|
|
9
|
+
import {
|
|
10
|
+
resolveTransition,
|
|
11
|
+
springs,
|
|
12
|
+
type Spring,
|
|
13
|
+
type SpringName,
|
|
14
|
+
type Tween,
|
|
15
|
+
} from './transition.js'
|
|
16
|
+
import {
|
|
17
|
+
dimension,
|
|
18
|
+
sizeLayout,
|
|
19
|
+
type SizeAxis,
|
|
20
|
+
type SizePixels,
|
|
21
|
+
type SizeValues,
|
|
22
|
+
} from './size-layout.js'
|
|
23
|
+
import type { Target } from './types.js'
|
|
24
|
+
|
|
25
|
+
export type SizeDimensions = SizeValues
|
|
26
|
+
export type SizeOptions =
|
|
27
|
+
| Omit<Spring, 'repeat' | 'repeatType' | 'repeatDelay'>
|
|
28
|
+
| Omit<Tween, 'repeat' | 'repeatType' | 'repeatDelay' | 'times' | 'easeTrack'>
|
|
29
|
+
export interface SizeAnimation extends Animation {
|
|
30
|
+
pause(): void
|
|
31
|
+
play(): void
|
|
32
|
+
readonly paused: boolean
|
|
33
|
+
/** Hold the current size; restore:true releases the temporary size back to the original declaration. */
|
|
34
|
+
stop(options?: { restore?: boolean }): void
|
|
35
|
+
}
|
|
36
|
+
const owners = new WeakMap<
|
|
37
|
+
View,
|
|
38
|
+
{ animation: SizeAnimation; axes: readonly SizeAxis[] }
|
|
39
|
+
>()
|
|
40
|
+
|
|
41
|
+
/** Animate the actual layout box, including intrinsic auto sizes. Layout runs in JS on the UI thread. */
|
|
42
|
+
export function animateSize(
|
|
43
|
+
target: Target,
|
|
44
|
+
dimensions: SizeDimensions,
|
|
45
|
+
options?: SizeOptions | SpringName,
|
|
46
|
+
): SizeAnimation
|
|
47
|
+
export function animateSize(
|
|
48
|
+
target: Target,
|
|
49
|
+
dimensions: SizeDimensions,
|
|
50
|
+
change: () => void,
|
|
51
|
+
options?: SizeOptions | SpringName,
|
|
52
|
+
): SizeAnimation
|
|
53
|
+
export function animateSize(
|
|
54
|
+
target: Target,
|
|
55
|
+
dimensions: SizeDimensions,
|
|
56
|
+
changeOrOptions?: (() => void) | SizeOptions | SpringName,
|
|
57
|
+
options?: SizeOptions | SpringName,
|
|
58
|
+
): SizeAnimation {
|
|
59
|
+
const view = toView(target)
|
|
60
|
+
if (!dimensions || typeof dimensions !== 'object' || Array.isArray(dimensions))
|
|
61
|
+
throw new TypeError('animateSize takes a width/height object.')
|
|
62
|
+
const axes = Object.keys(dimensions) as SizeAxis[]
|
|
63
|
+
if (!axes.length || axes.some((axis) => axis !== 'width' && axis !== 'height'))
|
|
64
|
+
throw new TypeError('animateSize needs width and/or height.')
|
|
65
|
+
for (const axis of axes) dimension(dimensions[axis], axis)
|
|
66
|
+
const destination = { ...dimensions }
|
|
67
|
+
const change = typeof changeOrOptions === 'function' ? changeOrOptions : undefined
|
|
68
|
+
if (change?.constructor.name === 'AsyncFunction')
|
|
69
|
+
throw new TypeError('animateSize changes must be synchronous.')
|
|
70
|
+
const transition = normalize(
|
|
71
|
+
change ? options : (changeOrOptions as SizeOptions | SpringName | undefined),
|
|
72
|
+
)
|
|
73
|
+
// Solve before ownership changes, including custom easing/physical-spring validation.
|
|
74
|
+
const validation = createTrack('x', [0, 100], transition, 0)
|
|
75
|
+
if (!Number.isFinite(validation.end) || !validation.values.every(Number.isFinite))
|
|
76
|
+
throw new RangeError('animateSize needs a finite curve with finite samples.')
|
|
77
|
+
const previous = owners.get(view)
|
|
78
|
+
const driver = getDriver()
|
|
79
|
+
const layout = sizeLayout(view, axes, previous?.axes, () => {
|
|
80
|
+
const frame = driver.layoutFrame?.(view)
|
|
81
|
+
return frame ? { width: frame.width, height: frame.height } : view.getActualSize()
|
|
82
|
+
})
|
|
83
|
+
// Release the previous overlay only after every caller-supplied input was validated.
|
|
84
|
+
const start = layout.read()
|
|
85
|
+
previous?.animation.stop({ restore: true })
|
|
86
|
+
let resolve!: (value: boolean) => void
|
|
87
|
+
let reject!: (error: unknown) => void
|
|
88
|
+
const finished = new Promise<boolean>((done, fail) => {
|
|
89
|
+
resolve = done
|
|
90
|
+
reject = fail
|
|
91
|
+
})
|
|
92
|
+
let settled = false
|
|
93
|
+
let paused = false
|
|
94
|
+
let initialized = false
|
|
95
|
+
let cancelFrame: (() => void) | undefined
|
|
96
|
+
let unwatch: (() => void) | undefined
|
|
97
|
+
let clock: PlaybackClock | undefined
|
|
98
|
+
let tracks = new Map<SizeAxis, Track>()
|
|
99
|
+
let endpoint: SizePixels | undefined
|
|
100
|
+
const cleanup = () => {
|
|
101
|
+
cancelFrame?.()
|
|
102
|
+
cancelFrame = undefined
|
|
103
|
+
clock?.cancel()
|
|
104
|
+
clock = undefined
|
|
105
|
+
unwatch?.()
|
|
106
|
+
layout.dispose()
|
|
107
|
+
view.off('unloaded', unloaded)
|
|
108
|
+
Application.off(Application.suspendEvent, unloaded)
|
|
109
|
+
if (owners.get(view)?.animation === animation) owners.delete(view)
|
|
110
|
+
}
|
|
111
|
+
const finish = (success: boolean, error?: unknown) => {
|
|
112
|
+
if (settled) return
|
|
113
|
+
settled = true
|
|
114
|
+
cleanup()
|
|
115
|
+
if (error !== undefined) reject(error)
|
|
116
|
+
else resolve(success)
|
|
117
|
+
}
|
|
118
|
+
const complete = () => {
|
|
119
|
+
layout.commit(destination)
|
|
120
|
+
layout.layout()
|
|
121
|
+
finish(true)
|
|
122
|
+
}
|
|
123
|
+
const stop = (restore = false) => {
|
|
124
|
+
if (settled) return
|
|
125
|
+
layout.commit(restore ? undefined : layout.read())
|
|
126
|
+
finish(false)
|
|
127
|
+
view.requestLayout()
|
|
128
|
+
}
|
|
129
|
+
function unloaded() {
|
|
130
|
+
stop(true)
|
|
131
|
+
}
|
|
132
|
+
const schedule = () => {
|
|
133
|
+
if (!cancelFrame && !settled && !paused) cancelFrame = driver.frame(tick)
|
|
134
|
+
}
|
|
135
|
+
function tick() {
|
|
136
|
+
cancelFrame = undefined
|
|
137
|
+
if (settled || paused) return
|
|
138
|
+
try {
|
|
139
|
+
if (!view.isLoaded || view.visibility === 'collapse' || !view.nativeViewProtected) {
|
|
140
|
+
stop(true)
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
const next = layout.measure(destination)
|
|
144
|
+
if (settled) return
|
|
145
|
+
if (axes.some((axis) => !Number.isFinite(next[axis]) || next[axis] < 0))
|
|
146
|
+
throw new Error('animateSize measured an invalid native size.')
|
|
147
|
+
if (reducedMotion()) {
|
|
148
|
+
complete()
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
if (
|
|
152
|
+
!initialized &&
|
|
153
|
+
axes.every((axis) => Math.abs(next[axis] - start[axis]) < 0.5)
|
|
154
|
+
) {
|
|
155
|
+
complete()
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
const elapsed = clock?.time() ?? 0
|
|
159
|
+
if (
|
|
160
|
+
!endpoint ||
|
|
161
|
+
axes.some((axis) => Math.abs(next[axis] - endpoint![axis]) > 0.5)
|
|
162
|
+
) {
|
|
163
|
+
const from = initialized ? layout.read() : start
|
|
164
|
+
const fresh = new Map<SizeAxis, Track>()
|
|
165
|
+
for (const axis of axes)
|
|
166
|
+
fresh.set(
|
|
167
|
+
axis,
|
|
168
|
+
createTrack(
|
|
169
|
+
'x',
|
|
170
|
+
[from[axis], next[axis]],
|
|
171
|
+
initialized ? { ...transition, delay: 0 } : transition,
|
|
172
|
+
tracks.get(axis)?.velocityAt(elapsed) ?? 0,
|
|
173
|
+
),
|
|
174
|
+
)
|
|
175
|
+
tracks = fresh
|
|
176
|
+
endpoint = next
|
|
177
|
+
clock?.cancel()
|
|
178
|
+
const end = Math.max(...[...tracks.values()].map((track) => track.end))
|
|
179
|
+
clock = driver.clock?.(end) ?? createClock(() => driver.now())
|
|
180
|
+
initialized = true
|
|
181
|
+
}
|
|
182
|
+
const time = clock!.time()
|
|
183
|
+
if ([...tracks.values()].every((track) => time >= track.end)) {
|
|
184
|
+
complete()
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
const values: SizeValues = {}
|
|
188
|
+
for (const [axis, track] of tracks) values[axis] = Math.max(0, track.valueAt(time))
|
|
189
|
+
layout.write(values)
|
|
190
|
+
layout.layout()
|
|
191
|
+
if (settled) return
|
|
192
|
+
schedule()
|
|
193
|
+
} catch (error) {
|
|
194
|
+
layout.commit()
|
|
195
|
+
view.requestLayout()
|
|
196
|
+
finish(false, error)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const animation: SizeAnimation = {
|
|
200
|
+
finished,
|
|
201
|
+
then: (done, fail) => finished.then(done, fail),
|
|
202
|
+
get paused() {
|
|
203
|
+
return paused
|
|
204
|
+
},
|
|
205
|
+
stop: (options) => stop(options?.restore === true),
|
|
206
|
+
pause() {
|
|
207
|
+
if (settled || paused) return
|
|
208
|
+
paused = true
|
|
209
|
+
clock?.pause()
|
|
210
|
+
cancelFrame?.()
|
|
211
|
+
cancelFrame = undefined
|
|
212
|
+
},
|
|
213
|
+
play() {
|
|
214
|
+
if (settled || !paused) return
|
|
215
|
+
paused = false
|
|
216
|
+
clock?.play()
|
|
217
|
+
schedule()
|
|
218
|
+
},
|
|
219
|
+
}
|
|
220
|
+
owners.set(view, { animation, axes })
|
|
221
|
+
try {
|
|
222
|
+
layout.watch(() => stop(true))
|
|
223
|
+
layout.write(start)
|
|
224
|
+
view.on('unloaded', unloaded)
|
|
225
|
+
Application.on(Application.suspendEvent, unloaded)
|
|
226
|
+
unwatch = onReducedMotion((reduced) => {
|
|
227
|
+
if (reduced && !settled) {
|
|
228
|
+
try {
|
|
229
|
+
complete()
|
|
230
|
+
} catch (error) {
|
|
231
|
+
finish(false, error)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
})
|
|
235
|
+
const result: unknown = change?.()
|
|
236
|
+
if (result && typeof (result as PromiseLike<unknown>).then === 'function')
|
|
237
|
+
throw new TypeError('animateSize changes must be synchronous.')
|
|
238
|
+
if (!view.isLoaded || view.visibility === 'collapse' || !layout.ready()) {
|
|
239
|
+
layout.commit(destination)
|
|
240
|
+
finish(false)
|
|
241
|
+
} else schedule()
|
|
242
|
+
} catch (error) {
|
|
243
|
+
stop(true)
|
|
244
|
+
throw error
|
|
245
|
+
}
|
|
246
|
+
return animation
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function normalize(input?: SizeOptions | SpringName) {
|
|
250
|
+
if (typeof input === 'string') {
|
|
251
|
+
if (!Object.hasOwn(springs, input))
|
|
252
|
+
throw new TypeError(`Unknown size spring "${input}".`)
|
|
253
|
+
input = springs[input]
|
|
254
|
+
}
|
|
255
|
+
if (
|
|
256
|
+
input !== undefined &&
|
|
257
|
+
(!input || typeof input !== 'object' || Array.isArray(input))
|
|
258
|
+
)
|
|
259
|
+
throw new TypeError('Invalid animateSize options.')
|
|
260
|
+
const allowed = new Set([
|
|
261
|
+
'type',
|
|
262
|
+
'duration',
|
|
263
|
+
'ease',
|
|
264
|
+
'stiffness',
|
|
265
|
+
'damping',
|
|
266
|
+
'mass',
|
|
267
|
+
'bounce',
|
|
268
|
+
'velocity',
|
|
269
|
+
'delay',
|
|
270
|
+
])
|
|
271
|
+
for (const [key, value] of Object.entries(input ?? {})) {
|
|
272
|
+
if (!allowed.has(key))
|
|
273
|
+
throw new TypeError(
|
|
274
|
+
`Unsupported animateSize option "${key}"; repeat and seek are not supported.`,
|
|
275
|
+
)
|
|
276
|
+
if (value === undefined) continue
|
|
277
|
+
if (
|
|
278
|
+
key !== 'type' &&
|
|
279
|
+
key !== 'ease' &&
|
|
280
|
+
(typeof value !== 'number' || !Number.isFinite(value))
|
|
281
|
+
)
|
|
282
|
+
throw new RangeError(`animateSize ${key} must be finite.`)
|
|
283
|
+
if (key === 'type' && value !== 'spring' && value !== 'tween')
|
|
284
|
+
throw new TypeError('Invalid size transition type.')
|
|
285
|
+
if (typeof value === 'number') {
|
|
286
|
+
if (['duration', 'delay', 'damping'].includes(key) && value < 0)
|
|
287
|
+
throw new RangeError(`${key} must be nonnegative.`)
|
|
288
|
+
if (['mass', 'stiffness'].includes(key) && value <= 0)
|
|
289
|
+
throw new RangeError(`${key} must be positive.`)
|
|
290
|
+
if (key === 'bounce' && (value < -1 || value > 1))
|
|
291
|
+
throw new RangeError('bounce must be between -1 and 1.')
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const resolved = resolveTransition(input ?? {}, 'x', 2)
|
|
295
|
+
if (
|
|
296
|
+
resolved.curve.type === 'spring' &&
|
|
297
|
+
input?.duration === 0 &&
|
|
298
|
+
!('stiffness' in input || 'damping' in input || 'mass' in input)
|
|
299
|
+
)
|
|
300
|
+
throw new RangeError('Use a zero-duration tween for immediate sizing.')
|
|
301
|
+
return resolved
|
|
302
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -84,7 +84,7 @@ export type AnimateOptions = Transition & {
|
|
|
84
84
|
relative?: boolean
|
|
85
85
|
/** With several targets, delays each one by this much more than the previous. */
|
|
86
86
|
stagger?: number | Stagger
|
|
87
|
-
/**
|
|
87
|
+
/** Native haptic feedback at arrival, or at explicit points in a finite animation. */
|
|
88
88
|
haptic?: Haptic
|
|
89
89
|
/**
|
|
90
90
|
* Plays on a named layer, on top of the property's other animations instead of replacing
|
package/src/vue/index.ts
CHANGED
|
@@ -40,6 +40,8 @@ import {
|
|
|
40
40
|
type GradientOptions,
|
|
41
41
|
type Transition,
|
|
42
42
|
} from '../index.js'
|
|
43
|
+
import { isDerived } from '../motion-value.js'
|
|
44
|
+
import { prepareGradient, type GradientValues } from '../gradient.js'
|
|
43
45
|
import { currentKeyboard } from '../keyboard.js'
|
|
44
46
|
|
|
45
47
|
type Host = NSVElement
|
|
@@ -70,7 +72,10 @@ export interface TransitionProps {
|
|
|
70
72
|
const animations = new WeakMap<object, PlaybackAnimation>()
|
|
71
73
|
const scopes = new WeakMap<object, MotionScope>()
|
|
72
74
|
const removers = new WeakMap<object, () => void>()
|
|
73
|
-
const gradients = new WeakMap<
|
|
75
|
+
const gradients = new WeakMap<
|
|
76
|
+
object,
|
|
77
|
+
{ gradient: Gradient; colors: number; release?: () => void }
|
|
78
|
+
>()
|
|
74
79
|
|
|
75
80
|
function split<T>(
|
|
76
81
|
value: T | readonly [T, Options?],
|
|
@@ -82,6 +87,7 @@ function split<T>(
|
|
|
82
87
|
|
|
83
88
|
function same(a: unknown, b: unknown): boolean {
|
|
84
89
|
if (Object.is(a, b)) return true
|
|
90
|
+
if (isDerived(a) || isDerived(b)) return false
|
|
85
91
|
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null)
|
|
86
92
|
return false
|
|
87
93
|
if (Array.isArray(a) !== Array.isArray(b)) return false
|
|
@@ -161,17 +167,35 @@ function paint(el: Host, value: GradientValue, animated: boolean) {
|
|
|
161
167
|
(Transition | SpringName)?,
|
|
162
168
|
]
|
|
163
169
|
const current = gradients.get(el)
|
|
170
|
+
prepareGradient(options)
|
|
164
171
|
const colors = options.colors.length
|
|
172
|
+
const hasBindings = [
|
|
173
|
+
options.angle,
|
|
174
|
+
options.radius,
|
|
175
|
+
options.center?.x,
|
|
176
|
+
options.center?.y,
|
|
177
|
+
...options.colors,
|
|
178
|
+
...(options.stops ?? []),
|
|
179
|
+
].some(isDerived)
|
|
165
180
|
if (current && current.gradient.type === options.type) {
|
|
166
181
|
const { type, ...values } = options
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
else
|
|
182
|
+
const previous = current.release
|
|
183
|
+
if (hasBindings) current.release = current.gradient.bind(values)
|
|
184
|
+
else {
|
|
185
|
+
if (animated && current.colors === colors)
|
|
186
|
+
current.gradient.animate(values as GradientValues, transition)
|
|
187
|
+
else current.gradient.set(values as GradientValues)
|
|
188
|
+
current.release = undefined
|
|
189
|
+
}
|
|
190
|
+
previous?.()
|
|
170
191
|
current.colors = colors
|
|
171
192
|
return
|
|
172
193
|
}
|
|
173
194
|
current?.gradient.remove()
|
|
174
|
-
|
|
195
|
+
// Track directive ownership explicitly, including bindings omitted on a later update.
|
|
196
|
+
const made = gradient(el, options)
|
|
197
|
+
const release = hasBindings ? made.bind(options) : undefined
|
|
198
|
+
gradients.set(el, { gradient: made, colors, release })
|
|
175
199
|
}
|
|
176
200
|
|
|
177
201
|
/**
|