Pular para o conteúdo
charmotion

Rive no Svelte e SvelteKit (guia de Svelte 5)

Por equipe Charmotion · Atualizado em

Ilustração de capa do guia de frameworks e no-code: Rive no Svelte e no SvelteKit (guia do Svelte 5), com personagens de demonstração do Charmotion

O Svelte não tem um runtime oficial do Rive, então você usa diretamente o runtime web @rive-app/canvas. Crie a instância dentro de onMount, que nunca roda durante a renderização no servidor, com autoBind: true. Atualize o número mood do view model a partir de um $effect, dispare triggers nos event handlers e retorne uma função de limpeza síncrona que chame r.cleanup().

Principais pontos

  • O Rive não lista runtime para Svelte; @rive-app/canvas funciona em qualquer componente Svelte 5.
  • onMount não roda no servidor, então o SSR do SvelteKit pode continuar ativado.
  • Mantenha o callback de onMount síncrono para que a função de limpeza retornada seja executada.
  • Guarde a instância em uma variável $state.raw para que o $effect rode de novo assim que ela existir.
  • Coloque os arquivos .riv em static/ e carregue-os a partir do caminho raiz.

O Rive tem um pacote para Svelte?

Não oficialmente. A documentação do Rive cobre runtimes oficiais para web, React, Apple, Android, Flutter, React Native e game engines, e lista quatro runtimes da comunidade: Angular, C#, Qt e RiveCMP. Não há entrada para Svelte nem SvelteKit. Existem alguns wrappers da comunidade no npm, mas são projetos pequenos que podem ficar defasados em relação ao runtime.

O caminho confiável é o próprio runtime web do Rive, que é independente de framework. Um componente Svelte já oferece tudo o que ele precisa: um elemento canvas via bind:this, um hook de montagem e um hook de desmontagem.

NecessidadePacote
Personagem interativo com estados@rive-app/canvas (ou @rive-app/webgl2)
Clipe Lottie linear@lottiefiles/dotlottie-svelte

Instale com npm install @rive-app/canvas. O Rive recomenda o pacote canvas quando uma página mostra muitas animações ao mesmo tempo, já que ele não está sujeito ao limite de contextos WebGL do navegador. O pacote WebGL2 tem a mesma API, caso você precise do Rive Renderer.

Como montar o Rive no onMount com o SSR ativado?

A documentação do Svelte diz que onMount não roda em um componente renderizado no servidor, então criar a instância do Rive ali é seguro para SSR sem desativá-lo. Veja um componente Svelte 5 completo:

<script>
  import { onMount } from "svelte";
  import { Rive } from "@rive-app/canvas";

  let { mood = 0 } = $props();
  let canvas;
  let r = $state.raw(null);

  onMount(() => {
    const instance = new Rive({
      src: "/character.riv",
      canvas,
      stateMachine: "CharacterViewModelMachine",
      autoplay: true,
      autoBind: true,
      onLoad: () => {
        instance.resizeDrawingSurfaceToCanvas();
        r = instance; // ready: the $effect below runs now
      },
    });
    return () => instance.cleanup(); // sync callback, so this runs
  });

  $effect(() => {
    const prop = r?.viewModelInstance?.number("mood");
    if (prop) prop.value = mood;
  });

  export function fire(name) {
    r?.viewModelInstance?.trigger(name)?.trigger();
  }
</script>

<canvas bind:this={canvas} width="320" height="320"
        role="img" aria-label="Animated mascot"></canvas>

O import estático funciona no SvelteKit porque nada roda até onMount. Se algum bundler ou pacote ainda reclamar durante a renderização no servidor, importe-o dinamicamente dentro do hook ou proteja o código fora dos componentes com browser de $app/environment.

Como vincular uma prop mood ao view model com $effect?

$effect roda de novo sempre que um valor reativo lido por ele muda. No componente acima, ele lê dois: a prop mood e r. Por isso r é declarado com $state.raw e atribuído em onLoad: o effect roda primeiro com r ainda nulo, não faz nada e roda de novo assim que o arquivo fica pronto, aplicando o humor inicial. Depois disso, cada mudança de prop escreve direto no view model.

Usar $state.raw em vez de $state faz diferença. O $state comum torna objetos profundamente reativos com proxies, algo que você não quer em volta de um objeto de runtime baseado em WASM. A variante raw só rastreia reatribuições.

O componente pai controla o mascote com props comuns e uma referência ao componente:

<script>
  import Mascot from "$lib/Mascot.svelte";
  let mascot;
  let saving = $state(false);

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

<Mascot bind:this={mascot} mood={saving ? 5 : 0} />
<button onclick={save}>Save</button>
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.

Humores são estados, então ficam nas props. Triggers são eventos pontuais, então passam pela função exportada fire().

Onde coloco os arquivos .riv no SvelteKit?

Coloque-os em static/. O SvelteKit serve essa pasta a partir da raiz do site, então static/character.riv carrega como /character.riv, exatamente como no componente acima. Os arquivos ali são copiados como estão, sem hash, então dê um novo nome a um arquivo alterado (por exemplo character-v2.riv) ou defina headers de cache curtos se você o substitui com frequência.

