Communitygithub.com

hiwurun/remotion-saas-boilerplate

Estructura el marcado de Remotion para hacerlo interactivo

¿Qué es remotion-saas-boilerplate?

Este skill detalla cómo escribir marcado de Remotion de manera que el Remotion Studio pueda reconocer la estructura del código y hacerlo interactivo, permitiendo seleccionar elementos al hacer clic, arrastrar y soltar, redimensionar, rotar, editar estilos CSS y modificar keyframes y valores de easing. Se enfoca en prácticas como usar componentes interactivos con sus propias líneas de tiempo mediante `Interactive.withSchema()`, mantener los estilos CSS en línea, animar con `interpolate()` directamente en las propiedades, y dar a cada elemento editable su propio nodo JSX para que el Studio pueda editarlos de forma independiente. El skill está dirigido a desarrolladores que usan Remotion para crear videos programáticos con una interfaz visual interactiva y cubre plataformas como Remotion Studio y composiciones conectadas. Su documentación incluye ejemplos detallados de código sobre cómo estructurar el marcado, elegir campos de esquema, registrar componentes reutilizables y manejar el tiempo directamente en los componentes.

Compatible con~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/hiwurun/remotion-saas-boilerplate/tree/HEAD/.agents/skills/remotion-interactivity

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Vista previa

Del README del skill

remotion-saas-boilerplate preview

Documentación

Al escribir el marcado de Remotion de una manera específica, el Remotion Studio puede reconocer la estructura del código y hacerlo interactivo:

  • Permitir seleccionar elementos haciendo clic en ellos
  • Permitir arrastrar y soltar, redimensionar y rotar
  • Editar los estilos CSS
  • Hacer que los keyframes y valores de easing sean editables

Si el marcado es demasiado complejo para que el Studio lo haga interactivo, los valores se vuelven grises.

Preferir componentes interactivos con sus propias líneas de tiempo

Usa Interactive.withSchema({wrapInSequence: true, layout: 'absolute-fill'}) para componentes que llenan su contenedor padre. El wrapper proporciona su raíz visual y elemento de línea de tiempo, y recorta el desbordamiento por defecto, para que el componente interno pueda devolver un fragment. Coloca el tiempo, premontaje, dimensiones y estilos por instancia en el componente exportado.

Mantén los elementos visuales que pertenecen juntos en el mismo componente, para que se muevan y recorten juntos. Un contenedor de diseño interno aún puede proporcionar alineación, recorte dentro de un área más pequeña o un grupo animado; un contenedor que solo llena la escena generalmente es innecesario. Las tarjetas, cuadros de texto y SVG con sus propios límites pueden mantener el layout: 'none' por defecto y reenviar style a su raíz visual.

import {Video} from '@remotion/media';
import type React from 'react';
import {Interactive, useVideoConfig, type InteractivitySchema} from 'remotion';

type SceneProps = {
  readonly title: string;
};

const SceneInner: React.FC<SceneProps> = ({title}) => {
  const {fps} = useVideoConfig();

  return (
    <>
      <Video
        name="Background footage"
        src="https://remotion.media/video.mp4"
        premountFor={fps}
        objectFit="cover"
        style={{width: '100%', height: '100%'}}
      />
      <Interactive.Div
        name="Scene title"
        style={{
          position: 'absolute',
          left: 80,
          bottom: 80,
          color: 'white',
          fontFamily: 'Helvetica, Arial, sans-serif',
          fontSize: 80,
        }}
      >
        {title}
      </Interactive.Div>
    </>
  );
};

const sceneSchema = {
  title: {type: 'string', default: '', description: 'Title'},
} as const satisfies InteractivitySchema;

export const Scene = Interactive.withSchema({
  Component: SceneInner,
  componentName: 'Scene',
  schema: sceneSchema,
  wrapInSequence: true,
  layout: 'absolute-fill',
});

Elegir campos de esquema

Cada clave en el esquema es una prop que Studio puede leer y editar en el lugar de uso. Las props que no están en el esquema aún funcionan, pero no son editables en Studio. Las claves pueden usar notación de puntos, como style.color.

