Como eu criei um vídeo viral sobre Evolução Humana usando código (e você também pode)

Como eu criei um vídeo viral sobre Evolução Humana usando código (e você também pode)

Fernando·

A maioria dos vídeos de curiosidades no TikTok e Instagram são feitos com templates prontos no CapCut ou no Canva. Funcionam. Mas são todos iguais. Mesmas transições, mesmos efeitos, mesma cara.

Eu queria algo diferente. Queria controle total sobre cada frame, cada animação, cada detalhe visual. E queria que o conteúdo fosse dinâmico o suficiente pra eu trocar os dados e gerar um vídeo completamente novo em minutos.

A resposta: Remotion. Uma biblioteca que transforma componentes React em vídeos renderizados frame a frame.

Nesse post vou te mostrar exatamente como criei um vídeo sobre a Evolução Humana, de 3,7 bilhões de anos atrás até o futuro, com 28 estágios evolutivos e imagens geradas por IA via API do Google Gemini.

Sem banco de imagens. Sem direitos autorais. Sem limitação criativa.

O que é o Remotion?

Remotion é uma ferramenta open-source que permite criar vídeos programaticamente usando React. Em vez de arrastar blocos numa timeline, você escreve componentes. Cada frame do vídeo é renderizado como se fosse uma página web. Tudo que você sabe sobre CSS, animações e lógica de programação funciona dentro de um vídeo.

Por que eu escolhi o Remotion:

Controle absoluto. Cada pixel, cada frame, cada transição é definido no código. Quer mudar a cor de fundo de todos os cards? Uma linha. Quer trocar a fonte? Uma linha. No CapCut isso seria arrastar 28 elementos um por um.

Reprodutibilidade. Mude um dado no JSON e o vídeo inteiro se adapta. Isso significa que se amanhã eu quiser fazer "Evolução dos Computadores", eu troco o array de dados e pronto. Layout, animações, estrutura, tudo fica intacto.

Automação real. Conecte com APIs, bancos de dados, planilhas. O conteúdo do vídeo pode ser 100% dinâmico. Pensa que você tem um funcionário que trabalha 24/7 gerando vídeos sob demanda. É isso.

Se você já trabalha com React, a curva de aprendizado é quase zero.

O projeto: Evolução Humana em 1 minuto

A inspiração veio de um vídeo no YouTube que mostrava a evolução humana como uma timeline horizontal com cards deslizando. Legal, mas feito com motion graphics tradicional. Trabalho manual pra cada frame.

Eu pensei: e se eu fizesse isso com código?

O resultado: um vídeo vertical (9:16) para Instagram e TikTok, com 28 estágios evolutivos, imagens geradas por IA, scroll vertical suave com aceleração progressiva, e um CTA no final.

Vou te guiar por cada etapa.

Passo 1: Setup do projeto

Primeiro, crie um novo projeto Remotion:

pnpm create video@latest my-video
cd my-video
pnpm install

O Remotion já vem com um servidor de preview. Rode pnpm dev e abra http://localhost:3000 para ver o studio.

Estrutura básica

Após criar o projeto, a estrutura relevante fica assim:

my-video/
  public/          # Arquivos estáticos (imagens, áudios)
  src/
    Root.tsx        # Registro das composições
    index.ts        # Entry point
  package.json

O Root.tsx é onde você registra seus vídeos. Pense nele como o "roteador" dos seus vídeos:

import { Composition } from "remotion";
import { EvolutionTimeline, EVOLUTION_TOTAL_FRAMES } from "./EvolutionTimeline";

export const RemotionRoot: React.FC = () => {
  return (
    <Composition
      id="EvolutionTimeline"
      component={EvolutionTimeline}
      durationInFrames={EVOLUTION_TOTAL_FRAMES}
      fps={30}
      width={1080}
      height={1920}
    />
  );
};

Cada <Composition> define um vídeo com um id único (aparece no studio), o componente React que será renderizado, duração total em frames, fps (30 é padrão para redes sociais) e resolução (1080x1920 = vertical 9:16).

Passo 2: Estruturando os dados

Antes de pensar em animação, eu precisava dos dados. Criei um arquivo data.ts com todas as 28 fases da evolução:

// src/EvolutionTimeline/data.ts

export interface EvolutionEntry {
  name: string;
  timeLabel: string;
  description: string;
  image: string;
  color: string;
}

