@grandgular/rive-angular 0.1.2 → 0.3.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.
- package/README.md +149 -19
- package/fesm2022/grandgular-rive-angular.mjs +685 -124
- package/fesm2022/grandgular-rive-angular.mjs.map +1 -1
- package/package.json +3 -3
- package/types/grandgular-rive-angular.d.ts +128 -15
|
@@ -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,106 @@ 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
|
+
RiveErrorCode["TextRunNotFound"] = "RIVE_205";
|
|
77
|
+
// Configuration Errors
|
|
78
|
+
RiveErrorCode["NoSource"] = "RIVE_301";
|
|
79
|
+
RiveErrorCode["InvalidCanvas"] = "RIVE_302";
|
|
80
|
+
})(RiveErrorCode || (RiveErrorCode = {}));
|
|
81
|
+
/**
|
|
82
|
+
* Template messages for each error code.
|
|
83
|
+
* Used by formatErrorMessage to generate user-friendly descriptions.
|
|
84
|
+
*/
|
|
85
|
+
const ERROR_MESSAGES = {
|
|
86
|
+
[RiveErrorCode.FileNotFound]: 'File not found: {url}',
|
|
87
|
+
[RiveErrorCode.InvalidFormat]: 'Invalid .riv file format',
|
|
88
|
+
[RiveErrorCode.NetworkError]: 'Network error while loading file',
|
|
89
|
+
[RiveErrorCode.ArtboardNotFound]: 'Artboard "{name}" not found',
|
|
90
|
+
[RiveErrorCode.AnimationNotFound]: 'Animation "{name}" not found',
|
|
91
|
+
[RiveErrorCode.StateMachineNotFound]: 'State machine "{name}" not found',
|
|
92
|
+
[RiveErrorCode.InputNotFound]: 'Input "{name}" not found in "{stateMachine}"',
|
|
93
|
+
[RiveErrorCode.TextRunNotFound]: 'Text run "{name}" not found',
|
|
94
|
+
[RiveErrorCode.NoSource]: 'No animation source provided',
|
|
95
|
+
[RiveErrorCode.InvalidCanvas]: 'Invalid canvas element',
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Formats an error message by replacing placeholders with actual values.
|
|
99
|
+
*
|
|
100
|
+
* @param code - The error code
|
|
101
|
+
* @param params - Record of values to replace in the template (e.g. { name: 'MyAnim' })
|
|
102
|
+
* @returns The formatted error string
|
|
103
|
+
*/
|
|
104
|
+
function formatErrorMessage(code, params = {}) {
|
|
105
|
+
let message = ERROR_MESSAGES[code] || 'Unknown Rive error';
|
|
106
|
+
for (const [key, value] of Object.entries(params)) {
|
|
107
|
+
message = message.replace(`{${key}}`, value);
|
|
108
|
+
}
|
|
109
|
+
return message;
|
|
110
|
+
}
|
|
21
111
|
|
|
22
112
|
/**
|
|
23
113
|
* Fake IntersectionObserver for environments where it's not available (e.g., SSR)
|
|
@@ -45,12 +135,21 @@ const MyIntersectionObserver = (typeof globalThis !== 'undefined' && globalThis.
|
|
|
45
135
|
* Singleton IntersectionObserver wrapper for observing multiple elements
|
|
46
136
|
* with individual callbacks. This avoids creating multiple IntersectionObserver
|
|
47
137
|
* instances which is more efficient.
|
|
138
|
+
*
|
|
139
|
+
* Provided as an Angular service for better testability and DI integration.
|
|
48
140
|
*/
|
|
49
141
|
class ElementObserver {
|
|
50
142
|
observer;
|
|
51
143
|
elementsMap = new Map();
|
|
144
|
+
platformId = inject(PLATFORM_ID);
|
|
52
145
|
constructor() {
|
|
53
|
-
|
|
146
|
+
// Only create real observer in browser environment
|
|
147
|
+
if (isPlatformBrowser(this.platformId)) {
|
|
148
|
+
this.observer = new MyIntersectionObserver(this.onObserved);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
this.observer = new FakeIntersectionObserver();
|
|
152
|
+
}
|
|
54
153
|
}
|
|
55
154
|
onObserved = (entries) => {
|
|
56
155
|
entries.forEach((entry) => {
|
|
@@ -68,17 +167,238 @@ class ElementObserver {
|
|
|
68
167
|
this.observer.unobserve(element);
|
|
69
168
|
this.elementsMap.delete(element);
|
|
70
169
|
}
|
|
170
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: ElementObserver, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
171
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: ElementObserver, providedIn: 'root' });
|
|
71
172
|
}
|
|
72
|
-
|
|
73
|
-
|
|
173
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: ElementObserver, decorators: [{
|
|
174
|
+
type: Injectable,
|
|
175
|
+
args: [{
|
|
176
|
+
providedIn: 'root',
|
|
177
|
+
}]
|
|
178
|
+
}], ctorParameters: () => [] });
|
|
179
|
+
// Legacy function for backward compatibility
|
|
180
|
+
// New code should inject ElementObserver directly
|
|
181
|
+
let legacyObserverInstance = null;
|
|
74
182
|
/**
|
|
183
|
+
* @deprecated Use dependency injection instead: `inject(ElementObserver)`
|
|
75
184
|
* Get the singleton ElementObserver instance
|
|
76
185
|
*/
|
|
77
186
|
function getElementObserver() {
|
|
78
|
-
if (!
|
|
79
|
-
|
|
187
|
+
if (!legacyObserverInstance) {
|
|
188
|
+
legacyObserverInstance = new ElementObserver();
|
|
189
|
+
}
|
|
190
|
+
return legacyObserverInstance;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Injection token for global Rive debug configuration.
|
|
195
|
+
* Can be provided via provideRiveDebug().
|
|
196
|
+
*/
|
|
197
|
+
const RIVE_DEBUG_CONFIG = new InjectionToken('RIVE_DEBUG_CONFIG');
|
|
198
|
+
/**
|
|
199
|
+
* Provides global configuration for Rive debugging.
|
|
200
|
+
* Use this in your app.config.ts or module providers.
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* providers: [
|
|
204
|
+
* provideRiveDebug({ logLevel: 'debug' })
|
|
205
|
+
* ]
|
|
206
|
+
*/
|
|
207
|
+
function provideRiveDebug(config) {
|
|
208
|
+
return {
|
|
209
|
+
provide: RIVE_DEBUG_CONFIG,
|
|
210
|
+
useValue: config,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Internal logger for Rive Angular library.
|
|
216
|
+
* Handles log levels and formatting.
|
|
217
|
+
* Not exported publicly.
|
|
218
|
+
*/
|
|
219
|
+
class RiveLogger {
|
|
220
|
+
level;
|
|
221
|
+
constructor(globalConfig, localDebug) {
|
|
222
|
+
this.level = this.resolveLogLevel(globalConfig, localDebug);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Resolve effective log level based on precedence rules:
|
|
226
|
+
* 1. Local debug=true -> 'debug'
|
|
227
|
+
* 2. Local debug=false/undefined -> Use global config
|
|
228
|
+
* 3. No config -> 'error' (default)
|
|
229
|
+
*/
|
|
230
|
+
resolveLogLevel(globalConfig, localDebug) {
|
|
231
|
+
if (localDebug === true) {
|
|
232
|
+
return 'debug';
|
|
233
|
+
}
|
|
234
|
+
if (globalConfig?.logLevel) {
|
|
235
|
+
return globalConfig.logLevel;
|
|
236
|
+
}
|
|
237
|
+
return 'error';
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Update log level dynamically (e.g. when input changes)
|
|
241
|
+
*/
|
|
242
|
+
update(globalConfig, localDebug) {
|
|
243
|
+
this.level = this.resolveLogLevel(globalConfig, localDebug);
|
|
244
|
+
}
|
|
245
|
+
debug(message, ...args) {
|
|
246
|
+
if (this.shouldLog('debug')) {
|
|
247
|
+
console.debug(`[Rive] ${message}`, ...args);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
info(message, ...args) {
|
|
251
|
+
if (this.shouldLog('info')) {
|
|
252
|
+
console.info(`[Rive] ${message}`, ...args);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
warn(message, ...args) {
|
|
256
|
+
if (this.shouldLog('warn')) {
|
|
257
|
+
console.warn(`[Rive] ${message}`, ...args);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
error(message, ...args) {
|
|
261
|
+
if (this.shouldLog('error')) {
|
|
262
|
+
console.error(`[Rive] ${message}`, ...args);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
shouldLog(level) {
|
|
266
|
+
const levels = ['none', 'error', 'warn', 'info', 'debug'];
|
|
267
|
+
const currentIdx = levels.indexOf(this.level);
|
|
268
|
+
const targetIdx = levels.indexOf(level);
|
|
269
|
+
return currentIdx >= targetIdx;
|
|
80
270
|
}
|
|
81
|
-
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Validates requested artboard name against available artboards.
|
|
275
|
+
* Returns error if not found.
|
|
276
|
+
*/
|
|
277
|
+
function validateArtboard(rive, requestedName) {
|
|
278
|
+
if (!requestedName)
|
|
279
|
+
return null;
|
|
280
|
+
try {
|
|
281
|
+
// Safe check: ensure artboardNames exist on runtime
|
|
282
|
+
// Note: These properties exist at runtime but may not be in type definitions
|
|
283
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
284
|
+
const available = rive.artboardNames;
|
|
285
|
+
if (!available || !available.includes(requestedName)) {
|
|
286
|
+
return new RiveValidationError(formatErrorMessage(RiveErrorCode.ArtboardNotFound, {
|
|
287
|
+
name: requestedName,
|
|
288
|
+
}), RiveErrorCode.ArtboardNotFound, available || [], available && available.length > 0
|
|
289
|
+
? `Available artboards: ${available.join(', ')}`
|
|
290
|
+
: 'No artboards found in file');
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
// Graceful fallback if runtime metadata is not accessible
|
|
295
|
+
// Return null silently to avoid breaking validation flow
|
|
296
|
+
}
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Validates requested animation names against available animations.
|
|
301
|
+
* Returns first error found.
|
|
302
|
+
*/
|
|
303
|
+
function validateAnimations(rive, requestedNames) {
|
|
304
|
+
if (!requestedNames)
|
|
305
|
+
return null;
|
|
306
|
+
const names = Array.isArray(requestedNames)
|
|
307
|
+
? requestedNames
|
|
308
|
+
: [requestedNames];
|
|
309
|
+
try {
|
|
310
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
311
|
+
const available = rive.animationNames;
|
|
312
|
+
for (const name of names) {
|
|
313
|
+
if (!available || !available.includes(name)) {
|
|
314
|
+
return new RiveValidationError(formatErrorMessage(RiveErrorCode.AnimationNotFound, { name }), RiveErrorCode.AnimationNotFound, available || [], available && available.length > 0
|
|
315
|
+
? `Available animations: ${available.join(', ')}`
|
|
316
|
+
: 'No animations found in file');
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
// Graceful fallback if runtime metadata is not accessible
|
|
322
|
+
// Return null silently to avoid breaking validation flow
|
|
323
|
+
}
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Validates requested state machine names against available state machines.
|
|
328
|
+
* Returns first error found.
|
|
329
|
+
*/
|
|
330
|
+
function validateStateMachines(rive, requestedNames) {
|
|
331
|
+
if (!requestedNames)
|
|
332
|
+
return null;
|
|
333
|
+
const names = Array.isArray(requestedNames)
|
|
334
|
+
? requestedNames
|
|
335
|
+
: [requestedNames];
|
|
336
|
+
try {
|
|
337
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
338
|
+
const available = rive.stateMachineNames;
|
|
339
|
+
for (const name of names) {
|
|
340
|
+
if (!available || !available.includes(name)) {
|
|
341
|
+
return new RiveValidationError(formatErrorMessage(RiveErrorCode.StateMachineNotFound, { name }), RiveErrorCode.StateMachineNotFound, available || [], available && available.length > 0
|
|
342
|
+
? `Available state machines: ${available.join(', ')}`
|
|
343
|
+
: 'No state machines found in file');
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
// Graceful fallback if runtime metadata is not accessible
|
|
349
|
+
// Return null silently to avoid breaking validation flow
|
|
350
|
+
}
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Validates if an input exists within a specific state machine.
|
|
355
|
+
*/
|
|
356
|
+
function validateInput(rive, stateMachineName, inputName) {
|
|
357
|
+
try {
|
|
358
|
+
const inputs = rive.stateMachineInputs(stateMachineName);
|
|
359
|
+
if (!inputs)
|
|
360
|
+
return null; // Should not happen if SM exists
|
|
361
|
+
const found = inputs.find((i) => i.name === inputName);
|
|
362
|
+
if (!found) {
|
|
363
|
+
const available = inputs.map((i) => i.name);
|
|
364
|
+
return new RiveValidationError(formatErrorMessage(RiveErrorCode.InputNotFound, {
|
|
365
|
+
name: inputName,
|
|
366
|
+
stateMachine: stateMachineName,
|
|
367
|
+
}), RiveErrorCode.InputNotFound, available, available.length > 0
|
|
368
|
+
? `Available inputs in "${stateMachineName}": ${available.join(', ')}`
|
|
369
|
+
: `No inputs found in state machine "${stateMachineName}"`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
// Graceful fallback if runtime metadata is not accessible
|
|
374
|
+
// Return null silently to avoid breaking validation flow
|
|
375
|
+
}
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Runs full configuration validation.
|
|
380
|
+
* Logs warnings and returns array of errors.
|
|
381
|
+
*/
|
|
382
|
+
function validateConfiguration(rive, config, logger) {
|
|
383
|
+
const errors = [];
|
|
384
|
+
const artboardError = validateArtboard(rive, config.artboard);
|
|
385
|
+
if (artboardError)
|
|
386
|
+
errors.push(artboardError);
|
|
387
|
+
const animError = validateAnimations(rive, config.animations);
|
|
388
|
+
if (animError)
|
|
389
|
+
errors.push(animError);
|
|
390
|
+
const smError = validateStateMachines(rive, config.stateMachines);
|
|
391
|
+
if (smError)
|
|
392
|
+
errors.push(smError);
|
|
393
|
+
if (errors.length > 0) {
|
|
394
|
+
logger.warn(`Validation failed with ${errors.length} errors:`);
|
|
395
|
+
errors.forEach((err) => {
|
|
396
|
+
logger.warn(`- ${err.message}`);
|
|
397
|
+
if (err.suggestion)
|
|
398
|
+
logger.warn(` Suggestion: ${err.suggestion}`);
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
return errors;
|
|
82
402
|
}
|
|
83
403
|
|
|
84
404
|
/**
|
|
@@ -110,6 +430,8 @@ class RiveCanvasComponent {
|
|
|
110
430
|
#destroyRef = inject(DestroyRef);
|
|
111
431
|
#platformId = inject(PLATFORM_ID);
|
|
112
432
|
#ngZone = inject(NgZone);
|
|
433
|
+
#globalDebugConfig = inject(RIVE_DEBUG_CONFIG, { optional: true });
|
|
434
|
+
#elementObserver = inject(ElementObserver);
|
|
113
435
|
src = input(...(ngDevMode ? [undefined, { debugName: "src" }] : []));
|
|
114
436
|
buffer = input(...(ngDevMode ? [undefined, { debugName: "buffer" }] : []));
|
|
115
437
|
/**
|
|
@@ -139,6 +461,20 @@ class RiveCanvasComponent {
|
|
|
139
461
|
* Default is false for security - events must be handled manually via riveEvent output.
|
|
140
462
|
*/
|
|
141
463
|
automaticallyHandleEvents = input(false, ...(ngDevMode ? [{ debugName: "automaticallyHandleEvents" }] : []));
|
|
464
|
+
/**
|
|
465
|
+
* Enable debug mode for this specific instance.
|
|
466
|
+
* Overrides global configuration if set.
|
|
467
|
+
* - true: 'debug' level
|
|
468
|
+
* - false/undefined: use global level
|
|
469
|
+
*/
|
|
470
|
+
debugMode = input(...(ngDevMode ? [undefined, { debugName: "debugMode" }] : []));
|
|
471
|
+
/**
|
|
472
|
+
* Record of text run names to values for declarative text setting.
|
|
473
|
+
* Keys present in this input are CONTROLLED — the input is the source of truth.
|
|
474
|
+
* Keys absent from this input are UNCONTROLLED — managed imperatively.
|
|
475
|
+
* Values are applied reactively when input changes.
|
|
476
|
+
*/
|
|
477
|
+
textRuns = input(...(ngDevMode ? [undefined, { debugName: "textRuns" }] : []));
|
|
142
478
|
// Outputs (Events)
|
|
143
479
|
loaded = output();
|
|
144
480
|
loadError = output();
|
|
@@ -153,21 +489,28 @@ class RiveCanvasComponent {
|
|
|
153
489
|
*/
|
|
154
490
|
riveEvent = output();
|
|
155
491
|
/**
|
|
156
|
-
* Emitted when Rive instance is
|
|
492
|
+
* Emitted when Rive instance is fully loaded and ready.
|
|
157
493
|
* Provides direct access to the Rive instance for advanced use cases.
|
|
494
|
+
* Note: This fires AFTER the animation is loaded, not just instantiated.
|
|
158
495
|
*/
|
|
159
496
|
riveReady = output();
|
|
160
|
-
//
|
|
161
|
-
isPlaying = signal(false, ...(ngDevMode ? [{ debugName: "isPlaying" }] : []));
|
|
162
|
-
isPaused = signal(false, ...(ngDevMode ? [{ debugName: "isPaused" }] : []));
|
|
163
|
-
isLoaded = signal(false, ...(ngDevMode ? [{ debugName: "isLoaded" }] : []));
|
|
497
|
+
// Private writable signals
|
|
498
|
+
#isPlaying = signal(false, ...(ngDevMode ? [{ debugName: "#isPlaying" }] : []));
|
|
499
|
+
#isPaused = signal(false, ...(ngDevMode ? [{ debugName: "#isPaused" }] : []));
|
|
500
|
+
#isLoaded = signal(false, ...(ngDevMode ? [{ debugName: "#isLoaded" }] : []));
|
|
501
|
+
#riveInstance = signal(null, ...(ngDevMode ? [{ debugName: "#riveInstance" }] : []));
|
|
502
|
+
// Public readonly signals
|
|
503
|
+
isPlaying = this.#isPlaying.asReadonly();
|
|
504
|
+
isPaused = this.#isPaused.asReadonly();
|
|
505
|
+
isLoaded = this.#isLoaded.asReadonly();
|
|
164
506
|
/**
|
|
165
507
|
* Public signal providing access to the Rive instance.
|
|
166
508
|
* Use this to access advanced Rive SDK features.
|
|
167
509
|
*/
|
|
168
|
-
riveInstance =
|
|
510
|
+
riveInstance = this.#riveInstance.asReadonly();
|
|
169
511
|
// Private state
|
|
170
512
|
#rive = null;
|
|
513
|
+
logger;
|
|
171
514
|
resizeObserver = null;
|
|
172
515
|
isInitialized = false;
|
|
173
516
|
isPausedByIntersectionObserver = false;
|
|
@@ -176,11 +519,20 @@ class RiveCanvasComponent {
|
|
|
176
519
|
lastWidth = 0;
|
|
177
520
|
lastHeight = 0;
|
|
178
521
|
constructor() {
|
|
179
|
-
|
|
522
|
+
this.logger = new RiveLogger(this.#globalDebugConfig, this.debugMode());
|
|
523
|
+
// Effect to update logger level when debugMode changes
|
|
524
|
+
effect(() => {
|
|
525
|
+
this.logger.update(this.#globalDebugConfig, this.debugMode());
|
|
526
|
+
});
|
|
527
|
+
// Effect to reload animation when src, buffer, riveFile, or configuration changes
|
|
180
528
|
effect(() => {
|
|
181
529
|
const src = this.src();
|
|
182
530
|
const buffer = this.buffer();
|
|
183
531
|
const riveFile = this.riveFile();
|
|
532
|
+
// Track configuration changes to trigger reload
|
|
533
|
+
this.artboard();
|
|
534
|
+
this.animations();
|
|
535
|
+
this.stateMachines();
|
|
184
536
|
untracked(() => {
|
|
185
537
|
if ((src || buffer || riveFile) &&
|
|
186
538
|
isPlatformBrowser(this.#platformId) &&
|
|
@@ -188,6 +540,27 @@ class RiveCanvasComponent {
|
|
|
188
540
|
this.loadAnimation();
|
|
189
541
|
});
|
|
190
542
|
});
|
|
543
|
+
// Effect to update layout when fit or alignment changes
|
|
544
|
+
effect(() => {
|
|
545
|
+
const fit = this.fit();
|
|
546
|
+
const alignment = this.alignment();
|
|
547
|
+
untracked(() => {
|
|
548
|
+
if (this.#rive && isPlatformBrowser(this.#platformId)) {
|
|
549
|
+
const layoutParams = { fit, alignment };
|
|
550
|
+
this.#rive.layout = new Layout(layoutParams);
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
// Effect to apply text runs when input changes or animation loads
|
|
555
|
+
effect(() => {
|
|
556
|
+
const runs = this.textRuns();
|
|
557
|
+
const isLoaded = this.#isLoaded();
|
|
558
|
+
untracked(() => {
|
|
559
|
+
if (runs && isLoaded && this.#rive) {
|
|
560
|
+
this.applyTextRuns(runs);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
});
|
|
191
564
|
// Auto cleanup on destroy
|
|
192
565
|
this.#destroyRef.onDestroy(() => {
|
|
193
566
|
this.cleanupRive();
|
|
@@ -208,7 +581,6 @@ class RiveCanvasComponent {
|
|
|
208
581
|
*/
|
|
209
582
|
setupResizeObserver() {
|
|
210
583
|
const canvas = this.canvas().nativeElement;
|
|
211
|
-
const dpr = window.devicePixelRatio || 1;
|
|
212
584
|
this.resizeObserver = new ResizeObserver((entries) => {
|
|
213
585
|
// Cancel any pending resize frame
|
|
214
586
|
if (this.resizeRafId) {
|
|
@@ -224,6 +596,8 @@ class RiveCanvasComponent {
|
|
|
224
596
|
this.lastHeight = height;
|
|
225
597
|
// Defer resize to next animation frame to prevent excessive updates in Safari
|
|
226
598
|
this.resizeRafId = requestAnimationFrame(() => {
|
|
599
|
+
// Read current DPR to support monitor changes and zoom
|
|
600
|
+
const dpr = window.devicePixelRatio || 1;
|
|
227
601
|
// Set canvas size with device pixel ratio for sharp rendering
|
|
228
602
|
canvas.width = width * dpr;
|
|
229
603
|
canvas.height = height * dpr;
|
|
@@ -255,7 +629,6 @@ class RiveCanvasComponent {
|
|
|
255
629
|
if (!this.shouldUseIntersectionObserver())
|
|
256
630
|
return;
|
|
257
631
|
const canvas = this.canvas().nativeElement;
|
|
258
|
-
const observer = getElementObserver();
|
|
259
632
|
const onIntersectionChange = (entry) => {
|
|
260
633
|
if (entry.isIntersecting) {
|
|
261
634
|
// Canvas is visible - start rendering
|
|
@@ -282,7 +655,7 @@ class RiveCanvasComponent {
|
|
|
282
655
|
}
|
|
283
656
|
}
|
|
284
657
|
};
|
|
285
|
-
|
|
658
|
+
this.#elementObserver.registerCallback(canvas, onIntersectionChange);
|
|
286
659
|
}
|
|
287
660
|
/**
|
|
288
661
|
* Retest intersection - workaround for Chrome bug
|
|
@@ -314,8 +687,7 @@ class RiveCanvasComponent {
|
|
|
314
687
|
}
|
|
315
688
|
if (this.shouldUseIntersectionObserver()) {
|
|
316
689
|
const canvas = this.canvas().nativeElement;
|
|
317
|
-
|
|
318
|
-
observer.removeCallback(canvas);
|
|
690
|
+
this.#elementObserver.removeCallback(canvas);
|
|
319
691
|
}
|
|
320
692
|
}
|
|
321
693
|
/**
|
|
@@ -331,16 +703,27 @@ class RiveCanvasComponent {
|
|
|
331
703
|
const src = this.src();
|
|
332
704
|
const buffer = this.buffer();
|
|
333
705
|
const riveFile = this.riveFile();
|
|
334
|
-
if (!src && !buffer && !riveFile)
|
|
706
|
+
if (!src && !buffer && !riveFile) {
|
|
707
|
+
this.logger.warn('No animation source provided (src, buffer, or riveFile)');
|
|
708
|
+
this.#ngZone.run(() => this.loadError.emit(new RiveLoadError({
|
|
709
|
+
message: 'No animation source provided',
|
|
710
|
+
code: RiveErrorCode.NoSource,
|
|
711
|
+
})));
|
|
335
712
|
return;
|
|
713
|
+
}
|
|
714
|
+
this.logger.info(`Loading animation`, {
|
|
715
|
+
src: src || (buffer ? 'buffer' : 'riveFile'),
|
|
716
|
+
canvasWidth: canvas.width,
|
|
717
|
+
canvasHeight: canvas.height,
|
|
718
|
+
dpr: window.devicePixelRatio,
|
|
719
|
+
});
|
|
336
720
|
// Build layout configuration
|
|
337
721
|
const layoutParams = {
|
|
338
722
|
fit: this.fit(),
|
|
339
723
|
alignment: this.alignment(),
|
|
340
724
|
};
|
|
341
|
-
//
|
|
342
|
-
|
|
343
|
-
const config = {
|
|
725
|
+
// Build typed Rive configuration
|
|
726
|
+
const baseConfig = {
|
|
344
727
|
canvas,
|
|
345
728
|
autoplay: this.autoplay(),
|
|
346
729
|
layout: new Layout(layoutParams),
|
|
@@ -355,73 +738,114 @@ class RiveCanvasComponent {
|
|
|
355
738
|
onStateChange: (event) => this.onStateChange(event),
|
|
356
739
|
onRiveEvent: (event) => this.onRiveEvent(event),
|
|
357
740
|
};
|
|
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
|
|
741
|
+
// Add source (priority: riveFile > src > buffer)
|
|
742
|
+
const sourceConfig = riveFile
|
|
743
|
+
? { riveFile }
|
|
744
|
+
: src
|
|
745
|
+
? { src }
|
|
746
|
+
: buffer
|
|
747
|
+
? { buffer }
|
|
748
|
+
: {};
|
|
749
|
+
// Add optional configuration
|
|
750
|
+
const optionalConfig = {
|
|
751
|
+
...(this.artboard() ? { artboard: this.artboard() } : {}),
|
|
752
|
+
...(this.animations() ? { animations: this.animations() } : {}),
|
|
753
|
+
...(this.stateMachines()
|
|
754
|
+
? { stateMachines: this.stateMachines() }
|
|
755
|
+
: {}),
|
|
756
|
+
};
|
|
757
|
+
// Combine all configurations
|
|
758
|
+
const config = { ...baseConfig, ...sourceConfig, ...optionalConfig };
|
|
382
759
|
this.#rive = new Rive(config);
|
|
383
|
-
// Update public signal
|
|
760
|
+
// Update public signal (riveReady will be emitted in onLoad)
|
|
384
761
|
this.#ngZone.run(() => {
|
|
385
|
-
this
|
|
386
|
-
this.riveReady.emit(this.#rive);
|
|
762
|
+
this.#riveInstance.set(this.#rive);
|
|
387
763
|
});
|
|
388
764
|
}
|
|
389
765
|
catch (error) {
|
|
390
|
-
|
|
391
|
-
this.#ngZone.run(() => this.loadError.emit(new RiveLoadError(
|
|
766
|
+
this.logger.error('Failed to initialize Rive instance:', error);
|
|
767
|
+
this.#ngZone.run(() => this.loadError.emit(new RiveLoadError({
|
|
768
|
+
message: 'Failed to initialize Rive instance',
|
|
769
|
+
code: RiveErrorCode.InvalidFormat,
|
|
770
|
+
cause: error instanceof Error ? error : undefined,
|
|
771
|
+
})));
|
|
392
772
|
}
|
|
393
773
|
});
|
|
394
774
|
}
|
|
395
775
|
// Event handlers (run inside Angular zone for change detection)
|
|
396
776
|
onLoad() {
|
|
777
|
+
// Validate loaded configuration
|
|
778
|
+
if (this.#rive) {
|
|
779
|
+
const validationErrors = validateConfiguration(this.#rive, {
|
|
780
|
+
artboard: this.artboard(),
|
|
781
|
+
animations: this.animations(),
|
|
782
|
+
stateMachines: this.stateMachines(),
|
|
783
|
+
}, this.logger);
|
|
784
|
+
// Emit validation errors via loadError output
|
|
785
|
+
if (validationErrors.length > 0) {
|
|
786
|
+
this.#ngZone.run(() => {
|
|
787
|
+
validationErrors.forEach((err) => this.loadError.emit(err));
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
// Log available assets in debug mode
|
|
791
|
+
// Note: These properties exist at runtime but may not be in type definitions
|
|
792
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
793
|
+
const riveWithMetadata = this.#rive;
|
|
794
|
+
this.logger.debug('Animation loaded successfully. Available assets:', {
|
|
795
|
+
artboards: riveWithMetadata.artboardNames,
|
|
796
|
+
animations: riveWithMetadata.animationNames,
|
|
797
|
+
stateMachines: riveWithMetadata.stateMachineNames,
|
|
798
|
+
});
|
|
799
|
+
}
|
|
397
800
|
this.#ngZone.run(() => {
|
|
398
|
-
this
|
|
801
|
+
this.#isLoaded.set(true);
|
|
399
802
|
this.loaded.emit();
|
|
803
|
+
// Emit riveReady after animation is fully loaded
|
|
804
|
+
if (this.#rive) {
|
|
805
|
+
this.riveReady.emit(this.#rive);
|
|
806
|
+
}
|
|
400
807
|
});
|
|
401
808
|
}
|
|
402
809
|
onLoadError(originalError) {
|
|
403
810
|
this.#ngZone.run(() => {
|
|
404
|
-
|
|
405
|
-
|
|
811
|
+
// Determine probable cause and code
|
|
812
|
+
let code = RiveErrorCode.NetworkError;
|
|
813
|
+
let message = 'Failed to load Rive animation';
|
|
814
|
+
if (originalError instanceof Error) {
|
|
815
|
+
if (originalError.message.includes('404')) {
|
|
816
|
+
code = RiveErrorCode.FileNotFound;
|
|
817
|
+
message = `File not found: ${this.src()}`;
|
|
818
|
+
}
|
|
819
|
+
else if (originalError.message.includes('format')) {
|
|
820
|
+
code = RiveErrorCode.InvalidFormat;
|
|
821
|
+
message = 'Invalid .riv file format';
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
const error = new RiveLoadError({
|
|
825
|
+
message,
|
|
826
|
+
code,
|
|
827
|
+
cause: originalError instanceof Error ? originalError : undefined,
|
|
828
|
+
});
|
|
829
|
+
this.logger.error('Rive load error:', error);
|
|
406
830
|
this.loadError.emit(error);
|
|
407
831
|
});
|
|
408
832
|
}
|
|
409
833
|
onPlay() {
|
|
410
834
|
this.#ngZone.run(() => {
|
|
411
|
-
this
|
|
412
|
-
this
|
|
835
|
+
this.#isPlaying.set(true);
|
|
836
|
+
this.#isPaused.set(false);
|
|
413
837
|
});
|
|
414
838
|
}
|
|
415
839
|
onPause() {
|
|
416
840
|
this.#ngZone.run(() => {
|
|
417
|
-
this
|
|
418
|
-
this
|
|
841
|
+
this.#isPlaying.set(false);
|
|
842
|
+
this.#isPaused.set(true);
|
|
419
843
|
});
|
|
420
844
|
}
|
|
421
845
|
onStop() {
|
|
422
846
|
this.#ngZone.run(() => {
|
|
423
|
-
this
|
|
424
|
-
this
|
|
847
|
+
this.#isPlaying.set(false);
|
|
848
|
+
this.#isPaused.set(false);
|
|
425
849
|
});
|
|
426
850
|
}
|
|
427
851
|
onStateChange(event) {
|
|
@@ -493,6 +917,13 @@ class RiveCanvasComponent {
|
|
|
493
917
|
if (!this.#rive)
|
|
494
918
|
return;
|
|
495
919
|
this.#ngZone.runOutsideAngular(() => {
|
|
920
|
+
// Validate input existence first
|
|
921
|
+
const error = validateInput(this.#rive, stateMachineName, inputName);
|
|
922
|
+
if (error) {
|
|
923
|
+
this.logger.warn(error.message);
|
|
924
|
+
this.#ngZone.run(() => this.loadError.emit(error));
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
496
927
|
const inputs = this.#rive.stateMachineInputs(stateMachineName);
|
|
497
928
|
const input = inputs.find((i) => i.name === inputName);
|
|
498
929
|
if (input && 'value' in input) {
|
|
@@ -507,6 +938,13 @@ class RiveCanvasComponent {
|
|
|
507
938
|
if (!this.#rive)
|
|
508
939
|
return;
|
|
509
940
|
this.#ngZone.runOutsideAngular(() => {
|
|
941
|
+
// Validate trigger (input) existence first
|
|
942
|
+
const error = validateInput(this.#rive, stateMachineName, triggerName);
|
|
943
|
+
if (error) {
|
|
944
|
+
this.logger.warn(error.message);
|
|
945
|
+
this.#ngZone.run(() => this.loadError.emit(error));
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
510
948
|
const inputs = this.#rive.stateMachineInputs(stateMachineName);
|
|
511
949
|
const input = inputs.find((i) => i.name === triggerName);
|
|
512
950
|
if (input && 'fire' in input && typeof input.fire === 'function') {
|
|
@@ -514,6 +952,104 @@ class RiveCanvasComponent {
|
|
|
514
952
|
}
|
|
515
953
|
});
|
|
516
954
|
}
|
|
955
|
+
/**
|
|
956
|
+
* Get the current value of a text run.
|
|
957
|
+
* Returns undefined if the text run doesn't exist or Rive instance is not loaded.
|
|
958
|
+
*/
|
|
959
|
+
getTextRunValue(textRunName) {
|
|
960
|
+
if (!this.#rive)
|
|
961
|
+
return undefined;
|
|
962
|
+
try {
|
|
963
|
+
return this.#ngZone.runOutsideAngular(() => {
|
|
964
|
+
return this.#rive.getTextRunValue(textRunName);
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
catch (error) {
|
|
968
|
+
this.logger.warn(`Failed to get text run "${textRunName}":`, error);
|
|
969
|
+
return undefined;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Set a text run value.
|
|
974
|
+
* Warning: If the text run is controlled by textRuns input, this change will be overwritten
|
|
975
|
+
* on the next input update.
|
|
976
|
+
*/
|
|
977
|
+
setTextRunValue(textRunName, textRunValue) {
|
|
978
|
+
if (!this.#rive)
|
|
979
|
+
return;
|
|
980
|
+
// Check if this key is controlled by textRuns input
|
|
981
|
+
const controlledRuns = this.textRuns();
|
|
982
|
+
if (controlledRuns && textRunName in controlledRuns) {
|
|
983
|
+
this.logger.warn(`Text run "${textRunName}" is controlled by textRuns input. This change will be overwritten on next input update.`);
|
|
984
|
+
}
|
|
985
|
+
this.#ngZone.runOutsideAngular(() => {
|
|
986
|
+
try {
|
|
987
|
+
this.#rive.setTextRunValue(textRunName, textRunValue);
|
|
988
|
+
this.logger.debug(`Text run "${textRunName}" set to "${textRunValue}"`);
|
|
989
|
+
}
|
|
990
|
+
catch (error) {
|
|
991
|
+
this.logger.warn(`Failed to set text run "${textRunName}":`, error);
|
|
992
|
+
this.#ngZone.run(() => this.loadError.emit(new RiveValidationError(formatErrorMessage(RiveErrorCode.TextRunNotFound, {
|
|
993
|
+
name: textRunName,
|
|
994
|
+
}), RiveErrorCode.TextRunNotFound)));
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Get the current value of a text run at a specific path (for nested artboards/components).
|
|
1000
|
+
* Returns undefined if the text run doesn't exist or Rive instance is not loaded.
|
|
1001
|
+
*/
|
|
1002
|
+
getTextRunValueAtPath(textRunName, path) {
|
|
1003
|
+
if (!this.#rive)
|
|
1004
|
+
return undefined;
|
|
1005
|
+
try {
|
|
1006
|
+
return this.#ngZone.runOutsideAngular(() => {
|
|
1007
|
+
return this.#rive.getTextRunValueAtPath(textRunName, path);
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
catch (error) {
|
|
1011
|
+
this.logger.warn(`Failed to get text run "${textRunName}" at path "${path}":`, error);
|
|
1012
|
+
return undefined;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Set a text run value at a specific path (for nested artboards/components).
|
|
1017
|
+
* Note: AtPath text runs are always uncontrolled (not managed by textRuns input).
|
|
1018
|
+
*/
|
|
1019
|
+
setTextRunValueAtPath(textRunName, textRunValue, path) {
|
|
1020
|
+
if (!this.#rive)
|
|
1021
|
+
return;
|
|
1022
|
+
this.#ngZone.runOutsideAngular(() => {
|
|
1023
|
+
try {
|
|
1024
|
+
this.#rive.setTextRunValueAtPath(textRunName, textRunValue, path);
|
|
1025
|
+
this.logger.debug(`Text run "${textRunName}" at path "${path}" set to "${textRunValue}"`);
|
|
1026
|
+
}
|
|
1027
|
+
catch (error) {
|
|
1028
|
+
this.logger.warn(`Failed to set text run "${textRunName}" at path "${path}":`, error);
|
|
1029
|
+
this.#ngZone.run(() => this.loadError.emit(new RiveValidationError(formatErrorMessage(RiveErrorCode.TextRunNotFound, {
|
|
1030
|
+
name: textRunName,
|
|
1031
|
+
}), RiveErrorCode.TextRunNotFound)));
|
|
1032
|
+
}
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Apply all text runs from input (controlled keys).
|
|
1037
|
+
* Called on every input change or load.
|
|
1038
|
+
*/
|
|
1039
|
+
applyTextRuns(runs) {
|
|
1040
|
+
this.#ngZone.runOutsideAngular(() => {
|
|
1041
|
+
for (const [name, value] of Object.entries(runs)) {
|
|
1042
|
+
try {
|
|
1043
|
+
this.#rive.setTextRunValue(name, value);
|
|
1044
|
+
this.logger.debug(`Text run "${name}" set to "${value}"`);
|
|
1045
|
+
}
|
|
1046
|
+
catch (error) {
|
|
1047
|
+
this.logger.warn(`Failed to set text run "${name}":`, error);
|
|
1048
|
+
this.#ngZone.run(() => this.loadError.emit(new RiveValidationError(formatErrorMessage(RiveErrorCode.TextRunNotFound, { name }), RiveErrorCode.TextRunNotFound)));
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
517
1053
|
/**
|
|
518
1054
|
* Clean up Rive instance only
|
|
519
1055
|
*/
|
|
@@ -523,18 +1059,18 @@ class RiveCanvasComponent {
|
|
|
523
1059
|
this.#rive.cleanup();
|
|
524
1060
|
}
|
|
525
1061
|
catch (error) {
|
|
526
|
-
|
|
1062
|
+
this.logger.warn('Error during Rive cleanup:', error);
|
|
527
1063
|
}
|
|
528
1064
|
this.#rive = null;
|
|
529
1065
|
}
|
|
530
1066
|
// Reset signals
|
|
531
|
-
this
|
|
532
|
-
this
|
|
533
|
-
this
|
|
534
|
-
this
|
|
1067
|
+
this.#riveInstance.set(null);
|
|
1068
|
+
this.#isLoaded.set(false);
|
|
1069
|
+
this.#isPlaying.set(false);
|
|
1070
|
+
this.#isPaused.set(false);
|
|
535
1071
|
}
|
|
536
1072
|
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: `
|
|
1073
|
+
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 }, textRuns: { classPropertyName: "textRuns", publicName: "textRuns", 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
1074
|
<canvas #canvas [style.width.%]="100" [style.height.%]="100"></canvas>
|
|
539
1075
|
`, isInline: true, styles: [":host{display:block;width:100%;height:100%}canvas{display:block}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
540
1076
|
}
|
|
@@ -543,7 +1079,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImpor
|
|
|
543
1079
|
args: [{ selector: 'rive-canvas', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
544
1080
|
<canvas #canvas [style.width.%]="100" [style.height.%]="100"></canvas>
|
|
545
1081
|
`, 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"] }] } });
|
|
1082
|
+
}], 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 }] }], textRuns: [{ type: i0.Input, args: [{ isSignal: true, alias: "textRuns", 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
1083
|
|
|
548
1084
|
/**
|
|
549
1085
|
* Service for preloading and caching Rive files.
|
|
@@ -576,7 +1112,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImpor
|
|
|
576
1112
|
class RiveFileService {
|
|
577
1113
|
cache = new Map();
|
|
578
1114
|
pendingLoads = new Map();
|
|
1115
|
+
bufferIdMap = new WeakMap();
|
|
579
1116
|
bufferIdCounter = 0;
|
|
1117
|
+
// Optional debug configuration
|
|
1118
|
+
globalDebugConfig = inject(RIVE_DEBUG_CONFIG, {
|
|
1119
|
+
optional: true,
|
|
1120
|
+
});
|
|
580
1121
|
/**
|
|
581
1122
|
* Load a RiveFile from URL or ArrayBuffer.
|
|
582
1123
|
* Returns a signal with the file state and loading status.
|
|
@@ -588,15 +1129,20 @@ class RiveFileService {
|
|
|
588
1129
|
*/
|
|
589
1130
|
loadFile(params) {
|
|
590
1131
|
const cacheKey = this.getCacheKey(params);
|
|
1132
|
+
// Initialize logger for this request
|
|
1133
|
+
const logger = new RiveLogger(this.globalDebugConfig, params.debug);
|
|
1134
|
+
logger.debug(`RiveFileService: Request to load file`, { cacheKey });
|
|
591
1135
|
// Return cached entry if exists
|
|
592
1136
|
const cached = this.cache.get(cacheKey);
|
|
593
1137
|
if (cached) {
|
|
594
1138
|
cached.refCount++;
|
|
1139
|
+
logger.debug(`RiveFileService: Cache hit for ${cacheKey}`);
|
|
595
1140
|
return cached.state;
|
|
596
1141
|
}
|
|
597
1142
|
// Return pending load if already in progress
|
|
598
1143
|
const pending = this.pendingLoads.get(cacheKey);
|
|
599
1144
|
if (pending) {
|
|
1145
|
+
logger.debug(`RiveFileService: Reuse pending load for ${cacheKey}`);
|
|
600
1146
|
return pending.stateSignal.asReadonly();
|
|
601
1147
|
}
|
|
602
1148
|
// Create new loading state
|
|
@@ -605,7 +1151,7 @@ class RiveFileService {
|
|
|
605
1151
|
status: 'loading',
|
|
606
1152
|
}, ...(ngDevMode ? [{ debugName: "stateSignal" }] : []));
|
|
607
1153
|
// Start loading and track as pending
|
|
608
|
-
const promise = this.loadRiveFile(params, stateSignal, cacheKey);
|
|
1154
|
+
const promise = this.loadRiveFile(params, stateSignal, cacheKey, logger);
|
|
609
1155
|
this.pendingLoads.set(cacheKey, { stateSignal, promise });
|
|
610
1156
|
return stateSignal.asReadonly();
|
|
611
1157
|
}
|
|
@@ -631,9 +1177,18 @@ class RiveFileService {
|
|
|
631
1177
|
}
|
|
632
1178
|
}
|
|
633
1179
|
/**
|
|
634
|
-
* Clear all cached files
|
|
1180
|
+
* Clear all cached files and abort pending loads
|
|
635
1181
|
*/
|
|
636
1182
|
clearCache() {
|
|
1183
|
+
// Clear pending loads first to prevent them from populating the cache
|
|
1184
|
+
this.pendingLoads.forEach((pending) => {
|
|
1185
|
+
pending.stateSignal.set({
|
|
1186
|
+
riveFile: null,
|
|
1187
|
+
status: 'failed',
|
|
1188
|
+
});
|
|
1189
|
+
});
|
|
1190
|
+
this.pendingLoads.clear();
|
|
1191
|
+
// Clean up cached files
|
|
637
1192
|
this.cache.forEach((entry) => {
|
|
638
1193
|
try {
|
|
639
1194
|
entry.file.cleanup();
|
|
@@ -652,67 +1207,73 @@ class RiveFileService {
|
|
|
652
1207
|
return `src:${params.src}`;
|
|
653
1208
|
}
|
|
654
1209
|
if (params.buffer) {
|
|
655
|
-
// For buffers,
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
1210
|
+
// For buffers, use WeakMap to track unique IDs without mutating the buffer
|
|
1211
|
+
let bufferId = this.bufferIdMap.get(params.buffer);
|
|
1212
|
+
if (bufferId === undefined) {
|
|
1213
|
+
bufferId = ++this.bufferIdCounter;
|
|
1214
|
+
this.bufferIdMap.set(params.buffer, bufferId);
|
|
660
1215
|
}
|
|
661
|
-
return `buffer:${
|
|
1216
|
+
return `buffer:${bufferId}`;
|
|
662
1217
|
}
|
|
663
1218
|
return 'unknown';
|
|
664
1219
|
}
|
|
665
1220
|
/**
|
|
666
|
-
* Load RiveFile and update state signal
|
|
1221
|
+
* Load RiveFile and update state signal.
|
|
1222
|
+
* Addresses race condition by setting up listeners BEFORE init.
|
|
667
1223
|
*/
|
|
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
|
-
|
|
1224
|
+
async loadRiveFile(params, stateSignal, cacheKey, logger) {
|
|
1225
|
+
// Guard to ensure pending load is cleaned up exactly once
|
|
1226
|
+
let pendingCleanupDone = false;
|
|
1227
|
+
const finalizePendingLoadOnce = () => {
|
|
1228
|
+
if (!pendingCleanupDone) {
|
|
1229
|
+
this.pendingLoads.delete(cacheKey);
|
|
1230
|
+
pendingCleanupDone = true;
|
|
1231
|
+
}
|
|
1232
|
+
};
|
|
1233
|
+
try {
|
|
1234
|
+
// Extract debug parameter - it's not part of RiveFile SDK API
|
|
1235
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1236
|
+
const { debug, ...sdkParams } = params;
|
|
1237
|
+
const file = new RiveFile(sdkParams);
|
|
1238
|
+
// Listeners must be attached BEFORE calling init() to avoid race conditions
|
|
1239
|
+
// where init() completes or fails synchronously/immediately.
|
|
1240
|
+
file.on(EventType.Load, () => {
|
|
1241
|
+
logger.debug(`RiveFileService: File loaded successfully`, { cacheKey });
|
|
1242
|
+
// Request an instance to increment reference count
|
|
1243
|
+
// This prevents the file from being destroyed while in use
|
|
1244
|
+
file.getInstance();
|
|
1245
|
+
stateSignal.set({
|
|
1246
|
+
riveFile: file,
|
|
1247
|
+
status: 'success',
|
|
690
1248
|
});
|
|
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();
|
|
1249
|
+
// Cache the successfully loaded file
|
|
1250
|
+
this.cache.set(cacheKey, {
|
|
1251
|
+
file,
|
|
1252
|
+
state: stateSignal.asReadonly(),
|
|
1253
|
+
refCount: 1,
|
|
702
1254
|
});
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
1255
|
+
finalizePendingLoadOnce();
|
|
1256
|
+
});
|
|
1257
|
+
file.on(EventType.LoadError, () => {
|
|
1258
|
+
logger.warn(`RiveFileService: Failed to load file`, { cacheKey });
|
|
706
1259
|
stateSignal.set({
|
|
707
1260
|
riveFile: null,
|
|
708
1261
|
status: 'failed',
|
|
709
1262
|
});
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
}
|
|
1263
|
+
finalizePendingLoadOnce();
|
|
1264
|
+
});
|
|
1265
|
+
logger.debug(`RiveFileService: Initializing file`, { cacheKey });
|
|
1266
|
+
// Await init() to catch initialization errors (e.g. WASM issues)
|
|
1267
|
+
await file.init();
|
|
1268
|
+
}
|
|
1269
|
+
catch (error) {
|
|
1270
|
+
logger.error('RiveFileService: Unexpected error loading file', error);
|
|
1271
|
+
stateSignal.set({
|
|
1272
|
+
riveFile: null,
|
|
1273
|
+
status: 'failed',
|
|
1274
|
+
});
|
|
1275
|
+
finalizePendingLoadOnce();
|
|
1276
|
+
}
|
|
716
1277
|
}
|
|
717
1278
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: RiveFileService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
718
1279
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: RiveFileService, providedIn: 'root' });
|
|
@@ -733,5 +1294,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImpor
|
|
|
733
1294
|
* Generated bundle index. Do not edit.
|
|
734
1295
|
*/
|
|
735
1296
|
|
|
736
|
-
export { RiveCanvasComponent, RiveFileService, RiveLoadError };
|
|
1297
|
+
export { RiveCanvasComponent, RiveErrorCode, RiveFileService, RiveLoadError, RiveValidationError, provideRiveDebug };
|
|
737
1298
|
//# sourceMappingURL=grandgular-rive-angular.mjs.map
|