Pular para o conteúdo
charmotion

Rive no Vue 3 e Nuxt: configuração, Data Binding e SSR

Por equipe Charmotion · Atualizado em

Ilustração de capa do guia de frameworks e no-code: Rive no Vue 3 e no Nuxt: configuração, Data Binding, SSR, com personagens de demonstração do Charmotion

O Rive não tem runtime oficial para Vue, então apps em Vue 3 e Nuxt usam diretamente o runtime web @rive-app/canvas. Crie a instância do Rive no onMounted com autoBind: true, defina as propriedades do view model a partir de um watcher e chame cleanup() no onUnmounted. No Nuxt, renderize o componente só no cliente, porque o canvas e o WASM precisam de um navegador.

Principais pontos

  • Não existe pacote do Rive para Vue; @rive-app/canvas ou @rive-app/webgl2 funciona em qualquer componente Vue 3.
  • Crie a instância no onMounted, nunca no nível superior do setup, e mantenha-a fora de ref().
  • Com autoBind: true, defina r.viewModelInstance.number("mood").value e chame .trigger("success").trigger().
  • Chame r.cleanup() no onUnmounted para liberar os objetos WASM.
  • No Nuxt, use um componente .client.vue ou <ClientOnly> para evitar erros no servidor.

Existe um pacote oficial do Rive para Vue?