export const evolutionData: EvolutionEntry[] = [
  {
    name: "Archaea",
    timeLabel: "3,7 Bi",
    description: "Primeiros micro-organismos",
    image: "evolution/archaea.jpg",
    color: "#1a5276",
  },
  {
    name: "Homo sapiens",
    timeLabel: "300 Mil",
    description: "Humano moderno",
    image: "evolution/homo-sapiens.jpg",
    color: "#196f3d",
  },
  // ... mais 26 entradas
];

Separar os dados dos componentes foi a melhor decisão do projeto. Se amanhã eu quiser fazer o mesmo vídeo sobre outro tema, eu só troco o array. O visual inteiro fica intacto. Dados são dados, layout é layout. Misturar os dois é pedir pra sofrer na manutenção.

Passo 3: A anatomia de uma animação no Remotion

Antes de mergulhar nos componentes, três conceitos que são a base de tudo no Remotion:

useCurrentFrame()

Retorna o número do frame atual. É a única fonte de verdade para qualquer animação.

const frame = useCurrentFrame();
// Frame 0 no início, incrementa a cada frame renderizado

interpolate()

Mapeia um valor (geralmente o frame) de um range para outro. É o motor de qualquer animação linear.

const opacity = interpolate(frame, [0, 30], [0, 1], {
  extrapolateLeft: "clamp",
  extrapolateRight: "clamp",
});
// Frame 0 → opacity 0 (invisível)
// Frame 15 → opacity 0.5 (meio visível)
// Frame 30 → opacity 1 (totalmente visível)
// "clamp" impede que o valor ultrapasse os limites

spring()

Cria animações com física de mola. Mais naturais que interpolações lineares.

const scale = spring({
  frame,
  fps,
  config: { damping: 200 },
  durationInFrames: 20,
});
// Retorna de 0 a 1 com curva de mola
// damping alto (200) = sem bounce, suave
// damping baixo (15) = bounce, elástico

Uma coisa importante: nunca use CSS transitions ou animações do Tailwind no Remotion. Elas simplesmente não funcionam na renderização frame a frame. Toda animação deve ser derivada do useCurrentFrame(). Se você tentar usar transition: opacity 0.3s ease, o Remotion vai ignorar. Ele renderiza cada frame isoladamente, não existe "tempo real" passando.

Passo 4: Componente Intro

A intro é o gancho do vídeo. Precisa ser rápida, impactante, e comunicar o tema em 3 segundos.

// src/EvolutionTimeline/Intro.tsx

export const Intro: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps, durationInFrames } = useVideoConfig();

  // Título entra com spring (bounce suave)
  const titleScale = spring({
    frame,
    fps,
    config: { damping: 15, stiffness: 80 },
    durationInFrames: 30,
  });

  // Subtítulo aparece com fade-in atrasado
  const subtitleOpacity = interpolate(frame, [20, 40], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  // Tudo desaparece nos últimos 15 frames
  const exitOpacity = interpolate(
    frame,
    [durationInFrames - 15, durationInFrames],
    [1, 0],
    { extrapolateLeft: "clamp", extrapolateRight: "clamp" },
  );

  return (
    <AbsoluteFill style={{ opacity: exitOpacity }}>
      <div style={{ transform: `scale(${titleScale})` }}>
        <div style={{ fontSize: 72, color: "white" }}>
          A Evolução
        </div>
        <div style={{ fontSize: 72, color: "#00FFFF" }}>
          do Ser Humano
        </div>
      </div>

      <div style={{ opacity: subtitleOpacity }}>
        De 3,7 bilhões de anos atrás até o futuro
      </div>
    </AbsoluteFill>
  );
};

O que acontece aqui: nos frames 0-30, o título escala de 0 para 1 com efeito de mola (damping 15 dá aquele bounce sutil). Nos frames 20-40, o subtítulo faz um fade-in com 20 frames de atraso pra não competir com o título. Nos frames 75-90, tudo desaparece com fade-out para a transição.

A combinação de spring para entrada e interpolate para saída é um padrão que você vai usar em quase todo componente no Remotion. Funciona.

Passo 5: O componente Timeline

Essa é a parte mais complexa e mais interessante. A Timeline controla o scroll de 28 cards verticalmente, com uma aceleração gradual que começa devagar e vai ficando mais rápido.

O conceito de scroll offset

