@arcane-engine/runtime 0.1.0 → 0.2.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 (47) hide show
  1. package/package.json +4 -2
  2. package/src/agent/protocol.ts +35 -1
  3. package/src/agent/types.ts +98 -13
  4. package/src/particles/emitter.test.ts +323 -0
  5. package/src/particles/emitter.ts +409 -0
  6. package/src/particles/index.ts +25 -0
  7. package/src/particles/types.ts +236 -0
  8. package/src/pathfinding/astar.ts +27 -0
  9. package/src/pathfinding/types.ts +39 -0
  10. package/src/physics/aabb.ts +55 -8
  11. package/src/rendering/animation.ts +73 -0
  12. package/src/rendering/audio.ts +29 -9
  13. package/src/rendering/camera.ts +28 -4
  14. package/src/rendering/input.ts +45 -9
  15. package/src/rendering/lighting.ts +29 -3
  16. package/src/rendering/loop.ts +16 -3
  17. package/src/rendering/sprites.ts +24 -1
  18. package/src/rendering/text.ts +52 -6
  19. package/src/rendering/texture.ts +22 -4
  20. package/src/rendering/tilemap.ts +36 -4
  21. package/src/rendering/types.ts +37 -19
  22. package/src/rendering/validate.ts +48 -3
  23. package/src/state/error.ts +21 -2
  24. package/src/state/observe.ts +40 -9
  25. package/src/state/prng.ts +88 -10
  26. package/src/state/query.ts +115 -15
  27. package/src/state/store.ts +42 -11
  28. package/src/state/transaction.ts +116 -12
  29. package/src/state/types.ts +31 -5
  30. package/src/systems/system.ts +77 -5
  31. package/src/systems/types.ts +52 -6
  32. package/src/testing/harness.ts +103 -5
  33. package/src/testing/mock-renderer.test.ts +16 -20
  34. package/src/tweening/chain.test.ts +191 -0
  35. package/src/tweening/chain.ts +103 -0
  36. package/src/tweening/easing.test.ts +134 -0
  37. package/src/tweening/easing.ts +288 -0
  38. package/src/tweening/helpers.test.ts +185 -0
  39. package/src/tweening/helpers.ts +166 -0
  40. package/src/tweening/index.ts +76 -0
  41. package/src/tweening/tween.test.ts +322 -0
  42. package/src/tweening/tween.ts +296 -0
  43. package/src/tweening/types.ts +134 -0
  44. package/src/ui/colors.ts +129 -0
  45. package/src/ui/index.ts +1 -0
  46. package/src/ui/primitives.ts +44 -5
  47. package/src/ui/types.ts +41 -2
@@ -1,35 +1,81 @@
1
1
  import type { ArcaneError } from "../state/error.ts";
2
2
 
3
- /** A condition that must be true for a rule to fire. */
3
+ /**
4
+ * A predicate that checks whether a rule's preconditions are met.
5
+ * Must be a pure function.
6
+ *
7
+ * @param state - Current game state (read-only by convention).
8
+ * @param args - Additional arguments passed to {@link applyRule}.
9
+ * @returns True if the condition is satisfied.
10
+ */
4
11
  export type Condition<S> = (state: S, args: Record<string, unknown>) => boolean;
5
12
 
6
- /** An action that transforms state when a rule fires. */
13
+ /**
14
+ * A pure transform that produces new state when a rule fires.
15
+ * Multiple actions on a rule are chained: each receives the output of the previous.
16
+ *
17
+ * @param state - Current game state.
18
+ * @param args - Additional arguments passed to {@link applyRule}.
19
+ * @returns New state (must not mutate the input).
20
+ */
7
21
  export type Action<S> = (state: S, args: Record<string, unknown>) => S;
8
22
 
