Communitygithub.com

mvpdsigner/façade-motion-reel

Cria um vídeo vertical de motion-graphics sem rosto (tipografia cinética, canvas branco, uma cor de destaque, ícones animados, pills, contadores, transições whip) como código com Remotion e renderize para MP4. Use para qualquer reel, short, TikTok, Instagram ou vídeo do LinkedIn, vídeo sem rosto, motion graphics, texto cinético ou solicitação de explicador animado, ou quando um script, dica ou citação deve se tornar um vídeo vertical curto.

O que é façade-motion-reel?

Esta skill permite construir vídeos verticais curtos (15 a 30 segundos) no estilo 'visual mínimo, impacto máximo' usando Remotion. O fluxo de trabalho é orientado por um 'beat sheet' (quadro de cenas) onde cada linha de texto é combinada com um objeto animado, garantindo que cada cena transmita uma única ideia de forma clara, mesmo sem som. A implementação usa React com Remotion, fornecendo um kit de componentes reutilizáveis (cena, palavras, brilho, anéis) que lidam com animações de entrada/saída, transições com blur de movimento e zoom suave, eliminando a necessidade de edição manual de vídeo. A skill é ideal para transformar scripts, dicas ou citações em conteúdo viral para TikTok, Instagram Reels ou LinkedIn, com total controle sobre tipografia, cores (apenas três: branco, preto e uma cor de destaque) e ritmo. O resultado é um MP4 exato e facilmente revisável, sem depender de bibliotecas de mídia ou conexão de rede após a configuração inicial.

Funciona com~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/mvpdsigner/claude-skills/tree/HEAD/skills/faceless-motion-reel

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

Reel de motion graphics sem rosto

Transforme um script curto em um vídeo vertical de 15 a 30 segundos no estilo "visual mínimo, impacto máximo": uma tela branca limpa, tipografia preta, uma cor de destaque e um pequeno objeto animado por ideia. Sem filmagens, sem rosto, sem narração necessária. O vídeo é escrito como código React com Remotion, então cada quadro é exato e fácil de revisar.

A aparência (o que faz esse estilo funcionar)

Essas regras vieram do estudo de um reel de referência quadro a quadro. Elas importam mais do que qualquer efeito isolado, então mantenha-as mesmo quando inventar novas cenas.

  • Uma ideia por cena. Cada cena mostra uma linha curta (2 a 6 palavras) mais um objeto que carrega o significado. Um espectador deve entendê-la com o som desligado.
  • Apenas três cores. Fundo branco, tinta quase preta, um destaque. Cinza é usado apenas para texto que "ainda não pousou". Um segundo destaque faz parecer um modelo.
  • Duas vozes tipográficas. Uma sans-serif justa (Inter, peso 500, tracking por volta de -0.04em) para a maioria das palavras, e uma serifa itálica na cor de destaque para a frase que importa. Use a serifa no máximo uma vez por cena.
  • O texto chega, não apenas aparece. Novas palavras entram em cinza e ligeiramente borradas, depois se fixam em preto. O olho segue a palavra mais nova.
  • Nada nunca fica parado. Cada cena mantém um zoom lento e os anéis de fundo continuam se movendo. Um quadro congelado parece uma apresentação de slides.
  • Transições são movimentos de câmera. As cenas empurram para cima ou para os lados com desfoque de movimento, ou desaparecem para branco. Sem cortes, giros ou explosões de zoom.
  • Tipografia pequena, muito espaço. Títulos ficam por volta de 84 a 100px em um quadro de 1080 de largura. O espaço vazio é o estilo.
  • O objeto é a ideia. Um anel de contagem regressiva para tempo, badges rotulados para uma lista de problemas, pills empilhadas para uma lista de traços, um loop desenhado ao redor da linha de recompensa. Quando o script tem uma imagem forte (um relógio, um gráfico, um cursor), desenhe esse objeto em SVG em vez de reutilizar uma peça do kit.

Fluxo de trabalho

1. Obtenha o briefing

Você precisa: da mensagem ou script, da cor de destaque, do logotipo ou nome para a última cena, e se há um arquivo de música. Se o usuário deu um tópico, mas nenhum script, escreva o script você mesmo e mostre-o no beat sheet. Não empate em perguntas. Padrões sensatos: cerca de 20 segundos, 1080x1920, 30fps, destaque #1FB81A, sem áudio.

2. Escreva o beat sheet primeiro

Antes de qualquer código, escreva o plano como uma pequena tabela e mostre ao usuário na mesma mensagem em que você começar a construir. É aqui que o vídeo é ganho ou perdido, porque beats fracos não podem ser corrigidos com animação.

