diff --git a/e2e/mapa.e2e.ts b/e2e/mapa.e2e.ts new file mode 100644 index 0000000..b02f087 --- /dev/null +++ b/e2e/mapa.e2e.ts @@ -0,0 +1,92 @@ +import { expect, test } from '@playwright/test'; + +/* + * El mapa de la portada de géneros. + * + * Todo lo que se comprueba aquí se ha roto al menos una vez, y ninguna de las + * roturas daba error: el mapa seguía saliendo, solo que mal. Por eso son + * pruebas y no una mirada. + */ +test.describe('Mapa de géneros', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/generos'); + }); + + test('los puntos se pintan después de todas las tierras', async ({ page }) => { + // En un SVG no hay `z-index`: manda el orden en que está escrito. Con el + // punto dentro de su país, el relleno del país siguiente se lo comía por + // un lado y el punto salía mordido. + const mordidos = await page.evaluate(() => { + const fuera: string[] = []; + + for (const svg of document.querySelectorAll('svg[role="group"]')) { + const todos = [...svg.querySelectorAll('path, circle.punto')]; + const ultimaTierra = todos.findLastIndex((e) => e.tagName === 'path'); + const primerPunto = todos.findIndex((e) => e.classList.contains('punto')); + if (primerPunto !== -1 && primerPunto < ultimaTierra) { + fuera.push(svg.getAttribute('aria-label') ?? '?'); + } + } + return fuera; + }); + + expect(mordidos).toEqual([]); + }); + + test('el punto mide lo mismo en América que en la Península', async ({ page }) => { + // Los dos lienzos se dibujan a anchos muy distintos —seiscientas unidades + // de encuadre uno, ciento diez el otro—, así que un radio en unidades del + // mapa daba en España la mitad de punto. + const medir = async (pais: string) => { + const grupo = page.locator(`g.pais[aria-label^="${pais}"]`).first(); + const diana = (await grupo.locator('circle.golpe').boundingBox())!; + await page.mouse.move(diana.x + diana.width / 2, diana.y + diana.height / 2); + + const punto = page.locator('circle.punto.visible').first(); + await expect(punto).toBeAttached(); + return Math.round((await punto.boundingBox())!.width); + }; + + expect(await medir('España')).toBe(await medir('México')); + }); + + test('todos los países con géneros llevan el mismo relleno', async ({ page }) => { + // Hubo una versión con dos verdes, uno para la cuna y otro para donde solo + // se adoptó, y España —que solo adoptó el bolero— salía de otro color que + // el resto. Esa diferencia se cuenta en la lista, no en el mapa. + const rellenos = await page + .locator('g.pais:not(.elegido) path.tierra') + .evaluateAll((es) => [...new Set(es.map((e) => getComputedStyle(e).fill))]); + + expect(rellenos).toHaveLength(1); + }); + + test('el punto solo sale en el país señalado', async ({ page }) => { + // En reposo no hay ninguno: el mapa es de países, no de chinchetas. + await expect(page.locator('circle.punto.visible')).toHaveCount(0); + + const grupo = page.locator('g.pais[aria-label^="Cuba"]').first(); + const diana = (await grupo.locator('circle.golpe').boundingBox())!; + await page.mouse.move(diana.x + diana.width / 2, diana.y + diana.height / 2); + + await expect(page.locator('circle.punto.visible')).toHaveCount(1); + }); + + test('España se dibuja sin islas', async ({ page }) => { + // Canarias caía fuera del recuadro y acababa pintada sobre América; + // las Baleares eran cuatro píxeles sueltos al este. + const trazados = await page + .locator('g.pais[aria-label^="España"] path.tierra') + .first() + .evaluate((e) => (e.getAttribute('d')?.match(/M/g) ?? []).length); + + expect(trazados).toBe(1); + }); + + test('pulsar un país filtra la lista de géneros', async ({ page }) => { + await page.locator('g.pais[aria-label^="Cuba"]').first().click(); + + await expect(page.getByRole('heading', { name: 'Nació en Cuba' })).toBeVisible(); + await expect(page.getByRole('button', { name: /Quitar el filtro de país/ })).toBeVisible(); + }); +}); diff --git a/scripts/mapa/generar.mjs b/scripts/mapa/generar.mjs index 227be3e..da8b29b 100644 --- a/scripts/mapa/generar.mjs +++ b/scripts/mapa/generar.mjs @@ -231,6 +231,17 @@ const centro = {}; const principal = {}; let descartados = 0; +/** + * Países de los que solo se dibuja el trozo mayor. + * + * España es la Península más Canarias y Baleares. Canarias cae a mil + * kilómetros al suroeste, fuera del recuadro; las Baleares son cuatro píxeles + * sueltos al este. En un mapa que trata de géneros, ninguna de las dos dice + * nada que no diga ya la Península, y sueltas al borde del encuadre solo + * ensucian. + */ +const SIN_ISLAS = ['es']; + for (const codigo of PAISES) { const buenos = trozos[codigo].filter((d) => { const { ancho, alto } = tamano(d); @@ -238,7 +249,6 @@ for (const codigo of PAISES) { if (!vale) descartados++; return vale; }); - contorno[codigo] = unaDecimal(unir(buenos)); /* * Un punto donde agarrar el país. @@ -257,6 +267,8 @@ for (const codigo of PAISES) { const medio = (ns) => ns.reduce((t, n) => t + n, 0) / ns.length; centro[codigo] = [medio(xs), medio(ys)].map((n) => Math.round(n * 10) / 10); principal[codigo] = mayor; + + contorno[codigo] = unaDecimal(unir(SIN_ISLAS.includes(codigo) ? [mayor] : buenos)); } /** @@ -290,8 +302,8 @@ const encuadre = encuadrar(PAISES.filter((c) => !IBERIA.includes(c))); /* * El inserto encuadra solo la parte continental. Con Canarias dentro, el * rectángulo se estiraba mil kilómetros al suroeste y la Península quedaba del - * tamaño de una mota. Las islas se siguen dibujando; lo que cambia es el - * encuadre. + * tamaño de una mota. De España ya no se dibujan las islas; de Portugal sí, y + * Azores y Madeira caen fuera de este rectángulo, que es donde deben caer. */ const encuadreIberia = encuadrar(IBERIA, true); diff --git a/src/app.css b/src/app.css index 1852206..cf3fb7f 100644 --- a/src/app.css +++ b/src/app.css @@ -62,8 +62,8 @@ * elige, que es su trabajo en el resto del sitio. */ --c-dato: oklch(0.74 0.09 198); - /* El mismo tono, más flojo: «también se canta aquí», no otro dato. */ - --c-dato-tenue: oklch(0.55 0.075 198); + /* El mismo tono, un paso por debajo: «también se canta aquí», no otro dato. */ + --c-dato-tenue: oklch(0.66 0.085 198); --c-dato-vacio: oklch(0.29 0.03 255); --c-danger: oklch(0.72 0.16 22); @@ -215,8 +215,8 @@ /* En claro se invierten: el dato pintado tiene que ser oscuro sobre papel. */ --c-dato: oklch(0.5 0.085 200); - /* El mismo tono, más flojo: «también se canta aquí», no otro dato. */ - --c-dato-tenue: oklch(0.68 0.07 200); + /* El mismo tono, un paso por debajo: «también se canta aquí», no otro dato. */ + --c-dato-tenue: oklch(0.62 0.08 200); --c-dato-vacio: oklch(0.93 0.012 255); --c-danger: oklch(0.5 0.17 25); diff --git a/src/lib/components/MapaDeGeneros.svelte b/src/lib/components/MapaDeGeneros.svelte index d408835..44c1611 100644 --- a/src/lib/components/MapaDeGeneros.svelte +++ b/src/lib/components/MapaDeGeneros.svelte @@ -51,28 +51,46 @@ return espana ? [espana, ...resto] : resto; }); - /** El ancho de un encuadre, en unidades del mapa. */ - const anchoDe = (encuadre: string) => Number(encuadre.split(' ')[2]); - - /* - * El radio del punto, en píxeles de pantalla. - * - * No basta con hacerlo proporcional al encuadre: los dos mapas se dibujan a - * anchos distintos —América ocupa una columna y la Península menos de la - * mitad—, así que el mismo porcentaje daba un punto la mitad de grande en - * España. Se mide lo que ocupa cada lienzo y se convierte. - */ + /** Lo que mide el punto en pantalla, de radio. */ const PUNTO_PX = 7; let anchoAmerica = $state(0); + let altoAmerica = $state(0); let anchoPeninsula = $state(0); + let altoPeninsula = $state(0); - const radio = (anchoDelEncuadre: number, anchoEnPantalla: number) => - anchoEnPantalla > 0 ? (PUNTO_PX * anchoDelEncuadre) / anchoEnPantalla : 0; + /** + * El radio del punto en unidades del encuadre, para que salga del mismo + * tamaño en los dos mapas. + * + * Hacen falta las dos medidas de la caja, no solo el ancho: el dibujo se + * ajusta al lado que primero se queda corto, y con `max-height` eso le pasa + * a América en una ventana baja. Ahí la caja seguía midiendo cuatrocientos + * treinta y cuatro píxeles de ancho pero el mapa se pintaba a 0,57 en vez de + * a 0,70, y el punto salía dos píxeles y medio más pequeño que el de España. + */ + function radio(encuadre: string, ancho: number, alto: number): number { + const [, , anchoDelEncuadre, altoDelEncuadre] = encuadre.split(' ').map(Number); + const escala = Math.min(ancho / anchoDelEncuadre, alto / altoDelEncuadre); + return escala > 0 ? PUNTO_PX / escala : 0; + } const enAmerica = PAISES.filter((p) => !IBERIA.includes(p.codigo)); const enIberia = PAISES.filter((p) => IBERIA.includes(p.codigo)); + /* + * Qué país tiene el puntero encima y cuál tiene el foco. + * + * Antes lo resolvía `:hover` a secas, pero el punto ya no vive dentro de su + * país —está en una capa aparte, encima de todas las tierras—, y desde ahí + * ningún selector alcanza al país que lo enciende. Se anota aquí. + */ + let bajoPuntero = $state(''); + let conFoco = $state(''); + + const destacado = (codigo: string) => + bajoPuntero === codigo || conFoco === codigo || paisElegido === codigo; + /** Lo que lee un lector de pantalla y lo que sale al pasar el ratón. */ function resumen(codigo: string): string { const suyo = porPais[codigo]; @@ -110,10 +128,23 @@ role="group" aria-label="Géneros por país" bind:clientWidth={anchoAmerica} + bind:clientHeight={altoAmerica} > + {#each enAmerica as p (p.codigo)} - {@render pais(p, radio(anchoDe(ENCUADRE), anchoAmerica))} + {@render pais(p, radio(ENCUADRE, anchoAmerica, altoAmerica))} {/each} + + + {#each enAmerica as p (p.codigo)} + {@render chincheta(p, radio(ENCUADRE, anchoAmerica, altoAmerica))} + {/each} + 0} class:elegido={paisElegido === p.codigo} role="button" tabindex="0" aria-pressed={paisElegido === p.codigo} aria-label={resumen(p.codigo)} onclick={() => alElegir(p.codigo)} + onpointerenter={() => (bajoPuntero = p.codigo)} + onpointerleave={() => (bajoPuntero = bajoPuntero === p.codigo ? '' : bajoPuntero)} + onfocusin={() => (conFoco = p.codigo)} + onfocusout={() => (conFoco = conFoco === p.codigo ? '' : conFoco)} onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); @@ -197,7 +239,7 @@ > {resumen(p.codigo)} - + {:else} +{#snippet chincheta(p: (typeof PAISES)[number], r: number)} + {#if porPais[p.codigo]} +