@grandgular/rive-angular 0.2.0 → 0.4.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 CHANGED
@@ -142,6 +142,270 @@ export class InteractiveComponent {
142
142
  }
143
143
  ```
144
144
 
145
+ ### Text Runs
146
+
147
+ Rive text runs allow you to update text content at runtime. The library provides two approaches:
148
+
149
+ #### Declarative (Controlled Keys)
150
+
151
+ Use the `textRuns` input for reactive, template-driven text updates:
152
+
153
+ ```html
154
+ <rive-canvas
155
+ src="assets/hello.riv"
156
+ [textRuns]="{ greeting: userName(), subtitle: 'Welcome' }"
157
+ />
158
+ ```
159
+
160
+ Keys present in `textRuns` are **controlled** — the input is the source of truth and will override any imperative changes.
161
+
162
+ #### Imperative (Uncontrolled Keys)
163
+
164
+ Use methods for reading values or managing keys not in `textRuns`:
165
+
166
+ ```typescript
167
+ riveRef = viewChild.required(RiveCanvasComponent);
168
+
169
+ // Read current value
170
+ const greeting = this.riveRef().getTextRunValue('greeting');
171
+
172
+ // Set uncontrolled key
173
+ this.riveRef().setTextRunValue('dynamicText', 'New value');
174
+ ```
175
+
176
+ #### Nested Text Runs
177
+
178
+ For text runs inside nested components, use the `AtPath` variants:
179
+
180
+ ```typescript
181
+ this.riveRef().setTextRunValueAtPath(
182
+ 'button_text',
183
+ 'Click Me',
184
+ 'NestedArtboard/ButtonComponent'
185
+ );
186
+ ```
187
+
188
+ #### Controlled vs Uncontrolled
189
+
190
+ - **Controlled**: Keys in `textRuns` input — managed by Angular, input is source of truth
191
+ - **Uncontrolled**: Keys not in `textRuns` — managed imperatively via methods
192
+ - **Warning**: Calling `setTextRunValue()` on a controlled key logs a warning and the change will be overwritten on next input update
193
+
194
+ ### Data Binding (ViewModel)
195
+
196
+ Rive's ViewModel system allows you to bind dynamic data (colors, numbers, strings, booleans, etc.) to your animations. ViewModels are created in the Rive editor and provide a structured way to control animation properties at runtime.
197
+
198
+ #### What is a ViewModel?
199
+
200
+ A ViewModel in Rive is a data structure that:
201
+ - Is created by designers in the Rive editor
202
+ - Contains typed properties (color, number, string, boolean, enum, trigger)
203
+ - Can be bound to animation elements (fills, strokes, transforms, etc.)
204
+ - Supports two-way data binding (changes in code affect animation, changes in animation can trigger events)
205
+
206
+ #### When to use Data Binding vs Text Runs?
207
+
208
+ - **Text Runs**: Simple text updates, no ViewModel setup required in editor
209
+ - **Data Binding**: Dynamic colors, numbers, complex data structures, two-way reactivity
210
+
211
+ #### Declarative (Controlled) Approach
212
+
213
+ Use the `dataBindings` input for reactive, template-driven data binding:
214
+
215
+ ```typescript
216
+ import { Component, signal } from '@angular/core';
217
+ import { RiveCanvasComponent } from '@grandgular/rive-angular';
218
+
219
+ @Component({
220
+ selector: 'app-data-binding',
221
+ standalone: true,
222
+ imports: [RiveCanvasComponent],
223
+ template: `
224
+ <rive-canvas
225
+ src="assets/animation.riv"
226
+ [dataBindings]="{
227
+ backgroundColor: themeColor(),
228
+ score: playerScore(),
229
+ playerName: userName(),
230
+ isActive: isGameActive()
231
+ }"
232
+ (dataBindingChange)="onDataChange($event)"
233
+ />
234
+
235
+ <button (click)="changeTheme()">Change Theme</button>
236
+ <button (click)="incrementScore()">+10 Points</button>
237
+ `
238
+ })
239
+ export class DataBindingComponent {
240
+ themeColor = signal('#FF5733');
241
+ playerScore = signal(0);
242
+ userName = signal('Player 1');
243
+ isGameActive = signal(true);
244
+
245
+ changeTheme() {
246
+ const colors = ['#FF5733', '#33FF57', '#3357FF', '#F333FF'];
247
+ const randomColor = colors[Math.floor(Math.random() * colors.length)];
248
+ this.themeColor.set(randomColor);
249
+ }
250
+
251
+ incrementScore() {
252
+ this.playerScore.update(score => score + 10);
253
+ }
254
+
255
+ onDataChange(event: DataBindingChangeEvent) {
256
+ console.log('Property changed from animation:', event);
257
+ // event.path: property path
258
+ // event.value: new value (for triggers, value is always true)
259
+ // event.propertyType: 'color' | 'number' | 'string' | 'boolean' | 'enum' | 'trigger'
260
+
261
+ if (event.propertyType === 'trigger') {
262
+ console.log(`Trigger "${event.path}" fired from animation`);
263
+ // Handle trigger event (e.g., show popup, play sound, etc.)
264
+ }
265
+ }
266
+ }
267
+ ```
268
+
269
+ #### Imperative (Uncontrolled) Approach
270
+
271
+ Use methods for direct, programmatic control:
272
+
273
+ ```typescript
274
+ import { Component, viewChild } from '@angular/core';
275
+ import { RiveCanvasComponent } from '@grandgular/rive-angular';
276
+
277
+ @Component({
278
+ selector: 'app-imperative',
279
+ standalone: true,
280
+ imports: [RiveCanvasComponent],
281
+ template: `
282
+ <rive-canvas src="assets/animation.riv" />
283
+
284
+ <button (click)="updateColor()">Update Color</button>
285
+ <button (click)="updateScore()">Update Score</button>
286
+ <button (click)="triggerAnimation()">Fire Trigger</button>
287
+ `
288
+ })
289
+ export class ImperativeComponent {
290
+ riveRef = viewChild.required(RiveCanvasComponent);
291
+
292
+ updateColor() {
293
+ // Set color using hex string
294
+ this.riveRef().setColor('backgroundColor', '#00FF00');
295
+
296
+ // Or using RGBA components
297
+ this.riveRef().setColorRgba('backgroundColor', 0, 255, 0, 255);
298
+
299
+ // Or change only opacity
300
+ this.riveRef().setColorOpacity('backgroundColor', 0.5);
301
+ }
302
+
303
+ updateScore() {
304
+ // Set any data binding value (auto-detects type)
305
+ this.riveRef().setDataBinding('score', 100);
306
+ this.riveRef().setDataBinding('playerName', 'Winner');
307
+ this.riveRef().setDataBinding('isActive', false);
308
+ }
309
+
310
+ triggerAnimation() {
311
+ // Fire a trigger property
312
+ this.riveRef().fireViewModelTrigger('onComplete');
313
+ }
314
+
315
+ readValues() {
316
+ // Read current values
317
+ const color = this.riveRef().getColor('backgroundColor');
318
+ // color: { r: 0, g: 255, b: 0, a: 255 }
319
+
320
+ const score = this.riveRef().getDataBinding('score');
321
+ // score: 100 (auto-detected as number)
322
+ }
323
+ }
324
+ ```
325
+
326
+ #### Color Utilities
327
+
328
+ The library exports color conversion utilities for advanced use cases:
329
+
330
+ ```typescript
331
+ import { parseRiveColor, riveColorToArgb, riveColorToHex } from '@grandgular/rive-angular';
332
+
333
+ // Parse various color formats
334
+ const color1 = parseRiveColor('#FF5733'); // { r: 255, g: 87, b: 51, a: 255 }
335
+ const color2 = parseRiveColor('#FF573380'); // { r: 255, g: 87, b: 51, a: 128 }
336
+ const color3 = parseRiveColor(0x80FF5733); // { r: 255, g: 87, b: 51, a: 128 }
337
+ const color4 = parseRiveColor({ r: 255, g: 0, b: 0, a: 255 });
338
+
339
+ // Convert to ARGB integer
340
+ const argb = riveColorToArgb({ r: 255, g: 0, b: 0, a: 255 }); // 0xFFFF0000
341
+
342
+ // Convert to hex string
343
+ const hex = riveColorToHex({ r: 255, g: 0, b: 0, a: 255 }); // '#FF0000FF'
344
+ ```
345
+
346
+ #### Selecting a ViewModel
347
+
348
+ If your `.riv` file contains multiple ViewModels, specify which one to use:
349
+
350
+ ```typescript
351
+ <rive-canvas
352
+ src="assets/animation.riv"
353
+ viewModelName="GameViewModel"
354
+ [dataBindings]="{ score: 42 }"
355
+ />
356
+ ```
357
+
358
+ If `viewModelName` is not provided, the default ViewModel for the artboard is used.
359
+
360
+ #### Controlled vs Uncontrolled
361
+
362
+ Same semantics as Text Runs:
363
+
364
+ - **Controlled**: Keys in `dataBindings` input — managed by Angular, input is source of truth
365
+ - **Uncontrolled**: Keys not in `dataBindings` — managed imperatively via methods
366
+ - **Warning**: Calling `setDataBinding()` or `setColor()` on a controlled key logs a warning and the change will be overwritten on next input update
367
+
368
+ #### Validation and Error Handling
369
+
370
+ Imperative methods (`setDataBinding`, `setColor`, `setColorOpacity`, `fireViewModelTrigger`) emit validation errors via the `loadError` output when:
371
+
372
+ - Property path doesn't exist in the ViewModel (`RIVE_402`)
373
+ - Value type doesn't match property type (`RIVE_403`)
374
+ - Color format is invalid (hex string, ARGB integer, or RiveColor object expected)
375
+ - Opacity value is out of range (must be between 0.0 and 1.0)
376
+
377
+ ```typescript
378
+ <rive-canvas
379
+ src="assets/animation.riv"
380
+ (loadError)="handleError($event)"
381
+ />
382
+
383
+ handleError(error: Error) {
384
+ if (error instanceof RiveValidationError) {
385
+ console.error('Validation error:', error.code, error.message);
386
+ }
387
+ }
388
+ ```
389
+
390
+ #### Advanced: Direct ViewModel Access
391
+
392
+ For advanced scenarios, access the ViewModel instance directly:
393
+
394
+ ```typescript
395
+ riveRef = viewChild.required(RiveCanvasComponent);
396
+
397
+ advancedUsage() {
398
+ const vmi = this.riveRef().viewModelInstance();
399
+ if (vmi) {
400
+ // Direct access to ViewModel SDK methods
401
+ const colorProp = vmi.color('backgroundColor');
402
+ if (colorProp) {
403
+ colorProp.rgba(255, 0, 0, 255);
404
+ }
405
+ }
406
+ }
407
+ ```
408
+
145
409
  ### Preloading files with RiveFileService
146
410
 
147
411
  For better performance, you can preload and cache .riv files:
@@ -252,8 +516,12 @@ In this case, `onError` receives a `RiveValidationError` with code `RIVE_201`, a
252
516
  | `RIVE_202` | Validation | Animation not found |
253
517
  | `RIVE_203` | Validation | State machine not found |
254
518
  | `RIVE_204` | Validation | Input/Trigger not found in State Machine |
519
+ | `RIVE_205` | Validation | Text run not found |
255
520
  | `RIVE_301` | Config | No animation source provided |
256
521
  | `RIVE_302` | Config | Invalid canvas element |
522
+ | `RIVE_401` | Data Binding | ViewModel not found |
523
+ | `RIVE_402` | Data Binding | Property not found in ViewModel |
524
+ | `RIVE_403` | Data Binding | Type mismatch (value doesn't match property type) |
257
525
 
258
526
  ## API Reference
259
527
 
@@ -277,6 +545,7 @@ In this case, `onError` receives a `RiveValidationError` with code `RIVE_201`, a
277
545
  | `shouldDisableRiveListeners` | `boolean` | `false` | Disable Rive event listeners |
278
546
  | `automaticallyHandleEvents` | `boolean` | `false` | Auto-handle Rive events (e.g., OpenUrlEvent) |
279
547
  | `debugMode` | `boolean` | `undefined` | Enable verbose logging for this instance |
548
+ | `textRuns` | `Record<string, string>` | - | Declarative text run values. Keys present are controlled by input. |
280
549
 
281
550
  #### Outputs
282
551
 
@@ -311,6 +580,10 @@ All signals are **readonly** and cannot be mutated externally. Use the public me
311
580
  | `reset()` | Reset animation to beginning |
312
581
  | `setInput(stateMachine: string, input: string, value: number \| boolean)` | Set state machine input value |
313
582
  | `fireTrigger(stateMachine: string, trigger: string)` | Fire state machine trigger |
583
+ | `getTextRunValue(name: string): string \| undefined` | Get text run value |
584
+ | `setTextRunValue(name: string, value: string)` | Set text run value (warns if key is controlled) |
585
+ | `getTextRunValueAtPath(name: string, path: string): string \| undefined` | Get nested text run value |
586
+ | `setTextRunValueAtPath(name: string, value: string, path: string)` | Set nested text run |
314
587
 
315
588
  ### RiveFileService
316
589