TipoUsoCon keyframes
stringCadenas de texto, como un título, mensaje o fragmento de códigoNo
numberNúmeros, con min, max, step, integer opcionalesSí
booleanInterruptores de encendido/apagadoSí (mantener)
colorCadenas de color CSSSí
enumUna elección entre variants, cada una con su propio esquema anidadoOpt-in (mantener)
arrayListas de números, colores, enums y másNo
assetFuentes de medios, con assetType opcionalNo
font-familyFamilia tipográfica CSSNo
font-weightPeso de fuenteSí
translate, scale, rotation-css, rotation-degrees, transform-originTransformacionesSí
uv-coordinateUn punto normalizado [x, y] en el elementoSí
svg-pathDatos de ruta SVGSí
remotion-captionsDatos Caption[]No
hiddenUn valor que se mantiene fuera de los controles—

Cada campo necesita un default y debe tener un description, que Studio muestra como la etiqueta. Establece keyframable: false para mostrar un control estático.

Expón el contenido y la apariencia por instancia, como el texto y un color de acento. No agregues campos para style.translate, style.scale, style.rotate, style.transformOrigin y style.opacity: wrapInSequence: true ya los añade.

Para hacer que las propiedades CSS de la raíz sean editables por instancia, extiende los fragmentos de esquema integrados:

const cardSchema = {
  ...Interactive.textSchema, // style.color, style.fontSize, style.fontWeight, ...
  ...Interactive.backgroundSchema, // style.backgroundColor
  ...Interactive.borderRadiusSchema, // style.borderRadius, ...
} as const satisfies InteractivitySchema;

También disponibles: Interactive.borderSchema, y Interactive.captionsSchema para componentes que aceptan subtítulos. Para componentes que aceptan hijos React.ReactNode, agrega ...Interactive.childrenSchema. Proporciona controles para el contenido de los hijos y puede evolucionar para soportar contenido más rico. Actualmente, usa un campo de cadena etiquetado como "Texto" con un valor predeterminado de ''. Si un componente requiere solo hijos de cadena, declara un campo children explícito con type: 'string'. Interactive.textSchema controla los estilos tipográficos; childrenSchema controla el contenido. El fragmento de hijos no está incluido en baseSchema ni se agrega automáticamente a componentes personalizados. El marcado anidado y los hijos calculados actualmente son de solo lectura. text-content es un alias obsoleto para el tipo de campo string. Consulta InteractivitySchema para todas las opciones.

Registrar componentes reutilizables como composiciones conectadas

Registra escenas sustanciales y componentes reutilizables con sus propias capas o animación como composiciones independientes. Studio las llama composiciones conectadas: pueden abrirse en su propia línea de tiempo mientras comparten la misma implementación del componente con el video padre.

Define los estilos requeridos y carga las fuentes dentro del componente para que funcione sin su padre. Consulta independencia del padre.

Registra la misma referencia del componente exportado que el padre renderiza. Para el ejemplo anterior, usa component={Scene}, no SceneInner o un wrapper en línea. Con wrapInSequence: true, no se necesita un <Sequence> adicional para la conexión.

import {Composition} from 'remotion';
import {Scene} from './Scene';

export const RemotionRoot = () => (
  <Composition
    id="Scene"
    component={Scene}
    width={1280}
    height={720}
    fps={30}
    durationInFrames={30}
    defaultProps={{
      title: 'A new perspective',
    }}
  />
);

Elige un ID único, valores predeterminados representativos en línea, y dimensiones, fps y duración adecuados para previsualizar el componente. Los valores predeterminados de registro no copian automáticamente las props de una instancia padre. Consulta composiciones conectadas para detalles de registro y extracción.

Colocar el tiempo directamente en el componente

Evita un <Sequence> separado alrededor de un componente que ya acepta props de tiempo. Los componentes interactivos integrados y los componentes personalizados creados con wrapInSequence: true pueden recibir name, from, durationInFrames, trimBefore, playbackRate, y premountFor directamente.

<Scene
  name="Opening scene"
  from={30}
  durationInFrames={90}
  premountFor={fps}
  style={{opacity: 0.9}}
  title="A new perspective"