Em vez de mover cada card individualmente, eu calculo um único scrollOffset que representa a posição da câmera. Cada card calcula sua posição com base nesse offset:

// src/EvolutionTimeline/Timeline.tsx

export const Timeline: React.FC = () => {
  const frame = useCurrentFrame();
  const { durationInFrames } = useVideoConfig();

  // Altura total de todos os cards empilhados
  const totalScrollHeight =
    evolutionData.length * (CARD_HEIGHT + CARD_GAP) - CARD_GAP;
  // 28 * (777 + 30) - 30 = 22.566px

  const startOffset = 0;
  const endOffset = totalScrollHeight - 1920 + 100;

  // Bezier: começa devagar, vai acelerando
  const scrollOffset = interpolate(
    frame,
    [0, durationInFrames],
    [startOffset, endOffset],
    {
      extrapolateLeft: "clamp",
      extrapolateRight: "clamp",
      easing: Easing.bezier(0.25, 0.0, 0.8, 1.0),
    },
  );

  return (
    <AbsoluteFill>
      {evolutionData.map((entry, index) => (
        <TimelineCard
          key={entry.name}
          entry={entry}
          index={index}
          scrollOffset={scrollOffset}
        />
      ))}
    </AbsoluteFill>
  );
};

A mágica está na curva de easing Easing.bezier(0.25, 0.0, 0.8, 1.0). Essa curva faz o scroll começar lento, dando tempo para o espectador absorver as primeiras fases, e ir acelerando conforme avança na timeline. Como se o tempo estivesse se comprimindo. Faz sentido narrativamente: bilhões de anos com pouca mudança no início, evolução cada vez mais rápida no final.

Não é só estética. É storytelling com código.

Detalhes que elevam a qualidade

Adicionei dois efeitos visuais que fazem diferença no resultado final.

Uma barra de progresso vertical na lateral direita que preenche conforme o vídeo avança. Dá ao espectador uma noção de onde estamos na timeline:

<div style={{
  position: "absolute",
  top: 120, bottom: 120, right: 16,
  width: 4,
  backgroundColor: "#222",
}}>
  <div style={{
    width: "100%",
    height: `${progress}%`,
    backgroundColor: "#00FFFF",
  }} />
</div>

E gradientes no topo e na base que fazem os cards aparecerem e desaparecerem suavemente, em vez de cortar bruscamente:

<div style={{
  position: "absolute",
  top: 0, left: 0, right: 0,
  height: 120,
  background: "linear-gradient(to bottom, #0d0d0d, transparent)",
  zIndex: 5,
  pointerEvents: "none",
}} />

Detalhe pequeno, impacto grande. A diferença entre um vídeo que parece amador e um que parece profissional geralmente está nesses ajustes de 5 minutos.

Passo 6: O componente TimelineCard

Cada card é um componente independente que recebe seus dados e a posição do scroll:

// src/EvolutionTimeline/TimelineCard.tsx

export const CARD_HEIGHT = 777;
export const CARD_GAP = 30;

export const TimelineCard: React.FC<TimelineCardProps> = ({
  entry, index, scrollOffset,
}) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  // Posição vertical do card
  const cardY = index * (CARD_HEIGHT + CARD_GAP) - scrollOffset;

  // Otimização: não renderiza cards fora da tela
  if (cardY < -CARD_HEIGHT - 100 || cardY > 1920 + 100) {
    return null;
  }

  // Animação de entrada escalonada
  const entryProgress = spring({
    frame: Math.max(0, frame - index * 5),
    fps,
    config: { damping: 200 },
    durationInFrames: 20,
  });

  return (
    <div style={{
      position: "absolute",
      top: cardY,
      left: 40, right: 40,
      height: CARD_HEIGHT,
      opacity: entryProgress,
      transform: `scale(${0.85 + entryProgress * 0.15})`,
      borderRadius: 20,
      overflow: "hidden",
    }}>
      {/* Imagem ocupa a maior parte do card */}
      <div style={{ width: "100%", height: CARD_HEIGHT - 110 }}>
        <Img
          src={staticFile(entry.image)}
          style={{ width: "100%", height: "100%", objectFit: "cover" }}
        />
      </div>

      {/* Footer com badge, nome e descrição */}
      <div style={{ height: 110, backgroundColor: "#1a1a1a" }}>
        <svg viewBox="0 0 140 80">
          <polygon
            points="70,0 135,20 135,60 70,80 5,60 5,20"
            fill="#00FFFF"
          />
        </svg>
      </div>
    </div>
  );
};

