Ir al contenido
charmotion

Rive en Angular: wrapper de la comunidad vs canvas directo

Por el equipo de Charmotion · Actualizado el

Ilustración de portada de la guía de frameworks y no-code: Rive en Angular: wrapper de la comunidad vs canvas directo, con personajes de demostración de Charmotion

Rive no tiene un runtime oficial para Angular. ng-rive figura como desactualizado, y el wrapper de la comunidad que aparece en la documentación de Rive, @grandgular/rive-angular-*, requiere Angular 18+ y tiene pocos usuarios. Para productos de larga duración, llama al runtime web oficial @rive-app/canvas desde un componente: créalo después de que se inicialice la vista, contrólalo con signals y libera los recursos al destruirlo.

Puntos clave

  • Rive incluye Angular solo como runtime de la comunidad; no es de su propiedad ni lo mantiene.
  • ng-rive figura como desactualizado; los paquetes de Grandgular son más recientes, pero poco adoptados.
  • El runtime web directo necesita unas 30 líneas en un componente standalone y no agrega dependencias.
  • Protege la creación con isPlatformBrowser (o usa afterNextRender) para SSR.
  • Envía los valores del signal a viewModelInstance.number("mood") con effect().

¿Existe una librería oficial de Rive para Angular?

No. La documentación de Rive incluye Angular entre los runtimes de la comunidad y aclara que no es un proyecto propiedad de Rive ni mantenido por Rive. Tienes tres opciones realistas:

OpciónMantenido porAngularEstado
@rive-app/canvas o @rive-app/webgl2 usados directamenteRive (runtime web oficial)CualquieraLa más segura a largo plazo
@grandgular/rive-angular-webgl2 / -canvasComunidad, listado en la documentación de Rive18+Activo, pocos usuarios
ng-riveComunidadVersiones antiguasDocumentado como desactualizado

Un wrapper te ahorra algo de código repetitivo, pero agrega una dependencia que tiene que seguir el ritmo de las versiones de Angular y de Rive. Cuando revisamos el repositorio de Grandgular para esta guía, tenía apenas unas pocas estrellas en GitHub. Eso no significa que el código sea malo, pero sí que menos personas detectan y corrigen problemas. El enfoque directo que verás a continuación usa solo el paquete que publica Rive, así que las actualizaciones del runtime nunca dependen de un tercero.

¿ng-rive sigue teniendo mantenimiento?

Considéralo legacy. La documentación antigua de Rive para Angular, que explicaba npm install ng-rive, también se marcó como desactualizada, y la documentación actual de Rive remite en su lugar al wrapper de Grandgular. ng-rive se escribió para versiones antiguas de Angular y es anterior a los componentes standalone y a los signals.

Si una app existente lo usa y funciona, no hace falta quitarlo hoy. Planifica la migración la próxima vez que actualices Angular o adoptes una función más reciente de los archivos de Rive. Data Binding es la principal: Rive dejó obsoletos los inputs de la máquina de estados en favor de los view models, y un wrapper escrito antes de Data Binding no lo expone.

El paquete anterior del propio proyecto Grandgular, @grandgular/rive-angular, también está obsoleto en favor de los paquetes separados -webgl2 y -canvas. Si eliges el wrapper, instala los nombres actuales:

npm install @grandgular/rive-angular-webgl2 @rive-app/webgl2
# or
npm install @grandgular/rive-angular-canvas @rive-app/canvas

Su README indica como mínimo Angular 18.0.0 y TypeScript 5.4. Revisa el README para conocer los nombres actuales del componente y de los inputs antes de escribir las plantillas.

¿Cómo uso @rive-app/canvas directamente en un componente de Angular?

Instala el runtime con npm install @rive-app/canvas, coloca character.riv en public/ (o en src/assets/ en estructuras de proyecto más antiguas) y crea un componente standalone:

import { AfterViewInit, Component, ElementRef, OnDestroy, PLATFORM_ID,
  ViewChild, effect, inject, input } from "@angular/core";