#Linha na telaFrase de destaqueObjetoEntrada / SaídaQuadros

Diretrizes:

  • Beat 1 é o gancho: 2 a 5 palavras, declaradas como uma tensão ou uma afirmação ousada.
  • 6 a 9 beats para 15 a 30 segundos. A maioria dos beats dura de 60 a 110 quadros (2 a 3,5 segundos). Dê mais tempo aos beats de lista (badges, pills): cerca de 20 quadros por item mais 30 para ler.
  • Construa em direção a uma linha de recompensa perto do fim e dê a ela o loop (Círculo).
  • O último beat é o logotipo ou handle, de 45 a 60 quadros.
  • Varie o objeto de beat a beat. Dois beats apenas com texto seguidos é aceitável. Dois beats de badge seguidos não é.
  • Varie as transições: principalmente pushes para cima, um push lateral, um fade antes da recompensa.

3. Configure o projeto

Execute isso uma vez em uma pasta vazia. As fontes são copiadas para o projeto para que a renderização nunca dependa da rede.

npm init -y
npm i remotion @remotion/cli @remotion/fonts react react-dom lucide-react @fontsource/inter @fontsource/instrument-serif
npm i -D typescript @types/react
mkdir -p src public/fonts out
cp node_modules/@fontsource/inter/files/inter-latin-400-normal.woff2 node_modules/@fontsource/inter/files/inter-latin-500-normal.woff2 node_modules/@fontsource/instrument-serif/files/instrument-serif-latin-400-italic.woff2 public/fonts/

tsconfig.json:

{"compilerOptions":{"target":"ES2022","module":"ESNext","moduleResolution":"Bundler","jsx":"react-jsx","strict":true,"skipLibCheck":true,"noEmit":true,"esModuleInterop":true},"include":["src"]}

src/index.ts:

import {registerRoot} from 'remotion';
import {Root} from './Root';
registerRoot(Root);

src/Root.tsx:

import React from 'react';
import {Composition} from 'remotion';
import {totalFrames} from './kit';
import {Video, beats} from './Video';

export const Root: React.FC = () => (
  <Composition id="Reel" component={Video} width={1080} height={1920} fps={30} durationInFrames={totalFrames(beats)} />
);

4. Adicione o kit de motion

Crie src/kit.tsx com exatamente este código. Ele foi renderizado e verificado, então copie-o como está e altere apenas a linha do tema. Construa novas peças ao lado dele no mesmo espírito.

import React from 'react';
import {
  AbsoluteFill, Easing, Sequence, interpolate, interpolateColors, random, spring,
  staticFile, useCurrentFrame, useVideoConfig,
} from 'remotion';
import {loadFont} from '@remotion/fonts';

// As fontes são arquivos locais (copiados do @fontsource na configuração), então as renderizações nunca precisam da rede.
const font = (family: string, file: string, weight: string, style = 'normal') =>
  loadFont({family, url: staticFile(`fonts/${file}.woff2`), weight, style});
font('Inter', 'inter-latin-400-normal', '400');
font('Inter', 'inter-latin-500-normal', '500');
font('Instrument Serif', 'instrument-serif-latin-400-italic', '400', 'italic');
const sans = 'Inter, sans-serif';
const serif = '"Instrument Serif", serif';

// ---- Tema: um fundo, uma tinta, um cinza, UM destaque. Troque o destaque por marca (hex de 6 dígitos). ----
export const T = {bg: '#FFFFFF', ink: '#0B0B0C', muted: '#B4B6BB', accent: '#1FB81A', sans, serif};

const CL = {extrapolateLeft: 'clamp', extrapolateRight: 'clamp'} as const;
const c01 = (n: number) => Math.min(1, Math.max(0, n));
const whip = Easing.bezier(0.76, 0, 0.18, 1); // lento, rápido, lento: o "chicote de câmera"

/** Mola de 0 a 1 que começa em `delay`. Suave por padrão; passe uma configuração para bounce. */
export const useEnter = (delay = 0, config: object = {damping: 200}) => {
  const frame = useCurrentFrame();
  const {fps} = useVideoConfig();
  return spring({frame: frame - delay, fps, config});
};

// ---- Linha do tempo: as cenas se sobrepõem por OVERLAP quadros para que a saída e a entrada aconteçam juntas ----
export const OVERLAP = 12;
export type Beat = {dur: number; el: React.ReactNode};
export const beatStarts = (beats: Beat[], overlap = OVERLAP) =>
  beats.map((_, i) => beats.slice(0, i).reduce((s, x) => s + x.dur - overlap, 0));
