@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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +53 -0
  3. package/UPSTREAM.md +103 -0
  4. package/package.json +73 -0
  5. package/src/animated/Animated.ts +24 -0
  6. package/src/animated/AnimatedArray.ts +26 -0
  7. package/src/animated/index.ts +3 -0
  8. package/src/browser.ts +246 -0
  9. package/src/components.tsrx +60 -0
  10. package/src/components.tsrx.d.ts +33 -0
  11. package/src/context.ts +13 -0
  12. package/src/core/AnimationConfig.ts +60 -0
  13. package/src/core/AnimationResult.ts +19 -0
  14. package/src/core/Controller.ts +200 -0
  15. package/src/core/FrameValue.ts +16 -0
  16. package/src/core/Interpolation.ts +46 -0
  17. package/src/core/SpringRef.ts +26 -0
  18. package/src/core/SpringValue.ts +384 -0
  19. package/src/core/index.ts +9 -0
  20. package/src/core/runAsync.ts +17 -0
  21. package/src/core/scheduleProps.ts +34 -0
  22. package/src/core/valueInterpolation.ts +54 -0
  23. package/src/engine.ts +9 -0
  24. package/src/hooks.ts +610 -0
  25. package/src/index.ts +26 -0
  26. package/src/parallax.ts +8 -0
  27. package/src/parallax.tsrx +321 -0
  28. package/src/parallax.tsrx.d.ts +60 -0
  29. package/src/shared/FrameLoop.ts +75 -0
  30. package/src/shared/createInterpolator.ts +85 -0
  31. package/src/shared/globals.ts +7 -0
  32. package/src/shared/index.ts +4 -0
  33. package/src/shared/stringInterpolation.ts +56 -0
  34. package/src/spring-component.ts +36 -0
  35. package/src/types/animated.ts +10 -0
  36. package/src/types/common.ts +29 -0
  37. package/src/types/fluid.ts +9 -0
  38. package/src/types/index.ts +9 -0
  39. package/src/types/interpolate.ts +50 -0
  40. package/src/types/interpolation.ts +18 -0
  41. package/src/types/objects.ts +17 -0
  42. package/src/types/props.ts +275 -0
  43. package/src/types/utils.ts +57 -0
  44. package/src/upstream-compat.ts +164 -0
  45. package/src/web/AnimatedStyle.ts +51 -0
  46. package/src/web/animated.ts +118 -0
  47. package/src/web/applyAnimatedValues.ts +92 -0
  48. package/src/web/index.ts +3 -0
