Landing fly-over
Una película fija y artísticamente dirigida de una página de aterrizaje. Cada fotograma es una función pura del tiempo (seek(t) en WebGL2), capturada con Playwright, renderizada en la GPU real con desenfoque de movimiento de 16 subfotogramas y codificada con ffmpeg. Sin editor de vídeo, sin línea de tiempo, sin cortes.
Necesitas ejecutar código en la máquina del usuario. Si no puedes (una superficie solo de chat), dilo de inmediato: puedes describir la película, pero el MP4 necesita un agente de codificación con un shell (Claude Code, Codex, Cursor…).
0. Solicita las entradas
Pregunta una vez, en un solo mensaje:
- Enlace a la página de aterrizaje.
- Nombre de la marca y logotipo (un PNG cuadrado o una ruta SVG; busca primero en el repositorio:
logo*,favicon*,brand*). - Un cierre de dos partes: una declaración corta, luego una cola corta en cursiva. Mantén la línea 1 por debajo de ~22 caracteres (se muestra a 74px en un fotograma de 1440px). Ofrece 2–3 opciones extraídas del texto de la propia página.
- Una pista de 15 segundos (opcional). Si no tiene, ofrécete a buscar una libre de regalías (la licencia gratuita de Mixkit permite uso comercial sin atribución; descarga desde
https://assets.mixkit.co/music/<id>/<id>.mp3). Nunca subas una pista descargada a un repositorio; solo el MP4 renderizado.
1. Configuración
cp -R <this-skill-dir>/engine ./flyover && cd flyover
npm install && npx playwright install chromium # se prefiere Chrome instalado automáticamente si está presente
cp config.example.json config.json
Requiere Node 18+, ffmpeg en PATH, y Python 3 con numpy para la partitura (opencv-python-headless para la comprobación de flujo). Coloca el logotipo junto a config.json y define url, brand, logo y title.
2. Inspecciona, luego elige qué se eleva
node inspect.mjs # → dom.txt (árbol de secciones con posiciones en la página) + inspect.png
Lee dom.txt y completa config.json:
hero.section: la sección hero.hero.mockup: el marco del teléfono/producto/vídeo en el hero (la película comienza enfocada en él y la tarjeta final corta justo debajo de él).hero.tileAnchor: el último botón del hero (las cuatro fichas del logotipo comienzan junto a él).hero.footerTitle: el último encabezado grande antes del pie de página (aparece nítido a los 8.0s).layers: piezas que se levantan, como{ "sel", "role" }, opcionalmente sobrescribiendoz(elevación en reposo),zMax(elevación cuando pasa la cámara) ystand(grados que se inclina hacia el objetivo). Roles y valores predeterminados:
| rol | uso para | z → zMax · stand |
|---|---|---|
button | botones hero / CTA | 6 → 28 · 0° |
chip | insignias, píldoras, pestañas, logotipos pequeños | 4 → 64 · 30° |
mockup | el marco del teléfono / producto / vídeo del hero | 10 → 24 · 0° |
card | tarjetas de características, tablas, terminales, celdas de métricas | 6 → 30 · 0° |
photo | imágenes dentro de tarjetas, capturas de pantalla, muestras de código | 8 → 115 · 46° |
flychip | 1–2 chips pequeños que vuelan más allá de los bordes del fotograma | 4 → 200 · 54° |
testimonial | una tarjeta de cita / terminal; muestra un borde blanco de 34px | 22 → 112 · 30° |
Mantén la mayoría de las elevaciones pequeñas (20–40px). Apunta a 15–35 piezas en toda la página, unas pocas por sección. Las piezas anidadas están bien: lista la interna (photo) antes de su tarjeta.
waitFor: selectores de números en vivo alimentados por una API (contadores de visitantes, medidores de tokens). La captura espera hasta que cada uno muestre un dígito distinto de cero antes de congelar la página; de lo contrario, pueden filmarse como "0".hide: banners de cookies, widgets de chat, ventanas emergentes.css: cualquier CSS adicional para forzar el estado final de los efectos basados en desplazamiento (por ejemplo, palabras que se iluminan al hacer scroll).routes:[{ "match": "**/broken.jpg*", "file": "/abs/local/copy.jpg" }]para activos que el sitio en vivo no sirve correctamente.
3. Captura y bloquea la página
node capture.mjs
Esto se desplaza hasta el fondo una vez (imágenes perezosas, contadores, revelaciones), finaliza las animaciones, congela los vídeos en un fotograma (o su póster), colapsa las secciones de "pista de aterrizaje" fijadas por desplazamiento (sticky + espaciadores de desplazamiento de 200vh de lo contrario se filmarían como tramos en blanco), desbloquea las barras fijas, luego guarda la placa base en fragmentos de 4096px (Chrome limita una captura a 16384 píxeles de dispositivo: las capturas más grandes repiten silenciosamente la parte superior de la página) más cada pieza elevada individualmente sobre transparencia con 28px de relleno, y capture/layers.json con cada posición.
Mira la captura antes de temporizar cualquier cosa: abre capture/page_*.png y algunas layer_*.png. Verifica si hay fotogramas de vídeo en blanco, texto medio revelado, regiones duplicadas, ventanas emergentes. Corrige con css/hide/routes y vuelve a capturar. Bloquea la página antes de temporizar: cualquier cambio en la altura de una sección desplaza todo lo que está después.
4. Fotogramas fijos para aprobación
node render.mjs stills 0,1.6,2.5,5,6.5,9.3,11 # → stills/contact_sheet.png
Muestra al usuario la hoja de contactos y espera una respuesta afirmativa antes del renderizado final. Lo que debe mostrar cada fotograma fijo:
- 0s: bajo sobre el hero, maqueta nítida, cuatro fichas de logotipo de 32px junto al botón.
- 1.6s: hero cayendo detrás, el título de la segunda sección cruzando el centro, el contenido cercano difuminándose.
- 2.5s: las fotos de esa sección levantadas y desenfocadas cerca del objetivo; un flychip saliendo de un borde.
- 5s / 6.5s: planeo constante; una sección nítida en el centro, suave debajo; el testimonio elevado con su borde blanco.
- 9.3s: directamente desde arriba, toda la página como una tira centrada.
- 11s: la tarjeta final: una píldora de insignia, la línea 1 en Geist 500, cola en cursiva Instrument Serif gris, el héroe como una tarjeta de 1018px, fichas de logotipo en sus esquinas, dos desenfoques suaves en la parte inferior.
5. Verifica el movimiento
node render.mjs video preview30.mp4 30 1 && node render.mjs cam
python3 flow.py preview30.mp4 # PASS = el planeo nunca se detiene ni invierte, el vuelo nunca se acerca
6. Partitura y renderizado
python3 score.py track.mp3 auto # encuentra el drop, lo aterriza en 9.97s; whooshes en 8.66s y 9.80s → audio/score.wav
node render.mjs video flyover.mp4 60 16 # → flyover.mp4, más flyover-audio.mp4 cuando existe audio/score.wav
Aproximadamente 0.1–0.2s por fotograma en una GPU de la serie M de Apple (870 fotogramas). El harness selecciona ANGLE Metal en macOS, D3D11 en Windows, Vulkan en Linux; si informa SwiftShader/llvmpipe estás en un rasterizador de software y será lento.
La película (fija: no la retemporices por página)
- 14.5s a 60fps, 1440x1152. Vuelo 0–8.2s · ascenso 7.75–9.25s · vista aérea 9.25–9.47s · inmersión 9.47–9.97s · tarjeta de título 9.97–14.5s.
- Una cámara, nunca un corte. Distancia focal 1880px, inclinación 56°, distancia con relajación 1360→1700px, sin girar nunca. La velocidad a lo largo de la página es una curva integrada (proporción de crucero): 0 → 0.5 a los 0.6s, mantenida hasta 1.25s, 1.0 a los 1.6s, mantenida hasta 7.3s, relajándose a 0.12 a los 8.3s. El crucero se resuelve para que el título del pie de página esté nítido a los 8.0s, por lo que las páginas largas se deslizan más rápido.
- Profundidad de campo por píxel: desenfoque = 75·(enfoque−profundidad)/profundidad en el frente, 6·(profundidad−enfoque)/profundidad detrás, limitado a 120px; 40 muestras de ángulo dorado desde niveles mip, rotadas por subfotograma de desenfoque de movimiento; desenfoque de campo cercano dilatado para que el primer plano desenfocado se extienda sobre el fondo nítido.
- Las piezas levantadas se colocan alrededor de su propio centro (el borde superior hacia el objetivo) y proyectan una sombra: la misma imagen en #1a2133 plano, desplazada (0.10z, 4+0.36z), desenfocada 7+0.5z, 26%.
- Ascenso: zoom de escala logarítmica en una velocidad trapezoidal, aplanamiento de inclinación 8.15–8.95s, entregando a una cámara que fija el borde inferior de la página 42px sobre el fotograma. Inmersión: easing cuadrático de entrada-salida en escala logarítmica con el borde superior fijado, luego caída a y 283; una máscara de rectángulo redondeado transforma esquinas de 24px → 38px. El contenido debajo del hero se desvanece primero en el color de la neblina, luego se corta a transparente.
- Título: la píldora de insignia se desvanece a los 10.02s (y 44); línea 1 a partir de 10.12s una palabra cada 0.06s; cola a partir de 10.36s una palabra cada 0.05s; cada palabra se eleva 16px desde un desenfoque de 12px en 0.34s. Se mantiene con un deslizamiento del 2%.
Prohibido: cortes, fundidos cruzados, paradas o aceleraciones en el planeo, la cámara girando o balanceándose lateralmente, zoom durante el vuelo, resplandores, cualquier texto sobre la página antes de la tarjeta final.
Problemas comunes
- Páginas enormes: la placa base se divide en fragmentos, nunca una sola captura de pantalla. Si ves el héroe repetido a mitad de camino, una captura superó los 16384 píxeles de dispositivo.
- Sitios con desplazamiento: las pistas adhesivas se colapsan automáticamente; los resaltados de desplazamiento palabra por palabra necesitan una regla
cssque fuerce la opacidad completa. - Vídeos: se congelan en
videoTimesegundos. Si el vídeo del sitio en vivo da 404, obtienes una tarjeta negra; sirve un póster local medianteroutes. - Tarjetas que se desvanecen: muestran lo que hay detrás, así que desvanece primero al color de la neblina (el motor hace esto para todo lo que está debajo del hero).
- Línea 1 demasiado larga: las palabras se salen del fotograma a 74px. Acorta la línea en lugar de reducir el tipo.
- Licencias: la música sigue siendo responsabilidad del usuario; prefiere pistas cuya licencia cubra el uso comercial.