canvasengine 2.0.0-beta.34 → 2.0.0-beta.36

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 (56) hide show
  1. package/dist/{DebugRenderer-BYa_lwD-.js → DebugRenderer-DDfZuvTR.js} +2 -2
  2. package/dist/{DebugRenderer-BYa_lwD-.js.map → DebugRenderer-DDfZuvTR.js.map} +1 -1
  3. package/dist/components/Container.d.ts +4 -0
  4. package/dist/components/Container.d.ts.map +1 -1
  5. package/dist/components/DOMContainer.d.ts +4 -0
  6. package/dist/components/DOMContainer.d.ts.map +1 -1
  7. package/dist/components/DisplayObject.d.ts +4 -0
  8. package/dist/components/DisplayObject.d.ts.map +1 -1
  9. package/dist/components/Mesh.d.ts +4 -0
  10. package/dist/components/Mesh.d.ts.map +1 -1
  11. package/dist/components/Sprite.d.ts +48 -0
  12. package/dist/components/Sprite.d.ts.map +1 -1
  13. package/dist/components/Viewport.d.ts +4 -0
  14. package/dist/components/Viewport.d.ts.map +1 -1
  15. package/dist/components/types/DisplayObject.d.ts +4 -0
  16. package/dist/components/types/DisplayObject.d.ts.map +1 -1
  17. package/dist/directives/Controls.d.ts +102 -0
  18. package/dist/directives/Controls.d.ts.map +1 -0
  19. package/dist/directives/ControlsBase.d.ts +198 -0
  20. package/dist/directives/ControlsBase.d.ts.map +1 -0
  21. package/dist/directives/Flash.d.ts +117 -0
  22. package/dist/directives/Flash.d.ts.map +1 -0
  23. package/dist/directives/GamepadControls.d.ts +223 -0
  24. package/dist/directives/GamepadControls.d.ts.map +1 -0
  25. package/dist/directives/KeyboardControls.d.ts +55 -366
  26. package/dist/directives/KeyboardControls.d.ts.map +1 -1
  27. package/dist/directives/Shake.d.ts +98 -0
  28. package/dist/directives/Shake.d.ts.map +1 -0
  29. package/dist/directives/index.d.ts +11 -1
  30. package/dist/directives/index.d.ts.map +1 -1
  31. package/dist/engine/trigger.d.ts +2 -3
  32. package/dist/engine/trigger.d.ts.map +1 -1
  33. package/dist/engine/utils.d.ts.map +1 -1
  34. package/dist/{index-BLbc2zG5.js → index--faZajmD.js} +4547 -3970
  35. package/dist/index--faZajmD.js.map +1 -0
  36. package/dist/index.d.ts +1 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.global.js +6 -6
  39. package/dist/index.global.js.map +1 -1
  40. package/dist/index.js +70 -57
  41. package/package.json +2 -1
  42. package/src/components/Container.ts +17 -0
  43. package/src/components/DisplayObject.ts +45 -6
  44. package/src/components/Sprite.ts +87 -3
  45. package/src/components/types/DisplayObject.ts +4 -0
  46. package/src/directives/Controls.ts +182 -0
  47. package/src/directives/ControlsBase.ts +266 -0
  48. package/src/directives/Flash.ts +409 -0
  49. package/src/directives/GamepadControls.ts +515 -0
  50. package/src/directives/KeyboardControls.ts +66 -426
  51. package/src/directives/Shake.ts +282 -0
  52. package/src/directives/index.ts +11 -6
  53. package/src/engine/trigger.ts +2 -2
  54. package/src/engine/utils.ts +4 -0
  55. package/src/index.ts +1 -1
  56. package/dist/index-BLbc2zG5.js.map +0 -1
