@grandgular/rive-angular 0.1.2 → 0.2.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.
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import {
|
|
2
|
+
import { inject, PLATFORM_ID, Injectable, InjectionToken, viewChild, DestroyRef, NgZone, input, output, signal, effect, untracked, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
3
|
import { isPlatformBrowser } from '@angular/common';
|
|
4
4
|
import { Fit, Alignment, Layout, Rive, RiveFile, EventType } from '@rive-app/canvas';
|
|
5
5
|
export { Alignment, EventType, Fit, Layout, Rive, RiveFile, StateMachineInput } from '@rive-app/canvas';
|
|
@@ -8,16 +8,104 @@ export { Alignment, EventType, Fit, Layout, Rive, RiveFile, StateMachineInput }
|
|
|
8
8
|
* Re-export Rive SDK types for consumer convenience
|
|
9
9
|
*/
|
|
10
10
|
/**
|
|
11
|
-
* Error thrown when Rive animation fails to load
|
|
11
|
+
* Error thrown when Rive animation fails to load.
|
|
12
|
+
* Supports legacy constructor for backward compatibility.
|
|
12
13
|
*/
|
|
13
14
|
class RiveLoadError extends Error {
|
|
15
|
+
code;
|
|
16
|
+
suggestion;
|
|
17
|
+
docsUrl;
|
|
14
18
|
originalError;
|
|
15
|
-
constructor(
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
constructor(messageOrOptions, originalError) {
|
|
20
|
+
if (typeof messageOrOptions === 'string') {
|
|
21
|
+
// Legacy constructor: new RiveLoadError(message, originalError)
|
|
22
|
+
super(messageOrOptions);
|
|
23
|
+
this.originalError =
|
|
24
|
+
originalError instanceof Error ? originalError : undefined;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
// New constructor: new RiveLoadError(options)
|
|
28
|
+
super(messageOrOptions.message);
|
|
29
|
+
this.code = messageOrOptions.code;
|
|
30
|
+
this.suggestion = messageOrOptions.suggestion;
|
|
31
|
+
this.docsUrl = messageOrOptions.docsUrl;
|
|
32
|
+
this.originalError =
|
|
33
|
+
messageOrOptions.cause instanceof Error
|
|
34
|
+
? messageOrOptions.cause
|
|
35
|
+
: undefined;
|
|
36
|
+
}
|
|
18
37
|
this.name = 'RiveLoadError';
|
|
19
38
|
}
|
|
20
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Error thrown when validation fails (e.g. missing artboard/animation/input).
|
|
42
|
+
* These errors are typically non-fatal but indicate a configuration mismatch.
|
|
43
|
+
*/
|
|
44
|
+
class RiveValidationError extends Error {
|
|
45
|
+
code;
|
|
46
|
+
availableOptions;
|
|
47
|
+
suggestion;
|
|
48
|
+
constructor(message, code, availableOptions, suggestion) {
|
|
49
|
+
super(message);
|
|
50
|
+
this.code = code;
|
|
51
|
+
this.availableOptions = availableOptions;
|
|
52
|
+
this.suggestion = suggestion;
|
|
53
|
+
this.name = 'RiveValidationError';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Error codes used throughout the Rive Angular library.
|
|
59
|
+
*
|
|
60
|
+
* Ranges:
|
|
61
|
+
* - RIVE_1xx: Load errors (file not found, network, bad format)
|
|
62
|
+
* - RIVE_2xx: Validation errors (artboard, animation, state machine mismatch)
|
|
63
|
+
* - RIVE_3xx: Configuration/Usage errors (missing source, bad canvas)
|
|
64
|
+
*/
|
|
65
|
+
var RiveErrorCode;
|
|
66
|
+
(function (RiveErrorCode) {
|
|
67
|
+
// Load Errors
|
|
68
|
+
RiveErrorCode["FileNotFound"] = "RIVE_101";
|
|
69
|
+
RiveErrorCode["InvalidFormat"] = "RIVE_102";
|
|
70
|
+
RiveErrorCode["NetworkError"] = "RIVE_103";
|
|
71
|
+
// Validation Errors
|
|
72
|
+
RiveErrorCode["ArtboardNotFound"] = "RIVE_201";
|
|
73
|
+
RiveErrorCode["AnimationNotFound"] = "RIVE_202";
|
|
74
|
+
RiveErrorCode["StateMachineNotFound"] = "RIVE_203";
|
|
75
|
+
RiveErrorCode["InputNotFound"] = "RIVE_204";
|
|
76
|
+
// Configuration Errors
|
|
77
|
+
RiveErrorCode["NoSource"] = "RIVE_301";
|
|
78
|
+
RiveErrorCode["InvalidCanvas"] = "RIVE_302";
|
|
79
|
+
})(RiveErrorCode || (RiveErrorCode = {}));
|
|
80
|
+
/**
|
|
81
|
+
* Template messages for each error code.
|
|
82
|
+
* Used by formatErrorMessage to generate user-friendly descriptions.
|
|
83
|
+
*/
|
|
84
|
+
const ERROR_MESSAGES = {
|
|
85
|
+
[RiveErrorCode.FileNotFound]: 'File not found: {url}',
|
|
86
|
+
[RiveErrorCode.InvalidFormat]: 'Invalid .riv file format',
|
|
87
|
+
[RiveErrorCode.NetworkError]: 'Network error while loading file',
|
|
88
|
+
[RiveErrorCode.ArtboardNotFound]: 'Artboard "{name}" not found',
|
|
89
|
+
[RiveErrorCode.AnimationNotFound]: 'Animation "{name}" not found',
|
|
90
|
+
[RiveErrorCode.StateMachineNotFound]: 'State machine "{name}" not found',
|
|
91
|
+
[RiveErrorCode.InputNotFound]: 'Input "{name}" not found in "{stateMachine}"',
|
|
92
|
+
[RiveErrorCode.NoSource]: 'No animation source provided',
|
|
93
|
+
[RiveErrorCode.InvalidCanvas]: 'Invalid canvas element',
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Formats an error message by replacing placeholders with actual values.
|
|
97
|
+
*
|
|
98
|
+
* @param code - The error code
|
|
99
|
+
* @param params - Record of values to replace in the template (e.g. { name: 'MyAnim' })
|
|
100
|
+
* @returns The formatted error string
|
|
101
|
+
*/
|
|
102
|
+
function formatErrorMessage(code, params = {}) {
|
|
103
|
+
let message = ERROR_MESSAGES[code] || 'Unknown Rive error';
|
|
104
|
+
for (const [key, value] of Object.entries(params)) {
|
|
105
|
+
message = message.replace(`{${key}}`, value);
|
|
106
|
+
}
|
|
107
|
+
return message;
|
|
108
|
+
}
|
|
21
109
|
|
|
22
110
|
/**
|
|
23
111
|
* Fake IntersectionObserver for environments where it's not available (e.g., SSR)
|
|
@@ -45,12 +133,21 @@ const MyIntersectionObserver = (typeof globalThis !== 'undefined' && globalThis.
|
|
|
45
133
|
* Singleton IntersectionObserver wrapper for observing multiple elements
|
|
46
134
|
* with individual callbacks. This avoids creating multiple IntersectionObserver
|
|
47
135
|
* instances which is more efficient.
|
|
136
|
+
*
|
|
137
|
+
* Provided as an Angular service for better testability and DI integration.
|
|
48
138
|
*/
|
|
49
139
|
class ElementObserver {
|
|
50
140
|
observer;
|
|
51
141
|
elementsMap = new Map();
|
|
142
|
+
platformId = inject(PLATFORM_ID);
|
|
52
143
|
constructor() {
|
|
53
|
-
|
|
144
|
+
// Only create real observer in browser environment
|
|
145
|
+
if (isPlatformBrowser(this.platformId)) {
|
|
146
|
+
this.observer = new MyIntersectionObserver(this.onObserved);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
this.observer = new FakeIntersectionObserver();
|
|
150
|
+
}
|
|
54
151
|
}
|
|
55
152
|
onObserved = (entries) => {
|
|
56
153
|
entries.forEach((entry) => {
|
|
@@ -68,17 +165,238 @@ class ElementObserver {
|
|
|
68
165
|
this.observer.unobserve(element);
|
|
69
166
|
this.elementsMap.delete(element);
|
|
70
167
|
}
|
|
168
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: ElementObserver, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
169
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: ElementObserver, providedIn: 'root' });
|
|
71
170
|
}
|
|
72
|
-
|
|
73
|
-
|
|
171
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: ElementObserver, decorators: [{
|
|
172
|
+
type: Injectable,
|
|
173
|
+
args: [{
|
|
174
|
+
providedIn: 'root',
|
|
175
|
+
}]
|
|
176
|
+
}], ctorParameters: () => [] });
|
|
177
|
+
// Legacy function for backward compatibility
|
|
178
|
+
// New code should inject ElementObserver directly
|
|
179
|
+
let legacyObserverInstance = null;
|
|
74
180
|
/**
|
|
181
|
+
* @deprecated Use dependency injection instead: `inject(ElementObserver)`
|
|
75
182
|
* Get the singleton ElementObserver instance
|
|
76
183
|
*/
|
|
77
184
|
function getElementObserver() {
|
|
78
|
-
if (!
|
|
79
|
-
|
|
185
|
+
if (!legacyObserverInstance) {
|
|
186
|
+
legacyObserverInstance = new ElementObserver();
|
|
187
|
+
}
|
|
188
|
+
return legacyObserverInstance;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Injection token for global Rive debug configuration.
|
|
193
|
+
* Can be provided via provideRiveDebug().
|
|
194
|
+
*/
|
|
195
|
+
const RIVE_DEBUG_CONFIG = new InjectionToken('RIVE_DEBUG_CONFIG');
|
|
196
|
+
/**
|
|
197
|
+
* Provides global configuration for Rive debugging.
|
|
198
|
+
* Use this in your app.config.ts or module providers.
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* providers: [
|
|
202
|
+
* provideRiveDebug({ logLevel: 'debug' })
|
|
203
|
+
* ]
|
|
204
|
+
*/
|
|
205
|
+
function provideRiveDebug(config) {
|
|
206
|
+
return {
|
|
207
|
+
provide: RIVE_DEBUG_CONFIG,
|
|
208
|
+
useValue: config,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Internal logger for Rive Angular library.
|
|
214
|
+
* Handles log levels and formatting.
|
|
215
|
+
* Not exported publicly.
|
|
216
|
+
*/
|
|
217
|
+
class RiveLogger {
|
|
218
|
+
level;
|
|
219
|
+
constructor(globalConfig, localDebug) {
|
|
220
|
+
this.level = this.resolveLogLevel(globalConfig, localDebug);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Resolve effective log level based on precedence rules:
|
|
224
|
+
* 1. Local debug=true -> 'debug'
|
|
225
|
+
* 2. Local debug=false/undefined -> Use global config
|
|
226
|
+
* 3. No config -> 'error' (default)
|
|
227
|
+
*/
|
|
228
|
+
resolveLogLevel(globalConfig, localDebug) {
|
|
229
|
+
if (localDebug === true) {
|
|
230
|
+
return 'debug';
|
|
231
|
+
}
|
|
232
|
+
if (globalConfig?.logLevel) {
|
|
233
|
+
return globalConfig.logLevel;
|
|
234
|
+
}
|
|
235
|
+
return 'error';
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Update log level dynamically (e.g. when input changes)
|
|
239
|
+
*/
|
|
240
|
+
update(globalConfig, localDebug) {
|
|
241
|
+
this.level = this.resolveLogLevel(globalConfig, localDebug);
|
|
242
|
+
}
|
|
243
|
+
debug(message, ...args) {
|
|
244
|
+
if (this.shouldLog('debug')) {
|
|
245
|
+
console.debug(`[Rive] ${message}`, ...args);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
info(message, ...args) {
|
|
249
|
+
if (this.shouldLog('info')) {
|
|
250
|
+
console.info(`[Rive] ${message}`, ...args);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
warn(message, ...args) {
|
|
254
|
+
if (this.shouldLog('warn')) {
|
|
255
|
+
console.warn(`[Rive] ${message}`, ...args);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
error(message, ...args) {
|
|
259
|
+
if (this.shouldLog('error')) {
|
|
260
|
+
console.error(`[Rive] ${message}`, ...args);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
shouldLog(level) {
|
|
264
|
+
const levels = ['none', 'error', 'warn', 'info', 'debug'];
|
|
265
|
+
const currentIdx = levels.indexOf(this.level);
|
|
266
|
+
const targetIdx = levels.indexOf(level);
|
|
267
|
+
return currentIdx >= targetIdx;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Validates requested artboard name against available artboards.
|
|
273
|
+
* Returns error if not found.
|
|
274
|
+
*/
|
|
275
|
+
function validateArtboard(rive, requestedName) {
|
|
276
|
+
if (!requestedName)
|
|
277
|
+
return null;
|
|
278
|
+
try {
|
|
279
|
+
// Safe check: ensure artboardNames exist on runtime
|
|
280
|
+
// Note: These properties exist at runtime but may not be in type definitions
|
|
281
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
282
|
+
const available = rive.artboardNames;
|
|
283
|
+
if (!available || !available.includes(requestedName)) {
|
|
284
|
+
return new RiveValidationError(formatErrorMessage(RiveErrorCode.ArtboardNotFound, {
|
|
285
|
+
name: requestedName,
|
|
286
|
+
}), RiveErrorCode.ArtboardNotFound, available || [], available && available.length > 0
|
|
287
|
+
? `Available artboards: ${available.join(', ')}`
|
|
288
|
+
: 'No artboards found in file');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
// Graceful fallback if runtime metadata is not accessible
|
|
293
|
+
// Return null silently to avoid breaking validation flow
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Validates requested animation names against available animations.
|
|
299
|
+
* Returns first error found.
|
|
300
|
+
*/
|
|
301
|
+
function validateAnimations(rive, requestedNames) {
|
|
302
|
+
if (!requestedNames)
|
|
303
|
+
return null;
|
|
304
|
+
const names = Array.isArray(requestedNames)
|
|
305
|
+
? requestedNames
|
|
306
|
+
: [requestedNames];
|
|
307
|
+
try {
|
|
308
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
309
|
+
const available = rive.animationNames;
|
|
310
|
+
for (const name of names) {
|
|
311
|
+
if (!available || !available.includes(name)) {
|
|
312
|
+
return new RiveValidationError(formatErrorMessage(RiveErrorCode.AnimationNotFound, { name }), RiveErrorCode.AnimationNotFound, available || [], available && available.length > 0
|
|
313
|
+
? `Available animations: ${available.join(', ')}`
|
|
314
|
+
: 'No animations found in file');
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
// Graceful fallback if runtime metadata is not accessible
|
|
320
|
+
// Return null silently to avoid breaking validation flow
|
|
321
|
+
}
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Validates requested state machine names against available state machines.
|
|
326
|
+
* Returns first error found.
|
|
327
|
+
*/
|
|
328
|
+
function validateStateMachines(rive, requestedNames) {
|
|
329
|
+
if (!requestedNames)
|
|
330
|
+
return null;
|
|
331
|
+
const names = Array.isArray(requestedNames)
|
|
332
|
+
? requestedNames
|
|
333
|
+
: [requestedNames];
|
|
334
|
+
try {
|
|
335
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
336
|
+
const available = rive.stateMachineNames;
|
|
337
|
+
for (const name of names) {
|
|
338
|
+
if (!available || !available.includes(name)) {
|
|
339
|
+
return new RiveValidationError(formatErrorMessage(RiveErrorCode.StateMachineNotFound, { name }), RiveErrorCode.StateMachineNotFound, available || [], available && available.length > 0
|
|
340
|
+
? `Available state machines: ${available.join(', ')}`
|
|
341
|
+
: 'No state machines found in file');
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
// Graceful fallback if runtime metadata is not accessible
|
|
347
|
+
// Return null silently to avoid breaking validation flow
|
|
348
|
+
}
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Validates if an input exists within a specific state machine.
|
|
353
|
+
*/
|
|
354
|
+
function validateInput(rive, stateMachineName, inputName) {
|
|
355
|
+
try {
|
|
356
|
+
const inputs = rive.stateMachineInputs(stateMachineName);
|
|
357
|
+
if (!inputs)
|
|
358
|
+
return null; // Should not happen if SM exists
|
|
359
|
+
const found = inputs.find((i) => i.name === inputName);
|
|
360
|
+
if (!found) {
|
|
361
|
+
const available = inputs.map((i) => i.name);
|
|
362
|
+
return new RiveValidationError(formatErrorMessage(RiveErrorCode.InputNotFound, {
|
|
363
|
+
name: inputName,
|
|
364
|
+
stateMachine: stateMachineName,
|
|
365
|
+
}), RiveErrorCode.InputNotFound, available, available.length > 0
|
|
366
|
+
? `Available inputs in "${stateMachineName}": ${available.join(', ')}`
|
|
367
|
+
: `No inputs found in state machine "${stateMachineName}"`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
// Graceful fallback if runtime metadata is not accessible
|
|
372
|
+
// Return null silently to avoid breaking validation flow
|
|
80
373
|
}
|
|
81
|
-
return
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Runs full configuration validation.
|
|
378
|
+
* Logs warnings and returns array of errors.
|
|
379
|
+
*/
|
|
380
|
+
function validateConfiguration(rive, config, logger) {
|
|
381
|
+
const errors = [];
|
|
382
|
+
const artboardError = validateArtboard(rive, config.artboard);
|
|
383
|
+
if (artboardError)
|
|
384
|
+
errors.push(artboardError);
|
|
385
|
+
const animError = validateAnimations(rive, config.animations);
|
|
386
|
+
if (animError)
|
|
387
|
+
errors.push(animError);
|
|
388
|
+
const smError = validateStateMachines(rive, config.stateMachines);
|
|
389
|
+
if (smError)
|
|
390
|
+
errors.push(smError);
|
|
391
|
+
if (errors.length > 0) {
|
|
392
|
+
logger.warn(`Validation failed with ${errors.length} errors:`);
|
|
393
|
+
errors.forEach((err) => {
|
|
394
|
+
logger.warn(`- ${err.message}`);
|
|
395
|
+
if (err.suggestion)
|
|
396
|
+
logger.warn(` Suggestion: ${err.suggestion}`);
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
return errors;
|
|
82
400
|
}
|
|
83
401
|
|
|
84
402
|
/**
|
|
@@ -110,6 +428,8 @@ class RiveCanvasComponent {
|
|
|
110
428
|
#destroyRef = inject(DestroyRef);
|
|
111
429
|
#platformId = inject(PLATFORM_ID);
|
|
112
430
|
#ngZone = inject(NgZone);
|
|
431
|
+
#globalDebugConfig = inject(RIVE_DEBUG_CONFIG, { optional: true });
|
|
432
|
+
#elementObserver = inject(ElementObserver);
|
|
113
433
|
src = input(...(ngDevMode ? [undefined, { debugName: "src" }] : []));
|
|
114
434
|
buffer = input(...(ngDevMode ? [undefined, { debugName: "buffer" }] : []));
|
|
115
435
|
/**
|
|
@@ -139,6 +459,13 @@ class RiveCanvasComponent {
|
|
|
139
459
|
* Default is false for security - events must be handled manually via riveEvent output.
|
|
140
460
|
*/
|
|
141
461
|
automaticallyHandleEvents = input(false, ...(ngDevMode ? [{ debugName: "automaticallyHandleEvents" }] : []));
|
|
462
|
+
/**
|
|
463
|
+
* Enable debug mode for this specific instance.
|
|
464
|
+
* Overrides global configuration if set.
|
|
465
|
+
* - true: 'debug' level
|
|
466
|
+
* - false/undefined: use global level
|
|
467
|
+
*/
|
|
468
|
+
debugMode = input(...(ngDevMode ? [undefined, { debugName: "debugMode" }] : []));
|
|
142
469
|
// Outputs (Events)
|
|
143
470
|
loaded = output();
|
|
144
471
|
loadError = output();
|
|
@@ -153,21 +480,28 @@ class RiveCanvasComponent {
|
|
|
153
480
|
*/
|
|
154
481
|
riveEvent = output();
|
|
155
482
|
/**
|
|
156
|
-
* Emitted when Rive instance is
|
|
483
|
+
* Emitted when Rive instance is fully loaded and ready.
|
|
157
484
|
* Provides direct access to the Rive instance for advanced use cases.
|
|
485
|
+
* Note: This fires AFTER the animation is loaded, not just instantiated.
|
|
158
486
|
*/
|
|
159
487
|
riveReady = output();
|
|
160
|
-
//
|
|
161
|
-
isPlaying = signal(false, ...(ngDevMode ? [{ debugName: "isPlaying" }] : []));
|
|
162
|
-
isPaused = signal(false, ...(ngDevMode ? [{ debugName: "isPaused" }] : []));
|
|
163
|
-
isLoaded = signal(false, ...(ngDevMode ? [{ debugName: "isLoaded" }] : []));
|
|
488
|
+
// Private writable signals
|
|
489
|
+
#isPlaying = signal(false, ...(ngDevMode ? [{ debugName: "#isPlaying" }] : []));
|
|
490
|
+
#isPaused = signal(false, ...(ngDevMode ? [{ debugName: "#isPaused" }] : []));
|
|
491
|
+
#isLoaded = signal(false, ...(ngDevMode ? [{ debugName: "#isLoaded" }] : []));
|
|
492
|
+
#riveInstance = signal(null, ...(ngDevMode ? [{ debugName: "#riveInstance" }] : []));
|
|
493
|
+
// Public readonly signals
|
|
494
|
+
isPlaying = this.#isPlaying.asReadonly();
|
|
495
|
+
isPaused = this.#isPaused.asReadonly();
|
|
496
|
+
isLoaded = this.#isLoaded.asReadonly();
|
|
164
497
|
/**
|
|
165
498
|
* Public signal providing access to the Rive instance.
|
|
166
499
|
* Use this to access advanced Rive SDK features.
|
|
167
500
|
*/
|
|
168
|
-
riveInstance =
|
|
501
|
+
riveInstance = this.#riveInstance.asReadonly();
|
|
169
502
|
// Private state
|
|
170
503
|
#rive = null;
|
|
504
|
+
logger;
|
|
171
505
|
resizeObserver = null;
|
|
172
506
|
isInitialized = false;
|
|
173
507
|
isPausedByIntersectionObserver = false;
|
|
@@ -176,11 +510,20 @@ class RiveCanvasComponent {
|
|
|
176
510
|
lastWidth = 0;
|
|
177
511
|
lastHeight = 0;
|
|
178
512
|
constructor() {
|
|
179
|
-
|
|
513
|
+
this.logger = new RiveLogger(this.#globalDebugConfig, this.debugMode());
|
|
514
|
+
// Effect to update logger level when debugMode changes
|
|
515
|
+
effect(() => {
|
|
516
|
+
this.logger.update(this.#globalDebugConfig, this.debugMode());
|
|
517
|
+
});
|
|
518
|
+
// Effect to reload animation when src, buffer, riveFile, or configuration changes
|
|
180
519
|
effect(() => {
|
|
181
520
|
const src = this.src();
|
|
182
521
|
const buffer = this.buffer();
|
|
183
522
|
const riveFile = this.riveFile();
|
|
523
|
+
// Track configuration changes to trigger reload
|
|
524
|
+
this.artboard();
|
|
525
|
+
this.animations();
|
|
526
|
+
this.stateMachines();
|
|
184
527
|
untracked(() => {
|
|
185
528
|
if ((src || buffer || riveFile) &&
|
|
186
529
|
isPlatformBrowser(this.#platformId) &&
|
|
@@ -188,6 +531,17 @@ class RiveCanvasComponent {
|
|
|
188
531
|
this.loadAnimation();
|
|
189
532
|
});
|
|
190
533
|
});
|
|
534
|
+
// Effect to update layout when fit or alignment changes
|
|
535
|
+
effect(() => {
|
|
536
|
+
const fit = this.fit();
|
|
537
|
+
const alignment = this.alignment();
|
|
538
|
+
untracked(() => {
|
|
539
|
+
if (this.#rive && isPlatformBrowser(this.#platformId)) {
|
|
540
|
+
const layoutParams = { fit, alignment };
|
|
541
|
+
this.#rive.layout = new Layout(layoutParams);
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
});
|
|
191
545
|
// Auto cleanup on destroy
|
|
192
546
|
this.#destroyRef.onDestroy(() => {
|
|
193
547
|
this.cleanupRive();
|
|
@@ -208,7 +562,6 @@ class RiveCanvasComponent {
|
|
|
208
562
|
*/
|
|
209
563
|
setupResizeObserver() {
|
|
210
564
|
const canvas = this.canvas().nativeElement;
|
|
211
|
-
const dpr = window.devicePixelRatio || 1;
|
|
212
565
|
this.resizeObserver = new ResizeObserver((entries) => {
|
|
213
566
|
// Cancel any pending resize frame
|
|
214
567
|
if (this.resizeRafId) {
|
|
@@ -224,6 +577,8 @@ class RiveCanvasComponent {
|
|
|
224
577
|
this.lastHeight = height;
|
|
225
578
|
// Defer resize to next animation frame to prevent excessive updates in Safari
|
|
226
579
|
this.resizeRafId = requestAnimationFrame(() => {
|
|
580
|
+
// Read current DPR to support monitor changes and zoom
|
|
581
|
+
const dpr = window.devicePixelRatio || 1;
|
|
227
582
|
// Set canvas size with device pixel ratio for sharp rendering
|
|
228
583
|
canvas.width = width * dpr;
|
|
229
584
|
canvas.height = height * dpr;
|
|
@@ -255,7 +610,6 @@ class RiveCanvasComponent {
|
|
|
255
610
|
if (!this.shouldUseIntersectionObserver())
|
|
256
611
|
return;
|
|
257
612
|
const canvas = this.canvas().nativeElement;
|
|
258
|
-
const observer = getElementObserver();
|
|
259
613
|
const onIntersectionChange = (entry) => {
|
|
260
614
|
if (entry.isIntersecting) {
|
|
261
615
|
// Canvas is visible - start rendering
|
|
@@ -282,7 +636,7 @@ class RiveCanvasComponent {
|
|
|
282
636
|
}
|
|
283
637
|
}
|
|
284
638
|
};
|
|
285
|
-
|
|
639
|
+
this.#elementObserver.registerCallback(canvas, onIntersectionChange);
|
|
286
640
|
}
|
|
287
641
|
/**
|
|
288
642
|
* Retest intersection - workaround for Chrome bug
|
|
@@ -314,8 +668,7 @@ class RiveCanvasComponent {
|
|
|
314
668
|
}
|
|
315
669
|
if (this.shouldUseIntersectionObserver()) {
|
|
316
670
|
const canvas = this.canvas().nativeElement;
|
|
317
|
-
|
|
318
|
-
observer.removeCallback(canvas);
|
|
671
|
+
this.#elementObserver.removeCallback(canvas);
|
|
319
672
|
}
|
|
320
673
|
}
|
|
321
674
|
/**
|
|
@@ -331,16 +684,27 @@ class RiveCanvasComponent {
|
|
|
331
684
|
const src = this.src();
|
|
332
685
|
const buffer = this.buffer();
|
|
333
686
|
const riveFile = this.riveFile();
|
|
334
|
-
if (!src && !buffer && !riveFile)
|
|
687
|
+
if (!src && !buffer && !riveFile) {
|
|
688
|
+
this.logger.warn('No animation source provided (src, buffer, or riveFile)');
|
|
689
|
+
this.#ngZone.run(() => this.loadError.emit(new RiveLoadError({
|
|
690
|
+
message: 'No animation source provided',
|
|
691
|
+
code: RiveErrorCode.NoSource,
|
|
692
|
+
})));
|
|
335
693
|
return;
|
|
694
|
+
}
|
|
695
|
+
this.logger.info(`Loading animation`, {
|
|
696
|
+
src: src || (buffer ? 'buffer' : 'riveFile'),
|
|
697
|
+
canvasWidth: canvas.width,
|
|
698
|
+
canvasHeight: canvas.height,
|
|
699
|
+
dpr: window.devicePixelRatio,
|
|
700
|
+
});
|
|
336
701
|
// Build layout configuration
|
|
337
702
|
const layoutParams = {
|
|
338
703
|
fit: this.fit(),
|
|
339
704
|
alignment: this.alignment(),
|
|
340
705
|
};
|
|
341
|
-
//
|
|
342
|
-
|
|
343
|
-
const config = {
|
|
706
|
+
// Build typed Rive configuration
|
|
707
|
+
const baseConfig = {
|
|
344
708
|
canvas,
|
|
345
709
|
autoplay: this.autoplay(),
|
|
346
710
|
layout: new Layout(layoutParams),
|
|
@@ -355,73 +719,114 @@ class RiveCanvasComponent {
|
|
|
355
719
|
onStateChange: (event) => this.onStateChange(event),
|
|
356
720
|
onRiveEvent: (event) => this.onRiveEvent(event),
|
|
357
721
|
};
|
|
358
|
-
// Add
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
// Add state machines if specified
|
|
377
|
-
const stateMachines = this.stateMachines();
|
|
378
|
-
if (stateMachines)
|
|
379
|
-
config['stateMachines'] = stateMachines;
|
|
380
|
-
// Safe type assertion - config contains all required properties
|
|
381
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
722
|
+
// Add source (priority: riveFile > src > buffer)
|
|
723
|
+
const sourceConfig = riveFile
|
|
724
|
+
? { riveFile }
|
|
725
|
+
: src
|
|
726
|
+
? { src }
|
|
727
|
+
: buffer
|
|
728
|
+
? { buffer }
|
|
729
|
+
: {};
|
|
730
|
+
// Add optional configuration
|
|
731
|
+
const optionalConfig = {
|
|
732
|
+
...(this.artboard() ? { artboard: this.artboard() } : {}),
|
|
733
|
+
...(this.animations() ? { animations: this.animations() } : {}),
|
|
734
|
+
...(this.stateMachines()
|
|
735
|
+
? { stateMachines: this.stateMachines() }
|
|
736
|
+
: {}),
|
|
737
|
+
};
|
|
738
|
+
// Combine all configurations
|
|
739
|
+
const config = { ...baseConfig, ...sourceConfig, ...optionalConfig };
|
|
382
740
|
this.#rive = new Rive(config);
|
|
383
|
-
// Update public signal
|
|
741
|
+
// Update public signal (riveReady will be emitted in onLoad)
|
|
384
742
|
this.#ngZone.run(() => {
|
|
385
|
-
this
|
|
386
|
-
this.riveReady.emit(this.#rive);
|
|
743
|
+
this.#riveInstance.set(this.#rive);
|
|
387
744
|
});
|
|
388
745
|
}
|
|
389
746
|
catch (error) {
|
|
390
|
-
|
|
391
|
-
this.#ngZone.run(() => this.loadError.emit(new RiveLoadError(
|
|
747
|
+
this.logger.error('Failed to initialize Rive instance:', error);
|
|
748
|
+
this.#ngZone.run(() => this.loadError.emit(new RiveLoadError({
|
|
749
|
+
message: 'Failed to initialize Rive instance',
|
|
750
|
+
code: RiveErrorCode.InvalidFormat,
|
|
751
|
+
cause: error instanceof Error ? error : undefined,
|
|
752
|
+
})));
|
|
392
753
|
}
|
|
393
754
|
});
|
|
394
755
|
}
|
|
395
756
|
// Event handlers (run inside Angular zone for change detection)
|
|
396
757
|
onLoad() {
|
|
758
|
+
// Validate loaded configuration
|
|
759
|
+
if (this.#rive) {
|
|
760
|
+
const validationErrors = validateConfiguration(this.#rive, {
|
|
761
|
+
artboard: this.artboard(),
|
|
762
|
+
animations: this.animations(),
|
|
763
|
+
stateMachines: this.stateMachines(),
|
|
764
|
+
}, this.logger);
|
|
765
|
+
// Emit validation errors via loadError output
|
|
766
|
+
if (validationErrors.length > 0) {
|
|
767
|
+
this.#ngZone.run(() => {
|
|
768
|
+
validationErrors.forEach((err) => this.loadError.emit(err));
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
// Log available assets in debug mode
|
|
772
|
+
// Note: These properties exist at runtime but may not be in type definitions
|
|
773
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
774
|
+
const riveWithMetadata = this.#rive;
|
|
775
|
+
this.logger.debug('Animation loaded successfully. Available assets:', {
|
|
776
|
+
artboards: riveWithMetadata.artboardNames,
|
|
777
|
+
animations: riveWithMetadata.animationNames,
|
|
778
|
+
stateMachines: riveWithMetadata.stateMachineNames,
|
|
779
|
+
});
|
|
780
|
+
}
|
|
397
781
|
this.#ngZone.run(() => {
|
|
398
|
-
this
|
|
782
|
+
this.#isLoaded.set(true);
|
|
399
783
|
this.loaded.emit();
|
|
784
|
+
// Emit riveReady after animation is fully loaded
|
|
785
|
+
if (this.#rive) {
|
|
786
|
+
this.riveReady.emit(this.#rive);
|
|
787
|
+
}
|
|
400
788
|
});
|
|
401
789
|
}
|
|
402
790
|
onLoadError(originalError) {
|
|
403
791
|
this.#ngZone.run(() => {
|
|
404
|
-
|
|
405
|
-
|
|
792
|
+
// Determine probable cause and code
|
|
793
|
+
let code = RiveErrorCode.NetworkError;
|
|
794
|
+
let message = 'Failed to load Rive animation';
|
|
795
|
+
if (originalError instanceof Error) {
|
|
796
|
+
if (originalError.message.includes('404')) {
|
|
797
|
+
code = RiveErrorCode.FileNotFound;
|
|
798
|
+
message = `File not found: ${this.src()}`;
|
|
799
|
+
}
|
|
800
|
+
else if (originalError.message.includes('format')) {
|
|
801
|
+
code = RiveErrorCode.InvalidFormat;
|
|
802
|
+
message = 'Invalid .riv file format';
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
const error = new RiveLoadError({
|
|
806
|
+
message,
|
|
807
|
+
code,
|
|
808
|
+
cause: originalError instanceof Error ? originalError : undefined,
|
|
809
|
+
});
|
|
810
|
+
this.logger.error('Rive load error:', error);
|
|
406
811
|
this.loadError.emit(error);
|
|
407
812
|
});
|
|
408
813
|
}
|
|
409
814
|
onPlay() {
|
|
410
815
|
this.#ngZone.run(() => {
|
|
411
|
-
this
|
|
412
|
-
this
|
|
816
|
+
this.#isPlaying.set(true);
|
|
817
|
+
this.#isPaused.set(false);
|
|
413
818
|
});
|
|
414
819
|
}
|
|
415
820
|
onPause() {
|
|
416
821
|
this.#ngZone.run(() => {
|
|
417
|
-
this
|
|
418
|
-
this
|
|
822
|
+
this.#isPlaying.set(false);
|
|
823
|
+
this.#isPaused.set(true);
|
|
419
824
|
});
|
|
420
825
|
}
|
|
421
826
|
onStop() {
|
|
422
827
|
this.#ngZone.run(() => {
|
|
423
|
-
this
|
|
424
|
-
this
|
|
828
|
+
this.#isPlaying.set(false);
|
|
829
|
+
this.#isPaused.set(false);
|
|
425
830
|
});
|
|
426
831
|
}
|
|
427
832
|
onStateChange(event) {
|
|
@@ -493,6 +898,13 @@ class RiveCanvasComponent {
|
|
|
493
898
|
if (!this.#rive)
|
|
494
899
|
return;
|
|
495
900
|
this.#ngZone.runOutsideAngular(() => {
|
|
901
|
+
// Validate input existence first
|
|
902
|
+
const error = validateInput(this.#rive, stateMachineName, inputName);
|
|
903
|
+
if (error) {
|
|
904
|
+
this.logger.warn(error.message);
|
|
905
|
+
this.#ngZone.run(() => this.loadError.emit(error));
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
496
908
|
const inputs = this.#rive.stateMachineInputs(stateMachineName);
|
|
497
909
|
const input = inputs.find((i) => i.name === inputName);
|
|
498
910
|
if (input && 'value' in input) {
|
|
@@ -507,6 +919,13 @@ class RiveCanvasComponent {
|
|
|
507
919
|
if (!this.#rive)
|
|
508
920
|
return;
|
|
509
921
|
this.#ngZone.runOutsideAngular(() => {
|
|
922
|
+
// Validate trigger (input) existence first
|
|
923
|
+
const error = validateInput(this.#rive, stateMachineName, triggerName);
|
|
924
|
+
if (error) {
|
|
925
|
+
this.logger.warn(error.message);
|
|
926
|
+
this.#ngZone.run(() => this.loadError.emit(error));
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
510
929
|
const inputs = this.#rive.stateMachineInputs(stateMachineName);
|
|
511
930
|
const input = inputs.find((i) => i.name === triggerName);
|
|
512
931
|
if (input && 'fire' in input && typeof input.fire === 'function') {
|
|
@@ -523,18 +942,18 @@ class RiveCanvasComponent {
|
|
|
523
942
|
this.#rive.cleanup();
|
|
524
943
|
}
|
|
525
944
|
catch (error) {
|
|
526
|
-
|
|
945
|
+
this.logger.warn('Error during Rive cleanup:', error);
|
|
527
946
|
}
|
|
528
947
|
this.#rive = null;
|
|
529
948
|
}
|
|
530
949
|
// Reset signals
|
|
531
|
-
this
|
|
532
|
-
this
|
|
533
|
-
this
|
|
534
|
-
this
|
|
950
|
+
this.#riveInstance.set(null);
|
|
951
|
+
this.#isLoaded.set(false);
|
|
952
|
+
this.#isPlaying.set(false);
|
|
953
|
+
this.#isPaused.set(false);
|
|
535
954
|
}
|
|
536
955
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: RiveCanvasComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
537
|
-
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: `
|
|
956
|
+
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 }, debugMode: { classPropertyName: "debugMode", publicName: "debugMode", 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: `
|
|
538
957
|
<canvas #canvas [style.width.%]="100" [style.height.%]="100"></canvas>
|
|
539
958
|
`, isInline: true, styles: [":host{display:block;width:100%;height:100%}canvas{display:block}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
540
959
|
}
|
|
@@ -543,7 +962,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImpor
|
|
|
543
962
|
args: [{ selector: 'rive-canvas', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
544
963
|
<canvas #canvas [style.width.%]="100" [style.height.%]="100"></canvas>
|
|
545
964
|
`, styles: [":host{display:block;width:100%;height:100%}canvas{display:block}\n"] }]
|
|
546
|
-
}], 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"] }] } });
|
|
965
|
+
}], 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 }] }], debugMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "debugMode", 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"] }] } });
|
|
547
966
|
|
|
548
967
|
/**
|
|
549
968
|
* Service for preloading and caching Rive files.
|
|
@@ -576,7 +995,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImpor
|
|
|
576
995
|
class RiveFileService {
|
|
577
996
|
cache = new Map();
|
|
578
997
|
pendingLoads = new Map();
|
|
998
|
+
bufferIdMap = new WeakMap();
|
|
579
999
|
bufferIdCounter = 0;
|
|
1000
|
+
// Optional debug configuration
|
|
1001
|
+
globalDebugConfig = inject(RIVE_DEBUG_CONFIG, {
|
|
1002
|
+
optional: true,
|
|
1003
|
+
});
|
|
580
1004
|
/**
|
|
581
1005
|
* Load a RiveFile from URL or ArrayBuffer.
|
|
582
1006
|
* Returns a signal with the file state and loading status.
|
|
@@ -588,15 +1012,20 @@ class RiveFileService {
|
|
|
588
1012
|
*/
|
|
589
1013
|
loadFile(params) {
|
|
590
1014
|
const cacheKey = this.getCacheKey(params);
|
|
1015
|
+
// Initialize logger for this request
|
|
1016
|
+
const logger = new RiveLogger(this.globalDebugConfig, params.debug);
|
|
1017
|
+
logger.debug(`RiveFileService: Request to load file`, { cacheKey });
|
|
591
1018
|
// Return cached entry if exists
|
|
592
1019
|
const cached = this.cache.get(cacheKey);
|
|
593
1020
|
if (cached) {
|
|
594
1021
|
cached.refCount++;
|
|
1022
|
+
logger.debug(`RiveFileService: Cache hit for ${cacheKey}`);
|
|
595
1023
|
return cached.state;
|
|
596
1024
|
}
|
|
597
1025
|
// Return pending load if already in progress
|
|
598
1026
|
const pending = this.pendingLoads.get(cacheKey);
|
|
599
1027
|
if (pending) {
|
|
1028
|
+
logger.debug(`RiveFileService: Reuse pending load for ${cacheKey}`);
|
|
600
1029
|
return pending.stateSignal.asReadonly();
|
|
601
1030
|
}
|
|
602
1031
|
// Create new loading state
|
|
@@ -605,7 +1034,7 @@ class RiveFileService {
|
|
|
605
1034
|
status: 'loading',
|
|
606
1035
|
}, ...(ngDevMode ? [{ debugName: "stateSignal" }] : []));
|
|
607
1036
|
// Start loading and track as pending
|
|
608
|
-
const promise = this.loadRiveFile(params, stateSignal, cacheKey);
|
|
1037
|
+
const promise = this.loadRiveFile(params, stateSignal, cacheKey, logger);
|
|
609
1038
|
this.pendingLoads.set(cacheKey, { stateSignal, promise });
|
|
610
1039
|
return stateSignal.asReadonly();
|
|
611
1040
|
}
|
|
@@ -631,9 +1060,18 @@ class RiveFileService {
|
|
|
631
1060
|
}
|
|
632
1061
|
}
|
|
633
1062
|
/**
|
|
634
|
-
* Clear all cached files
|
|
1063
|
+
* Clear all cached files and abort pending loads
|
|
635
1064
|
*/
|
|
636
1065
|
clearCache() {
|
|
1066
|
+
// Clear pending loads first to prevent them from populating the cache
|
|
1067
|
+
this.pendingLoads.forEach((pending) => {
|
|
1068
|
+
pending.stateSignal.set({
|
|
1069
|
+
riveFile: null,
|
|
1070
|
+
status: 'failed',
|
|
1071
|
+
});
|
|
1072
|
+
});
|
|
1073
|
+
this.pendingLoads.clear();
|
|
1074
|
+
// Clean up cached files
|
|
637
1075
|
this.cache.forEach((entry) => {
|
|
638
1076
|
try {
|
|
639
1077
|
entry.file.cleanup();
|
|
@@ -652,67 +1090,73 @@ class RiveFileService {
|
|
|
652
1090
|
return `src:${params.src}`;
|
|
653
1091
|
}
|
|
654
1092
|
if (params.buffer) {
|
|
655
|
-
// For buffers,
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
1093
|
+
// For buffers, use WeakMap to track unique IDs without mutating the buffer
|
|
1094
|
+
let bufferId = this.bufferIdMap.get(params.buffer);
|
|
1095
|
+
if (bufferId === undefined) {
|
|
1096
|
+
bufferId = ++this.bufferIdCounter;
|
|
1097
|
+
this.bufferIdMap.set(params.buffer, bufferId);
|
|
660
1098
|
}
|
|
661
|
-
return `buffer:${
|
|
1099
|
+
return `buffer:${bufferId}`;
|
|
662
1100
|
}
|
|
663
1101
|
return 'unknown';
|
|
664
1102
|
}
|
|
665
1103
|
/**
|
|
666
|
-
* Load RiveFile and update state signal
|
|
1104
|
+
* Load RiveFile and update state signal.
|
|
1105
|
+
* Addresses race condition by setting up listeners BEFORE init.
|
|
667
1106
|
*/
|
|
668
|
-
loadRiveFile(params, stateSignal, cacheKey) {
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
1107
|
+
async loadRiveFile(params, stateSignal, cacheKey, logger) {
|
|
1108
|
+
// Guard to ensure pending load is cleaned up exactly once
|
|
1109
|
+
let pendingCleanupDone = false;
|
|
1110
|
+
const finalizePendingLoadOnce = () => {
|
|
1111
|
+
if (!pendingCleanupDone) {
|
|
1112
|
+
this.pendingLoads.delete(cacheKey);
|
|
1113
|
+
pendingCleanupDone = true;
|
|
1114
|
+
}
|
|
1115
|
+
};
|
|
1116
|
+
try {
|
|
1117
|
+
// Extract debug parameter - it's not part of RiveFile SDK API
|
|
1118
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1119
|
+
const { debug, ...sdkParams } = params;
|
|
1120
|
+
const file = new RiveFile(sdkParams);
|
|
1121
|
+
// Listeners must be attached BEFORE calling init() to avoid race conditions
|
|
1122
|
+
// where init() completes or fails synchronously/immediately.
|
|
1123
|
+
file.on(EventType.Load, () => {
|
|
1124
|
+
logger.debug(`RiveFileService: File loaded successfully`, { cacheKey });
|
|
1125
|
+
// Request an instance to increment reference count
|
|
1126
|
+
// This prevents the file from being destroyed while in use
|
|
1127
|
+
file.getInstance();
|
|
1128
|
+
stateSignal.set({
|
|
1129
|
+
riveFile: file,
|
|
1130
|
+
status: 'success',
|
|
690
1131
|
});
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
// Remove from pending loads
|
|
697
|
-
this.pendingLoads.delete(cacheKey);
|
|
698
|
-
// Resolve (not reject) — error state is communicated via the signal.
|
|
699
|
-
// Rejecting would cause an unhandled promise rejection since no
|
|
700
|
-
// consumer awaits or catches this promise.
|
|
701
|
-
resolve();
|
|
1132
|
+
// Cache the successfully loaded file
|
|
1133
|
+
this.cache.set(cacheKey, {
|
|
1134
|
+
file,
|
|
1135
|
+
state: stateSignal.asReadonly(),
|
|
1136
|
+
refCount: 1,
|
|
702
1137
|
});
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
1138
|
+
finalizePendingLoadOnce();
|
|
1139
|
+
});
|
|
1140
|
+
file.on(EventType.LoadError, () => {
|
|
1141
|
+
logger.warn(`RiveFileService: Failed to load file`, { cacheKey });
|
|
706
1142
|
stateSignal.set({
|
|
707
1143
|
riveFile: null,
|
|
708
1144
|
status: 'failed',
|
|
709
1145
|
});
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
}
|
|
1146
|
+
finalizePendingLoadOnce();
|
|
1147
|
+
});
|
|
1148
|
+
logger.debug(`RiveFileService: Initializing file`, { cacheKey });
|
|
1149
|
+
// Await init() to catch initialization errors (e.g. WASM issues)
|
|
1150
|
+
await file.init();
|
|
1151
|
+
}
|
|
1152
|
+
catch (error) {
|
|
1153
|
+
logger.error('RiveFileService: Unexpected error loading file', error);
|
|
1154
|
+
stateSignal.set({
|
|
1155
|
+
riveFile: null,
|
|
1156
|
+
status: 'failed',
|
|
1157
|
+
});
|
|
1158
|
+
finalizePendingLoadOnce();
|
|
1159
|
+
}
|
|
716
1160
|
}
|
|
717
1161
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: RiveFileService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
718
1162
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: RiveFileService, providedIn: 'root' });
|
|
@@ -733,5 +1177,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImpor
|
|
|
733
1177
|
* Generated bundle index. Do not edit.
|
|
734
1178
|
*/
|
|
735
1179
|
|
|
736
|
-
export { RiveCanvasComponent, RiveFileService, RiveLoadError };
|
|
1180
|
+
export { RiveCanvasComponent, RiveErrorCode, RiveFileService, RiveLoadError, RiveValidationError, provideRiveDebug };
|
|
737
1181
|
//# sourceMappingURL=grandgular-rive-angular.mjs.map
|