export const totalFrames = (beats: Beat[], overlap = OVERLAP) =>
  beats.reduce((s, b) => s + b.dur, 0) - overlap * (beats.length - 1);

export const Timeline: React.FC<{beats: Beat[]; overlap?: number}> = ({beats, overlap = OVERLAP}) => {
  const starts = beatStarts(beats, overlap);
  return (
    <AbsoluteFill style={{background: T.bg}}>
      {beats.map((b, i) => (
        <Sequence key={i} from={starts[i]} durationInFrames={b.dur}>
          {b.el}
        </Sequence>
      ))}
    </AbsoluteFill>
  );
};

// ---- Cena: envolve uma ideia. Lida com o push/fade de entrada e saída, desfoque de movimento, zoom lento. ----
type Dir = 'up' | 'down' | 'left' | 'right' | 'fade';
const VEC: Record<Dir, [number, number]> = {up: [0, -1], down: [0, 1], left: [-1, 0], right: [1, 0], fade: [0, 0]};

/** `enter="up"` = conteúdo viaja para cima até o lugar. Para um push, dê à cena A exit="up" e à cena B enter="up". */
export const Scene: React.FC<{
  enter?: Dir; exit?: Dir; t?: number; zoom?: number; children: React.ReactNode;
}> = ({enter = 'fade', exit = 'fade', t = OVERLAP, zoom = 0.05, children}) => {
  const frame = useCurrentFrame();
  const {width, height, durationInFrames: dur} = useVideoConfig(); // = duração desta Sequência
  const id = React.useId().replace(/:/g, '');
  const pos = (f: number) => {
    const a = 1 - whip(c01(f / t));
    const b = whip(c01((f - (dur - t)) / t));
    const [ex, ey] = VEC[enter];
    const [lx, ly] = VEC[exit];
    return [(-ex * a + lx * b) * width, (-ey * a + ly * b) * height];
  };
  const [x, y] = pos(frame);
  const [px, py] = pos(frame - 1);
  const bx = Math.abs(x - px) * 0.35; // desfoque segue a velocidade, como um obturador real
  const by = Math.abs(y - py) * 0.35;
  const fIn = enter === 'fade' ? interpolate(frame, [t * 0.5, t], [0, 1], CL) : 1; // a cena antiga limpa primeiro
  const fOut = exit === 'fade' ? interpolate(frame, [dur - t, dur - t * 0.5], [1, 0], CL) : 1;
  const soft = (1 - fIn * fOut) * 14;
  const filter = bx + by > 0.4 ? `url(#${id})` : soft > 0.2 ? `blur(${soft}px)` : undefined;
  return (
    <>
      <svg width="0" height="0" style={{position: 'absolute'}}>
        <filter id={id} x="-40%" y="-40%" width="180%" height="180%">
          <feGaussianBlur stdDeviation={`${bx} ${by}`} />
        </filter>
      </svg>
      <AbsoluteFill
        style={{
          transform: `translate(${x}px, ${y}px) scale(${1 + (zoom * frame) / dur})`,
          opacity: fIn * fOut, filter,
          fontFamily: T.sans, color: T.ink, alignItems: 'center', justifyContent: 'center',
        }}
      >
        {children}
      </AbsoluteFill>
    </>
  );
};

// ---- Texto ----
/** Revela o texto palavra por palavra (ou letra por letra). O novo texto chega cinza e depois se fixa na tinta.
 *  Envolva a frase-chave em *estrelas* para torná-la o destaque. Use \n para uma quebra de linha. */
