From 379d80b915efe60ee1329818c67b68ebaf6db543 Mon Sep 17 00:00:00 2001 From: erikgh Date: Mon, 3 Aug 2026 11:58:08 -0600 Subject: [PATCH] fix(recorrido): no se puede quedar entre dos estados MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Las tarjetas iban atadas a la posición del scroll, así que cualquier punto intermedio era un fotograma válido: parando a media rueda, la tarjeta de creeer se quedaba como una losa gris translúcida y parecía un fallo de render. Ahora el scroll solo elige el tiempo —doce: pregunta, respuesta, pregunta…— y las tarjetas animan hacia ese tiempo por duración. Pares donde pares, aterrizan. Verificado en 41 paradas repartidas por la sección: cero opacidades a medias. Co-Authored-By: Claude Opus 5 --- components/primitives/EscenaDialogo.tsx | 120 ++++++++++++------------ components/sections/PuntosInflexion.tsx | 15 ++- 2 files changed, 70 insertions(+), 65 deletions(-) diff --git a/components/primitives/EscenaDialogo.tsx b/components/primitives/EscenaDialogo.tsx index 8fcc9c4..117efd3 100644 --- a/components/primitives/EscenaDialogo.tsx +++ b/components/primitives/EscenaDialogo.tsx @@ -6,19 +6,25 @@ 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. +// 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. // -// 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. +// 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, @@ -27,46 +33,26 @@ export function EscenaDialogo({ i: number; total: number; progreso: MotionValue; + activa: boolean; + respondida: boolean; 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]); + // 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 ( ); diff --git a/components/sections/PuntosInflexion.tsx b/components/sections/PuntosInflexion.tsx index 6919836..31767b8 100644 --- a/components/sections/PuntosInflexion.tsx +++ b/components/sections/PuntosInflexion.tsx @@ -88,10 +88,17 @@ function Escenificado() { // hilo conductor taught this project what an overdamped spring feels like. const progreso = useSpring(bruto, { stiffness: 260, damping: 42, restDelta: 0.0005 }); - const [activo, setActivo] = useState(0); + // Twelve beats, not a continuum: question, answer, question, answer… Scroll + // picks the beat and nothing else. When the cards were driven straight off + // the scroll value, every position in between was a frame you could stop on, + // and creeer's card sat there as a half-transparent grey slab. + const TIEMPOS = TOTAL * 2; + const [tiempo, setTiempo] = useState(0); useMotionValueEvent(progreso, 'change', (v) => { - setActivo(Math.min(TOTAL - 1, Math.max(0, Math.floor(v * TOTAL)))); + setTiempo(Math.min(TIEMPOS - 1, Math.max(0, Math.floor(v * TIEMPOS)))); }); + const activo = Math.floor(tiempo / 2); + const respondida = tiempo % 2 === 1; const barra = useTransform(progreso, [0, 1], [0, 1]); const fondoY = useTransform(progreso, [0, 1], [0, -140]); @@ -104,7 +111,7 @@ function Escenificado() { {/* One viewport of stage, six viewports of scroll to move through it. */}
-
+
@@ -140,6 +147,8 @@ function Escenificado() { i={i} total={TOTAL} progreso={progreso} + activa={i === activo} + respondida={i === activo && respondida} n={String(i + 1).padStart(2, '0')} {...item} />