/>

Al eliminar una secuencia redundante, mueve su tiempo y nombre al hijo. Preserva cualquier tiempo existente del hijo; no sobrescribas ni apliques dobles desplazamientos. Una llamada a useCurrentFrame() dentro del componente lee su fotograma local, mientras que una expresión de estilo en el lugar de uso aún usa el fotograma del llamante. Preserva esa distinción al mover estilos de animación.

Mantén un <Sequence> cuando proporciona tiempo compartido para múltiples hermanos, envuelve un componente que no maneja tiempo, o suministra dimensiones que el hijo no puede aceptar directamente. Las raíces administradas también aceptan width y height para useVideoConfig(). Mantén <TransitionSeries.Sequence> para diseño consecutivo, con o sin transiciones; las props from directas no reemplazan esos comportamientos.

Dar a cada elemento editable su propio nodo JSX

El Studio edita el nodo fuente JSX que creó un elemento. Si múltiples elementos en tiempo de ejecución provienen del mismo nodo JSX, comparten un único objetivo de edición de fuente.

Para cada registro de composición, clip, escena, capa o secuencia que deba ser editable por sí mismo, escribe un nodo JSX separado y mantén sus props editables en ese nodo. Esto aplica a <Composition>, <Still>, componentes multimedia integrados, <Sequence>, <TransitionSeries.Sequence> y componentes personalizados.

Por ejemplo, crea una línea de tiempo editable así:

import { TransitionSeries } from "@remotion/transitions";

<TransitionSeries>
  <TransitionSeries.Sequence name="Introduction" durationInFrames={90}>
    <Introduction />
  </TransitionSeries.Sequence>
  <TransitionSeries.Sequence name="Demo" durationInFrames={150}>
    <Demo />
  </TransitionSeries.Sequence>
</TransitionSeries>

Un .map() u otro bucle programático crearía múltiples elementos en tiempo de ejecución a partir de un solo nodo fuente JSX. Eso es apropiado para salida repetida que se controla intencionalmente como una plantilla, como barras de visualización o partículas. No es apropiado cuando las instancias necesitan IDs o nombres independientes, props, metadatos, tiempo, orden, eliminación o duplicación en el Studio.

Hacer un elemento HTML interactivo usando Interactive

Cada elemento HTML y SVG (excepto <Img>, que ya es interactivo) como <div> se puede convertir en interactivo usando Interactive:

<Interactive.Div
  name="Greeting card"
  style={{fontSize: 80, padding: 24}}
>
  Hello
</Interactive.Div>

Esto permite establecer estilos y keyframes en el Studio. Sé sensato; si un componente tiene muchos elementos, la línea de tiempo podría desordenarse.

Preferir texto en línea

Si el texto es fijo y solo se usa una vez, escríbelo directamente dentro del elemento interactivo en lugar de extraerlo en una constante.

// 👍 El texto fijo permanece editable
<Interactive.Div name="Title">
  Remotion Best Practices
</Interactive.Div>

Usa una prop o variable solo cuando el texto es dinámico o se reutiliza.

Dar a los elementos interactivos un nombre descriptivo

Agrega una prop name a los elementos para hacerlos fácilmente identificables. Evita nombres calculados; escríbelos directamente.

<>
  <Interactive.Div name="Hero title" style={{fontSize: 80}}>
    Launch day
  </Interactive.Div>
  <Img name="Avatar" src="https://remotion.media/image.jpeg" />
  <Video name="Background" src="https://remotion.media/video.mp4" />
  <Interactive.Div name="Title">
    Launch day
  </Interactive.Div>
</>

Mantener todos los estilos CSS en línea

La mejor manera es pasar un objeto simple a style - sin referencias a constantes, sin propagación de objetos, sin operaciones matemáticas.

<Interactive.Div
  style={{
    fontSize: 80,
    color: 'red',
  }}
>
  Hello World!
</Interactive.Div>
const baseStyle = useMemo(() => {
  return {
    fontSize: 12 // ❌ Los estilos no en línea no son compatibles
  }
}, []);