9
- /** A named rule with conditions and actions. */
23
+ /**
24
+ * A named rule consisting of conditions and actions.
25
+ *
26
+ * When applied via {@link applyRule}, all conditions must pass for the
27
+ * actions to execute. Actions are chained in order.
28
+ *
29
+ * @typeParam S - The game state type.
30
+ */
10
31
  export type Rule<S> = Readonly<{
32
+ /** Unique name within the system. Used for lookup by {@link applyRule} and {@link extend}. */
11
33
  name: string;
34
+ /** Conditions that must all return true for the rule to fire. Empty = always fires. */
12
35
  conditions: readonly Condition<S>[];
36
+ /** State transforms to apply in order when the rule fires. */
13
37
  actions: readonly Action<S>[];
38
+ /** If set, this rule replaces the rule with the given name when used in {@link extend}. */
14
39
  replaces?: string;
15
40
  }>;
16
41
 
17
- /** A named system: a collection of rules. */
42
+ /**
43
+ * A named system: an ordered collection of rules that together define game mechanics.
44
+ * Created via {@link system} and extended via {@link extend}.
45
+ *
46
+ * @typeParam S - The game state type.
47
+ */
18
48
  export type SystemDef<S> = Readonly<{
49
+ /** System name (e.g., "combat", "inventory"). */
19
50
  name: string;
51
+ /** Ordered list of rules in this system. */
20
52
  rules: readonly Rule<S>[];
21
53
  }>;
22
54
 
23
- /** Result of applying a single rule. */
55
+ /**
56
+ * Result of applying a single rule via {@link applyRule}.
57
+ *
58
+ * @typeParam S - The game state type.
59
+ */
24
60
  export type RuleResult<S> = Readonly<{
61
+ /** True if all conditions passed and actions executed successfully. */
25
62
  ok: boolean;
63
+ /** The resulting state (unchanged if ok is false). */
26
64
  state: S;
65
+ /** Name of the rule that was applied. */
27
66
  ruleName: string;
67
+ /** Error details if ok is false (rule not found or conditions failed). */
28
68
  error?: ArcaneError;
29
69
  }>;
30
70
 
31
- /** Options for extending a system. */
71
+ /**
72
+ * Options for extending a system via {@link extend}.
73
+ *
74
+ * @typeParam S - The game state type.
75
+ */
32
76
  export type ExtendOptions<S> = {
77
+ /** New rules to add. Rules with `replaces` set will replace existing rules by name. */
33
78
  rules?: readonly Rule<S>[];
79
+ /** Names of existing rules to remove from the system. */
34
80
  remove?: readonly string[];
35
81
  };
@@ -1,20 +1,97 @@
1
- // Universal test harness — works in both Node.js and V8 (deno_core).
2
- //
3
- // Node mode: delegates to node:test and node:assert.
4
- // V8 mode: standalone implementations with result reporting via
5
- // globalThis.__reportTest(suite, test, passed, error?)
1
+ /**
2
+ * Universal test harness for Arcane — works in both Node.js and V8 (deno_core).
3
+ *
4
+ * In **Node mode**: delegates to `node:test` and `node:assert`.
5
+ * In **V8 mode**: standalone implementations with result reporting via
6
+ * `globalThis.__reportTest(suite, test, passed, error?)`.
7
+ *
8
+ * Test files import `{ describe, it, assert }` from this module and work
9
+ * identically in both environments.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { describe, it, assert } from "../testing/harness.ts";
14
+ *
15
+ * describe("math", () => {
16
+ * it("adds numbers", () => {
17
+ * assert.equal(1 + 1, 2);
18
+ * });
19
+ *
20
+ * it("supports deep equality", () => {
21
+ * assert.deepEqual({ a: 1 }, { a: 1 });
22
+ * });
23
+ * });
24
+ * ```
25
+ */
6
26
 
27
+ /** A synchronous or async test function. */
7
28
  type TestFn = () => void | Promise<void>;
29
+
30
+ /** Function signature for `describe()` — defines a test suite. */
8
31
  type DescribeFn = (name: string, fn: () => void) => void;