Três coisas importantes aqui.

Culling de performance. Se o card está fora da área visível (acima ou abaixo da tela), ele simplesmente não é renderizado. Com 28 cards, isso evita renderizar uns 25 componentes desnecessários por frame. Em um vídeo de 2.400 frames, são dezenas de milhares de renderizações economizadas. Sem isso, o preview fica inutilizável.

Entrada escalonada. O frame - index * 5 faz cada card começar sua animação 5 frames depois do anterior. Isso cria o efeito cascata: os cards não aparecem todos ao mesmo tempo, mas em sequência, como dominós.

Escala + opacidade. O card começa 15% menor e invisível, depois cresce até o tamanho normal enquanto aparece. Essa combinação cria uma entrada muito mais sofisticada do que apenas um fade-in. É a diferença entre "apareceu" e "entrou em cena".

Passo 7: Sequenciando tudo com Sequence

O Remotion usa o componente <Sequence> para organizar o tempo. Cada Sequence tem seu próprio relógio interno. O useCurrentFrame() dentro de um Sequence retorna 0 no início daquela seção, não o frame global. Isso é importante porque permite que cada componente seja desenvolvido e testado isoladamente.

// src/EvolutionTimeline/index.tsx

const INTRO_DURATION = 90;       // 3 segundos
const TIMELINE_DURATION = 2400;  // 80 segundos
const OUTRO_DURATION = 120;      // 4 segundos
const OUTRO_OVERLAP = 30;        // 1 segundo de sobreposição

export const EVOLUTION_TOTAL_FRAMES =
  INTRO_DURATION + TIMELINE_DURATION + OUTRO_DURATION - OUTRO_OVERLAP;
// = 2580 frames = 86 segundos

export const EvolutionTimeline: React.FC = () => {
  return (
    <AbsoluteFill style={{ fontFamily }}>
      <Sequence durationInFrames={INTRO_DURATION} premountFor={30}>
        <Intro />
      </Sequence>

      <Sequence
        from={INTRO_DURATION}
        durationInFrames={TIMELINE_DURATION}
        premountFor={30}
      >
        <Timeline />
      </Sequence>

      <Sequence
        from={INTRO_DURATION + TIMELINE_DURATION - OUTRO_OVERLAP}
        durationInFrames={OUTRO_DURATION}
        premountFor={30}
      >
        <Outro />
      </Sequence>
    </AbsoluteFill>
  );
};

O pulo do gato é o overlap: o Outro começa 30 frames (1 segundo) antes do Timeline terminar. Como ambos os Sequences estão visíveis ao mesmo tempo nesse período, o Outro cobre o Timeline com seu fundo escuro, criando uma transição natural. Sem biblioteca extra de transitions. Às vezes a solução mais simples é a melhor.

O premountFor={30} é outra sutileza: ele pré-carrega o componente 30 frames antes de aparecer. Garante que fontes estejam carregadas e imagens pré-renderizadas, evitando flickers. Parece detalhe, mas num vídeo final faz toda a diferença.

Passo 8: Gerando imagens com IA

Essa foi uma das partes mais legais do projeto. Em vez de procurar imagens no Google e lidar com copyright, eu gerei todas as 28 imagens usando a API do Google Gemini.

Criei um script em TypeScript que define um prompt detalhado para cada fase evolutiva, chama a API do Gemini com configuração de aspect ratio 3:2, salva o resultado como JPG na pasta public/evolution/, e tem retry automático para lidar com rate limits.

// scripts/generate-images.ts

const MODEL = "gemini-3.1-flash-image-preview";
const API_URL = `https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent`;

const entries: ImageEntry[] = [
  {
    filename: "archaea.jpg",
    prompt: "Microscopic view of blue rod-shaped archaea bacteria on dark background, scientific illustration style, high detail, dramatic lighting",
  },
  {
    filename: "homo-erectus.jpg",
    prompt: "Homo erectus standing near a campfire at dusk, holding a spear, muscular build, realistic paleoart reconstruction",
  },
  // ... 26 mais
];