<Interactive.Div
  style={{
    ...baseStyle, // ❌ La propagación no es compatible
    color: RED, // ❌ Las referencias a constantes no son compatibles
    scale: frame * 10 // ❌ Las operaciones matemáticas no son compatibles
  }}
>
  Hello World!
</Interactive.Div>

Animar usando interpolate()

Escribe animaciones como llamadas a interpolate() en línea sobre la propiedad que cambia. El rango de salida, easing, extrapolación y la propiedad output deben usar valores fijos.

El rango de entrada puede usar adicionalmente durationInFrames, fps, width y height desestructurados directamente de useVideoConfig(). Identificadores simples como durationInFrames, multiplicación con un número como 2 * fps o fps * 2, y resta de un número como durationInFrames - 1 son compatibles.

const {fps, durationInFrames} = useVideoConfig();

// 👍 Los valores en línea se pueden estandarizar y animar con keyframes
<Interactive.Div
  name="Product card"
  style={{
    color: 'white',
    fontSize: 80,
    scale: interpolate(frame, [0, fps], [0, 1], {
      easing: Easing.spring({damping: 200}),
      output: 'perceptual-scale',
      extrapolateLeft: 'clamp',
      extrapolateRight: 'clamp'
    }),
    rotate: interpolate(frame, [0, 1 * fps], ['0deg', '20deg'], {
      easing: Easing.spring({damping: 200}),
      extrapolateLeft: 'clamp',
      extrapolateRight: 'clamp'
    }),
    translate: interpolate(
      frame,
      [durationInFrames - 30, durationInFrames],
      ['0px 0px', '0px 120px'],
      {
        easing: Easing.spring({damping: 200}),
        extrapolateLeft: 'clamp',
        extrapolateRight: 'clamp'
      }
    )
  }}
>
  Product card content
</Interactive.Div>

Usa interpolate() en línea, no en variables:

const {fps} = useVideoConfig();
const scale = interpolate(frame, [0, fps], [0, 1], {
  easing: Easing.spring({damping: 200}),
});

<Interactive.Div
  name="Product card"
  style={{
    scale, // ❌ El valor calculado no se puede hacer interactivo
  }}
>
  {children}
</Interactive.Div>

Cada interpolación en línea debe ser sintácticamente independiente: una llamada a función con arrays literales y una opción de easing literal. No anides interpolaciones dentro de objetos de estilo que ya están en una variable, o dentro de un array de estilo.

Sintaxis estandarizada.

Formatea la llamada de esta manera para que el analizador la reconozca:

interpolate(frame, [inputStart, inputEnd], [outputStart, outputEnd], {
  easing: Easing.tipo({...params}),
  extrapolateLeft: 'valor',
  extrapolateRight: 'valor',
  output?: 'valor',
})

Donde tipo es uno de: spring, bezier, ease, circle, cubic, elastic, exp, poly, quad, sin, back, bounce, linear, out, in, inOut, outIn.

Cada tipo tiene sus propias opciones de easing. Los parámetros deben ser valores literales. No uses variables o expresiones dentro de Easing.XXX(). Las únicas excepciones son Easing.out(Easing.tipo), Easing.in(Easing.tipo), Easing.inOut(Easing.tipo) y Easing.outIn(Easing.tipo), donde tipo es uno de los tipos de easing anteriores.

extrapolateLeft y extrapolateRight pueden ser 'clamp' o 'extend', Opción output: 'perceptual-scale' y 'number-as-string'.

Si una interpolación no se reconoce como estandarizada, aparece atenuada.

Exportar el método como una habilidad de IA

Para que otros LLM puedan aplicar esta habilidad, expórtala como una constante:

// ... tus componentes aquí...

export const aiSkill = {
  name: 'Estructurar el marcado de Remotion para la interactividad',
  globs: ['**/*.tsx', '**/*.ts'],
  context: {
    reason: 'El usuario está trabajando en un proyecto de Remotion.',
  },
};

El usuario no necesita hacer esto. Es para mantener un registro de cómo se puede usar esta habilidad.

Lectura adicional

Individual skills in this repo

This repo contains 2 individual skills — each has its own dedicated page.

Skills relacionados