import { isPlatformBrowser } from "@angular/common";
import { Rive } from "@rive-app/canvas";

@Component({
  selector: "app-mascot",
  standalone: true,
  template: '<canvas #c width="320" height="320" role="img" aria-label="Animated mascot"></canvas>',
})
export class MascotComponent implements AfterViewInit, OnDestroy {
  mood = input(0);
  @ViewChild("c") canvas!: ElementRef<HTMLCanvasElement>;
  private platformId = inject(PLATFORM_ID);
  private r?: Rive;

  constructor() {
    effect(() => this.setMood(this.mood()));
  }

  ngAfterViewInit() {
    if (!isPlatformBrowser(this.platformId)) return;
    this.r = new Rive({
      src: "character.riv",
      canvas: this.canvas.nativeElement,
      stateMachine: "CharacterViewModelMachine",
      autoplay: true,
      autoBind: true,
      onLoad: () => {
        this.r?.resizeDrawingSurfaceToCanvas();
        this.setMood(this.mood());
      },
    });
  }

  fire(name: "success" | "error" | "yes" | "no" | "enter" | "exit") {
    this.r?.viewModelInstance?.trigger(name)?.trigger();
  }

  private setMood(value: number) {
    const prop = this.r?.viewModelInstance?.number("mood");
    if (prop) prop.value = value;
  }

  ngOnDestroy() {
    this.r?.cleanup();
  }
}

cleanup() libera los objetos de C++ que el runtime reservó en WebAssembly, así que llámalo siempre al destruir el componente.

¿Cómo controlo el número mood desde un signal?

El componente anterior ya lo hace: mood es un signal input, y el effect() del constructor se vuelve a ejecutar cada vez que cambia y escribe el valor en el view model vinculado. Como el effect solo lee el signal y escribe en el runtime de Rive, no dispara la detección de cambios ni crea un ciclo.

Es posible que el effect se ejecute antes de que el archivo termine de cargar. El setter comprueba que viewModelInstance exista, y onLoad aplica el valor actual en cuanto el archivo está listo, así que el primer estado de ánimo nunca se pierde.

En un componente padre, deriva el estado de ánimo del estado de la app y llama a fire() para las reacciones puntuales:

import { Component, signal } from "@angular/core";

@Component({
  selector: "app-save",
  standalone: true,
  imports: [MascotComponent],
  template: `<app-mascot #m [mood]="saving() ? 5 : 0" />
             <button (click)="save(m)">Save</button>`,
})
export class SaveComponent {
  saving = signal(false);

  async save(m: MascotComponent) {
    this.saving.set(true);                 // mood 5: loading
    try { await this.api.save(); m.fire("success"); }
    catch { m.fire("error"); }
    finally { this.saving.set(false); }    // mood 0: idle
  }
}
Diagrama de una máquina de estados de Rive: un estado de entrada, diez estados de loop elegidos por un número mood y estados de reacción puntual activados por triggers que vuelven al loop actual
Cómo está conectado un .riv de Charmotion: el número mood elige un loop y los triggers reproducen reacciones puntuales desde cualquier estado.

Usa los estados de ánimo para estados continuos (idle, loading, listening) y los triggers para eventos (success, error). Esa división encaja limpiamente con signals para el estado y métodos para los eventos.

¿Cómo evito errores de SSR con el renderizado en servidor de Angular?

El renderizador de servidor de Angular ejecuta tus componentes en Node.js, donde no hay canvas, no hay window ni forma de ejecutar el runtime de Rive. Crear la instancia en el servidor lanza un error o desperdicia trabajo. La comprobación isPlatformBrowser(this.platformId) en ngAfterViewInit sale antes en el servidor, así que ahí solo se renderiza el markup del canvas y la animación empieza después de la hidratación.

Angular también ofrece afterNextRender, que según su documentación se ejecuta solo en el navegador, después del renderizado. Es una alternativa más limpia a la comprobación de plataforma:

constructor() {
  afterNextRender(() => this.start()); // never runs on the server
  effect(() => this.setMood(this.mood()));
}

En cualquier caso, conserva la limpieza en ngOnDestroy; con optional chaining no hace nada en el servidor. Si el propio import llega a dar problemas en un build de servidor, carga el runtime de forma diferida dentro del callback exclusivo del navegador con await import("@rive-app/canvas").

Los archivos de Charmotion están hechos justo para esta integración: un .riv con 17 animaciones de tu personaje o logo, un view model Character con un número mood de 0 a 9 y seis triggers, además de inputs heredados de CharacterStateMachine para wrappers antiguos. Los sets también incluyen código de ejemplo para web, React y SwiftUI.

Paquete canvas vs WebGL2: ¿cuál elegir?

Ambos paquetes exponen la misma clase Rive y las mismas opciones, así que cambiar de uno a otro solo requiere modificar una línea de import.

  • @rive-app/canvas dibuja con la API de canvas 2D del navegador. Rive lo recomienda para páginas que muestran muchas animaciones a la vez, porque no está sujeto al límite de contextos WebGL del navegador.
  • @rive-app/webgl2 usa el Rive Renderer sobre WebGL2 y admite todas las funciones, incluidos efectos que el canvas 2D no puede dibujar. Rive lo sugiere para la mayoría de las páginas con una sola animación.

Para un panel con una pequeña mascota de marca en varias tarjetas, elige canvas. Para un personaje grande en la sección hero con efectos avanzados, elige WebGL2. El wrapper de Grandgular publica un paquete para cada uno, con la misma elección.

Elijas el que elijas, instala solo uno. Ambos incluyen su propio WASM, y distribuir dos runtimes duplica la descarga sin ningún beneficio. Si más adelante cambias, la ruta del wasm alojado por ti también cambia, así que actualiza al mismo tiempo cualquier import de RuntimeLoader.setWasmUrl().

Cómo agregar un personaje de Rive a una app de Angular sin wrapper

  1. Instala el runtime. Ejecuta npm install @rive-app/canvas y agrega el .riv a public/.
  2. Crea un componente standalone. Agrega un canvas con una referencia de plantilla y un signal input mood.
  3. Inicia solo en el navegador. Crea new Rive({ stateMachine: "CharacterViewModelMachine", autoBind: true, ... }) en ngAfterViewInit detrás de isPlatformBrowser, o en afterNextRender.
  4. Sincroniza el signal. Usa effect() para escribir el estado de ánimo en viewModelInstance.number("mood").
  5. Expón los triggers. Agrega un método fire(name) que llame a trigger(name).trigger().
  6. Limpieza. Llama a this.r?.cleanup() en ngOnDestroy.

Preguntas frecuentes

¿Debo usar el wrapper de Rive de Grandgular?

Es el wrapper de la comunidad que la documentación de Rive incluye actualmente, requiere Angular 18 o posterior y está en desarrollo activo, pero todavía pocos proyectos lo usan. Es razonable para prototipos. Para un producto que vas a mantener durante años, el runtime web directo evita depender de un paquete pequeño de terceros.

¿Rive funciona con Angular sin zonas (zoneless)?

Sí. El runtime de Rive maneja su propio ciclo de renderizado con requestAnimationFrame y no depende de Zone.js ni de la detección de cambios. Los signals y effect() envían los valores directamente al runtime, así que el componente anterior funciona igual con o sin zonas. No necesitas envolverlo en NgZone.runOutsideAngular.

¿Dónde debe ir el archivo .riv en un proyecto de Angular?

En los proyectos actuales de Angular, colócalo en public/ y cárgalo como character.riv. Las estructuras más antiguas sirven src/assets/, así que la ruta pasa a ser assets/character.riv. La carpeta que uses debe figurar en la configuración de assets del build, algo que los proyectos nuevos ya traen por defecto.

Respuestas rápidas de esta guía

Fuentes

Guías relacionadas