@grandgular/rive-angular 0.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.
@@ -0,0 +1,717 @@
1
+ import * as i0 from '@angular/core';
2
+ import { viewChild, inject, DestroyRef, PLATFORM_ID, NgZone, input, output, signal, effect, untracked, ChangeDetectionStrategy, Component, Injectable } from '@angular/core';
3
+ import { isPlatformBrowser } from '@angular/common';
4
+ import { Fit, Alignment, Layout, Rive, RiveFile, EventType } from '@rive-app/canvas';
5
+ export { Alignment, EventType, Fit, Layout, Rive, RiveFile, StateMachineInput } from '@rive-app/canvas';
6
+
7
+ /**
8
+ * Re-export Rive SDK types for consumer convenience
9
+ */
10
+ /**
11
+ * Error thrown when Rive animation fails to load
12
+ */
13
+ class RiveLoadError extends Error {
14
+ originalError;
15
+ constructor(message, originalError) {
16
+ super(message);
17
+ this.originalError = originalError;
18
+ this.name = 'RiveLoadError';
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Fake IntersectionObserver for environments where it's not available (e.g., SSR)
24
+ */
25
+ class FakeIntersectionObserver {
26
+ root = null;
27
+ rootMargin = '';
28
+ thresholds = [];
29
+ observe() {
30
+ // Intentionally empty for SSR compatibility
31
+ }
32
+ unobserve() {
33
+ // Intentionally empty for SSR compatibility
34
+ }
35
+ disconnect() {
36
+ // Intentionally empty for SSR compatibility
37
+ }
38
+ takeRecords() {
39
+ return [];
40
+ }
41
+ }
42
+ const MyIntersectionObserver = (typeof globalThis !== 'undefined' && globalThis.IntersectionObserver) ||
43
+ FakeIntersectionObserver;
44
+ /**
45
+ * Singleton IntersectionObserver wrapper for observing multiple elements
46
+ * with individual callbacks. This avoids creating multiple IntersectionObserver
47
+ * instances which is more efficient.
48
+ */
49
+ class ElementObserver {
50
+ observer;
51
+ elementsMap = new Map();
52
+ constructor() {
53
+ this.observer = new MyIntersectionObserver(this.onObserved);
54
+ }
55
+ onObserved = (entries) => {
56
+ entries.forEach((entry) => {
57
+ const elementCallback = this.elementsMap.get(entry.target);
58
+ if (elementCallback) {
59
+ elementCallback(entry);
60
+ }
61
+ });
62
+ };
63
+ registerCallback(element, callback) {
64
+ this.observer.observe(element);
65
+ this.elementsMap.set(element, callback);
66
+ }
67
+ removeCallback(element) {
68
+ this.observer.unobserve(element);
69
+ this.elementsMap.delete(element);
70
+ }
71
+ }
72
+ // Singleton instance
73
+ let observerInstance = null;
74
+ /**
75
+ * Get the singleton ElementObserver instance
76
+ */
77
+ function getElementObserver() {
78
+ if (!observerInstance) {
79
+ observerInstance = new ElementObserver();
80
+ }
81
+ return observerInstance;
82
+ }
83
+
84
+ /**
85
+ * Standalone Angular component for Rive animations
86
+ *
87
+ * Features:
88
+ * - Signal-based inputs for reactive updates
89
+ * - Automatic canvas sizing via ResizeObserver with DPR support
90
+ * - OnPush change detection strategy
91
+ * - SSR compatible
92
+ * - Zoneless architecture ready
93
+ * - Automatic resource cleanup
94
+ * - Runs outside Angular zone for optimal performance
95
+ *
96
+ * @example
97
+ * ```html
98
+ * <rive-canvas
99
+ * src="assets/animations/rive/animation.riv"
100
+ * [stateMachines]="'StateMachine'"
101
+ * [autoplay]="true"
102
+ * [fit]="Fit.Cover"
103
+ * [alignment]="Alignment.Center"
104
+ * (loaded)="onLoad()"
105
+ * />
106
+ * ```
107
+ */
108
+ class RiveCanvasComponent {
109
+ canvas = viewChild.required('canvas');
110
+ #destroyRef = inject(DestroyRef);
111
+ #platformId = inject(PLATFORM_ID);
112
+ #ngZone = inject(NgZone);
113
+ src = input(...(ngDevMode ? [undefined, { debugName: "src" }] : []));
114
+ buffer = input(...(ngDevMode ? [undefined, { debugName: "buffer" }] : []));
115
+ /**
116
+ * Preloaded RiveFile instance (from RiveFileService).
117
+ * If provided, this takes precedence over src/buffer.
118
+ */
119
+ riveFile = input(...(ngDevMode ? [undefined, { debugName: "riveFile" }] : []));
120
+ artboard = input(...(ngDevMode ? [undefined, { debugName: "artboard" }] : []));
121
+ animations = input(...(ngDevMode ? [undefined, { debugName: "animations" }] : []));
122
+ stateMachines = input(...(ngDevMode ? [undefined, { debugName: "stateMachines" }] : []));
123
+ autoplay = input(true, ...(ngDevMode ? [{ debugName: "autoplay" }] : []));
124
+ fit = input(Fit.Contain, ...(ngDevMode ? [{ debugName: "fit" }] : []));
125
+ alignment = input(Alignment.Center, ...(ngDevMode ? [{ debugName: "alignment" }] : []));
126
+ useOffscreenRenderer = input(false, ...(ngDevMode ? [{ debugName: "useOffscreenRenderer" }] : []));
127
+ /**
128
+ * Enable IntersectionObserver to automatically stop rendering when canvas is not visible.
129
+ * This optimizes performance by pausing animations that are off-screen.
130
+ */
131
+ shouldUseIntersectionObserver = input(true, ...(ngDevMode ? [{ debugName: "shouldUseIntersectionObserver" }] : []));
132
+ /**
133
+ * Disable Rive event listeners on the canvas (pointer events, touch events).
134
+ * Useful for decorative animations without interactivity.
135
+ */
136
+ shouldDisableRiveListeners = input(false, ...(ngDevMode ? [{ debugName: "shouldDisableRiveListeners" }] : []));
137
+ /**
138
+ * Allow Rive to automatically handle Rive Events (e.g., OpenUrlEvent opens URLs).
139
+ * Default is false for security - events must be handled manually via riveEvent output.
140
+ */
141
+ automaticallyHandleEvents = input(false, ...(ngDevMode ? [{ debugName: "automaticallyHandleEvents" }] : []));
142
+ // Outputs (Events)
143
+ loaded = output();
144
+ loadError = output();
145
+ /**
146
+ * Emitted when state machine state changes.
147
+ * Contains information about the state change event.
148
+ */
149
+ stateChange = output();
150
+ /**
151
+ * Emitted for Rive Events (custom events defined in the .riv file).
152
+ * Use this to handle custom events like OpenUrlEvent, etc.
153
+ */
154
+ riveEvent = output();
155
+ /**
156
+ * Emitted when Rive instance is created and ready.
157
+ * Provides direct access to the Rive instance for advanced use cases.
158
+ */
159
+ riveReady = output();
160
+ // Signals for reactive state
161
+ isPlaying = signal(false, ...(ngDevMode ? [{ debugName: "isPlaying" }] : []));
162
+ isPaused = signal(false, ...(ngDevMode ? [{ debugName: "isPaused" }] : []));
163
+ isLoaded = signal(false, ...(ngDevMode ? [{ debugName: "isLoaded" }] : []));
164
+ /**
165
+ * Public signal providing access to the Rive instance.
166
+ * Use this to access advanced Rive SDK features.
167
+ */
168
+ riveInstance = signal(null, ...(ngDevMode ? [{ debugName: "riveInstance" }] : []));
169
+ // Private state
170
+ #rive = null;
171
+ resizeObserver = null;
172
+ isInitialized = false;
173
+ isPausedByIntersectionObserver = false;
174
+ retestIntersectionTimeoutId = null;
175
+ constructor() {
176
+ // Effect to reload animation when src, buffer, or riveFile changes
177
+ effect(() => {
178
+ const src = this.src();
179
+ const buffer = this.buffer();
180
+ const riveFile = this.riveFile();
181
+ untracked(() => {
182
+ if ((src || buffer || riveFile) &&
183
+ isPlatformBrowser(this.#platformId) &&
184
+ this.isInitialized)
185
+ this.loadAnimation();
186
+ });
187
+ });
188
+ // Auto cleanup on destroy
189
+ this.#destroyRef.onDestroy(() => {
190
+ this.cleanupRive();
191
+ this.disconnectResizeObserver();
192
+ this.disconnectIntersectionObserver();
193
+ });
194
+ }
195
+ ngAfterViewInit() {
196
+ if (isPlatformBrowser(this.#platformId)) {
197
+ this.isInitialized = true;
198
+ this.setupResizeObserver();
199
+ this.setupIntersectionObserver();
200
+ this.loadAnimation();
201
+ }
202
+ }
203
+ /**
204
+ * Setup ResizeObserver for automatic canvas sizing with DPR support
205
+ */
206
+ setupResizeObserver() {
207
+ const canvas = this.canvas().nativeElement;
208
+ const dpr = window.devicePixelRatio || 1;
209
+ this.resizeObserver = new ResizeObserver((entries) => {
210
+ for (const entry of entries) {
211
+ const { width, height } = entry.contentRect;
212
+ // Set canvas size with device pixel ratio for sharp rendering
213
+ canvas.width = width * dpr;
214
+ canvas.height = height * dpr;
215
+ // Resize Rive instance if it exists
216
+ if (this.#rive)
217
+ this.#rive.resizeDrawingSurfaceToCanvas();
218
+ }
219
+ });
220
+ this.resizeObserver.observe(canvas);
221
+ }
222
+ /**
223
+ * Disconnect ResizeObserver
224
+ */
225
+ disconnectResizeObserver() {
226
+ if (this.resizeObserver) {
227
+ this.resizeObserver.disconnect();
228
+ this.resizeObserver = null;
229
+ }
230
+ }
231
+ /**
232
+ * Setup IntersectionObserver to stop rendering when canvas is not visible
233
+ */
234
+ setupIntersectionObserver() {
235
+ if (!this.shouldUseIntersectionObserver())
236
+ return;
237
+ const canvas = this.canvas().nativeElement;
238
+ const observer = getElementObserver();
239
+ const onIntersectionChange = (entry) => {
240
+ if (entry.isIntersecting) {
241
+ // Canvas is visible - start rendering
242
+ if (this.#rive) {
243
+ this.#rive.startRendering();
244
+ }
245
+ this.isPausedByIntersectionObserver = false;
246
+ }
247
+ else {
248
+ // Canvas is not visible - stop rendering
249
+ if (this.#rive) {
250
+ this.#rive.stopRendering();
251
+ }
252
+ this.isPausedByIntersectionObserver = true;
253
+ // Workaround for Chrome bug with insertBefore
254
+ // Retest after 10ms if boundingClientRect.width is 0
255
+ if (this.retestIntersectionTimeoutId) {
256
+ clearTimeout(this.retestIntersectionTimeoutId);
257
+ }
258
+ if (entry.boundingClientRect.width === 0) {
259
+ this.retestIntersectionTimeoutId = setTimeout(() => {
260
+ this.retestIntersection();
261
+ }, 10);
262
+ }
263
+ }
264
+ };
265
+ observer.registerCallback(canvas, onIntersectionChange);
266
+ }
267
+ /**
268
+ * Retest intersection - workaround for Chrome bug
269
+ */
270
+ retestIntersection() {
271
+ if (!this.isPausedByIntersectionObserver)
272
+ return;
273
+ const canvas = this.canvas().nativeElement;
274
+ const rect = canvas.getBoundingClientRect();
275
+ const isIntersecting = rect.width > 0 &&
276
+ rect.height > 0 &&
277
+ rect.top <
278
+ (window.innerHeight || document.documentElement.clientHeight) &&
279
+ rect.bottom > 0 &&
280
+ rect.left < (window.innerWidth || document.documentElement.clientWidth) &&
281
+ rect.right > 0;
282
+ if (isIntersecting && this.#rive) {
283
+ this.#rive.startRendering();
284
+ this.isPausedByIntersectionObserver = false;
285
+ }
286
+ }
287
+ /**
288
+ * Disconnect IntersectionObserver
289
+ */
290
+ disconnectIntersectionObserver() {
291
+ if (this.retestIntersectionTimeoutId) {
292
+ clearTimeout(this.retestIntersectionTimeoutId);
293
+ this.retestIntersectionTimeoutId = null;
294
+ }
295
+ if (this.shouldUseIntersectionObserver()) {
296
+ const canvas = this.canvas().nativeElement;
297
+ const observer = getElementObserver();
298
+ observer.removeCallback(canvas);
299
+ }
300
+ }
301
+ /**
302
+ * Load animation from src or buffer
303
+ */
304
+ loadAnimation() {
305
+ // Run outside Angular zone for better performance
306
+ this.#ngZone.runOutsideAngular(() => {
307
+ try {
308
+ // Clean up existing Rive instance only
309
+ this.cleanupRive();
310
+ const canvas = this.canvas().nativeElement;
311
+ const src = this.src();
312
+ const buffer = this.buffer();
313
+ const riveFile = this.riveFile();
314
+ if (!src && !buffer && !riveFile)
315
+ return;
316
+ // Build layout configuration
317
+ const layoutParams = {
318
+ fit: this.fit(),
319
+ alignment: this.alignment(),
320
+ };
321
+ // Create Rive instance configuration
322
+ // Using Record to allow dynamic property assignment
323
+ const config = {
324
+ canvas,
325
+ autoplay: this.autoplay(),
326
+ layout: new Layout(layoutParams),
327
+ useOffscreenRenderer: this.useOffscreenRenderer(),
328
+ shouldDisableRiveListeners: this.shouldDisableRiveListeners(),
329
+ automaticallyHandleEvents: this.automaticallyHandleEvents(),
330
+ onLoad: () => this.onLoad(),
331
+ onLoadError: (error) => this.onLoadError(error),
332
+ onPlay: () => this.onPlay(),
333
+ onPause: () => this.onPause(),
334
+ onStop: () => this.onStop(),
335
+ onStateChange: (event) => this.onStateChange(event),
336
+ onRiveEvent: (event) => this.onRiveEvent(event),
337
+ };
338
+ // Add src, buffer, or riveFile (priority: riveFile > src > buffer)
339
+ if (riveFile) {
340
+ config['riveFile'] = riveFile;
341
+ }
342
+ else if (src) {
343
+ config['src'] = src;
344
+ }
345
+ else if (buffer) {
346
+ config['buffer'] = buffer;
347
+ }
348
+ // Add artboard if specified
349
+ const artboard = this.artboard();
350
+ if (artboard)
351
+ config['artboard'] = artboard;
352
+ // Add animations if specified
353
+ const animations = this.animations();
354
+ if (animations)
355
+ config['animations'] = animations;
356
+ // Add state machines if specified
357
+ const stateMachines = this.stateMachines();
358
+ if (stateMachines)
359
+ config['stateMachines'] = stateMachines;
360
+ // Safe type assertion - config contains all required properties
361
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
362
+ this.#rive = new Rive(config);
363
+ // Update public signal and emit riveReady event
364
+ this.#ngZone.run(() => {
365
+ this.riveInstance.set(this.#rive);
366
+ this.riveReady.emit(this.#rive);
367
+ });
368
+ }
369
+ catch (error) {
370
+ console.error('Failed to initialize Rive instance:', error);
371
+ this.#ngZone.run(() => this.loadError.emit(new RiveLoadError('Failed to load Rive animation', error)));
372
+ }
373
+ });
374
+ }
375
+ // Event handlers (run inside Angular zone for change detection)
376
+ onLoad() {
377
+ this.#ngZone.run(() => {
378
+ this.isLoaded.set(true);
379
+ this.loaded.emit();
380
+ });
381
+ }
382
+ onLoadError(originalError) {
383
+ this.#ngZone.run(() => {
384
+ const error = new RiveLoadError('Failed to load Rive animation', originalError instanceof Error ? originalError : undefined);
385
+ console.error('Rive load error:', error);
386
+ this.loadError.emit(error);
387
+ });
388
+ }
389
+ onPlay() {
390
+ this.#ngZone.run(() => {
391
+ this.isPlaying.set(true);
392
+ this.isPaused.set(false);
393
+ });
394
+ }
395
+ onPause() {
396
+ this.#ngZone.run(() => {
397
+ this.isPlaying.set(false);
398
+ this.isPaused.set(true);
399
+ });
400
+ }
401
+ onStop() {
402
+ this.#ngZone.run(() => {
403
+ this.isPlaying.set(false);
404
+ this.isPaused.set(false);
405
+ });
406
+ }
407
+ onStateChange(event) {
408
+ this.#ngZone.run(() => this.stateChange.emit(event));
409
+ }
410
+ onRiveEvent(event) {
411
+ this.#ngZone.run(() => this.riveEvent.emit(event));
412
+ }
413
+ // Public API methods
414
+ /**
415
+ * Play animation(s)
416
+ */
417
+ playAnimation(animations) {
418
+ if (!this.#rive)
419
+ return;
420
+ this.#ngZone.runOutsideAngular(() => {
421
+ if (animations) {
422
+ this.#rive.play(animations);
423
+ }
424
+ else {
425
+ this.#rive.play();
426
+ }
427
+ });
428
+ }
429
+ /**
430
+ * Pause animation(s)
431
+ */
432
+ pauseAnimation(animations) {
433
+ if (!this.#rive)
434
+ return;
435
+ this.#ngZone.runOutsideAngular(() => {
436
+ if (animations) {
437
+ this.#rive.pause(animations);
438
+ }
439
+ else {
440
+ this.#rive.pause();
441
+ }
442
+ });
443
+ }
444
+ /**
445
+ * Stop animation(s)
446
+ */
447
+ stopAnimation(animations) {
448
+ if (!this.#rive)
449
+ return;
450
+ this.#ngZone.runOutsideAngular(() => {
451
+ if (animations) {
452
+ this.#rive.stop(animations);
453
+ }
454
+ else {
455
+ this.#rive.stop();
456
+ }
457
+ });
458
+ }
459
+ /**
460
+ * Reset the animation to the beginning
461
+ */
462
+ reset() {
463
+ if (!this.#rive)
464
+ return;
465
+ this.#ngZone.runOutsideAngular(() => {
466
+ this.#rive.reset();
467
+ });
468
+ }
469
+ /**
470
+ * Set a state machine input value
471
+ */
472
+ setInput(stateMachineName, inputName, value) {
473
+ if (!this.#rive)
474
+ return;
475
+ this.#ngZone.runOutsideAngular(() => {
476
+ const inputs = this.#rive.stateMachineInputs(stateMachineName);
477
+ const input = inputs.find((i) => i.name === inputName);
478
+ if (input && 'value' in input) {
479
+ input.value = value;
480
+ }
481
+ });
482
+ }
483
+ /**
484
+ * Fire a state machine trigger
485
+ */
486
+ fireTrigger(stateMachineName, triggerName) {
487
+ if (!this.#rive)
488
+ return;
489
+ this.#ngZone.runOutsideAngular(() => {
490
+ const inputs = this.#rive.stateMachineInputs(stateMachineName);
491
+ const input = inputs.find((i) => i.name === triggerName);
492
+ if (input && 'fire' in input && typeof input.fire === 'function') {
493
+ input.fire();
494
+ }
495
+ });
496
+ }
497
+ /**
498
+ * Clean up Rive instance only
499
+ */
500
+ cleanupRive() {
501
+ if (this.#rive) {
502
+ try {
503
+ this.#rive.cleanup();
504
+ }
505
+ catch (error) {
506
+ console.warn('Error during Rive cleanup:', error);
507
+ }
508
+ this.#rive = null;
509
+ }
510
+ // Reset signals
511
+ this.riveInstance.set(null);
512
+ this.isLoaded.set(false);
513
+ this.isPlaying.set(false);
514
+ this.isPaused.set(false);
515
+ }
516
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: RiveCanvasComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
517
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.1.4", type: RiveCanvasComponent, isStandalone: true, selector: "rive-canvas", inputs: { src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: false, transformFunction: null }, buffer: { classPropertyName: "buffer", publicName: "buffer", isSignal: true, isRequired: false, transformFunction: null }, riveFile: { classPropertyName: "riveFile", publicName: "riveFile", isSignal: true, isRequired: false, transformFunction: null }, artboard: { classPropertyName: "artboard", publicName: "artboard", isSignal: true, isRequired: false, transformFunction: null }, animations: { classPropertyName: "animations", publicName: "animations", isSignal: true, isRequired: false, transformFunction: null }, stateMachines: { classPropertyName: "stateMachines", publicName: "stateMachines", isSignal: true, isRequired: false, transformFunction: null }, autoplay: { classPropertyName: "autoplay", publicName: "autoplay", isSignal: true, isRequired: false, transformFunction: null }, fit: { classPropertyName: "fit", publicName: "fit", isSignal: true, isRequired: false, transformFunction: null }, alignment: { classPropertyName: "alignment", publicName: "alignment", isSignal: true, isRequired: false, transformFunction: null }, useOffscreenRenderer: { classPropertyName: "useOffscreenRenderer", publicName: "useOffscreenRenderer", isSignal: true, isRequired: false, transformFunction: null }, shouldUseIntersectionObserver: { classPropertyName: "shouldUseIntersectionObserver", publicName: "shouldUseIntersectionObserver", isSignal: true, isRequired: false, transformFunction: null }, shouldDisableRiveListeners: { classPropertyName: "shouldDisableRiveListeners", publicName: "shouldDisableRiveListeners", isSignal: true, isRequired: false, transformFunction: null }, automaticallyHandleEvents: { classPropertyName: "automaticallyHandleEvents", publicName: "automaticallyHandleEvents", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { loaded: "loaded", loadError: "loadError", stateChange: "stateChange", riveEvent: "riveEvent", riveReady: "riveReady" }, viewQueries: [{ propertyName: "canvas", first: true, predicate: ["canvas"], descendants: true, isSignal: true }], ngImport: i0, template: `
518
+ <canvas #canvas [style.width.%]="100" [style.height.%]="100"></canvas>
519
+ `, isInline: true, styles: [":host{display:block;width:100%;height:100%}canvas{display:block}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
520
+ }
521
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: RiveCanvasComponent, decorators: [{
522
+ type: Component,
523
+ args: [{ selector: 'rive-canvas', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
524
+ <canvas #canvas [style.width.%]="100" [style.height.%]="100"></canvas>
525
+ `, styles: [":host{display:block;width:100%;height:100%}canvas{display:block}\n"] }]
526
+ }], ctorParameters: () => [], propDecorators: { canvas: [{ type: i0.ViewChild, args: ['canvas', { isSignal: true }] }], src: [{ type: i0.Input, args: [{ isSignal: true, alias: "src", required: false }] }], buffer: [{ type: i0.Input, args: [{ isSignal: true, alias: "buffer", required: false }] }], riveFile: [{ type: i0.Input, args: [{ isSignal: true, alias: "riveFile", required: false }] }], artboard: [{ type: i0.Input, args: [{ isSignal: true, alias: "artboard", required: false }] }], animations: [{ type: i0.Input, args: [{ isSignal: true, alias: "animations", required: false }] }], stateMachines: [{ type: i0.Input, args: [{ isSignal: true, alias: "stateMachines", required: false }] }], autoplay: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoplay", required: false }] }], fit: [{ type: i0.Input, args: [{ isSignal: true, alias: "fit", required: false }] }], alignment: [{ type: i0.Input, args: [{ isSignal: true, alias: "alignment", required: false }] }], useOffscreenRenderer: [{ type: i0.Input, args: [{ isSignal: true, alias: "useOffscreenRenderer", required: false }] }], shouldUseIntersectionObserver: [{ type: i0.Input, args: [{ isSignal: true, alias: "shouldUseIntersectionObserver", required: false }] }], shouldDisableRiveListeners: [{ type: i0.Input, args: [{ isSignal: true, alias: "shouldDisableRiveListeners", required: false }] }], automaticallyHandleEvents: [{ type: i0.Input, args: [{ isSignal: true, alias: "automaticallyHandleEvents", required: false }] }], loaded: [{ type: i0.Output, args: ["loaded"] }], loadError: [{ type: i0.Output, args: ["loadError"] }], stateChange: [{ type: i0.Output, args: ["stateChange"] }], riveEvent: [{ type: i0.Output, args: ["riveEvent"] }], riveReady: [{ type: i0.Output, args: ["riveReady"] }] } });
527
+
528
+ /**
529
+ * Service for preloading and caching Rive files.
530
+ *
531
+ * This service allows you to:
532
+ * - Preload .riv files before they're needed
533
+ * - Share the same file across multiple components
534
+ * - Cache files to avoid redundant network requests
535
+ * - Deduplicate concurrent loads of the same file
536
+ *
537
+ * @example
538
+ * ```typescript
539
+ * export class MyComponent {
540
+ * private riveFileService = inject(RiveFileService);
541
+ * private destroyRef = inject(DestroyRef);
542
+ *
543
+ * fileState = this.riveFileService.loadFile({
544
+ * src: 'assets/animation.riv'
545
+ * });
546
+ *
547
+ * constructor() {
548
+ * // Auto-release on component destroy
549
+ * this.destroyRef.onDestroy(() => {
550
+ * this.riveFileService.releaseFile({ src: 'assets/animation.riv' });
551
+ * });
552
+ * }
553
+ * }
554
+ * ```
555
+ */
556
+ class RiveFileService {
557
+ cache = new Map();
558
+ pendingLoads = new Map();
559
+ bufferIdCounter = 0;
560
+ /**
561
+ * Load a RiveFile from URL or ArrayBuffer.
562
+ * Returns a signal with the file state and loading status.
563
+ * Files are cached by src URL to avoid redundant loads.
564
+ * Concurrent loads of the same file are deduplicated.
565
+ *
566
+ * @param params - Parameters containing src URL or buffer
567
+ * @returns Signal with RiveFileState
568
+ */
569
+ loadFile(params) {
570
+ const cacheKey = this.getCacheKey(params);
571
+ // Return cached entry if exists
572
+ const cached = this.cache.get(cacheKey);
573
+ if (cached) {
574
+ cached.refCount++;
575
+ return cached.state;
576
+ }
577
+ // Return pending load if already in progress
578
+ const pending = this.pendingLoads.get(cacheKey);
579
+ if (pending) {
580
+ return pending.stateSignal.asReadonly();
581
+ }
582
+ // Create new loading state
583
+ const stateSignal = signal({
584
+ riveFile: null,
585
+ status: 'loading',
586
+ }, ...(ngDevMode ? [{ debugName: "stateSignal" }] : []));
587
+ // Start loading and track as pending
588
+ const promise = this.loadRiveFile(params, stateSignal, cacheKey);
589
+ this.pendingLoads.set(cacheKey, { stateSignal, promise });
590
+ return stateSignal.asReadonly();
591
+ }
592
+ /**
593
+ * Release a cached file. Decrements reference count and cleans up if no longer used.
594
+ *
595
+ * @param params - Parameters used to load the file
596
+ */
597
+ releaseFile(params) {
598
+ const cacheKey = this.getCacheKey(params);
599
+ const cached = this.cache.get(cacheKey);
600
+ if (cached) {
601
+ cached.refCount--;
602
+ if (cached.refCount <= 0) {
603
+ try {
604
+ cached.file.cleanup();
605
+ }
606
+ catch (error) {
607
+ console.warn('Error cleaning up RiveFile:', error);
608
+ }
609
+ this.cache.delete(cacheKey);
610
+ }
611
+ }
612
+ }
613
+ /**
614
+ * Clear all cached files
615
+ */
616
+ clearCache() {
617
+ this.cache.forEach((entry) => {
618
+ try {
619
+ entry.file.cleanup();
620
+ }
621
+ catch (error) {
622
+ console.warn('Error cleaning up RiveFile:', error);
623
+ }
624
+ });
625
+ this.cache.clear();
626
+ }
627
+ /**
628
+ * Get cache key from params
629
+ */
630
+ getCacheKey(params) {
631
+ if (params.src) {
632
+ return `src:${params.src}`;
633
+ }
634
+ if (params.buffer) {
635
+ // For buffers, generate unique ID to avoid collisions
636
+ // Store the ID on the buffer object itself
637
+ const bufferWithId = params.buffer;
638
+ if (!bufferWithId.__riveBufferId) {
639
+ bufferWithId.__riveBufferId = ++this.bufferIdCounter;
640
+ }
641
+ return `buffer:${bufferWithId.__riveBufferId}`;
642
+ }
643
+ return 'unknown';
644
+ }
645
+ /**
646
+ * Load RiveFile and update state signal
647
+ */
648
+ loadRiveFile(params, stateSignal, cacheKey) {
649
+ return new Promise((resolve) => {
650
+ try {
651
+ const file = new RiveFile(params);
652
+ file.init();
653
+ file.on(EventType.Load, () => {
654
+ // Request an instance to increment reference count
655
+ // This prevents the file from being destroyed while in use
656
+ file.getInstance();
657
+ stateSignal.set({
658
+ riveFile: file,
659
+ status: 'success',
660
+ });
661
+ // Cache the successfully loaded file
662
+ this.cache.set(cacheKey, {
663
+ file,
664
+ state: stateSignal.asReadonly(),
665
+ refCount: 1,
666
+ });
667
+ // Remove from pending loads
668
+ this.pendingLoads.delete(cacheKey);
669
+ resolve();
670
+ });
671
+ file.on(EventType.LoadError, () => {
672
+ stateSignal.set({
673
+ riveFile: null,
674
+ status: 'failed',
675
+ });
676
+ // Remove from pending loads
677
+ this.pendingLoads.delete(cacheKey);
678
+ // Resolve (not reject) — error state is communicated via the signal.
679
+ // Rejecting would cause an unhandled promise rejection since no
680
+ // consumer awaits or catches this promise.
681
+ resolve();
682
+ });
683
+ }
684
+ catch (error) {
685
+ console.error('Failed to load RiveFile:', error);
686
+ stateSignal.set({
687
+ riveFile: null,
688
+ status: 'failed',
689
+ });
690
+ // Remove from pending loads
691
+ this.pendingLoads.delete(cacheKey);
692
+ // Resolve (not reject) — error state is communicated via the signal.
693
+ resolve();
694
+ }
695
+ });
696
+ }
697
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: RiveFileService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
698
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: RiveFileService, providedIn: 'root' });
699
+ }
700
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: RiveFileService, decorators: [{
701
+ type: Injectable,
702
+ args: [{
703
+ providedIn: 'root',
704
+ }]
705
+ }] });
706
+
707
+ /*
708
+ * Public API Surface of @Grandgular/rive-angular
709
+ */
710
+ // Component
711
+
712
+ /**
713
+ * Generated bundle index. Do not edit.
714
+ */
715
+
716
+ export { RiveCanvasComponent, RiveFileService, RiveLoadError };
717
+ //# sourceMappingURL=grandgular-rive-angular.mjs.map