canvasengine 2.0.0-beta.39 → 2.0.0-beta.40

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 (39) hide show
  1. package/dist/{DebugRenderer-Rrw9FlTd.js → DebugRenderer-DgECR3yZ.js} +2 -2
  2. package/dist/{DebugRenderer-Rrw9FlTd.js.map → DebugRenderer-DgECR3yZ.js.map} +1 -1
  3. package/dist/components/Canvas.d.ts.map +1 -1
  4. package/dist/directives/Controls.d.ts +3 -1
  5. package/dist/directives/Controls.d.ts.map +1 -1
  6. package/dist/engine/animation.d.ts +19 -5
  7. package/dist/engine/animation.d.ts.map +1 -1
  8. package/dist/engine/reactive.d.ts +10 -0
  9. package/dist/engine/reactive.d.ts.map +1 -1
  10. package/dist/engine/signal.d.ts.map +1 -1
  11. package/dist/engine/trigger.d.ts.map +1 -1
  12. package/dist/{index-BQ99FClW.js → index-gb763Hyx.js} +4460 -4245
  13. package/dist/index-gb763Hyx.js.map +1 -0
  14. package/dist/index.global.js +6 -6
  15. package/dist/index.global.js.map +1 -1
  16. package/dist/index.js +40 -37
  17. package/package.json +2 -2
  18. package/src/components/Canvas.ts +5 -0
  19. package/src/components/Container.ts +2 -0
  20. package/src/components/DisplayObject.ts +33 -12
  21. package/src/components/Sprite.ts +14 -1
  22. package/src/directives/Controls.ts +45 -7
  23. package/src/engine/animation.ts +137 -19
  24. package/src/engine/reactive.ts +123 -3
  25. package/src/engine/signal.ts +5 -0
  26. package/src/engine/trigger.ts +19 -2
  27. package/dist/components/Container.d.ts +0 -84
  28. package/dist/components/Container.d.ts.map +0 -1
  29. package/dist/components/DOMContainer.d.ts +0 -81
  30. package/dist/components/DOMContainer.d.ts.map +0 -1
  31. package/dist/components/DisplayObject.d.ts +0 -86
  32. package/dist/components/DisplayObject.d.ts.map +0 -1
  33. package/dist/components/Mesh.d.ts +0 -206
  34. package/dist/components/Mesh.d.ts.map +0 -1
  35. package/dist/components/Sprite.d.ts +0 -240
  36. package/dist/components/Sprite.d.ts.map +0 -1
  37. package/dist/components/Viewport.d.ts +0 -110
  38. package/dist/components/Viewport.d.ts.map +0 -1
  39. package/dist/index-BQ99FClW.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { h as a } from "./index-BQ99FClW.js";
2
- import { A as r, Y as o, Z as n, q as l, v as c, r as p, C as S, d as u, W as m, X as g, ak as d, f as b, D as C, aj as h, ah as j, w as T, j as k, G as w, t as D, c as f, I as v, _ as y, J as E, K as O, M as P, U as V, O as x, P as A, ai as B, R as G, z as H, S as M, g as J, e as N, B as R, y as q, L as F, N as I, T as K, x as U, b as z, H as L, Q, V as W, ag as X, af as Y, ad as Z, k as _, a4 as $, a2 as aa, a5 as sa, l as ea, a9 as ta, ae as ia, m as ra, n as oa, $ as na, o as la, i as ca, a0 as pa, p as Sa, aa as ua, a3 as ma, a7 as ga, a6 as da, ac as ba, a1 as Ca, s as ha, a8 as ja, ab as Ta, a as ka, u as wa } from "./index-BQ99FClW.js";
1
+ import { h as a } from "./index-gb763Hyx.js";
2
+ import { A as r, _ as o, $ as n, t as l, x as c, v as p, C as S, d as m, Y as u, Z as d, an as g, f as b, D as C, am as k, ak as h, y as j, j as T, G as w, w as D, c as E, I as f, a0 as v, J as y, K as O, M as P, X as V, O as x, P as A, al as B, R as G, H, S as M, g as F, e as J, L as N, B as R, Q as q, U as z, T as I, z as K, b as U, N as L, W as Q, V as W, aj as X, ai as Y, ag as Z, k as _, a7 as $, a5 as aa, a8 as sa, l as ea, ac as ta, ah as ia, m as ra, n as oa, a1 as na, a4 as la, o as ca, i as pa, a2 as Sa, p as ma, ad as ua, q as da, a6 as ga, aa as ba, a9 as Ca, af as ka, a3 as ha, s as ja, ab as Ta, ae as wa, r as Da, a as Ea, u as fa } from "./index-gb763Hyx.js";
3
3
  const e = a.Howler;
