feat(recorrido): la conversación como tarjetas, fijada al scroll
La sección se ancla una pantalla y avanza con el scroll: llega la tarjeta de la pregunta, se te devuelve la de creeer, y al seguir bajando entra la siguiente. Cifra fantasma, raíl de posición y barra de avance para saber dónde estás. Con prefers-reduced-motion no hay anclaje ni escenas: las seis parejas se leen seguidas. Dos fallos corregidos en el camino: - las escenas se cruzaban superpuestas y quedaban ilegibles las dos; entrada y salida ya no se solapan. - el rótulo se posicionaba con left-0 dentro de Container, que resuelve contra la caja de padding e ignora su px-6; en móvil cortaba la primera letra. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
90a35714f2
commit
81c36bb657
2 files changed
+251
-45
No files matched your search
@@ -0,0 +1,107 @@
|
||||
'use client';
|
||||
|
||||
import { motion, useTransform, type MotionValue } from 'framer-motion';
|
||||
import { Wordmark } from '@/components/primitives/Wordmark';
|
||||
|
||||
// One beat of the pinned conversation, staged as two calling cards laid on a
|
||||
// table: the reader's, in white, and creeer's, in navy, handed back.
|
||||
//
|
||||
// Every scene is mounted at all times and stacked in the same place; what
|
||||
// changes is opacity and offset, driven by the section's scroll position.
|
||||
// Mounting only the active scene would animate more cheaply and would strip
|
||||
// five sixths of the section out of the static export.
|
||||
//
|
||||
// Each scene owns a slice of that scroll: the question card arrives at the top
|
||||
// of its slice, creeer's card is handed over around the middle, and both leave
|
||||
// as the next question comes up.
|
||||
|
||||
export function EscenaDialogo({
|
||||
i,
|
||||
total,
|
||||
progreso,
|
||||
n,
|
||||
name,
|
||||
situacion,
|
||||
respuesta,
|
||||
}: {
|
||||
i: number;
|
||||
total: number;
|
||||
progreso: MotionValue<number>;
|
||||
n: string;
|
||||
name: string;
|
||||
situacion: string;
|
||||
respuesta: string;
|
||||
}) {
|
||||
const paso = 1 / total;
|
||||
const inicio = i * paso;
|
||||
const fin = inicio + paso;
|
||||
|
||||
// Entrance and exit must not overlap. Every scene is stacked in the same
|
||||
// place, so a cross-dissolve puts two full cards of type on top of each other
|
||||
// and both become unreadable — it looked like a rendering fault, not like a
|
||||
// transition. One card leaves, a beat of empty stage, the next arrives.
|
||||
const entrada = i === 0 ? [-1, inicio] : [inicio + paso * 0.04, inicio + paso * 0.17];
|
||||
const opacidad = useTransform(
|
||||
progreso,
|
||||
[entrada[0], entrada[1], fin - paso * 0.17, fin - paso * 0.04],
|
||||
[0, 1, 1, 0],
|
||||
);
|
||||
// Nothing sits perfectly still: static type on a pinned screen reads as a
|
||||
// stuck page rather than as a held moment.
|
||||
const deriva = useTransform(progreso, [inicio - paso, fin + paso], [70, -70]);
|
||||
|
||||
// Where creeer's card is handed over inside this scene's slice.
|
||||
const revelado = useTransform(progreso, [inicio + paso * 0.32, inicio + paso * 0.6], [0, 1]);
|
||||
const respY = useTransform(revelado, [0, 1], [70, 0]);
|
||||
const respEscala = useTransform(revelado, [0, 1], [0.94, 1]);
|
||||
const respGiro = useTransform(revelado, [0, 1], [5.5, 1.6]);
|
||||
// The reader's card gives way a little as the answer lands: it stays
|
||||
// legible, but it stops being the thing you are looking at.
|
||||
const pregOpacidad = useTransform(revelado, [0, 1], [1, 0.72]);
|
||||
const pregGiro = useTransform(revelado, [0, 1], [-0.6, -1.8]);
|
||||
const pregY = useTransform(revelado, [0, 1], [0, -14]);
|
||||
|
||||
const cifraY = useTransform(progreso, [inicio - paso, fin + paso], [60, -60]);
|
||||
|
||||
return (
|
||||
<motion.li
|
||||
style={{ opacity: opacidad }}
|
||||
className="absolute inset-x-0 top-1/2 -translate-y-1/2 will-change-[opacity]"
|
||||
>
|
||||
<motion.span
|
||||
aria-hidden="true"
|
||||
style={{ y: cifraY }}
|
||||
className="pointer-events-none absolute -top-32 right-0 select-none font-sans font-black leading-none text-azul-profundo/[0.045] text-[9rem] md:text-[17rem]"
|
||||
>
|
||||
{n}
|
||||
</motion.span>
|
||||
|
||||
<motion.div style={{ y: deriva }} className="relative">
|
||||
{/* The reader's card. */}
|
||||
<motion.div
|
||||
style={{ opacity: pregOpacidad, rotate: pregGiro, y: pregY }}
|
||||
className="relative z-10 max-w-xl rounded-xl border border-azul-profundo/10 bg-white p-7 shadow-[0_28px_70px_-32px_rgba(10,29,61,0.5)] md:p-9"
|
||||
>
|
||||
<p className="text-[0.65rem] font-black uppercase tracking-[0.34em] text-dorado">
|
||||
{n} · {name}
|
||||
</p>
|
||||
<p className="mt-4 font-sans font-black leading-[1.12] text-azul-profundo text-2xl md:text-4xl">
|
||||
«{situacion}»
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* creeer's card, handed back. Navy against the reader's white, tilted
|
||||
the other way, overlapping the corner — two cards on a table, not
|
||||
two paragraphs in a column. */}
|
||||
<motion.div
|
||||
style={{ opacity: revelado, y: respY, scale: respEscala, rotate: respGiro }}
|
||||
className="relative z-20 -mt-5 ml-auto max-w-lg rounded-xl bg-azul-profundo p-7 shadow-[0_34px_80px_-30px_rgba(10,29,61,0.75)] md:-mt-8 md:mr-6 md:p-9"
|
||||
>
|
||||
<span aria-hidden="true" className="absolute left-0 top-7 h-10 w-[3px] rounded-r bg-dorado md:top-9" />
|
||||
<Wordmark size="sm" onDark />
|
||||
<p className="mt-4 text-base leading-relaxed text-white/75 md:text-xl">{respuesta}</p>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</motion.li>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user