@grandgular/rive-angular 0.1.1 → 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
|
|
80
348
|
}
|
|
81
|
-
return
|
|
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
|
|
373
|
+
}
|
|
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,31 +480,50 @@ 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;
|
|
174
508
|
retestIntersectionTimeoutId = null;
|
|
509
|
+
resizeRafId = null;
|
|
510
|
+
lastWidth = 0;
|
|
511
|
+
lastHeight = 0;
|
|
175
512
|
constructor() {
|
|
176
|
-
|
|
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
|
|
177
519
|
effect(() => {
|
|
178
520
|
const src = this.src();
|
|
179
521
|
const buffer = this.buffer();
|
|
180
522
|
const riveFile = this.riveFile();
|
|
523
|
+
// Track configuration changes to trigger reload
|
|
524
|
+
this.artboard();
|
|
525
|
+
this.animations();
|
|
526
|
+
this.stateMachines();
|
|
181
527
|
untracked(() => {
|
|
182
528
|
if ((src || buffer || riveFile) &&
|
|
183
529
|
isPlatformBrowser(this.#platformId) &&
|
|
@@ -185,6 +531,17 @@ class RiveCanvasComponent {
|
|
|
185
531
|
this.loadAnimation();
|
|
186
532
|
});
|
|
187
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
|
+
});
|
|
188
545
|
// Auto cleanup on destroy
|
|
189
546
|
this.#destroyRef.onDestroy(() => {
|
|
190
547
|
this.cleanupRive();
|
|
@@ -205,16 +562,30 @@ class RiveCanvasComponent {
|
|
|
205
562
|
*/
|
|
206
563
|
setupResizeObserver() {
|
|
207
564
|
const canvas = this.canvas().nativeElement;
|
|
208
|
-
const dpr = window.devicePixelRatio || 1;
|
|
209
565
|
this.resizeObserver = new ResizeObserver((entries) => {
|
|
566
|
+
// Cancel any pending resize frame
|
|
567
|
+
if (this.resizeRafId) {
|
|
568
|
+
cancelAnimationFrame(this.resizeRafId);
|
|
569
|
+
}
|
|
210
570
|
for (const entry of entries) {
|
|
211
571
|
const { width, height } = entry.contentRect;
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
572
|
+
// Skip if dimensions haven't changed (prevents unnecessary updates)
|
|
573
|
+
if (width === this.lastWidth && height === this.lastHeight) {
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
this.lastWidth = width;
|
|
577
|
+
this.lastHeight = height;
|
|
578
|
+
// Defer resize to next animation frame to prevent excessive updates in Safari
|
|
579
|
+
this.resizeRafId = requestAnimationFrame(() => {
|
|
580
|
+
// Read current DPR to support monitor changes and zoom
|
|
581
|
+
const dpr = window.devicePixelRatio || 1;
|
|
582
|
+
// Set canvas size with device pixel ratio for sharp rendering
|
|
583
|
+
canvas.width = width * dpr;
|
|
584
|
+
canvas.height = height * dpr;
|
|
585
|
+
// Resize Rive instance if it exists
|
|
586
|
+
if (this.#rive)
|
|
587
|
+
this.#rive.resizeDrawingSurfaceToCanvas();
|
|
588
|
+
});
|
|
218
589
|
}
|
|
219
590
|
});
|
|
220
591
|
this.resizeObserver.observe(canvas);
|
|
@@ -223,6 +594,10 @@ class RiveCanvasComponent {
|
|
|
223
594
|
* Disconnect ResizeObserver
|
|
224
595
|
*/
|
|
225
596
|
disconnectResizeObserver() {
|
|
597
|
+
if (this.resizeRafId) {
|
|
598
|
+
cancelAnimationFrame(this.resizeRafId);
|
|
599
|
+
this.resizeRafId = null;
|
|
600
|
+
}
|
|
226
601
|
if (this.resizeObserver) {
|
|
227
602
|
this.resizeObserver.disconnect();
|
|
228
603
|
this.resizeObserver = null;
|
|
@@ -235,7 +610,6 @@ class RiveCanvasComponent {
|
|
|
235
610
|
if (!this.shouldUseIntersectionObserver())
|
|
236
611
|
return;
|
|
237
612
|
const canvas = this.canvas().nativeElement;
|
|
238
|
-
const observer = getElementObserver();
|
|
239
613
|
const onIntersectionChange = (entry) => {
|
|
240
614
|
if (entry.isIntersecting) {
|
|
241
615
|
// Canvas is visible - start rendering
|
|
@@ -262,7 +636,7 @@ class RiveCanvasComponent {
|
|
|
262
636
|
}
|
|
263
637
|
}
|
|
264
638
|
};
|
|
265
|
-
|
|
639
|
+
this.#elementObserver.registerCallback(canvas, onIntersectionChange);
|
|
266
640
|
}
|
|
267
641
|
/**
|
|
268
642
|
* Retest intersection - workaround for Chrome bug
|
|
@@ -294,8 +668,7 @@ class RiveCanvasComponent {
|
|
|
294
668
|
}
|
|
295
669
|
if (this.shouldUseIntersectionObserver()) {
|
|
296
670
|
const canvas = this.canvas().nativeElement;
|
|
297
|
-
|
|
298
|
-
observer.removeCallback(canvas);
|
|
671
|
+
this.#elementObserver.removeCallback(canvas);
|
|
299
672
|
}
|
|
300
673
|
}
|
|
301
674
|
/**
|
|
@@ -311,16 +684,27 @@ class RiveCanvasComponent {
|
|
|
311
684
|
const src = this.src();
|
|
312
685
|
const buffer = this.buffer();
|
|
313
686
|
const riveFile = this.riveFile();
|
|
314
|
-
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
|
+
})));
|
|
315
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
|
+
});
|
|
316
701
|
// Build layout configuration
|
|
317
702
|
const layoutParams = {
|
|
318
703
|
fit: this.fit(),
|
|
319
704
|
alignment: this.alignment(),
|
|
320
705
|
};
|
|
321
|
-
//
|
|
322
|
-
|
|
323
|
-
const config = {
|
|
706
|
+
// Build typed Rive configuration
|
|
707
|
+
const baseConfig = {
|
|
324
708
|
canvas,
|
|
325
709
|
autoplay: this.autoplay(),
|
|
326
710
|
layout: new Layout(layoutParams),
|
|
@@ -335,73 +719,114 @@ class RiveCanvasComponent {
|
|
|
335
719
|
onStateChange: (event) => this.onStateChange(event),
|
|
336
720
|
onRiveEvent: (event) => this.onRiveEvent(event),
|
|
337
721
|
};
|
|
338
|
-
// Add
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
|
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 };
|
|
362
740
|
this.#rive = new Rive(config);
|
|
363
|
-
// Update public signal
|
|
741
|
+
// Update public signal (riveReady will be emitted in onLoad)
|
|
364
742
|
this.#ngZone.run(() => {
|
|
365
|
-
this
|
|
366
|
-
this.riveReady.emit(this.#rive);
|
|
743
|
+
this.#riveInstance.set(this.#rive);
|
|
367
744
|
});
|
|
368
745
|
}
|
|
369
746
|
catch (error) {
|
|
370
|
-
|
|
371
|
-
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
|
+
})));
|
|
372
753
|
}
|
|
373
754
|
});
|
|
374
755
|
}
|
|
375
756
|
// Event handlers (run inside Angular zone for change detection)
|
|
376
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
|
+
}
|
|
377
781
|
this.#ngZone.run(() => {
|
|
378
|
-
this
|
|
782
|
+
this.#isLoaded.set(true);
|
|
379
783
|
this.loaded.emit();
|
|
784
|
+
// Emit riveReady after animation is fully loaded
|
|
785
|
+
if (this.#rive) {
|
|
786
|
+
this.riveReady.emit(this.#rive);
|
|
787
|
+
}
|
|
380
788
|
});
|
|
381
789
|
}
|
|
382
790
|
onLoadError(originalError) {
|
|
383
791
|
this.#ngZone.run(() => {
|
|
384
|
-
|
|
385
|
-
|
|
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);
|
|
386
811
|
this.loadError.emit(error);
|
|
387
812
|
});
|
|
388
813
|
}
|
|
389
814
|
onPlay() {
|
|
390
815
|
this.#ngZone.run(() => {
|
|
391
|
-
this
|
|
392
|
-
this
|
|
816
|
+
this.#isPlaying.set(true);
|
|
817
|
+
this.#isPaused.set(false);
|
|
393
818
|
});
|
|
394
819
|
}
|
|
395
820
|
onPause() {
|
|
396
821
|
this.#ngZone.run(() => {
|
|
397
|
-
this
|
|
398
|
-
this
|
|
822
|
+
this.#isPlaying.set(false);
|
|
823
|
+
this.#isPaused.set(true);
|
|
399
824
|
});
|
|
400
825
|
}
|
|
401
826
|
onStop() {
|
|
402
827
|
this.#ngZone.run(() => {
|
|
403
|
-
this
|
|
404
|
-
this
|
|
828
|
+
this.#isPlaying.set(false);
|
|
829
|
+
this.#isPaused.set(false);
|
|
405
830
|
});
|
|
406
831
|
}
|
|
407
832
|
onStateChange(event) {
|
|
@@ -473,6 +898,13 @@ class RiveCanvasComponent {
|
|
|
473
898
|
if (!this.#rive)
|
|
474
899
|
return;
|
|
475
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
|
+
}
|
|
476
908
|
const inputs = this.#rive.stateMachineInputs(stateMachineName);
|
|
477
909
|
const input = inputs.find((i) => i.name === inputName);
|
|
478
910
|
if (input && 'value' in input) {
|
|
@@ -487,6 +919,13 @@ class RiveCanvasComponent {
|
|
|
487
919
|
if (!this.#rive)
|
|
488
920
|
return;
|
|
489
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
|
+
}
|
|
490
929
|
const inputs = this.#rive.stateMachineInputs(stateMachineName);
|
|
491
930
|
const input = inputs.find((i) => i.name === triggerName);
|
|
492
931
|
if (input && 'fire' in input && typeof input.fire === 'function') {
|
|
@@ -503,18 +942,18 @@ class RiveCanvasComponent {
|
|
|
503
942
|
this.#rive.cleanup();
|
|
504
943
|
}
|
|
505
944
|
catch (error) {
|
|
506
|
-
|
|
945
|
+
this.logger.warn('Error during Rive cleanup:', error);
|
|
507
946
|
}
|
|
508
947
|
this.#rive = null;
|
|
509
948
|
}
|
|
510
949
|
// Reset signals
|
|
511
|
-
this
|
|
512
|
-
this
|
|
513
|
-
this
|
|
514
|
-
this
|
|
950
|
+
this.#riveInstance.set(null);
|
|
951
|
+
this.#isLoaded.set(false);
|
|
952
|
+
this.#isPlaying.set(false);
|
|
953
|
+
this.#isPaused.set(false);
|
|
515
954
|
}
|
|
516
955
|
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: `
|
|
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: `
|
|
518
957
|
<canvas #canvas [style.width.%]="100" [style.height.%]="100"></canvas>
|
|
519
958
|
`, isInline: true, styles: [":host{display:block;width:100%;height:100%}canvas{display:block}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
520
959
|
}
|
|
@@ -523,7 +962,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImpor
|
|
|
523
962
|
args: [{ selector: 'rive-canvas', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
524
963
|
<canvas #canvas [style.width.%]="100" [style.height.%]="100"></canvas>
|
|
525
964
|
`, 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"] }] } });
|
|
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"] }] } });
|
|
527
966
|
|
|
528
967
|
/**
|
|
529
968
|
* Service for preloading and caching Rive files.
|
|
@@ -556,7 +995,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImpor
|
|
|
556
995
|
class RiveFileService {
|
|
557
996
|
cache = new Map();
|
|
558
997
|
pendingLoads = new Map();
|
|
998
|
+
bufferIdMap = new WeakMap();
|
|
559
999
|
bufferIdCounter = 0;
|
|
1000
|
+
// Optional debug configuration
|
|
1001
|
+
globalDebugConfig = inject(RIVE_DEBUG_CONFIG, {
|
|
1002
|
+
optional: true,
|
|
1003
|
+
});
|
|
560
1004
|
/**
|
|
561
1005
|
* Load a RiveFile from URL or ArrayBuffer.
|
|
562
1006
|
* Returns a signal with the file state and loading status.
|
|
@@ -568,15 +1012,20 @@ class RiveFileService {
|
|
|
568
1012
|
*/
|
|
569
1013
|
loadFile(params) {
|
|
570
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 });
|
|
571
1018
|
// Return cached entry if exists
|
|
572
1019
|
const cached = this.cache.get(cacheKey);
|
|
573
1020
|
if (cached) {
|
|
574
1021
|
cached.refCount++;
|
|
1022
|
+
logger.debug(`RiveFileService: Cache hit for ${cacheKey}`);
|
|
575
1023
|
return cached.state;
|
|
576
1024
|
}
|
|
577
1025
|
// Return pending load if already in progress
|
|
578
1026
|
const pending = this.pendingLoads.get(cacheKey);
|
|
579
1027
|
if (pending) {
|
|
1028
|
+
logger.debug(`RiveFileService: Reuse pending load for ${cacheKey}`);
|
|
580
1029
|
return pending.stateSignal.asReadonly();
|
|
581
1030
|
}
|
|
582
1031
|
// Create new loading state
|
|
@@ -585,7 +1034,7 @@ class RiveFileService {
|
|
|
585
1034
|
status: 'loading',
|
|
586
1035
|
}, ...(ngDevMode ? [{ debugName: "stateSignal" }] : []));
|
|
587
1036
|
// Start loading and track as pending
|
|
588
|
-
const promise = this.loadRiveFile(params, stateSignal, cacheKey);
|
|
1037
|
+
const promise = this.loadRiveFile(params, stateSignal, cacheKey, logger);
|
|
589
1038
|
this.pendingLoads.set(cacheKey, { stateSignal, promise });
|
|
590
1039
|
return stateSignal.asReadonly();
|
|
591
1040
|
}
|
|
@@ -611,9 +1060,18 @@ class RiveFileService {
|
|
|
611
1060
|
}
|
|
612
1061
|
}
|
|
613
1062
|
/**
|
|
614
|
-
* Clear all cached files
|
|
1063
|
+
* Clear all cached files and abort pending loads
|
|
615
1064
|
*/
|
|
616
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
|
|
617
1075
|
this.cache.forEach((entry) => {
|
|
618
1076
|
try {
|
|
619
1077
|
entry.file.cleanup();
|
|
@@ -632,67 +1090,73 @@ class RiveFileService {
|
|
|
632
1090
|
return `src:${params.src}`;
|
|
633
1091
|
}
|
|
634
1092
|
if (params.buffer) {
|
|
635
|
-
// For buffers,
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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);
|
|
640
1098
|
}
|
|
641
|
-
return `buffer:${
|
|
1099
|
+
return `buffer:${bufferId}`;
|
|
642
1100
|
}
|
|
643
1101
|
return 'unknown';
|
|
644
1102
|
}
|
|
645
1103
|
/**
|
|
646
|
-
* Load RiveFile and update state signal
|
|
1104
|
+
* Load RiveFile and update state signal.
|
|
1105
|
+
* Addresses race condition by setting up listeners BEFORE init.
|
|
647
1106
|
*/
|
|
648
|
-
loadRiveFile(params, stateSignal, cacheKey) {
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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',
|
|
670
1131
|
});
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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();
|
|
1132
|
+
// Cache the successfully loaded file
|
|
1133
|
+
this.cache.set(cacheKey, {
|
|
1134
|
+
file,
|
|
1135
|
+
state: stateSignal.asReadonly(),
|
|
1136
|
+
refCount: 1,
|
|
682
1137
|
});
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
1138
|
+
finalizePendingLoadOnce();
|
|
1139
|
+
});
|
|
1140
|
+
file.on(EventType.LoadError, () => {
|
|
1141
|
+
logger.warn(`RiveFileService: Failed to load file`, { cacheKey });
|
|
686
1142
|
stateSignal.set({
|
|
687
1143
|
riveFile: null,
|
|
688
1144
|
status: 'failed',
|
|
689
1145
|
});
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
}
|
|
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
|
+
}
|
|
696
1160
|
}
|
|
697
1161
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: RiveFileService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
698
1162
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: RiveFileService, providedIn: 'root' });
|
|
@@ -713,5 +1177,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImpor
|
|
|
713
1177
|
* Generated bundle index. Do not edit.
|
|
714
1178
|
*/
|
|
715
1179
|
|
|
716
|
-
export { RiveCanvasComponent, RiveFileService, RiveLoadError };
|
|
1180
|
+
export { RiveCanvasComponent, RiveErrorCode, RiveFileService, RiveLoadError, RiveValidationError, provideRiveDebug };
|
|
717
1181
|
//# sourceMappingURL=grandgular-rive-angular.mjs.map
|