export const Words: React.FC<{
  text: string; delay?: number; stagger?: number; by?: 'word' | 'char';
  size?: number; accent?: 'serif' | 'sans'; style?: React.CSSProperties;
}> = ({text, delay = 0, stagger, by = 'word', size = 92, accent = 'serif', style}) => {
  const frame = useCurrentFrame();
  const step = stagger ?? (by === 'word' ? 5 : 1.2);
  let i = 0;
  const unit = (s: string, hot: boolean, key: string) => {
    const local = frame - delay - i++ * step;
    const p = interpolate(local, [0, 6], [0, 1], CL);
    const final = hot ? T.accent : T.ink;
    return (
      <span
        key={key}
        style={{
          display: 'inline-block', whiteSpace: 'pre', opacity: p,
          color: interpolateColors(c01((local - 3) / 9), [0, 1], [T.muted, final]),
          transform: by === 'word' ? `translateY(${(1 - p) * 0.2}em)` : undefined,
          filter: p < 1 ? `blur(${(1 - p) * 7}px)` : undefined,
        }}
      >
        {s}
      </span>
    );
  };
  return (
    <div style={{fontSize: size, fontWeight: 500, letterSpacing: '-0.04em', lineHeight: 1.08, textAlign: 'center', ...style}}>
      {text.split('\n').map((line, li) => (
        <div key={li}>
          {line.split('*').map((seg, si) => {
            const hot = si % 2 === 1;
            const hotStyle: React.CSSProperties = hot && accent === 'serif'
              ? {fontFamily: T.serif, fontStyle: 'italic', fontWeight: 400, fontSize: '1.14em', letterSpacing: '-0.02em'}
              : {};
            return (
              <span key={si} style={hotStyle}>
                {seg.split(/(\s+)/).filter(Boolean).map((w, wi) =>
                  /^\s+$/.test(w) ? <span key={wi} style={{whiteSpace: 'pre'}}>{w}</span>
                  : by === 'word' ? unit(w, hot, `${wi}`)
                  : <span key={wi} style={{display: 'inline-block', whiteSpace: 'nowrap'}}>{[...w].map((ch, ci) => unit(ch, hot, `${wi}-${ci}`))}</span>
                )}
              </span>
            );
          })}
        </div>
      ))}
    </div>
  );
};

/** O hit de abertura: começa enorme e desfocado, ajusta-se ao tamanho em cerca de 8 quadros. */
export const PunchIn: React.FC<{delay?: number; from?: number; children: React.ReactNode}> = ({delay = 0, from = 3, children}) => {
  const s = useEnter(delay, {damping: 200, stiffness: 220});
  return (
    <div style={{transform: `scale(${interpolate(s, [0, 1], [from, 1])})`, opacity: c01(s * 5), filter: s < 0.98 ? `blur(${(1 - s) * 34}px)` : undefined}}>
      {children}
    </div>
  );
};

/** Linha de apoio pequena abaixo do título. */
export const Caption: React.FC<{children: React.ReactNode; delay?: number; width?: number}> = ({children, delay = 0, width = 620}) => {
  const s = useEnter(delay);
  return (
    <div style={{fontSize: 31, lineHeight: 1.28, letterSpacing: '-0.02em', color: '#2B2C2F', textAlign: 'center', width, marginTop: 44, opacity: s, filter: s < 0.98 ? `blur(${(1 - s) * 6}px)` : undefined}}>
      {children}
    </div>
  );
};

// ---- Atmosfera ----
/** Luz de destaque suave atrás da palavra-chave. Coloque-a com x / y como valores CSS. */
export const Glow: React.FC<{x?: string; y?: string; size?: number; delay?: number}> = ({x = '50%', y = '50%', size = 900, delay = 0}) => {
  const s = useEnter(delay);
  const frame = useCurrentFrame();
  return (
    <div style={{position: 'absolute', left: x, top: y, width: size, height: size, borderRadius: '50%', opacity: s,
      transform: `translate(-50%, -50%) scale(${0.6 + 0.4 * s + frame * 0.0012})`,
      background: `radial-gradient(closest-side, ${T.accent}3D, ${T.accent}00)`}} />
  );
};

/** Círculos errantes perto das bordas para que o quadro nunca fique parado. thin = contornos, soft = anéis grossos desfocados. */
export const Rings: React.FC<{seed?: string; variant?: 'thin' | 'soft'; count?: number}> = ({seed = 'a', variant = 'thin', count = 3}) => {
  const frame = useCurrentFrame();
  const {width, height} = useVideoConfig();
  return (
    <AbsoluteFill>
      {Array.from({length: count}).map((_, i) => {
        const r = (k: string) => random(`${seed}-${i}-${k}`);
        const d = 300 + r('d') * 420;
        const cx = (i % 2 ? 0.86 + r('x') * 0.2 : -0.06 + r('x') * 0.2) * width;
        const cy = (r('y') * 1.1 - 0.05) * height;
        const ang = r('a') * Math.PI * 2;
        const s = spring({frame: frame - i * 4, fps: 30, config: {damping: 200}});
        return (
          <div key={i} style={{position: 'absolute', width: d, height: d, borderRadius: '50%', opacity: s,
            left: cx - d / 2 + Math.cos(ang) * frame * 0.9, top: cy - d / 2 + Math.sin(ang) * frame * 0.9,
            border: variant === 'thin' ? `2px solid ${T.accent}99` : `${d * 0.2}px solid ${T.accent}1F`,
            filter

…

[truncado para tradução]

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

Habilidades Relacionadas