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]}
+
+ {/if}
+{/snippet}
+