32
+
33
+ /** Function signature for `it()` — defines a single test case. */
9
34
  type ItFn = (name: string, fn: TestFn) => void;
10
35
 
36
+ /**
37
+ * Assertion interface providing common test assertions.
38
+ *
39
+ * All assertion methods throw on failure with a descriptive error message.
40
+ */
11
41
  interface Assert {
42
+ /**
43
+ * Assert strict equality (`===`).
44
+ * @param actual - The value to test.
45
+ * @param expected - The expected value.
46
+ * @param message - Optional custom failure message.
47
+ */
12
48
  equal(actual: unknown, expected: unknown, message?: string): void;
49
+
50
+ /**
51
+ * Assert deep structural equality (recursive comparison of objects/arrays).
52
+ * @param actual - The value to test.
53
+ * @param expected - The expected structure.
54
+ * @param message - Optional custom failure message.
55
+ */
13
56
  deepEqual(actual: unknown, expected: unknown, message?: string): void;
57
+
58
+ /**
59
+ * Assert strict inequality (`!==`).
60
+ * @param actual - The value to test.
61
+ * @param expected - The value that `actual` must not equal.
62
+ * @param message - Optional custom failure message.
63
+ */
14
64
  notEqual(actual: unknown, expected: unknown, message?: string): void;
65
+
66
+ /**
67
+ * Assert that two values are NOT deeply equal.
68
+ * @param actual - The value to test.
69
+ * @param expected - The value that `actual` must not deeply equal.
70
+ * @param message - Optional custom failure message.
71
+ */
15
72
  notDeepEqual(actual: unknown, expected: unknown, message?: string): void;
73
+
74
+ /**
75
+ * Assert that a value is truthy.
76
+ * @param value - The value to test.
77
+ * @param message - Optional custom failure message.
78
+ */
16
79
  ok(value: unknown, message?: string): void;
80
+
81
+ /**
82
+ * Assert that a string matches a regular expression.
83
+ * @param actual - The string to test.
84
+ * @param expected - The regex pattern to match against.
85
+ * @param message - Optional custom failure message.
86
+ */
17
87
  match(actual: string, expected: RegExp, message?: string): void;
88
+
89
+ /**
90
+ * Assert that a function throws an error.
91
+ * @param fn - The function expected to throw.
92
+ * @param expected - Optional regex to match against the error message.
93
+ * @param message - Optional custom failure message.
94
+ */
18
95
  throws(fn: () => unknown, expected?: RegExp, message?: string): void;
19
96
  }
20
97
 
