@octane-xplat/motion 0.0.0 → 0.7.1
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/LICENSE.motion +21 -0
- package/README.md +15 -0
- package/UPSTREAM.md +61 -0
- package/dist/native/index.js +1747 -0
- package/dist/native/octane-client-build.json +8 -0
- package/dist/web/index.js +1826 -0
- package/dist/web/octane-client-build.json +8 -0
- package/package.json +75 -5
- package/src/clock-types.ts +6 -0
- package/src/clock.ts +7 -0
- package/src/clock.web.ts +6 -0
- package/src/components.mobile.test.tsrx +84 -0
- package/src/components.tsrx +142 -0
- package/src/components.web.test.tsrx +134 -0
- package/src/config.tsrx +29 -0
- package/src/controller.ts +98 -0
- package/src/engine.test.ts +126 -0
- package/src/engine.ts +159 -0
- package/src/hooks.tsrx +56 -0
- package/src/host-types.ts +6 -0
- package/src/host.ts +50 -0
- package/src/host.web.ts +34 -0
- package/src/index.ts +9 -0
- package/src/index.web.ts +9 -0
- package/src/presence-input.ts +26 -0
- package/src/presence-input.web.ts +17 -0
- package/src/presence-state.test.ts +64 -0
- package/src/presence-state.ts +76 -0
- package/src/presence.tsrx +52 -0
- package/src/presence.web.test.tsrx +122 -0
- package/src/reduced-motion.ts +37 -0
- package/src/reduced-motion.web.ts +13 -0
- package/src/types.ts +55 -0
- package/src/value.ts +167 -0
- package/types/clock-types.d.ts +6 -0
- package/types/engine.d.ts +11 -0
- package/types/index.d.ts +53 -0
- package/types/types.d.ts +51 -0
- package/types/value.d.ts +48 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { act, createRoot, useEffect, useState } from 'octane'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { motion, Presence, MotionConfig } from './index.web'
|
|
4
|
+
|
|
5
|
+
let mounts = 0, cleanups = 0
|
|
6
|
+
function Child(props: any) @{
|
|
7
|
+
const [count, setCount] = useState(0)
|
|
8
|
+
useEffect(() => { mounts++; return () => { cleanups++ } }, [])
|
|
9
|
+
|
|
10
|
+
<motion.View id="exiting" initial={false} animate={{ x: 0 }} exit={{ x: 100 }} transition={{ duration: props.duration }}>
|
|
11
|
+
<button id="counter" onClick={() => setCount(count + 1)}>{count}</button>
|
|
12
|
+
</motion.View>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function Scene(props: any) @{
|
|
16
|
+
<MotionConfig reducedMotion={props.reduced ? 'always' : 'never'}>
|
|
17
|
+
<Presence present={props.present} onExitComplete={props.done} id="boundary">
|
|
18
|
+
<Child duration={props.duration ?? 0.1} />
|
|
19
|
+
</Presence>
|
|
20
|
+
</MotionConfig>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function mount(props: any) {
|
|
24
|
+
const container = document.createElement('div'); document.body.append(container)
|
|
25
|
+
const root = createRoot(container)
|
|
26
|
+
act(() => root.render(<Scene {...props} />))
|
|
27
|
+
return { container, root, update(next: any) { act(() => root.render(<Scene {...next} />)) }, close() { act(() => root.unmount()); container.remove() } }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const advance = async (ms: number) => { await act(async () => { await vi.advanceTimersByTimeAsync(ms) }) }
|
|
31
|
+
|
|
32
|
+
describe('retained presence', () => {
|
|
33
|
+
it('retains component state until exit finishes, blocks input and relinquishes focus', async () => {
|
|
34
|
+
vi.useFakeTimers(); mounts = cleanups = 0
|
|
35
|
+
const done = vi.fn(); const r = mount({ present: true, done })
|
|
36
|
+
const node = r.container.querySelector('#exiting')
|
|
37
|
+
const button = r.container.querySelector('button') as HTMLButtonElement
|
|
38
|
+
act(() => button.click()); button.focus()
|
|
39
|
+
expect(button.textContent).toBe('1')
|
|
40
|
+
r.update({ present: false, done })
|
|
41
|
+
expect(r.container.querySelector('#exiting')).toBe(node)
|
|
42
|
+
expect(cleanups).toBe(0)
|
|
43
|
+
expect(r.container.querySelector('#boundary')?.hasAttribute('inert')).toBe(true)
|
|
44
|
+
expect(document.activeElement).not.toBe(button)
|
|
45
|
+
await advance(160)
|
|
46
|
+
expect(r.container.querySelector('#exiting')).toBeNull()
|
|
47
|
+
expect(cleanups).toBe(1); expect(done).toHaveBeenCalledTimes(1)
|
|
48
|
+
r.close(); vi.useRealTimers()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('reverses without remounting or stale removal', async () => {
|
|
52
|
+
vi.useFakeTimers(); mounts = cleanups = 0
|
|
53
|
+
const done = vi.fn(); const r = mount({ present: true, done })
|
|
54
|
+
const node = r.container.querySelector('#exiting')
|
|
55
|
+
r.update({ present: false, done }); await advance(32)
|
|
56
|
+
r.update({ present: true, done }); await advance(200)
|
|
57
|
+
expect(r.container.querySelector('#exiting')).toBe(node)
|
|
58
|
+
expect(mounts).toBe(1); expect(cleanups).toBe(0)
|
|
59
|
+
expect(done).not.toHaveBeenCalled()
|
|
60
|
+
expect(r.container.querySelector('#boundary')?.hasAttribute('inert')).toBe(false)
|
|
61
|
+
r.close(); expect(cleanups).toBe(1); vi.useRealTimers()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('disposes immediately when the boundary unmounts', async () => {
|
|
65
|
+
vi.useFakeTimers(); mounts = cleanups = 0
|
|
66
|
+
const done = vi.fn(); const r = mount({ present: true, done })
|
|
67
|
+
r.update({ present: false, done }); r.close()
|
|
68
|
+
expect(cleanups).toBe(1)
|
|
69
|
+
await advance(200); expect(done).not.toHaveBeenCalled()
|
|
70
|
+
vi.useRealTimers()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('starts absent, supports repeated cycles and instant reduced exits', async () => {
|
|
74
|
+
vi.useFakeTimers(); mounts = cleanups = 0
|
|
75
|
+
const done = vi.fn(); const r = mount({ present: false, done, reduced: true })
|
|
76
|
+
expect(r.container.querySelector('#exiting')).toBeNull()
|
|
77
|
+
for (let i = 0; i < 2; i++) {
|
|
78
|
+
r.update({ present: true, done, reduced: true }); await advance(1)
|
|
79
|
+
expect(r.container.querySelector('#exiting')).not.toBeNull()
|
|
80
|
+
r.update({ present: false, done, reduced: true }); await advance(1)
|
|
81
|
+
expect(r.container.querySelector('#exiting')).toBeNull()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
expect(done).toHaveBeenCalledTimes(2)
|
|
85
|
+
r.close(); vi.useRealTimers()
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
import { PresenceDemo } from '../examples/PresenceDemo.tsrx'
|
|
90
|
+
it('mounts the retained-presence example', () => {
|
|
91
|
+
const container = document.createElement('div'); document.body.append(container)
|
|
92
|
+
const root = createRoot(container)
|
|
93
|
+
act(() => root.render(<PresenceDemo />))
|
|
94
|
+
expect(container.textContent).toContain('Count: 0')
|
|
95
|
+
act(() => root.unmount()); container.remove()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
import { useMotionValue } from './index.web'
|
|
99
|
+
function Independent(props: any) @{
|
|
100
|
+
const x = useMotionValue(0)
|
|
101
|
+
useEffect(() => { props.ready(x) }, [])
|
|
102
|
+
|
|
103
|
+
<Presence present={props.present}>
|
|
104
|
+
<motion.View id="independent" style={{ x }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.1 }} />
|
|
105
|
+
</Presence>
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
it('exit reversal does not take ownership of independent MotionValue channels', async () => {
|
|
109
|
+
vi.useFakeTimers()
|
|
110
|
+
let x: any
|
|
111
|
+
const ready = (value: any) => { x = value }
|
|
112
|
+
const container = document.createElement('div'); document.body.append(container)
|
|
113
|
+
const root = createRoot(container)
|
|
114
|
+
act(() => root.render(<Independent present={true} ready={ready} />))
|
|
115
|
+
act(() => root.render(<Independent present={false} ready={ready} />))
|
|
116
|
+
await advance(32)
|
|
117
|
+
x.set(50)
|
|
118
|
+
act(() => root.render(<Independent present={true} ready={ready} />))
|
|
119
|
+
await advance(200)
|
|
120
|
+
expect((container.querySelector('#independent') as HTMLElement).style.transform).toContain('translateX(50px)')
|
|
121
|
+
act(() => root.unmount()); container.remove(); vi.useRealTimers()
|
|
122
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Application, Utils } from '@nativescript/core'
|
|
2
|
+
|
|
3
|
+
export function readReducedMotion(): boolean {
|
|
4
|
+
const platform = globalThis as any
|
|
5
|
+
if (Application.ios) {
|
|
6
|
+
return Boolean(platform.UIAccessibilityIsReduceMotionEnabled?.())
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const context = Utils.android.getApplicationContext()
|
|
10
|
+
return (
|
|
11
|
+
platform.android.provider.Settings.Global.getFloat(
|
|
12
|
+
context.getContentResolver(),
|
|
13
|
+
'animator_duration_scale',
|
|
14
|
+
1,
|
|
15
|
+
) === 0
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Poll only while subscribed; this also detects Android developer-setting changes
|
|
20
|
+
// without owning a Java ContentObserver class. Resume refreshes immediately.
|
|
21
|
+
export function observeReducedMotion(notify: () => void): () => void {
|
|
22
|
+
let previous = readReducedMotion()
|
|
23
|
+
const check = () => {
|
|
24
|
+
const next = readReducedMotion()
|
|
25
|
+
if (next !== previous) {
|
|
26
|
+
previous = next
|
|
27
|
+
notify()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const timer = setInterval(check, 500)
|
|
32
|
+
Application.on(Application.resumeEvent, check)
|
|
33
|
+
return () => {
|
|
34
|
+
clearInterval(timer)
|
|
35
|
+
Application.off(Application.resumeEvent, check)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function readReducedMotion(): boolean {
|
|
2
|
+
return typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function observeReducedMotion(notify: () => void): () => void {
|
|
6
|
+
if (typeof matchMedia !== 'function') {
|
|
7
|
+
return () => {}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const media = matchMedia('(prefers-reduced-motion: reduce)')
|
|
11
|
+
media.addEventListener('change', notify)
|
|
12
|
+
return () => media.removeEventListener('change', notify)
|
|
13
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ViewProps, RowProps, PressableProps } from '@octane-xplat/ui'
|
|
2
|
+
import type { MotionValue } from './value'
|
|
3
|
+
|
|
4
|
+
/** Supported channels. Translation is CSS pixels/DIP; rotation is degrees. */
|
|
5
|
+
export type MotionKey = 'opacity' | 'x' | 'y' | 'scale' | 'scaleX' | 'scaleY' | 'rotate'
|
|
6
|
+
/** Absolute numeric destinations; omitted channels retain their current values. */
|
|
7
|
+
export type Target = Partial<Record<MotionKey, number>>
|
|
8
|
+
/** Tween easing, including a CSS-compatible cubic Bezier tuple. */
|
|
9
|
+
export type Ease = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut' | [number, number, number, number]
|
|
10
|
+
/** Timing is seconds. Spring velocity is units per second. */
|
|
11
|
+
export interface Transition {
|
|
12
|
+
type?: 'tween' | 'spring'
|
|
13
|
+
duration?: number
|
|
14
|
+
delay?: number
|
|
15
|
+
ease?: Ease
|
|
16
|
+
stiffness?: number
|
|
17
|
+
damping?: number
|
|
18
|
+
mass?: number
|
|
19
|
+
velocity?: number
|
|
20
|
+
restSpeed?: number
|
|
21
|
+
restDelta?: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Static platform styles plus numeric motion-value bindings. */
|
|
25
|
+
export type MotionStyle = Record<string, unknown> & Partial<Record<MotionKey, number | MotionValue>>
|
|
26
|
+
/** Supported declarative motion controls. */
|
|
27
|
+
export interface MotionProps {
|
|
28
|
+
initial?: Target | false
|
|
29
|
+
/** Destination while the nearest Presence boundary retains this host for exit. */
|
|
30
|
+
exit?: Target
|
|
31
|
+
animate?: Target
|
|
32
|
+
transition?: Transition
|
|
33
|
+
style?: MotionStyle
|
|
34
|
+
onAnimationComplete?: () => void
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** View layout and accessibility props with motion controls. */
|
|
38
|
+
export type MotionViewProps = Omit<ViewProps, 'style'> & MotionProps
|
|
39
|
+
/** Row layout and accessibility props with motion controls. */
|
|
40
|
+
export type MotionRowProps = Omit<RowProps, 'style'> & MotionProps
|
|
41
|
+
/** Pressable interaction props with motion controls. */
|
|
42
|
+
export type MotionPressableProps = Omit<PressableProps, 'style'> & MotionProps
|
|
43
|
+
/** Inherited defaults; `user` observes the live system preference. */
|
|
44
|
+
export interface MotionConfigProps {
|
|
45
|
+
transition?: Transition
|
|
46
|
+
reducedMotion?: 'always' | 'never' | 'user'
|
|
47
|
+
children?: any
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Presence renders a View wrapper; use its layout props to size retained content. */
|
|
51
|
+
export interface PresenceProps extends ViewProps {
|
|
52
|
+
present: boolean
|
|
53
|
+
/** Called once after all registered exits finish, never after reversal/disposal. */
|
|
54
|
+
onExitComplete?: () => void
|
|
55
|
+
}
|
package/src/value.ts
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type { Clock } from './clock-types'
|
|
2
|
+
import type { Transition } from './types'
|
|
3
|
+
import { runAnimation, validateTransition, type AnimationControls } from './engine'
|
|
4
|
+
|
|
5
|
+
/** Events supported by numeric motion values. */
|
|
6
|
+
export interface MotionValueEvents {
|
|
7
|
+
change: (value: number) => void
|
|
8
|
+
animationStart: () => void
|
|
9
|
+
animationComplete: () => void
|
|
10
|
+
animationCancel: () => void
|
|
11
|
+
destroy: () => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Numeric value with direct subscriptions; it does not subscribe Octane renders. */
|
|
15
|
+
export class MotionValue {
|
|
16
|
+
private current: number
|
|
17
|
+
private previous: number
|
|
18
|
+
private velocity = 0
|
|
19
|
+
private updated: number
|
|
20
|
+
private controls?: AnimationControls
|
|
21
|
+
private generation = 0
|
|
22
|
+
private listeners = new Map<keyof MotionValueEvents, Set<Function>>()
|
|
23
|
+
private passive?: (value: number) => void
|
|
24
|
+
private disposed = false
|
|
25
|
+
constructor(
|
|
26
|
+
initial: number,
|
|
27
|
+
private clock: Clock,
|
|
28
|
+
) {
|
|
29
|
+
this.assert(initial)
|
|
30
|
+
this.current = this.previous = initial
|
|
31
|
+
this.updated = clock.now()
|
|
32
|
+
}
|
|
33
|
+
private assert(value: number) {
|
|
34
|
+
if (!Number.isFinite(value)) {
|
|
35
|
+
throw new Error('motion: values must be finite numbers')
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Read the current sample. */
|
|
39
|
+
get() {
|
|
40
|
+
return this.current
|
|
41
|
+
}
|
|
42
|
+
/** Read the previous sample. */
|
|
43
|
+
getPrevious() {
|
|
44
|
+
return this.previous
|
|
45
|
+
}
|
|
46
|
+
/** Velocity in units per second; stale samples have zero velocity. */
|
|
47
|
+
getVelocity() {
|
|
48
|
+
return this.clock.now() - this.updated > 30 ? 0 : this.velocity
|
|
49
|
+
}
|
|
50
|
+
/** Set immediately, or animate when this is a useSpring value. */
|
|
51
|
+
set(value: number) {
|
|
52
|
+
this.assert(value)
|
|
53
|
+
if (this.disposed) {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (this.passive) {
|
|
58
|
+
this.passive(value)
|
|
59
|
+
} else {
|
|
60
|
+
this.stop()
|
|
61
|
+
this.write(value)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Snap and stop current playback, resetting velocity. */
|
|
65
|
+
jump(value: number) {
|
|
66
|
+
this.assert(value)
|
|
67
|
+
this.stop()
|
|
68
|
+
this.write(value)
|
|
69
|
+
this.velocity = 0
|
|
70
|
+
}
|
|
71
|
+
/** Subscribe without causing a component render. Returns an unsubscribe function. */
|
|
72
|
+
on<K extends keyof MotionValueEvents>(event: K, callback: MotionValueEvents[K]): () => void {
|
|
73
|
+
let listeners = this.listeners.get(event)
|
|
74
|
+
if (!listeners) {
|
|
75
|
+
this.listeners.set(event, (listeners = new Set()))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
listeners.add(callback)
|
|
79
|
+
return () => {
|
|
80
|
+
listeners.delete(callback)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
private emit(event: keyof MotionValueEvents, value?: number) {
|
|
84
|
+
// Snapshot because subscribers may unsubscribe or add listeners while notified.
|
|
85
|
+
// eslint-disable-next-line unicorn/no-useless-spread
|
|
86
|
+
for (const callback of [...(this.listeners.get(event) ?? [])]) {
|
|
87
|
+
callback(value)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
private write(value: number) {
|
|
91
|
+
if (this.disposed) {
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const now = this.clock.now()
|
|
96
|
+
const elapsed = now - this.updated
|
|
97
|
+
if (elapsed > 0) {
|
|
98
|
+
this.velocity = elapsed > 30 ? 0 : ((value - this.current) * 1000) / elapsed
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this.previous = this.current
|
|
102
|
+
this.current = value
|
|
103
|
+
this.updated = now
|
|
104
|
+
if (value !== this.previous) {
|
|
105
|
+
this.emit('change', value)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** Animate to a destination. New playback replaces old playback. */
|
|
109
|
+
animate(target: number, transition: Transition = {}): AnimationControls {
|
|
110
|
+
this.assert(target)
|
|
111
|
+
validateTransition(transition)
|
|
112
|
+
if (this.disposed) {
|
|
113
|
+
return { finished: Promise.resolve('cancelled'), stop() {} }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const velocity = transition.velocity ?? this.getVelocity()
|
|
117
|
+
this.stop('replaced')
|
|
118
|
+
const generation = ++this.generation
|
|
119
|
+
this.emit('animationStart')
|
|
120
|
+
const controls = runAnimation(
|
|
121
|
+
this.clock,
|
|
122
|
+
this.current,
|
|
123
|
+
target,
|
|
124
|
+
{ ...transition, velocity },
|
|
125
|
+
(value) => this.write(value),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
this.controls = controls
|
|
129
|
+
void controls.finished.then((result) => {
|
|
130
|
+
if (generation !== this.generation || this.disposed) {
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this.controls = undefined
|
|
135
|
+
if (result === 'finished') {
|
|
136
|
+
this.emit('animationComplete')
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
return controls
|
|
141
|
+
}
|
|
142
|
+
/** Stop playback at its current value. */
|
|
143
|
+
stop(reason: 'cancelled' | 'replaced' = 'cancelled') {
|
|
144
|
+
++this.generation
|
|
145
|
+
if (this.controls) {
|
|
146
|
+
this.controls.stop(reason)
|
|
147
|
+
this.controls = undefined
|
|
148
|
+
this.emit('animationCancel')
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** Release playback and listeners when the owning component unmounts. */
|
|
152
|
+
destroy() {
|
|
153
|
+
this.stop()
|
|
154
|
+
this.emit('destroy')
|
|
155
|
+
this.listeners.clear()
|
|
156
|
+
this.passive = undefined
|
|
157
|
+
this.disposed = true
|
|
158
|
+
}
|
|
159
|
+
/** @internal Install the useSpring setter interception. */
|
|
160
|
+
attach(setter: (value: number) => void) {
|
|
161
|
+
this.passive = setter
|
|
162
|
+
return () => {
|
|
163
|
+
this.passive = undefined
|
|
164
|
+
this.stop()
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Clock } from './clock-types';
|
|
2
|
+
import type { Transition } from './types';
|
|
3
|
+
/** Terminal status; cancellation never masquerades as completion. */
|
|
4
|
+
export type AnimationResult = 'finished' | 'cancelled' | 'replaced';
|
|
5
|
+
/** A cancellable animation whose finished promise always settles. */
|
|
6
|
+
export interface AnimationControls {
|
|
7
|
+
finished: Promise<AnimationResult>;
|
|
8
|
+
stop(reason?: Exclude<AnimationResult, 'finished'>): void;
|
|
9
|
+
}
|
|
10
|
+
export declare function validateTransition(t: Transition): void;
|
|
11
|
+
export declare function runAnimation(clock: Clock, from: number, to: number, transition: Transition, update: (value: number) => void, complete?: () => void): AnimationControls;
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { UniversalComponent } from 'octane/universal'
|
|
2
|
+
import type {
|
|
3
|
+
MotionViewProps,
|
|
4
|
+
MotionRowProps,
|
|
5
|
+
MotionPressableProps,
|
|
6
|
+
MotionConfigProps,
|
|
7
|
+
Transition,
|
|
8
|
+
} from './types'
|
|
9
|
+
import type { MotionValue, MotionValueEvents } from './value'
|
|
10
|
+
/** Bounded motion hosts for shared xplat UI primitives. */
|
|
11
|
+
export declare const motion: {
|
|
12
|
+
View: UniversalComponent<MotionViewProps>
|
|
13
|
+
Row: UniversalComponent<MotionRowProps>
|
|
14
|
+
Pressable: UniversalComponent<MotionPressableProps>
|
|
15
|
+
}
|
|
16
|
+
/** Inherit transition and reduced-motion defaults within this root. */
|
|
17
|
+
export declare const MotionConfig: UniversalComponent<MotionConfigProps>
|
|
18
|
+
/** Observe the live system reduced-motion preference. */
|
|
19
|
+
export declare function useReducedMotion(): boolean
|
|
20
|
+
/** Create an owned numeric value; writes do not render components. */
|
|
21
|
+
export declare function useMotionValue(initial: number): MotionValue
|
|
22
|
+
/** Subscribe to an event until replacement or unmount. */
|
|
23
|
+
export declare function useMotionValueEvent<K extends keyof MotionValueEvents>(
|
|
24
|
+
value: MotionValue,
|
|
25
|
+
event: K,
|
|
26
|
+
callback: MotionValueEvents[K],
|
|
27
|
+
): void
|
|
28
|
+
/** Derive a numeric value by mapping ranges or transforming source samples. */
|
|
29
|
+
export declare function useTransform(
|
|
30
|
+
source: MotionValue,
|
|
31
|
+
input: number[],
|
|
32
|
+
output: number[],
|
|
33
|
+
options?: { clamp?: boolean },
|
|
34
|
+
): MotionValue
|
|
35
|
+
export declare function useTransform(
|
|
36
|
+
source: MotionValue,
|
|
37
|
+
transform: (value: number) => number,
|
|
38
|
+
): MotionValue
|
|
39
|
+
export declare function useTransform(
|
|
40
|
+
source: MotionValue[],
|
|
41
|
+
transform: (values: number[]) => number,
|
|
42
|
+
): MotionValue
|
|
43
|
+
/** Spring toward set targets or a source MotionValue; jump snaps. */
|
|
44
|
+
export declare function useSpring(
|
|
45
|
+
source: number | MotionValue,
|
|
46
|
+
options?: Omit<Transition, 'type' | 'duration' | 'ease' | 'delay'>,
|
|
47
|
+
): MotionValue
|
|
48
|
+
export type { MotionValue, MotionValueEvents }
|
|
49
|
+
export type { AnimationControls, AnimationResult } from './engine'
|
|
50
|
+
export type * from './types'
|
|
51
|
+
|
|
52
|
+
/** Retain live children through exit; removing the boundary disposes immediately. */
|
|
53
|
+
export declare const Presence: UniversalComponent<import('./types').PresenceProps>
|
package/types/types.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ViewProps, RowProps, PressableProps } from '@octane-xplat/ui';
|
|
2
|
+
import type { MotionValue } from './value';
|
|
3
|
+
/** Supported channels. Translation is CSS pixels/DIP; rotation is degrees. */
|
|
4
|
+
export type MotionKey = 'opacity' | 'x' | 'y' | 'scale' | 'scaleX' | 'scaleY' | 'rotate';
|
|
5
|
+
/** Absolute numeric destinations; omitted channels retain their current values. */
|
|
6
|
+
export type Target = Partial<Record<MotionKey, number>>;
|
|
7
|
+
/** Tween easing, including a CSS-compatible cubic Bezier tuple. */
|
|
8
|
+
export type Ease = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut' | [number, number, number, number];
|
|
9
|
+
/** Timing is seconds. Spring velocity is units per second. */
|
|
10
|
+
export interface Transition {
|
|
11
|
+
type?: 'tween' | 'spring';
|
|
12
|
+
duration?: number;
|
|
13
|
+
delay?: number;
|
|
14
|
+
ease?: Ease;
|
|
15
|
+
stiffness?: number;
|
|
16
|
+
damping?: number;
|
|
17
|
+
mass?: number;
|
|
18
|
+
velocity?: number;
|
|
19
|
+
restSpeed?: number;
|
|
20
|
+
restDelta?: number;
|
|
21
|
+
}
|
|
22
|
+
/** Static platform styles plus numeric motion-value bindings. */
|
|
23
|
+
export type MotionStyle = Record<string, unknown> & Partial<Record<MotionKey, number | MotionValue>>;
|
|
24
|
+
/** Supported declarative motion controls. */
|
|
25
|
+
export interface MotionProps {
|
|
26
|
+
initial?: Target | false;
|
|
27
|
+
/** Destination while the nearest Presence boundary retains this host for exit. */
|
|
28
|
+
exit?: Target;
|
|
29
|
+
animate?: Target;
|
|
30
|
+
transition?: Transition;
|
|
31
|
+
style?: MotionStyle;
|
|
32
|
+
onAnimationComplete?: () => void;
|
|
33
|
+
}
|
|
34
|
+
/** View layout and accessibility props with motion controls. */
|
|
35
|
+
export type MotionViewProps = Omit<ViewProps, 'style'> & MotionProps;
|
|
36
|
+
/** Row layout and accessibility props with motion controls. */
|
|
37
|
+
export type MotionRowProps = Omit<RowProps, 'style'> & MotionProps;
|
|
38
|
+
/** Pressable interaction props with motion controls. */
|
|
39
|
+
export type MotionPressableProps = Omit<PressableProps, 'style'> & MotionProps;
|
|
40
|
+
/** Inherited defaults; `user` observes the live system preference. */
|
|
41
|
+
export interface MotionConfigProps {
|
|
42
|
+
transition?: Transition;
|
|
43
|
+
reducedMotion?: 'always' | 'never' | 'user';
|
|
44
|
+
children?: any;
|
|
45
|
+
}
|
|
46
|
+
/** Presence renders a View wrapper; use its layout props to size retained content. */
|
|
47
|
+
export interface PresenceProps extends ViewProps {
|
|
48
|
+
present: boolean;
|
|
49
|
+
/** Called once after all registered exits finish, never after reversal/disposal. */
|
|
50
|
+
onExitComplete?: () => void;
|
|
51
|
+
}
|
package/types/value.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Clock } from './clock-types';
|
|
2
|
+
import type { Transition } from './types';
|
|
3
|
+
import { type AnimationControls } from './engine';
|
|
4
|
+
/** Events supported by numeric motion values. */
|
|
5
|
+
export interface MotionValueEvents {
|
|
6
|
+
change: (value: number) => void;
|
|
7
|
+
animationStart: () => void;
|
|
8
|
+
animationComplete: () => void;
|
|
9
|
+
animationCancel: () => void;
|
|
10
|
+
destroy: () => void;
|
|
11
|
+
}
|
|
12
|
+
/** Numeric value with direct subscriptions; it does not subscribe Octane renders. */
|
|
13
|
+
export declare class MotionValue {
|
|
14
|
+
private clock;
|
|
15
|
+
private current;
|
|
16
|
+
private previous;
|
|
17
|
+
private velocity;
|
|
18
|
+
private updated;
|
|
19
|
+
private controls?;
|
|
20
|
+
private generation;
|
|
21
|
+
private listeners;
|
|
22
|
+
private passive?;
|
|
23
|
+
private disposed;
|
|
24
|
+
constructor(initial: number, clock: Clock);
|
|
25
|
+
private assert;
|
|
26
|
+
/** Read the current sample. */
|
|
27
|
+
get(): number;
|
|
28
|
+
/** Read the previous sample. */
|
|
29
|
+
getPrevious(): number;
|
|
30
|
+
/** Velocity in units per second; stale samples have zero velocity. */
|
|
31
|
+
getVelocity(): number;
|
|
32
|
+
/** Set immediately, or animate when this is a useSpring value. */
|
|
33
|
+
set(value: number): void;
|
|
34
|
+
/** Snap and stop current playback, resetting velocity. */
|
|
35
|
+
jump(value: number): void;
|
|
36
|
+
/** Subscribe without causing a component render. Returns an unsubscribe function. */
|
|
37
|
+
on<K extends keyof MotionValueEvents>(event: K, callback: MotionValueEvents[K]): () => void;
|
|
38
|
+
private emit;
|
|
39
|
+
private write;
|
|
40
|
+
/** Animate to a destination. New playback replaces old playback. */
|
|
41
|
+
animate(target: number, transition?: Transition): AnimationControls;
|
|
42
|
+
/** Stop playback at its current value. */
|
|
43
|
+
stop(reason?: 'cancelled' | 'replaced'): void;
|
|
44
|
+
/** Release playback and listeners when the owning component unmounts. */
|
|
45
|
+
destroy(): void;
|
|
46
|
+
/** @internal Install the useSpring setter interception. */
|
|
47
|
+
attach(setter: (value: number) => void): () => void;
|
|
48
|
+
}
|