Firmar debajo dejaba la respuesta sonando a voz de la propia página hasta la última línea. Ahora cre≡er va delante de la frase y a tamaño lg (260px en escritorio): antes de leer, ya sabes quién contesta. md eran 87px —la escala del logotipo compuesto, más pequeña que el propio texto de la respuesta— y se leía como pie de foto. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
119 lines
4.6 KiB
TypeScript
119 lines
4.6 KiB
TypeScript
'use client';
|
|
|
|
import { motion, useTransform, type MotionValue } from 'framer-motion';
|
|
import { Wordmark } from '@/components/primitives/Wordmark';
|
|
|
|
// One beat of the pinned conversation. Not a card on a page — the whole frame
|
|
// is the message: the reader's situation set large on the light ground, then
|
|
// creeer's answer set large on azul-profundo, the section's own background
|
|
// turning over with it.
|
|
//
|
|
// One thing per frame, never both. The two halves share a grid cell so the
|
|
// answer takes the stage the question leaves instead of stacking under it, and
|
|
// they arrive from opposite sides so the swap reads as a relay.
|
|
//
|
|
// 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 the answer as a half-transparent slab that read as a
|
|
// rendering fault. Scroll now only picks the beat; the frame animates to it
|
|
// over time and always lands somewhere finished, wherever the reader lets go.
|
|
//
|
|
// Every scene stays mounted. Rendering 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], [70, -70]);
|
|
|
|
return (
|
|
<motion.li
|
|
data-activa={activa ? 'true' : 'false'}
|
|
animate={{ opacity: activa ? 1 : 0 }}
|
|
transition={{ duration: 0.4, ease: EASE }}
|
|
className="absolute inset-0 flex items-center"
|
|
style={{ pointerEvents: activa ? 'auto' : 'none' }}
|
|
>
|
|
<motion.span
|
|
aria-hidden="true"
|
|
style={{ y: cifraY }}
|
|
animate={{ color: respondida ? 'rgba(255,255,255,0.06)' : 'rgba(10,29,61,0.05)' }}
|
|
transition={{ duration: 0.5, ease: EASE }}
|
|
className="pointer-events-none absolute right-0 top-8 select-none font-sans font-black leading-none text-[11rem] md:text-[22rem]"
|
|
>
|
|
{n}
|
|
</motion.span>
|
|
|
|
<div className="relative grid w-full">
|
|
{/* The reader's situation. */}
|
|
<motion.div
|
|
animate={{ opacity: respondida ? 0 : 1, y: respondida ? -60 : 0 }}
|
|
transition={{ duration: 0.5, ease: EASE }}
|
|
style={{ pointerEvents: respondida ? 'none' : 'auto' }}
|
|
className="col-start-1 row-start-1 max-w-5xl"
|
|
>
|
|
<p className="text-[0.7rem] font-black uppercase tracking-[0.34em] text-dorado md:text-xs">
|
|
{n} · {name}
|
|
</p>
|
|
<p className="mt-6 font-sans font-black leading-[0.98] text-azul-profundo text-4xl md:text-6xl lg:text-7xl">
|
|
«{situacion}»
|
|
</p>
|
|
</motion.div>
|
|
|
|
{/* creeer's answer, on the navy the whole frame turns to. The point
|
|
label repeats here on purpose: with the question gone from the
|
|
screen, it is the only thing tying the answer to what it answers. */}
|
|
<motion.div
|
|
animate={{ opacity: respondida ? 1 : 0, y: respondida ? 0 : 60 }}
|
|
transition={{ duration: 0.55, ease: EASE }}
|
|
style={{ pointerEvents: respondida ? 'auto' : 'none' }}
|
|
className="col-start-1 row-start-1 max-w-5xl"
|
|
>
|
|
<div className="flex items-center gap-4">
|
|
<span aria-hidden="true" className="h-[3px] w-10 rounded bg-dorado" />
|
|
<p className="text-[0.7rem] font-black uppercase tracking-[0.34em] text-dorado md:text-xs">
|
|
{n} · {name}
|
|
</p>
|
|
</div>
|
|
|
|
{/* The mark leads the answer, and it is the size of a speaker, not of
|
|
a footnote. The reader has to know who is talking before they read
|
|
the sentence — signing it underneath left the answer sounding like
|
|
more of the page's own voice until the very last line. */}
|
|
<div className="mt-7">
|
|
<Wordmark size="lg" onDark />
|
|
</div>
|
|
|
|
<p className="mt-6 font-sans font-bold leading-[1.06] text-white text-3xl md:text-5xl lg:text-[3.4rem]">
|
|
{respuesta}
|
|
</p>
|
|
</motion.div>
|
|
</div>
|
|
</motion.li>
|
|
);
|
|
}
|