@@ -244,8 +321,29 @@ declare const __reportTest: (
244
321
  // Node.js — delegate to built-in modules
245
322
  // ---------------------------------------------------------------------------
246
323
 
324
+ /**
325
+ * Define a test suite. Can be nested with other describe() calls.
326
+ * In V8 mode, nested suites have their test names prefixed with the parent suite name.
327
+ *
328
+ * @param name - Suite name displayed in test output.
329
+ * @param fn - Function containing `it()` test cases and/or nested `describe()` calls.
330
+ */
247
331
  let describe: DescribeFn;
332
+
333
+ /**
334
+ * Define a single test case within a describe() suite.
335
+ * Supports both synchronous and async test functions.
336
+ *
337
+ * @param name - Test name displayed in test output.
338
+ * @param fn - Test function. Throw (or reject) to indicate failure; return to pass.
339
+ */
248
340
  let it: ItFn;
341
+
342
+ /**
343
+ * Assertion helpers for test cases. Methods throw on failure with descriptive messages.
344
+ *
345
+ * Available assertions: `equal`, `deepEqual`, `notEqual`, `notDeepEqual`, `ok`, `match`, `throws`.
346
+ */
249
347
  let assert: Assert;
250
348
 
251
349
  if (isNode) {
@@ -2,16 +2,12 @@
2
2
  * Tests demonstrating mock renderer usage
3
3
  */
4
4
 
5
- // @ts-nocheck - Uses Node-specific test APIs (beforeEach/afterEach) not in harness
6
5
  import { describe, it, assert } from "./harness.ts";
7
6
  import { mockRenderer, installMockRenderer, restoreRenderer } from "./mock-renderer.ts";
8
7
 
9
8
  describe("Mock Renderer", () => {
10
- beforeEach(() => {
11
- mockRenderer.reset();
12
- });
13
-
14
9
  it("should validate drawRect parameters", () => {
10
+ mockRenderer.reset();
15
11
  mockRenderer.drawRect(10.0, 20.0, 100.0, 50.0, { color: { r: 1.0, g: 0.5, b: 0.0 } });
16
12
 
17
13
  mockRenderer.assertNoErrors();
@@ -19,24 +15,27 @@ describe("Mock Renderer", () => {
19
15
  });
20
16
 
21
17
  it("should catch wrong parameter types", () => {
18
+ mockRenderer.reset();
22
19
  // Wrong: passing object instead of separate params
23
20
  mockRenderer.drawRect({ x: 10 } as any, undefined, undefined, undefined);
24
21
 
25
- assert(mockRenderer.hasErrors(), "Should have errors");
22
+ assert.ok(mockRenderer.hasErrors(), "Should have errors");
26
23
  const errors = mockRenderer.getErrors();
27
- assert(errors.some(e => e.includes("must be number")), "Should complain about type");
24
+ assert.ok(errors.some(e => e.includes("must be number")), "Should complain about type");
28
25
  });
29
26
 
30
27
  it("should catch invalid color values", () => {
28
+ mockRenderer.reset();
31
29
  // Color values must be 0.0-1.0
32
30
  mockRenderer.drawRect(0, 0, 100, 100, { color: { r: 255, g: 255, b: 255 } });
33
31
 
34
- assert(mockRenderer.hasErrors(), "Should have errors");
32
+ assert.ok(mockRenderer.hasErrors(), "Should have errors");
35
33
  const errors = mockRenderer.getErrors();
36
- assert(errors.some(e => e.includes("color.r must be 0.0-1.0")), "Should catch invalid color");
34
+ assert.ok(errors.some(e => e.includes("color.r must be 0.0-1.0")), "Should catch invalid color");
37
35
  });
38
36
 
39
37
  it("should validate drawText parameters", () => {
38
+ mockRenderer.reset();
40
39
  mockRenderer.drawText("Hello", { x: 10.0, y: 20.0, size: 16.0, color: { r: 1.0, g: 1.0, b: 1.0 } });
41
40
 
42
41
  mockRenderer.assertNoErrors();
@@ -44,26 +43,21 @@ describe("Mock Renderer", () => {
44
43
  });
45
44
 
46
45
  it("should catch NaN values", () => {
46
+ mockRenderer.reset();
47
47
  mockRenderer.drawRect(NaN, 0, 100, 100);
48
48
 
49
- assert(mockRenderer.hasErrors(), "Should catch NaN");
49
+ assert.ok(mockRenderer.hasErrors(), "Should catch NaN");
50
50
  const errors = mockRenderer.getErrors();
51
- assert(errors.some(e => e.includes("NaN")), "Should complain about NaN");
51
+ assert.ok(errors.some(e => e.includes("NaN")), "Should complain about NaN");
52
52
  });
53
53
  });
54
54
 
55
55
  // Example: Testing a game's rendering code
56
56
  describe("Game Visual Tests", () => {
57
- beforeEach(() => {
57
+ it("should render HUD correctly", () => {
58
58
  mockRenderer.reset();
59
59
  installMockRenderer();
60
- });
61
-
62
- afterEach(() => {
63
- restoreRenderer();
64
- });
65
60
 
66
- it("should render HUD correctly", () => {
67
61
  // Import game code that uses rendering
68
62
  function renderHUD(health: number, gold: number) {
69
63
  (globalThis as any).drawText(`HP: ${health}`, {
@@ -87,7 +81,9 @@ describe("Game Visual Tests", () => {
87
81
  mockRenderer.assertCalled("drawText", 2);
88
82
 
89
83
  const calls = mockRenderer.getCalls("drawText");
90
- assert(calls[0].params[0] === "HP: 100", "First text correct");
91
- assert(calls[1].params[0] === "Gold: 50", "Second text correct");
84
+ assert.ok(calls[0].params[0] === "HP: 100", "First text correct");
85
+ assert.ok(calls[1].params[0] === "Gold: 50", "Second text correct");
86
+
87
+ restoreRenderer();
92
88
  });
93
89
  });
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Tests for tween chaining
3
+ */
4
+
5
+ import { describe, it, assert } from "../testing/harness.ts";
6
+ import { sequence, parallel, stagger } from "./chain.ts";
7
+ import { updateTweens, stopAllTweens, getActiveTweenCount, reverseTween } from "./tween.ts";
8
+
9
+ describe("Tween Chaining", () => {
10
+ it("sequence should run tweens one after another", () => {
11
+ stopAllTweens();
12
+
13
+ const target1 = { x: 0 };
14
+ const target2 = { y: 0 };
15
+ const target3 = { z: 0 };
16
+
17
+ sequence([
18
+ { target: target1, props: { x: 100 }, duration: 1.0 },
19
+ { target: target2, props: { y: 100 }, duration: 1.0 },
20
+ { target: target3, props: { z: 100 }, duration: 1.0 },
21
+ ]);
22
+
23
+ // First tween should start immediately
24
+ updateTweens(1.0);
25
+ assert.equal(target1.x, 100);
26
+ assert.equal(target2.y, 0); // Not started yet
27
+ assert.equal(target3.z, 0);
28
+
29
+ // Second tween starts after first completes
30
+ updateTweens(1.0);
31
+ assert.equal(target1.x, 100);
32
+ assert.equal(target2.y, 100);
33
+ assert.equal(target3.z, 0); // Not started yet
34
+
35
+ // Third tween starts after second completes
36
+ updateTweens(1.0);
37
+ assert.equal(target1.x, 100);
38
+ assert.equal(target2.y, 100);
39
+ assert.equal(target3.z, 100);
40
+ });
41
+
42
+ it("parallel should run tweens simultaneously", () => {
43
+ stopAllTweens();
44
+
45
+ const target1 = { x: 0 };
46
+ const target2 = { y: 0 };
47
+ const target3 = { z: 0 };
48
+
49
+ parallel([
50
+ { target: target1, props: { x: 100 }, duration: 1.0 },
51
+ { target: target2, props: { y: 100 }, duration: 1.0 },
52
+ { target: target3, props: { z: 100 }, duration: 1.0 },
53
+ ]);
54
+
55
+ // All tweens should run simultaneously
56
+ updateTweens(0.5);
57
+ assert.equal(target1.x, 50);
58
+ assert.equal(target2.y, 50);
59
+ assert.equal(target3.z, 50);
60
+
61
+ updateTweens(0.5);
62
+ assert.equal(target1.x, 100);
63
+ assert.equal(target2.y, 100);
64
+ assert.equal(target3.z, 100);
65
+ });
66
+
67
+ it("stagger should delay each tween by stagger amount", () => {
68
+ stopAllTweens();
69
+
70
+ const target1 = { x: 0 };
71
+ const target2 = { y: 0 };
72
+ const target3 = { z: 0 };
73
+
74
+ stagger(
75
+ [
76
+ { target: target1, props: { x: 100 }, duration: 1.0 },
77
+ { target: target2, props: { y: 100 }, duration: 1.0 },
78
+ { target: target3, props: { z: 100 }, duration: 1.0 },
79
+ ],
80
+ 0.5
81
+ );
82
+
83
+ // First tween starts immediately (delay 0)
84
+ updateTweens(0.5);
85
+ assert.equal(target1.x, 50);
86
+ assert.equal(target2.y, 0); // Still in delay
87
+ assert.equal(target3.z, 0); // Still in delay
88
+
89
+ // Second tween starts (delay 0.5), first continues
90
+ updateTweens(0.5);
91
+ assert.equal(target1.x, 100); // Complete
92
+ assert.equal(target2.y, 50); // Halfway
93
+ assert.equal(target3.z, 0); // Still in delay
94
+
95
+ // Third tween starts (delay 1.0), second continues
96
+ updateTweens(0.5);
97
+ assert.equal(target1.x, 100);
98
+ assert.equal(target2.y, 100); // Complete
99
+ assert.equal(target3.z, 50); // Halfway
100
+
101
+ // All complete
102
+ updateTweens(0.5);
103
+ assert.equal(target1.x, 100);
104
+ assert.equal(target2.y, 100);
105
+ assert.equal(target3.z, 100);
106
+ });
107
+
108
+ it("sequence should chain callbacks correctly", () => {
109
+ stopAllTweens();
110
+
111
+ let completionOrder: number[] = [];
112
+
113
+ sequence([
114
+ {
115
+ target: { x: 0 },
116
+ props: { x: 100 },
117
+ duration: 1.0,
118
+ options: { onComplete: () => completionOrder.push(1) },
119
+ },
120
+ {
121
+ target: { y: 0 },
122
+ props: { y: 100 },
123
+ duration: 1.0,
124
+ options: { onComplete: () => completionOrder.push(2) },
125
+ },
126
+ {
127
+ target: { z: 0 },
128
+ props: { z: 100 },
129
+ duration: 1.0,
130
+ options: { onComplete: () => completionOrder.push(3) },
131
+ },
132
+ ]);
133
+
134
+ updateTweens(1.0);
135
+ assert.equal(completionOrder.length, 1);
136
+ assert.equal(completionOrder[0], 1);
137
+
138
+ updateTweens(1.0);
139
+ assert.equal(completionOrder.length, 2);
140
+ assert.equal(completionOrder[1], 2);
141
+
142
+ updateTweens(1.0);
143
+ assert.equal(completionOrder.length, 3);
144
+ assert.equal(completionOrder[2], 3);
145
+ });
146
+
147
+ it("reverseTween should swap start and target values", () => {
148
+ stopAllTweens();
149
+
150
+ const target = { x: 0 };
151
+ const tweens = parallel([
152
+ { target, props: { x: 100 }, duration: 1.0 },
153
+ ]);
154
+
155
+ // Animate halfway
156
+ updateTweens(0.5);
157
+ assert.equal(target.x, 50);
158
+ assert.equal(getActiveTweenCount(), 1); // Tween still active
159
+
160
+ // Reverse the tween
161
+ reverseTween(tweens[0]);
162
+ assert.equal(getActiveTweenCount(), 1); // Tween still active
163
+
164
+ // Now it should animate back towards 0
165
+ updateTweens(0.5);
166
+ assert.equal(getActiveTweenCount(), 1); // Should still be active
167
+ assert.equal(target.x, 25); // Halfway from 50 to 0
168
+
169
+ updateTweens(0.5);
170
+ assert.equal(getActiveTweenCount(), 0); // Now complete
171
+ assert.equal(target.x, 0);
172
+ });
173
+
174
+ it("sequence should handle empty array", () => {
175
+ stopAllTweens();
176
+ const result = sequence([]);
177
+ assert.equal(result.length, 0);
178
+ });
179
+
180
+ it("parallel should handle empty array", () => {
181
+ stopAllTweens();
182
+ const result = parallel([]);
183
+ assert.equal(result.length, 0);
184
+ });
185
+
186
+ it("stagger should handle empty array", () => {
187
+ stopAllTweens();
188
+ const result = stagger([], 0.5);
189
+ assert.equal(result.length, 0);
190
+ });
191
+ });
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Tween chaining utilities for composing multiple tweens.
3
+ *
4
+ * - {@link sequence} — run tweens one after another.
5
+ * - {@link parallel} — run tweens simultaneously.
6
+ * - {@link stagger} — run tweens with a staggered delay between each.
7
+ */
8
+
9
+ import { tween, stopTween } from "./tween.ts";
10
+ import type { Tween, TweenOptions, TweenProps } from "./types.ts";
11
+
12
+ /**
13
+ * Configuration for a single tween within a {@link sequence}, {@link parallel}, or {@link stagger} chain.
14
+ */
15
+ export interface TweenConfig {
16
+ /** The object whose properties will be interpolated. */
17
+ target: any;
18
+ /** Map of property names to target (end) values. */
19
+ props: TweenProps;
20
+ /** Animation duration in seconds. */
21
+ duration: number;
22
+ /** Optional tween options (easing, callbacks, etc.). */
23
+ options?: TweenOptions;
24
+ }
25
+
26
+ /**
27
+ * Run tweens one after another in sequence. Each tween starts when the
28
+ * previous one completes. Wraps `onComplete` callbacks to chain automatically.
29
+ *
30
+ * Note: Only the first tween is created immediately. Subsequent tweens are
31
+ * created lazily as each predecessor completes.
32
+ *
33
+ * @param tweens - Array of tween configurations to run in order.
34
+ * @returns Array of created tweens (initially contains only the first; more are added as the sequence progresses).
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * sequence([
39
+ * { target: sprite, props: { x: 100 }, duration: 0.5 },
40
+ * { target: sprite, props: { y: 200 }, duration: 0.3 },
41
+ * ]);
42
+ * ```
43
+ */
44
+ export function sequence(tweens: TweenConfig[]): Tween[] {
45
+ if (tweens.length === 0) return [];
46
+
47
+ const createdTweens: Tween[] = [];
48
+ let currentIndex = 0;
49
+
50
+ function startNext() {
51
+ if (currentIndex >= tweens.length) return;
52
+
53
+ const config = tweens[currentIndex];
54
+ const options = config.options ?? {};
55
+
56
+ // Wrap onComplete to start next tween
57
+ const originalOnComplete = options.onComplete;
58
+ options.onComplete = () => {
59
+ if (originalOnComplete) {
60
+ originalOnComplete();
61
+ }
62
+ currentIndex++;
63
+ startNext();
64
+ };
65
+
66
+ const t = tween(config.target, config.props, config.duration, options);
67
+ createdTweens.push(t);
68
+ }
69
+
70
+ // Start the first tween
71
+ startNext();
72
+
73
+ return createdTweens;
74
+ }
75
+
76
+ /**
77
+ * Run all tweens simultaneously. All tweens start immediately.
78
+ *
79
+ * @param tweens - Array of tween configurations to run in parallel.
80
+ * @returns Array of all created tween instances.
81
+ */
82
+ export function parallel(tweens: TweenConfig[]): Tween[] {
83
+ return tweens.map((config) =>
84
+ tween(config.target, config.props, config.duration, config.options)
85
+ );
86
+ }
87
+
88
+ /**
89
+ * Run tweens with a staggered delay between each start.
90
+ * The i-th tween gets an additional delay of `i * staggerDelay` seconds
91
+ * (added to any delay already specified in its options).
92
+ *
93
+ * @param tweens - Array of tween configurations to stagger.
94
+ * @param staggerDelay - Delay in seconds between each successive tween start. Must be >= 0.
95
+ * @returns Array of all created tween instances.
96
+ */
97
+ export function stagger(tweens: TweenConfig[], staggerDelay: number): Tween[] {
98
+ return tweens.map((config, index) => {
99
+ const options = { ...(config.options ?? {}) };
100
+ options.delay = (options.delay ?? 0) + index * staggerDelay;
101
+ return tween(config.target, config.props, config.duration, options);
102
+ });
103
+ }