La pregunta y la respuesta compartían fotograma: la tarjeta de creeer entraba debajo de la del lector y se veían las dos a la vez. El flujo que pidió la dueña es pregunta → scroll → respuesta → scroll → pregunta, con una sola cosa en pantalla cada vez. Las dos tarjetas pasan a ocupar la misma celda de la rejilla, así que la segunda toma el escenario que deja la primera en vez de apilarse bajo ella, y entran desde márgenes opuestos para que el cambio se lea como un relevo. Verificado tiempo a tiempo: los doce muestran exactamente una. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
112 lines
4.4 KiB
TypeScript
112 lines
4.4 KiB
TypeScript
'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.
|
|
//
|
|
// The cards are driven by STATE, not by scroll position. Mapping opacity
|
|
// straight onto scroll made every intermediate position a valid frame, so
|
|
// stopping mid-swipe left creeer's card as a half-transparent grey rectangle —
|
|
// it read as a rendering fault. Scroll now only decides which beat we are on;
|
|
// the cards animate to that beat over time and always land somewhere finished,
|
|
// wherever the reader lets go.
|
|
//
|
|
// Every scene stays mounted and stacked in the same place. Mounting only the
|
|
// active one would animate more cheaply and would strip five sixths of the
|
|
// section out of the static export.
|
|
|
|
const EASE = [0.22, 1, 0.36, 1] as const;
|
|
|
|
export function EscenaDialogo({
|
|
i,
|
|
total,
|
|
progreso,
|
|
activa,
|
|
respondida,
|
|
n,
|
|
name,
|
|
situacion,
|
|
respuesta,
|
|
}: {
|
|
i: number;
|
|
total: number;
|
|
progreso: MotionValue<number>;
|
|
activa: boolean;
|
|
respondida: boolean;
|
|
n: string;
|
|
name: string;
|
|
situacion: string;
|
|
respuesta: string;
|
|
}) {
|
|
const paso = 1 / total;
|
|
// The one thing still tied to scroll. A drifting numeral never looks broken
|
|
// at any position it can be caught in, so it costs nothing and keeps the
|
|
// stage from feeling frozen between beats.
|
|
const cifraY = useTransform(progreso, [i * paso - paso, (i + 1) * paso + paso], [60, -60]);
|
|
|
|
return (
|
|
<motion.li
|
|
data-activa={activa ? 'true' : 'false'}
|
|
animate={{ opacity: activa ? 1 : 0, y: activa ? 0 : 26 }}
|
|
transition={{ duration: 0.45, ease: EASE }}
|
|
className="absolute inset-x-0 top-1/2 -translate-y-1/2"
|
|
style={{ pointerEvents: activa ? 'auto' : 'none' }}
|
|
>
|
|
<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>
|
|
|
|
{/* One card per frame — never both. Both live in the same grid cell so
|
|
the second can take the stage the first leaves, instead of stacking
|
|
under it: the question is asked, it goes, and then the answer is the
|
|
only thing on screen. They come in from opposite sides so the swap
|
|
reads as one card replacing another, not as one fading into the same
|
|
spot. */}
|
|
<div className="grid">
|
|
<motion.div
|
|
animate={{
|
|
opacity: respondida ? 0 : 1,
|
|
y: respondida ? -52 : 0,
|
|
rotate: respondida ? -3.4 : -0.8,
|
|
scale: respondida ? 0.96 : 1,
|
|
}}
|
|
transition={{ duration: 0.5, ease: EASE }}
|
|
style={{ pointerEvents: respondida ? 'none' : 'auto' }}
|
|
className="col-start-1 row-start-1 self-center justify-self-start 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, and coming in from the opposite margin. */}
|
|
<motion.div
|
|
animate={{
|
|
opacity: respondida ? 1 : 0,
|
|
y: respondida ? 0 : 64,
|
|
rotate: respondida ? 1.6 : 5,
|
|
scale: respondida ? 1 : 0.95,
|
|
}}
|
|
transition={{ duration: 0.55, ease: EASE }}
|
|
style={{ pointerEvents: respondida ? 'auto' : 'none' }}
|
|
className="col-start-1 row-start-1 self-center justify-self-end max-w-xl rounded-xl bg-azul-profundo p-7 shadow-[0_34px_80px_-30px_rgba(10,29,61,0.75)] 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>
|
|
</div>
|
|
</motion.li>
|
|
);
|
|
}
|