@octanejs/spring 0.0.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 +53 -0
- package/UPSTREAM.md +103 -0
- package/package.json +73 -0
- package/src/animated/Animated.ts +24 -0
- package/src/animated/AnimatedArray.ts +26 -0
- package/src/animated/index.ts +3 -0
- package/src/browser.ts +246 -0
- package/src/components.tsrx +60 -0
- package/src/components.tsrx.d.ts +33 -0
- package/src/context.ts +13 -0
- package/src/core/AnimationConfig.ts +60 -0
- package/src/core/AnimationResult.ts +19 -0
- package/src/core/Controller.ts +200 -0
- package/src/core/FrameValue.ts +16 -0
- package/src/core/Interpolation.ts +46 -0
- package/src/core/SpringRef.ts +26 -0
- package/src/core/SpringValue.ts +384 -0
- package/src/core/index.ts +9 -0
- package/src/core/runAsync.ts +17 -0
- package/src/core/scheduleProps.ts +34 -0
- package/src/core/valueInterpolation.ts +54 -0
- package/src/engine.ts +9 -0
- package/src/hooks.ts +610 -0
- package/src/index.ts +26 -0
- package/src/parallax.ts +8 -0
- package/src/parallax.tsrx +321 -0
- package/src/parallax.tsrx.d.ts +60 -0
- package/src/shared/FrameLoop.ts +75 -0
- package/src/shared/createInterpolator.ts +85 -0
- package/src/shared/globals.ts +7 -0
- package/src/shared/index.ts +4 -0
- package/src/shared/stringInterpolation.ts +56 -0
- package/src/spring-component.ts +36 -0
- package/src/types/animated.ts +10 -0
- package/src/types/common.ts +29 -0
- package/src/types/fluid.ts +9 -0
- package/src/types/index.ts +9 -0
- package/src/types/interpolate.ts +50 -0
- package/src/types/interpolation.ts +18 -0
- package/src/types/objects.ts +17 -0
- package/src/types/props.ts +275 -0
- package/src/types/utils.ts +57 -0
- package/src/upstream-compat.ts +164 -0
- package/src/web/AnimatedStyle.ts +51 -0
- package/src/web/animated.ts +118 -0
- package/src/web/applyAnimatedValues.ts +92 -0
- package/src/web/index.ts +3 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
// Behavioral Octane port of @react-spring/parallax v10.1.2.
|
|
2
|
+
import { raf } from '@react-spring/rafz';
|
|
3
|
+
import { createContext, useContext, useImperativeHandle, useLayoutEffect, useState } from 'octane';
|
|
4
|
+
import { config as configs, Controller, type SpringConfig } from './engine';
|
|
5
|
+
|
|
6
|
+
type ConfigProp = SpringConfig | ((key: string) => SpringConfig);
|
|
7
|
+
type RefObject<T> = { current: T | null };
|
|
8
|
+
|
|
9
|
+
export interface StickyConfig {
|
|
10
|
+
start?: number;
|
|
11
|
+
end?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface IParallaxLayer {
|
|
15
|
+
horizontal: boolean;
|
|
16
|
+
sticky?: StickyConfig;
|
|
17
|
+
isSticky: boolean;
|
|
18
|
+
setHeight(height: number, immediate?: boolean): void;
|
|
19
|
+
setPosition(height: number, scroll: number, immediate?: boolean): void;
|
|
20
|
+
stop(): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface IParallax {
|
|
24
|
+
config: ConfigProp;
|
|
25
|
+
horizontal: boolean;
|
|
26
|
+
busy: boolean;
|
|
27
|
+
space: number;
|
|
28
|
+
offset: number;
|
|
29
|
+
current: number;
|
|
30
|
+
controller: Controller<{ scroll: number }>;
|
|
31
|
+
layers: Set<IParallaxLayer>;
|
|
32
|
+
container: RefObject<HTMLDivElement>;
|
|
33
|
+
content: RefObject<HTMLDivElement>;
|
|
34
|
+
scrollTo(offset: number): void;
|
|
35
|
+
update(): void;
|
|
36
|
+
stop(): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ParallaxProps {
|
|
40
|
+
pages: number;
|
|
41
|
+
config?: ConfigProp;
|
|
42
|
+
horizontal?: boolean;
|
|
43
|
+
enabled?: boolean;
|
|
44
|
+
innerStyle?: Record<string, unknown>;
|
|
45
|
+
ref?: RefObject<IParallax> | ((value: IParallax | null) => void);
|
|
46
|
+
style?: Record<string, unknown>;
|
|
47
|
+
children?: unknown;
|
|
48
|
+
[key: string]: unknown;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ParallaxLayerProps {
|
|
52
|
+
horizontal?: boolean;
|
|
53
|
+
offset?: number;
|
|
54
|
+
speed?: number;
|
|
55
|
+
factor?: number;
|
|
56
|
+
sticky?: StickyConfig;
|
|
57
|
+
ref?: RefObject<IParallaxLayer> | ((value: IParallaxLayer | null) => void);
|
|
58
|
+
style?: Record<string, unknown>;
|
|
59
|
+
children?: unknown;
|
|
60
|
+
[key: string]: unknown;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface ParallaxState extends IParallax {
|
|
64
|
+
enabled: boolean;
|
|
65
|
+
pages: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface ParallaxLayerState extends IParallaxLayer {
|
|
69
|
+
factor: number;
|
|
70
|
+
offset: number;
|
|
71
|
+
speed: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const ParallaxContext = createContext<IParallax | null>(null);
|
|
75
|
+
const START_TRANSLATE_3D = 'translate3d(0px,0px,0px)';
|
|
76
|
+
|
|
77
|
+
function scrollProperty(horizontal: boolean): 'scrollLeft' | 'scrollTop' {
|
|
78
|
+
return horizontal ? 'scrollLeft' : 'scrollTop';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function updateLayerNode(
|
|
82
|
+
node: HTMLElement,
|
|
83
|
+
horizontal: boolean,
|
|
84
|
+
space: number,
|
|
85
|
+
translate: number,
|
|
86
|
+
): void {
|
|
87
|
+
node.style[horizontal ? 'width' : 'height'] = `${space}px`;
|
|
88
|
+
node.style[horizontal ? 'height' : 'width'] = '100%';
|
|
89
|
+
node.style.transform = horizontal
|
|
90
|
+
? `translate3d(${translate}px,0,0)`
|
|
91
|
+
: `translate3d(0,${translate}px,0)`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function Parallax(props: any) @{
|
|
95
|
+
const {
|
|
96
|
+
pages,
|
|
97
|
+
config = configs.slow,
|
|
98
|
+
enabled = true,
|
|
99
|
+
horizontal = false,
|
|
100
|
+
innerStyle,
|
|
101
|
+
children,
|
|
102
|
+
ref: givenRef,
|
|
103
|
+
style,
|
|
104
|
+
...rest
|
|
105
|
+
} = props;
|
|
106
|
+
const [container] = useState<RefObject<HTMLDivElement>>(() => ({ current: null }));
|
|
107
|
+
const [content] = useState<RefObject<HTMLDivElement>>(() => ({ current: null }));
|
|
108
|
+
const [state] = useState<ParallaxState>(() => {
|
|
109
|
+
const controller = new Controller<{ scroll: number }>({ from: { scroll: 0 } });
|
|
110
|
+
const parallax: ParallaxState = {
|
|
111
|
+
config,
|
|
112
|
+
horizontal,
|
|
113
|
+
enabled,
|
|
114
|
+
pages,
|
|
115
|
+
busy: false,
|
|
116
|
+
space: 0,
|
|
117
|
+
offset: 0,
|
|
118
|
+
current: 0,
|
|
119
|
+
controller,
|
|
120
|
+
layers: new Set(),
|
|
121
|
+
container,
|
|
122
|
+
content,
|
|
123
|
+
update() {
|
|
124
|
+
const node = container.current;
|
|
125
|
+
if (node === null) return;
|
|
126
|
+
parallax.space = parallax.horizontal ? node.clientWidth : node.clientHeight;
|
|
127
|
+
const scroll = scrollProperty(parallax.horizontal);
|
|
128
|
+
if (parallax.enabled) parallax.current = node[scroll];
|
|
129
|
+
else node[scroll] = parallax.current = parallax.offset * parallax.space;
|
|
130
|
+
if (content.current !== null) {
|
|
131
|
+
content.current.style[parallax.horizontal ? 'width' : 'height'] = `${
|
|
132
|
+
parallax.space * parallax.pages
|
|
133
|
+
}px`;
|
|
134
|
+
}
|
|
135
|
+
for (const layer of parallax.layers) {
|
|
136
|
+
layer.setHeight(parallax.space, true);
|
|
137
|
+
layer.setPosition(parallax.space, parallax.current, true);
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
scrollTo(offset: number) {
|
|
141
|
+
const node = container.current;
|
|
142
|
+
if (node === null || !parallax.enabled) return;
|
|
143
|
+
parallax.offset = offset;
|
|
144
|
+
const scroll = scrollProperty(parallax.horizontal);
|
|
145
|
+
controller.set({ scroll: parallax.current });
|
|
146
|
+
controller.stop().start({
|
|
147
|
+
to: { scroll: offset * parallax.space },
|
|
148
|
+
config: parallax.config,
|
|
149
|
+
onChange(result) {
|
|
150
|
+
const value = result.value.scroll;
|
|
151
|
+
node[scroll] = value;
|
|
152
|
+
parallax.current = value;
|
|
153
|
+
for (const layer of parallax.layers) layer.setPosition(parallax.space, value, true);
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
},
|
|
157
|
+
stop() {
|
|
158
|
+
controller.stop();
|
|
159
|
+
parallax.busy = false;
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
return parallax;
|
|
163
|
+
});
|
|
164
|
+
state.config = config;
|
|
165
|
+
state.horizontal = horizontal;
|
|
166
|
+
state.enabled = enabled;
|
|
167
|
+
state.pages = pages;
|
|
168
|
+
useImperativeHandle(givenRef, () => state, []);
|
|
169
|
+
|
|
170
|
+
useLayoutEffect(() => {
|
|
171
|
+
const node = container.current;
|
|
172
|
+
if (node === null) return;
|
|
173
|
+
let live = true;
|
|
174
|
+
const onScroll = () => {
|
|
175
|
+
if (!enabled || state.busy) return;
|
|
176
|
+
state.busy = true;
|
|
177
|
+
state.current = node[scrollProperty(state.horizontal)];
|
|
178
|
+
raf.onStart(() => {
|
|
179
|
+
if (!live) return;
|
|
180
|
+
for (const layer of state.layers) layer.setPosition(state.space, state.current);
|
|
181
|
+
state.busy = false;
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
const onResize = () => state.update();
|
|
185
|
+
state.update();
|
|
186
|
+
node.addEventListener('scroll', onScroll, { passive: true });
|
|
187
|
+
const observer =
|
|
188
|
+
typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(onResize);
|
|
189
|
+
observer?.observe(node);
|
|
190
|
+
return () => {
|
|
191
|
+
live = false;
|
|
192
|
+
node.removeEventListener('scroll', onScroll);
|
|
193
|
+
observer?.disconnect();
|
|
194
|
+
state.stop();
|
|
195
|
+
for (const layer of state.layers) layer.stop();
|
|
196
|
+
};
|
|
197
|
+
}, [horizontal, enabled, pages]);
|
|
198
|
+
|
|
199
|
+
<div
|
|
200
|
+
{...rest}
|
|
201
|
+
ref={container}
|
|
202
|
+
onWheel={enabled ? state.stop : undefined}
|
|
203
|
+
onTouchStart={enabled ? state.stop : undefined}
|
|
204
|
+
style={{
|
|
205
|
+
position: 'absolute',
|
|
206
|
+
top: 0,
|
|
207
|
+
left: 0,
|
|
208
|
+
width: '100%',
|
|
209
|
+
height: '100%',
|
|
210
|
+
overflowY: enabled && !horizontal ? 'scroll' : 'hidden',
|
|
211
|
+
overflowX: enabled && horizontal ? 'scroll' : 'hidden',
|
|
212
|
+
WebkitOverflowScrolling: 'touch',
|
|
213
|
+
transform: START_TRANSLATE_3D,
|
|
214
|
+
...style,
|
|
215
|
+
}}
|
|
216
|
+
>
|
|
217
|
+
<div
|
|
218
|
+
ref={content}
|
|
219
|
+
style={{
|
|
220
|
+
overflow: 'hidden',
|
|
221
|
+
position: 'absolute',
|
|
222
|
+
width: horizontal ? `${state.space * pages}px` : '100%',
|
|
223
|
+
height: horizontal ? '100%' : `${state.space * pages}px`,
|
|
224
|
+
transform: START_TRANSLATE_3D,
|
|
225
|
+
...innerStyle,
|
|
226
|
+
}}
|
|
227
|
+
>
|
|
228
|
+
<ParallaxContext value={state}>{children}</ParallaxContext>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function ParallaxLayer(props: any) @{
|
|
234
|
+
const {
|
|
235
|
+
horizontal: layerHorizontal,
|
|
236
|
+
factor = 1,
|
|
237
|
+
offset = 0,
|
|
238
|
+
speed = 0,
|
|
239
|
+
sticky,
|
|
240
|
+
ref: givenRef,
|
|
241
|
+
style,
|
|
242
|
+
children,
|
|
243
|
+
...rest
|
|
244
|
+
} = props;
|
|
245
|
+
const parent = useContext(ParallaxContext);
|
|
246
|
+
if (parent === null) throw new Error('ParallaxLayer must be rendered inside Parallax');
|
|
247
|
+
const [node] = useState<RefObject<HTMLDivElement>>(() => ({ current: null }));
|
|
248
|
+
const [layer] = useState<ParallaxLayerState>(
|
|
249
|
+
() => ({
|
|
250
|
+
horizontal: layerHorizontal === undefined || sticky ? parent.horizontal : layerHorizontal,
|
|
251
|
+
factor,
|
|
252
|
+
offset,
|
|
253
|
+
speed,
|
|
254
|
+
sticky: sticky === undefined
|
|
255
|
+
? undefined
|
|
256
|
+
: { start: sticky.start ?? 0, end: sticky.end ?? (sticky.start ?? 0) + 1 },
|
|
257
|
+
isSticky: false,
|
|
258
|
+
setHeight(height) {
|
|
259
|
+
if (node.current === null) return;
|
|
260
|
+
const size = layer.sticky ? height : height * layer.factor;
|
|
261
|
+
node.current.style[layer.horizontal ? 'width' : 'height'] = `${size}px`;
|
|
262
|
+
},
|
|
263
|
+
setPosition(height, scroll) {
|
|
264
|
+
const element = node.current;
|
|
265
|
+
if (element === null) return;
|
|
266
|
+
let translate: number;
|
|
267
|
+
if (layer.sticky) {
|
|
268
|
+
const start = (layer.sticky.start ?? 0) * height;
|
|
269
|
+
const end = (layer.sticky.end ?? layer.sticky.start ?? 0) * height;
|
|
270
|
+
layer.isSticky = scroll >= start && scroll <= end;
|
|
271
|
+
element.style.position = layer.isSticky ? 'sticky' : 'absolute';
|
|
272
|
+
translate = layer.isSticky ? 0 : scroll < start ? start : end;
|
|
273
|
+
} else {
|
|
274
|
+
const targetScroll = Math.floor(layer.offset) * height;
|
|
275
|
+
translate = -(scroll * layer.speed) + height * layer.offset + targetScroll * layer.speed;
|
|
276
|
+
}
|
|
277
|
+
updateLayerNode(
|
|
278
|
+
element,
|
|
279
|
+
layer.horizontal,
|
|
280
|
+
layer.sticky ? height : height * layer.factor,
|
|
281
|
+
translate,
|
|
282
|
+
);
|
|
283
|
+
},
|
|
284
|
+
stop() {},
|
|
285
|
+
}),
|
|
286
|
+
);
|
|
287
|
+
layer.horizontal = layerHorizontal === undefined || sticky ? parent.horizontal : layerHorizontal;
|
|
288
|
+
layer.factor = factor;
|
|
289
|
+
layer.offset = offset;
|
|
290
|
+
layer.speed = speed;
|
|
291
|
+
layer.sticky = sticky === undefined
|
|
292
|
+
? undefined
|
|
293
|
+
: { start: sticky.start ?? 0, end: sticky.end ?? (sticky.start ?? 0) + 1 };
|
|
294
|
+
useImperativeHandle(givenRef, () => layer, []);
|
|
295
|
+
useLayoutEffect(() => {
|
|
296
|
+
parent.layers.add(layer);
|
|
297
|
+
parent.update();
|
|
298
|
+
return () => {
|
|
299
|
+
parent.layers.delete(layer);
|
|
300
|
+
layer.stop();
|
|
301
|
+
parent.update();
|
|
302
|
+
};
|
|
303
|
+
}, [parent, offset, speed, factor, sticky?.start, sticky?.end, layerHorizontal]);
|
|
304
|
+
|
|
305
|
+
<div
|
|
306
|
+
{...rest}
|
|
307
|
+
ref={node}
|
|
308
|
+
style={{
|
|
309
|
+
position: 'absolute',
|
|
310
|
+
top: 0,
|
|
311
|
+
bottom: 0,
|
|
312
|
+
left: 0,
|
|
313
|
+
right: 0,
|
|
314
|
+
backgroundSize: 'auto',
|
|
315
|
+
backgroundRepeat: 'no-repeat',
|
|
316
|
+
willChange: 'transform',
|
|
317
|
+
transform: START_TRANSLATE_3D,
|
|
318
|
+
...style,
|
|
319
|
+
}}
|
|
320
|
+
>{children}</div>
|
|
321
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Controller, SpringConfig } from './engine';
|
|
2
|
+
|
|
3
|
+
type ConfigProp = SpringConfig | ((key: string) => SpringConfig);
|
|
4
|
+
type ElementRef<T> = { current: T | null };
|
|
5
|
+
|
|
6
|
+
export interface StickyConfig {
|
|
7
|
+
start?: number;
|
|
8
|
+
end?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface IParallaxLayer {
|
|
12
|
+
horizontal: boolean;
|
|
13
|
+
sticky?: StickyConfig;
|
|
14
|
+
isSticky: boolean;
|
|
15
|
+
setHeight(height: number, immediate?: boolean): void;
|
|
16
|
+
setPosition(height: number, scroll: number, immediate?: boolean): void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface IParallax {
|
|
20
|
+
config: ConfigProp;
|
|
21
|
+
horizontal: boolean;
|
|
22
|
+
busy: boolean;
|
|
23
|
+
space: number;
|
|
24
|
+
offset: number;
|
|
25
|
+
current: number;
|
|
26
|
+
controller: Controller<{ scroll: number }>;
|
|
27
|
+
layers: Set<IParallaxLayer>;
|
|
28
|
+
container: ElementRef<HTMLDivElement>;
|
|
29
|
+
content: ElementRef<HTMLDivElement>;
|
|
30
|
+
scrollTo(offset: number): void;
|
|
31
|
+
update(): void;
|
|
32
|
+
stop(): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ParallaxProps {
|
|
36
|
+
pages: number;
|
|
37
|
+
config?: ConfigProp;
|
|
38
|
+
horizontal?: boolean;
|
|
39
|
+
enabled?: boolean;
|
|
40
|
+
innerStyle?: Record<string, unknown>;
|
|
41
|
+
ref?: ElementRef<IParallax> | ((value: IParallax | null) => void);
|
|
42
|
+
style?: Record<string, unknown>;
|
|
43
|
+
children?: unknown;
|
|
44
|
+
[key: string]: unknown;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ParallaxLayerProps {
|
|
48
|
+
horizontal?: boolean;
|
|
49
|
+
offset?: number;
|
|
50
|
+
speed?: number;
|
|
51
|
+
factor?: number;
|
|
52
|
+
sticky?: StickyConfig;
|
|
53
|
+
ref?: ElementRef<IParallaxLayer> | ((value: IParallaxLayer | null) => void);
|
|
54
|
+
style?: Record<string, unknown>;
|
|
55
|
+
children?: unknown;
|
|
56
|
+
[key: string]: unknown;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export declare function Parallax(props: ParallaxProps): unknown;
|
|
60
|
+
export declare function ParallaxLayer(props: ParallaxLayerProps): unknown;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Ported from react-spring v10.1.2 packages/shared/src/FrameLoop.ts.
|
|
2
|
+
import { raf } from '@react-spring/rafz';
|
|
3
|
+
|
|
4
|
+
export interface OpaqueAnimation {
|
|
5
|
+
idle: boolean;
|
|
6
|
+
priority: number;
|
|
7
|
+
advance(dt: number): void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const startQueue = new Set<OpaqueAnimation>();
|
|
11
|
+
let currentFrame: OpaqueAnimation[] = [];
|
|
12
|
+
let previousFrame: OpaqueAnimation[] = [];
|
|
13
|
+
let priority = 0;
|
|
14
|
+
|
|
15
|
+
export const frameLoop = {
|
|
16
|
+
get idle(): boolean {
|
|
17
|
+
return startQueue.size === 0 && currentFrame.length === 0;
|
|
18
|
+
},
|
|
19
|
+
start(animation: OpaqueAnimation): void {
|
|
20
|
+
if (priority > animation.priority) {
|
|
21
|
+
startQueue.add(animation);
|
|
22
|
+
raf.onStart(flushStartQueue);
|
|
23
|
+
} else {
|
|
24
|
+
startSafely(animation);
|
|
25
|
+
raf(advance);
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
advance,
|
|
29
|
+
sort(animation: OpaqueAnimation): void {
|
|
30
|
+
if (priority) raf.onFrame(() => frameLoop.sort(animation));
|
|
31
|
+
else {
|
|
32
|
+
const index = currentFrame.indexOf(animation);
|
|
33
|
+
if (index >= 0) {
|
|
34
|
+
currentFrame.splice(index, 1);
|
|
35
|
+
startUnsafely(animation);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
clear(): void {
|
|
40
|
+
currentFrame = [];
|
|
41
|
+
previousFrame = [];
|
|
42
|
+
startQueue.clear();
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function flushStartQueue(): void {
|
|
47
|
+
startQueue.forEach(startSafely);
|
|
48
|
+
startQueue.clear();
|
|
49
|
+
raf(advance);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function startSafely(animation: OpaqueAnimation): void {
|
|
53
|
+
if (!currentFrame.includes(animation)) startUnsafely(animation);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function startUnsafely(animation: OpaqueAnimation): void {
|
|
57
|
+
const index = currentFrame.findIndex((other) => other.priority > animation.priority);
|
|
58
|
+
currentFrame.splice(index < 0 ? currentFrame.length : index, 0, animation);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function advance(dt: number): boolean {
|
|
62
|
+
const nextFrame = previousFrame;
|
|
63
|
+
for (const animation of currentFrame) {
|
|
64
|
+
priority = animation.priority;
|
|
65
|
+
if (!animation.idle) {
|
|
66
|
+
animation.advance(dt);
|
|
67
|
+
if (!animation.idle) nextFrame.push(animation);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
priority = 0;
|
|
71
|
+
previousFrame = currentFrame;
|
|
72
|
+
previousFrame.length = 0;
|
|
73
|
+
currentFrame = nextFrame;
|
|
74
|
+
return currentFrame.length > 0;
|
|
75
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Behavioral port of react-spring v10.1.2 packages/shared/src/createInterpolator.ts.
|
|
2
|
+
import type {
|
|
3
|
+
EasingFunction,
|
|
4
|
+
ExtrapolateType,
|
|
5
|
+
InterpolatorConfig,
|
|
6
|
+
InterpolatorFn,
|
|
7
|
+
} from '../types/index';
|
|
8
|
+
import { createStringInterpolator } from './stringInterpolation';
|
|
9
|
+
|
|
10
|
+
export function createInterpolator<Output>(
|
|
11
|
+
config: InterpolatorConfig<Output>,
|
|
12
|
+
): InterpolatorFn<Output>;
|
|
13
|
+
export function createInterpolator(
|
|
14
|
+
range: readonly number[],
|
|
15
|
+
output: readonly number[],
|
|
16
|
+
extrapolate?: ExtrapolateType,
|
|
17
|
+
): InterpolatorFn<number>;
|
|
18
|
+
export function createInterpolator<Output>(
|
|
19
|
+
range: InterpolatorConfig<Output> | readonly number[] | InterpolatorFn<Output>,
|
|
20
|
+
output?: readonly number[],
|
|
21
|
+
extrapolate?: ExtrapolateType,
|
|
22
|
+
): InterpolatorFn<Output> {
|
|
23
|
+
if (typeof range === 'function') return range;
|
|
24
|
+
if (Array.isArray(range)) {
|
|
25
|
+
return createInterpolator({ range, output: output!, extrapolate }) as InterpolatorFn<Output>;
|
|
26
|
+
}
|
|
27
|
+
const config = range as InterpolatorConfig<Output>;
|
|
28
|
+
if (typeof config.output[0] === 'string') {
|
|
29
|
+
return createStringInterpolator(config as InterpolatorConfig<string>) as InterpolatorFn<Output>;
|
|
30
|
+
}
|
|
31
|
+
const outputRange = config.output as readonly number[];
|
|
32
|
+
const inputRange = config.range ?? [0, 1];
|
|
33
|
+
const easing = config.easing ?? ((value) => value);
|
|
34
|
+
return ((input: number) => {
|
|
35
|
+
const index = findRange(input, inputRange);
|
|
36
|
+
return interpolate(
|
|
37
|
+
input,
|
|
38
|
+
inputRange[index]!,
|
|
39
|
+
inputRange[index + 1]!,
|
|
40
|
+
outputRange[index]!,
|
|
41
|
+
outputRange[index + 1]!,
|
|
42
|
+
easing,
|
|
43
|
+
config.extrapolateLeft ?? config.extrapolate ?? 'extend',
|
|
44
|
+
config.extrapolateRight ?? config.extrapolate ?? 'extend',
|
|
45
|
+
config.map,
|
|
46
|
+
);
|
|
47
|
+
}) as unknown as InterpolatorFn<Output>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function interpolate(
|
|
51
|
+
input: number,
|
|
52
|
+
inputMin: number,
|
|
53
|
+
inputMax: number,
|
|
54
|
+
outputMin: number,
|
|
55
|
+
outputMax: number,
|
|
56
|
+
easing: EasingFunction,
|
|
57
|
+
extrapolateLeft: ExtrapolateType,
|
|
58
|
+
extrapolateRight: ExtrapolateType,
|
|
59
|
+
map?: (value: number) => number,
|
|
60
|
+
): number {
|
|
61
|
+
let result = map ? map(input) : input;
|
|
62
|
+
if (result < inputMin) {
|
|
63
|
+
if (extrapolateLeft === 'identity') return result;
|
|
64
|
+
if (extrapolateLeft === 'clamp') result = inputMin;
|
|
65
|
+
}
|
|
66
|
+
if (result > inputMax) {
|
|
67
|
+
if (extrapolateRight === 'identity') return result;
|
|
68
|
+
if (extrapolateRight === 'clamp') result = inputMax;
|
|
69
|
+
}
|
|
70
|
+
if (outputMin === outputMax) return outputMin;
|
|
71
|
+
if (inputMin === inputMax) return input <= inputMin ? outputMin : outputMax;
|
|
72
|
+
if (inputMin === -Infinity) result = -result;
|
|
73
|
+
else if (inputMax === Infinity) result -= inputMin;
|
|
74
|
+
else result = (result - inputMin) / (inputMax - inputMin);
|
|
75
|
+
result = easing(result);
|
|
76
|
+
if (outputMin === -Infinity) return -result;
|
|
77
|
+
if (outputMax === Infinity) return result + outputMin;
|
|
78
|
+
return result * (outputMax - outputMin) + outputMin;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function findRange(input: number, inputRange: readonly number[]): number {
|
|
82
|
+
let index = 1;
|
|
83
|
+
for (; index < inputRange.length - 1; index++) if (inputRange[index]! >= input) break;
|
|
84
|
+
return index - 1;
|
|
85
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Mutable target globals adapted from react-spring v10.1.2 packages/shared/src/globals.ts.
|
|
2
|
+
export const Globals = {
|
|
3
|
+
skipAnimation: false,
|
|
4
|
+
assign(values: { skipAnimation?: boolean }) {
|
|
5
|
+
if (values.skipAnimation !== undefined) this.skipAnimation = values.skipAnimation;
|
|
6
|
+
},
|
|
7
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Behavioral port of react-spring v10.1.2 packages/shared/src/stringInterpolation.ts.
|
|
2
|
+
import type { InterpolatorConfig, InterpolatorFn } from '../types/index';
|
|
3
|
+
import { createInterpolator } from './createInterpolator';
|
|
4
|
+
|
|
5
|
+
const numberRegex = /[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?/g;
|
|
6
|
+
const unitRegex = /[a-z%]+/i;
|
|
7
|
+
const rgbaRegex = /rgba\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*\)/g;
|
|
8
|
+
|
|
9
|
+
export function createStringInterpolator(
|
|
10
|
+
config: InterpolatorConfig<string>,
|
|
11
|
+
): InterpolatorFn<number, string> {
|
|
12
|
+
const output = config.output.slice();
|
|
13
|
+
// Number-less outputs (e.g. `none`) yield `[]` rather than throwing on a
|
|
14
|
+
// null match — see https://github.com/pmndrs/react-spring/issues/2327.
|
|
15
|
+
const tokens = output.map(function extractNumbers(value) {
|
|
16
|
+
return value.match(numberRegex) ?? [];
|
|
17
|
+
});
|
|
18
|
+
const outputRanges = tokens[0]!.map(function buildRange(_, index) {
|
|
19
|
+
return tokens.map(function valueAt(values) {
|
|
20
|
+
if (!(index in values)) throw new Error('The arity of each "output" value must be equal');
|
|
21
|
+
return Number(values[index]);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
const interpolators = outputRanges.map((range) =>
|
|
25
|
+
createInterpolator({ ...config, output: range }),
|
|
26
|
+
);
|
|
27
|
+
const inputRange = config.range ?? [0, 1];
|
|
28
|
+
const decimalCounts = tokens[0]!.map((_, position) => {
|
|
29
|
+
const counts = tokens.map((values) => {
|
|
30
|
+
const token = values[position]!;
|
|
31
|
+
const dot = token.indexOf('.');
|
|
32
|
+
return dot < 0 ? 0 : token.length - dot - 1;
|
|
33
|
+
});
|
|
34
|
+
return counts.every((count) => count === counts[0]) && counts[0]! > 0 ? counts[0]! : null;
|
|
35
|
+
});
|
|
36
|
+
return ((input: number) => {
|
|
37
|
+
const keyframe = inputRange.indexOf(input);
|
|
38
|
+
if (keyframe >= 0) return output[keyframe]!;
|
|
39
|
+
const missingUnit = !unitRegex.test(output[0]!)
|
|
40
|
+
? output.find((value) => unitRegex.test(value))?.replace(numberRegex, '')
|
|
41
|
+
: undefined;
|
|
42
|
+
let position = 0;
|
|
43
|
+
return output[0]!
|
|
44
|
+
.replace(numberRegex, () => {
|
|
45
|
+
const index = position++;
|
|
46
|
+
const value = interpolators[index]!(input);
|
|
47
|
+
const decimals = decimalCounts[index];
|
|
48
|
+
return `${decimals == null ? value : value.toFixed(decimals)}${missingUnit ?? ''}`;
|
|
49
|
+
})
|
|
50
|
+
.replace(
|
|
51
|
+
rgbaRegex,
|
|
52
|
+
(_, r, g, b, alpha) =>
|
|
53
|
+
`rgba(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)}, ${alpha})`,
|
|
54
|
+
);
|
|
55
|
+
}) as unknown as InterpolatorFn<number, string>;
|
|
56
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Spring as SpringImpl } from './components.tsrx';
|
|
2
|
+
import type { Scope } from 'octane';
|
|
3
|
+
import type { SpringValues } from './types/objects';
|
|
4
|
+
import type {
|
|
5
|
+
ControllerUpdate,
|
|
6
|
+
NoInfer,
|
|
7
|
+
SpringChain,
|
|
8
|
+
SpringToFn,
|
|
9
|
+
UnknownProps,
|
|
10
|
+
} from './types/index';
|
|
11
|
+
|
|
12
|
+
type SpringComponentProps<State extends object = UnknownProps> = unknown &
|
|
13
|
+
ControllerUpdate<State> & {
|
|
14
|
+
children: (values: SpringValues<State>) => unknown;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Infer state from "from" object prop.
|
|
18
|
+
export function Spring<State extends object>(
|
|
19
|
+
props: {
|
|
20
|
+
from: State;
|
|
21
|
+
to?: SpringChain<NoInfer<State>> | SpringToFn<NoInfer<State>>;
|
|
22
|
+
} & Omit<SpringComponentProps<NoInfer<State>>, 'from' | 'to'>,
|
|
23
|
+
): unknown;
|
|
24
|
+
|
|
25
|
+
// Infer state from "to" object prop.
|
|
26
|
+
export function Spring<State extends object>(
|
|
27
|
+
props: { to: State } & Omit<SpringComponentProps<NoInfer<State>>, 'to'>,
|
|
28
|
+
): unknown;
|
|
29
|
+
|
|
30
|
+
export function Spring(props: any, scope?: Scope, extra?: unknown): unknown {
|
|
31
|
+
return (SpringImpl as (props: any, scope?: Scope, extra?: unknown) => unknown)(
|
|
32
|
+
props,
|
|
33
|
+
scope,
|
|
34
|
+
extra,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** These types can be animated */
|
|
2
|
+
export type Animatable<T = any> = T extends number
|
|
3
|
+
? number
|
|
4
|
+
: T extends string
|
|
5
|
+
? string
|
|
6
|
+
: T extends ReadonlyArray<number | string>
|
|
7
|
+
? Array<number | string> extends T
|
|
8
|
+
? ReadonlyArray<number | string>
|
|
9
|
+
: { [P in keyof T]: Animatable<T[P]> }
|
|
10
|
+
: never;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Any, Remap } from './utils';
|
|
2
|
+
import type { FluidValue } from './fluid';
|
|
3
|
+
|
|
4
|
+
/** Replace the type of each `T` property with `never` (unless compatible with `U`) */
|
|
5
|
+
export type Valid<T, U> = NeverProps<T, InvalidKeys<T, U>>;
|
|
6
|
+
|
|
7
|
+
/** Replace the type of each `P` property with `never` */
|
|
8
|
+
type NeverProps<T, P extends keyof T> = Remap<Pick<T, Exclude<keyof T, P>> & { [K in P]: never }>;
|
|
9
|
+
|
|
10
|
+
/** Return a union type of every key whose `T` value is incompatible with its `U` value */
|
|
11
|
+
type InvalidKeys<T, U> = {
|
|
12
|
+
[P in keyof T & keyof U]: T[P] extends U[P] ? never : P;
|
|
13
|
+
}[keyof T & keyof U];
|
|
14
|
+
|
|
15
|
+
/** Unwrap any `FluidValue` object types */
|
|
16
|
+
export type RawValues<T extends object> = {
|
|
17
|
+
[P in keyof T]: T[P] extends FluidValue<infer U> ? U : T[P];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* For testing whether a type is an object but not an array.
|
|
22
|
+
*
|
|
23
|
+
* T extends IsPlainObject<T> ? true : false
|
|
24
|
+
*
|
|
25
|
+
* When `any` is passed, the resolved type is `true | false`.
|
|
26
|
+
*/
|
|
27
|
+
export type IsPlainObject<T> = T extends ReadonlyArray<any> ? Any : T extends object ? object : Any;
|
|
28
|
+
|
|
29
|
+
export type StringKeys<T> = T extends IsPlainObject<T> ? string & keyof T : string;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Minimal FluidValue typing surface for public prop inference (runtime uses FrameValue).
|
|
2
|
+
|
|
3
|
+
export abstract class FluidValue<T = any> {
|
|
4
|
+
abstract get(): T;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type FluidProps<T> = T extends object
|
|
8
|
+
? { [P in keyof T]: T[P] | FluidValue<Exclude<T[P], void>> }
|
|
9
|
+
: unknown;
|