async function generateImage(entry: ImageEntry) {
  const response = await fetch(`${API_URL}?key=${API_KEY}`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      contents: [{ parts: [{ text: entry.prompt }] }],
      generationConfig: {
        responseModalities: ["IMAGE"],
        imageConfig: { aspectRatio: "3:2" },
      },
    }),
  });

  const data = await response.json();
  const imageData = data.candidates[0].content.parts
    .find((p) => p.inlineData)
    .inlineData.data;

  // Salva o base64 como arquivo
  fs.writeFileSync(outputPath, Buffer.from(imageData, "base64"));
}

Para rodar:

GEMINI_API_KEY="sua-chave-aqui" pnpm generate-images

O script pula imagens que já existem (verificando se o arquivo tem mais de 10KB), então você pode rodar várias vezes sem reprocessar tudo.

Sobre os prompts: seja específico sobre estilo artístico. "Realistic paleoart illustration" produz resultados muito melhores que "picture of a dinosaur". Inclua detalhes de iluminação, ângulo e contexto. Prompt genérico gera imagem genérica.

Gerar as 28 imagens via API levou menos de 5 minutos. Procurar e adaptar 28 imagens livres de royalty teria levado horas. E o resultado fica mais coeso visualmente porque todas as imagens seguem o mesmo estilo definido nos prompts. Aqui a IA resolve uma dor real: tempo e consistência visual.

Passo 9: Fontes e tipografia

O Remotion tem integração nativa com Google Fonts. Carregue a fonte no topo do componente e use o fontFamily retornado:

import { loadFont } from "@remotion/google-fonts/Inter";

const { fontFamily } = loadFont("normal", {
  weights: ["400", "500", "600", "700", "800", "900"],
  subsets: ["latin"],
});

// Aplique no container raiz
<AbsoluteFill style={{ fontFamily }}>

O loadFont garante que a fonte está carregada antes de renderizar. Sem FOUT (Flash of Unstyled Text) no vídeo final. Detalhe que parece irrelevante até você ver um frame com fonte fallback no meio do vídeo.

Passo 10: Renderizando o vídeo final

Quando tudo estiver pronto no studio (pnpm dev), renderize o vídeo final:

pnpm exec remotion render EvolutionTimeline out/evolution.mp4

Opções úteis:

# Qualidade máxima
pnpm exec remotion render EvolutionTimeline out/evolution.mp4 --codec h264 --crf 18

# Apenas um trecho (para testar)
pnpm exec remotion render EvolutionTimeline out/test.mp4 --frames=0-90

Renderizar só um trecho economiza muito tempo durante o desenvolvimento. Não renderize o vídeo inteiro toda vez que mudar uma cor.

O que eu aprendi

Separar dados do visual foi a decisão mais importante. Trocar o tema inteiro do vídeo é trocar um array. Cinco minutos.

A curva de easing no scroll transformou um scroll monótono em algo que conta uma história. O ritmo do vídeo comunica que o tempo se comprime conforme a evolução avança. Esse tipo de detalhe separa um vídeo amador de um profissional. E é uma linha de código.

Performance não é otimização prematura quando você tem 28 cards com imagens pesadas renderizando a cada frame. O culling é necessidade, não luxo.

IA para gerar assets resolve uma dor concreta: tempo e consistência. Não é hype, é produtividade. 5 minutos versus horas procurando imagens royalty-free.

E o overlap nas transições prova que às vezes a solução mais simples é a certa. Sem biblioteca extra, sem configuração complexa. Dois Sequences sobrepostos por 1 segundo. Pronto.

Próximos passos

Se você quer ir mais fundo, o Remotion tem muito mais a oferecer. O <TransitionSeries> traz transições prontas (fade, slide, wipe) entre cenas. Dá pra sincronizar animações com áudio usando <Audio> e useCurrentFrame(). Dá pra gerar legendas automáticas com IA e animar palavra por palavra. Dá pra parametrizar com Zod e criar vídeos a partir de formulários. E se precisar de escala, dá pra renderizar na nuvem com Lambda.

Conclusão

O Remotion coloca o poder de um motion designer nas mãos de um desenvolvedor. Se você sabe React, você sabe fazer vídeos. A diferença é que em vez de ficar arrastando keyframes manualmente, você escreve lógica. E lógica escala.

O vídeo que criei tem 28 cenas, 28 imagens geradas por IA, animações com física de mola, scroll com aceleração progressiva, e um sistema de dados que me permite recriar o vídeo inteiro com conteúdo diferente em minutos.

Tenta fazer isso no CapCut.