Um arquivo .riv costuma ser pequeno. O download maior é o rive.wasm do runtime, que o runtime web busca no unpkg por padrão. Para servi-lo você mesmo, informe ao runtime onde ele está antes de criar a primeira instância:

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);

O import ?url do Vite copia o wasm para o seu build e retorna a URL dele. A documentação do Rive exige que a versão do wasm corresponda ao pacote @rive-app instalado, e importá-lo do próprio pacote garante isso. Hospedar por conta própria vale a pena em apps que funcionam offline, com políticas de segurança de conteúdo rígidas e em páginas que pré-carregam o mascote.

Como fazer a limpeza ao destruir o componente?

Retorne de onMount uma função que chame cleanup() na instância, como mostrado acima. A documentação do Rive explica que cleanup() remove os objetos C++ de baixo nível criados durante a vida da instância; se você pular essa etapa, haverá vazamento de memória a cada navegação que remonta o componente.

Há uma armadilha. O Svelte só usa a função retornada quando o callback passado a onMount é síncrono. Um callback async retorna uma promise, então sua limpeza é ignorada sem aviso. Se você precisar usar await em um import dinâmico, use onDestroy para a limpeza ou mantenha o callback externo síncrono:

onMount(() => {
  let instance;
  import("@rive-app/canvas").then(({ Rive }) => {
    instance = new Rive({ /* same options as above */ });
  });
  return () => instance?.cleanup();
});

Também interrompa o que você iniciou fora do Rive: remova event listeners e desconecte qualquer IntersectionObserver usado para iniciar a animação sob demanda.

Como respeitar o prefers-reduced-motion?

Verifique a media query antes de reproduzir e mantenha o personagem parado quando ela estiver ativa. prefers-reduced-motion informa a configuração do sistema operacional do visitante e tem dois valores, no-preference e reduce.

const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;

const instance = new Rive({
  // ...same options as above
  autoplay: !reduce,
});

Com autoplay: false, o runtime ainda desenha o primeiro frame da state machine, então o mascote aparece em uma pose estática. Você ainda pode definir o humor para que a pose combine com a situação. Chame isso dentro de onMount, porque matchMedia não existe no servidor.

Para um arquivo pronto, o Charmotion transforma uma imagem de personagem ou logo em 17 animações em poucos minutos, entregues como um único .riv com um view model Character: um número mood (0 idle, 1 happy, 2 wave, 3 thinking, 4 sad, 5 loading, 6 listening, 7 talking, 8 sleeping, 9 attention) e os triggers success, error, yes, no, enter e exit, junto com exportações sob demanda de MP4, GIF e WebM e MOV transparentes.

Rive ou dotLottie no Svelte: qual usar?

Use dotLottie para um clipe que toca sempre do mesmo jeito, e Rive para um personagem que reage ao seu app.

  • dotLottie: npm install @lottiefiles/dotlottie-svelte e depois renderize o componente DotLottieSvelte com uma fonte .lottie ou .json. Bom para checks de sucesso, ilustrações de onboarding e loaders.
  • Rive: um único .riv contém idle, loading, success e todos os outros estados, com as transições controladas pela state machine. Seu código Svelte só escreve um número ou dispara um trigger.

A pergunta decisiva é se o seu código precisa dizer o que acontece em seguida. Se a resposta for "continuar em loop até a requisição terminar e depois comemorar", uma state machine poupa você de lidar com vários arquivos e de sincronizar sozinho a passagem de um para outro. Veja Rive vs Lottie para o comparativo completo.

Como adicionar um personagem Rive a um app SvelteKit

  1. Instale o runtime. Execute npm install @rive-app/canvas e copie o .riv para static/.
  2. Crie o componente. Vincule um canvas com bind:this e crie a instância do Rive em um onMount síncrono com autoBind: true.
  3. Acompanhe quando está pronto. Atribua a instância a uma variável $state.raw em onLoad.
  4. Vincule o humor. Em $effect, defina r.viewModelInstance.number("mood").value a partir da prop.
  5. Dispare triggers. Exporte uma função que chama trigger(name).trigger() no view model.
  6. Faça a limpeza. Retorne () => instance.cleanup() do onMount.

Perguntas frequentes

Preciso desativar o SSR para usar Rive no SvelteKit?

Não. A documentação do Svelte informa que onMount não roda durante a renderização no servidor, então criar a instância do Rive ali mantém a página renderizada no servidor. Só a marcação do canvas é enviada pelo servidor, e a animação começa quando o componente é montado no navegador. Definir ssr = false para a rota é desnecessário.

Por que o cleanup do meu onMount não roda?

Provavelmente seu callback de onMount é async. O Svelte só usa a função de cleanup retornada quando o callback é síncrono, porque uma função async retorna uma promise. Mantenha o callback síncrono e trate o import dinâmico com .then(), ou mova o cleanup para onDestroy.

Isso funciona no Svelte 4?

As chamadas ao runtime são as mesmas. Substitua $props() por export let mood = 0, substitua $effect por uma declaração reativa como $: if (r) setMood(mood) e transforme r em um let comum. onMount, bind:this e a função de cleanup funcionam da mesma forma.

Respostas rápidas deste guia

Fontes

Guias relacionados