Não. O Rive mantém runtimes oficiais para web (JavaScript), React, plataformas Apple, Android, Flutter e React Native, entre outros. A documentação lista quatro runtimes da comunidade (Angular, C#, Qt e RiveCMP para Compose Multiplatform), e nenhum deles é para Vue. Uma página de ajuda antiga do Rive, que orientava desenvolvedores Vue a usar o runtime web, foi removida, mas a recomendação continua valendo.

Isso é uma lacuna menor do que parece. O runtime web não depende de framework: ele precisa de um elemento <canvas>, um arquivo .riv e o nome de uma state machine. O Vue oferece exatamente os hooks necessários para fornecer isso no momento certo.

NecessidadePacoteObservações
Rive no Vue ou no Nuxt@rive-app/canvasRenderizador de canvas 2D; sem limite de contextos WebGL
Rive com efeitos avançados@rive-app/webgl2Mesma API, Rive Renderer sobre WebGL2
Lottie no Vue@lottiefiles/dotlottie-vueComponente oficial da LottieFiles
Renderização no servidor do Nuxt<ClientOnly> ou .client.vueCanvas e WASM só funcionam no navegador

O Rive descreve o @rive-app/canvas como adequado para páginas com muitos canvas animados, porque ele não está sujeito ao limite de contextos WebGL do navegador. Escolha @rive-app/webgl2 se o seu arquivo usa recursos que exigem o Rive Renderer; o código abaixo é idêntico, exceto pelo import.

Como carregar um .riv no onMounted e limpá-lo no onUnmounted?

Instale o runtime com npm install @rive-app/canvas e coloque o arquivo em public/, que o Vite e o Nuxt servem a partir da raiz do site. Depois, crie um componente que controla uma instância do Rive:

<script setup>
import { watch, onMounted, onUnmounted, useTemplateRef } from "vue";
import { Rive, Layout, Fit } from "@rive-app/canvas";

const props = defineProps({ mood: { type: Number, default: 0 } });
const canvas = useTemplateRef("canvas");
let r = null; // plain variable: don't wrap the instance in ref()

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

function fire(name) {
  r?.viewModelInstance?.trigger(name)?.trigger();
}

onMounted(() => {
  r = new Rive({
    src: "/character.riv",
    canvas: canvas.value,
    stateMachine: "CharacterViewModelMachine",
    autoplay: true,
    autoBind: true,
    layout: new Layout({ fit: Fit.Contain }),
    onLoad: () => {
      r.resizeDrawingSurfaceToCanvas();
      setMood(props.mood);
    },
  });
});

watch(() => props.mood, setMood);
onUnmounted(() => r?.cleanup());
defineExpose({ fire });
</script>

<template>
  <canvas ref="canvas" width="320" height="320"
          role="img" aria-label="Animated mascot" />
</template>

O onMounted do Vue roda depois que o DOM do componente existe, então a ref do canvas já está definida. useTemplateRef exige Vue 3.5; em versões anteriores, use const canvas = ref(null). Guarde a instância do Rive em uma variável simples ou em shallowRef: um proxy reativo profundo em volta de um objeto baseado em WASM adiciona overhead e pode quebrar verificações de identidade dentro do runtime.

cleanup() importa mais do que parece. A documentação do Rive diz que ele remove os objetos C++ de baixo nível criados durante a vida da instância. Sem ele, cada troca de rota que remonta o mascote vaza memória.

Como trocar o humor e disparar triggers a partir do Vue?

O componente acima já faz as duas coisas. O watch em props.mood grava o novo valor no view model vinculado, e o método exposto fire() toca uma reação única. Um componente pai o controla como qualquer outro componente:

<script setup>
import { ref, useTemplateRef } from "vue";
import CharacterRive from "./CharacterRive.vue";

const mascot = useTemplateRef("mascot");
const saving = ref(false);

async function save() {
  saving.value = true;               // mood 5: loading
  try {
    await api.save();
    mascot.value.fire("success");
  } catch {
    mascot.value.fire("error");
  } finally {
    saving.value = false;            // back to mood 0: idle
  }
}
</script>

<template>
  <CharacterRive ref="mascot" :mood="saving ? 5 : 0" />
  <button @click="save">Save</button>
</template>
Diagrama de uma state machine do Rive: um estado de entrada, dez estados de loop escolhidos por um número mood e estados de reação única por trigger que voltam ao loop atual
Como um .riv do Charmotion é conectado: o número mood escolhe um loop, e os triggers tocam reações únicas a partir de qualquer estado.

Mantenha o estado do app como fonte da verdade e derive o humor dele, como faz o binding :mood. Triggers são eventos, não estado, então passam por um método, e não por uma prop.

Se o arquivo usa o sistema antigo de inputs em vez de view models, o mesmo padrão funciona com r.stateMachineInputs("CharacterStateMachine"): encontre o input pelo nome, defina .value em um número e chame .fire() em um trigger. O guia de state machine explica os dois sistemas.

Como usar o Rive no Nuxt sem erros de SSR?

O Nuxt renderiza as páginas primeiro no servidor, onde não há window, nem canvas, nem carregador de WebAssembly para o Rive. O onMounted nunca roda no servidor, então o componente acima já é praticamente seguro. O risco que resta é o próprio import: se um módulo acessa globais do navegador ao ser importado, o bundle do servidor lança window is not defined.

O Nuxt oferece duas formas de manter o componente fora da renderização no servidor:

  • Nomeie o arquivo como CharacterRive.client.vue. Assim o Nuxt o renderiza só no cliente, e ele fica fora da renderização no servidor.
  • Envolva-o em <ClientOnly>. O slot é renderizado só no navegador, e o slot #fallback é renderizado no servidor.
<ClientOnly>
  <CharacterRive :mood="2" />
  <template #fallback>
    <img src="/character-poster.png" width="320" height="320" alt="">
  </template>
</ClientOnly>

A imagem de fallback mantém o layout estável e dá algo para crawlers e conexões lentas verem. Se preferir manter um único componente universal, mova o import para dentro do hook: const { Rive } = await import("@rive-app/canvas") dentro de um onMounted assíncrono. Registre o onUnmounted de forma síncrona no setup, não depois do await, senão o Vue não consegue associá-lo ao componente.

Como hospedar o rive.wasm por conta própria no Vite ou no Nuxt?

Por padrão, o runtime web baixa o rive.wasm do unpkg na primeira vez que você cria uma instância. É prático, mas adiciona uma requisição de terceiros e falha em ambientes offline ou restritos. A documentação do Rive mostra como apontar o runtime para a sua própria cópia com RuntimeLoader.setWasmUrl() e reforça que a versão do arquivo wasm precisa ser a mesma do pacote @rive-app instalado.

Com o Vite (que o Nuxt usa), o sufixo ?url copia o arquivo para o build e retorna a URL com hash, então a versão sempre corresponde ao package.json:

import { RuntimeLoader } from "@rive-app/canvas";
import riveWasmUrl from "@rive-app/canvas/rive.wasm?url";

// Run once, before the first new Rive(...)
RuntimeLoader.setWasmUrl(riveWasmUrl);

Coloque isso em um pequeno módulo importado pelo componente do Rive ou em um plugin de cliente do Nuxt (plugins/rive.client.js). Como a URL vem do pacote instalado, atualizar o @rive-app/canvas atualiza o wasm automaticamente. Não copie o rive.wasm para public/ manualmente: a próxima atualização do pacote deixaria os dois dessincronizados.

Hospedar por conta própria também permite pré-carregar o arquivo nas páginas em que o mascote aparece na primeira dobra, com um <link rel="preload"> para a URL do wasm, para que ele seja baixado em paralelo com o seu JavaScript.

Rive ou Lottie para um mascote no Vue?

Os dois funcionam no Vue. A escolha depende de o seu código precisar ou não dizer à animação o que fazer em seguida.

  • Lottie é indicado para clipes lineares: um check de sucesso, uma ilustração de estado vazio, um spinner. A LottieFiles publica o @lottiefiles/dotlottie-vue, que renderiza <DotLottieVue autoplay loop src="…" /> e toca arquivos .lottie e .json. Trocar de humor significa trocar de arquivo ou de segmento.
  • Rive é indicado para personagens com estados. Um único .riv contém todos os loops e reações, a state machine cuida das transições, e o seu componente só grava um número ou dispara um trigger.

Se você ainda não tem um arquivo Rive, o Charmotion cria um a partir da imagem de um personagem ou logo em poucos minutos: 17 animações em um único .riv com um view model Character (um número mood de 0 a 9 para idle, happy, wave, thinking, sad, loading, listening, talking, sleeping e attention, além dos triggers success, error, yes, no, enter e exit). O arquivo também mantém os inputs legados da CharacterStateMachine, e o conjunto inclui exportações em MP4, GIF e WebM e MOV com fundo transparente para os lugares onde um canvas não cabe.

Dez estados em loop de um personagem, cada um selecionado por um número de mood de 0 a 9
Os dez estados em loop em um arquivo Rive do Charmotion, selecionados com o input numérico mood.

Como adicionar um personagem Rive a um app Vue 3 ou Nuxt

  1. Instale o runtime web. Execute npm install @rive-app/canvas e coloque o arquivo .riv em public/.
  2. Crie o componente. Adicione um canvas com uma template ref e crie new Rive({ ..., stateMachine: "CharacterViewModelMachine", autoBind: true }) no onMounted.
  3. Vincule o humor. Observe a prop mood e defina r.viewModelInstance.number("mood").value, também uma vez no onLoad.
  4. Exponha os triggers. Exponha um método que chame r.viewModelInstance.trigger(name).trigger() para success e error.
  5. Faça a limpeza. Chame r.cleanup() em onUnmounted.
  6. Lide com o SSR do Nuxt. Nomeie o arquivo como .client.vue ou envolva-o em <ClientOnly> com uma imagem de pôster como fallback.

Perguntas frequentes

Posso usar @rive-app/react-canvas em um app Vue?

Não. O pacote React depende dos hooks e da renderização do React. No Vue, use @rive-app/canvas ou @rive-app/webgl2, os runtimes web independentes de framework que o próprio pacote React encapsula. A API para carregar arquivos, vincular view models e fazer a limpeza é a mesma documentada nas páginas do runtime web do Rive.

Por que o Nuxt diz que window is not defined quando importo o Rive?

O import foi executado durante a renderização no servidor, onde os globais do navegador não existem. Renomeie o componente para CharacterRive.client.vue para que o Nuxt o renderize só no navegador, envolva-o em <ClientOnly> ou importe @rive-app/canvas dinamicamente dentro de onMounted. Qualquer uma das três opções mantém o runtime fora do servidor.

A instância do Rive deve ficar em um ref?

Mantenha-a em uma variável comum ou em um shallowRef. Um ref() profundo envolve a instância em um proxy reativo, o que adiciona overhead e pode interferir nos objetos baseados em WASM dentro dela. Seu template nunca precisa ler a instância diretamente, então ela não precisa ser reativa.

O watcher de mood roda antes de o arquivo carregar?

Pode rodar, e é por isso que o setter verifica se viewModelInstance existe antes de escrever. Em seguida, o callback onLoad aplica o valor atual da prop assim que o arquivo fica pronto, então o primeiro humor nunca se perde. Depois disso, cada mudança de prop vai direto para o view model vinculado.

Respostas rápidas deste guia

Fontes

Guias relacionados