
Titular gigante
GratisBloques de web · GSAP
La pieza
Una portada a pantalla completa con un titular gigante en el centro y, detrás, dos carriles de tarjetas que nacen como un punto, se abren hacia los lados y crecen de forma geométrica hasta salirse por los bordes, girando de 6° a 28° como las paredes de un pasillo. Las fotos van en escala de grises y solo una pastilla de color vivo rompe el blanco y negro. Una tasa de reproducción única controla todo: arranca rápido y se asienta, la rueda y el arrastre aceleran o invierten el sentido con inercia, y al pasar el cursor por una tarjeta el corredor se detiene, la foto recupera su color y aparece un cursor propio con su título. Trae tres colores de acento, fondo oscuro o claro, botón imantado y pausa. Encaja en portadas de estudios creativos, arquitectura e interiorismo, fotografía, moda o portfolios. Sin librerías: 28 KB de HTML.
Encaja bien en webs de
Antes de copiar
Cambia lo que quieras y copia: el prompt sale ya con el nombre, los colores y los textos de tu cliente. Lo que dejes en blanco se queda como en la demo.
Gratis y entero
Qué trae el prompt, por apartados
Tokens estimados para español (≈3,6 caracteres por token): sirven para orientarte, no son exactos.
# Recrea en un único archivo HTML: Corredor de imágenes
Una sección a pantalla completa en blanco y negro con un color vivo: dos carriles de tarjetas con fotos en escala de grises vienen desde el fondo hacia el espectador en perspectiva, crecen de golpe y se van por los bordes mientras un titular gigante se queda en el centro; el cursor las detiene y las pinta de color.
Eres un desarrollador front-end creativo senior. Genera un único `index.html` autocontenido que
reproduzca exactamente la pieza descrita: mismo layout, textos, colores, movimiento e interacción.
HTML, CSS y JS en un solo archivo, sin build ni frameworks. Librerías solo por CDN y en las versiones
indicadas. Todos los valores numéricos de este documento son definitivos: úsalos tal cual.
Responde solo con el archivo completo.
## Qué es
La portada de **Umbral**, un estudio de imagen de Aldenor. Estética de estudio contemporáneo: fondo casi negro (o papel claro), tipografía grotesca condensada muy pesada, una sola pastilla de color vivo (lima ácido por defecto) y fotografías en blanco y negro. No hay cursivas serif, ni letra manuscrita, ni pasteles, ni degradados.
Cómo se comporta:
1. **Dos carriles.** 10 tarjetas horizontales (16:10) en el carril izquierdo y 10 tarjetas verticales (3:4) en el derecho. Cada tarjeta recorre un ciclo de 30 s en bucle infinito: nace como un punto (≈2,6 % del ancho del escenario) cruzando un poco el eje central, se abre enseguida hacia su carril y crece de forma **geométrica** hasta ≈46 % del ancho (horizontales) o ≈34 % (verticales). Gira sobre el eje Y de 6° a 28°, siempre con el borde exterior más cerca del espectador (como las paredes de un pasillo).
2. **Retardos negativos.** Las 20 tarjetas son la misma animación con retardos negativos repartidos de forma uniforme (el carril derecho va desfasado medio paso), así que el corredor está lleno desde el primer fotograma. Los keyframes se **muestrean por JS** (80 muestras) en función del tamaño real del escenario, no se escriben a mano.
3. **Titular en el centro** con una palabra sobre una pastilla de color de acento, subtítulo y un botón. Un velo radial muy suave detrás del texto mantiene el contraste cuando una tarjeta pasa por detrás.
4. **Velocidad viva.** Una única tasa de reproducción compartida por todas las animaciones: arranca a ×7 y se asienta en ×1 (efecto «de golpe a velocidad»); la rueda y el arrastre la aceleran o la invierten con inercia, y al pasar el cursor por una tarjeta el corredor se frena hasta parar.
5. **Tarjeta en foco.** La tarjeta bajo el cursor se queda quieta, recupera su color original, se rodea de un filete de acento y su pie se pinta de acento; las demás se oscurecen un 55 %. Un cursor propio, un punto de acento que se convierte en pastilla con el nombre del proyecto, sigue al puntero con un retardo suave.
6. **Controles.** Pausa, tres colores de acento (lima, naranja, azul eléctrico) y un conmutador de fondo oscuro/claro.
## Librerías y esqueleto
Sin librerías. Todo es HTML, CSS y la Web Animations API del navegador.
Fuentes (Google Fonts, `display=swap`; ambas tienen ñ, tildes, ¿ y ¡):
```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wdth,wght@12..96,75..100,200..800&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet">
```
Cabecera exacta:
```html
<!doctype html>
<html lang="es" data-tema="oscuro" data-acento="lima">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Umbral — Estudio de imagen en Aldenor</title>
<meta name="description" content="Identidades, espacios y producto. Veinte proyectos de Umbral en un solo corredor de imágenes.">
```
**Imágenes.** Son fotografías del banco de la pieza, en WebP y AVIF, a dos tamaños (`<archivo>-800.webp`, de 800 px de ancho, y `<archivo>.webp`, del tamaño original), siempre bajo `https://motionframe.app/assets/banco`. Verticales (1080×1440): `https://motionframe.app/assets/banco/v01-cromo-espiral.webp` (escultura de cromo), `v02-vidrio-prisma`, `v03-ceramica-apilada`, `v04-arco-escalera`, `v05-duna-nocturna`, `v06-orbe-obsidiana`, `v07-frasco-ambar`, `v08-columnata-hormigon`. Horizontales (1600×1000): `https://motionframe.app/assets/banco/h01-interior-arco.webp`, `h02-cordilleras-niebla`, `h03-cromo-liquido`, `h04-muralla-pastel`, `h05-lago-luna`, `h06-seda-granate`, `h07-piedras-equilibrio`, `h08-pabellon-agua`. Cada una existe también como `.avif` y `-800.avif`. Se pintan siempre en escala de grises, así que su color original no importa. Con otras fotos vale cualquier WebP/AVIF vertical 3:4 y horizontal 16:10.
## Tokens de diseño
```css
:root {
--fondo: #0a0a0a;
--texto: #f3f3ef;
--suave: rgba(243, 243, 239, .66);
--linea: rgba(243, 243, 239, .18);
--acento: #d4ff00;
--sobre-acento: #0a0a0a;
--f-display: "Bricolage Grotesque", "Helvetica Neue", Arial, sans-serif;
--f-mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace;
--gutter: clamp(16px, 2.2vw, 36px);
--e-salida: cubic-bezier(.16, 1, .3, 1);
--e-suave: cubic-bezier(.65, 0, .35, 1);
color-scheme: dark;
}
[data-tema="claro"] { --fondo: #f4f4f0; --texto: #0a0a0a; --suave: rgba(10, 10, 10, .64); --linea: rgba(10, 10, 10, .18); color-scheme: light; }
[data-acento="naranja"] { --acento: #ff4d00; --sobre-acento: #0a0a0a; }
[data-acento="azul"] { --acento: #2a4bff; --sobre-acento: #ffffff; }
```
Reglas de estilo: blanco y negro con **un** color vivo; nada de pasteles. Titular en Bricolage Grotesque 800 con `font-stretch: 78%` y `opsz` 96; textos en la misma familia; etiquetas, contadores y pies en Geist Mono en mayúsculas. Esquinas de 6 px en las tarjetas, pastillas redondas en botones. Sin sombras.
## Estructura y contenido
Árbol de la página (textos literales; el `<script>` va después, ver «Código clave»):
```html
<a class="saltar" href="#contenido">Saltar al contenido</a>
<main class="stage" id="stage">
<div class="lanes" id="lanes" aria-hidden="true"></div>
<div class="velo" aria-hidden="true"></div>
<header class="top">
<a class="logo" href="#contenido" aria-label="Umbral, inicio"><i></i>Umbral</a>
<nav class="nav" aria-label="Principal">
<a href="#contenido">Proyectos</a>
<a href="#contenido">Estudio</a>
<a href="#contenido">Contacto</a>
</nav>
</header>
<section class="centro" id="contenido" aria-labelledby="titulo">
<h1 id="titulo"><span class="l"><span>Lo mejor</span></span><span class="l"><span><span class="hl">viene</span> de frente.</span></span></h1>
<p class="sub">Veinte proyectos de Umbral en un solo pasillo: identidades, espacios y producto. Pasa el cursor por uno y el corredor se detiene.</p>
<a class="cta" id="cta" href="#contenido">Ver los proyectos
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"/></svg>
</a>
</section>
<footer class="hud">
<p class="ayuda" style="margin:0"><span class="teclas"><kbd>←</kbd> <kbd>→</kbd></span><span>Arrastra o gira la rueda para acelerar</span></p>
<div class="medidor" aria-hidden="true"><span>Vel.</span><span class="barra"><i id="barra"></i></span><output id="vel">×1,0</output></div>
<div class="ctrl">
<button class="btn btn-pausa" id="pausa" type="button" aria-pressed="false" aria-label="Pausar el corredor">
<svg class="i-pause" viewBox="0 0 14 14" aria-hidden="true"><rect x="2" y="1" width="3.4" height="12" rx=".6"/><rect x="8.6" y="1" width="3.4" height="12" rx=".6"/></svg>
<svg class="i-play" viewBox="0 0 14 14" aria-hidden="true"><path d="M3 1.2v11.6a.6.6 0 0 0 .9.5l9-5.8a.6.6 0 0 0 0-1L3.9.7A.6.6 0 0 0 3 1.2z"/></svg>
</button>
<span role="group" aria-label="Color de acento" style="display:flex;align-items:center">
<button class="sw" type="button" data-ac="lima" style="--c:#d4ff00" aria-pressed="true" aria-label="Acento lima"></button>
<button class="sw" type="button" data-ac="naranja" style="--c:#ff4d00" aria-pressed="false" aria-label="Acento naranja"></button>
<button class="sw" type="button" data-ac="azul" style="--c:#2a4bff" aria-pressed="false" aria-label="Acento azul eléctrico"></button>
</span>
<button class="btn tema" id="tema" type="button" aria-pressed="false" aria-label="Cambiar a fondo claro">
<svg viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="6.5" fill="none" stroke="currentColor" stroke-width="1.4"/><path d="M8 1.5a6.5 6.5 0 0 1 0 13z" fill="currentColor"/></svg>
</button>
</div>
</footer>
<div class="cur" id="cur" aria-hidden="true"><span id="curtxt">Pausa</span></div>
<ul class="sr" id="lista"></ul>
</main>
```
CSS completo (copiar tal cual dentro de `<style>`):
```css
:root {
--fondo: #0a0a0a;
--texto: #f3f3ef;
--suave: rgba(243, 243, 239, .66);
--linea: rgba(243, 243, 239, .18);
--acento: #d4ff00;
--sobre-acento: #0a0a0a;
--f-display: "Bricolage Grotesque", "Helvetica Neue", Arial, sans-serif;
--f-mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace;
--gutter: clamp(16px, 2.2vw, 36px);
--e-salida: cubic-bezier(.16, 1, .3, 1);
--e-suave: cubic-bezier(.65, 0, .35, 1);
color-scheme: dark;
}
[data-tema="claro"] {
--fondo: #f4f4f0;
--texto: #0a0a0a;
--suave: rgba(10, 10, 10, .64);
--linea: rgba(10, 10, 10, .18);
color-scheme: light;
}
[data-acento="naranja"] { --acento: #ff4d00; --sobre-acento: #0a0a0a; }
[data-acento="azul"] { --acento: #2a4bff; --sobre-acento: #ffffff; }
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; height: 100%; overflow: hidden; }
body {
background: var(--fondo);
color: var(--texto);
font-family: var(--f-display);
font-size: 16px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
transition: background-color .6s var(--e-suave), color .6s var(--e-suave);
}
a { color: inherit; }
button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
:focus-visible { outline: 2px solid var(--acento); outline-offset: 3px; border-radius: 3px; }
.sr { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; list-style: none; }
.saltar { position: absolute; left: var(--gutter); top: -60px; z-index: 40; background: var(--acento); color: var(--sobre-acento); padding: 10px 16px; border-radius: 999px; text-decoration: none; font-weight: 600; }
.saltar:focus { top: 12px; }
/* ——— Escenario ——— */
.stage { position: relative; height: 100svh; min-height: 480px; overflow: hidden; isolation: isolate; touch-action: pan-y; user-select: none; -webkit-user-select: none; }
.lanes { position: absolute; inset: 0; will-change: transform; }
.card {
position: absolute; left: 50%; top: 50%;
margin: 0; overflow: hidden; border-radius: 6px;
background: #161616;
outline: 1px solid rgba(255, 255, 255, .16); outline-offset: -1px;
will-change: transform, opacity;
backface-visibility: hidden;
opacity: 0;
}
.card picture, .card img { display: block; width: 100%; height: 100%; }
.card img { object-fit: cover; filter: grayscale(1) contrast(1.14) brightness(1.06); transition: filter .45s var(--e-suave); pointer-events: none; }
.card .vel { position: absolute; inset: 0; background: var(--fondo); opacity: .7; pointer-events: none; }
.card::after { content: ""; position: absolute; inset: 0; background: var(--fondo); opacity: 0; transition: opacity .45s var(--e-suave); pointer-events: none; }
.card .cap {
position: absolute; left: 14px; bottom: 14px; right: 14px;
display: flex; justify-content: space-between; gap: 10px; align-items: flex-end;
font: 500 12px/1.25 var(--f-mono); letter-spacing: .02em; text-transform: uppercase; color: #fff;
pointer-events: none; z-index: 2;
}
.card .cap b { font-weight: 500; background: #0a0a0a; padding: 5px 8px; border-radius: 3px; }
.card .cap i { font-style: normal; opacity: .85; text-shadow: 0 0 8px rgba(0, 0, 0, .8); }
.is-hold .card:not(.is-on)::after { opacity: .55; }
.card.is-on { outline: 2px solid var(--acento); outline-offset: -2px; }
.card.is-on img { filter: none; }
.card.is-on .vel { visibility: hidden; }
.card.is-on .cap b { background: var(--acento); color: var(--sobre-acento); }
.velo {
position: absolute; inset: 0; pointer-events: none;
background: radial-gradient(ellipse 40% 36% at 50% 50%, color-mix(in srgb, var(--fondo) 74%, transparent) 0, color-mix(in srgb, var(--fondo) 34%, transparent) 55%, transparent 100%);
}
/* ——— Texto y controles ——— */
.top, .hud { position: absolute; left: 0; right: 0; z-index: 5; display: flex; align-items: center; justify-content: space-between; padding: 0 var(--gutter); pointer-events: none; }
.top { top: 0; height: 76px; }
.hud { bottom: 0; height: 84px; gap: 16px; }
.top > *, .hud > * { pointer-events: auto; }
.logo { text-transform: lowercase; display: inline-flex; align-items: center; gap: 9px; font-weight: 700; font-size: 20px; letter-spacing: -.03em; text-decoration: none; }
.logo i { width: 11px; height: 11px; border-radius: 50%; background: var(--acento); display: block; }
.nav { display: flex; gap: clamp(14px, 2.4vw, 34px); font: 500 12px var(--f-mono); letter-spacing: .04em; text-transform: uppercase; }
.nav a { text-decoration: none; position: relative; padding: 6px 0; }
.nav a::after { content: ""; position: absolute; left: 0; right: 0; bottom: 1px; height: 1px; background: var(--acento); transform: scaleX(0); transform-origin: right; transition: transform .5s var(--e-salida); }
.nav a:hover::after, .nav a:focus-visible::after { transform: scaleX(1); transform-origin: left; }
.centro { position: absolute; inset: 0; z-index: 4; display: grid; place-content: center; justify-items: center; text-align: center; padding: 76px var(--gutter) 84px; pointer-events: none; }
h1 {
margin: 0; font-weight: 800; font-stretch: 78%;
font-size: clamp(54px, 8.4vw, 156px); line-height: .9; letter-spacing: -.032em; word-spacing: .06em;
font-variation-settings: "opsz" 96;
}
h1 .l { display: block; overflow: hidden; padding: .06em .12em .1em; margin: -.06em -.12em -.1em; }
h1 .l > span { display: inline-block; transform: translateY(112%); }
h1 .hl { position: relative; display: inline-block; color: var(--sobre-acento); padding: 0 .14em; margin: 0 .02em 0 -.14em; }
h1 .hl::before { content: ""; position: absolute; inset: .06em 0 .04em; background: var(--acento); border-radius: .06em; transform: scaleX(0); transform-origin: left; z-index: -1; }
.sub { margin: clamp(18px, 2.6vw, 34px) 0 0; max-width: 34ch; font-size: clamp(15px, 1.25vw, 18px); line-height: 1.45; color: var(--suave); opacity: 0; transform: translateY(14px); }
.cta {
--mx: 50%; --my: 50%;
position: relative; display: inline-flex; align-items: center; gap: 12px; overflow: hidden; isolation: isolate;
margin-top: clamp(20px, 2.4vw, 30px); padding: 15px 22px 15px 24px; border-radius: 999px;
border: 1px solid var(--texto); font: 600 15px var(--f-display); letter-spacing: -.01em; text-decoration: none;
pointer-events: auto; opacity: 0; transform: translateY(14px);
transition: color .45s var(--e-salida), border-color .45s var(--e-salida);
will-change: transform;
}
.cta::before { content: ""; position: absolute; z-index: -1; left: var(--mx); top: var(--my); width: 420px; height: 420px; margin: -210px 0 0 -210px; border-radius: 50%; background: var(--acento); transform: scale(0); transition: transform .6s var(--e-salida); }
.cta:hover, .cta:focus-visible { color: var(--sobre-acento); border-color: var(--acento); }
.cta:hover::before, .cta:focus-visible::before { transform: scale(1); }
.cta svg { width: 18px; height: 18px; transition: transform .5s var(--e-salida); }
.cta:hover svg { transform: translateX(4px); }
.ayuda { font: 400 12px/1.4 var(--f-mono); color: var(--suave); text-transform: uppercase; letter-spacing: .04em; display: flex; align-items: center; gap: 10px; }
.ayuda kbd { font: 500 11px var(--f-mono); border: 1px solid var(--linea); border-radius: 4px; padding: 2px 6px; color: var(--texto); }
.medidor { display: flex; align-items: center; gap: 12px; font: 500 12px var(--f-mono); letter-spacing: .04em; text-transform: uppercase; }
.medidor .barra { position: relative; width: clamp(60px, 9vw, 130px); height: 2px; background: var(--linea); }
.medidor .barra i { position: absolute; left: 0; top: -1px; height: 4px; width: 100%; background: var(--acento); transform-origin: 0 50%; transform: scaleX(.2); }
.medidor output { min-width: 5.4ch; text-align: right; font-variant-numeric: tabular-nums; }
.ctrl { display: flex; align-items: center; gap: 8px; }
.btn { width: 38px; height: 38px; border-radius: 50%; border: 1px solid var(--linea); display: grid; place-items: center; transition: background-color .3s, border-color .3s, transform .5s var(--e-salida); }
.btn:hover { border-color: var(--texto); transform: scale(1.08); }
.btn svg { width: 14px; height: 14px; fill: currentColor; }
.btn .i-play { display: none; }
.btn[aria-pressed="true"] .i-play { display: block; }
.btn[aria-pressed="true"] .i-pause { display: none; }
.sw { width: 18px; height: 18px; border-radius: 50%; background: var(--c); position: relative; transition: transform .45s var(--e-salida); margin: 0 6px; }
.sw::after { content: ""; position: absolute; inset: -5px; border-radius: 50%; border: 1px solid var(--texto); opacity: 0; transform: scale(.7); transition: opacity .3s, transform .45s var(--e-salida); }
.sw:hover { transform: scale(1.18); }
.sw[aria-pressed="true"]::after { opacity: 1; transform: scale(1); }
.tema { margin-left: 4px; }
.tema svg { width: 16px; height: 16px; }
/* ——— Cursor ——— */
.cur { position: absolute; left: 0; top: 0; z-index: 8; pointer-events: none; opacity: 0; will-change: transform; }
.cur span {
display: block; transform: translate(-50%, -50%) scale(.2);
min-width: 56px; padding: 9px 15px; border-radius: 999px; white-space: nowrap; color: transparent;
background: var(--acento);
font: 500 12px/1.1 var(--f-mono); letter-spacing: .03em; text-transform: uppercase; text-align: center;
transition: transform .5s var(--e-salida), color .3s;
}
.cur.on span { transform: translate(-50%, -50%) scale(1); color: var(--sobre-acento); }
.cur.vis { opacity: 1; }
@media (hover: hover) and (pointer: fine) { .stage.fino, .stage.fino .card { cursor: none; } .stage.fino a, .stage.fino button { cursor: pointer; } }
/* ——— Entrada ——— */
.listo h1 .l > span { transform: none; transition: transform 1.15s var(--e-salida); }
.listo h1 .l:nth-child(2) > span { transition-delay: .09s; }
.listo h1 .hl::before { transform: scaleX(1); transition: transform .9s var(--e-salida) .55s; }
.listo .sub { opacity: 1; transform: none; transition: opacity .9s var(--e-salida) .6s, transform .9s var(--e-salida) .6s; }
.listo .cta { opacity: 1; transform: none; transition: opacity .9s var(--e-salida) .75s, transform .9s var(--e-salida) .75s, color .45s var(--e-salida), border-color .45s var(--e-salida); }
.top, .hud { opacity: 0; transition: opacity 1s var(--e-suave) .9s; }
.listo .top, .listo .hud { opacity: 1; }
/* ——— Móvil ——— */
@media (max-width: 760px) {
.nav a:not(:last-child) { display: none; }
.ayuda kbd, .ayuda .teclas { display: none; }
.hud { height: 76px; }
.medidor .barra { display: none; }
.sub { max-width: 30ch; }
.tema { margin-left: 0; }
}
@media (max-width: 420px) { .medidor { display: none; } }
@media (max-height: 560px) { .sub { display: none; } }
@media (max-aspect-ratio: 9/10) { h1 { font-size: clamp(48px, 15.4vw, 120px); } .centro { padding-bottom: 96px; } }
@media (hover: none) { .cur { display: none; } }
/* ——— Movimiento reducido ——— */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { transition-duration: .01ms !important; transition-delay: 0s !important; }
h1 .l > span { transform: none; }
h1 .hl::before { transform: scaleX(1); }
.sub, .cta, .top, .hud { opacity: 1; transform: none; }
.btn-pausa, .medidor, .ayuda { display: none; }
}
```
## Movimiento
| Elemento | Propiedad | De → a | Duración | Easing | Retardo | Disparador |
|---|---|---|---|---|---|---|
| Tarjeta (20) | `transform` | punto de ≈2,6 % del ancho cruzando el eje → 46 % (horizontales) o 34 % (verticales) en su carril; rotateY 6°→28° | 30 s por vuelta, infinito | lineal entre 80 muestras (el crecimiento es geométrico: `s = s0^(1−t)`) | −(i/10)·30 s (derecha: −((i+.5)/10)·30 s) | al cargar |
| Tarjeta | `opacity` | 0 → 1 en t 0–.05; 1 hasta t .93; 1 → 0 en t .93–1 | idem | lineal | idem | idem |
| Tarjeta | `z-index` | round(t·100)+1 | idem | lineal | idem | idem |
| Velo de profundidad `.vel` | `opacity` | .7 → 0 hasta t .8 | idem | lineal | idem | idem |
| Pie `.cap` | `opacity` | 0 → 1 entre t .55 y .80 (ease-out cúbico) | idem | lineal | idem | idem |
| Tasa de reproducción | `playbackRate` | arranca con impulso +6 que decae con τ = 0,9 s; la tasa sigue a la meta con τ = 0,32 s (0,22 s al frenar) | continuo | exponencial | 0 | rAF |
| Titular (cada línea) | `translateY` | 112 % → 0 (máscara por línea) | 1,15 s | `--e-salida` | línea 2: +0,09 s | al cargar fuentes (máx. 1,2 s) |
| Pastilla de acento | `scaleX` | 0 → 1 (origen izquierda) | 0,9 s | `--e-salida` | 0,55 s | idem |
| Subtítulo y botón | `opacity`, `translateY` | 0, 14 px → 1, 0 | 0,9 s | `--e-salida` | 0,6 s / 0,75 s | idem |
| Cabecera y pie (HUD) | `opacity` | 0 → 1 | 1 s | `--e-suave` | 0,9 s | idem |
| Conjunto de carriles | `translate3d` | paralaje de −18·x px, −12·y px según el puntero (−1…1) | continuo | lerp τ = 0,18 s | 0 | puntero |
| Tarjeta en foco | `filter` de la imagen | gris → color | 0,45 s | `--e-suave` | 0 | cursor encima |
| Resto de tarjetas | `opacity` de `::after` | 0 → .55 | 0,45 s | `--e-suave` | 0 | cursor encima |
| Cursor propio | `transform` | sigue al puntero (lerp τ = 0,07 s); punto escala .2 → pastilla escala 1 | 0,5 s | `--e-salida` | 0 | cursor sobre tarjeta |
| Botón principal | relleno | círculo de 420 px que crece desde el punto de entrada | 0,6 s | `--e-salida` | 0 | hover/foco |
| Botón principal | `translate3d` | imán: .22·dx, .3·dy | inmediato (rAF) | — | 0 | hover |
## Código clave
Los pilares son tres: (1) el **muestreo de keyframes** en JS con retardos negativos, (2) la **tasa de reproducción única** que acelera, invierte o frena todo, y (3) el **foco** que detiene el corredor. Script completo (copiar tal cual al final del `<body>`):
```html
<script>
(() => {
'use strict';
const BANCO = 'https://motionframe.app/assets/banco';
const REDUCIDO = matchMedia('(prefers-reduced-motion: reduce)').matches;
// [archivo, ancho del original, proyecto, tipo]
const H = [
['h03-cromo-liquido', 'Metal líquido', 'Campaña'], ['h01-interior-arco', 'Casa Arco', 'Interiorismo'],
['h06-seda-granate', 'Seda', 'Packaging'], ['h08-pabellon-agua', 'Pabellón de agua', 'Arquitectura'],
['h02-cordilleras-niebla', 'Sierra blanca', 'Identidad'], ['h05-lago-luna', 'Lago luna', 'Fotografía'],
['h07-piedras-equilibrio', 'Equilibrio', 'Editorial'], ['h04-muralla-pastel', 'Terrazas del faro', 'Arquitectura'],
['h06-seda-granate', 'Seda', 'Packaging'], ['h03-cromo-liquido', 'Metal líquido', 'Campaña']
].map(a => ({ f: a[0], n: a[1], t: a[2], w: 1600, h: 1000 }));
const V = [
['v01-cromo-espiral', 'Espiral', 'Escultura'], ['v04-arco-escalera', 'Puerta azul', 'Fotografía'],
['v06-orbe-obsidiana', 'Obsidiana', 'Producto'], ['v03-ceramica-apilada', 'Torno', 'Cerámica'],
['v08-columnata-hormigon', 'Columnata', 'Arquitectura'], ['v02-vidrio-prisma', 'Prisma', 'Producto'],
['v05-duna-nocturna', 'Duna', 'Dirección de arte'], ['v07-frasco-ambar', 'Ámbar', 'Packaging'],
['v02-vidrio-prisma', 'Prisma', 'Producto'], ['v08-columnata-hormigon', 'Columnata', 'Arquitectura']
].map(a => ({ f: a[0], n: a[1], t: a[2], w: 1080, h: 1440 }));
const T = 30; // segundos por vuelta completa de un carril
const MUESTRAS = 80; // keyframes por tarjeta
const stage = document.getElementById('stage');
const lanes = document.getElementById('lanes');
const cartas = [];
[[-1, H, 0], [1, V, .5]].forEach(([lane, lista, desfase]) => {
lista.forEach((d, i) => {
const el = document.createElement('figure');
el.className = 'card';
el.innerHTML =
'<picture><source type="image/avif" srcset="' + BANCO + '/' + d.f + '-800.avif 800w, ' + BANCO + '/' + d.f + '.avif ' + d.w + 'w" sizes="46vw">' +
'<img alt="" width="' + d.w + '" height="' + d.h + '" decoding="async" srcset="' + BANCO + '/' + d.f + '-800.webp 800w, ' + BANCO + '/' + d.f + '.webp ' + d.w + 'w" sizes="46vw" src="' + BANCO + '/' + d.f + '-800.webp"></picture>' +
'<span class="vel"></span><figcaption class="cap"><b>' + String(cartas.length + 1).padStart(2, '0') + '</b><i>' + d.n + ' · ' + d.t + '</i></figcaption>';
el.dataset.nombre = d.n + ' · ' + d.t;
lanes.appendChild(el);
cartas.push({
el, vel: el.querySelector('.vel'), cap: el.querySelector('.cap'), lane, ar: d.h / d.w,
ancho: lane < 0 ? .46 : .34, // ancho final como fracción del del escenario
fase: (i + desfase) / lista.length, // reparto uniforme en el ciclo (retardo negativo)
yb: (((i * 37 + (lane > 0 ? 11 : 3)) % 9) - 4) / 4 * .16, // desvío vertical propio de cada tarjeta
j: (((i * 53) % 7) - 3) / 3 * .018 // holgura del pasillo propia de cada tarjeta
});
});
});
const lista = document.getElementById('lista');
[...H.slice(0, 8), ...V.slice(0, 8)].forEach(d => {
const li = document.createElement('li'); li.textContent = d.n + ', ' + d.t; lista.appendChild(li);
});
// —— Geometría: keyframes muestreados por JS ——
const suavizado = x => { x = Math.min(1, Math.max(0, x)); return 1 - Math.pow(1 - x, 3); };
let W = 0, Hh = 0, K = 1;
function fotogramas(c) {
const P = Math.max(900, W * .9);
const w1 = c.ancho * K * W; // ancho final en px
const s0 = (.026 * W) / w1; // escala inicial: ≈2,6 % del ancho
const hueco = (.17 + c.j) * W * (K > 1 ? .5 : 1);
const cruce = .035 * W;
const f = [], g = [], h = [];
for (let i = 0; i <= MUESTRAS; i++) {
const t = i / MUESTRAS;
const s = Math.pow(s0, 1 - t); // crecimiento geométrico
const abre = suavizado(t / .34); // los carriles se abren pronto y se mantienen
const borde = hueco * abre - cruce * (1 - abre); // arranca cruzando el eje
const x = c.lane * (borde + (w1 * s) / 2);
const y = Hh * (c.yb * abre - c.lane * .05 * Math.pow(t, 1.6));
const giro = -c.lane * (6 + 22 * t); // 6° → 28°
const op = t < .05 ? t / .05 : t > .93 ? (1 - t) / .07 : 1;
f.push({
offset: t, easing: 'linear', opacity: +op.toFixed(3), zIndex: Math.round(t * 100) + 1,
transform: 'perspective(' + P.toFixed(0) + 'px) translate3d(' + x.toFixed(1) + 'px,' + y.toFixed(1) + 'px,0) rotateY(' + giro.toFixed(2) + 'deg) scale(' + s.toFixed(5) + ')'
});
g.push({ offset: t, easing: 'linear', opacity: +(.7 * (1 - Math.min(1, t / .8))).toFixed(3) });
h.push({ offset: t, easing: 'linear', opacity: +suavizado((t - .55) / .25).toFixed(3) });
}
return [f, g, h];
}
const anims = [];
function medir() {
W = stage.clientWidth; Hh = stage.clientHeight;
K = W / Hh < .9 ? 1.6 : 1;
cartas.forEach(c => {
const w = c.ancho * K * W;
Object.assign(c.el.style, { width: w + 'px', height: (w * (c.lane < 0 ? .625 : c.ar)) + 'px', marginLeft: (-w / 2) + 'px', marginTop: (-(w * (c.lane < 0 ? .625 : c.ar)) / 2) + 'px' });
});
}
function construir() {
medir();
const primera = !anims.length;
cartas.forEach((c, i) => {
const [f, g, h] = fotogramas(c);
if (primera) {
const opc = { duration: T * 1000, iterations: Infinity, delay: -c.fase * T * 1000 };
const a = c.el.animate(f, opc), b = c.vel.animate(g, opc), d = c.cap.animate(h, opc);
a.currentTime = b.currentTime = d.currentTime = T * 1000 * 20; // margen para poder ir marcha atrás
anims.push(a, b, d);
} else {
anims[i * 3].effect.setKeyframes(f);
anims[i * 3 + 1].effect.setKeyframes(g);
anims[i * 3 + 2].effect.setKeyframes(h);
}
});
if (REDUCIDO) anims.forEach(a => a.pause());
}
construir();
let rt; addEventListener('resize', () => { clearTimeout(rt); rt = setTimeout(construir, 120); });
// —— Bucle: velocidad, paralaje y cursor ——
const lerp = (a, b, k) => a + (b - a) * k;
const velOut = document.getElementById('vel'), barra = document.getElementById('barra');
const cur = document.getElementById('cur'), curtxt = document.getElementById('curtxt');
const fino = matchMedia('(hover: hover) and (pointer: fine)').matches;
if (fino) stage.classList.add('fino');
else document.querySelector('.ayuda span:last-child').textContent = 'Desliza para acelerar · toca una imagen';
let tasa = 0, boost = REDUCIDO ? 0 : 6, retener = false, pausado = false, visible = true, ultimo = 0, ultimoTxt = '';
const p = { x: 0, y: 0, tx: 0, ty: 0, cx: -100, cy: -100, tcx: -100, tcy: -100 };
function tick(now) {
if (!visible) return;
requestAnimationFrame(tick);
const dt = Math.min(.05, ((now - ultimo) / 1000) || .016); ultimo = now;
boost *= Math.exp(-dt / .9);
const meta = (pausado || retener) ? 0 : 1 + boost;
tasa = lerp(tasa, meta, 1 - Math.exp(-dt / (meta === 0 ? .22 : .32)));
if (Math.abs(tasa) < .002 && meta === 0) tasa = 0;
if (!REDUCIDO) {
for (let i = 0; i < anims.length; i++) anims[i].playbackRate = tasa;
if (anims[0].currentTime < T * 4000) anims.forEach(a => { a.currentTime += T * 10000; }); // vueltas completas: no se nota
}
// lectura de velocidad
const txt = '×' + Math.abs(tasa).toFixed(1).replace('.', ',');
if (txt !== ultimoTxt) { ultimoTxt = txt; velOut.textContent = (tasa < -.05 ? '−' : '') + txt; }
barra.style.transform = 'scaleX(' + Math.min(1, Math.abs(tasa) / 8).toFixed(3) + ')';
// paralaje del conjunto + cursor
const k = 1 - Math.exp(-dt / .18);
p.x = lerp(p.x, p.tx, k); p.y = lerp(p.y, p.ty, k);
lanes.style.transform = 'translate3d(' + (-p.x * 18).toFixed(2) + 'px,' + (-p.y * 12).toFixed(2) + 'px,0)';
p.cx = lerp(p.cx, p.tcx, 1 - Math.exp(-dt / .07)); p.cy = lerp(p.cy, p.tcy, 1 - Math.exp(-dt / .07));
cur.style.transform = 'translate3d(' + p.cx.toFixed(1) + 'px,' + p.cy.toFixed(1) + 'px,0)';
}
requestAnimationFrame(t => { ultimo = t; tick(t); });
new IntersectionObserver(es => {
const v = es[0].isIntersecting && !document.hidden;
if (v !== visible) { visible = v; if (v) { ultimo = performance.now(); requestAnimationFrame(tick); } }
}).observe(stage);
document.addEventListener('visibilitychange', () => {
const v = !document.hidden;
if (v !== visible) { visible = v; if (v) { ultimo = performance.now(); requestAnimationFrame(tick); } }
});
// —— Interacción ——
let tarjeta = null, fijada = null, arrastre = null;
function foco(el) {
if (tarjeta === el) return;
if (tarjeta) tarjeta.classList.remove('is-on');
tarjeta = el;
if (el) { el.classList.add('is-on'); curtxt.textContent = el.dataset.nombre; }
stage.classList.toggle('is-hold', !!el);
cur.classList.toggle('on', !!el);
if (!el) curtxt.textContent = 'Pausa';
retener = !!el && !REDUCIDO;
}
const rect = () => stage.getBoundingClientRect();
stage.addEventListener('pointermove', e => {
const r = rect();
p.tx = (e.clientX - r.left) / r.width * 2 - 1; p.ty = (e.clientY - r.top) / r.height * 2 - 1;
p.tcx = e.clientX - r.left; p.tcy = e.clientY - r.top;
if (e.pointerType === 'mouse') cur.classList.add('vis');
if (arrastre) {
const dx = e.clientX - arrastre.x; arrastre.x = e.clientX;
if (Math.abs(e.clientX - arrastre.x0) > 6) arrastre.mov = true;
boost = Math.max(-9, Math.min(9, boost + dx * .035));
}
});
stage.addEventListener('pointerleave', () => { cur.classList.remove('vis'); if (!fijada) foco(null); p.tx = p.ty = 0; });
stage.addEventListener('pointerover', e => {
if (e.pointerType !== 'mouse' || arrastre) return;
foco(e.target.closest('.card'));
});
stage.addEventListener('pointerdown', e => {
if (e.target.closest('a,button')) return;
arrastre = { x: e.clientX, x0: e.clientX, mov: false, card: e.target.closest('.card') };
if (e.pointerType === 'mouse') foco(null);
});
const fin = e => {
if (!arrastre) return;
const a = arrastre; arrastre = null;
if (e.pointerType !== 'mouse' && !a.mov) { // toque: fija o suelta una tarjeta
const c = a.card && a.card !== fijada ? a.card : null;
fijada = c; foco(c);
} else if (e.pointerType === 'mouse') {
foco(document.elementFromPoint(e.clientX, e.clientY)?.closest('.card') || null);
}
};
addEventListener('pointerup', fin); addEventListener('pointercancel', fin);
stage.addEventListener('wheel', e => {
boost = Math.max(-9, Math.min(9, boost + (e.deltaY + e.deltaX) * .006));
}, { passive: true });
addEventListener('keydown', e => {
if (e.key === 'ArrowRight') boost = Math.min(9, boost + 2);
else if (e.key === 'ArrowLeft') boost = Math.max(-9, boost - 2);
});
// —— Controles ——
const bPausa = document.getElementById('pausa');
bPausa.addEventListener('click', () => {
pausado = !pausado;
bPausa.setAttribute('aria-pressed', pausado);
bPausa.setAttribute('aria-label', pausado ? 'Reanudar el corredor' : 'Pausar el corredor');
});
document.querySelectorAll('.sw').forEach(b => b.addEventListener('click', () => {
document.documentElement.dataset.acento = b.dataset.ac;
document.querySelectorAll('.sw').forEach(x => x.setAttribute('aria-pressed', x === b));
}));
const bTema = document.getElementById('tema');
bTema.addEventListener('click', () => {
const claro = document.documentElement.dataset.tema !== 'claro';
document.documentElement.dataset.tema = claro ? 'claro' : 'oscuro';
bTema.setAttribute('aria-pressed', claro);
bTema.setAttribute('aria-label', claro ? 'Cambiar a fondo oscuro' : 'Cambiar a fondo claro');
});
// Botón imantado con relleno desde el punto de entrada
const cta = document.getElementById('cta');
let cx = 0, cy = 0, rafCta = 0;
cta.addEventListener('pointerenter', e => { const r = cta.getBoundingClientRect(); cta.style.setProperty('--mx', (e.clientX - r.left) + 'px'); cta.style.setProperty('--my', (e.clientY - r.top) + 'px'); });
cta.addEventListener('pointerleave', e => { const r = cta.getBoundingClientRect(); cta.style.setProperty('--mx', (e.clientX - r.left) + 'px'); cta.style.setProperty('--my', (e.clientY - r.top) + 'px'); cta.style.transform = ''; });
cta.addEventListener('pointermove', e => {
if (e.pointerType !== 'mouse' || REDUCIDO) return;
const r = cta.getBoundingClientRect();
cx = (e.clientX - r.left - r.width / 2) * .22; cy = (e.clientY - r.top - r.height / 2) * .3;
cancelAnimationFrame(rafCta); rafCta = requestAnimationFrame(() => { cta.style.transform = 'translate3d(' + cx.toFixed(1) + 'px,' + cy.toFixed(1) + 'px,0)'; });
});
document.querySelectorAll('a[href="#contenido"]').forEach(a => a.addEventListener('click', e => e.preventDefault()));
// Entrada
const arrancar = () => requestAnimationFrame(() => requestAnimationFrame(() => document.body.classList.add('listo')));
(document.fonts && document.fonts.ready ? Promise.race([document.fonts.ready, new Promise(r => setTimeout(r, 1200))]) : Promise.resolve()).then(arrancar);
})();
</script>
```
Detalles del muestreo que no se pueden perder:
- La perspectiva va **dentro de cada transform** (`perspective(P) translate3d(x,y,0) rotateY(a) scale(s)`, con `P = max(900, ancho·.9)`): el punto de fuga de cada tarjeta es el centro del escenario porque todas están centradas antes de trasladarse.
- El escenario se mide con `clientWidth` y `clientHeight`; en pantallas verticales (ancho/alto < .9) las tarjetas se multiplican por 1,6 y el hueco central se reduce a la mitad.
- Los keyframes se recalculan al redimensionar con `effect.setKeyframes`, sin recrear las animaciones: el corredor no salta.
- `currentTime` arranca en 20 ciclos para poder ir marcha atrás sin llegar a 0; cuando baja de 4 ciclos se suman 10 ciclos exactos (imperceptible).
- Las tarjetas verticales usan su propia proporción (alto/ancho de la foto) y las horizontales, 0,625.
## Interacción
- **Puntero (ratón):** el corredor se frena hasta parar al entrar en una tarjeta (la tarjeta pasa a color y recibe el filete de acento) y arranca al salir. El cursor propio muestra «Nombre · Tipo». El conjunto se desplaza ligeramente con el puntero.
- **Rueda:** acelera hacia delante (deltaY positivo) o invierte (negativo); el impulso decae solo. No se hace `preventDefault`: la página puede seguir haciendo scroll.
- **Arrastre:** horizontal, suma impulso según el desplazamiento.
- **Teclado:** las flechas izquierda y derecha restan o suman 2 al impulso. El botón de pausa es un `button` con `aria-pressed`.
- **Táctil:** el arrastre horizontal acelera; un toque sobre una tarjeta la fija (color y parada) y otro toque en otro sitio la suelta. `touch-action: pan-y` deja libre el scroll vertical.
- Botón principal: imán sutil y relleno de acento que nace donde entra el puntero.
## Adaptación a móvil y tablet
- Hasta 760 px: solo queda visible el último enlace del menú («Contacto»), se ocultan las teclas del texto de ayuda y la barra de velocidad.
- Hasta 420 px: se oculta también el medidor de velocidad.
- Pantallas verticales (ancho/alto < .9): titular `clamp(48px, 15.4vw, 120px)`, tarjetas ×1,6, relleno inferior del bloque central de 96 px.
- Altura inferior a 560 px: se oculta el subtítulo.
- En dispositivos sin puntero fino el cursor propio no se muestra y el texto de ayuda pasa a «Desliza para acelerar · toca una imagen».
## Accesibilidad y movimiento reducido
- El corredor es decorativo: `aria-hidden="true"`. Los proyectos se listan en una lista visualmente oculta (`.sr`) con su nombre y tipo.
- Enlace «Saltar al contenido», foco visible de 2 px en el color de acento, botones con `aria-label` y `aria-pressed` que cambian con el estado.
- Contraste AA: texto sobre el fondo del tema; texto de la pastilla (`--sobre-acento`) negro sobre lima y naranja, blanco sobre azul.
- `prefers-reduced-motion: reduce`: las animaciones de las tarjetas se **pausan** en su estado inicial (composición estática con las 20 tarjetas repartidas), no hay impulso de arranque ni parada por hover, las transiciones duran 0,01 ms, el titular aparece ya completo y se ocultan pausa, medidor y ayuda. El color al pasar el cursor se mantiene.
## Rendimiento
- Todo el movimiento va por `transform` y `opacity` en capas propias (`will-change`), animadas con WAAPI: sin layout en cada fotograma. El único trabajo por fotograma en JS es fijar la tasa de reproducción de 60 animaciones y las transformaciones del paralaje y del cursor.
- Imágenes con `srcset` (800 px y original) y AVIF con WebP de respaldo; `decoding="async"`. Las repeticiones de cada carril reutilizan archivos ya descargados.
- El bucle se detiene cuando la pieza sale de pantalla (`IntersectionObserver`) o la pestaña está oculta, y se reanuda al volver.
- No usa WebGL: no hay DPR que limitar ni fallback.
## Personalización
- **Marca y textos:** `Umbral`, `Aldenor`, `Lo mejor`, `viene` (la palabra sobre la pastilla), `de frente.` y `Ver los proyectos`.
- **Colores:** `#0a0a0a`, `#f3f3ef` y `#d4ff00`. Para otro color vivo cambia también `--sobre-acento` (negro sobre colores claros, blanco sobre oscuros). Variantes que funcionan: rojo `#ff2d20`, verde ácido `#7cff00`, amarillo `#ffe600`.
- **Fotos:** sustituye los nombres de las listas `H` (horizontales 1600×1000) y `V` (verticales 1080×1440) y sus títulos.
- **Ritmo:** `T` (30 s por vuelta; más alto, más lento). **Densidad:** número de elementos de `H` y `V`.
- **Tamaños:** el `ancho` final de cada carril (.46 y .34) y el hueco central `.17` de `fotogramas()`.
- **Fotos en color siempre:** quita el `filter` del `img`.
## Comprobación final
1. Sin errores en consola; todas las imágenes cargan desde `https://motionframe.app/assets/banco`.
2. Al cargar ya hay tarjetas de todos los tamaños en los dos carriles (retardos negativos) y el corredor arranca rápido y se asienta en ×1,0 en unos 2,5 s.
3. Las tarjetas nacen pequeñas cerca del centro, se abren a su carril y crecen sin saltos; las del carril izquierdo tienen el borde derecho más lejano y las del derecho, el izquierdo.
4. El titular queda siempre legible, también cuando una tarjeta pasa por detrás.
5. Al pasar el cursor por una tarjeta el corredor se detiene en medio segundo, la tarjeta pasa a color y las demás se oscurecen; al salir, reanuda.
6. La rueda acelera y, hacia arriba, invierte el sentido; el medidor «Vel.» refleja el valor y vuelve a ×1,0.
7. Los tres colores de acento y el modo claro funcionan; el texto de la pastilla se lee en los tres.
8. Con `prefers-reduced-motion` todo queda quieto, legible y sin saltos.
9. Sin scroll horizontal entre 360 y 1920 px; a 390×844 el titular ocupa casi todo el ancho.
10. El enlace «Saltar al contenido», el botón de pausa y los colores se manejan con teclado.
11. Con la pieza fuera de pantalla o la pestaña oculta, el bucle de JS se detiene.
12. El HTML pesa menos de 40 KB.Prueba en frío
Aún sin publicar para esta pieza
En la prueba en frío pegamos el prompt en Claude Sonnet 5.5, sin ayuda ni contexto, y comparamos lo que genera con la demo. Cuando esté hecha, aquí verás la captura, el veredicto y la fecha.
Paso a paso
Personaliza
Copia el Completo o el Lite
Pega e itera
Publica
Licencia en claro
Todas las piezas incluyen licencia de uso comercial. Usa el resultado tal cual en tus productos, encargos freelance o webs de clientes, sin atribución.
Puedes
No puedes
Es la misma licencia para lo gratis y lo Pro. La tienes entera en licencia de uso.
Preguntas frecuentes
Versión 1.0 · publicada el 01-10-2026

Bloques de web · GSAP

Bloques de web · JS

Bloques de web · CSS

Bloques de web · CSS

Bloques de web · JS

Bloques de web · GSAP

Bloques de web · GSAP

Bloques de web · SVG