@praxisjs/motion 0.2.3 → 1.1.0
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/CHANGELOG.md +65 -0
- package/dist/__tests__/decorators.test.d.ts +2 -0
- package/dist/__tests__/decorators.test.d.ts.map +1 -0
- package/dist/__tests__/decorators.test.js +179 -0
- package/dist/__tests__/decorators.test.js.map +1 -0
- package/dist/__tests__/easings.test.js +82 -55
- package/dist/__tests__/easings.test.js.map +1 -1
- package/dist/__tests__/spring.test.js +80 -34
- package/dist/__tests__/spring.test.js.map +1 -1
- package/dist/__tests__/transition.test.js +111 -43
- package/dist/__tests__/transition.test.js.map +1 -1
- package/dist/__tests__/tween.test.js +107 -94
- package/dist/__tests__/tween.test.js.map +1 -1
- package/dist/decorators.d.ts +3 -1
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +39 -16
- package/dist/decorators.js.map +1 -1
- package/dist/easings.d.ts.map +1 -1
- package/dist/easings.js +7 -1
- package/dist/easings.js.map +1 -1
- package/dist/index.d.ts +2 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -6
- package/dist/index.js.map +1 -1
- package/dist/spring.d.ts.map +1 -1
- package/dist/spring.js +3 -0
- package/dist/spring.js.map +1 -1
- package/dist/transition.d.ts.map +1 -1
- package/dist/transition.js +34 -18
- package/dist/transition.js.map +1 -1
- package/dist/tween.d.ts.map +1 -1
- package/dist/tween.js +2 -1
- package/dist/tween.js.map +1 -1
- package/package.json +3 -2
- package/src/__tests__/decorators.test.ts +196 -0
- package/src/__tests__/easings.test.ts +86 -57
- package/src/__tests__/spring.test.ts +90 -34
- package/src/__tests__/transition.test.ts +122 -45
- package/src/__tests__/tween.test.ts +116 -97
- package/src/decorators.ts +42 -15
- package/src/easings.ts +6 -1
- package/src/index.ts +2 -15
- package/src/spring.ts +4 -0
- package/src/transition.ts +28 -18
- package/src/tween.ts +2 -1
- package/dist/__tests__/use-motion.test.d.ts +0 -2
- package/dist/__tests__/use-motion.test.d.ts.map +0 -1
- package/dist/__tests__/use-motion.test.js +0 -139
- package/dist/__tests__/use-motion.test.js.map +0 -1
- package/dist/use-motion.d.ts +0 -20
- package/dist/use-motion.d.ts.map +0 -1
- package/dist/use-motion.js +0 -58
- package/dist/use-motion.js.map +0 -1
- package/src/__tests__/use-motion.test.ts +0 -172
- package/src/use-motion.ts +0 -89
|
@@ -1,60 +1,116 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
3
3
|
|
|
4
4
|
import { spring } from "../spring";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.useFakeTimers();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
vi.clearAllTimers();
|
|
12
|
+
vi.useRealTimers();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe("spring()", () => {
|
|
16
|
+
it("starts at the initial value", () => {
|
|
9
17
|
const s = spring(42);
|
|
10
18
|
expect(s.value()).toBe(42);
|
|
11
|
-
|
|
12
|
-
vi.useRealTimers();
|
|
19
|
+
s.stop();
|
|
13
20
|
});
|
|
14
21
|
|
|
15
|
-
it("target
|
|
16
|
-
vi.useFakeTimers();
|
|
22
|
+
it("target starts equal to initial value", () => {
|
|
17
23
|
const s = spring(10);
|
|
18
24
|
expect(s.target()).toBe(10);
|
|
19
|
-
|
|
20
|
-
vi.useRealTimers();
|
|
25
|
+
s.stop();
|
|
21
26
|
});
|
|
22
27
|
|
|
23
|
-
it("stop() cancels
|
|
24
|
-
vi.useFakeTimers();
|
|
28
|
+
it("stop() cancels pending animation", () => {
|
|
25
29
|
const s = spring(0);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
s.target.set(100);
|
|
31
|
+
s.stop();
|
|
32
|
+
const valueAfterStop = s.value();
|
|
33
|
+
expect(typeof valueAfterStop).toBe("number");
|
|
29
34
|
});
|
|
30
35
|
|
|
31
|
-
it("
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
it("setting target triggers animation", () => {
|
|
37
|
+
const s = spring(0);
|
|
38
|
+
s.target.set(100);
|
|
39
|
+
expect(s.target()).toBe(100);
|
|
34
40
|
s.stop();
|
|
35
|
-
expect(() => { s.stop(); }).not.toThrow();
|
|
36
|
-
vi.clearAllTimers();
|
|
37
|
-
vi.useRealTimers();
|
|
38
41
|
});
|
|
39
42
|
|
|
40
|
-
it("
|
|
41
|
-
|
|
43
|
+
it("accepts custom stiffness and damping", () => {
|
|
44
|
+
const s = spring(0, { stiffness: 0.5, damping: 0.9 });
|
|
45
|
+
s.target.set(50);
|
|
46
|
+
expect(s.target()).toBe(50);
|
|
47
|
+
s.stop();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("accepts custom mass and precision", () => {
|
|
51
|
+
const s = spring(0, { mass: 2, precision: 0.01 });
|
|
52
|
+
s.target.set(100);
|
|
53
|
+
expect(typeof s.value()).toBe("number");
|
|
54
|
+
s.stop();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("value is a computed (function)", () => {
|
|
42
58
|
const s = spring(0);
|
|
59
|
+
expect(typeof s.value).toBe("function");
|
|
60
|
+
s.stop();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("target is a signal with a set method", () => {
|
|
64
|
+
const s = spring(0);
|
|
65
|
+
expect(typeof s.target.set).toBe("function");
|
|
66
|
+
s.stop();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("settting target multiple times accumulates velocity direction", () => {
|
|
70
|
+
const s = spring(0, { stiffness: 0.5, damping: 0.5 });
|
|
71
|
+
s.target.set(10);
|
|
72
|
+
s.target.set(20);
|
|
73
|
+
expect(s.target()).toBe(20);
|
|
74
|
+
s.stop();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("tick advances value toward target", () => {
|
|
78
|
+
const s = spring(0, { stiffness: 0.5, damping: 0.8, precision: 0.01 });
|
|
43
79
|
s.target.set(100);
|
|
44
|
-
|
|
45
|
-
vi.advanceTimersByTime(100);
|
|
46
|
-
// Value should have moved towards 100
|
|
80
|
+
vi.advanceTimersByTime(50);
|
|
47
81
|
expect(s.value()).toBeGreaterThan(0);
|
|
48
82
|
s.stop();
|
|
49
|
-
vi.clearAllTimers();
|
|
50
|
-
vi.useRealTimers();
|
|
51
83
|
});
|
|
52
84
|
|
|
53
|
-
it("
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
85
|
+
it("moves value closer to target over time", () => {
|
|
86
|
+
const s = spring(0, { stiffness: 0.5, damping: 0.95, precision: 0.01 });
|
|
87
|
+
s.target.set(100);
|
|
88
|
+
vi.advanceTimersByTime(500);
|
|
89
|
+
// Value should have moved significantly toward target
|
|
90
|
+
expect(s.value()).toBeGreaterThan(10);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("stiffness = 0 throws a descriptive error", () => {
|
|
94
|
+
expect(() => spring(0, { stiffness: 0 })).toThrow("stiffness must be greater than 0");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("initial value already equals target — animation stops immediately (no rAF loop)", () => {
|
|
98
|
+
const s = spring(50, { stiffness: 0.5, damping: 0.8, precision: 0.001 });
|
|
99
|
+
// target is also 50 (the default), so the spring should converge instantly
|
|
100
|
+
const initial = s.value();
|
|
101
|
+
vi.advanceTimersByTime(200);
|
|
102
|
+
// Value should remain at (or very close to) 50 — no drift
|
|
103
|
+
expect(s.value()).toBeCloseTo(initial, 3);
|
|
104
|
+
s.stop();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("stop() called while animating — value is frozen and does not continue to change", () => {
|
|
108
|
+
const s = spring(0, { stiffness: 0.5, damping: 0.8 });
|
|
109
|
+
s.target.set(100);
|
|
110
|
+
vi.advanceTimersByTime(50);
|
|
111
|
+
s.stop();
|
|
112
|
+
const valueAfterStop = s.value();
|
|
113
|
+
vi.advanceTimersByTime(500);
|
|
114
|
+
expect(s.value()).toBe(valueAfterStop);
|
|
59
115
|
});
|
|
60
116
|
});
|
|
@@ -1,75 +1,152 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
3
3
|
|
|
4
4
|
import { createTransition } from "../transition";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const t = createTransition({ name: "fade", duration: 100 });
|
|
10
|
-
const el = document.createElement("div");
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.useFakeTimers();
|
|
8
|
+
});
|
|
11
9
|
|
|
12
|
-
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
vi.clearAllTimers();
|
|
12
|
+
vi.useRealTimers();
|
|
13
|
+
});
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
function makeEl() {
|
|
16
|
+
return document.createElement("div");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("createTransition()", () => {
|
|
20
|
+
it("enter() adds enter-from class immediately", () => {
|
|
21
|
+
const t = createTransition({ name: "fade" });
|
|
22
|
+
const el = makeEl();
|
|
23
|
+
void t.enter(el);
|
|
15
24
|
expect(el.classList.contains("fade-enter-from")).toBe(true);
|
|
25
|
+
});
|
|
16
26
|
|
|
17
|
-
|
|
18
|
-
|
|
27
|
+
it("enter() removes enter-from and adds enter-to after rAF", () => {
|
|
28
|
+
const t = createTransition({ name: "fade", duration: 1000 });
|
|
29
|
+
const el = makeEl();
|
|
30
|
+
void t.enter(el);
|
|
31
|
+
// Advance past rAF (16ms) but not past duration (1000ms)
|
|
32
|
+
vi.advanceTimersByTime(20);
|
|
33
|
+
expect(el.classList.contains("fade-enter-from")).toBe(false);
|
|
34
|
+
expect(el.classList.contains("fade-enter-to")).toBe(true);
|
|
35
|
+
});
|
|
19
36
|
|
|
37
|
+
it("enter() resolves and cleans up after duration", async () => {
|
|
38
|
+
const t = createTransition({ name: "fade", duration: 300 });
|
|
39
|
+
const el = makeEl();
|
|
40
|
+
const p = t.enter(el);
|
|
41
|
+
vi.advanceTimersByTime(300);
|
|
20
42
|
await p;
|
|
21
|
-
expect(el.classList.contains("fade-enter-from")).toBe(false);
|
|
22
43
|
expect(el.classList.contains("fade-enter-to")).toBe(false);
|
|
23
|
-
vi.useRealTimers();
|
|
24
44
|
});
|
|
25
45
|
|
|
26
|
-
it("leave() adds
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const p = t.leave(el);
|
|
46
|
+
it("leave() adds leave-from class immediately", () => {
|
|
47
|
+
const t = createTransition({ name: "slide" });
|
|
48
|
+
const el = makeEl();
|
|
49
|
+
void t.leave(el);
|
|
32
50
|
expect(el.classList.contains("slide-leave-from")).toBe(true);
|
|
51
|
+
});
|
|
33
52
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
53
|
+
it("leave() removes leave-from and adds leave-to after rAF", () => {
|
|
54
|
+
const t = createTransition({ name: "slide", duration: 1000 });
|
|
55
|
+
const el = makeEl();
|
|
56
|
+
void t.leave(el);
|
|
57
|
+
// Advance past rAF (16ms) but not past duration (1000ms)
|
|
58
|
+
vi.advanceTimersByTime(20);
|
|
37
59
|
expect(el.classList.contains("slide-leave-from")).toBe(false);
|
|
60
|
+
expect(el.classList.contains("slide-leave-to")).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("leave() resolves and cleans up after duration", async () => {
|
|
64
|
+
const t = createTransition({ name: "slide", duration: 200 });
|
|
65
|
+
const el = makeEl();
|
|
66
|
+
const p = t.leave(el);
|
|
67
|
+
vi.advanceTimersByTime(200);
|
|
68
|
+
await p;
|
|
38
69
|
expect(el.classList.contains("slide-leave-to")).toBe(false);
|
|
39
|
-
vi.useRealTimers();
|
|
40
70
|
});
|
|
41
71
|
|
|
42
|
-
it("
|
|
43
|
-
|
|
72
|
+
it("uses default name 'transition' when none provided", () => {
|
|
73
|
+
const t = createTransition();
|
|
74
|
+
const el = makeEl();
|
|
75
|
+
void t.enter(el);
|
|
76
|
+
expect(el.classList.contains("transition-enter-from")).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("calls onEnter callback with element", () => {
|
|
44
80
|
const onEnter = vi.fn();
|
|
45
|
-
const t = createTransition({ onEnter
|
|
46
|
-
const el =
|
|
47
|
-
|
|
81
|
+
const t = createTransition({ onEnter });
|
|
82
|
+
const el = makeEl();
|
|
83
|
+
void t.enter(el);
|
|
48
84
|
expect(onEnter).toHaveBeenCalledWith(el);
|
|
49
|
-
await vi.runAllTimersAsync();
|
|
50
|
-
await p;
|
|
51
|
-
vi.useRealTimers();
|
|
52
85
|
});
|
|
53
86
|
|
|
54
|
-
it("calls onLeave callback",
|
|
55
|
-
vi.useFakeTimers();
|
|
87
|
+
it("calls onLeave callback with element", () => {
|
|
56
88
|
const onLeave = vi.fn();
|
|
57
|
-
const t = createTransition({ onLeave
|
|
58
|
-
const el =
|
|
59
|
-
|
|
89
|
+
const t = createTransition({ onLeave });
|
|
90
|
+
const el = makeEl();
|
|
91
|
+
void t.leave(el);
|
|
60
92
|
expect(onLeave).toHaveBeenCalledWith(el);
|
|
61
|
-
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("uses default duration of 300ms", async () => {
|
|
96
|
+
const t = createTransition({ name: "x" });
|
|
97
|
+
const el = makeEl();
|
|
98
|
+
const p = t.enter(el);
|
|
99
|
+
vi.advanceTimersByTime(300);
|
|
62
100
|
await p;
|
|
63
|
-
|
|
101
|
+
expect(el.classList.contains("x-enter-to")).toBe(false);
|
|
64
102
|
});
|
|
65
103
|
|
|
66
|
-
it("
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
t
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
104
|
+
it("enter() interrupted by leave() before completion — no mixed CSS classes left", async () => {
|
|
105
|
+
const t = createTransition({ name: "fade", duration: 300 });
|
|
106
|
+
const el = makeEl();
|
|
107
|
+
|
|
108
|
+
// Start enter but don't resolve it yet
|
|
109
|
+
const enterP = t.enter(el);
|
|
110
|
+
vi.advanceTimersByTime(20); // past rAF, enter-to is now present
|
|
111
|
+
|
|
112
|
+
// Immediately start leave
|
|
113
|
+
const leaveP = t.leave(el);
|
|
114
|
+
vi.advanceTimersByTime(300);
|
|
115
|
+
|
|
116
|
+
await leaveP;
|
|
117
|
+
// Advance well past enter duration too
|
|
118
|
+
vi.advanceTimersByTime(300);
|
|
119
|
+
await enterP;
|
|
120
|
+
|
|
121
|
+
// After both complete, no transition classes should remain
|
|
122
|
+
expect(el.classList.contains("fade-enter-from")).toBe(false);
|
|
123
|
+
expect(el.classList.contains("fade-enter-to")).toBe(false);
|
|
124
|
+
expect(el.classList.contains("fade-leave-from")).toBe(false);
|
|
125
|
+
expect(el.classList.contains("fade-leave-to")).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("onEnter callback throws — promise rejects with that error", async () => {
|
|
129
|
+
const t = createTransition({
|
|
130
|
+
onEnter: () => { throw new Error("enter callback error"); },
|
|
131
|
+
});
|
|
132
|
+
const el = makeEl();
|
|
133
|
+
await expect(t.enter(el)).rejects.toThrow("enter callback error");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("onLeave callback throws — promise rejects with that error", async () => {
|
|
137
|
+
const t = createTransition({
|
|
138
|
+
onLeave: () => { throw new Error("leave callback error"); },
|
|
139
|
+
});
|
|
140
|
+
const el = makeEl();
|
|
141
|
+
await expect(t.leave(el)).rejects.toThrow("leave callback error");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("duration = 0 — resolves immediately without hanging", async () => {
|
|
145
|
+
const t = createTransition({ name: "fast", duration: 0 });
|
|
146
|
+
const el = makeEl();
|
|
147
|
+
const p = t.enter(el);
|
|
148
|
+
vi.advanceTimersByTime(0);
|
|
149
|
+
await p;
|
|
150
|
+
expect(el.classList.contains("fast-enter-to")).toBe(false);
|
|
74
151
|
});
|
|
75
152
|
});
|
|
@@ -1,136 +1,155 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
3
3
|
|
|
4
|
-
import { Animate } from "../decorators";
|
|
5
4
|
import { tween } from "../tween";
|
|
6
5
|
|
|
7
|
-
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.useFakeTimers();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
vi.clearAllTimers();
|
|
12
|
+
vi.useRealTimers();
|
|
13
|
+
});
|
|
8
14
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
vi.
|
|
15
|
+
function flushRaf(frames = 60) {
|
|
16
|
+
for (let i = 0; i < frames; i++) {
|
|
17
|
+
vi.runAllTicks();
|
|
18
|
+
const cbs = (globalThis as unknown as { __rafCallbacks?: Array<(t: number) => void> }).__rafCallbacks;
|
|
19
|
+
if (cbs?.length) {
|
|
20
|
+
const batch = [...cbs];
|
|
21
|
+
cbs.length = 0;
|
|
22
|
+
batch.forEach((cb) => cb(performance.now()));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("tween()", () => {
|
|
28
|
+
it("starts at the `from` value", () => {
|
|
12
29
|
const t = tween(0, 100);
|
|
13
30
|
expect(t.value()).toBe(0);
|
|
14
|
-
|
|
15
|
-
vi.useRealTimers();
|
|
31
|
+
t.stop();
|
|
16
32
|
});
|
|
17
33
|
|
|
18
|
-
it("
|
|
19
|
-
vi.useFakeTimers();
|
|
34
|
+
it("exposes the target signal", () => {
|
|
20
35
|
const t = tween(0, 100);
|
|
21
36
|
expect(t.target()).toBe(100);
|
|
22
|
-
|
|
23
|
-
vi.useRealTimers();
|
|
37
|
+
t.stop();
|
|
24
38
|
});
|
|
25
39
|
|
|
26
|
-
it("playing is
|
|
27
|
-
vi.useFakeTimers();
|
|
40
|
+
it("playing is true when animating", () => {
|
|
28
41
|
const t = tween(0, 100);
|
|
29
42
|
expect(t.playing()).toBe(true);
|
|
30
|
-
|
|
31
|
-
vi.useRealTimers();
|
|
43
|
+
t.stop();
|
|
32
44
|
});
|
|
33
45
|
|
|
34
|
-
it("stop() sets playing to false", () => {
|
|
35
|
-
vi.useFakeTimers();
|
|
46
|
+
it("stop() halts animation and sets playing to false", () => {
|
|
36
47
|
const t = tween(0, 100);
|
|
37
48
|
t.stop();
|
|
38
49
|
expect(t.playing()).toBe(false);
|
|
39
|
-
vi.clearAllTimers();
|
|
40
|
-
vi.useRealTimers();
|
|
41
50
|
});
|
|
42
51
|
|
|
43
|
-
it("reset()
|
|
44
|
-
|
|
45
|
-
|
|
52
|
+
it("reset() returns value to `from` and resets progress", () => {
|
|
53
|
+
const t = tween(0, 100);
|
|
54
|
+
t.stop();
|
|
46
55
|
t.reset();
|
|
47
|
-
expect(t.value()).toBe(
|
|
56
|
+
expect(t.value()).toBe(0);
|
|
48
57
|
expect(t.progress()).toBe(0);
|
|
49
|
-
vi.clearAllTimers();
|
|
50
|
-
vi.useRealTimers();
|
|
51
58
|
});
|
|
52
59
|
|
|
53
|
-
it("progress
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
it("progress starts at 0", () => {
|
|
61
|
+
const t = tween(0, 100);
|
|
62
|
+
expect(t.progress()).toBe(0);
|
|
63
|
+
t.stop();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("accepts custom easing as function", () => {
|
|
67
|
+
const t = tween(0, 100, { easing: (x) => x, duration: 300 });
|
|
68
|
+
expect(t.value()).toBe(0);
|
|
69
|
+
t.stop();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("accepts custom duration and delay", () => {
|
|
73
|
+
const t = tween(0, 100, { duration: 500, delay: 100 });
|
|
74
|
+
expect(t.value()).toBe(0);
|
|
75
|
+
t.stop();
|
|
61
76
|
});
|
|
62
77
|
|
|
63
|
-
it("
|
|
64
|
-
vi.useFakeTimers();
|
|
78
|
+
it("setting target to same value re-triggers start", () => {
|
|
65
79
|
const t = tween(0, 50);
|
|
80
|
+
const before = t.value();
|
|
81
|
+
t.target.set(50);
|
|
82
|
+
expect(t.value()).toBe(before);
|
|
83
|
+
t.stop();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("changing target while playing updates target", () => {
|
|
87
|
+
const t = tween(0, 100);
|
|
66
88
|
t.target.set(200);
|
|
67
|
-
expect(t.
|
|
89
|
+
expect(t.target()).toBe(200);
|
|
90
|
+
t.stop();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("animate loop runs and updates value toward target", () => {
|
|
94
|
+
const t = tween(0, 100, { duration: 100, easing: "linear" });
|
|
95
|
+
// Advance through multiple rAF frames
|
|
96
|
+
vi.advanceTimersByTime(50);
|
|
97
|
+
// Value should be moving toward 100
|
|
98
|
+
const mid = t.value();
|
|
99
|
+
expect(mid).toBeGreaterThan(0);
|
|
100
|
+
vi.advanceTimersByTime(100);
|
|
68
101
|
t.stop();
|
|
69
|
-
vi.clearAllTimers();
|
|
70
|
-
vi.useRealTimers();
|
|
71
102
|
});
|
|
72
103
|
|
|
73
|
-
it("
|
|
74
|
-
|
|
75
|
-
|
|
104
|
+
it("completes animation and stops playing", () => {
|
|
105
|
+
const t = tween(0, 100, { duration: 100 });
|
|
106
|
+
vi.advanceTimersByTime(500);
|
|
107
|
+
// After several durations, value should be at or near target
|
|
108
|
+
expect(t.value()).toBeGreaterThan(50);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("delay defers animation start", () => {
|
|
112
|
+
const t = tween(0, 100, { duration: 100, delay: 50 });
|
|
113
|
+
vi.advanceTimersByTime(10);
|
|
114
|
+
// Within delay, value stays at 0
|
|
76
115
|
expect(t.value()).toBe(0);
|
|
77
|
-
|
|
78
|
-
vi.useRealTimers();
|
|
116
|
+
t.stop();
|
|
79
117
|
});
|
|
80
|
-
});
|
|
81
118
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
};
|
|
99
|
-
}
|
|
119
|
+
it("duration = 0 — resolves immediately with value at `to`", () => {
|
|
120
|
+
// duration is clamped to 1ms minimum; after a single rAF the tween should complete
|
|
121
|
+
const t = tween(0, 100, { duration: 0, easing: "linear" });
|
|
122
|
+
vi.advanceTimersByTime(50);
|
|
123
|
+
expect(t.value()).toBe(100);
|
|
124
|
+
expect(t.playing()).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("from === to — resolves immediately without animating", () => {
|
|
128
|
+
const t = tween(42, 42, { duration: 300, easing: "linear" });
|
|
129
|
+
// Even before any rAF fires the start value equals the target
|
|
130
|
+
expect(t.value()).toBe(42);
|
|
131
|
+
vi.advanceTimersByTime(300);
|
|
132
|
+
expect(t.value()).toBe(42);
|
|
133
|
+
t.stop();
|
|
134
|
+
});
|
|
100
135
|
|
|
101
|
-
it("
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
expect(instance.opacity).toBe(0);
|
|
120
|
-
vi.clearAllTimers();
|
|
121
|
-
vi.useRealTimers();
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it("updating the property updates the tween target", () => {
|
|
125
|
-
vi.useFakeTimers();
|
|
126
|
-
const { ctx, run } = makeCtx("scale");
|
|
127
|
-
Animate()(undefined, ctx);
|
|
128
|
-
const instance: Record<string, unknown> = {};
|
|
129
|
-
run(instance);
|
|
130
|
-
instance.scale = 1;
|
|
131
|
-
instance.scale = 2; // second set updates tween target
|
|
132
|
-
expect(typeof instance.scale).toBe("number");
|
|
133
|
-
vi.clearAllTimers();
|
|
134
|
-
vi.useRealTimers();
|
|
136
|
+
it("progress() is 0 during delay phase before animation starts", () => {
|
|
137
|
+
const t = tween(0, 100, { duration: 100, delay: 200 });
|
|
138
|
+
vi.advanceTimersByTime(50); // well within delay
|
|
139
|
+
expect(t.progress()).toBe(0);
|
|
140
|
+
t.stop();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("easing function is called with t in [0,1] range only", () => {
|
|
144
|
+
const tValues: number[] = [];
|
|
145
|
+
const captureEasing = (t: number) => { tValues.push(t); return t; };
|
|
146
|
+
const tw = tween(0, 100, { duration: 100, easing: captureEasing });
|
|
147
|
+
vi.advanceTimersByTime(300);
|
|
148
|
+
tw.stop();
|
|
149
|
+
expect(tValues.length).toBeGreaterThan(0);
|
|
150
|
+
for (const t of tValues) {
|
|
151
|
+
expect(t).toBeGreaterThanOrEqual(0);
|
|
152
|
+
expect(t).toBeLessThanOrEqual(1);
|
|
153
|
+
}
|
|
135
154
|
});
|
|
136
155
|
});
|
package/src/decorators.ts
CHANGED
|
@@ -1,21 +1,48 @@
|
|
|
1
|
+
import { createFieldDecorator, type FieldBinding } from "@praxisjs/decorators";
|
|
2
|
+
|
|
3
|
+
import { spring, type SpringOptions } from "./spring";
|
|
1
4
|
import { tween, type TweenOptions, type Tween } from "./tween";
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
+
type SpringInstance = ReturnType<typeof spring>;
|
|
7
|
+
|
|
8
|
+
export function Tween(options: TweenOptions = {}) {
|
|
9
|
+
const tweens = new WeakMap<object, Tween>();
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
return createFieldDecorator({
|
|
12
|
+
bind(_instance, _name, _initialValue): FieldBinding {
|
|
13
|
+
return {
|
|
14
|
+
descriptor: {
|
|
15
|
+
get(this: object): number {
|
|
16
|
+
return tweens.get(this)?.value() ?? 0;
|
|
17
|
+
},
|
|
18
|
+
set(this: object, value: number): void {
|
|
19
|
+
if (!tweens.has(this)) tweens.set(this, tween(value, value, options));
|
|
20
|
+
tweens.get(this)?.target.set(value);
|
|
21
|
+
},
|
|
11
22
|
},
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
|
+
}) as unknown as (_value: undefined, context: ClassFieldDecoratorContext<any>) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function Spring(options: SpringOptions = {}) {
|
|
30
|
+
const springs = new WeakMap<object, SpringInstance>();
|
|
31
|
+
|
|
32
|
+
return createFieldDecorator({
|
|
33
|
+
bind(_instance, _name, _initialValue): FieldBinding {
|
|
34
|
+
return {
|
|
35
|
+
descriptor: {
|
|
36
|
+
get(this: object): number {
|
|
37
|
+
return springs.get(this)?.value() ?? 0;
|
|
38
|
+
},
|
|
39
|
+
set(this: object, value: number): void {
|
|
40
|
+
if (!springs.has(this)) springs.set(this, spring(value, options));
|
|
41
|
+
springs.get(this)?.target.set(value);
|
|
42
|
+
},
|
|
15
43
|
},
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
};
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
47
|
+
}) as unknown as (_value: undefined, context: ClassFieldDecoratorContext<any>) => void;
|
|
21
48
|
}
|