@@ -0,0 +1,266 @@
1
+ import { fps2ms } from "../engine/utils";
2
+
3
+ export interface ControlOptions {
4
+ repeat?: boolean;
5
+ bind: string | string[];
6
+ keyUp?: Function;
7
+ keyDown?: Function;
8
+ delay?: number | {
9
+ duration: number;
10
+ otherControls?: (string)[];
11
+ };
12
+ }
13
+
14
+ export interface Controls {
15
+ [controlName: string]: ControlOptions;
16
+ }
17
+
18
+ export type BoundKey = { actionName: string, options: ControlOptions, parameters?: any };
19
+
20
+ /**
21
+ * Abstract base class for control systems (keyboard, gamepad, etc.)
22
+ *
23
+ * This class provides common functionality shared across all control implementations:
24
+ * - Input binding and management
25
+ * - Control configuration
26
+ * - Input state management
27
+ * - Common methods for querying and triggering controls
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * class MyControls extends ControlsBase {
32
+ * protected setupListeners() {
33
+ * // Setup specific input listeners
34
+ * }
35
+ *
36
+ * protected cleanup() {
37
+ * // Cleanup specific resources
38
+ * }
39
+ *
40
+ * protected preStep() {
41
+ * // Process inputs each frame
42
+ * }
43
+ * }
44
+ * ```
45
+ */
46
+ export abstract class ControlsBase {
47
+ protected boundKeys: {
48
+ [keyName: string]: BoundKey
49
+ } = {}
50
+ protected stop: boolean = false
51
+ protected _controlsOptions: Controls = {}
52
+ protected interval: any
53
+ protected serverFps: number = 60
54
+
55
+ /**
56
+ * Setup input listeners specific to this control implementation
57
+ * Must be implemented by subclasses
58
+ */
59
+ protected abstract setupListeners(): void;
60
+
61
+ /**
62
+ * Cleanup resources specific to this control implementation
63
+ * Must be implemented by subclasses
64
+ */
65
+ protected abstract cleanup(): void;
66
+
67
+ /**
68
+ * Process inputs each step/frame
69
+ * Must be implemented by subclasses
70
+ */
71
+ protected abstract preStep(): void;
72
+
73
+ /**
74
+ * Start the control processing loop
75
+ * Initializes listeners and starts the interval
76
+ */
77
+ start() {
78
+ this.setupListeners();
79
+ this.interval = setInterval(() => {
80
+ this.preStep()
81
+ }, fps2ms(this.serverFps ?? 60))
82
+ }
83
+
84
+ /**
85
+ * Stop the control processing and cleanup resources
86
+ */
87
+ destroy() {
88
+ if (this.interval) {
89
+ clearInterval(this.interval)
90
+ }
91
+ this.cleanup();
92
+ }
93
+
94
+ /**
95
+ * Bind a key/input to a control action
96
+ *
97
+ * @param keys - Single key or array of keys to bind
98
+ * @param actionName - Name of the control action
99
+ * @param options - Control options (repeat, keyDown, keyUp, etc.)
100
+ * @param parameters - Optional parameters to pass to the control callbacks
101
+ */
102
+ protected bindKey(keys: string | string[], actionName: string, options: ControlOptions, parameters?: object) {
103
+ if (!Array.isArray(keys)) keys = [keys]
104
+ const keyOptions = Object.assign({
105
+ repeat: false
106
+ }, options);
107
+ keys.forEach(keyName => {
108
+ this.boundKeys[keyName] = { actionName, options: keyOptions, parameters }
109
+ })
110
+ }
111
+
112
+ /**
113
+ * Apply an input action for a bound key
114
+ * Can be overridden by subclasses for custom behavior
115
+ *
116
+ * @param keyName - Name of the key/input to process
117
+ */
118
+ protected applyInput(keyName: string) {
119
+ const boundKey = this.boundKeys[keyName];
120
+ if (!boundKey) return;
121
+
122
+ const { repeat, keyDown } = boundKey.options;
123
+ // Default implementation - subclasses may override for state tracking
124
+ if (keyDown) {
125
+ let parameters = boundKey.parameters;
126
+ if (typeof parameters === "function") {
127
+ parameters = parameters();
128
+ }
129
+ keyDown(boundKey);
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Get a control by input name
135
+ *
136
+ * @param inputName - Name of the input/key
137
+ * @returns BoundKey if found, undefined otherwise
138
+ * @example
139
+ * ```ts
140
+ * const control = controls.getControl('up');
141
+ * if (control) {
142
+ * console.log(control.actionName); // 'up'
143
+ * }
144
+ * ```
145
+ */
146
+ getControl(inputName: string): BoundKey | undefined {
147
+ return this.boundKeys[inputName]
148
+ }
149
+
150
+ /**
151
+ * Get all bound controls
152
+ *
153
+ * @returns Object mapping input names to BoundKey objects
154
+ * @example
155
+ * ```ts
156
+ * const allControls = controls.getControls();
157
+ * console.log(Object.keys(allControls)); // ['up', 'down', 'left', 'right', ...]
158
+ * ```
159
+ */
160
+ getControls(): { [key: string]: BoundKey } {
161
+ return this.boundKeys
162
+ }
163
+
164
+ /**
165
+ * Apply a control action programmatically
166
+ *
167
+ * Must be implemented by subclasses to provide input-specific behavior
168
+ *
169
+ * @param controlName - Name or identifier of the control
170
+ * @param isDown - Whether the control is pressed down (true) or released (false)
171
+ * @returns Promise that resolves when the control action is complete
172
+ * @example
173
+ * ```ts
174
+ * // Press a control
175
+ * await controls.applyControl('action', true);
176
+ *
177
+ * // Release a control
178
+ * await controls.applyControl('action', false);
179
+ *
180
+ * // Press and release (default)
181
+ * await controls.applyControl('action');
182
+ * ```
183
+ */
184
+ abstract applyControl(controlName: string | number, isDown?: boolean): Promise<void>;
185
+
186
+ /**
187
+ * Stop listening to inputs
188
+ * Input events will be ignored until listenInputs() is called
189
+ *
190
+ * @example
191
+ * ```ts
192
+ * controls.stopInputs();
193
+ * // ... later
194
+ * controls.listenInputs();
195
+ * ```
196
+ */
197
+ stopInputs() {
198
+ this.stop = true
199
+ }
200
+
201
+ /**
202
+ * Resume listening to inputs after stopInputs() was called
203
+ *
204
+ * @example
205
+ * ```ts
206
+ * controls.stopInputs();
207
+ * // ... later
208
+ * controls.listenInputs();
209
+ * ```
210
+ */
211
+ listenInputs() {
212
+ this.stop = false
213
+ }
214
+
215
+ /**
216
+ * Configure controls with input mappings
217
+ *
218
+ * This method sets up the binding between input keys/buttons and control actions.
219
+ * It clears existing bindings and creates new ones based on the provided configuration.
220
+ *
221
+ * @param inputs - Control configuration object
222
+ * @example
223
+ * ```ts
224
+ * controls.setInputs({
225
+ * up: {
226
+ * repeat: true,
227
+ * bind: 'up',
228
+ * keyDown() {
229
+ * console.log('Up pressed');
230
+ * }
231
+ * },
232
+ * action: {
233
+ * bind: ['space', 'enter'],
234
+ * keyDown() {
235
+ * console.log('Action triggered');
236
+ * }
237
+ * }
238
+ * });
239
+ * ```
240
+ */
241
+ setInputs(inputs: Controls) {
242
+ if (!inputs) return
243
+ this.boundKeys = {}
244
+ for (let control in inputs) {
245
+ const option = inputs[control]
246
+ const { bind } = option
247
+ let inputsKey: any = bind
248
+ if (!Array.isArray(inputsKey)) {
249
+ inputsKey = [bind]
250
+ }
251
+ for (let input of inputsKey) {
252
+ this.bindKey(input, control, option)
253
+ }
254
+ }
255
+ this._controlsOptions = inputs
256
+ }
257
+
258
+ /**
259
+ * Get the current controls configuration
260
+ *
261
+ * @returns The controls options object
262
+ */
263
+ get options(): Controls {
264
+ return this._controlsOptions
265
+ }
266
+ }
@@ -0,0 +1,409 @@
1
+ import { Container } from 'pixi.js';
2
+ import { Directive, registerDirective } from '../engine/directive';
3
+ import { Element } from '../engine/reactive';
4
+ import { effect, isSignal } from '@signe/reactive';
5
+ import { on, isTrigger, Trigger } from '../engine/trigger';
6
+ import { useProps } from '../hooks/useProps';
7
+ import { SignalOrPrimitive } from '../components/types';
8
+ import { animatedSignal, AnimatedSignal } from '../engine/animation';
9
+ import { Subscription } from 'rxjs';
10
+
11
+ export type FlashType = 'alpha' | 'tint' | 'both';
12
+
13
+ export type FlashProps = {
14
+ /**
15
+ * Trigger that activates the flash animation
16
+ * When the trigger is activated, the flash animation will start
17
+ */
18
+ trigger?: Trigger<any>;
19
+ /**
20
+ * Type of flash effect: 'alpha' (opacity), 'tint' (color), or 'both'
21
+ * @default 'alpha'
22
+ */
23
+ type?: SignalOrPrimitive<FlashType>;
24
+ /**
25
+ * Duration of the flash animation in milliseconds
26
+ * @default 300
27
+ */
28
+ duration?: SignalOrPrimitive<number>;
29
+ /**
30
+ * Number of flash cycles (flash on/off)
31
+ * @default 1
32
+ */
33
+ cycles?: SignalOrPrimitive<number>;
34
+ /**
35
+ * Alpha value when flashing (0 to 1)
36
+ * Only used when type is 'alpha' or 'both'
37
+ * @default 0.3
38
+ */
39
+ alpha?: SignalOrPrimitive<number>;
40
+ /**
41
+ * Tint color when flashing (hex color value)
42
+ * Only used when type is 'tint' or 'both'
43
+ * @default 0xffffff (white)
44
+ */
45
+ tint?: SignalOrPrimitive<number>;
46
+ /**
47
+ * Original alpha value to restore after flash
48
+ * If not provided, uses the current alpha value
49
+ */
50
+ originalAlpha?: number;
51
+ /**
52
+ * Original tint value to restore after flash
53
+ * If not provided, uses the current tint value
54
+ */
55
+ originalTint?: number;
56
+ /**
57
+ * Callback function called when flash starts
58
+ */
59
+ onStart?: () => void;
60
+ /**
61
+ * Callback function called when flash completes
62
+ */
63
+ onComplete?: () => void;
64
+ }
65
+
66
+ /**
67
+ * Flash directive that animates a display object's alpha and/or tint when a trigger is activated.
68
+ * Creates a flash effect by rapidly changing opacity or color.
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * // Basic usage with trigger
73
+ * const flashTrigger = trigger();
74
+ *
75
+ * onMount(element) {
76
+ * // Element will flash when trigger is activated
77
+ * element.props.flash = { trigger: flashTrigger };
78
+ * }
79
+ *
80
+ * // Trigger the flash
81
+ * flashTrigger.start();
82
+ * ```
83
+ */
84
+ export class Flash extends Directive {
85
+ private elementRef: Element<Container> | null = null;
86
+ private progressSignal: AnimatedSignal<number> | null = null;
87
+ private flashSubscription: any = null;
88
+ private alphaEffect: Subscription | null = null;
89
+ private tintEffect: Subscription | null = null;
90
+ private originalAlpha: number = 1;
91
+ private originalTint: number = 0xffffff;
92
+ private currentFlashConfig: {
93
+ type: FlashType;
94
+ duration: number;
95
+ cycles: number;
96
+ flashAlpha: number;
97
+ flashTint: number;
98
+ } | null = null;
99
+
100
+ /**
101
+ * Initializes the flash directive
102
+ * @param element - The element to attach the flash effect to
103
+ */
104
+ onInit(element: Element<Container>) {
105
+ this.elementRef = element;
106
+ }
107
+
108
+ /**
109
+ * Mounts the flash directive and sets up trigger listener
110
+ * @param element - The element being mounted
111
+ */
112
+ onMount(element: Element<Container>) {
113
+ const instance = element.componentInstance;
114
+ if (!instance) return;
115
+
116
+ const flashProps = this.flashProps;
117
+
118
+ // Check if trigger is provided
119
+ if (!flashProps.trigger || !isTrigger(flashProps.trigger)) {
120
+ return;
121
+ }
122
+
123
+ // Store original values once at mount time
124
+ // Only set if not already stored (to preserve values from first mount)
125
+ if (this.originalAlpha === 1 && !flashProps.originalAlpha) {
126
+ this.originalAlpha = instance.alpha ?? 1;
127
+ } else if (flashProps.originalAlpha !== undefined) {
128
+ this.originalAlpha = flashProps.originalAlpha;
129
+ }
130
+
131
+ const currentTint = (instance as any).tint;
132
+ if (this.originalTint === 0xffffff && !flashProps.originalTint) {
133
+ this.originalTint = (isSignal(currentTint) ? currentTint() : currentTint) ?? 0xffffff;
134
+ } else if (flashProps.originalTint !== undefined) {
135
+ this.originalTint = flashProps.originalTint;
136
+ }
137
+
138
+ // Listen to trigger activation
139
+ this.flashSubscription = on(flashProps.trigger, async (data) => {
140
+ await this.performFlash(data);
141
+ });
142
+ }
143
+
144
+ /**
145
+ * Gets the flash props with default values
146
+ * @returns FlashProps with defaults applied
147
+ */
148
+ get flashProps(): FlashProps {
149
+ const flash = this.elementRef?.props.flash;
150
+ return useProps(flash?.value ?? flash, {
151
+ type: 'alpha',
152
+ duration: 300,
153
+ cycles: 1,
154
+ alpha: 0.3,
155
+ tint: 0xffffff,
156
+ });
157
+ }
158
+
159
+ /**
160
+ * Performs the flash animation using animatedSignal
161
+ * @param data - Optional data passed from the trigger that can override default options
162
+ */
163
+ private async performFlash(data?: any): Promise<void> {
164
+ if (!this.elementRef?.componentInstance) return;
165
+
166
+ const instance = this.elementRef.componentInstance;
167
+ const flashProps = this.flashProps;
168
+
169
+ // Use data from trigger to override defaults if provided
170
+ const type = data?.type ?? (typeof flashProps.type === 'function' ? flashProps.type() : flashProps.type);
171
+ const duration = data?.duration ?? (typeof flashProps.duration === 'function' ? flashProps.duration() : flashProps.duration);
172
+ const cycles = data?.cycles ?? (typeof flashProps.cycles === 'function' ? flashProps.cycles() : flashProps.cycles);
173
+ const flashAlpha = data?.alpha ?? (typeof flashProps.alpha === 'function' ? flashProps.alpha() : flashProps.alpha);
174
+ const flashTint = data?.tint ?? (typeof flashProps.tint === 'function' ? flashProps.tint() : flashProps.tint);
175
+
176
+ // Stop any existing animation first
177
+ if (this.progressSignal) {
178
+ // Stop the animation immediately
179
+ this.progressSignal.set(0, { duration: 0 });
180
+ }
181
+
182
+ // Clean up effects BEFORE restoring values
183
+ // This prevents effects from continuing to update values after we restore
184
+ if (this.alphaEffect) {
185
+ this.alphaEffect.unsubscribe();
186
+ this.alphaEffect = null;
187
+ }
188
+ if (this.tintEffect) {
189
+ this.tintEffect.unsubscribe();
190
+ this.tintEffect = null;
191
+ }
192
+
193
+ // Always restore to original values immediately after stopping effects
194
+ // This ensures that if a new flash starts before the previous one completes,
195
+ // we restore to the true original values, not the intermediate animation values
196
+ instance.alpha = this.originalAlpha;
197
+ const currentTint = (instance as any).tint;
198
+ if (currentTint !== undefined) {
199
+ // Ensure originalTint is a primitive value, not a signal
200
+ const tintValue = typeof this.originalTint === 'number' ? this.originalTint : 0xffffff;
201
+ // Handle both signal and primitive tint
202
+ if (isSignal(currentTint)) {
203
+ currentTint.set(tintValue);
204
+ } else {
205
+ (instance as any).tint = tintValue;
206
+ }
207
+ }
208
+
209
+ // Call onStart callback
210
+ flashProps.onStart?.();
211
+
212
+ // Store current flash configuration for use in effect
213
+ this.currentFlashConfig = {
214
+ type,
215
+ duration,
216
+ cycles,
217
+ flashAlpha,
218
+ flashTint,
219
+ };
220
+
221
+ // Create or recreate progress signal for flash animation
222
+ // Note: We already stopped the previous animation above, so we can reuse the signal
223
+ if (!this.progressSignal) {
224
+ this.progressSignal = animatedSignal(0, {
225
+ duration: duration,
226
+ ease: (t) => t, // Linear ease
227
+ });
228
+ }
229
+ // Reset to 0 immediately without animation to start fresh
230
+ this.progressSignal.set(0, { duration: 0 });
231
+ // Wait a bit to ensure the reset is complete before starting new animation
232
+ await new Promise(resolve => setTimeout(resolve, 0));
233
+
234
+ // Create effect to update alpha based on progress
235
+ if (type === 'alpha' || type === 'both') {
236
+ this.alphaEffect = effect(() => {
237
+ if (!instance || !this.progressSignal || !this.currentFlashConfig) return;
238
+
239
+ const progress = this.progressSignal();
240
+ const config = this.currentFlashConfig;
241
+
242
+ // Calculate flash value based on cycles
243
+ // Each cycle goes from 0 to 1, so we use modulo to repeat
244
+ const cycleProgress = (progress * config.cycles) % 1;
245
+
246
+ // Create flash effect: fade to flashAlpha then back to original
247
+ // For each cycle, we flash twice (on/off)
248
+ const flashPhase = cycleProgress < 0.5
249
+ ? cycleProgress * 2 // Fade to flashAlpha (0 to 1)
250
+ : 1 - ((cycleProgress - 0.5) * 2); // Fade back to original (1 to 0)
251
+
252
+ // Interpolate between original and flash alpha
253
+ const currentAlpha = this.originalAlpha + (config.flashAlpha - this.originalAlpha) * flashPhase;
254
+ instance.alpha = currentAlpha;
255
+ }).subscription;
256
+ }
257
+
258
+ // Create effect to update tint based on progress
259
+ if (type === 'tint' || type === 'both') {
260
+ this.tintEffect = effect(() => {
261
+ if (!instance || !this.progressSignal || !this.currentFlashConfig) return;
262
+
263
+ // Get current tint value - handle both signal and primitive
264
+ const currentTint = (instance as any).tint;
265
+ if (currentTint === undefined) return;
266
+
267
+ // Check if tint is a signal
268
+ const tintIsSignal = isSignal(currentTint);
269
+
270
+ const progress = this.progressSignal();
271
+ const config = this.currentFlashConfig;
272
+
273
+ // Calculate flash value based on cycles
274
+ const cycleProgress = (progress * config.cycles) % 1;
275
+
276
+ // Create flash effect: change to flashTint then back to original
277
+ const flashPhase = cycleProgress < 0.5
278
+ ? cycleProgress * 2 // Change to flashTint (0 to 1)
279
+ : 1 - ((cycleProgress - 0.5) * 2); // Change back to original (1 to 0)
280
+
281
+ // Interpolate between original and flash tint
282
+ // Simple linear interpolation for RGB values
283
+ const r1 = (this.originalTint >> 16) & 0xff;
284
+ const g1 = (this.originalTint >> 8) & 0xff;
285
+ const b1 = this.originalTint & 0xff;
286
+
287
+ const r2 = (config.flashTint >> 16) & 0xff;
288
+ const g2 = (config.flashTint >> 8) & 0xff;
289
+ const b2 = config.flashTint & 0xff;
290
+
291
+ const r = Math.round(r1 + (r2 - r1) * flashPhase);
292
+ const g = Math.round(g1 + (g2 - g1) * flashPhase);
293
+ const b = Math.round(b1 + (b2 - b1) * flashPhase);
294
+
295
+ const newTintValue = (r << 16) | (g << 8) | b;
296
+
297
+ // Handle both signal and primitive tint
298
+ if (tintIsSignal) {
299
+ currentTint.set(newTintValue);
300
+ } else {
301
+ (instance as any).tint = newTintValue;
302
+ }
303
+ }).subscription;
304
+ }
305
+
306
+ // Start animation and wait for completion
307
+ await this.progressSignal.set(1, {
308
+ duration: duration,
309
+ });
310
+
311
+ // Animation completed - clean up and call callbacks
312
+ // Restore original values
313
+ if (instance) {
314
+ instance.alpha = this.originalAlpha;
315
+ const currentTint = (instance as any).tint;
316
+ if (currentTint !== undefined) {
317
+ // Ensure originalTint is a primitive value, not a signal
318
+ const tintValue = typeof this.originalTint === 'number' ? this.originalTint : 0xffffff;
319
+ // Handle both signal and primitive tint
320
+ if (isSignal(currentTint)) {
321
+ currentTint.set(tintValue);
322
+ } else {
323
+ (instance as any).tint = tintValue;
324
+ }
325
+ }
326
+ }
327
+
328
+ // Clean up effects
329
+ if (this.alphaEffect) {
330
+ this.alphaEffect.unsubscribe();
331
+ this.alphaEffect = null;
332
+ }
333
+ if (this.tintEffect) {
334
+ this.tintEffect.unsubscribe();
335
+ this.tintEffect = null;
336
+ }
337
+
338
+ // Clear flash config
339
+ this.currentFlashConfig = null;
340
+
341
+ // Call onComplete callback
342
+ flashProps.onComplete?.();
343
+ }
344
+
345
+ /**
346
+ * Updates the flash directive when props change
347
+ * @param props - Updated props
348
+ */
349
+ onUpdate(props: any) {
350
+ // Re-mount if props change significantly
351
+ if (props.type && props.type === 'reset') {
352
+ this.onDestroy();
353
+ if (this.elementRef) {
354
+ this.onMount(this.elementRef);
355
+ }
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Cleans up the flash directive
361
+ */
362
+ onDestroy() {
363
+ // Stop any running animation by resetting progress
364
+ if (this.progressSignal) {
365
+ this.progressSignal.set(0, { duration: 0 });
366
+ this.progressSignal = null;
367
+ }
368
+
369
+ // Clean up effects
370
+ if (this.alphaEffect) {
371
+ this.alphaEffect.unsubscribe();
372
+ this.alphaEffect = null;
373
+ }
374
+ if (this.tintEffect) {
375
+ this.tintEffect.unsubscribe();
376
+ this.tintEffect = null;
377
+ }
378
+
379
+ // Clear flash config
380
+ this.currentFlashConfig = null;
381
+
382
+ // Restore original values
383
+ if (this.elementRef?.componentInstance) {
384
+ const instance = this.elementRef.componentInstance;
385
+ instance.alpha = this.originalAlpha;
386
+ const currentTint = (instance as any).tint;
387
+ if (currentTint !== undefined) {
388
+ // Ensure originalTint is a primitive value, not a signal
389
+ const tintValue = typeof this.originalTint === 'number' ? this.originalTint : 0xffffff;
390
+ // Handle both signal and primitive tint
391
+ if (isSignal(currentTint)) {
392
+ currentTint.set(tintValue);
393
+ } else {
394
+ (instance as any).tint = tintValue;
395
+ }
396
+ }
397
+ }
398
+
399
+ // Clean up subscription
400
+ if (this.flashSubscription) {
401
+ this.flashSubscription = null;
402
+ }
403
+
404
+ this.elementRef = null;
405
+ }
406
+ }
407
+
408
+ registerDirective('flash', Flash);
409
+