4
4
  export {
5
5
  r as ArraySubject,
@@ -9,23 +9,23 @@ export {
9
9
  c as Circle,
10
10
  p as Container,
11
11
  S as ControlsBase,
12
- u as ControlsDirective,
13
- m as DOMContainer,
14
- g as DOMElement,
15
- d as DisplayObject,
12
+ m as ControlsDirective,
13
+ u as DOMContainer,
14
+ d as DOMElement,
15
+ g as DisplayObject,
16
16
  b as Drag,
17
17
  C as Drop,
18
- h as EVENTS,
19
- j as Easing,
20
- T as Ellipse,
21
- k as Flash,
18
+ k as EVENTS,
19
+ h as Easing,
20
+ j as Ellipse,
21
+ T as Flash,
22
22
  w as GamepadControls,
23
23
  D as Graphics,
24
- f as Howl,
24
+ E as Howl,
25
25
  e as Howler,
26
- v as Input,
27
- y as Joystick,
28
- E as JoystickControls,
26
+ f as Input,
27
+ v as Joystick,
28
+ y as JoystickControls,
29
29
  O as KeyboardControls,
30
30
  P as Mesh,
31
31
  V as NineSliceSprite,
@@ -35,15 +35,15 @@ export {
35
35
  G as Rect,
36
36
  H as Scene,
37
37
  M as Scheduler,
38
- J as Shake,
39
- N as Sound,
40
- R as Sprite,
41
- q as Svg,
42
- F as Text,
43
- I as TilingSprite,
44
- K as Transition,
45
- U as Triangle,
46
- z as Utils,
38
+ F as Shake,
39
+ J as Sound,
40
+ N as Sprite,
41
+ R as Svg,
42
+ q as Text,
43
+ z as TilingSprite,
44
+ I as Transition,
45
+ K as Triangle,
46
+ U as Utils,
47
47
  L as Video,
48
48
  Q as Viewport,
49
49
  W as ViewportFollow,
@@ -60,20 +60,23 @@ export {
60
60
  ra as isArraySubject,
61
61
  oa as isComputed,
62
62
  na as isElement,
63
- la as isObjectSubject,
64
- ca as isObservable,
65
- pa as isPrimitive,
66
- Sa as isSignal,
63
+ la as isElementFrozen,
64
+ ca as isObjectSubject,
65
+ pa as isObservable,
66
+ Sa as isPrimitive,
67
+ ma as isSignal,
67
68
  ua as isTrigger,
68
- ma as loop,
69
- ga as mount,
70
- da as mountTracker,
71
- ba as on,
72
- Ca as registerComponent,
73
- ha as signal,
74
- ja as tick,
75
- Ta as trigger,
76
- ka as useDefineProps,
77
- wa as useProps
69
+ da as linkedSignal,
70
+ ga as loop,
71
+ ba as mount,
72
+ Ca as mountTracker,
73
+ ka as on,
74
+ ha as registerComponent,
75
+ ja as signal,
76
+ Ta as tick,
77
+ wa as trigger,
78
+ Da as untracked,
79
+ Ea as useDefineProps,
80
+ fa as useProps
78
81
  };
79
82
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasengine",
3
- "version": "2.0.0-beta.39",
3
+ "version": "2.0.0-beta.40",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -10,7 +10,7 @@
10
10
  "dependencies": {
11
11
  "@barvynkoa/particle-emitter": "^0.0.1",
12
12
  "@pixi/layout": "^3.0.2",
13
- "@signe/reactive": "^2.3.3",
13
+ "@signe/reactive": "^2.6.0",
14
14
  "howler": "^2.2.4",
15
15
  "joypad.js": "^2.3.5",
16
16
  "pixi-filters": "^6.0.5",
@@ -69,8 +69,13 @@ export const Canvas: ComponentFunction<CanvasProps> = async (props = {}) => {
69
69
  frame: 0,
70
70
  deltaRatio: 1,
71
71
  });
72
+ } else {
73
+ options.context!.tick = props.tick;
72
74
  }
73
75
 
76
+ // Register the tick signal globally so animatedSignal can use it by default
77
+ (globalThis as any).__CANVAS_ENGINE_TICK__ = options.context!.tick;
78
+
74
79
  const canvasElement = createComponent("Canvas", options) as CanvasElement;
75
80
 
76
81
  canvasElement.render = (rootElement: HTMLElement, app?: Application) => {
@@ -59,5 +59,7 @@ export interface CanvasContainer extends DisplayObjectProps {}
59
59
  registerComponent("Container", CanvasContainer);
60
60
 
61
61
  export const Container: ComponentFunction<ContainerProps> = (props) => {
62
+ // Ensure component is registered (useful in tests if module cache differs)
63
+ registerComponent("Container", CanvasContainer);
62
64
  return createComponent("Container", props);
63
65
  };
@@ -1,4 +1,4 @@
1
- import { Element, isElement, Props } from "../engine/reactive";
1
+ import { Element, isElement, Props, isElementFrozen } from "../engine/reactive";
2
2
  import { setObservablePoint } from "../engine/utils";
3
3
  import type {
4
4
  AlignContent,
@@ -120,6 +120,16 @@ export function DisplayObject(extendClass) {
120
120
  #registeredEvents: Map<string, Function> = new Map();
121
121
  // Store computed layout box dimensions
122
122
  #computedLayoutBox: { width?: number; height?: number } | null = null;
123
+ // Store reference to element for freeze checking
124
+ #element: Element<DisplayObject> | null = null;
125
+
126
+ /**
127
+ * Get the element reference for freeze checking
128
+ * @returns The element reference or null
129
+ */
130
+ protected getElement(): Element<DisplayObject> | null {
131
+ return this.#element;
132
+ }
123
133
 
124
134
  get deltaRatio() {
125
135
  return this.#canvasContext?.scheduler?.tick.value.deltaRatio;
@@ -135,15 +145,24 @@ export function DisplayObject(extendClass) {
135
145
  for (let event of EVENTS) {
136
146
  if (props[event] && !this.overrideProps.includes(event)) {
137
147
  this.eventMode = "static";
138
- const eventHandler = props[event];
148
+ const originalEventHandler = props[event];
149
+
150
+ // Wrap event handler to check freeze state
151
+ const wrappedHandler = (...args: any[]) => {
152
+ // Check if element is frozen before executing handler
153
+ if (this.#element && isElementFrozen(this.#element)) {
154
+ return;
155
+ }
156
+ return originalEventHandler(...args);
157
+ };
139
158
 
140
- // Store the event handler for cleanup
159
+ // Store the wrapped event handler for cleanup
141
160
  if (event === 'click') {
142
- this.on('pointertap', eventHandler);
143
- this.#registeredEvents.set('pointertap', eventHandler);
161
+ this.on('pointertap', wrappedHandler);
162
+ this.#registeredEvents.set('pointertap', wrappedHandler);
144
163
  } else {
145
- this.on(event, eventHandler);
146
- this.#registeredEvents.set(event, eventHandler);
164
+ this.on(event, wrappedHandler);
165
+ this.#registeredEvents.set(event, wrappedHandler);
147
166
  }
148
167
  }
149
168
  }
@@ -171,11 +190,12 @@ export function DisplayObject(extendClass) {
171
190
  this.subjectInit.next(this);
172
191
  }
173
192
 
174
- async onMount({ parent, props }: Element<DisplayObject>, index?: number) {
193
+ async onMount(element: Element<DisplayObject>, index?: number) {
175
194
  if (this.destroyed) return
176
- this.#canvasContext = props.context;
177
- if (parent) {
178
- const instance = parent.componentInstance as DisplayObject;
195
+ this.#element = element;
196
+ this.#canvasContext = element.props.context;
197
+ if (element.parent) {
198
+ const instance = element.parent.componentInstance as DisplayObject;
179
199
  if (instance.isFlex && !this.layout && !this.disableLayout) {
180
200
  try {
181
201
  this.layout = {};
@@ -189,7 +209,7 @@ export function DisplayObject(extendClass) {
189
209
  instance.addChildAt(this, index);
190
210
  }
191
211
  this.isMounted = true;
192
- this.onUpdate(props);
212
+ this.onUpdate(element.props);
193
213
 
194
214
  // Listen to layout events to store computed layout dimensions
195
215
  const layoutHandler = (event: any) => {
@@ -316,6 +336,7 @@ export function DisplayObject(extendClass) {
316
336
  this.off(eventName, eventHandler);
317
337
  }
318
338
  this.#registeredEvents.clear();
339
+ this.#element = null;
319
340
 
320
341
  if (this.onBeforeDestroy) {
321
342
  await this.onBeforeDestroy();
@@ -14,6 +14,7 @@ import {
14
14
  createComponent,
15
15
  isElement,
16
16
  registerComponent,
17
+ isElementFrozen,
17
18
  } from "../engine/reactive";
18
19
  import { arrayEquals, isFunction } from "../engine/utils";
19
20
  import { DisplayObject } from "./DisplayObject";
@@ -299,6 +300,10 @@ export class CanvasSprite extends DisplayObject(PixiSprite) {
299
300
  }
300
301
 
301
302
  async onMount(params: Element<CanvasSprite>) {
303
+ // Set #element manually for freeze checking before calling super.onMount
304
+ // We need to set it early so update() can check freeze state
305
+ (this as any)['#element'] = params;
306
+
302
307
  const { props, propObservables } = params;
303
308
  const tick: Signal = props.context.tick;
304
309
  const sheet = props.sheet ?? {};
@@ -421,7 +426,7 @@ export class CanvasSprite extends DisplayObject(PixiSprite) {
421
426
 
422
427
  if (sheet?.params) this.sheetParams = sheet?.params;
423
428
 
424
- if (sheet?.playing && this.isMounted) {
429
+ if (sheet?.playing && this.isMounted && this.spritesheet && this.animations.size > 0) {
425
430
  this.sheetCurrentAnimation = sheet?.playing;
426
431
  this.play(this.sheetCurrentAnimation, [this.sheetParams]);
427
432
  }
@@ -587,6 +592,12 @@ export class CanvasSprite extends DisplayObject(PixiSprite) {
587
592
  }
588
593
 
589
594
  update({ deltaRatio }) {
595
+ // Block animation update if element is frozen
596
+ const element = this.getElement();
597
+ if (element && isElementFrozen(element)) {
598
+ return;
599
+ }
600
+
590
601
  if (
591
602
  !this.isPlaying() ||
592
603
  !this.currentAnimation ||
@@ -752,5 +763,7 @@ export type SpritePropTypes = SpritePropsWithImage | SpritePropsWithSheet;
752
763
 
753
764
  // Update the Sprite function to use the props interface
754
765
  export const Sprite: ComponentFunction<SpritePropTypes> = (props) => {
766
+ // Ensure component is registered in test environments where module cache may differ
767
+ registerComponent("Sprite", CanvasSprite);
755
768
  return createComponent("Sprite", props);
756
769
  };
@@ -1,9 +1,11 @@
1
1
  import { Directive, registerDirective } from "../engine/directive";
2
- import { Element } from "../engine/reactive";
2
+ import { Element, isElementFrozen } from "../engine/reactive";
3
3
  import { ControlsBase, Controls } from "./ControlsBase";
4
4
  import { KeyboardControls } from "./KeyboardControls";
5
5
  import { GamepadControls, GamepadConfig } from "./GamepadControls";
6
6
  import { JoystickControls, JoystickConfig } from "./JoystickControls";
7
+ import { Signal, isSignal } from "@signe/reactive";
8
+ import { Subscription } from "rxjs";
7
9
 
8
10
  /**
9
11
  * Controls directive that coordinates keyboard, gamepad, and joystick input systems
@@ -27,12 +29,15 @@ export class ControlsDirective extends Directive {
27
29
  private keyboardControls: KeyboardControls | null = null;
28
30
  private gamepadControls: GamepadControls | null = null;
29
31
  private joystickControls: JoystickControls | null = null;
32
+ private freezeSubscription: Subscription | null = null;
33
+ private element: Element | null = null;
30
34
 
31
35
  /**
32
36
  * Initialize the controls directive
33
37
  * Sets up keyboard, gamepad, and joystick controls if available
34
38
  */
35
39
  onInit(element: Element) {
40
+ this.element = element;
36
41
  const value = element.props.controls?.value ?? element.props.controls;
37
42
  if (!value) return;
38
43
 
@@ -57,6 +62,23 @@ export class ControlsDirective extends Directive {
57
62
  this.joystickControls.setInputs(value as Controls & { joystick?: JoystickConfig });
58
63
  this.joystickControls.start();
59
64
  }
65
+
66
+ // Check initial freeze state
67
+ if (isElementFrozen(element)) {
68
+ this.stopInputs();
69
+ }
70
+
71
+ // Subscribe to freeze prop if it's a signal
72
+ const freezeProp = element.propObservables?.freeze ?? element.props?.freeze;
73
+ if (isSignal(freezeProp)) {
74
+ this.freezeSubscription = (freezeProp as Signal<boolean>).observable.subscribe((isFrozen) => {
75
+ if (isFrozen) {
76
+ this.stopInputs();
77
+ } else {
78
+ this.listenInputs();
79
+ }
80
+ });
81
+ }
60
82
  }
61
83
 
62
84
  /**
@@ -68,16 +90,25 @@ export class ControlsDirective extends Directive {
68
90
  * Update controls configuration
69
91
  * Updates both keyboard and gamepad controls
70
92
  */
71
- onUpdate(props: any) {
93
+ onUpdate(props: any, element: Element) {
72
94
  const value = props.controls?.value ?? props.controls;
73
- if (!value) return;
95
+ if (value) {
96
+ if (this.keyboardControls) {
97
+ this.keyboardControls.setInputs(value as Controls);
98
+ }
74
99
 
75
- if (this.keyboardControls) {
76
- this.keyboardControls.setInputs(value as Controls);
100
+ if (this.gamepadControls) {
101
+ this.gamepadControls.setInputs(value as Controls & { gamepad?: GamepadConfig });
102
+ }
77
103
  }
78
104
 
79
- if (this.gamepadControls) {
80
- this.gamepadControls.setInputs(value as Controls & { gamepad?: GamepadConfig });
105
+ // Handle freeze prop update
106
+ if (props.freeze !== undefined && this.element) {
107
+ if (isElementFrozen(this.element)) {
108
+ this.stopInputs();
109
+ } else {
110
+ this.listenInputs();
111
+ }
81
112
  }
82
113
  }
83
114
 
@@ -85,6 +116,11 @@ export class ControlsDirective extends Directive {
85
116
  * Cleanup and destroy all control systems
86
117
  */
87
118
  onDestroy(element: Element) {
119
+ if (this.freezeSubscription) {
120
+ this.freezeSubscription.unsubscribe();
121
+ this.freezeSubscription = null;
122
+ }
123
+
88
124
  if (this.keyboardControls) {
89
125
  this.keyboardControls.destroy();
90
126
  this.keyboardControls = null;
@@ -99,6 +135,8 @@ export class ControlsDirective extends Directive {
99
135
  this.joystickControls.destroy();
100
136
  this.joystickControls = null;
101
137
  }
138
+
139
+ this.element = null;
102
140
  }
103
141
 
104
142
  /**
@@ -1,11 +1,24 @@
1
- import { effect, signal, type WritableSignal } from "@signe/reactive";
1
+ import { effect, signal, type WritableSignal, type Signal } from "@signe/reactive";
2
2
  import { animate as animatePopmotion } from "popmotion";
3
+ import { Tick } from "../directives/Scheduler";
4
+ import { Subscription } from "rxjs";
5
+
6
+ /**
7
+ * Gets the global tick signal from the engine context.
8
+ * This is automatically set by the Canvas component when it initializes.
9
+ *
10
+ * @returns The global tick signal if available, undefined otherwise
11
+ */
12
+ function getGlobalTickSignal(): Signal<Tick> | undefined {
13
+ return (globalThis as any).__CANVAS_ENGINE_TICK__;
14
+ }
3
15
 
4
16
  export interface AnimateOptions<T> {
5
17
  duration?: number;
6
18
  ease?: (t: number) => number;
7
19
  onUpdate?: (value: T) => void;
8
20
  onComplete?: () => void;
21
+ tick?: Signal<Tick>;
9
22
  }
10
23
 
11
24
  export interface AnimatedState<T> {
@@ -19,12 +32,62 @@ export interface AnimatedSignal<T> extends Omit<WritableSignal<T>, 'set'> {
19
32
  set: (newValue: T, options?: AnimateOptions<T>) => Promise<void>;
20
33
  animatedState: WritableSignal<AnimatedState<T>>;
21
34
  update: (updater: (value: T) => T) => void;
35
+ pause: () => void;
36
+ resume: () => void;
22
37
  }
23
38
 
24
39
  export function isAnimatedSignal(signal: WritableSignal<any>): boolean {
25
40
  return (signal as unknown as AnimatedSignal<any>).animatedState !== undefined;
26
41
  }
27
42
 
43
+ /**
44
+ * Creates a popmotion driver that uses the engine's tick system.
45
+ * The driver subscribes to the tick signal and calls the update function with deltaTime on each tick.
46
+ *
47
+ * @param tickSignal - The tick signal from the engine context
48
+ * @returns A driver function for popmotion
49
+ * @example
50
+ * ```ts
51
+ * const driver = createTickDriver(context.tick);
52
+ * animate({
53
+ * to: 100,
54
+ * driver: driver
55
+ * });
56
+ * ```
57
+ */
58
+ function createTickDriver(tickSignal: Signal<Tick>) {
59
+ return (update: (delta: number) => void) => {
60
+ let subscription: Subscription | undefined;
61
+ let lastTimestamp: number | null = null;
62
+
63
+ const start = () => {
64
+ if (subscription) return;
65
+
66
+ subscription = (tickSignal.observable as any).subscribe((result: any) => {
67
+ const tick = result?.value ?? result;
68
+ if (!tick) return;
69
+
70
+ if (lastTimestamp === null) {
71
+ lastTimestamp = tick.timestamp;
72
+ return;
73
+ }
74
+
75
+ const delta = tick.deltaTime;
76
+ lastTimestamp = tick.timestamp;
77
+ update(delta);
78
+ });
79
+ };
80
+
81
+ const stop = () => {
82
+ subscription?.unsubscribe();
83
+ subscription = undefined;
84
+ lastTimestamp = null;
85
+ };
86
+
87
+ return { start, stop };
88
+ };
89
+ }
90
+
28
91
  /**
29
92
  * Creates an animated signal with the given initial value and animation options.
30
93
  * It's a writable signal that can be animated using popmotion. Properties of the animated signal are:
@@ -32,16 +95,26 @@ export function isAnimatedSignal(signal: WritableSignal<any>): boolean {
32
95
  * - start: the start value of the animation.
33
96
  * - end: the end value of the animation.
34
97
  *
98
+ * If a tick signal is provided in options, the animation will use the engine's tick system.
99
+ * Otherwise, it will automatically use the global tick signal from the Canvas context if available.
100
+ * If no tick signal is available, it will use requestAnimationFrame by default.
101
+ *
35
102
  * @param initialValue The initial value of the signal.
36
- * @param options The animation options.
103
+ * @param options The animation options. Can include a `tick` signal to use a specific tick system.
37
104
  * @returns The animated signal.
38
105
  * @example
106
+ * ```ts
107
+ * // Automatically uses the Canvas tick system if available, otherwise requestAnimationFrame
39
108
  * const animatedValue = animatedSignal(0, { duration: 1000 });
40
109
  * animatedValue.set(10);
41
- * animatedValue.update((value) => value + 1);
42
- * console.log(animatedValue()); // 11
43
110
  *
44
- * animatedValue.animatedState() // { current: 10, start: 10, end: 11 }
111
+ * // Explicitly using a specific tick signal
112
+ * mount((element) => {
113
+ * const tickSignal = element.props.context.tick;
114
+ * const animatedValue = animatedSignal(0, { duration: 1000, tick: tickSignal });
115
+ * animatedValue.set(10);
116
+ * });
117
+ * ```
45
118
  */
46
119
  export function animatedSignal<T>(initialValue: T, options: AnimateOptions<T> = {}): AnimatedSignal<T> {
47
120
  const state: AnimatedState<T> = {
@@ -49,7 +122,9 @@ export function animatedSignal<T>(initialValue: T, options: AnimateOptions<T> =
49
122
  start: initialValue,
50
123
  end: initialValue,
51
124
  };
52
- let animation
125
+ const DEFAULT_DURATION = 20;
126
+ let animation: { stop: () => void } | null = null;
127
+ let isPaused = false;
53
128
 
54
129
  const publicSignal = signal(initialValue);
55
130
  const privateSignal = signal(state);
@@ -76,24 +151,43 @@ export function animatedSignal<T>(initialValue: T, options: AnimateOptions<T> =
76
151
 
77
152
  privateSignal.set(newState);
78
153
 
154
+ // Stop any running animation
79
155
  if (animation) {
80
156
  animation.stop();
157
+ animation = null;
81
158
  }
82
159
 
160
+ isPaused = false;
161
+ const mergedConfig = { ...options, ...animationConfig };
162
+ const duration = mergedConfig.duration ?? DEFAULT_DURATION;
163
+ const ease = mergedConfig.ease ?? ((t: number) => t);
164
+ const tickSignal = mergedConfig.tick || getGlobalTickSignal();
165
+
166
+ const startValue = prevState.current;
167
+ const endValue = newValue;
168
+
169
+ const onCompleteCb = animationConfig.onComplete ?? options.onComplete;
170
+
171
+
83
172
  animation = animatePopmotion({
84
- // TODO
85
- duration: 20,
86
- ...options,
87
- ...animationConfig,
88
- from: prevState.current,
89
- to: newValue,
90
- onUpdate: (value) => {
91
- privateSignal.update(s => ({ ...s, current: value as T }));
92
- if (options.onUpdate) {
93
- options.onUpdate(value as T);
94
- }
173
+ from: startValue as any,
174
+ to: endValue as any,
175
+ duration,
176
+ ease,
177
+
178
+ onUpdate: (value: any) => {
179
+ if (isPaused) return;
180
+ const nextValue = value as T;
181
+ privateSignal.update(s => ({ ...s, current: nextValue }));
182
+ mergedConfig.onUpdate?.(nextValue);
183
+ animationConfig.onUpdate?.(nextValue);
95
184
  },
185
+ onComplete: () => {
186
+ privateSignal.update(s => ({ ...s, current: endValue }));
187
+ onCompleteCb?.();
188
+ }
96
189
  });
190
+
97
191
  }
98
192
 
99
193
  const fn = function() {
@@ -110,11 +204,35 @@ export function animatedSignal<T>(initialValue: T, options: AnimateOptions<T> =
110
204
  }
111
205
  fn.set = async (newValue: T, animationConfig: AnimateOptions<T> = {}) => {
112
206
  return new Promise<void>((resolve) => {
207
+ const userOnComplete = animationConfig.onComplete;
113
208
  animatedSignal(newValue, {
114
209
  ...animationConfig,
115
- onComplete: resolve
210
+ onComplete: () => {
211
+ if (userOnComplete) {
212
+ userOnComplete();
213
+ } else if (options.onComplete) {
214
+ options.onComplete();
215
+ }
216
+ resolve();
217
+ }
116
218
  });
117
- })
219
+ });
220
+ }
221
+ fn.pause = () => {
222
+ if (isPaused) return;
223
+ isPaused = true;
224
+ if (animation) {
225
+ animation.stop();
226
+ animation = null;
227
+ }
228
+ }
229
+ fn.resume = () => {
230
+ if (!isPaused) return;
231
+ isPaused = false;
232
+ const currentState = privateSignal();
233
+ if (currentState.current !== currentState.end) {
234
+ animatedSignal(currentState.end, options);
235
+ }
118
236
  }
119
237
 
120
238
  return fn as any