@@ -0,0 +1,384 @@
1
+ // React-free behavioral port of react-spring v10.1.2 packages/core/src/SpringValue.ts.
2
+ import { raf } from '@react-spring/rafz';
3
+ import { Globals } from '../shared/globals';
4
+ import {
5
+ type AnimationResult,
6
+ getCancelledResult,
7
+ getFinishedResult,
8
+ getNoopResult,
9
+ } from './AnimationResult';
10
+ import { FrameValue } from './FrameValue';
11
+ import { interpolateValue, isAnimatable } from './valueInterpolation';
12
+
13
+ export interface SpringConfig {
14
+ mass?: number;
15
+ tension?: number;
16
+ friction?: number;
17
+ precision?: number;
18
+ velocity?: number;
19
+ restVelocity?: number;
20
+ clamp?: boolean;
21
+ frequency?: number;
22
+ damping?: number;
23
+ bounce?: number;
24
+ decay?: boolean | number;
25
+ round?: number;
26
+ duration?: number;
27
+ easing?: (value: number) => number;
28
+ }
29
+
30
+ export type LoopProp<T> = boolean | (() => boolean | Partial<SpringUpdate<T>>);
31
+
32
+ export interface SpringUpdate<T> {
33
+ to?: T | FrameValue<T>;
34
+ from?: T;
35
+ config?: SpringConfig;
36
+ delay?: number;
37
+ immediate?: boolean;
38
+ reset?: boolean;
39
+ pause?: boolean;
40
+ cancel?: boolean;
41
+ loop?: LoopProp<T>;
42
+ /** When true (constructor default), capture event/config props into `defaultProps`. */
43
+ default?: boolean | Partial<SpringUpdate<T>>;
44
+ onStart?: (result: AnimationResult<T>, value: SpringValue<T>) => void;
45
+ onChange?: (result: AnimationResult<T>, value: SpringValue<T>) => void;
46
+ onPause?: (result: AnimationResult<T>, value: SpringValue<T>) => void;
47
+ onResume?: (result: AnimationResult<T>, value: SpringValue<T>) => void;
48
+ onRest?: (result: AnimationResult<T>, value: SpringValue<T>) => void;
49
+ onResolve?: (result: AnimationResult<T>, value: SpringValue<T>) => void;
50
+ }
51
+
52
+ // Upstream DEFAULT_PROPS: sticky only for `default: true`. One-shot flags
53
+ // (immediate/cancel/pause) and onResolve require `default: { ... }` object form.
54
+ const DEFAULT_PROP_KEYS = [
55
+ 'config',
56
+ 'onStart',
57
+ 'onChange',
58
+ 'onPause',
59
+ 'onResume',
60
+ 'onRest',
61
+ ] as const;
62
+
63
+ export const config = {
64
+ default: { tension: 170, friction: 26 },
65
+ gentle: { tension: 120, friction: 14 },
66
+ wobbly: { tension: 180, friction: 12 },
67
+ stiff: { tension: 210, friction: 20 },
68
+ slow: { tension: 280, friction: 60 },
69
+ molasses: { tension: 280, friction: 120 },
70
+ } as const;
71
+
72
+ interface Active<T> {
73
+ props: SpringUpdate<T>;
74
+ resolve: (result: AnimationResult<T>) => void;
75
+ frame?: (dt: number) => boolean;
76
+ timer?: ReturnType<typeof setTimeout>;
77
+ started: boolean;
78
+ settled: boolean;
79
+ }
80
+
81
+ export class SpringValue<T = number> extends FrameValue<T> {
82
+ private value: T;
83
+ private velocity = 0;
84
+ private active?: Active<T>;
85
+ private paused = false;
86
+ private hasAnimated = false;
87
+ /** Upstream-compatible defaults applied to later `start` calls. */
88
+ defaultProps: Partial<SpringUpdate<T>> = {};
89
+
90
+ constructor(from: T, props?: Omit<SpringUpdate<T>, 'to'> & { to?: T | FrameValue<T> });
91
+ constructor(props?: SpringUpdate<T>);
92
+ constructor(arg1?: any, arg2?: any) {
93
+ super();
94
+ const propsObject =
95
+ typeof arg1 === 'object' && arg1 !== null && !Array.isArray(arg1) && arg2 === undefined;
96
+ if (propsObject) {
97
+ const props = { ...(arg1 as SpringUpdate<T>) };
98
+ if (props.default === undefined) props.default = true;
99
+ this.value = (props.from ?? goal(props.to, undefined as T)) as T;
100
+ this.captureDefaults(props);
101
+ // Only start when there is a goal; event/config-only props must not set hasAnimated.
102
+ if (props.to !== undefined) void this.start({ ...props, default: false });
103
+ return;
104
+ }
105
+ if (arg2 !== undefined) {
106
+ // Upstream: spread props then assign positional `from` last; constructor defaults to true.
107
+ const props = { ...(arg2 as SpringUpdate<T>), from: arg1 as T };
108
+ if (props.default === undefined) props.default = true;
109
+ this.value = (props.from ?? goal(props.to, undefined as T)) as T;
110
+ this.captureDefaults(props);
111
+ if (props.to !== undefined) void this.start({ ...props, default: false });
112
+ return;
113
+ }
114
+ // Scalar seed only — leave hasAnimated false so later `from` updates still apply.
115
+ this.value = arg1 as T;
116
+ }
117
+ get(): T {
118
+ return this.value;
119
+ }
120
+ set(value: T): this {
121
+ this.stop(false);
122
+ this.setValue(value);
123
+ return this;
124
+ }
125
+ start(update: T | SpringUpdate<T>): Promise<AnimationResult<T>> {
126
+ const props = mergeWithDefaults(this.defaultProps, asUpdate(update));
127
+ this.captureDefaults(props);
128
+ if (props.cancel) {
129
+ this.stop(true);
130
+ return Promise.resolve(getCancelledResult(this.value));
131
+ }
132
+ const nextGoal = goal(props.to, this.value);
133
+ const active = this.active;
134
+ if (
135
+ active &&
136
+ !active.settled &&
137
+ !props.reset &&
138
+ props.from === undefined &&
139
+ Object.is(goal(active.props.to, this.value), nextGoal)
140
+ ) {
141
+ const previousResolve = active.resolve;
142
+ active.props = { ...active.props, ...props, to: active.props.to };
143
+ return new Promise(function waitForActive(resolve) {
144
+ active.resolve = function resolveBoth(result) {
145
+ previousResolve(result);
146
+ resolve(result);
147
+ };
148
+ });
149
+ }
150
+ this.stop(true);
151
+ this.paused = props.pause === true;
152
+ return new Promise((resolve) => {
153
+ const next: Active<T> = { props, resolve, started: false, settled: false };
154
+ this.active = next;
155
+ this.idle = false;
156
+ const begin = () => {
157
+ next.timer = undefined;
158
+ if (this.active !== next || next.settled) return;
159
+ this.run(next);
160
+ };
161
+ if ((props.delay ?? 0) > 0) next.timer = setTimeout(begin, props.delay);
162
+ else begin();
163
+ });
164
+ }
165
+ pause(): this {
166
+ if (!this.paused) {
167
+ this.paused = true;
168
+ const active = this.active;
169
+ if (active?.started) active.props.onPause?.(getFinishedResult(this.value, false), this);
170
+ }
171
+ return this;
172
+ }
173
+ resume(): this {
174
+ if (this.paused) {
175
+ this.paused = false;
176
+ const active = this.active;
177
+ if (active?.started) active.props.onResume?.(getFinishedResult(this.value, false), this);
178
+ }
179
+ return this;
180
+ }
181
+ stop(cancel = false): this {
182
+ const active = this.active;
183
+ if (!active || active.settled) return this;
184
+ if (active.timer) clearTimeout(active.timer);
185
+ if (active.frame) raf.cancel(active.frame);
186
+ const result = cancel ? getCancelledResult(this.value) : getFinishedResult(this.value, false);
187
+ this.settle(active, result, active.started);
188
+ return this;
189
+ }
190
+
191
+ private run(active: Active<T>): void {
192
+ const props = active.props;
193
+ // Upstream applies `from` only before the first animation or when `reset` is set.
194
+ if (props.from !== undefined && (props.reset || !this.hasAnimated)) {
195
+ this.setValue(props.from);
196
+ }
197
+ // From-only / noop starts must not poison later `from` updates.
198
+ if (props.to !== undefined || props.reset) this.hasAnimated = true;
199
+ active.started = true;
200
+ this.velocity = props.config?.velocity ?? 0;
201
+ const target = goal(props.to, this.value);
202
+ const immediate =
203
+ props.immediate === true ||
204
+ Globals.skipAnimation ||
205
+ !isAnimatable(this.value) ||
206
+ !isAnimatable(target) ||
207
+ Object.is(this.value, target);
208
+ props.onStart?.(
209
+ Object.is(this.value, target)
210
+ ? getNoopResult(this.value)
211
+ : getFinishedResult(this.value, false),
212
+ this,
213
+ );
214
+ if (immediate) {
215
+ this.setValue(target);
216
+ props.onChange?.(getFinishedResult(this.value, false), this);
217
+ void this.completeIteration(active);
218
+ return;
219
+ }
220
+ let elapsed = 0;
221
+ const from = this.value;
222
+ let progress = 0;
223
+ const options = { mass: 1, damping: 1, ...config.default, ...props.config };
224
+ if (options.frequency !== undefined) {
225
+ const frequency = Math.max(0.01, options.frequency);
226
+ options.tension = Math.pow((2 * Math.PI) / frequency, 2) * options.mass;
227
+ options.friction = (4 * Math.PI * Math.max(0, options.damping) * options.mass) / frequency;
228
+ }
229
+ active.frame = (dt) => {
230
+ if (this.active !== active || active.settled) return false;
231
+ if (this.paused) return true;
232
+ const currentTarget = goal(props.to, this.value);
233
+ let next: number;
234
+ let done: boolean;
235
+ if (options.decay) {
236
+ const decay = options.decay === true ? 0.998 : options.decay;
237
+ const step = (dt || 16.667) / 16.667;
238
+ this.velocity *= Math.pow(decay, step);
239
+ next = (this.value as number) + this.velocity * step;
240
+ done = Math.abs(this.velocity) <= (options.restVelocity ?? options.precision ?? 0.01);
241
+ } else if (options.duration !== undefined) {
242
+ elapsed += dt || 16.667;
243
+ progress = Math.min(1, elapsed / Math.max(1, options.duration));
244
+ next = options.easing?.(progress) ?? progress;
245
+ done = progress >= 1;
246
+ } else {
247
+ const step = Math.min(64, dt || 16.667) / 1000;
248
+ const displacement = 1 - progress;
249
+ this.velocity +=
250
+ ((options.tension * displacement - options.friction * this.velocity) /
251
+ (options.mass ?? 1)) *
252
+ step;
253
+ next = progress + this.velocity * step;
254
+ const precision = options.precision ?? 0.01;
255
+ done = Math.abs(this.velocity) <= precision && Math.abs(displacement) <= precision;
256
+ if (next > 1 && options.bounce !== undefined) {
257
+ next = 1;
258
+ this.velocity *= -1 + options.bounce;
259
+ } else if (options.clamp && Math.sign(1 - next) !== Math.sign(displacement)) done = true;
260
+ }
261
+ if (options.decay) {
262
+ if (options.round) next = Math.round(next / options.round) * options.round;
263
+ this.setValue(next as T);
264
+ } else {
265
+ progress = done ? 1 : next;
266
+ let shaped = interpolateValue(from, currentTarget, progress);
267
+ if (options.round && typeof shaped === 'number' && !done)
268
+ shaped = (Math.round(shaped / options.round) * options.round) as T;
269
+ this.setValue(done ? currentTarget : shaped);
270
+ }
271
+ props.onChange?.(getFinishedResult(this.value, false), this);
272
+ if (done) {
273
+ void this.completeIteration(active);
274
+ return false;
275
+ }
276
+ return true;
277
+ };
278
+ raf(active.frame);
279
+ }
280
+
281
+ private async completeIteration(active: Active<T>): Promise<void> {
282
+ const loop = active.props.loop;
283
+ const next = typeof loop === 'function' ? loop() : loop;
284
+ if (next && this.active === active && !active.settled) {
285
+ const nextProps = {
286
+ ...active.props,
287
+ ...(typeof next === 'object' ? next : {}),
288
+ loop,
289
+ reset: true,
290
+ };
291
+ if (nextProps.from === undefined && Object.is(this.value, goal(nextProps.to, this.value))) {
292
+ this.settle(active, getFinishedResult(this.value, true), true);
293
+ return;
294
+ }
295
+ active.props = nextProps;
296
+ this.run(active);
297
+ return;
298
+ }
299
+ this.settle(active, getFinishedResult(this.value, true), true);
300
+ }
301
+ private settle(active: Active<T>, result: AnimationResult<T>, notifyRest: boolean): void {
302
+ if (active.settled) return;
303
+ active.settled = true;
304
+ if (this.active === active) this.active = undefined;
305
+ this.idle = true;
306
+ if (notifyRest) active.props.onRest?.(result, this);
307
+ active.props.onResolve?.(result, this);
308
+ active.resolve(result);
309
+ }
310
+ private setValue(value: T): void {
311
+ if (Object.is(value, this.value)) return;
312
+ this.value = value;
313
+ this.emitChange(value);
314
+ }
315
+
316
+ private captureDefaults(props: SpringUpdate<T>): void {
317
+ if (!props.default) return;
318
+ if (props.default === true) {
319
+ for (const key of DEFAULT_PROP_KEYS) {
320
+ const value = props[key];
321
+ if (value !== undefined) {
322
+ (this.defaultProps as Record<string, unknown>)[key] = value;
323
+ }
324
+ }
325
+ return;
326
+ }
327
+ const source = props.default as Partial<SpringUpdate<T>>;
328
+ for (const key of Object.keys(source)) {
329
+ const value = (source as Record<string, unknown>)[key];
330
+ if (value !== undefined) {
331
+ (this.defaultProps as Record<string, unknown>)[key] = value;
332
+ }
333
+ }
334
+ }
335
+ }
336
+
337
+ function looksLikeSpringUpdate(value: object): boolean {
338
+ return (
339
+ 'to' in value ||
340
+ 'from' in value ||
341
+ 'config' in value ||
342
+ 'delay' in value ||
343
+ 'immediate' in value ||
344
+ 'reset' in value ||
345
+ 'pause' in value ||
346
+ 'cancel' in value ||
347
+ 'loop' in value ||
348
+ 'onStart' in value ||
349
+ 'onChange' in value ||
350
+ 'onPause' in value ||
351
+ 'onResume' in value ||
352
+ 'onRest' in value ||
353
+ 'onResolve' in value ||
354
+ 'default' in value
355
+ );
356
+ }
357
+
358
+ function asUpdate<T>(update: T | SpringUpdate<T>): SpringUpdate<T> {
359
+ if (
360
+ typeof update === 'object' &&
361
+ update !== null &&
362
+ !Array.isArray(update) &&
363
+ looksLikeSpringUpdate(update)
364
+ ) {
365
+ return update as SpringUpdate<T>;
366
+ }
367
+ return { to: update as T };
368
+ }
369
+
370
+ function mergeWithDefaults<T>(
371
+ defaults: Partial<SpringUpdate<T>>,
372
+ props: SpringUpdate<T>,
373
+ ): SpringUpdate<T> {
374
+ const merged: SpringUpdate<T> = { ...defaults, ...props };
375
+ if (defaults.config !== undefined || props.config !== undefined) {
376
+ merged.config = { ...defaults.config, ...props.config };
377
+ }
378
+ return merged;
379
+ }
380
+
381
+ function goal<T>(value: T | FrameValue<T> | undefined, fallback?: T): T {
382
+ if (value === undefined) return fallback as T;
383
+ return value instanceof FrameValue ? value.get() : value;
384
+ }
@@ -0,0 +1,9 @@
1
+ // React-free core surface ported from @react-spring/core v10.1.2.
2
+ export * from './AnimationConfig';
3
+ export * from './AnimationResult';
4
+ export * from './Controller';
5
+ export * from './Interpolation';
6
+ export * from './runAsync';
7
+ export * from './scheduleProps';
8
+ export * from './SpringRef';
9
+ export * from './SpringValue';
@@ -0,0 +1,17 @@
1
+ // React-free async sequencing primitive adapted from react-spring v10.1.2 packages/core/src/runAsync.ts.
2
+ import type { AnimationResult } from './AnimationResult';
3
+
4
+ export async function runAsync<State, Update>(
5
+ script: (next: (update: Update) => Promise<AnimationResult<State>>) => Promise<void> | void,
6
+ next: (update: Update) => Promise<AnimationResult<State>>,
7
+ getValue: () => State,
8
+ ): Promise<AnimationResult<State>> {
9
+ let result: AnimationResult<State> = {
10
+ value: getValue(),
11
+ finished: true,
12
+ cancelled: false,
13
+ noop: true,
14
+ };
15
+ await script(async (update) => (result = await next(update)));
16
+ return { ...result, value: getValue() };
17
+ }
@@ -0,0 +1,34 @@
1
+ // React-free delay/pause scheduler adapted from react-spring v10.1.2 packages/core/src/scheduleProps.ts.
2
+ export interface ScheduleState {
3
+ paused: boolean;
4
+ pauseQueue: Set<() => void>;
5
+ resumeQueue: Set<() => void>;
6
+ }
7
+
8
+ export function scheduleProps<T>(
9
+ delay: number,
10
+ state: ScheduleState,
11
+ start: () => Promise<T>,
12
+ ): Promise<T> {
13
+ return new Promise((resolve) => {
14
+ let remaining = Math.max(0, delay);
15
+ let startedAt = Date.now();
16
+ let timer: ReturnType<typeof setTimeout> | undefined;
17
+ const launch = () => {
18
+ state.pauseQueue.delete(pause);
19
+ state.resumeQueue.delete(resume);
20
+ void start().then(resolve);
21
+ };
22
+ const resume = () => {
23
+ startedAt = Date.now();
24
+ timer = setTimeout(launch, remaining);
25
+ };
26
+ const pause = () => {
27
+ if (timer) clearTimeout(timer);
28
+ remaining = Math.max(0, remaining - (Date.now() - startedAt));
29
+ };
30
+ state.pauseQueue.add(pause);
31
+ state.resumeQueue.add(resume);
32
+ if (!state.paused) resume();
33
+ });
34
+ }
@@ -0,0 +1,54 @@
1
+ // Value-shape normalization adapted from react-spring v10.1.2 animated/shared string machinery.
2
+ const numberPattern = /[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?/g;
3
+ const hasNumberPattern = /[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?/;
4
+
5
+ export function isAnimatable(value: unknown): boolean {
6
+ return (
7
+ typeof value === 'number' ||
8
+ (Array.isArray(value) && value.every((item) => typeof item === 'number')) ||
9
+ (typeof value === 'string' && (hasNumberPattern.test(value) || normalizeColor(value) !== value))
10
+ );
11
+ }
12
+
13
+ export function interpolateValue<T>(from: T, to: T, progress: number): T {
14
+ if (typeof from === 'number' && typeof to === 'number') return mix(from, to, progress) as T;
15
+ if (Array.isArray(from) && Array.isArray(to)) {
16
+ return from.map((value, index) => mix(Number(value), Number(to[index]), progress)) as T;
17
+ }
18
+ if (typeof from === 'string' && typeof to === 'string') {
19
+ const colorFrom = normalizeColor(from);
20
+ const colorTo = normalizeColor(to);
21
+ const color = colorFrom !== from || colorTo !== to;
22
+ const template = color ? colorFrom : from;
23
+ const fromTokens = template.match(numberPattern)?.map(Number) ?? [];
24
+ const toTokens = (color ? colorTo : to).match(numberPattern)?.map(Number) ?? [];
25
+ if (fromTokens.length !== toTokens.length) return (progress < 1 ? from : to) as T;
26
+ let index = 0;
27
+ const value = template.replace(numberPattern, () => {
28
+ const next = mix(fromTokens[index]!, toTokens[index]!, progress);
29
+ index++;
30
+ return String(next);
31
+ });
32
+ return (color ? roundRgba(value) : value) as T;
33
+ }
34
+ return (progress < 1 ? from : to) as T;
35
+ }
36
+
37
+ function mix(from: number, to: number, progress: number): number {
38
+ return from + (to - from) * progress;
39
+ }
40
+
41
+ function normalizeColor(value: string): string {
42
+ const hex = /^#([\da-f]{3}|[\da-f]{6})$/i.exec(value);
43
+ if (!hex) return value;
44
+ const digits = hex[1]!.length === 3 ? [...hex[1]!].map((char) => char + char).join('') : hex[1]!;
45
+ return `rgba(${Number.parseInt(digits.slice(0, 2), 16)}, ${Number.parseInt(digits.slice(2, 4), 16)}, ${Number.parseInt(digits.slice(4, 6), 16)}, 1)`;
46
+ }
47
+
48
+ function roundRgba(value: string): string {
49
+ return value.replace(
50
+ /rgba\(([^,]+),\s*([^,]+),\s*([^,]+),\s*([^\)]+)\)/,
51
+ (_, red, green, blue, alpha) =>
52
+ `rgba(${Math.round(Number(red))}, ${Math.round(Number(green))}, ${Math.round(Number(blue))}, ${alpha})`,
53
+ );
54
+ }
package/src/engine.ts ADDED
@@ -0,0 +1,9 @@
1
+ // Compatibility surface retained for the original Octane binding imports.
2
+ // Runtime implementations live in the React-free v10.1.2 core port.
3
+ export { Controller } from './core/Controller';
4
+ export type { ControllerTo, ControllerUpdate } from './core/Controller';
5
+ export { Interpolation, interpolate, to } from './core/Interpolation';
6
+ export { SpringRef } from './core/SpringRef';
7
+ export { SpringValue, config } from './core/SpringValue';
8
+ export type { LoopProp, SpringConfig, SpringUpdate } from './core/SpringValue';
9
+ export type { AnimationResult } from './core/AnimationResult';