feat(recorrido): una fotografía por punto, bajo las capas azul y blanca
Cada punto de inflexión trae su imagen de fondo, relacionada con lo que plantea: una carretera de montaña sin final visible para «el siguiente salto no tiene mapa», un edificio a medio construir, la cosecha —el esfuerzo convertido en resultado—, cables cruzando el cielo, un puerto de contenedores, una ciudad entera a la vez. Pasan por FondoFoto, que las funde en luminosidad sobre el degradado azul → dorado: ninguna aporta color propio. Encima, el velo claro sostiene la pregunta en azul y el velo azul se lleva el fotograma entero para la respuesta. La foto da grano, nunca manda. Todas de Wikimedia Commons con licencia de reuso, acreditadas en creditosFotos. Descarté la del enredo de cables sobre una calle: bajo el velo seguía leyéndose el rótulo de un bar ajeno. El test del footer fijaba dos créditos a mano; ahora cuenta contra la propia lista, que es el único sitio donde se hace cumplir la atribución. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8f1fd2a74d
commit
156f78c5a8
9 files changed
+77
-14
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Footer } from '../Footer';
|
||||
import { creditosFotos } from '@/lib/content';
|
||||
|
||||
describe('<Footer />', () => {
|
||||
it('renders the tagline and the legal line', () => {
|
||||
@@ -12,11 +13,19 @@ describe('<Footer />', () => {
|
||||
render(<Footer />);
|
||||
expect(screen.queryByRole('heading', { level: 1 })).toBeNull();
|
||||
});
|
||||
it('credits the stock photos per their CC BY-SA 4.0 licence', () => {
|
||||
it('credits every reused photograph, with author, licence and modification notice', () => {
|
||||
render(<Footer />);
|
||||
expect(screen.getAllByText(/CC BY-SA 4\.0/)).toHaveLength(2);
|
||||
expect(screen.getByText(/Golden Gate Bridge/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Monte Nardi/)).toBeInTheDocument();
|
||||
expect(screen.getAllByText(/modificada/).length).toBe(2);
|
||||
// The licences these photographs carry require three things: naming the
|
||||
// author, linking the licence, and saying the image was modified. This is
|
||||
// the only place in the project where that obligation is enforced, so it
|
||||
// counts against `creditosFotos` itself rather than a number typed here —
|
||||
// adding a photograph without crediting it has to fail, and a hard-coded
|
||||
// count only fails once someone remembers to update it.
|
||||
for (const credito of creditosFotos) {
|
||||
expect(credito.desc).toMatch(/—\s*\S/); // the author, after the em dash
|
||||
expect(screen.getByText(credito.desc)).toHaveAttribute('href', credito.href);
|
||||
expect(screen.getAllByText(credito.licencia).length).toBeGreaterThan(0);
|
||||
}
|
||||
expect(screen.getAllByText(/modificada/)).toHaveLength(creditosFotos.length);
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import { useEffect, useRef, useState } from 'react';
|
||||
import { motion, useMotionValue, useMotionValueEvent, useSpring, useTransform } from 'framer-motion';
|
||||
import { Container } from '@/components/primitives/Container';
|
||||
import { FondoPuntos } from '@/components/primitives/FondoPuntos';
|
||||
import { FondoFoto } from '@/components/primitives/FondoFoto';
|
||||
import { ParDialogo } from '@/components/primitives/ParDialogo';
|
||||
import { EscenaDialogo } from '@/components/primitives/EscenaDialogo';
|
||||
import { IlustracionInflexion } from '@/components/primitives/IlustracionInflexion';
|
||||
@@ -13,6 +14,24 @@ import { fadeInUp, staggerContainer, viewportOnce, usePrefersReducedMotion, moti
|
||||
const items = puntosInflexion.items;
|
||||
const TOTAL = items.length;
|
||||
|
||||
// One photograph per inflection point, in the order the points are declared.
|
||||
// Asset wiring, not copy — `lib/content.ts` stays the source of truth for words
|
||||
// only. Every file is from Wikimedia Commons under a licence that allows reuse,
|
||||
// and every one is credited in `creditosFotos`; the footer renders that list.
|
||||
//
|
||||
// They are read through FondoFoto, which pushes them through
|
||||
// mix-blend-luminosity over the brand's navy→gold gradient, so no photograph
|
||||
// ever brings its own colour onto the page. Under the veils below they read as
|
||||
// texture, not as stock photography.
|
||||
const FOTOS = [
|
||||
'/images/recorrido/crecer.jpg', // a mountain road with no visible end
|
||||
'/images/recorrido/consolidarse.jpg', // a building still open to the weather
|
||||
'/images/recorrido/rentabilidad.jpg', // the harvest: effort turned into result
|
||||
'/images/recorrido/simplificar.jpg', // wires crossing a sky, lines multiplying
|
||||
'/images/recorrido/expandirse.jpg', // a container port, new markets
|
||||
'/images/recorrido/portafolio.jpg', // a whole city at once, nothing prioritised
|
||||
];
|
||||
|
||||
function Cabecera({ reduced }: { reduced: boolean }) {
|
||||
return (
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between md:gap-12">
|
||||
@@ -102,6 +121,8 @@ function Escenificado() {
|
||||
|
||||
const barra = useTransform(progreso, [0, 1], [0, 1]);
|
||||
const fondoY = useTransform(progreso, [0, 1], [0, -140]);
|
||||
// A slower drift than the dots, so the photograph sits behind them in depth.
|
||||
const fotoY = useTransform(progreso, [0, 1], [0, -70]);
|
||||
|
||||
return (
|
||||
<section className="bg-gris-claro relative">
|
||||
@@ -112,20 +133,45 @@ function Escenificado() {
|
||||
{/* One viewport of stage, six viewports of scroll to move through it. */}
|
||||
<div ref={contenedor} className="relative" style={{ height: `${TOTAL * 100}vh` }}>
|
||||
<div data-escena={activo} data-tiempo={tiempo} className="sticky top-0 h-screen overflow-hidden bg-gris-claro">
|
||||
<motion.div style={{ y: fondoY }} className="absolute inset-0">
|
||||
<FondoPuntos className="-bottom-10 -right-16 w-80 md:w-[34rem] text-azul-profundo/10" distance={32} />
|
||||
</motion.div>
|
||||
{/* The photograph of whichever point is on screen, crossfading with
|
||||
the beat. Only the active one is opaque; the rest sit at zero, so
|
||||
the browser still holds them decoded and the swap costs nothing. */}
|
||||
{items.map((item, i) => (
|
||||
<motion.div
|
||||
key={item.name}
|
||||
aria-hidden="true"
|
||||
animate={{ opacity: i === activo ? 1 : 0 }}
|
||||
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
|
||||
className="absolute inset-0"
|
||||
>
|
||||
<motion.div style={{ y: fotoY }} className="absolute -inset-y-24 inset-x-0">
|
||||
<FondoFoto src={FOTOS[i]} wash="" className="inset-0" />
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
{/* The frame itself turns over on the answer. The whole stage goes to
|
||||
azul-profundo, not a panel inside it — the answer is not a card on
|
||||
the reader's page, it is creeer's ground. */}
|
||||
{/* "Respetando la capa azul y blanco": the photograph never carries
|
||||
the frame, it only gives it grain. The light veil holds the
|
||||
question legible in azul-profundo type; the navy veil takes the
|
||||
whole stage over for the answer. Both are heavy on purpose —
|
||||
anything thinner and the copy starts fighting the photograph. */}
|
||||
<motion.div
|
||||
aria-hidden="true"
|
||||
animate={{ opacity: respondida ? 0 : 1 }}
|
||||
transition={{ duration: 0.55, ease: [0.22, 1, 0.36, 1] }}
|
||||
className="absolute inset-0 bg-gris-claro/[0.90]"
|
||||
/>
|
||||
<motion.div
|
||||
aria-hidden="true"
|
||||
animate={{ opacity: respondida ? 1 : 0 }}
|
||||
transition={{ duration: 0.55, ease: [0.22, 1, 0.36, 1] }}
|
||||
className="absolute inset-0 bg-azul-profundo"
|
||||
className="absolute inset-0 bg-azul-profundo/[0.93]"
|
||||
/>
|
||||
|
||||
<motion.div style={{ y: fondoY }} className="absolute inset-0">
|
||||
<FondoPuntos className="-bottom-10 -right-16 w-80 md:w-[34rem] text-azul-profundo/10" distance={32} />
|
||||
</motion.div>
|
||||
|
||||
<Container className="relative h-full">
|
||||
{/* left-6 / md:left-12, not left-0: an absolutely positioned child
|
||||
resolves against Container's padding box, so `left-0` lands on
|
||||
|
||||
+10
-2
@@ -181,9 +181,17 @@ export const footer = {
|
||||
legal: '© 2026 creeer',
|
||||
};
|
||||
|
||||
// CC BY-SA 4.0 requires attribution + a link to the licence + noting the
|
||||
// image was modified (both got a navy/gold duotone treatment).
|
||||
// Every reused photograph is credited here and the footer renders the list.
|
||||
// CC BY-SA / CC BY require attribution + a link to the licence + noting the
|
||||
// image was modified — all of them get the navy/gold duotone treatment, and the
|
||||
// six in `recorrido` are additionally cropped and resized.
|
||||
export const creditosFotos = [
|
||||
{ desc: 'Foto: Golden Gate Bridge — Dietmar Rabich', href: 'https://commons.wikimedia.org/wiki/File:San_Francisco_(CA,_USA),_Golden_Gate_Bridge_--_2022_--_3023_(bw).jpg', licencia: 'CC BY-SA 4.0', licenciaHref: 'https://creativecommons.org/licenses/by-sa/4.0/' },
|
||||
{ desc: 'Foto: amanecer, Monte Nardi — Tony 1212', href: 'https://commons.wikimedia.org/wiki/File:Sunrise_over_the_summit_of_Mount_Nardi_from_the_south.jpg', licencia: 'CC BY-SA 4.0', licenciaHref: 'https://creativecommons.org/licenses/by-sa/4.0/' },
|
||||
{ desc: 'Foto: montaña de Sheikh, Somalilandia — Abdifatah Y. Barwaaqo', href: 'https://commons.wikimedia.org/wiki/File:Sheikh_mountain,_Somaliland.jpg', licencia: 'dominio público', licenciaHref: 'https://commons.wikimedia.org/wiki/File:Sheikh_mountain,_Somaliland.jpg' },
|
||||
{ desc: 'Foto: edificio en construcción — PattayaPatrol', href: 'https://commons.wikimedia.org/wiki/File:DFC_1635_Unfinished_concrete_building_with_exposed_floors_and_metal_scaffolding_rising_along_its_side_green_safety_netting_hanging_from_the_structure.jpg', licencia: 'CC BY-SA 4.0', licenciaHref: 'https://creativecommons.org/licenses/by-sa/4.0/' },
|
||||
{ desc: 'Foto: cosechadora en el trigo — Michael Gäbler', href: 'https://commons.wikimedia.org/wiki/File:The_combine_Claas_Lexion_584_in_the_wheat_harvest.jpg', licencia: 'CC BY-SA 3.0', licenciaHref: 'https://creativecommons.org/licenses/by-sa/3.0/' },
|
||||
{ desc: 'Foto: pájaros sobre cables — Alex Martinez', href: 'https://commons.wikimedia.org/wiki/File:Birds_On_Wires_(Unsplash).jpg', licencia: 'CC0', licenciaHref: 'https://creativecommons.org/publicdomain/zero/1.0/' },
|
||||
{ desc: 'Foto: puerto de contenedores de Miami — James R. Tourtellotte', href: 'https://commons.wikimedia.org/wiki/File:Aerial_photograph_of_the_Port_of_Miami_Container_Port.jpg', licencia: 'dominio público', licenciaHref: 'https://commons.wikimedia.org/wiki/File:Aerial_photograph_of_the_Port_of_Miami_Container_Port.jpg' },
|
||||
{ desc: 'Foto: Cracovia desde el aire — Robin', href: 'https://commons.wikimedia.org/wiki/File:Aerial_photograph_of_Krak%C3%B3w.jpg', licencia: 'CC BY 2.0', licenciaHref: 'https://creativecommons.org/licenses/by/2.0/' },
|
||||
];
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 282 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 359 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 273 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 394 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 278 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
Reference in new issue
Block a user