@effect-tui/react 0.1.0-alpha.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 +21 -0
- package/README.md +138 -0
- package/dist/jsx-dev-runtime.d.ts +3 -0
- package/dist/jsx-dev-runtime.d.ts.map +1 -0
- package/dist/jsx-dev-runtime.js +3 -0
- package/dist/jsx-dev-runtime.js.map +1 -0
- package/dist/jsx-runtime.d.ts +47 -0
- package/dist/jsx-runtime.d.ts.map +1 -0
- package/dist/jsx-runtime.js +6 -0
- package/dist/jsx-runtime.js.map +1 -0
- package/dist/src/codeblock.d.ts +9 -0
- package/dist/src/codeblock.d.ts.map +1 -0
- package/dist/src/codeblock.js +24 -0
- package/dist/src/codeblock.js.map +1 -0
- package/dist/src/constants.d.ts +3 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +3 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/debug/DiagnosticsPanel.d.ts +7 -0
- package/dist/src/debug/DiagnosticsPanel.d.ts.map +1 -0
- package/dist/src/debug/DiagnosticsPanel.js +13 -0
- package/dist/src/debug/DiagnosticsPanel.js.map +1 -0
- package/dist/src/highlight.d.ts +20 -0
- package/dist/src/highlight.d.ts.map +1 -0
- package/dist/src/highlight.js +51 -0
- package/dist/src/highlight.js.map +1 -0
- package/dist/src/hooks/index.d.ts +4 -0
- package/dist/src/hooks/index.d.ts.map +1 -0
- package/dist/src/hooks/index.js +3 -0
- package/dist/src/hooks/index.js.map +1 -0
- package/dist/src/hooks/use-keyboard.d.ts +18 -0
- package/dist/src/hooks/use-keyboard.d.ts.map +1 -0
- package/dist/src/hooks/use-keyboard.js +26 -0
- package/dist/src/hooks/use-keyboard.js.map +1 -0
- package/dist/src/hooks/use-paste.d.ts +5 -0
- package/dist/src/hooks/use-paste.d.ts.map +1 -0
- package/dist/src/hooks/use-paste.js +14 -0
- package/dist/src/hooks/use-paste.js.map +1 -0
- package/dist/src/hooks/useFrameStats.d.ts +7 -0
- package/dist/src/hooks/useFrameStats.d.ts.map +1 -0
- package/dist/src/hooks/useFrameStats.js +28 -0
- package/dist/src/hooks/useFrameStats.js.map +1 -0
- package/dist/src/hosts/base.d.ts +22 -0
- package/dist/src/hosts/base.d.ts.map +1 -0
- package/dist/src/hosts/base.js +53 -0
- package/dist/src/hosts/base.js.map +1 -0
- package/dist/src/hosts/box.d.ts +26 -0
- package/dist/src/hosts/box.d.ts.map +1 -0
- package/dist/src/hosts/box.js +84 -0
- package/dist/src/hosts/box.js.map +1 -0
- package/dist/src/hosts/canvas.d.ts +48 -0
- package/dist/src/hosts/canvas.d.ts.map +1 -0
- package/dist/src/hosts/canvas.js +109 -0
- package/dist/src/hosts/canvas.js.map +1 -0
- package/dist/src/hosts/codeblock.d.ts +32 -0
- package/dist/src/hosts/codeblock.d.ts.map +1 -0
- package/dist/src/hosts/codeblock.js +118 -0
- package/dist/src/hosts/codeblock.js.map +1 -0
- package/dist/src/hosts/hstack.d.ts +18 -0
- package/dist/src/hosts/hstack.d.ts.map +1 -0
- package/dist/src/hosts/hstack.js +45 -0
- package/dist/src/hosts/hstack.js.map +1 -0
- package/dist/src/hosts/index.d.ts +16 -0
- package/dist/src/hosts/index.d.ts.map +1 -0
- package/dist/src/hosts/index.js +40 -0
- package/dist/src/hosts/index.js.map +1 -0
- package/dist/src/hosts/spacer.d.ts +19 -0
- package/dist/src/hosts/spacer.d.ts.map +1 -0
- package/dist/src/hosts/spacer.js +28 -0
- package/dist/src/hosts/spacer.js.map +1 -0
- package/dist/src/hosts/text.d.ts +43 -0
- package/dist/src/hosts/text.d.ts.map +1 -0
- package/dist/src/hosts/text.js +148 -0
- package/dist/src/hosts/text.js.map +1 -0
- package/dist/src/hosts/vstack.d.ts +18 -0
- package/dist/src/hosts/vstack.d.ts.map +1 -0
- package/dist/src/hosts/vstack.js +45 -0
- package/dist/src/hosts/vstack.js.map +1 -0
- package/dist/src/hosts/zstack.d.ts +20 -0
- package/dist/src/hosts/zstack.d.ts.map +1 -0
- package/dist/src/hosts/zstack.js +65 -0
- package/dist/src/hosts/zstack.js.map +1 -0
- package/dist/src/index.d.ts +20 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +20 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/inline/index.d.ts +32 -0
- package/dist/src/inline/index.d.ts.map +1 -0
- package/dist/src/inline/index.js +111 -0
- package/dist/src/inline/index.js.map +1 -0
- package/dist/src/jsx.d.ts +2 -0
- package/dist/src/jsx.d.ts.map +1 -0
- package/dist/src/jsx.js +4 -0
- package/dist/src/jsx.js.map +1 -0
- package/dist/src/motion/color-motion-value.d.ts +32 -0
- package/dist/src/motion/color-motion-value.d.ts.map +1 -0
- package/dist/src/motion/color-motion-value.js +80 -0
- package/dist/src/motion/color-motion-value.js.map +1 -0
- package/dist/src/motion/color.d.ts +30 -0
- package/dist/src/motion/color.d.ts.map +1 -0
- package/dist/src/motion/color.js +172 -0
- package/dist/src/motion/color.js.map +1 -0
- package/dist/src/motion/color.test.d.ts +2 -0
- package/dist/src/motion/color.test.d.ts.map +1 -0
- package/dist/src/motion/color.test.js +97 -0
- package/dist/src/motion/color.test.js.map +1 -0
- package/dist/src/motion/event-emitter.d.ts +18 -0
- package/dist/src/motion/event-emitter.d.ts.map +1 -0
- package/dist/src/motion/event-emitter.js +30 -0
- package/dist/src/motion/event-emitter.js.map +1 -0
- package/dist/src/motion/frame.d.ts +9 -0
- package/dist/src/motion/frame.d.ts.map +1 -0
- package/dist/src/motion/frame.js +51 -0
- package/dist/src/motion/frame.js.map +1 -0
- package/dist/src/motion/hooks.d.ts +75 -0
- package/dist/src/motion/hooks.d.ts.map +1 -0
- package/dist/src/motion/hooks.js +190 -0
- package/dist/src/motion/hooks.js.map +1 -0
- package/dist/src/motion/index.d.ts +4 -0
- package/dist/src/motion/index.d.ts.map +1 -0
- package/dist/src/motion/index.js +7 -0
- package/dist/src/motion/index.js.map +1 -0
- package/dist/src/motion/motion-value.d.ts +40 -0
- package/dist/src/motion/motion-value.d.ts.map +1 -0
- package/dist/src/motion/motion-value.js +109 -0
- package/dist/src/motion/motion-value.js.map +1 -0
- package/dist/src/motion/motion-value.test.d.ts +2 -0
- package/dist/src/motion/motion-value.test.d.ts.map +1 -0
- package/dist/src/motion/motion-value.test.js +177 -0
- package/dist/src/motion/motion-value.test.js.map +1 -0
- package/dist/src/motion/spring-math.d.ts +28 -0
- package/dist/src/motion/spring-math.d.ts.map +1 -0
- package/dist/src/motion/spring-math.js +81 -0
- package/dist/src/motion/spring-math.js.map +1 -0
- package/dist/src/motion/types.d.ts +25 -0
- package/dist/src/motion/types.d.ts.map +1 -0
- package/dist/src/motion/types.js +13 -0
- package/dist/src/motion/types.js.map +1 -0
- package/dist/src/output.d.ts +47 -0
- package/dist/src/output.d.ts.map +1 -0
- package/dist/src/output.js +125 -0
- package/dist/src/output.js.map +1 -0
- package/dist/src/profiler.d.ts +6 -0
- package/dist/src/profiler.d.ts.map +1 -0
- package/dist/src/profiler.js +73 -0
- package/dist/src/profiler.js.map +1 -0
- package/dist/src/reconciler/host-config.d.ts +16 -0
- package/dist/src/reconciler/host-config.d.ts.map +1 -0
- package/dist/src/reconciler/host-config.js +174 -0
- package/dist/src/reconciler/host-config.js.map +1 -0
- package/dist/src/reconciler/types.d.ts +52 -0
- package/dist/src/reconciler/types.d.ts.map +1 -0
- package/dist/src/reconciler/types.js +2 -0
- package/dist/src/reconciler/types.js.map +1 -0
- package/dist/src/renderer.d.ts +101 -0
- package/dist/src/renderer.d.ts.map +1 -0
- package/dist/src/renderer.js +509 -0
- package/dist/src/renderer.js.map +1 -0
- package/dist/src/terminal.d.ts +37 -0
- package/dist/src/terminal.d.ts.map +1 -0
- package/dist/src/terminal.js +65 -0
- package/dist/src/terminal.js.map +1 -0
- package/dist/src/test/index.d.ts +3 -0
- package/dist/src/test/index.d.ts.map +1 -0
- package/dist/src/test/index.js +3 -0
- package/dist/src/test/index.js.map +1 -0
- package/dist/src/test/mock-streams.d.ts +44 -0
- package/dist/src/test/mock-streams.d.ts.map +1 -0
- package/dist/src/test/mock-streams.js +136 -0
- package/dist/src/test/mock-streams.js.map +1 -0
- package/dist/src/test/render-tui.d.ts +47 -0
- package/dist/src/test/render-tui.d.ts.map +1 -0
- package/dist/src/test/render-tui.js +76 -0
- package/dist/src/test/render-tui.js.map +1 -0
- package/dist/src/trace/SpanTree.d.ts +10 -0
- package/dist/src/trace/SpanTree.d.ts.map +1 -0
- package/dist/src/trace/SpanTree.js +104 -0
- package/dist/src/trace/SpanTree.js.map +1 -0
- package/dist/src/trace/index.d.ts +30 -0
- package/dist/src/trace/index.d.ts.map +1 -0
- package/dist/src/trace/index.js +142 -0
- package/dist/src/trace/index.js.map +1 -0
- package/dist/src/trace/location.d.ts +9 -0
- package/dist/src/trace/location.d.ts.map +1 -0
- package/dist/src/trace/location.js +88 -0
- package/dist/src/trace/location.js.map +1 -0
- package/dist/src/trace/span-processor.d.ts +16 -0
- package/dist/src/trace/span-processor.d.ts.map +1 -0
- package/dist/src/trace/span-processor.js +54 -0
- package/dist/src/trace/span-processor.js.map +1 -0
- package/dist/src/trace/span-state.d.ts +79 -0
- package/dist/src/trace/span-state.d.ts.map +1 -0
- package/dist/src/trace/span-state.js +229 -0
- package/dist/src/trace/span-state.js.map +1 -0
- package/dist/src/trace/tui-logger.d.ts +8 -0
- package/dist/src/trace/tui-logger.d.ts.map +1 -0
- package/dist/src/trace/tui-logger.js +70 -0
- package/dist/src/trace/tui-logger.js.map +1 -0
- package/dist/src/utils/border.d.ts +31 -0
- package/dist/src/utils/border.d.ts.map +1 -0
- package/dist/src/utils/border.js +81 -0
- package/dist/src/utils/border.js.map +1 -0
- package/dist/src/utils/flex-layout.d.ts +20 -0
- package/dist/src/utils/flex-layout.d.ts.map +1 -0
- package/dist/src/utils/flex-layout.js +85 -0
- package/dist/src/utils/flex-layout.js.map +1 -0
- package/dist/src/utils/index.d.ts +5 -0
- package/dist/src/utils/index.d.ts.map +1 -0
- package/dist/src/utils/index.js +5 -0
- package/dist/src/utils/index.js.map +1 -0
- package/dist/src/utils/padding.d.ts +26 -0
- package/dist/src/utils/padding.d.ts.map +1 -0
- package/dist/src/utils/padding.js +34 -0
- package/dist/src/utils/padding.js.map +1 -0
- package/dist/src/utils/styles.d.ts +13 -0
- package/dist/src/utils/styles.d.ts.map +1 -0
- package/dist/src/utils/styles.js +5 -0
- package/dist/src/utils/styles.js.map +1 -0
- package/dist/src/visualize/index.d.ts +50 -0
- package/dist/src/visualize/index.d.ts.map +1 -0
- package/dist/src/visualize/index.js +194 -0
- package/dist/src/visualize/index.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +94 -0
- package/src/codeblock.tsx +47 -0
- package/src/constants.ts +2 -0
- package/src/debug/DiagnosticsPanel.tsx +38 -0
- package/src/highlight.ts +76 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/use-keyboard.ts +37 -0
- package/src/hooks/use-paste.ts +14 -0
- package/src/hooks/useFrameStats.ts +32 -0
- package/src/hosts/base.ts +65 -0
- package/src/hosts/box.ts +105 -0
- package/src/hosts/canvas.ts +155 -0
- package/src/hosts/codeblock.ts +145 -0
- package/src/hosts/hstack.ts +64 -0
- package/src/hosts/index.ts +45 -0
- package/src/hosts/spacer.ts +40 -0
- package/src/hosts/text.ts +175 -0
- package/src/hosts/vstack.ts +64 -0
- package/src/hosts/zstack.ts +77 -0
- package/src/index.ts +62 -0
- package/src/inline/index.tsx +181 -0
- package/src/jsx.ts +3 -0
- package/src/motion/color-motion-value.ts +90 -0
- package/src/motion/color.test.ts +115 -0
- package/src/motion/color.ts +191 -0
- package/src/motion/event-emitter.ts +35 -0
- package/src/motion/frame.ts +59 -0
- package/src/motion/hooks.ts +237 -0
- package/src/motion/index.ts +17 -0
- package/src/motion/motion-value.test.ts +222 -0
- package/src/motion/motion-value.ts +140 -0
- package/src/motion/spring-math.ts +114 -0
- package/src/motion/types.ts +34 -0
- package/src/output.ts +156 -0
- package/src/profiler.ts +88 -0
- package/src/reconciler/host-config.ts +277 -0
- package/src/reconciler/types.ts +66 -0
- package/src/renderer.ts +661 -0
- package/src/terminal.ts +67 -0
- package/src/test/index.ts +8 -0
- package/src/test/mock-streams.ts +149 -0
- package/src/test/render-tui.ts +118 -0
- package/src/trace/SpanTree.tsx +195 -0
- package/src/trace/index.tsx +205 -0
- package/src/trace/location.ts +90 -0
- package/src/trace/span-processor.ts +65 -0
- package/src/trace/span-state.ts +286 -0
- package/src/trace/tui-logger.ts +72 -0
- package/src/utils/border.ts +108 -0
- package/src/utils/flex-layout.ts +125 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/padding.ts +45 -0
- package/src/utils/styles.ts +14 -0
- package/src/visualize/index.tsx +305 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React hooks for spring animations.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useCallback, useEffect, useRef } from "react"
|
|
6
|
+
import { useRenderer } from "../renderer.js"
|
|
7
|
+
import { MotionValue } from "./motion-value.js"
|
|
8
|
+
import { ColorMotionValue } from "./color-motion-value.js"
|
|
9
|
+
import type { SpringOptions } from "./types.js"
|
|
10
|
+
import type { EventName } from "./event-emitter.js"
|
|
11
|
+
import type { ColorInput } from "./color.js"
|
|
12
|
+
|
|
13
|
+
// Global requestRender callback - set by useSpringRenderer hook
|
|
14
|
+
let globalRequestRender: (() => void) | null = null
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create a MotionValue. Similar to Motion's useMotionValue.
|
|
18
|
+
*/
|
|
19
|
+
export function useMotionValue<T>(initial: T): MotionValue<T> {
|
|
20
|
+
const ref = useRef<MotionValue<T> | null>(null)
|
|
21
|
+
if (!ref.current) {
|
|
22
|
+
ref.current = new MotionValue(initial)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
return () => ref.current?.destroy()
|
|
27
|
+
}, [])
|
|
28
|
+
|
|
29
|
+
return ref.current
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Call this once in your app to connect springs to the renderer.
|
|
34
|
+
* If no renderer is passed, it will use the nearest RendererContext (useRenderer()).
|
|
35
|
+
*/
|
|
36
|
+
export function useSpringRenderer(renderer?: { requestRender: () => void }) {
|
|
37
|
+
// Prefer explicit renderer; otherwise pull from context
|
|
38
|
+
const inferred = renderer ?? useRenderer()
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (!inferred) return
|
|
41
|
+
globalRequestRender = inferred.requestRender.bind(inferred)
|
|
42
|
+
return () => {
|
|
43
|
+
if (globalRequestRender === inferred.requestRender) {
|
|
44
|
+
globalRequestRender = null
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}, [inferred])
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create a spring-animated value. When you call set(), it springs to the new value.
|
|
52
|
+
* Similar to Motion's useSpring.
|
|
53
|
+
*
|
|
54
|
+
* OPTIMIZED: Does NOT use React state during animation. Instead:
|
|
55
|
+
* - Returns MotionValue which you read via mv.get() in draw functions
|
|
56
|
+
* - Calls renderer.requestRender() directly on each frame
|
|
57
|
+
* - Bypasses React reconciliation entirely during animation
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* const [xMv, setX] = useSpring(0, { visualDuration: 0.5 })
|
|
61
|
+
* // In canvas draw: const x = xMv.get()
|
|
62
|
+
* // To animate: setX(100)
|
|
63
|
+
*/
|
|
64
|
+
export function useSpring(initial: number, options?: SpringOptions): [MotionValue<number>, (value: number) => void] {
|
|
65
|
+
const mv = useMotionValue(initial)
|
|
66
|
+
const optionsRef = useRef(options)
|
|
67
|
+
optionsRef.current = options
|
|
68
|
+
|
|
69
|
+
// Subscribe to changes and request render directly (no setState!)
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
return mv.on("change", () => {
|
|
72
|
+
globalRequestRender?.()
|
|
73
|
+
})
|
|
74
|
+
}, [mv])
|
|
75
|
+
|
|
76
|
+
const set = useCallback(
|
|
77
|
+
(target: number) => {
|
|
78
|
+
mv.set(target, optionsRef.current)
|
|
79
|
+
},
|
|
80
|
+
[mv],
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return [mv, set]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Create multiple spring-animated values. Similar to framer-motion's useSprings.
|
|
88
|
+
*
|
|
89
|
+
* @param count - Number of springs to create
|
|
90
|
+
* @param options - Spring configuration (shared by all springs)
|
|
91
|
+
* @returns [mvs, setAll] - Array of MotionValues and a setter function
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* const [dotMvs, setDots] = useSprings(9, { visualDuration: 0.3, bounce: 0 })
|
|
95
|
+
*
|
|
96
|
+
* // Update all with a mapper function
|
|
97
|
+
* setDots((i) => i === focusedIndex ? 1 : 0)
|
|
98
|
+
*
|
|
99
|
+
* // Or update with an array
|
|
100
|
+
* setDots([0, 0, 1, 0, 0, 0, 0, 0, 0])
|
|
101
|
+
*
|
|
102
|
+
* // In draw callback
|
|
103
|
+
* const brightness = dotMvs[idx].get()
|
|
104
|
+
*/
|
|
105
|
+
export function useSprings(
|
|
106
|
+
count: number,
|
|
107
|
+
options?: SpringOptions,
|
|
108
|
+
): [MotionValue<number>[], (values: number[] | ((index: number) => number)) => void] {
|
|
109
|
+
const mvsRef = useRef<MotionValue<number>[] | null>(null)
|
|
110
|
+
const optionsRef = useRef(options)
|
|
111
|
+
optionsRef.current = options
|
|
112
|
+
|
|
113
|
+
// Create MotionValues on first render
|
|
114
|
+
if (!mvsRef.current) {
|
|
115
|
+
mvsRef.current = Array.from({ length: count }, () => new MotionValue(0))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Handle count changes (recreate if count changes)
|
|
119
|
+
if (mvsRef.current.length !== count) {
|
|
120
|
+
// Destroy old ones
|
|
121
|
+
for (const mv of mvsRef.current) {
|
|
122
|
+
mv.destroy()
|
|
123
|
+
}
|
|
124
|
+
mvsRef.current = Array.from({ length: count }, () => new MotionValue(0))
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const mvs = mvsRef.current
|
|
128
|
+
|
|
129
|
+
// Subscribe all to requestRender
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
const unsubs = mvs.map((mv) =>
|
|
132
|
+
mv.on("change", () => {
|
|
133
|
+
globalRequestRender?.()
|
|
134
|
+
}),
|
|
135
|
+
)
|
|
136
|
+
return () => {
|
|
137
|
+
for (const unsub of unsubs) unsub()
|
|
138
|
+
}
|
|
139
|
+
}, [mvs])
|
|
140
|
+
|
|
141
|
+
// Cleanup on unmount
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
return () => {
|
|
144
|
+
if (mvsRef.current) {
|
|
145
|
+
for (const mv of mvsRef.current) {
|
|
146
|
+
mv.destroy()
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}, [])
|
|
151
|
+
|
|
152
|
+
const setAll = useCallback(
|
|
153
|
+
(values: number[] | ((index: number) => number)) => {
|
|
154
|
+
const opts = optionsRef.current
|
|
155
|
+
if (typeof values === "function") {
|
|
156
|
+
for (let i = 0; i < mvs.length; i++) {
|
|
157
|
+
mvs[i].set(values(i), opts)
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
for (let i = 0; i < Math.min(mvs.length, values.length); i++) {
|
|
161
|
+
mvs[i].set(values[i], opts)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
[mvs],
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return [mvs, setAll]
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Subscribe to MotionValue events. Similar to Motion's useMotionValueEvent.
|
|
173
|
+
*/
|
|
174
|
+
export function useMotionValueEvent<T, E extends EventName>(
|
|
175
|
+
mv: MotionValue<T>,
|
|
176
|
+
event: E,
|
|
177
|
+
callback: E extends "change" ? (value: T) => void : () => void,
|
|
178
|
+
) {
|
|
179
|
+
const callbackRef = useRef(callback)
|
|
180
|
+
callbackRef.current = callback
|
|
181
|
+
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
return mv.on(event, (v: any) => (callbackRef.current as any)(v))
|
|
184
|
+
}, [mv, event])
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Create a ColorMotionValue hook.
|
|
189
|
+
*/
|
|
190
|
+
export function useColorMotionValue(initial: ColorInput): ColorMotionValue {
|
|
191
|
+
const ref = useRef<ColorMotionValue | null>(null)
|
|
192
|
+
if (!ref.current) {
|
|
193
|
+
ref.current = new ColorMotionValue(initial)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
return () => ref.current?.destroy()
|
|
198
|
+
}, [])
|
|
199
|
+
|
|
200
|
+
return ref.current
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Create a spring-animated color. Accepts hex, rgb(), hsl(), or {r,g,b} object.
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* const [colorMv, setColor] = useColorSpring("#ff0000", { visualDuration: 0.5 })
|
|
208
|
+
* setColor("#00ff00") // Spring to green
|
|
209
|
+
* setColor("hsl(240, 100%, 50%)") // Spring to blue
|
|
210
|
+
*
|
|
211
|
+
* // In draw callback
|
|
212
|
+
* const { r, g, b } = colorMv.get()
|
|
213
|
+
*/
|
|
214
|
+
export function useColorSpring(
|
|
215
|
+
initial: ColorInput,
|
|
216
|
+
options?: SpringOptions,
|
|
217
|
+
): [ColorMotionValue, (color: ColorInput) => void] {
|
|
218
|
+
const mv = useColorMotionValue(initial)
|
|
219
|
+
const optionsRef = useRef(options)
|
|
220
|
+
optionsRef.current = options
|
|
221
|
+
|
|
222
|
+
// Subscribe to changes and request render
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
return mv._subscribeChannels(() => {
|
|
225
|
+
globalRequestRender?.()
|
|
226
|
+
})
|
|
227
|
+
}, [mv])
|
|
228
|
+
|
|
229
|
+
const set = useCallback(
|
|
230
|
+
(color: ColorInput) => {
|
|
231
|
+
mv.set(color, optionsRef.current)
|
|
232
|
+
},
|
|
233
|
+
[mv],
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
return [mv, set]
|
|
237
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Motion-inspired animation system for TUI
|
|
2
|
+
// Analytical spring physics (not Euler integration) + setTimeout-based frame loop
|
|
3
|
+
|
|
4
|
+
export type { SpringOptions, MotionValue, RGBA, ColorInput } from "./motion-value.js"
|
|
5
|
+
export {
|
|
6
|
+
motionValue,
|
|
7
|
+
useMotionValue,
|
|
8
|
+
useSpring,
|
|
9
|
+
useSprings,
|
|
10
|
+
useSpringRenderer,
|
|
11
|
+
useMotionValueEvent,
|
|
12
|
+
// Color springs
|
|
13
|
+
ColorMotionValue,
|
|
14
|
+
useColorMotionValue,
|
|
15
|
+
useColorSpring,
|
|
16
|
+
} from "./motion-value.js"
|
|
17
|
+
export { useAnimationFrame } from "./frame.js"
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import { motionValue } from "./motion-value.js"
|
|
3
|
+
|
|
4
|
+
describe("MotionValue", () => {
|
|
5
|
+
describe("basic operations", () => {
|
|
6
|
+
it("initializes with correct value", () => {
|
|
7
|
+
const mv = motionValue(100)
|
|
8
|
+
expect(mv.get()).toBe(100)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it("jump() sets value immediately", () => {
|
|
12
|
+
const mv = motionValue(0)
|
|
13
|
+
mv.jump(50)
|
|
14
|
+
expect(mv.get()).toBe(50)
|
|
15
|
+
expect(mv.getVelocity()).toBe(0)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it("jump() stops any active animation", () => {
|
|
19
|
+
const mv = motionValue(0)
|
|
20
|
+
mv.set(100) // Start animation
|
|
21
|
+
expect(mv.isAnimating()).toBe(true)
|
|
22
|
+
mv.jump(50)
|
|
23
|
+
expect(mv.isAnimating()).toBe(false)
|
|
24
|
+
expect(mv.get()).toBe(50)
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe("spring animation", () => {
|
|
29
|
+
it("set() starts animation toward target", () => {
|
|
30
|
+
const mv = motionValue(0)
|
|
31
|
+
mv.set(100)
|
|
32
|
+
expect(mv.isAnimating()).toBe(true)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it("notifies on change", () => {
|
|
36
|
+
const mv = motionValue(0)
|
|
37
|
+
const values: number[] = []
|
|
38
|
+
mv.on("change", (v) => values.push(v))
|
|
39
|
+
mv.jump(50)
|
|
40
|
+
expect(values).toContain(50)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it("notifies animationStart and animationComplete", async () => {
|
|
44
|
+
const mv = motionValue(0)
|
|
45
|
+
let started = false
|
|
46
|
+
let completed = false
|
|
47
|
+
mv.on("animationStart", () => {
|
|
48
|
+
started = true
|
|
49
|
+
})
|
|
50
|
+
mv.on("animationComplete", () => {
|
|
51
|
+
completed = true
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
mv.set(100, { stiffness: 1000, damping: 50 }) // Fast spring
|
|
55
|
+
expect(started).toBe(true)
|
|
56
|
+
|
|
57
|
+
// Wait for animation to complete
|
|
58
|
+
await new Promise((r) => setTimeout(r, 500))
|
|
59
|
+
expect(completed).toBe(true)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it("stop() halts animation", () => {
|
|
63
|
+
const mv = motionValue(0)
|
|
64
|
+
mv.set(100)
|
|
65
|
+
expect(mv.isAnimating()).toBe(true)
|
|
66
|
+
mv.stop()
|
|
67
|
+
expect(mv.isAnimating()).toBe(false)
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe("spring physics correctness", () => {
|
|
72
|
+
it("reaches target value when settled", async () => {
|
|
73
|
+
const mv = motionValue(0)
|
|
74
|
+
mv.set(100, { stiffness: 500, damping: 30 })
|
|
75
|
+
|
|
76
|
+
await new Promise((r) => setTimeout(r, 1000))
|
|
77
|
+
expect(mv.get()).toBeCloseTo(100, 1)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it("velocity is zero when settled", async () => {
|
|
81
|
+
const mv = motionValue(0)
|
|
82
|
+
mv.set(100, { stiffness: 500, damping: 30 })
|
|
83
|
+
|
|
84
|
+
await new Promise((r) => setTimeout(r, 1000))
|
|
85
|
+
expect(Math.abs(mv.getVelocity())).toBeLessThan(0.1)
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe("retargeting", () => {
|
|
90
|
+
it("preserves velocity when retargeting mid-animation", async () => {
|
|
91
|
+
const mv = motionValue(0)
|
|
92
|
+
mv.set(100, { stiffness: 100, damping: 10 })
|
|
93
|
+
|
|
94
|
+
// Wait a bit for velocity to build up
|
|
95
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
96
|
+
|
|
97
|
+
const velocityBefore = mv.getVelocity()
|
|
98
|
+
const positionBefore = mv.get()
|
|
99
|
+
|
|
100
|
+
// Retarget to opposite direction
|
|
101
|
+
mv.set(0, { stiffness: 100, damping: 10 })
|
|
102
|
+
|
|
103
|
+
// Velocity should be preserved (same value, continuing momentum)
|
|
104
|
+
expect(mv.getVelocity()).toBeCloseTo(velocityBefore, 1)
|
|
105
|
+
expect(mv.get()).toBeCloseTo(positionBefore, 1)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it("curves smoothly when reversing direction", async () => {
|
|
109
|
+
const mv = motionValue(0)
|
|
110
|
+
mv.set(100, { stiffness: 100, damping: 15 })
|
|
111
|
+
|
|
112
|
+
// Wait for positive velocity
|
|
113
|
+
await new Promise((r) => setTimeout(r, 30))
|
|
114
|
+
const velBefore = mv.getVelocity()
|
|
115
|
+
expect(velBefore).toBeGreaterThan(0)
|
|
116
|
+
|
|
117
|
+
// Retarget back to 0
|
|
118
|
+
mv.set(0, { stiffness: 100, damping: 15 })
|
|
119
|
+
|
|
120
|
+
// Should still have positive velocity immediately after retarget
|
|
121
|
+
expect(mv.getVelocity()).toBeGreaterThan(0)
|
|
122
|
+
|
|
123
|
+
// Wait a bit - velocity should decrease and eventually reverse
|
|
124
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
125
|
+
// Position should have moved past where it was (momentum carried it)
|
|
126
|
+
// or started moving back toward 0
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
describe("visualDuration/bounce API", () => {
|
|
131
|
+
it("visualDuration controls animation speed", async () => {
|
|
132
|
+
const fast = motionValue(0)
|
|
133
|
+
const slow = motionValue(0)
|
|
134
|
+
|
|
135
|
+
fast.set(100, { visualDuration: 0.1, bounce: 0 })
|
|
136
|
+
slow.set(100, { visualDuration: 0.5, bounce: 0 })
|
|
137
|
+
|
|
138
|
+
await new Promise((r) => setTimeout(r, 150))
|
|
139
|
+
|
|
140
|
+
// Fast should be closer to target
|
|
141
|
+
expect(Math.abs(100 - fast.get())).toBeLessThan(Math.abs(100 - slow.get()))
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it("bounce: 0 means no overshoot (critically damped)", async () => {
|
|
145
|
+
const mv = motionValue(0)
|
|
146
|
+
mv.set(100, { visualDuration: 0.3, bounce: 0 })
|
|
147
|
+
|
|
148
|
+
const values: number[] = []
|
|
149
|
+
mv.on("change", (v) => values.push(v))
|
|
150
|
+
|
|
151
|
+
await new Promise((r) => setTimeout(r, 500))
|
|
152
|
+
|
|
153
|
+
// Should never exceed target
|
|
154
|
+
const max = Math.max(...values)
|
|
155
|
+
expect(max).toBeLessThanOrEqual(100.5) // Small tolerance for numerical precision
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it("bounce > 0 allows overshoot", async () => {
|
|
159
|
+
const mv = motionValue(0)
|
|
160
|
+
mv.set(100, { visualDuration: 0.3, bounce: 0.3 })
|
|
161
|
+
|
|
162
|
+
const values: number[] = []
|
|
163
|
+
mv.on("change", (v) => values.push(v))
|
|
164
|
+
|
|
165
|
+
await new Promise((r) => setTimeout(r, 500))
|
|
166
|
+
|
|
167
|
+
// Should overshoot target
|
|
168
|
+
const max = Math.max(...values)
|
|
169
|
+
expect(max).toBeGreaterThan(100)
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
describe("cleanup", () => {
|
|
174
|
+
it("destroy() stops animation and clears subscribers", () => {
|
|
175
|
+
const mv = motionValue(0)
|
|
176
|
+
mv.on("change", () => {
|
|
177
|
+
// subscriber added
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
mv.set(100)
|
|
181
|
+
mv.destroy()
|
|
182
|
+
|
|
183
|
+
expect(mv.isAnimating()).toBe(false)
|
|
184
|
+
|
|
185
|
+
// After destroy, just verify it doesn't throw
|
|
186
|
+
mv.jump(50)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it("unsubscribe function works", () => {
|
|
190
|
+
const mv = motionValue(0)
|
|
191
|
+
const values: number[] = []
|
|
192
|
+
const unsub = mv.on("change", (v) => values.push(v))
|
|
193
|
+
|
|
194
|
+
mv.jump(10)
|
|
195
|
+
expect(values).toContain(10)
|
|
196
|
+
|
|
197
|
+
unsub()
|
|
198
|
+
mv.jump(20)
|
|
199
|
+
expect(values).not.toContain(20)
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
describe("createSpringResolver", () => {
|
|
205
|
+
// Test the analytical spring formulas directly
|
|
206
|
+
// These are internal but critical for correctness
|
|
207
|
+
|
|
208
|
+
it("initial position equals from", () => {
|
|
209
|
+
const mv = motionValue(50)
|
|
210
|
+
mv.set(100, { stiffness: 100, damping: 10 })
|
|
211
|
+
// At t=0, should still be at initial position
|
|
212
|
+
expect(mv.get()).toBe(50)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it("initial velocity is preserved", () => {
|
|
216
|
+
const mv = motionValue(0)
|
|
217
|
+
mv.set(100, { stiffness: 100, damping: 10 })
|
|
218
|
+
|
|
219
|
+
// Initial velocity should be 0 (starting from rest)
|
|
220
|
+
expect(mv.getVelocity()).toBe(0)
|
|
221
|
+
})
|
|
222
|
+
})
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MotionValue - tracks animated values with spring physics.
|
|
3
|
+
* Inspired by framer-motion's MotionValue.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { subscribeFrame } from "./frame.js"
|
|
7
|
+
import { EventEmitter } from "./event-emitter.js"
|
|
8
|
+
import { createSpringResolver, springFromVisualDuration } from "./spring-math.js"
|
|
9
|
+
import { type SpringOptions, DEFAULT_SPRING_OPTIONS } from "./types.js"
|
|
10
|
+
|
|
11
|
+
/** Create a MotionValue. Factory function like Motion's motionValue(). */
|
|
12
|
+
export function motionValue<T>(initial: T): MotionValue<T> {
|
|
13
|
+
return new MotionValue(initial)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* MotionValue tracks a value and its velocity, supporting spring animations.
|
|
18
|
+
*/
|
|
19
|
+
export class MotionValue<T = number> extends EventEmitter<T> {
|
|
20
|
+
private current: T
|
|
21
|
+
private target: T
|
|
22
|
+
private velocity = 0
|
|
23
|
+
private animation: { stop: () => void } | null = null
|
|
24
|
+
|
|
25
|
+
constructor(initial: T) {
|
|
26
|
+
super()
|
|
27
|
+
this.current = initial
|
|
28
|
+
this.target = initial
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Get the current value */
|
|
32
|
+
get(): T {
|
|
33
|
+
return this.current
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Set value immediately (no animation) */
|
|
37
|
+
jump(value: T) {
|
|
38
|
+
this.stop()
|
|
39
|
+
this.current = value
|
|
40
|
+
this.target = value
|
|
41
|
+
this.velocity = 0
|
|
42
|
+
this.notify("change", value)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Set target and animate with spring */
|
|
46
|
+
set(value: T, options?: SpringOptions) {
|
|
47
|
+
if (typeof value !== "number" || typeof this.current !== "number") {
|
|
48
|
+
// Non-numeric: jump immediately
|
|
49
|
+
this.jump(value)
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
this.target = value
|
|
53
|
+
this.animateSpring(options)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Get current velocity */
|
|
57
|
+
getVelocity(): number {
|
|
58
|
+
return this.velocity
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Check if currently animating */
|
|
62
|
+
isAnimating(): boolean {
|
|
63
|
+
return this.animation !== null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Stop any active animation */
|
|
67
|
+
stop() {
|
|
68
|
+
if (this.animation) {
|
|
69
|
+
this.animation.stop()
|
|
70
|
+
this.animation = null
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Destroy and clean up */
|
|
75
|
+
destroy() {
|
|
76
|
+
this.stop()
|
|
77
|
+
this.clearSubscribers()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private animateSpring(options?: SpringOptions) {
|
|
81
|
+
this.stop()
|
|
82
|
+
|
|
83
|
+
const opts = { ...DEFAULT_SPRING_OPTIONS, ...options }
|
|
84
|
+
const from = this.current as number
|
|
85
|
+
const to = this.target as number
|
|
86
|
+
|
|
87
|
+
if (from === to) return
|
|
88
|
+
|
|
89
|
+
// Derive stiffness/damping from visualDuration/bounce if provided
|
|
90
|
+
let { stiffness, damping, mass } = opts
|
|
91
|
+
if (opts.visualDuration && opts.visualDuration > 0) {
|
|
92
|
+
const derived = springFromVisualDuration(opts.visualDuration, opts.bounce)
|
|
93
|
+
stiffness = derived.stiffness
|
|
94
|
+
damping = derived.damping
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Create spring resolver (analytical solution)
|
|
98
|
+
const resolver = createSpringResolver(from, to, this.velocity, stiffness, damping, mass)
|
|
99
|
+
|
|
100
|
+
this.notify("animationStart", undefined)
|
|
101
|
+
const startTime = Date.now()
|
|
102
|
+
|
|
103
|
+
const unsubscribe = subscribeFrame(() => {
|
|
104
|
+
const elapsed = (Date.now() - startTime) / 1000 // seconds
|
|
105
|
+
|
|
106
|
+
const state = resolver(elapsed)
|
|
107
|
+
this.current = state.value as T
|
|
108
|
+
this.velocity = state.velocity
|
|
109
|
+
|
|
110
|
+
this.notify("change", this.current)
|
|
111
|
+
|
|
112
|
+
if (state.done) {
|
|
113
|
+
this.current = to as T
|
|
114
|
+
this.velocity = 0
|
|
115
|
+
this.animation = null
|
|
116
|
+
this.notify("change", this.current)
|
|
117
|
+
this.notify("animationComplete", undefined)
|
|
118
|
+
unsubscribe()
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
this.animation = { stop: unsubscribe }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Re-export everything for backwards compatibility
|
|
127
|
+
export { type SpringOptions, DEFAULT_SPRING_OPTIONS } from "./types.js"
|
|
128
|
+
export type { EventName, Subscriber } from "./event-emitter.js"
|
|
129
|
+
export { createSpringResolver, springFromVisualDuration, isSpringSettled } from "./spring-math.js"
|
|
130
|
+
export { ColorMotionValue } from "./color-motion-value.js"
|
|
131
|
+
export {
|
|
132
|
+
useMotionValue,
|
|
133
|
+
useSpring,
|
|
134
|
+
useSprings,
|
|
135
|
+
useSpringRenderer,
|
|
136
|
+
useMotionValueEvent,
|
|
137
|
+
useColorMotionValue,
|
|
138
|
+
useColorSpring,
|
|
139
|
+
} from "./hooks.js"
|
|
140
|
+
export type { RGBA, ColorInput } from "./color.js"
|