name: product-demo-video description: Esta skill debe usarse cuando el usuario solicite "hacer un video de demostración de producto", "crear un video de demostración de aplicación", "construir un video de anuncio de función", "hacer una demostración SaaS", "realizar un video de recorrido de interfaz", "animar capturas de pantalla de producto en una demostración", "agregar un cursor animado/clic a una captura de pantalla" o "acercar una parte de la interfaz". Cubre la transformación de capturas de pantalla/diseños estáticos (no grabaciones de pantalla) en una demostración animada y limpia: marcos de dispositivo/navegador, cursor animado + ondulación de clic, zoom/paneo a la interfaz focal, anotaciones tipo llamada, ritmo de pasos, transiciones entre pantallas y subtítulos de funciones. version: 0.1.0
Video de Demostración de Producto
Convierte capturas de pantalla o diseños estáticos de producto en una demostración animada y pulida — sin grabador de pantalla, sin cursor real tembloroso, sin desfase de versión de interfaz. Enmarca cada pantalla, desliza un cursor sintético hasta el píxel exacto, ondula el clic, luego acerca la parte que importa y subtitula la función. Como la entrada es estática y el movimiento es código, cada valor es determinista y se re-renderiza idénticamente cuando cambia la captura de pantalla.
Cuándo usarlo
- Demostración de aplicación / SaaS, anuncio de función, recorrido de incorporación (15–90s).
- Mostrar una ruta de clics a través de una interfaz de la que solo tienes capturas de pantalla/diseños.
- Destacar una región focal de un panel denso (zoom a región + llamada).
Por qué las capturas de pantalla superan a las grabaciones de pantalla
| Grabación de pantalla | Impulsada por capturas (esta skill) |
|---|---|
| El cursor real se desvía, sobrepasa, tiembla | El cursor sigue una ruta suavizada hasta un objetivo exacto |
| Re-grabar toda la toma para arreglar un paso | Intercambiar un PNG, re-renderizar el mismo código |
| La interfaz cambia durante la toma, el ratón busca | Cada pantalla es un activo limpio de estado final |
| Difícil de acercar sin pixelación | Exportar capturas a 2× (DPR 2) → zooms nítidos |
Captura las entradas al doble del tamaño de visualización (retina / deviceScaleFactor: 2) para que un zoom de 2× se mantenga nítido. Consulta references/screenshot-demo.md para un script de captura con Playwright.
El bucle de demostración
Cada demostración es el mismo ritmo, repetido por función. Mantén el tiempo suficiente para leer, muévete con intención, aterriza el clic, revela el resultado.
| Ritmo | Duración | Trabajo |
|---|---|---|
| Permanecer en pantalla | 0.5–1s | Dejar que el espectador se oriente antes de cualquier movimiento |
| El cursor viaja al objetivo | 0.4–0.8s | Movimiento suavizado (ease-in-out), no lineal |
| Ondulación de clic + cambio de estado | 0.2–0.3s | Retroalimentación táctil; la pantalla reacciona en el mismo fotograma |
| Zoom a región focal | 1–2s de entrada, mantener 2–4s | 2× es el punto ideal — suficiente detalle, mantiene el contexto |
| Subtitular la función | mantener con el zoom | Una frase de beneficio, ej. "Exportación con un clic" |
Mueve la cámara o el cursor, rara vez ambos a la vez — dos movimientos simultáneos dividen la atención.
Enmarcar la captura de pantalla
Una captura de pantalla cruda se lee como un informe de error; una enmarcada se lee como producto. Envuelve cada pantalla en un navegador o dispositivo con marco sobre un fondo con marca.
// Marco de navegador — coloca una captura, obtén una ventana creíble
const BrowserFrame = ({ url, children }) => (
<div style={{ borderRadius: 12, overflow: "hidden", background: "#1e1e22",
boxShadow: "0 40px 80px rgba(0,0,0,.35)" }}>
<div style={{ height: 40, display: "flex", alignItems: "center", gap: 8, padding: "0 14px" }}>
{["#ff5f57", "#febc2e", "#28c840"].map((c) => (
<span key={c} style={{ width: 12, height: 12, borderRadius: "50%", background: c }} />
))}
<div style={{ marginLeft: 12, flex: 1, height: 24, borderRadius: 6, background: "#2b2b30",
color: "#9aa", fontSize: 13, display: "flex", alignItems: "center", padding: "0 10px" }}>
{url}
</div>
</div>
<div style={{ background: "#fff" }}>{children}</div>
</div>
);
Coloca una URL real en la barra de direcciones (autenticidad), agrega una sombra suave y un fondo degradado. Para móvil, cambia a un marco de teléfono con muesca y esquinas redondeadas. Mantén el estilo del marco en un solo objeto de tema para que todas las pantallas coincidan.
Cursor animado + ondulación de clic (el movimiento principal)
Impulsa la posición desde el fotograma actual, nunca desde una transición CSS o reloj de pared — un renderizador de video necesita que cada fotograma sea una función pura del número de fotograma, o el cursor se desincroniza y parpadea.
import { useCurrentFrame, interpolate, spring, useVideoConfig } from "remotion";
// Viaje suavizado de `from` a `to` entre startFrame y startFrame+dur
const useCursor = (from, to, startFrame, dur) => {
const frame = useCurrentFrame();
const t = interpolate(frame, [startFrame, startFrame + dur], [0, 1],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: (x) => x * x * (3 - 2 * x) }); // smoothstep
return { x: from.x + (to.x - from.x) * t, y: from.y + (to.y - from.y) * t };
};
export const Cursor = ({ from, to, startFrame, dur, clickAt }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const { x, y } = useCursor(from, to, startFrame, dur);
// La ondulación se expande una vez, en el fotograma de clic
const since = frame - clickAt;
const showRipple = since >= 0 && since < 18;
const r = spring({ frame: since, fps, config: { damping: 18 }, durationInFrames: 18 });
return (
<>
{showRipple && (
<div style={{ position: "absolute", left: x, top: y, transform: "translate(-50%,-50%)",
width: 12 + r * 60, height: 12 + r * 60, borderRadius: "50%",
border: "2px solid rgba(80,140,255,.9)", opacity: 1 - r }} />
)}
<svg width="28" height="28" viewBox="0 0 28 28"
style={{ position: "absolute", left: x, top: y, filter: "drop-shadow(0 2px 3px rgba(0,0,0,.4))" }}>
<path d="M3 2 L3 21 L9 15 L13 24 L16 23 L12 14 L20 14 Z" fill="#fff" stroke="#222" strokeWidth="1.4" />
</svg>
</>
);
};
El cursor usa smoothstep para acelerar desde el reposo y desacelerar hacia el objetivo (un cursor lineal se lee como robótico). Activa la ondulación y el cambio de estado de la pantalla en el mismo fotograma clickAt para que causa y efecto coincidan. Consulta references/screenshot-demo.md para la composición completa que conecta múltiples clics en múltiples pantallas.
Zoom a una región (Ken Burns para interfaz)
Para acercar un elemento focal, escala la pantalla y establece transform-origin en el centro de ese elemento, para que el zoom converja en él en lugar del centro del fotograma.
const ZoomToRegion = ({ target, scale = 2, startFrame, dur, children }) => {
const frame = useCurrentFrame();
const s = interpolate(frame, [startFrame, startFrame + dur], [1, scale],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: (x) => 1 - Math.pow(1 - x, 3) }); // easeOutCubic
return (
<div style={{ transform: `scale(${s})`, transformOrigin: `${target.x}px ${target.y}px`,
willChange: "transform" }}>
{children}
</div>
);
};
Limita el zoom a alrededor de 2× (más de 3× pierde contexto y desorienta). Mantén el estado de zoom 2–4s — suficiente para leer el detalle — luego regresa suavemente antes de moverte a la siguiente pantalla. Combina el zoom con una llamada, nunca en bruto.
Anotaciones tipo llamada y subtítulos de funciones
Anota el punto de inflexión, luego déjalo claro. Una llamada es una etiqueta + un conector (flecha, círculo o subrayado) que apunta al elemento focal.
- Destaca el elemento con un anillo suave o un entorno atenuado (
box-shadow: 0 0 0 9999px rgba(0,0,0,.45)corta un agujero de foco). - Subtitula un beneficio por ritmo — frase nominal corta ("Análisis en tiempo real"), no una oración. Entra en el zoom, sale antes del siguiente paso.
- Mantén los subtítulos en el tercio inferior y dentro del área segura para que sobrevivan a recortes a 9:16.
Consulta references/annotation-and-transitions.md para máscaras de foco, conectores de flecha y una pista de subtítulos tipificada.
Transiciones entre pantallas
Entre pasos, mueve las pantallas con un lenguaje consistente. Elige uno y mantente:
| Transición | Sensación | Uso |
|---|---|---|
| Empujar / deslizar | movimiento "siguiente paso" hacia adelante | Recorridos lineales |
| Cruz-zoom | impulso, similar a aplicación | Saltar a una vista de detalle |
| Cruz-desvanecer | tranquilo, neutral | Cambiar contexto (configuración → panel) |
| Corte de coincidencia en un elemento compartido | sin costuras | La misma tarjeta persiste entre pantallas |
Mantén la pantalla saliente hasta que se resuelva su clic, luego transiciona — nunca cortes a mitad del movimiento. Consulta references/annotation-and-transitions.md.
Lista de verificación de salida
- Entradas capturadas a 2× (DPR 2); los zooms se mantienen nítidos a 2×.
- Cada valor de cursor/zoom es una función pura de
useCurrentFrame()— sin transiciones CSS para estado animado. - El cursor viaja en una ruta suavizada (smoothstep); la ondulación y el cambio de estado se activan en el mismo fotograma de clic.
- Cada pantalla enmarcada (navegador/dispositivo) con una URL real y tema consistente.
- Un movimiento a la vez (cámara o cursor); zoom ≤ 2×, mantenido 2–4s.
- Un subtítulo de beneficio por ritmo, en el tercio inferior seguro; las transiciones usan un lenguaje consistente.
Entregar y verificar (fotos fijas renderizadas → MP4)
Helper empaquetado (
scripts/): organiza tus fotos fijas conscripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.png, luego verifica la codificación conscripts/probe-mp4.sh out.mp4 [WxH] [fps]. Consultascripts/README.md.
Esto es de nivel pesado: el entregable es un MP4, no una página en vivo. La demostración ingiere activos del usuario — capturas de pantalla/diseños, logo opcional — por lo que la verificación es principalmente si la imagen correcta realmente se cargó y aterrizó en el fotograma.
Contrato de salida:
- Un proyecto de Remotion con la composición registrada (
<Composition>+ zodschema+defaultProps); cada valor de cursor/zoom/transición es una función pura deuseCurrentFrame()(sin transiciones CSS, temporizadores,Date.now()). - Capturas de pantalla cargadas mediante
staticFile(), controladas condelayRender/continueRenderpara que el PNG a 2× (y cualquier fuente web) esté presente antes de que se renderice el fotograma — de lo contrario, la pantalla aparece en blanco a mitad de demostración. - Entregable = el
out/*.mp4renderizado más el proyecto (re-renderizar cuando cambie la captura de pantalla).
Bucle de verificación — renderizar fotos fijas → inspeccionar → codificar. PNGs baratos primero, video solo cuando estén correctos.
# Fotos fijas exactas al fotograma CON LAS PROPS QUE ENVIARÁS (rutas de captura reales), no defaultProps
npx remotion still ProductDemo out/f-settle.png --frame=15 --props=demo.json # pantalla enmarcada, antes del movimiento
npx remotion still ProductDemo out/f-click.png --frame=90 --props=demo.json # cursor en objetivo + ondulación
npx remotion still ProductDemo out/f-zoom.png --frame=150 --props=demo.json # zoom a región focal + subtítulo
# fotograma final = durationInFrames - 1 (npx remotion compositions lo lee)
Inspecciona cada PNG en busca de fidelidad (captura de pantalla correcta cargada; cursor en el píxel exacto; ondulación de clic y cambio de estado en el mismo fotograma; texto de subtítulo correcto) Y artefactos (imagen en blanco/no cargada, pantalla fuera del lienzo, zoom pixelado o sobrepasando el fotograma, subtítulo fuera del tercio inferior seguro, relación de aspecto/letterboxing incorrecto).
# Solo después de que las fotos fijas estén limpias:
npx remotion render ProductDemo out/demo.mp4 --props=demo.json
npx remotion render ProductDemo out/demo.gif --props=demo.json --codec=gif # Prueba de primera pantalla para README
Por lotes (una plantilla, muchos productos): al re-skinner la demostración por producto, verifica las props de UN producto representativo mediante fotos fijas antes de renderizar el catálogo por lotes — atrapa un error de imagen en blanco o fuera del lienzo una vez, no N veces.
Antes de terminar:
- Las fotos fijas se renderizan limpiamente en permanencia / clic / final — sin errores, las capturas de pantalla realmente se cargaron (no en blanco).
- El cursor aterriza en el objetivo exacto; ondulación + cambio de estado en el mismo fotograma; texto de subtítulo correcto y en el tercio inferior seguro.
- Todo el movimiento impulsado por fotogramas — sin transiciones CSS / temporizadores /
Date.now()/Math.random(). - Las props enviadas (rutas de captura reales) se renderizan correctamente, no solo
defaultProps. - MP4 completo codificado y se reproduce; (opcional) GIF renderizado para el README.
Archivos de referencia
references/screenshot-demo.md— una demostración completa y ejecutable de Remotion: captura de capturas de pantalla a 2× con Playwright, los componentes de marco de navegador/teléfono, una composición de varios pasos que mueve un cursor a través de varias pantallas con ondulaciones de clic y un comando de renderizado.references/annotation-and-transitions.md— máscaras de foco, conectores de flecha y una pista de subtítulos tipificada, además de estilos de transición entre pantallas.