{"version":"https://jsonfeed.org/version/1.1","title":"PAIGAR","home_page_url":"https://paigar.eu/","feed_url":"https://paigar.eu/feed.json","description":"Portal de Apuntes, Ideas, Garabatos, Artilugios y Retrofuturismo","language":"es","items":[{"id":"https://paigar.eu/juego-tetris-tutorial/","url":"https://paigar.eu/juego-tetris-tutorial/","title":"Cómo programar el juego de Tetris desde cero","language":"es","content_html":"<p>Tetris lo escribió Alekséi Pájitnov en 1984, en un Electronika 60 ruso que ni siquiera tenía gráficos: las piezas se dibujaban con caracteres de texto. El nombre viene del prefijo griego <em>tetra</em> (cuatro, por las cuatro celdas que tiene cada pieza) y el deporte favorito de Pájitnov, el tenis. La historia de cómo el juego se filtró fuera del bloque soviético en plena Guerra Fría, pasó por Hungría, llegó a una empresa británica, terminó en Atari y Nintendo, y desató una guerra de licencias que duró años, es uno de los grandes culebrones de la industria del software. Pero la idea, la mecánica nuclear del juego, es de las cosas más limpias que ha producido el medio: siete piezas, diez columnas, gravedad constante, líneas que desaparecen cuando se completan. Cuarenta años después sigue siendo el ejemplo canónico de &quot;diseño de juego perfecto&quot;.</p>\n<p>Como tutorial, además, es ideal. Mete sobre la mesa cosas que los seis juegos anteriores de esta serie no han tocado: la representación de las piezas como <strong>matrices que se rotan</strong>, la <strong>gestión del tiempo</strong> para una caída acompasada que se acelera con los niveles, y sobre todo la lógica de <strong>detección y eliminación de líneas</strong>, que es deceptivamente simple. Si has seguido los anteriores, este se va a sentir como un salto natural; si llegas nuevo, también funciona porque es un sistema autocontenido.</p>\n<h2>La idea general antes de tocar código</h2>\n<p>Hay un tablero rectangular de <strong>10 columnas por 20 filas</strong>. Por arriba van apareciendo, una a una, <strong>piezas de cuatro celdas</strong> —los siete tetrominos clásicos— que caen automáticamente hacia abajo. El jugador puede moverlas a izquierda y derecha, rotarlas en su sitio, acelerar la caída o soltarlas de golpe. Cuando una pieza no puede bajar más porque hay suelo o porque se apoya en bloques ya fijados, se queda donde está y se convierte en parte del tablero. Aparece una nueva pieza arriba.</p>\n<p>Cuando una <strong>fila se completa entera</strong> —diez bloques de la columna 0 a la 9—, esa fila se elimina y todo lo que había encima cae una posición. Cuanto más alto se acumulan los bloques, menos sitio queda para meter las nuevas piezas; el juego termina cuando una pieza nueva ya no cabe al aparecer. Eso es todo. La gracia está en que el ritmo de caída se va acelerando a medida que limpias líneas, así que el juego se va volviendo más exigente sin necesidad de cambiar las reglas.</p>\n<h2>La cuadrícula como matriz 2D</h2>\n<p>La estructura central de todo el juego es un <strong>array de arrays</strong> que representa el tablero. Diez columnas, veinte filas. Cada celda guarda o bien <code>null</code> (vacía) o bien el color del bloque que la ocupa. Lo organizamos como <code>tablero[fila][columna]</code>, no al revés, porque así dibujar el tablero es un doble bucle natural por filas.</p>\n<pre><code class=\"language-js\">const COLUMNAS = 10;\nconst FILAS = 20;\nconst TAMANO = 30;            // píxeles por celda\n\nconst ANCHO = COLUMNAS * TAMANO;  // 300\nconst ALTO = FILAS * TAMANO;      // 600\n\nlet tablero = Array.from({ length: FILAS }, () =&gt; Array(COLUMNAS).fill(null));\n</code></pre>\n<p>Ese <code>Array.from({length: N}, () =&gt; Array(M).fill(null))</code> es la forma idiomática en JS para crear una matriz 2D <strong>sin compartir referencias</strong> entre filas. Si lo hicieras con <code>Array(FILAS).fill(Array(COLUMNAS).fill(null))</code> —que parece lo mismo—, todas las filas serían el mismo array y modificar una modificaría todas. Es uno de los gotchas clásicos.</p>\n<h2>Las siete piezas como matrices</h2>\n<p>Cada pieza la representamos también como una pequeña matriz: 1 = bloque, 0 = hueco. La I es de 1×4, la O de 2×2, las demás de 2×3. Los colores los fijo siguiendo la paleta canónica de Tetris (cyan para la I, amarillo para la O, etcétera).</p>\n<pre><code class=\"language-js\">const PIEZAS = {\n  I: [[1, 1, 1, 1]],\n  O: [[1, 1], [1, 1]],\n  T: [[0, 1, 0], [1, 1, 1]],\n  L: [[0, 0, 1], [1, 1, 1]],\n  J: [[1, 0, 0], [1, 1, 1]],\n  S: [[0, 1, 1], [1, 1, 0]],\n  Z: [[1, 1, 0], [0, 1, 1]],\n};\nconst COLORES = {\n  I: '#00d4d4', O: '#d4d400', T: '#a040c0',\n  L: '#d48028', J: '#3050d0', S: '#40c440', Z: '#d44040',\n};\n</code></pre>\n<p>Cuando aparece una pieza nueva, elegimos un tipo al azar y guardamos una <strong>copia</strong> de su matriz —no el original—, porque la pieza activa va a mutar (rotar) y no queremos contaminar la plantilla de su tipo:</p>\n<pre><code class=\"language-js\">function clonarMatriz(m) {\n  return m.map((fila) =&gt; [...fila]);\n}\n\nfunction nuevaPieza() {\n  const TIPOS = Object.keys(PIEZAS);\n  const tipo = TIPOS[Math.floor(Math.random() * TIPOS.length)];\n  pieza = { tipo, forma: clonarMatriz(PIEZAS[tipo]), color: COLORES[tipo] };\n  x = Math.floor((COLUMNAS - pieza.forma[0].length) / 2);\n  y = 0;\n}\n</code></pre>\n<p>La pieza activa la guardamos como un objeto con <code>tipo</code>, <code>forma</code> y <code>color</code>, más dos variables <code>x</code> e <code>y</code> que indican la <strong>posición de su esquina superior izquierda dentro del tablero</strong>. Ese par <code>(x, y)</code> es lo único que cambia cuando la pieza se mueve.</p>\n<h2>La función <code>colisiona</code>: el corazón de todo</h2>\n<p>Antes de mover, rotar o fijar una pieza, hay que saber si el movimiento sería válido. Toda esa lógica vive en una sola función que centraliza la pregunta &quot;¿cabe esta forma en esta posición?&quot;:</p>\n<pre><code class=\"language-js\">function colisiona(forma, px, py) {\n  for (let f = 0; f &lt; forma.length; f++) {\n    for (let c = 0; c &lt; forma[f].length; c++) {\n      if (!forma[f][c]) continue;          // celda vacía de la pieza, ignorar\n      const tx = px + c;\n      const ty = py + f;\n      if (tx &lt; 0 || tx &gt;= COLUMNAS) return true;   // fuera por los lados\n      if (ty &gt;= FILAS) return true;                 // fuera por abajo\n      if (ty &gt;= 0 &amp;&amp; tablero[ty][tx]) return true;  // pisa un bloque fijo\n    }\n  }\n  return false;\n}\n</code></pre>\n<p>Recibe la <code>forma</code> (no la pieza completa, solo su matriz) y una posición tentativa <code>(px, py)</code>. Recorre cada celda de la forma; si la celda es 1, comprueba si su posición proyectada en el tablero saldría de los límites o pisaría un bloque ya fijado. Devuelve <code>true</code> en cuanto encuentra un problema.</p>\n<p>Este detalle de no recibir <code>pieza</code> sino <code>forma</code> es importante: nos permite reutilizar la función para preguntar &quot;¿cabría la pieza si la moviéramos un paso a la derecha?&quot; o &quot;¿cabría si la rotáramos?&quot;. Pasamos una posición o forma hipotética y obtenemos la respuesta sin tocar el estado real del juego.</p>\n<h2>Movimiento y caída</h2>\n<p>Con <code>colisiona</code> definida, mover lateralmente es trivial:</p>\n<pre><code class=\"language-js\">function moverHorizontal(dx) {\n  if (!colisiona(pieza.forma, x + dx, y)) x += dx;\n}\n</code></pre>\n<p>Si el movimiento no causaría colisión, lo aplicamos. Si causaría colisión, no hacemos nada y la pieza se queda donde estaba. Lo mismo para la caída paso a paso, con un matiz importante:</p>\n<pre><code class=\"language-js\">function moverAbajo() {\n  if (!colisiona(pieza.forma, x, y + 1)) {\n    y++;\n    return true;\n  }\n  fijarPieza();\n  return false;\n}\n</code></pre>\n<p>Si la pieza puede bajar, baja. Si no, <strong>se fija al tablero</strong>. Es decir: el momento exacto en que una pieza colisionaría al bajar es el momento en que deja de ser pieza activa y pasa a ser parte permanente del tablero. Esa transición —de pieza-en-movimiento a bloques-fijos— es el latido del juego.</p>\n<h2>La rotación como transformación de matriz</h2>\n<p>La parte más bonita del tutorial. Rotar una pieza 90 grados en sentido horario es exactamente lo mismo que rotar su matriz: la columna 0 de la matriz original pasa a ser la última fila de la rotada (en orden inverso), la columna 1 pasa a ser la penúltima, y así. La fórmula es:</p>\n<pre><code>rotada[c][N - 1 - f] = original[f][c]\n</code></pre>\n<p>donde <code>N</code> es el número de filas de la matriz original. Implementado en código:</p>\n<pre><code class=\"language-js\">function rotar() {\n  if (pieza.tipo === 'O') return;   // El cuadrado no cambia al rotar\n  const N = pieza.forma.length;\n  const M = pieza.forma[0].length;\n  const nueva = Array.from({ length: M }, () =&gt; Array(N).fill(0));\n  for (let f = 0; f &lt; N; f++) {\n    for (let c = 0; c &lt; M; c++) {\n      nueva[c][N - 1 - f] = pieza.forma[f][c];\n    }\n  }\n  if (!colisiona(nueva, x, y)) pieza.forma = nueva;\n}\n</code></pre>\n<p>Tres detalles de los que merece la pena hablar:</p>\n<p><strong>Primero</strong>, la pieza O es un cuadrado simétrico, así que rotarla no cambia nada visualmente. Saltamos esa rotación al inicio para no gastar trabajo. No es estrictamente necesario —la rotación del 2×2 da el mismo 2×2 y sería equivalente— pero es buena práctica.</p>\n<p><strong>Segundo</strong>, la matriz nueva tiene dimensiones invertidas: si la original era <code>N×M</code>, la rotada es <code>M×N</code>. Por eso la I, que era <code>1×4</code> (una fila de cuatro celdas), después de rotar es <code>4×1</code> (una columna de cuatro celdas). Si dibujáramos antes y después no veríamos un cuadrado raro, sino la barra vertical clásica.</p>\n<p><strong>Tercero</strong>, antes de aceptar la rotación verificamos que la nueva forma cabría en la posición actual. Si rotar la pieza la haría chocar con una pared o con bloques ya fijados, <strong>simplemente no rotamos</strong>. En implementaciones más sofisticadas existe el concepto de <em>wall kick</em> —si la rotación choca contra la pared, se intenta desplazar uno o dos espacios para que entre—, pero para una versión de tutorial limpia, no rotar es la decisión correcta.</p>\n<h2>Fijar la pieza al tablero</h2>\n<p>Cuando <code>moverAbajo()</code> falla, llamamos a <code>fijarPieza</code>. Esta función &quot;imprime&quot; la pieza activa en el tablero y luego intenta una nueva pieza:</p>\n<pre><code class=\"language-js\">function fijarPieza() {\n  for (let f = 0; f &lt; pieza.forma.length; f++) {\n    for (let c = 0; c &lt; pieza.forma[f].length; c++) {\n      if (pieza.forma[f][c]) {\n        if (y + f &lt; 0) continue;   // fuera del tablero por arriba: ignorar\n        tablero[y + f][x + c] = pieza.color;\n      }\n    }\n  }\n  eliminarLineas();\n  nuevaPieza();\n}\n</code></pre>\n<p>El <code>if (y + f &lt; 0)</code> evita escribir fuera del tablero cuando una pieza alta queda parcialmente arriba del techo —improbable pero defensivo—. Tras fijar, comprobamos si se han completado líneas y generamos una nueva pieza. Si la nueva pieza no cabe al aparecer, el juego ha terminado.</p>\n<h2>La eliminación de líneas</h2>\n<p>El núcleo conceptual del juego. Recorremos el tablero <strong>de abajo arriba</strong>, y cuando encontramos una fila donde todas las celdas tienen color (es decir, no hay ningún <code>null</code>), la quitamos del array y añadimos una nueva fila vacía por arriba:</p>\n<pre><code class=\"language-js\">function eliminarLineas() {\n  let eliminadas = 0;\n  for (let f = FILAS - 1; f &gt;= 0; f--) {\n    if (tablero[f].every((c) =&gt; c)) {\n      tablero.splice(f, 1);\n      tablero.unshift(Array(COLUMNAS).fill(null));\n      eliminadas++;\n      f++;   // el splice ha desplazado todo hacia abajo, hay que re-mirar esta f\n    }\n  }\n  if (eliminadas &gt; 0) {\n    const puntosPorN = [0, 100, 300, 500, 800];\n    puntos += puntosPorN[eliminadas] * nivel;\n    lineas += eliminadas;\n    nivel = Math.floor(lineas / 10) + 1;\n    intervaloCaida = Math.max(80, 800 - (nivel - 1) * 60);\n  }\n}\n</code></pre>\n<p>El detalle del <code>f++</code> después de un <code>splice</code> merece comentario. Cuando eliminamos la fila <code>f</code>, todas las filas que estaban encima bajan una posición; la fila que <strong>ahora</strong> ocupa el índice <code>f</code> no la hemos comprobado. Si la dejamos pasar, podríamos saltarnos una línea completa que acaba de bajar. Por eso incrementamos <code>f</code> para compensar el <code>f--</code> del bucle, manteniendo el índice. Es uno de esos pequeños bailes de índices que cuestan veinte minutos la primera vez que los escribes y cinco segundos cada vez después.</p>\n<p>La puntuación sigue la tabla canónica de Tetris: 100 puntos por una línea, 300 por dos, 500 por tres y 800 por cuatro (el famoso <em>Tetris</em>, una sola jugada de 4 líneas a la vez). Multiplicado por el nivel actual, así que ir más rápido vale más. El nivel sube cada 10 líneas y la velocidad de caída baja con él, hasta un mínimo de 80ms por paso.</p>\n<h2>El bucle del juego con tiempo real</h2>\n<p>Esta es la primera vez en la serie que necesitamos <strong>tiempo real, no por frames</strong>. Si hiciéramos que la pieza bajara una posición cada <code>requestAnimationFrame</code>, en pantallas a 60Hz iría a 60 caídas por segundo. Lo que queremos es que baje según un intervalo en milisegundos que vamos ajustando con el nivel. La solución es acumular el delta de tiempo entre frames y avanzar la caída solo cuando hayamos acumulado el suficiente:</p>\n<pre><code class=\"language-js\">let ultimoTiempo = 0;\nlet contadorCaida = 0;\nlet intervaloCaida = 800;\n\nfunction bucle(tiempo) {\n  if (!ultimoTiempo) ultimoTiempo = tiempo;\n  const delta = tiempo - ultimoTiempo;\n  ultimoTiempo = tiempo;\n\n  contadorCaida += delta;\n  if (contadorCaida &gt;= intervaloCaida) {\n    moverAbajo();\n    contadorCaida = 0;\n  }\n\n  dibujar();\n  requestAnimationFrame(bucle);\n}\n</code></pre>\n<p><code>requestAnimationFrame</code> pasa al callback un timestamp en milisegundos. Restamos el tiempo del frame anterior para obtener cuánto ha pasado realmente, y vamos sumando ese delta a un contador. Cuando el contador supera el intervalo de caída actual, hacemos bajar la pieza y reseteamos. El resto del frame, dibujamos. Este patrón —acumular delta para acciones discretas— funciona para cualquier mecánica tipo &quot;X cosa cada Y milisegundos&quot; y es muy reutilizable.</p>\n<h2>Dibujado</h2>\n<p>La función de dibujo recorre el tablero pintando los bloques fijos y luego pinta la pieza activa encima. La rejilla tenue de fondo es opcional pero ayuda mucho a la legibilidad de las piezas:</p>\n<pre><code class=\"language-js\">function dibujarCelda(cx, cy, color) {\n  ctx.fillStyle = color;\n  ctx.fillRect(cx * TAMANO, cy * TAMANO, TAMANO, TAMANO);\n  ctx.fillStyle = 'rgba(255,255,255,0.18)';\n  ctx.fillRect(cx * TAMANO, cy * TAMANO, TAMANO, 3);            // brillo arriba\n  ctx.fillStyle = 'rgba(0,0,0,0.25)';\n  ctx.fillRect(cx * TAMANO, cy * TAMANO + TAMANO - 3, TAMANO, 3); // sombra abajo\n}\n</code></pre>\n<p>Las dos franjas adicionales —una clara arriba, una oscura abajo— dan a cada bloque un pequeño efecto de bisel que evita que el tablero se vea plano. Es el truco más barato para que un puzzle de cuadrículas no parezca una hoja de cálculo.</p>\n<h2>Controles</h2>\n<p>Asignamos las teclas estándar: izquierda/derecha mueven la pieza, arriba la rota, abajo acelera la caída por una celda (soft drop), espacio la suelta de golpe hasta el fondo (hard drop). Cada acción reinicia el contador de caída para evitar comportamientos raros como que pulsar abajo justo antes de un tick automático cuente como dos pasos:</p>\n<pre><code class=\"language-js\">document.addEventListener('keydown', (e) =&gt; {\n  if (e.key === 'ArrowLeft')  moverHorizontal(-1);\n  if (e.key === 'ArrowRight') moverHorizontal(1);\n  if (e.key === 'ArrowUp')    rotar();\n  if (e.key === 'ArrowDown') { moverAbajo(); contadorCaida = 0; }\n  if (e.key === ' ')          caidaInstantanea();\n});\n</code></pre>\n<p><code>caidaInstantanea</code> es un loop simple: mientras la pieza pueda bajar, baja; cuando ya no, fijamos:</p>\n<pre><code class=\"language-js\">function caidaInstantanea() {\n  while (!colisiona(pieza.forma, x, y + 1)) y++;\n  fijarPieza();\n}\n</code></pre>\n<h2>Cierre</h2>\n<p>Tetris funcionó en 1984 sobre una máquina sin gráficos y sigue funcionando hoy en una página HTML con menos de trescientas líneas de JavaScript. Los conceptos centrales —matriz 2D para el tablero, formas como matrices pequeñas, una función de colisión unificada, rotación por transposición de matriz, eliminación de líneas con <code>splice + unshift</code>, gravedad acompasada por delta time— son patrones que se repiten en muchos otros juegos del mismo género: Columns, Dr. Mario, Puyo Puyo, Lumines. Si has llegado hasta aquí entendiendo cada paso, en realidad has aprendido una familia entera de juegos, no uno solo.</p>\n<p>La versión que tienes encima del artículo está embebida en la propia página y funciona con teclado en escritorio y con tap/swipe en móvil. Si quieres trastear con el código, lo tienes todo en una sola IIFE autocontenida; cambiar los colores, las dimensiones del tablero o la curva de velocidad es un par de constantes. Si quieres más profundidad, el siguiente nivel sería implementar el sistema de <em>wall kicks</em> (Super Rotation System), añadir un <em>ghost piece</em> que muestre dónde caería la pieza si la soltaras, y meter una pieza guardada (<em>hold</em>) que se pueda intercambiar con la actual. Son tres mejoras independientes que multiplican el placer del juego sin tocar la mecánica nuclear que acabamos de construir.</p>\n","date_published":"2026-05-04T00:00:00.000Z","image":"https://paigar.eu/juego-tetromino.png"},{"id":"https://paigar.eu/autonomo-societario-discriminacion/","url":"https://paigar.eu/autonomo-societario-discriminacion/","title":"Autónomo societario: la trampa perfecta del sistema","language":"es","content_html":"<p>Hay una forma de trabajar por cuenta propia que el sistema ha diseñado para que te cueste más, te proteja menos y, encima, nadie te lo haya explicado antes de que cayeras en ella. Se llama ser autónomo societario, y lo más probable es que hayas llegado ahí no por ambición ni por facturar una fortuna, sino precisamente por intentar resolver un problema cotidiano y absurdo que el propio sistema te había creado.</p>\n<h2>El embrollo del IRPF que nadie sabe gestionar</h2>\n<p>Cuando trabajas como autónomo persona física y tus clientes son principalmente pequeñas empresas y otros autónomos, cada factura que emites lleva una retención de IRPF. En teoría, el mecanismo es sencillo: tú facturas, tu cliente descuenta esa retención del total que te paga y se encarga de ingresarla a Hacienda en tu nombre. Tú, al hacer la declaración anual, ya tienes ese dinero adelantado al fisco y el resultado es más o menos equilibrado.</p>\n<p>En teoría.</p>\n<p>La realidad del tejido empresarial español es bastante más caótica. Una parte significativa de las pymes y los autónomos con los que trabajas no sabe gestionar correctamente esas retenciones. Algunos no saben que tienen esa obligación. Otros lo hacen mal. El resultado es que a final de año te encuentras ante una situación kafkiana: Hacienda considera que has ingresado unos rendimientos que en realidad no has cobrado íntegramente (porque parte era retención), pero esas retenciones tampoco han llegado al fisco. El dinero ha desaparecido en un limbo contable y tú tienes que ponerte a gestionar el caos cliente por cliente, factura por factura, reclamando unos importes que son técnicamente tuyos pero que nunca viste.</p>\n<p>La solución que te propone cualquier asesor con experiencia es montar una sociedad limitada. Las SL no aplican retención de IRPF en sus facturas comerciales; eso se gestiona cuando la sociedad te paga a ti tu nómina. El problema desaparece del trato con clientes y se centraliza donde debería estar: en tu relación fiscal con tu propia empresa. Limpio, ordenado, lógico.</p>\n<p>Y entonces descubres que eres autónomo societario.</p>\n<h2>Una categoría que nadie te anuncia</h2>\n<p>Constituyes la SL, te das de alta como administrador, y la Seguridad Social te encuadra silenciosamente en una figura que tiene nombre propio pero que muy poca gente conoce hasta que le afecta: el trabajador autónomo societario. Quien tiene más de un 25% de participación en una sociedad mercantil y ejerce funciones de dirección o gerencia entra automáticamente en esta categoría. No hay aviso previo, no hay folleto informativo, no hay nadie al otro lado de la ventanilla que te explique que acabas de cambiar de régimen y que eso tiene consecuencias.</p>\n<p>A partir de ese momento, sigues siendo autónomo a todos los efectos —cotizas en el RETA, te abonas tu cuota mensual, rellenas los mismos modelos trimestrales— pero con un conjunto de restricciones y penalizaciones específicas que no comparten el resto de autónomos. Has resuelto el problema del IRPF y a cambio has entrado en una categoría especialmente maltratada por el sistema.</p>\n<h2>La base mínima de cotización: pagando por lo que no ganas</h2>\n<p>Uno de los cambios más recientes en el sistema de autónomos fue el paso a la cotización por ingresos reales, que se implantó de forma escalonada desde 2023. La idea es justa en su concepción: que cada trabajador por cuenta propia cotice en función de lo que realmente gana. Para los autónomos ordinarios, esto significa poder elegir bases de cotización bajas en los tramos inferiores de renta. Para el autónomo societario, no: la ley establece una base mínima obligatoria que no puede bajar de los 1.000 euros mensuales si ha estado dado de alta más de 90 días en el año.</p>\n<p>La premisa implícita del legislador es que quien monta una sociedad lo hace porque gana mucho dinero. Es un prejuicio de clase disfrazado de normativa. La realidad —que cualquier asesor fiscal puede confirmar con su propia cartera de clientes— es que una enorme proporción de autónomos societarios son pequeños profesionales, consultores, diseñadores, programadores o técnicos que facturan cifras modestas y que optaron por la SL precisamente por los problemas que hemos descrito, no porque sean empresarios de éxito con cuentas en el extranjero.</p>\n<p>Y si 2025 ya era injusto, 2026 ha sido directamente un golpe. La base mínima de cotización para autónomos societarios saltó de los 1.000 euros a los 1.424,40 euros mensuales, lo que significa que la cuota mínima mensual ha pasado de 314 euros a 448 euros. Un incremento de más del 42% de un año para otro. Un autónomo que factura 1.200 euros al mes y tiene gastos fijos está pagando su cuota de autónomo con dinero que no gana. El sistema considera que eso es imposible. El sistema se equivoca.</p>\n<h2>La tarifa plana: un derecho recuperado a regañadientes</h2>\n<p>Hasta bien entrado 2020, los autónomos societarios directamente no tenían acceso a la tarifa plana de arranque —esa reducción de la cuota mensual durante el primer año de actividad que se creó como incentivo al emprendimiento. La Seguridad Social interpretaba que la figura del societario era incompatible con el espíritu de la medida. Hubo que llegar a varias sentencias del Tribunal Supremo para que el criterio cambiara y los nuevos autónomos societarios pudieran acogerse a los 80 euros mensuales del primer año.</p>\n<p>El problema es que esas sentencias tienen efecto retroactivo de solo cuatro años, lo que significa que todos los autónomos societarios que pagaron de más antes de ese cambio de criterio y que habían superado ese plazo perdieron su derecho a reclamación. El Estado se benefició durante años de una interpretación restrictiva que los tribunales acabaron declarando injusta, y luego limitó la reparación para que afectara al menor número posible de personas. No fue un error administrativo que se corrigió con diligencia; fue una posición sostenida hasta que se hizo insostenible.</p>\n<h2>La jubilación activa: el cierre que nadie esperaba</h2>\n<p>La jubilación activa es el sistema que permite compatibilizar el cobro de la pensión con la continuidad de la actividad laboral. Para muchos autónomos mayores que no quieren o no pueden retirarse de golpe, es una herramienta fundamental de transición. Para los autónomos societarios con control efectivo de su empresa —es decir, para la mayoría— el acceso es extremadamente complicado.</p>\n<p>La Seguridad Social ha mantenido históricamente un criterio restrictivo: como la jubilación del societario no implica la extinción de los contratos de sus trabajadores (la SL sobrevive), no se cumple la finalidad de &quot;conservación del empleo&quot; que justifica la medida. Y tras los cambios normativos de abril de 2025, la situación ha empeorado: ya no es posible cobrar el 100% de la pensión desde el primer día para quien solo realiza funciones de propiedad sin trabajo efectivo. El acceso pleno exige acumular años de demora de la jubilación o reorganizar el rol dentro de la empresa de una forma que en muchos casos resulta artificial o directamente impracticable.</p>\n<p>Una vez más, la persona física que ha trabajado toda su vida y llega a los 67 años tiene más margen de maniobra que el societario que ha construido exactamente la misma trayectoria pero a través de una estructura mercantil.</p>\n<h2>El paro que existe pero no existe</h2>\n<p>Desde 2019, los autónomos cotizan obligatoriamente por cese de actividad, lo que se presentó en su momento como el gran avance que equiparaba a los trabajadores por cuenta propia con los asalariados en materia de protección ante el desempleo. En la práctica, la equiparación es más nominal que real.</p>\n<p>El cese de actividad tiene requisitos de acceso estrictos —hay que acreditar causas económicas, técnicas, productivas u organizativas de suficiente entidad— y su duración máxima es considerablemente inferior a la de una prestación contributiva por desempleo equivalente. No es el paro; es una prestación temporal con condiciones de acceso más exigentes. Para el autónomo societario, además, la relación entre sus ingresos reales como administrador y los rendimientos de la sociedad añade capas de complejidad que pueden dificultar aún más la acreditación de las causas que dan derecho al cese.</p>\n<p>Y cuando se agota esa prestación, o cuando nunca se cumplieron los requisitos para acceder a ella, ¿qué queda?</p>\n<h2>El subsidio de los 52 que nunca llegará</h2>\n<p>El subsidio para mayores de 52 años del SEPE es una de las protecciones más importantes del sistema para trabajadores que se quedan sin empleo cerca de la edad de jubilación. Permite seguir cotizando hasta los 65 o 67 años y proporciona unos ingresos de alrededor de 480 euros mensuales mientras se busca trabajo o se espera la pensión. Para los autónomos, incluyendo los societarios, ese subsidio no existe.</p>\n<p>El motivo técnico es que para acceder a él hay que haber cotizado al menos seis años por desempleo en el Régimen General, algo que los autónomos no hacen porque cotizan en el RETA y por cese de actividad. El SEPE lo deniega sistemáticamente aunque el autónomo lleve treinta años cotizando sin interrupción y haya construido toda su vida profesional trabajando para sí mismo. Las organizaciones de autónomos llevan años reclamando el acceso a esta protección; el Ministerio de Trabajo responde que &quot;el ámbito de la reforma del desempleo asistencial es el trabajo por cuenta ajena&quot;. Punto final.</p>\n<p>Lo mismo ocurre con las ayudas de complemento salarial hasta el Ingreso Mínimo Vital para personas en activo: el diseño de esas prestaciones asistenciales parte de la figura del trabajador asalariado y no contempla adecuadamente la realidad del autónomo que factura poco y tiene meses con ingresos muy irregulares.</p>\n<h2>¿Qué queda, entonces?</h2>\n<p>Lo que queda es seguir trabajando. Sin red de seguridad real si la actividad flaquea. Sin jubilación activa si quieres retirarte progresivamente. Sin subsidio si llegas a los 52 con el negocio cerrado. Sin tarifa plana histórica si la montaste antes de que el Supremo dijera lo que era de sentido común. Sin poder cotizar por lo que realmente ganas si tus ingresos son bajos.</p>\n<p>La figura del autónomo societario nació como una solución práctica a problemas concretos del mercado laboral español —la complejidad fiscal, la gestión de retenciones, la separación del patrimonio personal y profesional— y el sistema la ha convertido en un cajón donde acumular restricciones y excepciones desfavorables con la excusa de que quien monta una sociedad debe de ser rico.</p>\n<p>El resultado es una categoría de trabajadores que cotiza más que el resto, tiene acceso a menos prestaciones, y cuya única salida cuando el cuerpo o el mercado dicen basta es seguir adelante. No por vocación, sino porque el sistema no ha dejado otra puerta abierta.</p>\n<p>Quizás algún día alguien en la Seguridad Social visite la realidad de las pequeñas sociedades unipersonales españolas antes de diseñar la siguiente norma. Mientras tanto, toca seguir facturando.</p>\n","date_published":"2026-05-03T00:00:00.000Z","image":"https://paigar.eu/autonomo-societario-discriminacion.png"},{"id":"https://paigar.eu/validar-nif-cif-formulario/","url":"https://paigar.eu/validar-nif-cif-formulario/","title":"Cómo validar un NIF español en un formulario web (con CIF incluido)","language":"es","content_html":"<p>Si tu formulario solo necesita identificar personas físicas, el <a href=\"https://paigar.eu/validar-dni-nie-pasaporte/\">artículo sobre validación de documentos de identidad</a> cubre ese escenario por completo: DNI, NIE y pasaporte, con toda la lógica de letra de control. Este artículo es para cuando el mismo campo «NIF» también puede recibir el identificador de una empresa, una asociación o un organismo público.</p>\n<p>Conviene aclarar un matiz desde el principio: el autónomo persona física tributa con su DNI como NIF fiscal, así que en ese caso el documento de la persona y el del contribuyente son el mismo. Pero en cuanto hay una sociedad de por medio —una limitada, una cooperativa, una fundación—, aparece el CIF, y con él un algoritmo de validación completamente distinto que merece su propio tratamiento.</p>\n<p>El NIF —Número de Identificación Fiscal— es el identificador fiscal único en España. Para las personas físicas españolas equivale al DNI; para los extranjeros residentes es el NIE; para las personas jurídicas (empresas, asociaciones, organismos públicos...) es lo que durante décadas se llamó CIF y que desde 2008 se denomina oficialmente NIF de persona jurídica, aunque en la práctica todo el mundo sigue llamándolo CIF. Los tres conviven en el mismo campo.</p>\n<h2>DNI y NIE: terreno conocido</h2>\n<p>El DNI y el NIE son los mismos que ya conocemos. Ocho dígitos más una letra de control para el DNI; la letra inicial <code>X</code>, <code>Y</code> o <code>Z</code> seguida de siete dígitos y una letra de control para el NIE. En ambos casos la letra se calcula con el mismo algoritmo: módulo 23 del número sobre la cadena <code>TRWAGMYFPDXBNJZSQVHLCKE</code>.</p>\n<p>No voy a repetir aquí los detalles porque los tienes explicados con calma en el artículo sobre documentos de identidad. Lo que sí importa para el validador de NIF es tener claro que estos dos formatos hay que detectarlos primero, antes de intentar interpretar lo que queda como un CIF.</p>\n<h2>El CIF: una letra de tipo y un carácter de control que puede ser letra o dígito</h2>\n<p>El CIF tiene una estructura diferente: una letra que identifica el tipo de entidad, seguida de siete dígitos, y un carácter final de control que —y aquí viene el primer matiz— puede ser tanto una letra como un dígito, dependiendo del tipo de entidad.</p>\n<p>La letra inicial indica la naturaleza jurídica: <code>A</code> para sociedades anónimas, <code>B</code> para limitadas, <code>F</code> para cooperativas, <code>G</code> para asociaciones y fundaciones, <code>Q</code> para organismos públicos, y así hasta una veintena de opciones. Esa letra no es solo cosmética: condiciona qué tipo de carácter de control es válido al final.</p>\n<pre><code>[Letra de tipo] + [7 dígitos] + [Letra o dígito de control]\n\nEjemplo: A  1 2 3 4 5 6 7  4\n         ^  ───────────── ^\n    tipo S.A.   dígitos   control\n</code></pre>\n<p>La regla para el carácter de control es esta: los tipos <code>P</code>, <code>Q</code>, <code>S</code> y <code>W</code> (corporaciones locales, organismos públicos, órganos de la Administración y establecimientos permanentes de entidades no residentes) deben terminar siempre con una letra. El resto de tipos aceptan indistintamente letra o dígito —ambos son representaciones válidas del mismo valor.</p>\n<h2>El algoritmo del CIF, paso a paso</h2>\n<p>El cálculo del carácter de control opera sobre los siete dígitos centrales y sigue este proceso:</p>\n<p>Se recorre cada dígito teniendo en cuenta su posición. Los que ocupan posiciones impares (primera, tercera, quinta y séptima) se multiplican por dos; si el resultado supera nueve, se suman sus dos cifras —igual que en otros algoritmos de Luhn. Los que ocupan posiciones pares se usan directamente sin transformación. Se suman todos los valores resultantes.</p>\n<p>Con esa suma, el índice de control es <code>(10 − (suma % 10)) % 10</code>. Ese índice sirve para dos cosas: como dígito de control directamente, y como posición en la cadena <code>JABCDEFGHI</code> para obtener la letra de control equivalente.</p>\n<pre><code class=\"language-javascript\">const LETRAS_CIF = &quot;JABCDEFGHI&quot;;\n\nfunction calcularControlCIF(tipo, digitos) {\n\tlet suma = 0;\n\tfor (let i = 0; i &lt; 7; i++) {\n\t\tlet d = parseInt(digitos[i], 10);\n\t\tif ((i + 1) % 2 !== 0) {\n\t\t\t// posición impar (base 1)\n\t\t\td *= 2;\n\t\t\tif (d &gt;= 10) d = Math.floor(d / 10) + (d % 10);\n\t\t}\n\t\tsuma += d;\n\t}\n\tconst idx = (10 - (suma % 10)) % 10;\n\treturn { digit: String(idx), letter: LETRAS_CIF[idx] };\n}\n</code></pre>\n<p>Con el índice en mano, la validación es directa: el carácter final del CIF debe coincidir con el dígito calculado o con la letra calculada. Si el tipo de entidad pertenece al grupo <code>PQSW</code> y el carácter es un dígito, el CIF no es válido aunque el valor sea matemáticamente correcto.</p>\n<pre><code class=\"language-javascript\">function validateCIF(value) {\n\tconst cif = normalizar(value);\n\tconst match = cif.match(/^([ABCDEFGHJNPQRSTUVW])(\\d{7})([0-9A-J])$/);\n\tif (!match) return { valid: false, type: &quot;CIF&quot;, error: &quot;formato_incorrecto&quot; };\n\n\tconst [, tipo, digitos, control] = match;\n\tconst { digit, letter } = calcularControlCIF(tipo, digitos);\n\n\tif (&quot;PQSW&quot;.includes(tipo) &amp;&amp; /\\d/.test(control)) {\n\t\treturn { valid: false, type: &quot;CIF&quot;, error: &quot;control_debe_ser_letra&quot; };\n\t}\n\n\tconst ok = control === digit || control === letter;\n\treturn {\n\t\tvalid: ok,\n\t\ttype: &quot;CIF&quot;,\n\t\tvalue: cif,\n\t\tentityType: tipo,\n\t\tentityName: TIPOS_CIF[tipo] || null,\n\t\terror: ok ? undefined : &quot;caracter_control_incorrecto&quot;,\n\t};\n}\n</code></pre>\n<h2>Detectar automáticamente qué tipo de NIF es</h2>\n<p>Con los tres validadores listos, la función principal se limita a mirar el formato y derivar al validador correcto. El orden importa: hay que comprobar primero DNI y NIE —cuya primera posición puede solaparse con letras válidas de CIF— antes de intentar interpretar la cadena como un CIF.</p>\n<pre><code class=\"language-javascript\">function validate(raw) {\n\tconst doc = normalizar(raw);\n\tif (!doc) return { valid: false, type: null, error: &quot;vacio&quot; };\n\n\tif (/^\\d{8}[A-Z]$/.test(doc)) return validateDNI(doc);\n\tif (/^[XYZ]\\d{7}[A-Z]$/.test(doc)) return validateNIE(doc);\n\tif (/^[ABCDEFGHJNPQRSTUVW]\\d{7}[0-9A-J]$/.test(doc)) return validateCIF(doc);\n\n\treturn { valid: false, type: null, value: doc, error: &quot;tipo_no_reconocido&quot; };\n}\n</code></pre>\n<p>Vale la pena detenerse en el regex del CIF. La clase de caracteres <code>[ABCDEFGHJNPQRSTUVW]</code> para la letra inicial excluye explícitamente <code>I</code> y <code>O</code> por riesgo de confusión visual, y también <code>X</code>, <code>Y</code> y <code>Z</code>, que ya están capturadas por el patrón NIE. Para el carácter final, <code>[0-9A-J]</code> cubre tanto los diez posibles dígitos como las diez letras posibles de la tabla <code>JABCDEFGHI</code>.</p>\n<h2>Un resultado rico: más que un booleano</h2>\n<p>La ventaja de devolver un objeto estructurado en lugar de un simple <code>true</code>/<code>false</code> es que el formulario puede reaccionar de manera diferente según el tipo de NIF validado. Si es un CIF, puedes mostrar el tipo de entidad; si es un DNI o NIE inválido, puedes ofrecer un mensaje específico sobre la letra de control; si el formato no es reconocido, puedes sugerir un ejemplo.</p>\n<pre><code class=\"language-javascript\">const resultado = IdeFormsNIF.validate(campo.value);\n\nif (resultado.valid &amp;&amp; resultado.type === &quot;CIF&quot;) {\n\tmostrarInfo(`Empresa registrada como: ${resultado.entityName}`);\n}\n\nif (!resultado.valid &amp;&amp; resultado.error === &quot;caracter_control_incorrecto&quot;) {\n\tmostrarError(&quot;La letra o dígito de control no es correcto para este NIF&quot;);\n}\n</code></pre>\n<p>Ese nivel de detalle es lo que marca la diferencia entre un formulario que rechaza sin explicar y uno que ayuda al usuario a corregir el problema.</p>\n<h2>Qué cubre esta validación y qué no</h2>\n<p>El validador confirma que el NIF tiene un formato correcto y que el carácter de control es matemáticamente válido. No verifica que el NIF esté dado de alta en la Agencia Tributaria, que la empresa exista o que el DNI pertenezca a quien lo introduce. Para eso habría que consumir servicios externos —y entrar en territorios de privacidad y normativa que están bastante más allá de un campo de formulario.</p>\n<p>Con este validador y los dos anteriores de la serie —<a href=\"https://paigar.eu/validar-dni-nie-pasaporte/\">documentos de identidad</a> e <a href=\"https://paigar.eu/validar-iban-formulario/\">IBAN</a>—, tienes cubiertos los tres identificadores que aparecen con más frecuencia en formularios de facturación y contratación en España.</p>\n<p>A continuación puedes encontrar un validador funcional que cubre los tres casos: detecta automáticamente si el documento es un DNI, un NIE o un CIF, valida el carácter de control y, en el caso del CIF, identifica el tipo de entidad.</p>\n","date_published":"2026-05-02T00:00:00.000Z","image":"https://paigar.eu/validar-nif-cif-formulario.png"},{"id":"https://paigar.eu/lqip-en-lume/","url":"https://paigar.eu/lqip-en-lume/","title":"LQIP en Lume: placeholders inline generados en build","language":"es","content_html":"<p>Cuando una imagen tarda en descargarse del CDN, el navegador deja un hueco. La página da un saltito cuando la imagen finalmente entra. Si he reservado el espacio con <code>width</code> y <code>height</code> no hay layout shift, pero el hueco vacío sigue ahí. Y si la conexión es lenta, el hueco dura más de lo razonable.</p>\n<p>LQIP — <em>Low Quality Image Placeholder</em> — es la técnica que llena ese hueco: durante la espera muestro una versión diminuta y borrosa de la imagen, y cuando la real termina de descargar, sustituyo una por la otra con un cross-fade. Es lo que hace Medium desde hace años, y antes lo hizo Pinterest.</p>\n<p>La técnica en sí está documentada en mil sitios. Lo que cuento aquí es cómo la implementé en <a href=\"https://idenautas.com/\">Idenautas</a>, que corre sobre Lume: el script Deno que genera los placeholders en build, cómo los incrusto en el HTML, y cómo encajo todo en <code>_config.ts</code> sin meter JavaScript de cliente más allá del <code>onload</code> del propio <code>&lt;img&gt;</code>.</p>\n<h2>La idea</h2>\n<p>Tres decisiones que conviene fijar antes de escribir nada:</p>\n<ol>\n<li><strong>El placeholder se genera en build, no en runtime.</strong> El servidor (o el CDN, en mi caso Bunny) no tiene que hacer nada en cada visita. La consecuencia es que el placeholder viaja inline en el HTML como <code>data:image/jpeg;base64,...</code> y aparece sin una sola petición HTTP adicional.</li>\n<li><strong>El placeholder es una versión de 16 píxeles de ancho de la propia imagen, en JPG.</strong> A esa resolución el peso ronda los 300-500 bytes. Codificado en base64 son unos ~600 bytes por imagen — irrelevante en el HTML.</li>\n<li><strong>El cross-fade lo hace el navegador.</strong> El <code>&lt;img&gt;</code> lleva un <code>onload</code> que añade la clase <code>.loaded</code> a su contenedor, y el CSS hace el resto con <code>opacity</code> y <code>transition</code>. Cero JavaScript propio, cero IntersectionObserver, cero librerías.</li>\n</ol>\n<p>El truco está en que las tres decisiones son interdependientes. Si genero el placeholder en runtime, no puedo incrustarlo. Si no es minúsculo, no puedo permitirme incrustarlo en cada <code>&lt;img&gt;</code>. Si no lo incrusto, necesito una segunda petición HTTP solo para el placeholder, y eso elimina la mitad de la ventaja.</p>\n<h2>El script: <code>scripts/lqip.ts</code></h2>\n<p>El script tiene tres responsabilidades: encontrar las imágenes que se usan en el sitio, descargar su versión de 16 píxeles del CDN, y guardar el resultado en un JSON cacheable.</p>\n<h3>Descubrir las imágenes referenciadas</h3>\n<p>No quiero mantener una lista de imágenes a mano. El script camina <code>src/</code> con <code>@std/fs/walk</code> y busca dos patrones en los archivos <code>.md</code>, <code>.vto</code>, <code>.njk</code> y <code>.ts</code>:</p>\n<ul>\n<li>Llamadas a los shortcodes <code>{{ img(&quot;ruta&quot;) }}</code> y <code>{{ cardPicture(&quot;ruta&quot;) }}</code>.</li>\n<li>La clave <code>heroImage:</code> en el frontmatter.</li>\n</ul>\n<pre><code class=\"language-typescript\">const SHORTCODE_RE =\n\t/(?:\\{%[-\\s]*|\\{\\{[-\\s]*(?:await\\s+)?)(?:img|cardPicture)\\s*\\(?\\s*[&quot;']([^&quot;']+)[&quot;']/g;\nconst HERO_RE = /^heroImage:\\s*(.+)$/m;\n\nasync function findImagePaths(): Promise&lt;string[]&gt; {\n\tconst paths = new Set&lt;string&gt;();\n\tfor await (const entry of walk(SRC_DIR, {\n\t\texts: [&quot;.md&quot;, &quot;.vto&quot;, &quot;.njk&quot;, &quot;.ts&quot;],\n\t\tskip: [/node_modules/, /_data\\/lqip\\.json$/],\n\t})) {\n\t\tif (!entry.isFile) continue;\n\t\tconst content = await Deno.readTextFile(entry.path);\n\t\tlet m: RegExpExecArray | null;\n\t\tSHORTCODE_RE.lastIndex = 0;\n\t\twhile ((m = SHORTCODE_RE.exec(content)) !== null) paths.add(m[1]);\n\t\tconst hero = content.match(HERO_RE);\n\t\tif (hero) paths.add(hero[1].trim().replace(/^[&quot;']|[&quot;']$/g, &quot;&quot;));\n\t}\n\treturn [...paths];\n}\n</code></pre>\n<p>El regex de los shortcodes acepta tanto la sintaxis Nunjucks heredada (<code>{% img &quot;...&quot; %}</code>) como la nueva de Vento (<code>{{ img(&quot;...&quot;) }}</code>). Migrar de una a otra es trabajo que hago a fuego lento, así que el script tiene que entender ambas durante el periodo de transición.</p>\n<p>Es una solución imperfecta —un parser real entendería el código sin riesgo de falsos positivos—, pero a la práctica el regex acierta en el 100% de los casos del sitio. Si una imagen se referencia desde un layout o un sitio menos estándar, basta con añadir su patrón al regex.</p>\n<h3>Descargar los placeholders del CDN</h3>\n<p>Las imágenes de Idenautas viven en Bunny Storage y el build no las regenera: las versiones a 480 px, 800 px, 1200 px, 1920 px y 16 px (esta última, mi placeholder) ya están subidas con sufijo en el nombre. La ruta de cada placeholder es predecible:</p>\n<pre><code class=\"language-typescript\">function imgBase(imgPath: string): string {\n\tconst i = imgPath.lastIndexOf(&quot;.&quot;);\n\treturn i &gt;= 0 ? imgPath.slice(0, i) : imgPath;\n}\n\n// imgBase(&quot;portada.jpg&quot;) + &quot;-16.jpg&quot; → &quot;portada-16.jpg&quot;\n</code></pre>\n<p>Para cada imagen, descargo <code>${CDN}${base}-16.jpg</code> y la convierto a data URI:</p>\n<pre><code class=\"language-typescript\">async function fetchBase64(url: string): Promise&lt;string&gt; {\n\tconst res = await fetch(url);\n\tif (!res.ok) throw new Error(`HTTP ${res.status}`);\n\tconst type = res.headers.get(&quot;content-type&quot;) ?? &quot;image/jpeg&quot;;\n\tconst bytes = new Uint8Array(await res.arrayBuffer());\n\treturn `data:${type};base64,${encodeBase64(bytes)}`;\n}\n</code></pre>\n<p><code>encodeBase64</code> viene de <code>jsr:@std/encoding/base64</code>. Es una primitiva de la librería estándar de Deno; no añado dependencias.</p>\n<h3>El cache: <code>src/_data/lqip.json</code></h3>\n<p>El detalle que marca la diferencia entre un script aceptable y uno usable a diario es el cache. Sin cache, cada <code>npm run publicar</code> haría tantas peticiones HTTP como imágenes hay en el sitio. Con cache, solo se descargan las nuevas:</p>\n<pre><code class=\"language-typescript\">let existing: Record&lt;string, string&gt; = {};\ntry {\n\texisting = JSON.parse(await Deno.readTextFile(OUTPUT));\n} catch {\n\t// primera ejecución, no hay cache\n}\n\nconst lqip: Record&lt;string, string&gt; = {};\nlet downloaded = 0;\nfor (const img of images) {\n\tif (existing[img]) {\n\t\tlqip[img] = existing[img];\n\t\tcontinue;\n\t}\n\tconst url = `${CDN}${imgBase(img)}-16.jpg`;\n\ttry {\n\t\tlqip[img] = await fetchBase64(url);\n\t\tdownloaded++;\n\t} catch (err) {\n\t\tconsole.error(`  [lqip] ✗ ${img}: ${(err as Error).message}`);\n\t}\n}\n</code></pre>\n<p>El JSON resultante es un mapa <code>ruta-original → data URI</code>. El script lo guarda en <code>src/_data/lqip.json</code> solo si el contenido ha cambiado — escribir el archivo en cada build invalidaría el watcher de Lume sin necesidad y dispararía recargas en desarrollo:</p>\n<pre><code class=\"language-typescript\">const prevJson = JSON.stringify(existing, null, 2);\nconst nextJson = JSON.stringify(lqip, null, 2);\nif (prevJson !== nextJson) {\n\tawait Deno.writeTextFile(OUTPUT, nextJson);\n}\n</code></pre>\n<p>Otra ventaja del JSON cacheado: las imágenes que ya no se referencian desde ningún lado se eliminan del mapa automáticamente, porque el script reconstruye el objeto desde cero a partir del escaneo. No necesita una lógica de garbage collection aparte.</p>\n<h2>Integración con Lume</h2>\n<p>El script expone una función <code>generateLQIP()</code> para poder llamarse desde <code>_config.ts</code>. La conexión es mínima:</p>\n<pre><code class=\"language-typescript\">import { generateLQIP } from &quot;./scripts/lqip.ts&quot;;\n\nlet lqipData: Record&lt;string, string&gt; = {};\ntry {\n\tlqipData = JSON.parse(await Deno.readTextFile(&quot;./src/_data/lqip.json&quot;));\n} catch {\n\t// primer build, todavía no hay cache\n}\n\nsite.addEventListener(&quot;beforeBuild&quot;, async () =&gt; {\n\tlqipData = await generateLQIP({ quiet: false });\n});\n</code></pre>\n<p>Dos detalles aquí:</p>\n<ul>\n<li><strong>Carga del cache al arrancar.</strong> El <code>JSON.parse</code> inicial existe para que los servidores de desarrollo en frío arranquen con el mapa ya rellenado, sin esperar a la primera regeneración.</li>\n<li><strong><code>beforeBuild</code> y no <code>beforeUpdate</code>.</strong> En desarrollo, mientras edito un post, no quiero que cada cambio dispare una conexión al CDN. La regeneración solo ocurre en builds completos.</li>\n</ul>\n<p>Con <code>lqipData</code> en memoria, los shortcodes que generan el HTML pueden consultarlo:</p>\n<pre><code class=\"language-typescript\">site.data(&quot;img&quot;, function (imgPath: string, alt: string, ...) {\n  const lqip = lqipData[imgPath] || imgUrl(imgPath, 16, &quot;jpg&quot;);\n  return `&lt;div class=&quot;lqip-wrap&quot; style=&quot;background-image:url('${lqip}')&quot;&gt;\n    &lt;picture&gt;...&lt;/picture&gt;\n  &lt;/div&gt;`;\n});\n</code></pre>\n<p>El fallback <code>|| imgUrl(imgPath, 16, &quot;jpg&quot;)</code> cubre el caso en el que añado una imagen al post pero todavía no he regenerado el cache. En vez de quedarme sin placeholder, sirvo la URL del placeholder directamente desde el CDN — funciona, solo es marginalmente menos eficiente porque el navegador hace una petición HTTP extra mientras llega la imagen real.</p>\n<h2>El HTML resultante</h2>\n<p>Para cada imagen, el shortcode produce este HTML:</p>\n<pre><code class=\"language-html\">&lt;div\n\tclass=&quot;lqip-wrap&quot;\n\tstyle=&quot;background-image:url('data:image/jpeg;base64,/9j/4AAQ...')&quot;&gt;\n\t&lt;picture&gt;\n\t\t&lt;source type=&quot;image/avif&quot; srcset=&quot;foto-480.avif 480w, ...&quot; sizes=&quot;...&quot; /&gt;\n\t\t&lt;source type=&quot;image/webp&quot; srcset=&quot;foto-480.webp 480w, ...&quot; sizes=&quot;...&quot; /&gt;\n\t\t&lt;img\n\t\t\tsrc=&quot;foto-1200.jpg&quot;\n\t\t\tsrcset=&quot;foto-480.jpg 480w, ...&quot;\n\t\t\tsizes=&quot;...&quot;\n\t\t\talt=&quot;...&quot;\n\t\t\tloading=&quot;lazy&quot;\n\t\t\twidth=&quot;1200&quot;\n\t\t\theight=&quot;800&quot;\n\t\t\tonload=&quot;this.parentNode.classList.add('loaded')&quot; /&gt;\n\t&lt;/picture&gt;\n&lt;/div&gt;\n</code></pre>\n<p>Tres piezas:</p>\n<ul>\n<li><strong>El wrapper lleva el placeholder como <code>background-image</code>.</strong> Aparece instantáneo: ya viaja en el HTML.</li>\n<li><strong>El <code>&lt;picture&gt;</code> sirve la imagen definitiva</strong> con <code>srcset</code> para densidades y formatos modernos. Eso es ortogonal al LQIP — es la técnica de imágenes responsive, aplicada a la imagen real.</li>\n<li><strong>El <code>onload</code> añade <code>.loaded</code></strong> al wrapper cuando el <code>&lt;img&gt;</code> termina de descargar, lo que dispara el cross-fade.</li>\n</ul>\n<h2>El CSS: cross-fade sin JavaScript</h2>\n<pre><code class=\"language-css\">.lqip-wrap {\n\tposition: relative;\n\tbackground-size: cover;\n\tbackground-position: center;\n\tbackground-repeat: no-repeat;\n\toverflow: hidden;\n\twidth: 100%;\n\theight: 100%;\n}\n\n.lqip-wrap &gt; img {\n\tdisplay: block;\n\twidth: 100%;\n\theight: 100%;\n\tobject-fit: cover;\n\topacity: 0;\n\ttransition: opacity 0.4s ease;\n}\n\n.lqip-wrap.loaded &gt; img {\n\topacity: 1;\n}\n</code></pre>\n<p>El placeholder es el fondo del wrapper. El <code>&lt;img&gt;</code> empieza con <code>opacity: 0</code>, ocupando el mismo espacio. Cuando dispara su <code>onload</code>, el wrapper recibe <code>.loaded</code>, el <code>&lt;img&gt;</code> pasa a <code>opacity: 1</code>, y la transición de 0,4 s hace el cross-fade.</p>\n<p><code>object-fit: cover</code> se asegura de que la imagen real cubra el wrapper sin deformarse, lo que importa porque el <code>width</code> y <code>height</code> del <code>&lt;img&gt;</code> definen la proporción pero el contenedor real lo controla CSS.</p>\n<h2>Por qué 16 píxeles y por qué JPG</h2>\n<p>Probé valores entre 8 y 32 píxeles. Por debajo de 16 el placeholder se nota pixelado en la transición; por encima, el peso crece más rápido que la mejora visual. El JPG a 16 px y calidad por defecto pesa unos 350 bytes — aceptable.</p>\n<p>Sobre el formato: aquí JPG gana a WebP y AVIF. A 16 píxeles las cabeceras de WebP/AVIF representan un porcentaje ridículamente alto del archivo, y la ganancia de compresión sobre JPG es marginal. Además, los placeholders viajan en el HTML, donde el ahorro de bytes brutos sí cuenta — y JPG genera buffers pequeños y predecibles. He medido los tres formatos: a 16 px, JPG es el más ligero en mi caso.</p>\n<h2>Por qué no BlurHash, Plaiceholder o transform_images</h2>\n<p>Existen alternativas conocidas:</p>\n<ul>\n<li><strong>BlurHash</strong> codifica el placeholder como un string ASCII de unos 30 caracteres y lo reconstruye con JavaScript en el cliente. El string es más compacto que una data URI base64, sí — pero requiere ~3 KB de JavaScript en cada página y un canvas para reconstruir el placeholder. Para una web sin frameworks no compensa.</li>\n<li><strong>Plaiceholder</strong> es una librería de Node.js que genera LQIPs (entre otros formatos: blurhash, color dominante, SVG). En Idenautas no me hace falta el paso de generar la imagen pequeña — Bunny ya tiene la versión de 16 px subida —, y prefiero un script de cien líneas que entiendo entero a una dependencia más.</li>\n<li><strong>El plugin oficial <code>transform_images</code></strong> procesa imágenes con Sharp dentro del propio build de Lume. No genera placeholders como tales, pero sí puede producir la variante de 16 px y leerla luego para incrustarla en base64 — todo en una sola pasada de Lume, sin un script aparte como el mío. Si dejas que Lume gestione también tus variantes responsive con <code>transform_images</code> o el plugin <code>picture</code>, esa ruta es más coherente. En Idenautas las variantes están subidas a Bunny Storage por un pipeline anterior a Lume, así que el script de LQIP solo se ocupa del placeholder; si arrancase el proyecto desde cero hoy, probablemente movería todo el flujo de imágenes a <code>transform_images</code> y haría el LQIP ahí mismo.</li>\n</ul>\n<p>Si fuera un proyecto donde las imágenes solo viven a tamaño completo y necesito regenerarlas, <code>transform_images</code> (o <code>sharp</code> directamente) sería la opción razonable. Mi pipeline ya produce las variantes responsive con un script aparte, así que añadir <code>-16.jpg</code> a esa lista era trivial.</p>\n<h2>Lo que cuesta y lo que aporta</h2>\n<p>El coste, en bytes incrustados, es de unos 600 bytes por imagen en el HTML. En una página con cinco imágenes son 3 KB extra antes de cualquier compresión gzip — y gzip los reduce todavía más, porque las cabeceras JPG son repetitivas entre placeholders. Es un coste muy bajo para evitar huecos vacíos en la primera pintada.</p>\n<p>Lo que aporta es perceptual: la página se siente más rápida sin serlo necesariamente. Las imágenes ya están en su sitio cuando entras, solo enfocan. El usuario rara vez identifica conscientemente la técnica, pero nota la diferencia cuando la quitas.</p>\n<p>Es una de esas inversiones de unas pocas horas que se quedan trabajando en silencio durante años. Y en Lume, con un script Deno y un evento <code>beforeBuild</code>, encaja sin necesidad de plugins ni configuración adicional.</p>\n","date_published":"2026-04-28T00:00:00.000Z","image":"https://paigar.eu/lqip-en-lume.png"},{"id":"https://paigar.eu/juego-ladrillos-tutorial/","url":"https://paigar.eu/juego-ladrillos-tutorial/","title":"Cómo programar el juego de ladrillos desde cero","language":"es","content_html":"<p>El juego de ladrillos es uno de los géneros arcade más influyentes de la historia. La versión original, llamada Breakout, salió de Atari en 1976 y la diseñaron, entre otros, Steve Jobs y Steve Wozniak antes de fundar Apple. Once años después, Taito lanzó Arkanoid, que era básicamente el mismo juego con mejor presentación, niveles diseñados, ladrillos especiales y power-ups. Las dos versiones comparten el mismo núcleo: una paleta abajo, una pelota que rebota, una pared de ladrillos arriba, y el objetivo de romper todos los ladrillos sin que la pelota se te escape por debajo.</p>\n<p>Lo bonito de este juego como proyecto educativo es que junta en una sola pieza los conceptos de varios tutoriales anteriores. La física de la pelota es prácticamente la misma que en Pong. La detección de colisiones es la misma que con la paleta, pero ahora aplicada a docenas de objetos a la vez. Y aparece por primera vez en la serie un concepto nuevo: la gestión de muchos objetos del mismo tipo (los ladrillos) y la lógica de eliminarlos cuando son golpeados. Si has seguido los tutoriales anteriores, este se va a sentir como una continuación natural y bastante satisfactoria.</p>\n<h2>La idea general antes de tocar código</h2>\n<p>El juego funciona así. La pantalla está dividida en tres zonas: una paleta horizontal en la parte inferior que el jugador mueve a izquierda y derecha, una pelota que se mueve con velocidad continua y rebota contra los bordes, la paleta y los ladrillos, y una rejilla de ladrillos en la parte superior que hay que destruir. La pelota empieza pegada a la paleta y se lanza con un click o una tecla. Cuando rebota contra un ladrillo, el ladrillo se rompe y desaparece, y la pelota cambia de dirección. Si la pelota se sale por debajo de la paleta, el jugador pierde una vida. Si se rompen todos los ladrillos, ha terminado el nivel.</p>\n<p>A diferencia de Pong, que es un duelo entre dos jugadores, este es un juego solitario contra el escenario. La estrategia no consiste en ganarle a nadie sino en controlar la trayectoria de la pelota para llegar a los ladrillos más difíciles antes de que la pelota se descontrole y termine cayéndose. Por eso, igual que en Pong, conviene que el ángulo de rebote dependa del punto donde la pelota toca la paleta. Eso es lo que separa una versión sin gracia de una realmente jugable.</p>\n<h2>El esqueleto HTML y CSS</h2>\n<p>Vamos con canvas, igual que con Snake o Pong, porque tenemos movimiento continuo y muchos objetos que pintar.</p>\n<pre><code class=\"language-html\">&lt;!DOCTYPE html&gt;\n&lt;html lang=&quot;es&quot;&gt;\n\t&lt;head&gt;\n\t\t&lt;meta charset=&quot;UTF-8&quot; /&gt;\n\t\t&lt;title&gt;Ladrillos&lt;/title&gt;\n\t\t&lt;style&gt;\n\t\t\tbody {\n\t\t\t\tdisplay: flex;\n\t\t\t\tflex-direction: column;\n\t\t\t\talign-items: center;\n\t\t\t\tbackground: #1a1d24;\n\t\t\t\tcolor: #e8e8e8;\n\t\t\t\tfont-family: system-ui, sans-serif;\n\t\t\t}\n\t\t\tcanvas {\n\t\t\t\tbackground: #0f1115;\n\t\t\t\tborder: 1px solid #2a2e36;\n\t\t\t}\n\t\t&lt;/style&gt;\n\t&lt;/head&gt;\n\t&lt;body&gt;\n\t\t&lt;h1&gt;Ladrillos&lt;/h1&gt;\n\t\t&lt;div&gt;\n\t\t\tPuntos: &lt;span id=&quot;puntos&quot;&gt;0&lt;/span&gt; · Vidas: &lt;span id=&quot;vidas&quot;&gt;3&lt;/span&gt;\n\t\t&lt;/div&gt;\n\t\t&lt;canvas id=&quot;lienzo&quot; width=&quot;480&quot; height=&quot;600&quot;&gt;&lt;/canvas&gt;\n\t\t&lt;script&gt;\n\t\t\t// aquí irá todo el código del juego\n\t\t&lt;/script&gt;\n\t&lt;/body&gt;\n&lt;/html&gt;\n</code></pre>\n<p>He elegido un canvas vertical de 480 por 600 píxeles porque para este tipo de juego es la proporción más natural. Necesitas espacio vertical para que la pelota tenga recorrido entre la pared de ladrillos y la paleta. Las proporciones horizontales típicas de un Snake o un Pong harían que la pelota cruce la pantalla demasiado deprisa.</p>\n<h2>El estado del juego</h2>\n<p>El estado tiene cuatro piezas: la pelota con su posición y velocidad, la paleta con su posición horizontal, un array de ladrillos con su posición y un flag de si están vivos, y los marcadores de puntos y vidas.</p>\n<pre><code class=\"language-javascript\">const ANCHO = 480;\nconst ALTO = 600;\nconst ANCHO_PALETA = 80;\nconst ALTO_PALETA = 12;\nconst TAMANO_PELOTA = 10;\nconst FILAS_LADRILLOS = 5;\nconst COLUMNAS_LADRILLOS = 8;\nconst ANCHO_LADRILLO = 54;\nconst ALTO_LADRILLO = 18;\nconst MARGEN_LADRILLOS = 6;\n\nlet pelota = { x: 0, y: 0, vx: 0, vy: 0 };\nlet paleta = { x: ANCHO / 2 - ANCHO_PALETA / 2 };\nlet ladrillos = [];\nlet puntos = 0;\nlet vidas = 3;\nlet pegada = true;\n</code></pre>\n<p>Aquí hay una variable que conviene explicar: <code>pegada</code>. Es un booleano que indica si la pelota está pegada a la paleta esperando a ser lanzada, o si ya está en movimiento. Al inicio de cada vida la pelota empieza pegada a la paleta y se mueve con ella. Cuando el jugador pulsa espacio o hace click, se despega y empieza el juego. Es una mecánica clásica del Breakout y Arkanoid originales que conviene respetar.</p>\n<h2>Generar los ladrillos</h2>\n<p>Los ladrillos son una rejilla regular en la parte superior de la pantalla. Para crearlos basta con dos bucles anidados que generen una entrada en el array por cada combinación de fila y columna.</p>\n<pre><code class=\"language-javascript\">function generarLadrillos() {\n\tladrillos = [];\n\tconst colores = [&quot;#e74c3c&quot;, &quot;#e67e22&quot;, &quot;#f1c40f&quot;, &quot;#27ae60&quot;, &quot;#3498db&quot;];\n\tconst offsetX =\n\t\t(ANCHO -\n\t\t\t(COLUMNAS_LADRILLOS * (ANCHO_LADRILLO + MARGEN_LADRILLOS) -\n\t\t\t\tMARGEN_LADRILLOS)) /\n\t\t2;\n\tconst offsetY = 60;\n\n\tfor (let f = 0; f &lt; FILAS_LADRILLOS; f++) {\n\t\tfor (let c = 0; c &lt; COLUMNAS_LADRILLOS; c++) {\n\t\t\tladrillos.push({\n\t\t\t\tx: offsetX + c * (ANCHO_LADRILLO + MARGEN_LADRILLOS),\n\t\t\t\ty: offsetY + f * (ALTO_LADRILLO + MARGEN_LADRILLOS),\n\t\t\t\tcolor: colores[f],\n\t\t\t\tpuntos: (FILAS_LADRILLOS - f) * 10,\n\t\t\t\tvivo: true,\n\t\t\t});\n\t\t}\n\t}\n}\n</code></pre>\n<p>Cada ladrillo tiene su posición, su color, una puntuación que da al ser destruido y un flag <code>vivo</code> que dice si todavía está en pantalla. He hecho que las filas superiores valgan más puntos que las inferiores, que es la convención clásica del juego: los ladrillos más difíciles de alcanzar (los que están arriba, escondidos detrás de los demás) recompensan más al jugador. El cálculo de <code>offsetX</code> puede parecer farragoso pero solo está centrando la rejilla horizontalmente: calcula el ancho total que ocuparán todas las columnas con sus márgenes y reparte el espacio sobrante a izquierda y derecha por igual.</p>\n<h2>Mover la pelota y rebotes en bordes</h2>\n<p>El movimiento de la pelota es esencialmente igual que en Pong. La diferencia es que ahora rebota contra tres lados (izquierdo, derecho y superior) y por el cuarto (inferior) la pelota se sale y el jugador pierde una vida.</p>\n<pre><code class=\"language-javascript\">function actualizarPelota() {\n\tif (pegada) {\n\t\tpelota.x = paleta.x + ANCHO_PALETA / 2 - TAMANO_PELOTA / 2;\n\t\tpelota.y = ALTO - ALTO_PALETA - TAMANO_PELOTA - 5;\n\t\treturn;\n\t}\n\n\tpelota.x += pelota.vx;\n\tpelota.y += pelota.vy;\n\n\tif (pelota.x &lt; 0) {\n\t\tpelota.x = 0;\n\t\tpelota.vx = -pelota.vx;\n\t}\n\tif (pelota.x + TAMANO_PELOTA &gt; ANCHO) {\n\t\tpelota.x = ANCHO - TAMANO_PELOTA;\n\t\tpelota.vx = -pelota.vx;\n\t}\n\tif (pelota.y &lt; 0) {\n\t\tpelota.y = 0;\n\t\tpelota.vy = -pelota.vy;\n\t}\n\tif (pelota.y &gt; ALTO) {\n\t\tperderVida();\n\t}\n}\n</code></pre>\n<p>Si la pelota está pegada, no se mueve por su cuenta sino que se ancla a la posición de la paleta. Esto es lo que hace que la pelota acompañe a la paleta cuando el jugador la mueve antes de lanzar. En el momento en que el jugador lanza, <code>pegada</code> se vuelve <code>false</code> y la pelota empieza a moverse con su propia velocidad.</p>\n<h2>Colisión con la paleta</h2>\n<p>La colisión con la paleta funciona exactamente igual que en Pong. La única diferencia es que la paleta está abajo y solo nos importa el rebote desde arriba, no desde los lados.</p>\n<pre><code class=\"language-javascript\">function comprobarColisionPaleta() {\n\tconst paletaY = ALTO - ALTO_PALETA - 10;\n\tif (\n\t\tpelota.x &lt; paleta.x + ANCHO_PALETA &amp;&amp;\n\t\tpelota.x + TAMANO_PELOTA &gt; paleta.x &amp;&amp;\n\t\tpelota.y + TAMANO_PELOTA &gt; paletaY &amp;&amp;\n\t\tpelota.y &lt; paletaY + ALTO_PALETA &amp;&amp;\n\t\tpelota.vy &gt; 0\n\t) {\n\t\tpelota.y = paletaY - TAMANO_PELOTA;\n\t\tconst centroPaleta = paleta.x + ANCHO_PALETA / 2;\n\t\tconst centroPelota = pelota.x + TAMANO_PELOTA / 2;\n\t\tconst offset = (centroPelota - centroPaleta) / (ANCHO_PALETA / 2);\n\t\tconst velocidad = Math.sqrt(pelota.vx ** 2 + pelota.vy ** 2);\n\t\tconst angulo = offset * (Math.PI / 3);\n\t\tpelota.vx = velocidad * Math.sin(angulo);\n\t\tpelota.vy = -velocidad * Math.cos(angulo);\n\t}\n}\n</code></pre>\n<p>Aquí hago algo más sofisticado que en Pong. En lugar de cambiar <code>vy</code> simplemente, recalculo la dirección entera de la pelota usando trigonometría. La idea es que el ángulo de salida dependa de en qué punto de la paleta golpeó la pelota: si golpea en el centro, sale recta hacia arriba; si golpea en el extremo izquierdo, sale con un ángulo de hasta sesenta grados hacia la izquierda; si golpea en el extremo derecho, sale con sesenta grados hacia la derecha.</p>\n<p>La constante <code>Math.PI / 3</code> es sesenta grados expresados en radianes. Si quieres que el rango de ángulos sea más estrecho (rebotes más predecibles), reduce ese valor. Si lo quieres más amplio (rebotes más extremos), súbelo. He puesto sesenta grados porque es el clásico de los juegos arcade de la época y da control suficiente al jugador sin que la pelota pueda salir casi horizontal y volverse incontrolable.</p>\n<h2>Colisión con los ladrillos</h2>\n<p>Aquí está el corazón nuevo del juego respecto a Pong. La pelota tiene que colisionar contra todos los ladrillos vivos, y cuando golpea uno, hay que destruirlo y rebotar.</p>\n<pre><code class=\"language-javascript\">function comprobarColisionLadrillos() {\n\tfor (const l of ladrillos) {\n\t\tif (!l.vivo) continue;\n\t\tif (\n\t\t\tpelota.x &lt; l.x + ANCHO_LADRILLO &amp;&amp;\n\t\t\tpelota.x + TAMANO_PELOTA &gt; l.x &amp;&amp;\n\t\t\tpelota.y &lt; l.y + ALTO_LADRILLO &amp;&amp;\n\t\t\tpelota.y + TAMANO_PELOTA &gt; l.y\n\t\t) {\n\t\t\tl.vivo = false;\n\t\t\tpuntos += l.puntos;\n\n\t\t\tconst centroPelota = {\n\t\t\t\tx: pelota.x + TAMANO_PELOTA / 2,\n\t\t\t\ty: pelota.y + TAMANO_PELOTA / 2,\n\t\t\t};\n\t\t\tconst centroLadrillo = {\n\t\t\t\tx: l.x + ANCHO_LADRILLO / 2,\n\t\t\t\ty: l.y + ALTO_LADRILLO / 2,\n\t\t\t};\n\t\t\tconst dx =\n\t\t\t\tMath.abs(centroPelota.x - centroLadrillo.x) - ANCHO_LADRILLO / 2;\n\t\t\tconst dy =\n\t\t\t\tMath.abs(centroPelota.y - centroLadrillo.y) - ALTO_LADRILLO / 2;\n\n\t\t\tif (dx &gt; dy) {\n\t\t\t\tpelota.vx = -pelota.vx;\n\t\t\t} else {\n\t\t\t\tpelota.vy = -pelota.vy;\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t}\n}\n</code></pre>\n<p>Lo bonito aquí es decidir si la pelota rebotó en un lateral o en la parte superior/inferior del ladrillo. Sin esa distinción, una pelota que viene en diagonal podría hacer rebotes raros que no se corresponden con la geometría real. La técnica que uso es comparar las distancias del centro de la pelota a los bordes del ladrillo en cada eje. Si la diferencia horizontal es mayor que la vertical, la pelota golpeó por el lateral (rebote horizontal). Si es al revés, golpeó por arriba o por abajo (rebote vertical). Es una heurística sencilla que funciona muy bien en la práctica para rectángulos de proporción razonable.</p>\n<p>El <code>return</code> al final del bloque es importante: solo procesamos un ladrillo por fotograma. Sin él, una pelota muy rápida podría golpear dos ladrillos al mismo tiempo y rebotar dos veces, anulando el rebote y atravesando la pared. Con el <code>return</code>, garantizamos que el rebote es siempre limpio.</p>\n<h2>Mover la paleta</h2>\n<p>La paleta se controla con las flechas izquierda y derecha o con A y D. Misma lógica que en Pong, pero horizontal en lugar de vertical.</p>\n<pre><code class=\"language-javascript\">const teclas = { izquierda: false, derecha: false };\n\ndocument.addEventListener(&quot;keydown&quot;, (e) =&gt; {\n\tif (e.key === &quot;ArrowLeft&quot; || e.key === &quot;a&quot; || e.key === &quot;A&quot;)\n\t\tteclas.izquierda = true;\n\tif (e.key === &quot;ArrowRight&quot; || e.key === &quot;d&quot; || e.key === &quot;D&quot;)\n\t\tteclas.derecha = true;\n\tif (e.key === &quot; &quot; &amp;&amp; pegada) lanzarPelota();\n});\n\ndocument.addEventListener(&quot;keyup&quot;, (e) =&gt; {\n\tif (e.key === &quot;ArrowLeft&quot; || e.key === &quot;a&quot; || e.key === &quot;A&quot;)\n\t\tteclas.izquierda = false;\n\tif (e.key === &quot;ArrowRight&quot; || e.key === &quot;d&quot; || e.key === &quot;D&quot;)\n\t\tteclas.derecha = false;\n});\n\nfunction moverPaleta() {\n\tconst VELOCIDAD = 7;\n\tif (teclas.izquierda) paleta.x -= VELOCIDAD;\n\tif (teclas.derecha) paleta.x += VELOCIDAD;\n\tpaleta.x = Math.max(0, Math.min(ANCHO - ANCHO_PALETA, paleta.x));\n}\n\nfunction lanzarPelota() {\n\tpegada = false;\n\tpelota.vx = (Math.random() - 0.5) * 4;\n\tpelota.vy = -5;\n}\n</code></pre>\n<p>La función <code>lanzarPelota</code> despega la pelota de la paleta y le da una velocidad inicial. La velocidad horizontal es ligeramente aleatoria para que cada saque sea distinto, y la velocidad vertical es siempre hacia arriba.</p>\n<h2>Perder y reiniciar</h2>\n<p>Cuando la pelota se sale por debajo, el jugador pierde una vida. Si todavía le quedan vidas, la pelota vuelve a pegarse a la paleta para empezar de nuevo. Si no, fin del juego.</p>\n<pre><code class=\"language-javascript\">function perderVida() {\n\tvidas--;\n\tpegada = true;\n\tpelota.vx = 0;\n\tpelota.vy = 0;\n\tif (vidas &lt;= 0) {\n\t\tsetTimeout(() =&gt; alert(`Fin del juego. Puntos: ${puntos}`), 100);\n\t\treiniciarJuego();\n\t}\n}\n\nfunction reiniciarJuego() {\n\tpuntos = 0;\n\tvidas = 3;\n\tpegada = true;\n\tgenerarLadrillos();\n}\n</code></pre>\n<p>También hay que comprobar si el jugador ha ganado, lo que ocurre cuando todos los ladrillos están muertos. Esto se mira en cada fotograma después del bucle de colisiones.</p>\n<pre><code class=\"language-javascript\">function comprobarVictoria() {\n\tif (ladrillos.every((l) =&gt; !l.vivo)) {\n\t\tsetTimeout(() =&gt; alert(`¡Nivel completado! Puntos: ${puntos}`), 100);\n\t\treiniciarJuego();\n\t}\n}\n</code></pre>\n<h2>Dibujar todo</h2>\n<p>La función de dibujado limpia el canvas y pinta la paleta, la pelota y todos los ladrillos vivos.</p>\n<pre><code class=\"language-javascript\">const lienzo = document.getElementById(&quot;lienzo&quot;);\nconst ctx = lienzo.getContext(&quot;2d&quot;);\n\nfunction dibujar() {\n\tctx.fillStyle = &quot;#0f1115&quot;;\n\tctx.fillRect(0, 0, ANCHO, ALTO);\n\n\tfor (const l of ladrillos) {\n\t\tif (!l.vivo) continue;\n\t\tctx.fillStyle = l.color;\n\t\tctx.fillRect(l.x, l.y, ANCHO_LADRILLO, ALTO_LADRILLO);\n\t}\n\n\tctx.fillStyle = &quot;#e8e8e8&quot;;\n\tctx.fillRect(paleta.x, ALTO - ALTO_PALETA - 10, ANCHO_PALETA, ALTO_PALETA);\n\tctx.fillRect(pelota.x, pelota.y, TAMANO_PELOTA, TAMANO_PELOTA);\n}\n</code></pre>\n<h2>El bucle principal</h2>\n<p>Igual que en Pong, usamos <code>requestAnimationFrame</code> para sincronizar el bucle con la frecuencia de refresco de la pantalla.</p>\n<pre><code class=\"language-javascript\">function bucle() {\n\tmoverPaleta();\n\tactualizarPelota();\n\tcomprobarColisionPaleta();\n\tcomprobarColisionLadrillos();\n\tcomprobarVictoria();\n\tdibujar();\n\tdocument.getElementById(&quot;puntos&quot;).textContent = puntos;\n\tdocument.getElementById(&quot;vidas&quot;).textContent = vidas;\n\trequestAnimationFrame(bucle);\n}\n\ngenerarLadrillos();\nbucle();\n</code></pre>\n<p>Y con esto el juego ya funciona. Tienes una paleta que se mueve, una pelota que rebota contra los bordes y los ladrillos, una pared que se va destruyendo y un sistema de vidas. Todo en menos de doscientas líneas de código, que es bastante poco para un juego con esta complejidad aparente.</p>\n<h2>Cosas que se pueden añadir</h2>\n<p>Las posibilidades a partir de aquí son enormes. Power-ups que aparecen al destruir ciertos ladrillos: una paleta más grande, una segunda pelota, un láser que dispara desde la paleta. Ladrillos especiales que necesitan dos golpes para romperse. Niveles diseñados a mano con patrones de ladrillos en forma de letra, dibujo o paisaje. Aceleración progresiva de la pelota a medida que se rompen más ladrillos. Sonido al rebotar y al destruir. Una pantalla de inicio. Un sistema de vidas extra cada cierta puntuación. Y, por supuesto, cuando se completa un nivel pasar al siguiente con una pared distinta en lugar de reiniciar.</p>\n<p>La mayoría de estas mejoras son fáciles de añadir sobre el esqueleto que tenemos. Los power-ups, por ejemplo, son simplemente otro array de objetos que caen desde los ladrillos destruidos y modifican el estado de la paleta o la pelota cuando son recogidos. Los niveles son matrices que reemplazan la generación uniforme actual. La aceleración progresiva es un multiplicador que se aplica a la velocidad de la pelota cada cierto número de ladrillos destruidos.</p>\n<h2>El prototipo funcional</h2>\n<p>Aquí abajo dejo el juego completo y funcionando, con cinco filas de ladrillos de colores, control con teclado y soporte táctil para móvil (la paleta sigue al dedo deslizado en la mitad inferior de la pantalla), aceleración progresiva, niveles infinitos y una estética cuidada. Está todo aislado bajo un id propio para que no afecte al resto del blog.</p>\n<p><strong>Otros tutoriales de la serie</strong>: <a href=\"https://paigar.eu/juego-2048-tutorial/\">2048</a> · <a href=\"https://paigar.eu/juego-pong-tutorial/\">Pong</a> · <a href=\"https://paigar.eu/juego-serpiente-tutorial/\">Serpiente</a> · <a href=\"https://paigar.eu/juego-parejas-tutorial/\">Parejas</a>.</p>\n","date_published":"2026-04-21T00:00:00.000Z","image":"https://paigar.eu/juego-ladrillos.png"},{"id":"https://paigar.eu/validar-iban-formulario/","url":"https://paigar.eu/validar-iban-formulario/","title":"Cómo validar un código IBAN en un formulario web","language":"es","content_html":"<p>Pedir un número de cuenta bancaria en un formulario es uno de esos momentos en los que el margen de error duele especialmente. No como un campo de nombre, donde un typo es molesto pero recuperable. Aquí, un carácter de más, uno de menos, o una letra en el sitio equivocado puede significar una transferencia que no llega, un cobro que falla o un usuario que abandona el proceso pensando que el problema es suyo.</p>\n<p>El IBAN —International Bank Account Number— es el estándar europeo (y de bastantes países más) para identificar cuentas bancarias de forma inequívoca. Y lo interesante es que tiene un mecanismo de validación matemática incorporado, lo que significa que podemos saber, con certeza, si un IBAN dado es formalmente correcto antes de enviar nada a ningún servidor. No si la cuenta existe, ojo, pero sí si el número tiene sentido.</p>\n<h2>La estructura del IBAN</h2>\n<p>Un IBAN tiene siempre la misma anatomía. Empieza con dos letras que identifican el país según la norma ISO 3166-1, seguidas de dos dígitos de control —el corazón del mecanismo de validación— y a continuación el BBAN (Basic Bank Account Number), que es el número de cuenta propio del sistema bancario nacional y cuya estructura varía según el país.</p>\n<p>El IBAN español, por ejemplo, tiene 24 caracteres en total: <code>ES</code> + 2 dígitos de control + 4 dígitos de código de banco + 4 de código de sucursal + 2 dígitos de control interno + 10 dígitos de número de cuenta. Alemania usa 22 caracteres, Francia 27, Malta 31. No hay una longitud universal, y eso importa a la hora de validar.</p>\n<h2>El algoritmo: mover, convertir, dividir</h2>\n<p>El método de validación es elegante en su sencillez. Se llama MOD-97-10 y funciona en tres pasos.</p>\n<p>Primero, se mueven los cuatro primeros caracteres del IBAN al final. Si tenemos <code>ES9121000418450200051332</code>, lo transformamos en <code>21000418450200051332ES91</code>. Segundo, cada letra se sustituye por su valor numérico según el estándar: <code>A</code> = 10, <code>B</code> = 11, y así hasta <code>Z</code> = 35. Con esa sustitución, el IBAN completo se convierte en una cadena de dígitos, potencialmente muy larga. Tercero, se calcula el resto de dividir ese número entre 97. Si el resultado es 1, el IBAN es válido.</p>\n<pre><code class=\"language-javascript\">function mod97(numStr) {\n\tlet resto = 0;\n\tfor (const digito of numStr) {\n\t\tresto = (resto * 10 + parseInt(digito)) % 97;\n\t}\n\treturn resto;\n}\n\nfunction validarIBAN(raw) {\n\tconst iban = raw\n\t\t.trim()\n\t\t.toUpperCase()\n\t\t.replace(/[\\s\\-]/g, &quot;&quot;);\n\n\tif (!/^[A-Z]{2}\\d{2}[A-Z0-9]{4,}$/.test(iban)) {\n\t\treturn { valido: false, error: &quot;formato&quot; };\n\t}\n\n\tconst reordenado = iban.substring(4) + iban.substring(0, 4);\n\tconst numerico = reordenado.replace(/[A-Z]/g, (c) =&gt;\n\t\t(c.charCodeAt(0) - 55).toString(),\n\t);\n\n\treturn { valido: mod97(numerico) === 1, pais: iban.substring(0, 2) };\n}\n</code></pre>\n<h2>El problema de los números enormes</h2>\n<p>Un IBAN puede convertirse en una cadena de hasta 34 dígitos. Eso es un número que desborda con holgura la precisión de un <code>Number</code> estándar en JavaScript, que empieza a perder exactitud con enteros por encima de <code>Number.MAX_SAFE_INTEGER</code> (2^53 − 1). Si intentáramos hacer <code>parseInt(numerico) % 97</code> directamente con un número tan grande, el resultado sería incorrecto.</p>\n<p>Hay dos soluciones. La primera es la función <code>mod97</code> que aparece en el código anterior: procesa la cadena dígito a dígito, acumulando el resto parcial en cada paso. Es la opción más compatible, funciona en cualquier entorno y no requiere nada especial. La segunda es usar <code>BigInt</code>, disponible en todos los navegadores modernos desde hace años:</p>\n<pre><code class=\"language-javascript\">const valido = BigInt(numerico) % 97n === 1n;\n</code></pre>\n<p>Ambas opciones son correctas. La primera es más pedagógica y compatible; la segunda es más compacta. Quédate con la que mejor encaje en tu contexto.</p>\n<h2>Validar la longitud según el país</h2>\n<p>El formato mínimo descrito hasta aquí detecta IBANs con letras donde no toca o con una estructura radicalmente incorrecta, pero no avisa si alguien introduce un IBAN español de 22 caracteres cuando debería tener 24. Para eso, conviene incluir una tabla de longitudes por país.</p>\n<pre><code class=\"language-javascript\">const LONGITUDES_IBAN = {\n\tAL: 28,\n\tAD: 24,\n\tAT: 20,\n\tBE: 16,\n\tBA: 20,\n\tBR: 29,\n\tBG: 22,\n\tHR: 21,\n\tCY: 28,\n\tCZ: 24,\n\tDK: 18,\n\tEE: 20,\n\tFI: 18,\n\tFR: 27,\n\tDE: 22,\n\tGI: 23,\n\tGR: 27,\n\tHU: 28,\n\tIS: 26,\n\tIE: 22,\n\tIT: 27,\n\tLV: 21,\n\tLI: 21,\n\tLT: 20,\n\tLU: 20,\n\tMT: 31,\n\tMC: 27,\n\tNL: 18,\n\tNO: 15,\n\tPL: 28,\n\tPT: 25,\n\tRO: 24,\n\tSM: 27,\n\tSK: 24,\n\tSI: 19,\n\tES: 24,\n\tSE: 24,\n\tCH: 21,\n\tGB: 22,\n};\n</code></pre>\n<p>Con esta tabla, la función de validación puede ofrecer mensajes de error mucho más útiles: no solo «IBAN incorrecto», sino «este IBAN tiene 22 caracteres y los IBANs españoles tienen 24». Esa diferencia entre el error genérico y el error específico puede ahorrar al usuario varios intentos de frustración.</p>\n<h2>La función completa, con mensajes detallados</h2>\n<p>Juntando todo —limpieza de entrada, validación de formato, comprobación de longitud y algoritmo MOD-97— queda algo así:</p>\n<pre><code class=\"language-javascript\">const LONGITUDES_IBAN = {\n\tES: 24,\n\tDE: 22,\n\tFR: 27,\n\tGB: 22,\n\tIT: 27,\n\tPT: 25,\n\tNL: 18,\n\tBE: 16 /* ... */,\n};\n\nfunction mod97(numStr) {\n\tlet resto = 0;\n\tfor (const d of numStr) resto = (resto * 10 + parseInt(d)) % 97;\n\treturn resto;\n}\n\nfunction validarIBAN(raw) {\n\tconst iban = raw\n\t\t.trim()\n\t\t.toUpperCase()\n\t\t.replace(/[\\s\\-]/g, &quot;&quot;);\n\tconst pais = iban.substring(0, 2);\n\n\tif (!/^[A-Z]{2}\\d{2}[A-Z0-9]{4,}$/.test(iban)) {\n\t\treturn { valido: false, mensaje: &quot;Formato no reconocido&quot; };\n\t}\n\n\tconst longEsperada = LONGITUDES_IBAN[pais];\n\tif (longEsperada &amp;&amp; iban.length !== longEsperada) {\n\t\treturn {\n\t\t\tvalido: false,\n\t\t\tmensaje: `El IBAN de ${pais} debe tener ${longEsperada} caracteres (este tiene ${iban.length})`,\n\t\t};\n\t}\n\n\tconst numerico = (iban.substring(4) + iban.substring(0, 4)).replace(\n\t\t/[A-Z]/g,\n\t\t(c) =&gt; (c.charCodeAt(0) - 55).toString(),\n\t);\n\n\treturn {\n\t\tvalido: mod97(numerico) === 1,\n\t\tpais,\n\t\tcontrol: iban.substring(2, 4),\n\t\tbban: iban.substring(4),\n\t\tmensaje:\n\t\t\tmod97(numerico) === 1\n\t\t\t\t? &quot;IBAN válido&quot;\n\t\t\t\t: &quot;Los dígitos de control no son correctos&quot;,\n\t};\n}\n</code></pre>\n<p>El <code>replace(/[\\s\\-]/g, '')</code> al inicio merece una mención: los IBANs se presentan habitualmente agrupados en bloques de cuatro caracteres separados por espacios (<code>ES91 2100 0418...</code>), y algunos usuarios los copian así directamente. Limpiar esos espacios antes de procesar evita rechazos innecesarios y mejora la experiencia sin ningún coste.</p>\n<h2>Integración en un formulario con feedback en tiempo real</h2>\n<p>La validación funciona mejor cuando acompaña al usuario mientras escribe, no cuando le sorprende al pulsar «Enviar». Un pequeño listener sobre el campo es suficiente para dar ese feedback progresivo:</p>\n<pre><code class=\"language-javascript\">const campo = document.getElementById(&quot;iban&quot;);\nconst aviso = document.getElementById(&quot;aviso-iban&quot;);\n\ncampo.addEventListener(&quot;input&quot;, () =&gt; {\n\tconst valor = campo.value;\n\n\tif (!valor.trim()) {\n\t\taviso.textContent = &quot;&quot;;\n\t\tcampo.removeAttribute(&quot;aria-invalid&quot;);\n\t\treturn;\n\t}\n\n\tconst resultado = validarIBAN(valor);\n\taviso.textContent = resultado.mensaje;\n\taviso.className = resultado.valido ? &quot;aviso ok&quot; : &quot;aviso error&quot;;\n\tcampo.setAttribute(&quot;aria-invalid&quot;, resultado.valido ? &quot;false&quot; : &quot;true&quot;);\n});\n</code></pre>\n<p>Un detalle que vale la pena añadir: si tu formulario va a recibir IBANs de muchos países, muestra al usuario la longitud esperada o un ejemplo en el campo. Un placeholder como <code>ES91 2100 0418 4502 0005 1332</code> ayuda más que un campo vacío con una etiqueta que solo dice «IBAN».</p>\n<h2>Lo que esta validación no puede decirte</h2>\n<p>Conviene ser honesto sobre los límites del algoritmo. Un IBAN puede pasar la validación MOD-97 y aun así no existir como cuenta real —un número inventado que, por azar matemático, cumple el criterio. Esto es estadísticamente poco probable (la probabilidad de que un IBAN aleatorio sea válido es aproximadamente 1 entre 97), pero no imposible.</p>\n<p>La validación matemática descarta errores tipográficos, transposiciones de dígitos y formatos incorrectos con gran fiabilidad. Para verificar que una cuenta es real y está activa, habría que recurrir a servicios externos, y eso ya es otro artículo.</p>\n<p>A continuación puedes encontrar un validador funcional con todo lo que hemos descrito: detección del país, comprobación de longitud, algoritmo MOD-97 y desglose del resultado.</p>\n","date_published":"2026-04-14T00:00:00.000Z","image":"https://paigar.eu/validar-iban-formulario.png"},{"id":"https://paigar.eu/software-espanol-80s/","url":"https://paigar.eu/software-espanol-80s/","title":"La España que exportaba videojuegos","language":"es","content_html":"<p>Tenía el Abu Simbel Profanation en casete. Y el Hundra. Los cargaba en el MSX con esa paciencia que los ordenadores de entonces exigían —la ranura del lector, el contador en pantalla, el pitido de la cinta— y me quedaba mirando cómo se construía el juego en la pantalla línea a línea. Tardaba lo suyo. Pero era mío.</p>\n<p>Lo que no sabía entonces, o sabía solo vagamente, es que esos juegos los habían hecho españoles. Que detrás de aquellas carátulas con ilustraciones que parecían sacadas de una novela de fantasía había equipos pequeñísimos, a veces de dos o tres personas, en pisos de Madrid, programando en ensamblador para un ordenador de 8 bits con 48 kilobytes de RAM. Y que algunos de esos juegos —los mismos que yo cargaba en Bilbao— se estaban vendiendo en tiendas de Londres.</p>\n<h2>Por qué España y por qué entonces</h2>\n<p>La penetración de ordenadores domésticos en España a principios de los ochenta fue, por razones de precio y distribución, especialmente alta para el ZX Spectrum. Sinclair había encontrado aquí un mercado hambriento, y con el Spectrum llegó todo lo demás: las tiendas de software, las revistas especializadas, la comunidad de aficionados.</p>\n<p>MicroHobby, la publicación de referencia para los usuarios del Spectrum en España, llegó a tiradas de decenas de miles de ejemplares. Micromanía cubría el territorio más amplio de la informática personal. El ecosistema editorial era real y sostenido, y eso creó el caldo de cultivo para que hubiera programadores que quisieran publicar y un público dispuesto a comprar.</p>\n<p>El otro factor fue el bajo coste de entrada. Programar un juego para 8 bits no requería un estudio, ni un equipo de cincuenta personas, ni un presupuesto de producción. Requería tiempo, conocimientos de ensamblador y, en los mejores casos, una idea que valiera la pena. Esas condiciones favorecieron a los emprendedores jóvenes que no tenían capital pero sí tenían talento y muchas horas disponibles.</p>\n<h2>Dinamic y el oficio de construir desde la nada</h2>\n<p>Dinamic Software nació de tres hermanos: Víctor, Nacho y Pablo Ruiz. Empezaron a principios de los ochenta con juegos modestos y fueron ganando terreno hasta convertirse en la empresa de software de entretenimiento más importante que ha tenido este país. Abu Simbel Profanation —1985, Spectrum— fue uno de sus primeros grandes éxitos: un juego de plataformas con una dificultad implacable y una precisión técnica que se notaba en cada pantalla. Hundra llegó dos años después, con una protagonista que era una guerrera de la fantasía épica en un momento en que los videojuegos con personajes femeninos principales eran casi inexistentes.</p>\n<p>Las carátulas de Dinamic eran otro mundo. Ilustraciones de fantasía y ciencia ficción con una energía visual que no tenía nada que envidiar a las portadas de las novelas americanas del género. En una época en que el packaging era muchas veces la única seña de identidad de un producto que luego ocupaba 48K de memoria, Dinamic entendió que la primera impresión contaba y la cuidó con consistencia.</p>\n<p>Game Over, Army Moves, Freddy Hardest, After the War. Juego tras juego, la empresa fue construyendo un catálogo que se reconocía de un vistazo y que sus compradores esperaban con la misma anticipación con que hoy se espera un triple A.</p>\n<h2>Opera Soft y el argumento definitivo</h2>\n<p>Si Dinamic representa el músculo comercial de aquella industria, Opera Soft tiene el argumento más sólido para quien quiera hablar de excelencia sin matices. La Abadía del Crimen, publicado en 1987, es una de las obras más extraordinarias que ha producido el software de entretenimiento español en cualquier época.</p>\n<p>Paco Menéndez construyó un juego isométrico en 3D —en un Spectrum con 48K— inspirado directamente en <em>El nombre de la rosa</em> de Umberto Eco. No era una adaptación con licencia, sino una reinterpretación libre que tomaba el monasterio medieval, el monje detective y la atmósfera del libro y los convertía en mecánicas de juego. La arquitectura del edificio era coherente y navegable. Los personajes tenían rutinas propias. El argumento tenía peso.</p>\n<p>Pocos juegos de esa generación resisten el análisis de hoy como lo resiste La Abadía. Es el tipo de obra que dice algo sobre la persona que la hizo: alguien con algo concreto que expresar que encontró la manera de expresarlo dentro de unas limitaciones técnicas brutales.</p>\n<h2>El dato que cambia la perspectiva</h2>\n<p>Los juegos de Dinamic se vendían en el Reino Unido. Aparecían reseñados en <em>Crash</em> y <em>Your Sinclair</em>, que eran las publicaciones de referencia mundial para el Spectrum. No como curiosidades exóticas: como juegos que competían en el mismo mercado que los títulos británicos y americanos y que a veces los superaban en las valoraciones.</p>\n<p>España no era un mercado periférico que consumía lo que se producía en otros sitios. Era, junto con el Reino Unido, uno de los dos países europeos con una industria doméstica de desarrollo real para los ordenadores de 8 bits. Eso no lo sabe casi nadie hoy. Valdría la pena que lo supiera más gente.</p>\n<h2>El precipicio de los 16 bits</h2>\n<p>El problema llegó con la transición: el Amiga 500, el Atari ST, y poco después el PC. Los equipos de dos o tres personas que habían bastado para el Spectrum ya no eran suficientes. Los presupuestos de producción se dispararon, los tiempos de desarrollo se alargaron y la competencia internacional se profesionalizó de golpe.</p>\n<p>Algunas de esas empresas lo intentaron. Dinamic publicó títulos para Amiga y PC. Pero el salto era enorme, y las estructuras que habían funcionado perfectamente para los 8 bits no escalaban con facilidad. A principios de los noventa, la mayor parte de aquella generación de estudios había desaparecido o se había transformado en otra cosa.</p>\n<h2>Lo que queda</h2>\n<p>Queda la conciencia, para quien quiera buscarla, de que hubo aquí una industria creativa real, construida desde cero por gente joven con pocos recursos y mucho talento, que llegó a mercados internacionales en un momento en que eso no era fácil para ningún producto cultural español.</p>\n<p>Y quedan los juegos. El Abu Simbel en casete que tardaba tres minutos en cargar. El Hundra con su protagonista imposible. La Abadía, que sigue siendo visitable hoy y sigue funcionando como experiencia.</p>\n<p>No está mal para un país que, según el relato oficial, llegó tarde a la tecnología.</p>\n","date_published":"2026-04-07T00:00:00.000Z","image":"https://paigar.eu/software-espanol-80s.png"},{"id":"https://paigar.eu/color-harmony-article/","url":"https://paigar.eu/color-harmony-article/","title":"Cómo construir una herramienta de armonías de color con JavaScript","language":"es","content_html":"<p>Hace unas semanas necesitaba generar una paleta de colores para un proyecto y me encontré abriendo por décima vez la misma página de Adobe Color, haciendo clic en el complementario, copiando el hex, pegándolo en el CSS... una cadencia absurda para algo que en el fondo es pura matemática. Así que decidí construir mi propia herramienta. El resultado es una página HTML bastante sencilla que, dado un color de entrada, calcula y muestra sus distintas armonías cromáticas. Este artículo explica cómo funciona por dentro.</p>\n<h2>El círculo cromático, o por qué algunos colores se llevan bien</h2>\n<p>La teoría del color lleva siglos siendo estudiada, desde los primeros trabajos de Newton descomponiendo la luz blanca hasta el círculo de Itten que se enseña en cualquier escuela de diseño. La idea central es siempre la misma: los colores no son entidades aisladas, sino que existen en relación unos con otros, y esas relaciones tienen una geometría.</p>\n<p>El círculo cromático organiza los colores por su tono —lo que en inglés se llama <em>hue</em>— en un espacio circular de 360 grados. Los colores primarios de la luz (rojo, verde, azul) están separados a 120 grados entre sí. Entre ellos se distribuyen los intermedios: amarillos, cians, magentas, naranjas. La clave es que cuando hablamos de armonía cromática, estamos hablando de ángulos: qué ocurre si tomamos el color opuesto, o los que están a un tercio del círculo, o los vecinos inmediatos.</p>\n<p>Esta geometría es lo que convierte un problema de &quot;¿qué color combina con este?&quot; en algo calculable con una suma simple. Si tu color está en la posición 30 grados del círculo, su complementario está exactamente en 30 + 180 = 210 grados. Su triádico, a 30 + 120 = 150 y a 30 + 240 = 270. No hay magia, solo rotación.</p>\n<h2>Los cinco tipos de armonía que vale la pena conocer</h2>\n<p>La armonía <strong>complementaria</strong> es la más elemental: el color opuesto en el círculo, a 180 grados. Produce el contraste más fuerte posible y es lo que hace que un cartel naranja sobre azul resulte tan llamativo. Úsala cuando quieras impacto; evítala cuando quieras sutileza.</p>\n<p>El <strong>complementario dividido</strong> es una versión más refinada: en lugar de ir directamente al opuesto, tomas los dos colores que flanquean ese opuesto, a ±150 grados del original. El resultado es casi igual de llamativo pero bastante menos agresivo, y tienes tres colores en lugar de dos.</p>\n<p>La armonía <strong>triádica</strong> coloca tres colores a 120 grados entre sí, formando un triángulo equilátero dentro del círculo. Es vibrante y equilibrada. Es la favorita de muchos diseñadores de interfaces porque ninguno de los tres colores domina demasiado sobre los otros.</p>\n<p>La armonía <strong>análoga</strong> toma los vecinos inmediatos del color base, normalmente a ±30 grados. El resultado es la más tranquila y natural de todas —piensa en los degradados de una puesta de sol, o en los verdes de un bosque. Los colores análogos son siempre seguros y agradables, pero pueden volverse aburridos si no hay suficiente variación de luminosidad o saturación.</p>\n<p>Por último, la <strong>tetrádica</strong> —también llamada cuadrada— coloca cuatro colores a 90 grados entre sí. Es la más compleja de manejar porque da mucho color, pero en manos de alguien que sabe lo que hace produce resultados muy ricos.</p>\n<h2>HSL: el espacio de color que hace todo esto natural</h2>\n<p>Para implementar estas armonías en código, lo primero es elegir el sistema de representación del color adecuado. El formato hexadecimal que usamos en CSS (<code>#3B82F6</code>) es cómodo para escribir pero inútil para calcular. El RGB tampoco ayuda mucho. Lo que necesitamos es el sistema <strong>HSL</strong>: Hue (tono), Saturation (saturación), Lightness (luminosidad).</p>\n<p>En HSL, el tono es exactamente el ángulo en el círculo cromático, un valor entre 0 y 360. La saturación va de 0% (gris neutro) a 100% (color puro). La luminosidad va de 0% (negro) a 100% (blanco), con el 50% como punto donde el color es más vívido. Esto significa que calcular el complementario de un color HSL es tan directo como sumar 180 al valor H:</p>\n<pre><code class=\"language-javascript\">function complementario(h, s, l) {\n\treturn [(h + 180) % 360, s, l];\n}\n</code></pre>\n<p>Y el triádico, añadir 120 y 240:</p>\n<pre><code class=\"language-javascript\">function triadica(h, s, l) {\n\treturn [\n\t\t[h, s, l],\n\t\t[(h + 120) % 360, s, l],\n\t\t[(h + 240) % 360, s, l],\n\t];\n}\n</code></pre>\n<p>El operador <code>% 360</code> asegura que si el ángulo supera 360, vuelve a empezar desde cero. En JavaScript, hay que tener cuidado con los negativos —si sumamos -30 a un hue de 10, obtenemos -20, que no tiene sentido en el círculo— así que conviene usar <code>((h + offset) % 360 + 360) % 360</code> para estar seguros.</p>\n<h2>Convertir entre formatos: de hex a HSL y vuelta</h2>\n<p>El problema práctico es que los usuarios (y los selectores de color del navegador) trabajan con hexadecimal, pero nuestros cálculos necesitan HSL. Así que la herramienta necesita dos conversiones: <code>hexToHsl</code> y <code>hslToHex</code>.</p>\n<p>La conversión de hex a HSL funciona así: primero extraemos los canales R, G y B del string hexadecimal, los normalizamos a un rango de 0 a 1, y luego calculamos el tono, la saturación y la luminosidad con unas pocas operaciones aritméticas. El tono depende de cuál de los tres canales es el máximo, y se expresa como un ángulo en el círculo.</p>\n<pre><code class=\"language-javascript\">function hexToHsl(hex) {\n\tconst r = parseInt(hex.slice(1, 3), 16) / 255;\n\tconst g = parseInt(hex.slice(3, 5), 16) / 255;\n\tconst b = parseInt(hex.slice(5, 7), 16) / 255;\n\n\tconst max = Math.max(r, g, b);\n\tconst min = Math.min(r, g, b);\n\tconst l = (max + min) / 2;\n\tlet h = 0,\n\t\ts = 0;\n\n\tif (max !== min) {\n\t\tconst d = max - min;\n\t\ts = l &gt; 0.5 ? d / (2 - max - min) : d / (max + min);\n\t\tif (max === r) h = ((g - b) / d + (g &lt; b ? 6 : 0)) / 6;\n\t\telse if (max === g) h = ((b - r) / d + 2) / 6;\n\t\telse h = ((r - g) / d + 4) / 6;\n\t}\n\n\treturn [Math.round(h * 360), Math.round(s * 100), Math.round(l * 100)];\n}\n</code></pre>\n<p>La conversión inversa, de HSL a hex, es algo más compleja matemáticamente pero sigue el mismo principio. Se reconstruyen los canales RGB a partir del tono, la saturación y la luminosidad, y luego se convierten a hexadecimal:</p>\n<pre><code class=\"language-javascript\">function hslToHex(h, s, l) {\n\th = ((h % 360) + 360) % 360;\n\ts /= 100;\n\tl /= 100;\n\tconst a = s * Math.min(l, 1 - l);\n\tconst f = (n) =&gt; {\n\t\tconst k = (n + h / 30) % 12;\n\t\tconst c = l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));\n\t\treturn Math.round(255 * c)\n\t\t\t.toString(16)\n\t\t\t.padStart(2, &quot;0&quot;);\n\t};\n\treturn &quot;#&quot; + f(0) + f(8) + f(4);\n}\n</code></pre>\n<p>Con estas dos funciones, el flujo completo es: recibir un color en hex → convertir a HSL → aplicar los offsets angulares → convertir cada resultado de vuelta a hex → mostrar en pantalla.</p>\n<h2>La interfaz: selector de color, swatches y rueda visual</h2>\n<p>La parte visual de la herramienta se construye con HTML y CSS estándar, sin frameworks ni librerías externas. El selector de color usa el elemento nativo <code>&lt;input type=&quot;color&quot;&gt;</code>, que en los navegadores modernos abre un selector completo con soporte para cualquier color del espacio sRGB. Complementamos esto con un campo de texto donde el usuario puede escribir directamente el código hex, validando en tiempo real que tenga el formato correcto (<code>#</code> seguido de seis caracteres hexadecimales).</p>\n<p>La rueda de color que aparece en la interfaz es un <code>&lt;canvas&gt;</code> de HTML5. Se dibuja recorriendo los 360 grados y pintando para cada ángulo un arco con un degradado radial que va del blanco en el centro al color puro del borde. El resultado es una representación visual del espacio HSL con saturación y luminosidad fijas. Sobre esta rueda se colocan puntos que marcan la posición del color base y su complementario.</p>\n<p>Los swatches de cada armonía son simples <code>&lt;div&gt;</code> con su <code>background-color</code> calculado. Al hacer clic sobre cualquiera de ellos, se copia el hex al portapapeles usando la API <code>navigator.clipboard.writeText()</code>. Una pequeña notificación emergente confirma la acción.</p>\n<h2>Lo que me llevé de todo esto</h2>\n<p>Construir esta herramienta me recordó que detrás de muchas decisiones de diseño que parecen intuitivas hay estructuras matemáticas bastante elegantes. El círculo cromático no es una metáfora bonita: es un espacio vectorial donde la armonía es literalmente una cuestión de geometría. Puedes intuir que el naranja y el azul combinan bien, o puedes saber que están a 180 grados entre sí en un espacio de 360, y que eso maximiza el contraste perceptivo entre dos tonos.</p>\n<p>El código completo de la herramienta está disponible en esta misma página para que puedas usarla directamente, o coger las funciones de conversión y adaptarlas a tus propios proyectos. No hay dependencias, no hay build step, no hay nada que instalar. Un archivo HTML y ya está.</p>\n","date_published":"2026-04-03T00:00:00.000Z","image":"https://paigar.eu/color-harmony-tool.png"},{"id":"https://paigar.eu/venus-maritima-gripari/","url":"https://paigar.eu/venus-maritima-gripari/","title":"Venus Marítima, el relato que me voló la cabeza","language":"es","content_html":"<p>Algunos cuentos los lees una vez y se te quedan dentro para siempre. No me refiero a los grandes clásicos, esos que ya vienen avalados por todos los profesores de literatura del mundo. Me refiero a esos otros, raros, casi clandestinos, que descubres por casualidad en algún volumen prestado y que años después siguen apareciendo en tu cabeza cuando menos te lo esperas. <em>Venus Marítima</em>, de Pierre Gripari, es uno de esos para mí. Lo leí hace bastantes años y desde entonces vuelve a aparecer cada cierto tiempo, sin avisar, normalmente cuando paso cerca de una playa atlántica con la marea baja.</p>\n<p>He vuelto al texto esta semana después de mucho tiempo y me he llevado una sorpresa: lo recordaba a medias. Recordaba bien la parte que más me había impresionado en su momento —los hombres de Arcachón y su cofradía silenciosa— y había olvidado todo lo demás, que es prácticamente la mitad del cuento. Releerlo entero ahora ha sido como descubrirlo otra vez. Y como me ha encantado por segunda vez, viene aquí.</p>\n<h2>El planteamiento, que ya te coloca de entrada</h2>\n<p>El relato empieza con una frase que es directamente una bofetada: <em>cuando los venusianos vuelvan a la Tierra, el mundo comprenderá por qué Jesucristo no rió jamás</em>. Así, sin preámbulo. Y a partir de ahí Gripari se pone a contarte el futuro de la humanidad como si ya lo hubiera vivido. Todo el cuento está escrito en futuro, no en pasado ni en presente, lo cual es bastante raro y le da un aire profético de los que dejan huella. Hacia el final del relato te explica de pasada que un equipo de médiums diplomados ha previsto la historia. Cuando uno termina y vuelve al principio, descubre que la frase de apertura ya contenía todo el cuento. Solo que entonces no podías saberlo.</p>\n<p>El argumento, contado a grandes rasgos: en 1972 los venusianos llegan a la Tierra, físicamente idénticos a Jesucristo, y aterrizan justo a tiempo de evitar la Tercera Guerra Mundial entre los tres imperios del momento. A cambio imponen un gobierno planetario único y declaran el socialismo filosofía universal obligatoria. La Tierra se reorganiza en ocho repúblicas étnicamente puras —con deportaciones masivas para conseguirlo— y el mundo entero pasa a estar gobernado desde la Luna por una octarquía que colabora estrechamente con los venusianos. Hasta aquí ya tienes ciencia ficción política para parar un tren.</p>\n<p>Pero lo bueno viene después.</p>\n<h2>Cuando el sexo pasa a ser propiedad del Estado</h2>\n<p>En este nuevo orden mundial, las cosas íntimas se van regulando poco a poco. Primero se abole el matrimonio. Luego se prohíbe la unión libre. Después aparece un invento llamado el Automacon, una máquina paregórica de pago propiedad del Estado, que canaliza la satisfacción sexual de manera ordenada y contable. Y en 1999, cualquier coito entre individuos de sexos diferentes pasa a estar castigado con reclusión perpetua para los dos. Sí, los dos.</p>\n<p>Hay una frase del cuento que resume toda la lógica del sistema y que es de las cosas más afiladas que recuerdo haber leído. Es el artículo 127 de la Carta Mundial del Socialismo: <em>todo deseo cuya satisfacción no proporcione dinero al Estado no es más que un vicio</em>. Léela despacio. Y luego léela otra vez. Porque ahí dentro hay algo que da bastante miedo.</p>\n<p>Eso lo escribió Gripari en 1972, fíjate. Cuando todavía existía la URSS, cuando la Guerra Fría estaba en su apogeo, cuando la idea de un gobierno mundial sonaba a delirio de novela. Hoy, leyéndolo en 2026, esa frase sigue funcionando perfectamente. Solo hay que cambiar la palabra Estado por la que más rabia te dé: plataforma, algoritmo, suscripción, lo que sea. La idea de fondo es la misma y se ha vuelto, si acaso, más actual.</p>\n<h2>Y entonces aparece el microbio rosa</h2>\n<p>En medio de toda esa pesadilla burocrática, ocurre algo que nadie había previsto. Hacia 1987, las aguas de la bahía de Arcachón empiezan a cambiar de color. Se van volviendo poco a poco rosadas, espesas, opacas, ligeramente viscosas. Cuando los científicos las analizan al microscopio descubren que están saturadas de una bacteria nueva, una mutación del esperma venusiano que se ha adaptado a la vida marina. La bautizan Venus Marítima, aunque pronto la gente la llamará simplemente <em>el microbio rosa</em>.</p>\n<p>Las autoridades dudan si destruirla o dejarla vivir. Al final la dejan vivir porque parece inofensiva, está confinada a la bahía, y resulta que constituye un alimento extraordinario para las ostras de la zona, cuya producción se dispara. Una decisión razonable. Una decisión, también, que el régimen va a lamentar muchísimo en los años siguientes. Porque lo que las autoridades no saben todavía es que ese microbio se alimenta exclusivamente de esperma de mamíferos, y que tiene una especie de premonición de la sensualidad masculina que le permite ofrecer al macho humano una experiencia infinitamente superior al Automacon estatal. Y, lo más imperdonable de todo desde el punto de vista del régimen, gratuita.</p>\n<h2>El secreto mejor guardado del mundo</h2>\n<p>Y aquí viene la parte que me dejó marcado cuando leí el cuento por primera vez y que sigue pareciéndome la más extraordinaria. Durante quince años, de 1990 a 2005, los hombres de Eurasia entera viajan a Arcachón en cuanto pueden. Trabajadores rusos pidiendo vacaciones en las Landas, gente que hace el viaje desde Vladivostok, ancianos, adolescentes, adultos. Todos van. Todos se bañan en aquella agua rosada y tibia. Y nadie dice nada.</p>\n<p>No estalla el escándalo, no se nombra el secreto, no se hace ni siquiera alusión. Adolescentes, adultos, ancianos, todo el mundo calla. Hay un párrafo en el cuento donde Gripari subraya que esa discreción colectiva supone una facultad de disimulo <em>no solamente frente al poder, sino también frente a la población femenina</em>, que tiene visos de prodigio. Y luego añade, con la mala leche que le sale natural, que una discreción así solo es posible bajo régimen socialista.</p>\n<p>Aparecen señales raras, claro. Los hombres se bañan menos rato que las mujeres, ciertos individuos van en grupos al caer la noche, se forma un amago de secta llamada <em>los adoradores del mar</em> que las autoridades disuelven aplicando la ley contra ideologías no marxistas. Pero ni siquiera durante el proceso judicial los acusados hablan. Y los jueces, esto es magistral, <em>se guardarán de hacerles preguntas demasiado precisas</em>. Es decir: no solo callan los gobernados. También callan los gobernantes, porque saben que conocer ciertas cosas les obligaría a actuar contra placeres que, en el fondo, también se reservan para sí mismos.</p>\n<h2>El perrito que lo arruina todo</h2>\n<p>Como casi siempre pasa con los grandes secretos, el final no llega por una traición ni por una investigación policial. Llega por un accidente menor. Una turista rusa, presentada en el cuento como <em>la Dama del Perrito</em>, fuerza a su caniche Polkan a bañarse en Arcachón. El perro, ajeno a las cautelas humanas que habían sostenido el secreto durante tres lustros, se niega a salir del agua. La escena que Gripari describe es comiquísima y desoladora a la vez: el caniche con el trasero metido en la bahía, los riñones moviéndose espasmódicamente, los ojos en blanco, la lengua fuera, gruñendo a su dueña cuando esta intenta sacarlo del agua.</p>\n<p>A partir de ahí todo se precipita. La dama escribe a un amigo escritor, este viaja a Arcachón a comprobarlo personalmente, redacta un informe oficial que se publica en todas las lenguas del mundo, y el régimen pone en marcha la maquinaria de la propaganda. Al microbio rosa lo bautizan con nombres burlescos en cada idioma —los ingleses lo llaman <em>sea-whore</em>, los alemanes <em>Wassarhure</em>, los rusos <em>Vodobliad</em>— para intentar quitarle dignidad. Los científicos desarrollan un antibiótico específico, lo lanzan en dosis masivas a la bahía, y Venus Marítima desaparece para siempre. Las playas se desinfectan y se reabren al público.</p>\n<p>El cuento termina con una sola frase que llevo intentando olvidar desde que la leí por primera vez y no consigo: <em>tras este intermedio, la humanidad podrá dormirse de nuevo, para largos milenios, en el profundo aburrimiento de la era socialista</em>. Telón.</p>\n<h2>Por qué me parece que el cuento sigue dando en el clavo</h2>\n<p>Me he pasado los últimos días pensando por qué este cuento, escrito hace más de cincuenta años por un autor casi desconocido, sigue resonando tan fuerte. Y creo que es por el artículo 127. Por esa frase del Estado y los deseos que no producen dinero. Porque el cuento, al final, no va de venusianos ni de microbios rosa. Va de cómo cualquier sistema, cuando logra suficiente poder, intenta capturar las formas en que las personas obtienen placer, y de cómo las cosas que se escapan de esa captura terminan siendo perseguidas, ridiculizadas o exterminadas con antibiótico.</p>\n<p>Gripari estaba pensando en la URSS, eso queda claro. Pero el mecanismo que describe no es exclusivo de aquel régimen. Cualquier sistema, en cuanto puede, intenta canalizar los placeres hacia formas que pueda contar, gravar o regular. Y cualquier sistema reacciona mal cuando descubre que existe un placer importante al que no puede meterle factura. Por eso este cuento de 1972 sigue pareciendo escrito ayer. La forma exterior es de los setenta, pero el motor de la historia funciona perfectamente con el combustible que le eches: comunismo, capitalismo de plataforma, moralismo de cualquier color. Lo único que no perdona el sistema, sea cual sea, es la felicidad gratuita.</p>\n<h2>Por qué conviene buscarlo</h2>\n<p>El libro se sigue editando en francés, dentro de un volumen que lleva el mismo título que el relato. En castellano hubo traducciones antiguas, de tirada limitada, y conviene buscarlas en librerías de viejo o en plataformas de segunda mano. Yo no recuerdo bien dónde encontré la mía, pero sé que no fue en una librería normal de las grandes. La búsqueda forma parte del juego. Hay algo bonito en perseguir un libro que no está en todos los escaparates, especialmente cuando lo que vas a encontrar dentro es justamente un cuento sobre un secreto compartido por pocos.</p>\n<p>Si lo encuentras, léelo en una tarde tranquila. Cerca del mar a poder ser, aunque no es imprescindible. Y si después de leerlo te quedas pensando en el artículo 127 durante varios días, no te preocupes. A mí me lleva pasando desde la primera vez. Es lo que tienen ciertos cuentos. Una vez te entran, se quedan.</p>\n","date_published":"2026-03-24T00:00:00.000Z","image":"https://paigar.eu/venus-maritima-gripari.png"},{"id":"https://paigar.eu/juego-serpiente-tutorial/","url":"https://paigar.eu/juego-serpiente-tutorial/","title":"Cómo programar el juego de la serpiente desde cero","language":"es","content_html":"<p>El juego de la serpiente es uno de los proyectos clásicos para aprender a programar. Cabe en menos de doscientas líneas de código, no necesita librerías externas ni herramientas complicadas, y en el camino te obliga a tocar prácticamente todos los conceptos importantes de un programa interactivo: bucle de juego, gestión de estado, captura de eventos de teclado, detección de colisiones, dibujo en pantalla y puntuación. Es ese tipo de proyecto pequeño que enseña mucho, y que además da satisfacción inmediata porque al terminarlo tienes algo que se puede jugar de verdad.</p>\n<p>En este artículo voy a explicar cómo construirlo paso a paso en HTML, CSS y JavaScript, sin frameworks de ningún tipo, sin compiladores, sin servidores. Solo un archivo <code>.html</code> que abres en tu navegador y ya está. Al final del artículo dejo el prototipo entero funcionando para que puedas jugarlo y, si te apetece, ver el código fuente y trastear con él.</p>\n<h2>La idea general antes de tocar código</h2>\n<p>Antes de escribir una sola línea conviene tener claro qué es lo que vamos a construir. Un juego de la serpiente, en su esencia, consiste en una cuadrícula sobre la que se mueve una serpiente formada por segmentos cuadrados. La serpiente avanza automáticamente en una dirección, y el jugador puede cambiar esa dirección con las flechas del teclado. En la cuadrícula aparece una manzana en una posición aleatoria. Cuando la cabeza de la serpiente llega a la casilla donde está la manzana, esta desaparece, aparece otra en una nueva posición aleatoria y la serpiente crece un segmento. El juego termina si la serpiente choca contra los bordes del tablero o si se muerde a sí misma.</p>\n<p>Conceptualmente, todo el juego se reduce a repetir el mismo paso una y otra vez, varias veces por segundo. Ese paso consiste en mover la serpiente una casilla en la dirección actual, comprobar si ha chocado contra algo, comprobar si ha comido la manzana y dibujar el resultado en pantalla. Esto se llama el <em>bucle de juego</em>, y es la pieza central de cualquier videojuego, desde el más sencillo hasta el más complejo. Si entiendes el bucle, entiendes la estructura. Lo demás son detalles.</p>\n<h2>El esqueleto HTML y CSS</h2>\n<p>Empezamos por la página web que va a contener el juego. Es lo más sencillo de todo: un archivo HTML con un elemento <code>&lt;canvas&gt;</code> donde vamos a dibujar, un par de elementos para mostrar la puntuación y un poco de CSS para que tenga un aspecto decente. El <code>&lt;canvas&gt;</code> es un elemento del HTML pensado precisamente para dibujar gráficos mediante JavaScript, y es la herramienta natural para hacer este tipo de juegos.</p>\n<pre><code class=\"language-html\">&lt;!DOCTYPE html&gt;\n&lt;html lang=&quot;es&quot;&gt;\n\t&lt;head&gt;\n\t\t&lt;meta charset=&quot;UTF-8&quot; /&gt;\n\t\t&lt;title&gt;Snake&lt;/title&gt;\n\t\t&lt;style&gt;\n\t\t\tbody {\n\t\t\t\tdisplay: flex;\n\t\t\t\tflex-direction: column;\n\t\t\t\talign-items: center;\n\t\t\t\tbackground: #1a1d24;\n\t\t\t\tcolor: #e8e8e8;\n\t\t\t\tfont-family: system-ui, sans-serif;\n\t\t\t}\n\t\t\tcanvas {\n\t\t\t\tbackground: #0f1115;\n\t\t\t\tborder: 1px solid #2a2e36;\n\t\t\t}\n\t\t&lt;/style&gt;\n\t&lt;/head&gt;\n\t&lt;body&gt;\n\t\t&lt;h1&gt;Snake&lt;/h1&gt;\n\t\t&lt;div&gt;Puntuación: &lt;span id=&quot;puntuacion&quot;&gt;0&lt;/span&gt;&lt;/div&gt;\n\t\t&lt;canvas id=&quot;lienzo&quot; width=&quot;400&quot; height=&quot;400&quot;&gt;&lt;/canvas&gt;\n\t\t&lt;script&gt;\n\t\t\t// aquí irá todo el código del juego\n\t\t&lt;/script&gt;\n\t&lt;/body&gt;\n&lt;/html&gt;\n</code></pre>\n<p>Con esto ya tenemos un canvas negro de 400 por 400 píxeles centrado en la pantalla. Vamos a dibujar dentro de él una cuadrícula imaginaria de 20 por 20 casillas, cada una de 20 píxeles. Esa cuadrícula no la vamos a dibujar realmente, pero sí la vamos a usar mentalmente como sistema de coordenadas para colocar la serpiente y la manzana.</p>\n<h2>El estado del juego</h2>\n<p>Lo siguiente es decidir qué información necesitamos guardar en cada momento para que el juego funcione. Esto se llama <em>estado del juego</em>, y para Snake es bastante sencillo. Necesitamos saber dónde está cada segmento de la serpiente, en qué dirección se mueve, dónde está la manzana, cuál es la puntuación y si la partida ha terminado. Lo representamos así:</p>\n<pre><code class=\"language-javascript\">const TAMANO_CASILLA = 20;\nconst COLUMNAS = 20;\nconst FILAS = 20;\n\nlet serpiente = [\n\t{ x: 10, y: 10 },\n\t{ x: 9, y: 10 },\n\t{ x: 8, y: 10 },\n];\nlet direccion = { x: 1, y: 0 };\nlet manzana = { x: 5, y: 5 };\nlet puntuacion = 0;\nlet terminado = false;\n</code></pre>\n<p>La serpiente es un array de objetos donde cada objeto representa un segmento con sus coordenadas en la cuadrícula. El primer elemento del array es siempre la cabeza, y los siguientes el cuerpo. La dirección la representamos como un vector con valores <code>x</code> e <code>y</code> que pueden ser uno, menos uno o cero. Por ejemplo, <code>{ x: 1, y: 0 }</code> significa que la serpiente se mueve hacia la derecha. Esto es muy cómodo porque permite calcular la siguiente posición de la cabeza simplemente sumando la dirección a la posición actual.</p>\n<h2>Mover la serpiente</h2>\n<p>Mover la serpiente es probablemente lo más bonito conceptualmente del juego, y lo más sencillo si lo piensas bien. La idea es la siguiente: en cada paso del juego, calculamos dónde estaría la nueva cabeza, la añadimos al principio del array, y quitamos el último elemento. Así la serpiente se desplaza una casilla sin tener que mover todos los segmentos uno a uno. Cuando la serpiente come una manzana, hacemos lo mismo pero sin quitar el último elemento, y así crece.</p>\n<pre><code class=\"language-javascript\">function paso() {\n\tif (terminado) return;\n\n\tconst cabeza = serpiente[0];\n\tconst nuevaCabeza = {\n\t\tx: cabeza.x + direccion.x,\n\t\ty: cabeza.y + direccion.y,\n\t};\n\n\tserpiente.unshift(nuevaCabeza);\n\n\tif (nuevaCabeza.x === manzana.x &amp;&amp; nuevaCabeza.y === manzana.y) {\n\t\tpuntuacion += 10;\n\t\tcolocarManzana();\n\t} else {\n\t\tserpiente.pop();\n\t}\n}\n</code></pre>\n<p>El método <code>unshift</code> añade un elemento al principio del array, y <code>pop</code> elimina el último. Ese pequeño truco evita tener que recorrer todos los segmentos en cada iteración, lo cual sería ineficiente si la serpiente fuera muy larga.</p>\n<h2>Detectar colisiones</h2>\n<p>Una serpiente que se mueve sin parar pero nunca pierde no es un juego. Necesitamos comprobar dos tipos de colisiones: contra los bordes del tablero y contra el propio cuerpo de la serpiente. Las dos comprobaciones se hacen justo antes de añadir la nueva cabeza al array.</p>\n<pre><code class=\"language-javascript\">const fueraDeLimites =\n\tnuevaCabeza.x &lt; 0 ||\n\tnuevaCabeza.x &gt;= COLUMNAS ||\n\tnuevaCabeza.y &lt; 0 ||\n\tnuevaCabeza.y &gt;= FILAS;\n\nconst seMuerde = serpiente.some(\n\t(s) =&gt; s.x === nuevaCabeza.x &amp;&amp; s.y === nuevaCabeza.y,\n);\n\nif (fueraDeLimites || seMuerde) {\n\tterminado = true;\n\treturn;\n}\n</code></pre>\n<p>El método <code>some</code> recorre el array y devuelve <code>true</code> si encuentra al menos un elemento que cumpla la condición. En este caso, comprobamos si hay algún segmento de la serpiente cuya posición coincida con la nueva cabeza. Si lo hay, es que la serpiente se ha mordido a sí misma, y el juego termina.</p>\n<h2>Colocar la manzana</h2>\n<p>La manzana tiene que aparecer en una posición aleatoria del tablero, pero hay que tener cuidado: no puede aparecer encima de la serpiente, porque si lo hace el jugador la comería sin querer en cuanto se moviera. La forma más sencilla y robusta de evitarlo es generar posiciones aleatorias hasta dar con una que no esté ocupada.</p>\n<pre><code class=\"language-javascript\">function colocarManzana() {\n\twhile (true) {\n\t\tconst candidata = {\n\t\t\tx: Math.floor(Math.random() * COLUMNAS),\n\t\t\ty: Math.floor(Math.random() * FILAS),\n\t\t};\n\t\tconst colisiona = serpiente.some(\n\t\t\t(s) =&gt; s.x === candidata.x &amp;&amp; s.y === candidata.y,\n\t\t);\n\t\tif (!colisiona) {\n\t\t\tmanzana = candidata;\n\t\t\treturn;\n\t\t}\n\t}\n}\n</code></pre>\n<p>Para una serpiente de tamaño normal este bucle termina prácticamente al primer intento, así que la ineficiencia teórica de tener un bucle infinito hipotético no es un problema real. Solo se notaría si la serpiente llegase a ocupar casi todo el tablero, en cuyo caso el jugador estaría a punto de ganar de todas formas.</p>\n<h2>Capturar el teclado</h2>\n<p>El jugador necesita poder cambiar la dirección de la serpiente. Esto se hace escuchando el evento <code>keydown</code> del documento y traduciendo cada tecla a un nuevo vector de dirección. Hay un detalle importante: no podemos permitir que la serpiente gire ciento ochenta grados de golpe, porque eso haría que la cabeza chocara inmediatamente con el segundo segmento del cuerpo. Así que filtramos las teclas que invierten exactamente la dirección actual.</p>\n<pre><code class=\"language-javascript\">document.addEventListener(&quot;keydown&quot;, (e) =&gt; {\n\tconst teclas = {\n\t\tArrowUp: { x: 0, y: -1 },\n\t\tArrowDown: { x: 0, y: 1 },\n\t\tArrowLeft: { x: -1, y: 0 },\n\t\tArrowRight: { x: 1, y: 0 },\n\t};\n\tif (teclas[e.key]) {\n\t\tconst nueva = teclas[e.key];\n\t\tif (nueva.x === -direccion.x &amp;&amp; nueva.y === -direccion.y) return;\n\t\tdireccion = nueva;\n\t}\n});\n</code></pre>\n<p>En el prototipo final del artículo añado también las teclas WASD como alternativa a las flechas, una pausa con la barra espaciadora y soporte táctil con botones para que el juego funcione en móvil. Pero la lógica es exactamente la misma.</p>\n<h2>Dibujar en el canvas</h2>\n<p>Dibujar es la parte más visual y, por suerte, una de las más fáciles. El canvas tiene un objeto llamado <em>contexto</em> que ofrece métodos para dibujar rectángulos, líneas, texto y formas. Para Snake nos basta con dibujar rectángulos de colores. En cada iteración del juego limpiamos todo el canvas pintándolo del color de fondo y luego dibujamos la manzana y los segmentos de la serpiente en sus posiciones actuales.</p>\n<pre><code class=\"language-javascript\">const lienzo = document.getElementById(&quot;lienzo&quot;);\nconst ctx = lienzo.getContext(&quot;2d&quot;);\n\nfunction dibujar() {\n\tctx.fillStyle = &quot;#0f1115&quot;;\n\tctx.fillRect(0, 0, lienzo.width, lienzo.height);\n\n\tctx.fillStyle = &quot;#e74c3c&quot;;\n\tctx.fillRect(\n\t\tmanzana.x * TAMANO_CASILLA,\n\t\tmanzana.y * TAMANO_CASILLA,\n\t\tTAMANO_CASILLA,\n\t\tTAMANO_CASILLA,\n\t);\n\n\tctx.fillStyle = &quot;#3ecf8e&quot;;\n\tserpiente.forEach((segmento) =&gt; {\n\t\tctx.fillRect(\n\t\t\tsegmento.x * TAMANO_CASILLA,\n\t\t\tsegmento.y * TAMANO_CASILLA,\n\t\t\tTAMANO_CASILLA,\n\t\t\tTAMANO_CASILLA,\n\t\t);\n\t});\n}\n</code></pre>\n<p>Las coordenadas de la serpiente y la manzana están en casillas, así que para convertirlas a píxeles basta con multiplicar por el tamaño de casilla. Si en algún momento quieres hacer la cuadrícula más grande o más pequeña, solo tienes que cambiar las constantes y el resto del código sigue funcionando sin tocar nada.</p>\n<h2>El bucle principal</h2>\n<p>Lo último que falta es repetir todo esto varias veces por segundo. La forma más sencilla es usar <code>setInterval</code>, que ejecuta una función cada cierto número de milisegundos. Para Snake, un intervalo de unos cien o ciento veinte milisegundos da una velocidad razonable. Más rápido se vuelve frustrante, más lento se vuelve aburrido.</p>\n<pre><code class=\"language-javascript\">function bucle() {\n\tpaso();\n\tdibujar();\n}\n\nsetInterval(bucle, 110);\n</code></pre>\n<p>Y con esto, el juego ya funciona. Tienes una serpiente que se mueve por el tablero, responde a las flechas del teclado, come manzanas y crece, choca con los bordes y consigo misma, y muestra una puntuación que va subiendo. Todo en menos de cien líneas de JavaScript si lo escribes apretado, y bastante por debajo de doscientas si lo organizas con calma como en el prototipo final.</p>\n<h2>Cosas que se pueden añadir si te apetece seguir</h2>\n<p>A partir de aquí, las posibilidades son enormes. Puedes añadir niveles que aumenten la velocidad cada cierta puntuación. Puedes añadir manzanas especiales que valgan más puntos pero aparezcan solo durante unos segundos. Puedes hacer que el tablero tenga obstáculos fijos. Puedes guardar el récord en <code>localStorage</code> para que persista entre sesiones. Puedes añadir sonidos. Puedes hacer que la serpiente atraviese los bordes y aparezca por el lado contrario, en lugar de morir. Puedes meter dos jugadores en el mismo tablero. Cualquier idea que se te ocurra cabe en este esqueleto.</p>\n<p>Pero el verdadero valor de programar Snake no está en lo que se le añada después, sino en lo que se aprende construyéndolo. Cuando entiendes este código, entiendes la estructura básica de prácticamente cualquier juego clásico. El bucle, el estado, los eventos, el dibujado, las colisiones. Cambia los detalles y tienes un Tetris. Cambia otros y tienes un Pong. Cambia más cosas y tienes un Pac-Man. Todos comparten el mismo esqueleto que acabamos de construir aquí.</p>\n<h2>El prototipo funcional</h2>\n<p>Aquí abajo dejo el juego completo y funcionando. Es exactamente el mismo código que hemos ido viendo a lo largo del artículo, organizado un poco más limpio, con soporte para WASD y flechas, pausa con la barra espaciadora, controles táctiles para móvil, un récord que se mantiene durante la sesión y una pantalla de fin de partida con instrucciones para reiniciar.</p>\n<p><strong>Otros tutoriales de la serie</strong>: <a href=\"https://paigar.eu/juego-2048-tutorial/\">2048</a> · <a href=\"https://paigar.eu/juego-pong-tutorial/\">Pong</a> · <a href=\"https://paigar.eu/juego-parejas-tutorial/\">Parejas</a> · <a href=\"https://paigar.eu/juego-ladrillos-tutorial/\">Ladrillos</a>.</p>\n","date_published":"2026-03-17T00:00:00.000Z","image":"https://paigar.eu/juego-serpiente.png"},{"id":"https://paigar.eu/cuando-tu-herramienta-favorita-cambia-de-rumbo/","url":"https://paigar.eu/cuando-tu-herramienta-favorita-cambia-de-rumbo/","title":"Cuando tu herramienta favorita cambia de rumbo","language":"es","content_html":"<p>El pasado 3 de marzo, Eleventy confirmó lo que muchos intuíamos desde que en septiembre de 2024 se anunciara su incorporación a Font Awesome: el generador de sitios estáticos que tantos hemos adoptado como herramienta de cabecera iba a dejar de llamarse 11ty para convertirse en <strong>Build Awesome</strong>.</p>\n<p>Y por si el nombre no fuera suficientemente desafortunado por sí solo, la campaña de Kickstarter asociada al lanzamiento —que alcanzó su objetivo de financiación en un solo día— acaba de ser cancelada, alegando problemas de entrega de correo electrónico. No es el tipo de estabilidad que inspira confianza.</p>\n<h2>El patrón conocido</h2>\n<p>Zach Leatherman, creador y mantenedor de Eleventy, ha insistido en que el proyecto sigue siendo código abierto, que la compatibilidad con el ecosistema actual está garantizada y que <strong>Build Awesome Pro</strong> no será un requisito para usar la herramienta. Le creo. Pero el patrón es conocido: Font Awesome siguió exactamente el mismo camino con Web Awesome (antes Shoelace), convirtiendo un proyecto comunitario en un producto freemium con capa de pago.</p>\n<p>El argumento de que <em>&quot;la versión gratuita nunca desaparecerá&quot;</em> puede ser cierto, pero cambia inevitablemente la naturaleza del proyecto y sus prioridades. Cuando hay una versión de pago, los recursos de desarrollo se dirigen hacia las funcionalidades que justifican el precio. La versión gratuita se mantiene, pero deja de ser el foco.</p>\n<p>Para quienes llevamos años en el nicho de los generadores estáticos precisamente porque nos alejaban de esa dinámica —porque 11ty era un proyecto de una sola persona, sin inversores, sin agenda comercial, con una comunidad que construía por placer— la transformación supone un punto de inflexión. No hace falta abandonar el barco hoy mismo, pero sí tiene sentido empezar a mirar el horizonte.</p>\n<h2>Lo que busco en un generador estático</h2>\n<p>Antes de mirar alternativas, conviene definir el punto de partida. No todo el mundo necesita lo mismo.</p>\n<p><strong>Hugo</strong> es extraordinariamente rápido, pero su sistema de plantillas en Go tiene una curva de aprendizaje pronunciada y resulta árido para quienes venimos de Nunjucks o Liquid. <strong>Astro</strong> es potente y moderno, pero arrastra una complejidad y una orientación hacia el componente JavaScript que lo aleja bastante de la filosofía que muchos valoramos en 11ty: cero JavaScript en el cliente por defecto, sin magia, sin estructura impuesta.</p>\n<p>Lo que busco es algo que comparta esa misma filosofía: un generador que salga del camino. Que no inyecte nada en mi HTML sin pedírselo. Que soporte múltiples lenguajes de plantillas. Que sea mantenido por una comunidad real —o al menos por una persona comprometida sin agenda corporativa—. Y que tenga builds rápidos.</p>\n<h2>Lume: el sucesor espiritual</h2>\n<p>Buscando alternativas me he encontrado con <a href=\"https://lume.land/\">Lume</a>, y la impresión ha sido inmediata. Creado y mantenido por Óscar Otero, un desarrollador gallego, Lume es un generador de sitios estáticos construido sobre Deno que comparte con 11ty una cantidad notable de principios: soporte para múltiples lenguajes de plantillas, cero JavaScript en el cliente por defecto, estructura de proyecto libre, y una configuración en TypeScript que resulta familiar a cualquier usuario de Eleventy. Que sea un proyecto nacional, de un desarrollador independiente con una filosofía clara, también pesa en la balanza.</p>\n<p>La gran diferencia respecto a un proyecto Node.js es que Deno elimina completamente la carpeta <code>node_modules</code>. Las dependencias se descargan y cachean automáticamente, lo que convierte el setup inicial en una experiencia notablemente más limpia. Quienes hayan sufrido alguna vez la gestión de dependencias en un proyecto Node grande entenderán lo que esto significa en términos de mantenimiento a largo plazo. Y Deno es compatible con los paquetes npm, así que la transición no implica renunciar al ecosistema existente.</p>\n<p>En cuanto a migración, el salto desde Eleventy parece más sencillo que a cualquier otro generador. Los conceptos de layouts, includes, data files y colecciones funcionan de manera análoga. Incluso Nunjucks funciona de fábrica como lenguaje de plantillas, lo que puede facilitar mucho la migración de proyectos existentes. El proyecto lleva activo desde 2020, tiene una versión 3 estable y madura, y su repositorio en GitHub muestra commits regulares.</p>\n<p>La comunidad de Lume es todavía pequeña comparada con la de Eleventy o Hugo. La documentación es buena pero no tan exhaustiva, y las búsquedas de soluciones a problemas específicos podrían ser menos fructíferas. Pero eso, lejos de ser un freno, es lo que hace interesante el momento: un proyecto joven donde todavía puedes formar parte de su crecimiento, contribuir con documentación, reportar problemas y sentir que tu aportación importa. Es exactamente el tipo de comunidad que muchos echamos de menos en proyectos que ya han crecido demasiado.</p>\n<h2>La decisión</h2>\n<p>No es que Eleventy haya dejado de funcionar. Es que la promesa original —un proyecto pequeño, independiente, sin compromisos comerciales— ya no está sobre la mesa. Y si una de las razones por las que uso generadores estáticos es mantener la independencia de mi stack, tiene sentido que la herramienta que lo sostiene sea también independiente.</p>\n<p>Hay aquí una ironía pequeña que conviene confesar: <strong>paigar.eu</strong> —el sitio donde estás leyendo este texto— ya corre sobre Lume desde su primer día. Lo monté hace meses como experimento personal: un proyecto pequeño, controlado, donde podía permitirme romper cosas y aprender por el camino sin coste para nadie. Lo que iba a ser una prueba terminó siendo lo definitivo, y este post se escribe ya sobre la herramienta cuya elección defiende.</p>\n<p>La conversación de fondo —&quot;si funciona, migro; si no, Eleventy sigue ahí&quot;— la dejo para Idenautas, que es el sitio donde se juega la cara con clientes y donde la migración tiene consecuencias prácticas. Allí la decisión sigue abierta, todavía sopesando esfuerzos. Pero la noticia del rebrand de Eleventy no hace más que confirmar que la apuesta valía la pena, y que tarde o temprano todo lo que hoy mantengo en 11ty va a terminar sobre Deno.</p>\n<p>A veces las herramientas que más quieres son las que te empujan a mirar más allá de ellas.</p>\n","date_published":"2026-03-12T00:00:00.000Z","image":"https://paigar.eu/hta-cambio-rumbo.png"},{"id":"https://paigar.eu/validar-dni-nie-pasaporte/","url":"https://paigar.eu/validar-dni-nie-pasaporte/","title":"Cómo validar un DNI, NIE o Pasaporte en un formulario web","language":"es","content_html":"<p>Hay pocos momentos más frustrantes en el uso de un formulario web que introducir tu número de documento con toda la tranquilidad del mundo y recibir a cambio un aviso de error que dice, simplemente, «documento inválido». Sin más. Sin pistas. Sin misericordia. Como si el formulario supiera algo de ti que tú no sabes.</p>\n<p>El problema, muchas veces, no está en quien rellena el campo. Está en quien lo programó. Porque validar un DNI, un NIE o un pasaporte no es algo especialmente complicado, pero tampoco es tan trivial como aplicar una expresión regular y dar el asunto por zanjado.</p>\n<h2>La anatomía del DNI español</h2>\n<p>El Documento Nacional de Identidad español tiene una estructura muy concreta: ocho dígitos numéricos seguidos de una letra. Esa letra no es decorativa ni aleatoria —es una letra de control calculada a partir de los ocho dígitos, y ahí está la clave de la validación.</p>\n<p>El algoritmo es sencillo: se toma el número formado por los ocho dígitos, se divide entre 23, y el resto de esa división se usa como índice para localizar la letra correspondiente en una cadena de 23 caracteres: <code>TRWAGMYFPDXBNJZSQVHLCKE</code>. Si la letra que aparece en el documento coincide con la que devuelve el algoritmo, el DNI es válido. Si no coincide, algo va mal —ya sea un error tipográfico o un intento de colar un número inventado.</p>\n<pre><code class=\"language-javascript\">const LETRAS = &quot;TRWAGMYFPDXBNJZSQVHLCKE&quot;;\n\nfunction validarDNI(dni) {\n\tconst match = dni.toUpperCase().match(/^(\\d{8})([A-Z])$/);\n\tif (!match) return false;\n\tconst numero = parseInt(match[1], 10);\n\tconst letra = match[2];\n\treturn LETRAS[numero % 23] === letra;\n}\n</code></pre>\n<p>Vale la pena destacar que las letras <code>I</code>, <code>O</code>, <code>U</code> y <code>Ñ</code> no aparecen en esa cadena, precisamente para evitar confusiones tipográficas con el <code>1</code>, el <code>0</code>, o simplemente por convención histórica. Nuestros antepasados burócratas tenían su lógica.</p>\n<h2>El NIE, primo hermano con letra por delante</h2>\n<p>El Número de Identificación de Extranjero sigue la misma lógica de validación que el DNI, pero con una diferencia estructural: empieza por una de estas tres letras —<code>X</code>, <code>Y</code> o <code>Z</code>— seguida de siete dígitos y la letra de control al final.</p>\n<p>Para aplicar el mismo algoritmo, hay que sustituir esa letra inicial por su equivalente numérico: <code>X</code> se convierte en <code>0</code>, <code>Y</code> en <code>1</code> y <code>Z</code> en <code>2</code>. Con ese número reconstruido, el cálculo es idéntico al del DNI.</p>\n<pre><code class=\"language-javascript\">function validarNIE(nie) {\n\tconst match = nie.toUpperCase().match(/^([XYZ])(\\d{7})([A-Z])$/);\n\tif (!match) return false;\n\tconst prefijo = { X: &quot;0&quot;, Y: &quot;1&quot;, Z: &quot;2&quot; };\n\tconst numero = parseInt(prefijo[match[1]] + match[2], 10);\n\tconst letra = match[3];\n\treturn LETRAS[numero % 23] === letra;\n}\n</code></pre>\n<p>Un detalle que merece atención: el NIE con prefijo <code>Z</code> es relativamente reciente. Si tu base de usuarios es antigua y no lo contemplas, podrías estar dejando fuera a un buen número de personas. La burocracia evoluciona, los formularios también deberían.</p>\n<h2>El pasaporte, el pariente sin algoritmo público</h2>\n<p>Aquí las cosas se complican levemente, porque no existe un algoritmo público de validación de dígito de control para los pasaportes españoles. Lo que sí podemos hacer es validar el formato: los pasaportes españoles actuales siguen una estructura de dos o tres letras seguidas de cinco o seis dígitos numéricos.</p>\n<p>No es una validación matemáticamente infalible —alguien con imaginación podría inventar un <code>AAA123456</code> perfectamente formateado pero inexistente—, pero sirve para descartar entradas claramente incorrectas y guiar al usuario hacia el formato esperado.</p>\n<pre><code class=\"language-javascript\">function validarPasaporte(pasaporte) {\n\treturn /^[A-Z]{2,3}\\d{5,6}$/i.test(pasaporte);\n}\n</code></pre>\n<p>Si tu aplicación necesita verificar que un pasaporte es real y pertenece a quien dice ser, eso ya es otro asunto —y probablemente implica APIs de terceros y consideraciones legales que se escapan del alcance de un campo de formulario.</p>\n<h2>Juntarlo todo: detección automática del tipo de documento</h2>\n<p>Lo ideal, desde el punto de vista de la experiencia de usuario, es no obligar a la persona a indicar si su documento es un DNI, un NIE o un pasaporte. El formato ya lo dice. Podemos detectarlo automáticamente con una función que evalúa la estructura del valor introducido y llama a la función de validación correspondiente.</p>\n<pre><code class=\"language-javascript\">const LETRAS = &quot;TRWAGMYFPDXBNJZSQVHLCKE&quot;;\n\nfunction validarDNI(doc) {\n\tconst m = doc.match(/^(\\d{8})([A-Z])$/);\n\tif (!m) return { valido: false };\n\tconst ok = LETRAS[parseInt(m[1], 10) % 23] === m[2];\n\treturn { tipo: &quot;DNI&quot;, valido: ok };\n}\n\nfunction validarNIE(doc) {\n\tconst m = doc.match(/^([XYZ])(\\d{7})([A-Z])$/);\n\tif (!m) return { valido: false };\n\tconst pref = { X: &quot;0&quot;, Y: &quot;1&quot;, Z: &quot;2&quot; };\n\tconst ok = LETRAS[parseInt(pref[m[1]] + m[2], 10) % 23] === m[3];\n\treturn { tipo: &quot;NIE&quot;, valido: ok };\n}\n\nfunction validarPasaporte(doc) {\n\tconst ok = /^[A-Z]{2,3}\\d{5,6}$/.test(doc);\n\treturn { tipo: &quot;Pasaporte&quot;, valido: ok };\n}\n\nfunction validarDocumento(raw) {\n\tconst doc = raw\n\t\t.trim()\n\t\t.toUpperCase()\n\t\t.replace(/[\\s\\-\\.]/g, &quot;&quot;);\n\n\tif (/^\\d{8}[A-Z]$/.test(doc)) return validarDNI(doc);\n\tif (/^[XYZ]\\d{7}[A-Z]$/.test(doc)) return validarNIE(doc);\n\tif (/^[A-Z]{2,3}\\d{5,6}$/.test(doc)) return validarPasaporte(doc);\n\n\treturn { tipo: &quot;desconocido&quot;, valido: false };\n}\n</code></pre>\n<p>Fíjate en el <code>replace(/[\\s\\-\\.]/g, '')</code> antes de evaluar: es un pequeño gesto de buena fe hacia el usuario que escribe <code>12.345.678-Z</code> o <code>12 345 678 Z</code>. Limpiar la entrada antes de validarla evita muchos falsos negativos innecesarios.</p>\n<h2>Integración en un formulario real</h2>\n<p>Con la función lista, integrarla en un campo de formulario es cuestión de escuchar el evento <code>input</code> y actuar en consecuencia. No hay que esperar a que el usuario pulse «Enviar» para decirle que algo va mal —la validación en tiempo real, con un poco de gracia y sin ponerse histérico con el primer carácter, mejora notablemente la experiencia.</p>\n<pre><code class=\"language-javascript\">const campo = document.getElementById(&quot;documento&quot;);\nconst aviso = document.getElementById(&quot;aviso-documento&quot;);\n\ncampo.addEventListener(&quot;input&quot;, () =&gt; {\n\tconst resultado = validarDocumento(campo.value);\n\n\tif (!campo.value.trim()) {\n\t\taviso.textContent = &quot;&quot;;\n\t\tcampo.removeAttribute(&quot;aria-invalid&quot;);\n\t\treturn;\n\t}\n\n\tif (resultado.valido) {\n\t\taviso.textContent = `${resultado.tipo} válido ✓`;\n\t\taviso.className = &quot;aviso ok&quot;;\n\t\tcampo.setAttribute(&quot;aria-invalid&quot;, &quot;false&quot;);\n\t} else {\n\t\taviso.textContent =\n\t\t\tresultado.tipo === &quot;desconocido&quot;\n\t\t\t\t? &quot;Formato no reconocido&quot;\n\t\t\t\t: `La letra de control no es correcta`;\n\t\taviso.className = &quot;aviso error&quot;;\n\t\tcampo.setAttribute(&quot;aria-invalid&quot;, &quot;true&quot;);\n\t}\n});\n</code></pre>\n<p>Un apunte de accesibilidad que conviene no ignorar: usar <code>aria-invalid</code> sobre el campo permite que los lectores de pantalla comuniquen el estado del campo a quienes los necesitan. No cuesta nada y marca la diferencia para una parte nada despreciable de los usuarios.</p>\n<h2>Una última consideración antes de confiar ciegamente en esto</h2>\n<p>La validación del lado del cliente es útil para mejorar la experiencia de usuario, pero nunca debe ser la única línea de defensa. Cualquiera puede desactivar JavaScript o manipular las peticiones antes de que lleguen al servidor. Si el número de documento tiene alguna relevancia funcional en tu aplicación —y probablemente la tiene si lo estás pidiendo—, repite siempre la validación en el servidor.</p>\n<p>Lo que hemos visto aquí es suficiente para que tus formularios sean más amables, más inteligentes y menos fuente de fricciones innecesarias. Que no es poco.</p>\n<p>A continuación puedes encontrar un validador funcional con todo lo que hemos descrito: detección automática del tipo de documento, validación en tiempo real y ejemplos para probar.</p>\n","date_published":"2026-03-10T00:00:00.000Z","image":"https://paigar.eu/validar-dni-nie-pasaporte.png"},{"id":"https://paigar.eu/imagenes-og-lume/","url":"https://paigar.eu/imagenes-og-lume/","title":"Imágenes Open Graph automáticas con Lume","language":"es","content_html":"<p>Cuando compartes un enlace en redes sociales, lo primero que ves es una imagen. Si no la tienes, tu enlace aparece como un rectángulo gris con texto plano. No es el fin del mundo, pero es una oportunidad perdida.</p>\n<p>Crear esas imágenes a mano para cada post es tedioso. Y conectar un servicio externo para algo tan simple es sobredimensionar el problema. La solución está en el propio build: generar las imágenes durante la compilación, sin servicios externos.</p>\n<p>La idea original la encontré en el artículo de Bernard Nijenhuis para Eleventy, y la he adaptado a Lume con las herramientas que Deno ofrece.</p>\n<h2>La estrategia</h2>\n<p>El truco es usar SVG como plantilla intermedia. SVG es código, así que puedes generarlo programáticamente. Después, una librería WASM convierte ese SVG a PNG durante el build.</p>\n<p>El flujo completo:</p>\n<ol>\n<li>Un generador TypeScript (<code>og-images.page.ts</code>) produce un archivo SVG por cada post</li>\n<li>El SVG contiene el título del post, la sección y el branding del sitio</li>\n<li>Después del build, un evento <code>afterBuild</code> en <code>_config.ts</code> convierte todos los SVG a PNG con resvg</li>\n<li>Las meta tags <code>og:image</code> apuntan a las imágenes PNG generadas</li>\n</ol>\n<p>Todo ocurre en el build. No hay servicios externos, no hay APIs, no hay imágenes que mantener a mano.</p>\n<h2>El generador: og-images.page.ts</h2>\n<p>En Lume, los archivos <code>.page.ts</code> son generadores: exportan una función que puede producir múltiples páginas. Cada <code>yield</code> genera un archivo. Es el equivalente a la paginación de otros SSG, pero con TypeScript puro.</p>\n<p>El generador empieza recopilando todos los posts de ambas secciones con <code>search.pages()</code>:</p>\n<pre><code class=\"language-typescript\">export default function* ({ search }: Lume.Data) {\n\tconst posts = [...search.pages(&quot;bitacora&quot;), ...search.pages(&quot;reflexiones&quot;)];\n\n\tfor (const post of posts) {\n\t\tconst title = post.title as string;\n\t\tconst tags = (post.tags || []) as string[];\n\t\t// ...\n\t}\n}\n</code></pre>\n<p>Para cada post hay que resolver tres cosas: partir el título en líneas, determinar la sección, y extraer el slug para el nombre de archivo.</p>\n<h3>Partir el título en líneas</h3>\n<p>SVG no sabe partir texto automáticamente. Si el título tiene 80 caracteres, se sale del canvas. La solución es dividir el texto en líneas de máximo 36 caracteres, cortando siempre por espacios:</p>\n<pre><code class=\"language-typescript\">const parts = title.split(&quot; &quot;);\nconst titleLines: string[] = parts.reduce((prev: string[], current: string) =&gt; {\n\tif (!prev.length) return [current];\n\tconst lastLine = prev[prev.length - 1];\n\tif (lastLine.length + 1 + current.length &gt; 36) {\n\t\treturn [...prev, current];\n\t}\n\tprev[prev.length - 1] = lastLine + &quot; &quot; + current;\n\treturn prev;\n}, []);\n</code></pre>\n<p>El 36 depende del tamaño de fuente y del ancho del canvas. Con <code>font-size=&quot;48&quot;</code> y un canvas de 1200 px, 36 caracteres encajan bien.</p>\n<h3>Posición vertical del título</h3>\n<p>La posición Y del título se ajusta según el número de líneas, para que quede centrado visualmente en la imagen:</p>\n<pre><code class=\"language-typescript\">const lineCount = titleLines.length;\nlet titleY: number;\nif (lineCount === 1) titleY = 310;\nelse if (lineCount === 2) titleY = 280;\nelse if (lineCount === 3) titleY = 240;\nelse titleY = 200;\n</code></pre>\n<h3>Sección y slug</h3>\n<p>La sección se determina a partir de los tags del post. El slug se extrae de la URL — es el último segmento:</p>\n<pre><code class=\"language-typescript\">const seccion = tags.includes(&quot;bitacora&quot;) ? &quot;BITACORA&quot; : &quot;REFLEXIONES&quot;;\n\nconst urlParts = (post.url as string).split(&quot;/&quot;).filter(Boolean);\nconst slug = urlParts[urlParts.length - 1];\n</code></pre>\n<h3>El SVG</h3>\n<p>Con todos los datos preparados, se construye el SVG como un template literal. Las líneas del título se generan como <code>&lt;tspan&gt;</code> con la coordenada Y incrementada en 62 px por línea. El texto se escapa con una función auxiliar <code>escapeXml</code> para evitar que caracteres como <code>&amp;</code> o <code>&lt;</code> rompan el XML:</p>\n<pre><code class=\"language-typescript\">const tspans = titleLines\n\t.map(\n\t\t(line: string, i: number) =&gt;\n\t\t\t`    &lt;tspan x=&quot;80&quot; y=&quot;${titleY + i * 62}&quot;&gt;${escapeXml(line)}&lt;/tspan&gt;`,\n\t)\n\t.join(&quot;\\n&quot;);\n</code></pre>\n<p>El diseño es intencionalmente sencillo: fondo oscuro (<code>#111118</code>), una barra naranja lateral (<code>#f86624</code>) como marca visual, el nombre de la sección en naranja, el título en claro, y el branding del sitio abajo. Todo con <code>&lt;rect&gt;</code>, <code>&lt;text&gt;</code>, <code>&lt;line&gt;</code> y <code>&lt;circle&gt;</code>.</p>\n<p>Finalmente, el generador produce el archivo:</p>\n<pre><code class=\"language-typescript\">yield {\n  url: `/og-images/${slug}.svg`,\n  content: svg,\n};\n</code></pre>\n<h2>Por qué PNG y no JPEG o WebP</h2>\n<p>La elección del formato no es casual. Estas imágenes son texto sobre fondos planos, sin fotografías ni degradados complejos. PNG comprime ese tipo de contenido muy bien y mantiene los bordes del texto nítidos. JPEG introduciría artefactos de compresión visibles en las letras y líneas rectas — necesitarías calidad alta para disimularlos, y el archivo acabaría pesando lo mismo o más.</p>\n<p>WebP sería ideal por tamaño, pero los crawlers de redes sociales (Facebook, LinkedIn, WhatsApp) históricamente han tenido problemas con WebP en <code>og:image</code>. Facebook recomienda oficialmente PNG o JPEG.</p>\n<p>En la práctica, las imágenes generadas pesan entre 22 y 38 KB. No merece la pena buscar más optimización.</p>\n<h2>La conversión: SVG a PNG con resvg-wasm</h2>\n<p>Los SVG no sirven directamente como imágenes Open Graph — los crawlers de redes sociales esperan formatos rasterizados. Aquí es donde la migración a Lume trajo un reto interesante.</p>\n<p>En Eleventy, la conversión era trivial: <code>@11ty/eleventy-img</code> usa Sharp, que es una librería nativa de Node.js con bindings precompilados. En Deno, Sharp no funciona directamente. Y la mayoría de paquetes npm de conversión SVG→PNG están o deprecados, o usan binarios nativos incompatibles con Deno, o tienen APIs inestables.</p>\n<p>La solución fue resvg-wasm, una versión compilada a WebAssembly del renderizador SVG de Mozilla. Funciona en cualquier plataforma sin binarios nativos.</p>\n<p>La conversión se ejecuta en un evento <code>afterBuild</code> de Lume, cuando los SVG ya están generados en <code>_site/og-images/</code>:</p>\n<pre><code class=\"language-typescript\">import { render as renderSvgToPng } from &quot;https://deno.land/x/resvg_wasm@0.2.0/mod.ts&quot;;\n\nsite.addEventListener(&quot;afterBuild&quot;, async () =&gt; {\n\tconst ogDir = site.dest() + &quot;/og-images&quot;;\n\n\ttry {\n\t\tconst entries = [...Deno.readDirSync(ogDir)];\n\t\tconst svgFiles = entries.filter((e) =&gt; e.name.endsWith(&quot;.svg&quot;));\n\n\t\tif (svgFiles.length === 0) return;\n\n\t\tlet converted = 0;\n\t\tfor (const entry of svgFiles) {\n\t\t\tconst svgPath = `${ogDir}/${entry.name}`;\n\t\t\tconst pngPath = svgPath.replace(&quot;.svg&quot;, &quot;.png&quot;);\n\t\t\tconst svgContent = await Deno.readTextFile(svgPath);\n\n\t\t\tconst pngBuffer = await renderSvgToPng(svgContent);\n\t\t\tawait Deno.writeFile(pngPath, pngBuffer);\n\t\t\tawait Deno.remove(svgPath);\n\t\t\tconverted++;\n\t\t}\n\n\t\tconsole.log(`[og-images] ${converted} SVG convertidos a PNG`);\n\t} catch (err) {\n\t\tif (!(err instanceof Deno.errors.NotFound)) {\n\t\t\tconsole.error(&quot;[og-images] Error:&quot;, err);\n\t\t}\n\t}\n});\n</code></pre>\n<p>La API es mínima — una sola función <code>render()</code> que recibe SVG como string y devuelve PNG como <code>Uint8Array</code>. Por cada SVG, genera el PNG y elimina el original.</p>\n<h2>Las meta tags</h2>\n<p>Solo queda apuntar las meta tags a las imágenes generadas. En el layout base:</p>\n<pre><code class=\"language-html\">{{ if tags &amp;&amp; (tags.includes(&quot;bitacora&quot;) || tags.includes(&quot;reflexiones&quot;)) }}\n&lt;meta\n\tproperty=&quot;og:image&quot;\n\tcontent=&quot;{{ metadata.url }}/og-images/{{ page.src.slug }}.png&quot; /&gt;\n{{ else }}\n&lt;meta property=&quot;og:image&quot; content=&quot;{{ metadata.url }}/og-images/default.png&quot; /&gt;\n{{ /if }}\n&lt;meta property=&quot;og:image:width&quot; content=&quot;1200&quot; /&gt;\n&lt;meta property=&quot;og:image:height&quot; content=&quot;630&quot; /&gt;\n&lt;meta name=&quot;twitter:card&quot; content=&quot;summary_large_image&quot; /&gt;\n</code></pre>\n<p>Los posts obtienen su imagen específica. El resto de páginas usan una imagen genérica con el nombre y la descripción del sitio. El valor <code>summary_large_image</code> en <code>twitter:card</code> hace que la imagen se muestre en grande al compartir en X.</p>\n<h2>Sobre las fuentes</h2>\n<p>Un detalle importante: el renderizador SVG usa las fuentes del sistema donde se ejecuta el build. Si usas una tipografía personalizada que no está instalada en la máquina, el resultado será diferente. En mi caso uso Arial como fuente para las imágenes OG, que está disponible en prácticamente cualquier sistema.</p>\n<h2>El archivo completo</h2>\n<p>Para referencia, este es el <code>og-images.page.ts</code> completo tal como funciona en producción:</p>\n<pre><code class=\"language-typescript\">export default function* ({ search }: Lume.Data) {\n\tconst posts = [...search.pages(&quot;bitacora&quot;), ...search.pages(&quot;reflexiones&quot;)];\n\n\tfor (const post of posts) {\n\t\tconst title = post.title as string;\n\t\tconst tags = (post.tags || []) as string[];\n\n\t\tconst parts = title.split(&quot; &quot;);\n\t\tconst titleLines: string[] = parts.reduce(\n\t\t\t(prev: string[], current: string) =&gt; {\n\t\t\t\tif (!prev.length) return [current];\n\t\t\t\tconst lastLine = prev[prev.length - 1];\n\t\t\t\tif (lastLine.length + 1 + current.length &gt; 36) {\n\t\t\t\t\treturn [...prev, current];\n\t\t\t\t}\n\t\t\t\tprev[prev.length - 1] = lastLine + &quot; &quot; + current;\n\t\t\t\treturn prev;\n\t\t\t},\n\t\t\t[],\n\t\t);\n\n\t\tconst lineCount = titleLines.length;\n\t\tlet titleY: number;\n\t\tif (lineCount === 1) titleY = 310;\n\t\telse if (lineCount === 2) titleY = 280;\n\t\telse if (lineCount === 3) titleY = 240;\n\t\telse titleY = 200;\n\n\t\tconst seccion = tags.includes(&quot;bitacora&quot;) ? &quot;BITACORA&quot; : &quot;REFLEXIONES&quot;;\n\n\t\tconst urlParts = (post.url as string).split(&quot;/&quot;).filter(Boolean);\n\t\tconst slug = urlParts[urlParts.length - 1];\n\n\t\tconst tspans = titleLines\n\t\t\t.map(\n\t\t\t\t(line: string, i: number) =&gt;\n\t\t\t\t\t`    &lt;tspan x=&quot;80&quot; y=&quot;${titleY + i * 62}&quot;&gt;${escapeXml(line)}&lt;/tspan&gt;`,\n\t\t\t)\n\t\t\t.join(&quot;\\n&quot;);\n\n\t\tconst svg = `&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;\n&lt;svg width=&quot;1200&quot; height=&quot;630&quot; viewBox=&quot;0 0 1200 630&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot;&gt;\n\n  &lt;!-- Fondo --&gt;\n  &lt;rect width=&quot;1200&quot; height=&quot;630&quot; fill=&quot;#111118&quot;/&gt;\n\n  &lt;!-- Barra naranja lateral --&gt;\n  &lt;rect x=&quot;0&quot; y=&quot;0&quot; width=&quot;8&quot; height=&quot;630&quot; fill=&quot;#f86624&quot;/&gt;\n\n  &lt;!-- Seccion --&gt;\n  &lt;text x=&quot;80&quot; y=&quot;${titleY - 60}&quot; font-family=&quot;Arial, Helvetica, sans-serif&quot; font-size=&quot;22&quot; fill=&quot;#f86624&quot; letter-spacing=&quot;3&quot;&gt;${seccion}&lt;/text&gt;\n\n  &lt;!-- Titulo --&gt;\n  &lt;text font-family=&quot;Arial, Helvetica, sans-serif&quot; font-size=&quot;48&quot; font-weight=&quot;bold&quot; fill=&quot;#dcdcd4&quot;&gt;\n${tspans}\n  &lt;/text&gt;\n\n  &lt;!-- Linea separadora --&gt;\n  &lt;line x1=&quot;80&quot; y1=&quot;530&quot; x2=&quot;1120&quot; y2=&quot;530&quot; stroke=&quot;#2a2a3a&quot; stroke-width=&quot;1&quot;/&gt;\n\n  &lt;!-- Branding --&gt;\n  &lt;text x=&quot;80&quot; y=&quot;575&quot; font-family=&quot;Arial, Helvetica, sans-serif&quot; font-size=&quot;24&quot; fill=&quot;#8e8e86&quot;&gt;paigar.es&lt;/text&gt;\n\n  &lt;!-- Punto naranja --&gt;\n  &lt;circle cx=&quot;1120&quot; cy=&quot;568&quot; r=&quot;6&quot; fill=&quot;#f86624&quot;/&gt;\n\n&lt;/svg&gt;`;\n\n\t\tyield {\n\t\t\turl: `/og-images/${slug}.svg`,\n\t\t\tcontent: svg,\n\t\t};\n\t}\n}\n\nfunction escapeXml(str: string): string {\n\treturn str\n\t\t.replace(/&amp;/g, &quot;&amp;amp;&quot;)\n\t\t.replace(/&lt;/g, &quot;&amp;lt;&quot;)\n\t\t.replace(/&gt;/g, &quot;&amp;gt;&quot;)\n\t\t.replace(/&quot;/g, &quot;&amp;quot;&quot;)\n\t\t.replace(/'/g, &quot;&amp;apos;&quot;);\n}\n</code></pre>\n<h2>La alternativa oficial: el plugin og_images</h2>\n<p>Lume tiene un plugin oficial de imágenes Open Graph que resuelve el mismo problema. Usa Satori (de Vercel) para convertir componentes JSX en SVG, y Sharp para rasterizar a PNG. Los layouts se definen como funciones JSX con estilos inline, y se asignan desde el frontmatter con <code>openGraphLayout</code>.</p>\n<p>Es una opción válida si prefieres un enfoque más integrado con el ecosistema de Lume. En mi caso elegí la implementación manual por varias razones:</p>\n<ul>\n<li><strong>Control total del SVG</strong> — puedo usar cualquier elemento SVG (<code>&lt;line&gt;</code>, <code>&lt;circle&gt;</code>, <code>&lt;tspan&gt;</code>) sin las limitaciones de Satori, que solo soporta un subconjunto de CSS basado en flexbox.</li>\n<li><strong>Sin Sharp</strong> — Sharp es una librería nativa de Node.js que no funciona directamente en Deno. Con resvg-wasm no hay binarios nativos ni dependencias de plataforma.</li>\n<li><strong>Menos dependencias</strong> — el generador es un único archivo TypeScript de 89 líneas, sin configuración JSX ni paquetes adicionales.</li>\n</ul>\n<p>El plugin oficial es más cómodo si no necesitas un diseño muy específico o si ya usas JSX en tu proyecto. Pero para un sitio que busca minimizar dependencias, la solución manual encaja mejor.</p>\n<h2>El resultado</h2>\n<p>Con esta solución, cada vez que hago build se generan automáticamente las imágenes de vista previa para todos los posts. Sin intervención manual, sin servicios externos, sin imágenes que versionar en el repositorio. Solo código que genera código que genera imágenes.</p>\n<p>La técnica original es para Eleventy con Sharp. Mi adaptación a Lume usa generadores <code>.page.ts</code> para la creación de SVGs y resvg-wasm para la conversión a PNG, eliminando la dependencia de Node.js.</p>\n","date_published":"2026-03-03T00:00:00.000Z","image":"https://paigar.eu/imagenes-og-lume.png"},{"id":"https://paigar.eu/pago-con-tarjeta-comisiones/","url":"https://paigar.eu/pago-con-tarjeta-comisiones/","title":"La pequeña estafa cotidiana del pago con tarjeta","language":"es","content_html":"<p>Hay cosas que hemos normalizado tan deprisa que ya nos parecen el orden natural del mundo. Pagar con tarjeta es una de ellas. Llevo años sin sacar dinero del cajero más que de uvas a peras, he viajado a países fuera de la zona euro sin llegar a ver de cerca su moneda local porque todos los pagos los hice con tarjeta o con el móvil, y reconozco abiertamente que en el supermercado prefiero pasar el plástico por el datáfono mientras embolso la compra que ponerme a buscar monedas en la cartera. La comodidad gana. Suele ganar. No voy a fingir que estoy por encima de eso.</p>\n<p>Pero llevo un tiempo dándole vueltas al tema, y hay un par de cosas que conviene poner en orden antes de seguir tragando con la narrativa oficial de que pagar con tarjeta es siempre, automáticamente, un avance. Porque hay un trozo de la película que casi nadie cuenta. Y cuando alguien me lo contó en su momento con un ejemplo muy sencillo, ya no he conseguido dejar de verlo.</p>\n<h2>El ejemplo del billete de cincuenta euros</h2>\n<p>Imagina un billete de cincuenta euros. Sale de tu cartera y entra en la caja de una tienda de barrio cuando pagas la compra. El tendero, al cabo de unos días, usa ese mismo billete para pagar a su distribuidor. El distribuidor lo lleva en la cartera y se lo deja al empleado de la gasolinera al llenar el depósito. El empleado de la gasolinera, esa misma noche, paga con él la cena en un restaurante. El cocinero del restaurante lo coge para pagar al pescadero al día siguiente. Y así sucesivamente, durante veinte transacciones, treinta, las que tú quieras imaginar.</p>\n<p>Al final del recorrido, el billete vale exactamente lo que valía al principio: cincuenta euros. Sigue ahí, intacto, listo para participar en la siguiente operación. Ha facilitado decenas de intercambios económicos sin perder un céntimo por el camino. Su valor es estable, completo, íntegro.</p>\n<p>Ahora imagina ese mismo recorrido pero pagando con tarjeta. Tú pagas tus cincuenta euros en la tienda de barrio y, entre el banco emisor, la red de pago y otros intermediarios, se quedan cinco céntimos. Una nimiedad. Cuando el tendero paga al distribuidor, otros cinco céntimos. Cuando el distribuidor paga la gasolina, otros cinco. Cuando el empleado paga la cena en el restaurante, otros cinco. Cuando el cocinero paga al pescadero, otros cinco. Y así sucesivamente. Cada operación parece insignificante por separado, una mordida tan pequeña que ni te molestas en mirarla.</p>\n<p>Pero echa la cuenta. Después de veinte transacciones, esas mordidas insignificantes suman un euro entero. En cien transacciones, cinco euros. Y aquí está el detalle que cambia la perspectiva: veinte o cien transacciones no son nada. Un único billete de cincuenta euros, cuando estaba en circulación de mano en mano, podía participar en cientos o miles de operaciones a lo largo de su vida útil sin perder un solo céntimo. En su versión digital, esos mismos cincuenta euros, al cabo de doscientas operaciones, se han convertido en cuarenta. Diez euros han desaparecido por el camino. No los ha gastado nadie en nada. No han comprado nada. Simplemente han ido cayendo, céntimo a céntimo, en las cuentas de quienes hacen de peaje en cada transacción.</p>\n<h2>Un peaje invisible que no existía con el efectivo</h2>\n<p>Esto es lo que más me sorprende cuando lo pienso bien. Que estamos hablando de un coste que sencillamente no existía cuando usábamos efectivo. El billete de cincuenta euros pasaba de mano en mano sin que nadie cobrara comisión por intermediar. El sistema de pagos era, literalmente, gratis para todos los participantes. La fricción era cero. Y por mucho que las comisiones individuales por operación parezcan pequeñas —céntimos, fracciones de céntimo, porcentajes ridículos— el efecto acumulado es enorme cuando lo multiplicas por miles de millones de transacciones diarias en todo el mundo.</p>\n<p>Sí, es verdad que ese dinero no se lo queda solo el banco. Se reparte entre el banco emisor, la red de pago internacional, el banco adquirente, el procesador, el fabricante del datáfono y unos cuantos intermediarios más con nombres de tres letras que casi nadie conoce. Pero a efectos prácticos, eso me da exactamente igual. Lo relevante es lo otro. Lo relevante es que los consumidores y los pequeños comerciantes han perdido colectivamente un dinero que con el efectivo no perdían, y que ese dinero ha aterrizado en los balances de un sector financiero que jamás había tenido acceso tan directo y tan automático a un porcentaje de cada compra que se hace en el mundo. Eso es un cambio histórico de proporciones inmensas, y se ha colado en nuestra vida cotidiana sin debate público alguno, simplemente porque el datáfono pita más rápido que la calderilla.</p>\n<h2>Y encima te cobran por usar tu propio dinero</h2>\n<p>Como si esto no fuera suficiente, el sistema tiene capas adicionales. Muchos bancos cobran a sus clientes una cuota anual por el simple hecho de tener una tarjeta. Pagas a tu banco para que te deje pagar. Es un giro lingüístico tan absurdo que cuando lo dices en voz alta cuesta creérselo, pero ahí está, en los extractos mensuales, año tras año. Hay tarjetas gratuitas si cumples ciertos requisitos —domiciliar la nómina, alcanzar un volumen de gasto, contratar otros productos—, pero eso no es gratis tampoco. Es solo otra forma de pago, esta vez en forma de fidelidad y datos.</p>\n<p>El comercio, por su lado, paga su propia colección de tributos al sistema. Le alquilan el datáfono al banco a un precio mensual fijo. Le cobran una comisión por cada operación, normalmente como porcentaje del importe. A veces le cobran también por las operaciones canceladas, por las devoluciones, por la conexión a la red. Cuando entras en una tienda pequeña y ves el cartel que dice <em>importe mínimo para pagar con tarjeta: diez euros</em>, no es porque al tendero le caigas mal. Es porque para una venta de dos euros, la comisión que le cobra el banco se come directamente todo el margen. No es un capricho del tendero. Es supervivencia.</p>\n<p>Y por supuesto, todos esos costes que paga el comercio no salen del aire. Salen del precio que pagamos los consumidores. Cuando compras una barra de pan, una parte minúscula de su precio está pagando la comisión que el datáfono cobrará si pagas con tarjeta, aunque tú pagues en efectivo. El sobrecoste se ha incorporado al precio final de las cosas, igual que se incorporan otros costes operativos. La diferencia es que este es un coste añadido por una infraestructura que antes no existía y de la que no podemos prescindir, porque cada vez hay más sitios donde directamente no aceptan otra cosa.</p>\n<h2>El servicio que recibimos a cambio de toda esta sangría</h2>\n<p>Aquí es donde la cosa empieza a ser ya directamente irritante. Porque uno podría aceptar todos estos costes si recibiera a cambio un servicio espectacular. Pues no. El servicio que ofrece la banca al cliente medio es lamentable en una proporción difícil de exagerar. Las oficinas bancarias se han ido cerrando una tras otra. Las que quedan abren con horarios laborales que coinciden exactamente con los horarios laborales del cliente, lo cual es magia logística pura. Las operaciones en ventanilla están racionadas como si fuesen un bien escaso: pagos de recibos en horario restringido, retirada de efectivo solo hasta una hora concreta de la mañana, gestiones presenciales atendidas con cita previa concertada con varios días de antelación.</p>\n<p>Esto no lo digo desde la queja del que pierde el tiempo en colas. Lo digo desde la constatación objetiva de que el sector financiero ha conseguido, en pocos años, cobrar más por hacer menos. Más comisiones por operación, menos atención al cliente. Más cuotas por tarjeta, menos oficinas. Más beneficios récord trimestrales, menos servicio. Y todo bajo la narrativa de la <em>digitalización</em> y la <em>modernidad</em>, como si renunciar a hablar con un humano cuando tienes un problema con tu propio dinero fuera un avance civilizatorio. No lo es. Es el resultado de un cambio en el equilibrio de fuerzas entre el sector y sus clientes que se ha producido sin que apenas nadie protestase.</p>\n<p>Y queda otro detalle que me parece importante mencionar, aunque sea brevemente: la privacidad. El efectivo es anónimo. Cuando pagas un café con un billete, nadie sabe que has pagado un café. Cuando pagas con tarjeta, hay un registro permanente de qué compraste, dónde, a qué hora, junto a qué otras compras y por qué importe. Ese registro lo guardan tu banco, la red de pago, el comercio, sus respectivos proveedores tecnológicos y, dependiendo de la jurisdicción, varias administraciones más. Es un nivel de trazabilidad de la vida cotidiana que hace cuarenta años habría parecido distopía y que hoy hemos aceptado a cambio de pagar el supermercado más rápido. Que cada cual decida si el intercambio le compensa, pero al menos conviene que lo decida sabiéndolo.</p>\n<h2>Lo que estoy haciendo, sin pretender heroísmos</h2>\n<p>No voy a venir aquí a anunciar que he desterrado la tarjeta de mi vida. No es verdad ni lo va a ser. La pereza, la prisa, la comodidad y la cantidad de sitios donde ya directamente no aceptan otra cosa pesan demasiado para fingir lo contrario. En el supermercado, después de quince minutos llenando el carro, sigo prefiriendo pasar el plástico mientras embolso que ponerme a contar monedas. En los viajes, especialmente fuera de la zona euro, el cambio de divisa en efectivo tiene sus propias trampas que muchas veces son peores que las de la tarjeta. Ser realista en estos temas también es importante, porque el moralismo del consumo ético sin matices acaba siendo siempre un postureo que no soluciona nada.</p>\n<p>Lo que sí estoy haciendo desde hace un tiempo es un esfuerzo consciente por reducir el número de operaciones con tarjeta cuando puedo hacerlo sin grandes complicaciones. Llevar algo de efectivo encima cuando salgo. Pagar en metálico en la cafetería, en la panadería, en el bar del barrio, en los pequeños comercios donde sé que cada comisión se le come el margen al dueño. Sacar una cantidad razonable de dinero del cajero al principio de la semana y administrarla. Pequeños gestos que individualmente no cambian nada, pero que al menos me permiten saber que no estoy contribuyendo automáticamente, en cada movimiento, al peaje invisible que hemos aceptado sin discutirlo.</p>\n<p>Y mientras tanto, recordar de vez en cuando el ejemplo del billete de cincuenta euros. Que circulaba cien veces y seguía valiendo cincuenta. Y que ahora, en su versión digital, circula cien veces y vale cuarenta y cinco. Cinco euros que se han evaporado por el camino sin haber comprado nada. Y multiplica eso por todos los billetes virtuales que circulan cada día en una economía moderna. La cifra que sale es astronómica. Y es una cifra que alguien se está quedando. No tú. No el tendero. No el camarero. Alguien que hace de peaje y que no estaba ahí antes. Y que cuando uno se para a mirar quién es y cuánto está cobrando por estar ahí, la respuesta no resulta especialmente tranquilizadora.</p>\n","date_published":"2026-02-23T00:00:00.000Z","image":"https://paigar.eu/pago-con-tarjeta-comisiones.png"},{"id":"https://paigar.eu/css-grid-columnas-nombradas/","url":"https://paigar.eu/css-grid-columnas-nombradas/","title":"Un sistema de layout con CSS Grid y columnas nombradas","language":"es","content_html":"<p>Uno de los patrones que más uso en mis webs es un sistema de grid con columnas nombradas. Lo llamo &quot;límites&quot; y permite que cualquier elemento hijo defina su propio ancho sin necesidad de clases wrapper adicionales. Lo uso en este sitio, en Bilbonauta, y en casi todos los proyectos de Idenautas.</p>\n<h2>El problema</h2>\n<p>En la mayoría de webs, el contenido tiene diferentes anchos. El texto principal suele estar a 65-70 caracteres para una lectura cómoda, pero a veces quieres que una imagen ocupe todo el viewport, que una cita sea más estrecha, o que una sección de tarjetas sea más ancha que el texto.</p>\n<p>La solución habitual es anidar divs con <code>max-width</code> y <code>margin: 0 auto</code>:</p>\n<pre><code class=\"language-html\">&lt;!-- El markup típico --&gt;\n&lt;div class=&quot;wrapper&quot;&gt;\n  &lt;div class=&quot;narrow-wrapper&quot;&gt;\n    &lt;p&gt;Texto estrecho...&lt;/p&gt;\n  &lt;/div&gt;\n&lt;/div&gt;\n&lt;div class=&quot;full-width&quot;&gt;\n  &lt;img src=&quot;hero.jpg&quot; alt=&quot;&quot;&gt;\n&lt;/div&gt;\n&lt;div class=&quot;wrapper&quot;&gt;\n  &lt;p&gt;Texto normal...&lt;/p&gt;\n&lt;/div&gt;\n</code></pre>\n<p>Esto funciona, pero ensucia el HTML con contenedores que no aportan semántica. Y empeora cuando tienes cinco o seis anchos diferentes.</p>\n<h2>La solución: columnas nombradas</h2>\n<p>La idea es definir un único grid con columnas nombradas que representan los diferentes anchos:</p>\n<pre><code class=\"language-css\">.limites {\n  display: grid;\n  grid-template-columns:\n    [total-start] minmax(1rem, 1fr)\n    [ancho-start] minmax(0, calc((var(--anchura-ancho) - var(--anchura-normal)) / 2))\n    [normal-start] minmax(0, calc((var(--anchura-normal) - var(--anchura-estrecho)) / 2))\n    [estrecho-start] min(var(--anchura-estrecho), 100% - 2rem)\n    [estrecho-end] minmax(0, calc((var(--anchura-normal) - var(--anchura-estrecho)) / 2))\n    [normal-end] minmax(0, calc((var(--anchura-ancho) - var(--anchura-normal)) / 2))\n    [ancho-end] minmax(1rem, 1fr)\n    [total-end];\n}\n</code></pre>\n<p>Parece intimidante al principio, pero la lógica es sencilla: estás definiendo columnas simétricas que actúan como márgenes entre cada nivel de ancho. La columna central (<code>estrecho</code>) tiene el contenido más estrecho, y las columnas a los lados se expanden progresivamente hasta <code>total</code>, que ocupa todo el viewport.</p>\n<h2>Cómo se usa</h2>\n<p>Cada hijo del grid elige su ancho con una simple clase:</p>\n<pre><code class=\"language-css\">.limites &gt; * { grid-column: normal; }\n.limites &gt; .ancho { grid-column: ancho; }\n.limites &gt; .estrecho { grid-column: estrecho; }\n.limites &gt; .total { grid-column: total; }\n</code></pre>\n<p>Y en el HTML:</p>\n<pre><code class=\"language-html\">&lt;main class=&quot;limites&quot;&gt;\n  &lt;p&gt;Este párrafo ocupa el ancho normal (65ch).&lt;/p&gt;\n  &lt;section class=&quot;ancho&quot;&gt;Esto es más ancho (55rem).&lt;/section&gt;\n  &lt;blockquote class=&quot;estrecho&quot;&gt;Esto es más estrecho.&lt;/blockquote&gt;\n  &lt;img class=&quot;total&quot; src=&quot;panoramica.jpg&quot; alt=&quot;&quot;&gt;\n&lt;/main&gt;\n</code></pre>\n<p>Sin wrappers, sin media queries para los anchos, sin <code>max-width</code> repetidos. El grid se encarga de todo.</p>\n<h2>Por qué funciona</h2>\n<p>El truco está en las líneas nombradas de CSS Grid. Cuando defines <code>[nombre-start]</code> y <code>[nombre-end]</code>, puedes usar <code>grid-column: nombre</code> como shorthand. El navegador entiende que <code>nombre</code> se refiere al rango entre <code>nombre-start</code> y <code>nombre-end</code>.</p>\n<p>Los <code>minmax()</code> hacen que sea responsive por naturaleza. Cuando el viewport se estrecha, las columnas exteriores colapsan a su mínimo (1rem de padding lateral) y las columnas intermedias llegan a 0. El resultado: en móvil, <code>normal</code>, <code>ancho</code> y <code>estrecho</code> convergen al mismo ancho, que es el viewport menos 2rem de margen.</p>\n<h2>Las custom properties</h2>\n<p>Los anchos están definidos como variables CSS en los tokens del sitio:</p>\n<pre><code class=\"language-css\">:root {\n  --anchura-ancho: 55rem;\n  --anchura-normal: 65ch;\n  --anchura-estrecho: 50ch;\n}\n</code></pre>\n<p>Esto permite ajustar los anchos en un solo lugar. Y como son custom properties, podrías cambiarlos por sección si alguna página necesita un layout diferente.</p>\n<p>Un detalle importante: <code>--anchura-normal</code> usa <code>ch</code> (el ancho del carácter &quot;0&quot;) en lugar de <code>rem</code> o <code>px</code>. Es deliberado — quiero que la anchura del texto dependa del tamaño de la fuente, no de un número arbitrario de píxeles. Si cambio la fuente o el tamaño, la anchura óptima de lectura se ajusta sola.</p>\n<h2>Lo que no encontrarás en un framework</h2>\n<p>Este patrón no viene en Bootstrap, no está en Tailwind, no lo genera ningún plugin. Es el tipo de solución que aparece después de años trabajando con CSS, entendiendo las especificaciones y pensando en el problema real en vez de buscar la clase preconstruida.</p>\n<p>No digo que los frameworks no tengan su lugar. Pero para un sistema de layout, cuatro líneas de CSS Grid y tres custom properties hacen más que cientos de clases de utilidad.</p>\n","date_published":"2026-02-16T00:00:00.000Z","image":"https://paigar.eu/css-grid-columnas-nombradas.png"},{"id":"https://paigar.eu/juego-parejas-tutorial/","url":"https://paigar.eu/juego-parejas-tutorial/","title":"Cómo programar el juego de parejas desde cero","language":"es","content_html":"<p>El juego de parejas, también conocido como Memory o emparejamiento, es probablemente el videojuego más sencillo de toda esta serie. No tiene física, no tiene movimiento continuo, no tiene matrices que se transforman, no tiene rebotes ni colisiones. Tiene un puñado de cartas boca abajo, las giras de dos en dos buscando parejas, y cuando aciertas se quedan visibles. Cuando aciertas todas, has ganado. Eso es todo. Y precisamente por ser tan sencillo es un proyecto perfecto para alguien que está empezando, porque te permite concentrarte en cómo se organiza un juego sin que la mecánica te pida un esfuerzo intelectual grande.</p>\n<p>En este tutorial vamos a construirlo en HTML, CSS y JavaScript, sin frameworks ni dependencias. La gran diferencia respecto a Snake, 2048 o Pong es que aquí no usamos <code>&lt;canvas&gt;</code> para nada. Todo el juego se construye con elementos HTML normales y se anima con transiciones de CSS, lo cual lo hace mucho más cercano al desarrollo web cotidiano que a la programación de videojuegos clásica. Si lo tuyo es maquetar páginas, este tutorial te va a parecer especialmente familiar.</p>\n<h2>La idea general antes de tocar código</h2>\n<p>El juego de parejas consiste en una rejilla de cartas, normalmente entre 12 y 24, donde cada carta tiene una pareja idéntica. Las cartas empiezan boca abajo, mostrando un reverso uniforme. El jugador hace click en una carta para girarla y verla. Después hace click en otra. Si las dos cartas son iguales, se quedan boca arriba. Si no lo son, después de un breve momento para que el jugador memorice las posiciones, se vuelven a girar boca abajo. El juego termina cuando todas las parejas están descubiertas.</p>\n<p>La parte interesante de programarlo no está en el juego en sí, sino en cómo gestionar el estado entre clicks: hay que distinguir entre la primera carta seleccionada, la segunda carta seleccionada, los momentos donde el jugador no debería poder hacer click (mientras se están comparando dos cartas, por ejemplo) y las cartas que ya están emparejadas y deben quedar bloqueadas. Todo eso son cuatro o cinco estados sencillos pero hay que tenerlos claros desde el principio para que el código no se enrede.</p>\n<h2>El esqueleto HTML y CSS</h2>\n<p>A diferencia de los tutoriales anteriores, aquí no hay canvas. Cada carta es un <code>&lt;div&gt;</code> con dos caras, anverso y reverso, que se gira con una transformación CSS de tres dimensiones. Ese efecto del giro es uno de los detalles que más bonito quedan en este tipo de juegos y, sorprendentemente, no requiere prácticamente JavaScript: se hace casi todo con CSS.</p>\n<pre><code class=\"language-html\">&lt;!DOCTYPE html&gt;\n&lt;html lang=&quot;es&quot;&gt;\n\t&lt;head&gt;\n\t\t&lt;meta charset=&quot;UTF-8&quot; /&gt;\n\t\t&lt;title&gt;Parejas&lt;/title&gt;\n\t\t&lt;style&gt;\n\t\t\tbody {\n\t\t\t\tdisplay: flex;\n\t\t\t\tflex-direction: column;\n\t\t\t\talign-items: center;\n\t\t\t\tbackground: #faf8ef;\n\t\t\t\tfont-family: system-ui, sans-serif;\n\t\t\t}\n\t\t\t.tablero {\n\t\t\t\tdisplay: grid;\n\t\t\t\tgrid-template-columns: repeat(4, 80px);\n\t\t\t\tgap: 10px;\n\t\t\t}\n\t\t\t.carta {\n\t\t\t\twidth: 80px;\n\t\t\t\theight: 80px;\n\t\t\t\tperspective: 600px;\n\t\t\t\tcursor: pointer;\n\t\t\t}\n\t\t\t.carta-interior {\n\t\t\t\tposition: relative;\n\t\t\t\twidth: 100%;\n\t\t\t\theight: 100%;\n\t\t\t\ttransition: transform 0.5s;\n\t\t\t\ttransform-style: preserve-3d;\n\t\t\t}\n\t\t\t.carta.girada .carta-interior {\n\t\t\t\ttransform: rotateY(180deg);\n\t\t\t}\n\t\t\t.cara {\n\t\t\t\tposition: absolute;\n\t\t\t\twidth: 100%;\n\t\t\t\theight: 100%;\n\t\t\t\tbackface-visibility: hidden;\n\t\t\t\tborder-radius: 6px;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tjustify-content: center;\n\t\t\t\tfont-size: 2rem;\n\t\t\t}\n\t\t\t.reverso {\n\t\t\t\tbackground: #bbada0;\n\t\t\t}\n\t\t\t.anverso {\n\t\t\t\tbackground: #fff;\n\t\t\t\ttransform: rotateY(180deg);\n\t\t\t}\n\t\t&lt;/style&gt;\n\t&lt;/head&gt;\n\t&lt;body&gt;\n\t\t&lt;h1&gt;Parejas&lt;/h1&gt;\n\t\t&lt;div&gt;Movimientos: &lt;span id=&quot;movimientos&quot;&gt;0&lt;/span&gt;&lt;/div&gt;\n\t\t&lt;div class=&quot;tablero&quot; id=&quot;tablero&quot;&gt;&lt;/div&gt;\n\t\t&lt;script&gt;\n\t\t\t// aquí irá todo el código del juego\n\t\t&lt;/script&gt;\n\t&lt;/body&gt;\n&lt;/html&gt;\n</code></pre>\n<p>El truco visual está en cuatro propiedades CSS combinadas: <code>perspective</code> en el contenedor exterior, <code>transform-style: preserve-3d</code> en el contenedor interior, <code>backface-visibility: hidden</code> en cada cara y un <code>transform: rotateY(180deg)</code> que se aplica al contenedor interior cuando la carta tiene la clase <code>girada</code>. Lo que ocurre con esto es que el navegador trata la carta como un objeto tridimensional con dos lados, y al girarlo ciento ochenta grados sobre su eje vertical, la cara que estaba escondida pasa al frente y la que estaba al frente se esconde. Si nunca habías hecho esto antes, te recomiendo que lo escribas en una página vacía y juegues con los valores: es uno de esos efectos que parecen complicados y resulta que están a tres líneas de CSS de distancia.</p>\n<h2>El estado del juego</h2>\n<p>El estado del juego es un array de cartas, donde cada carta tiene un símbolo (lo que muestra cuando está girada) y un estado: si está girada o no, y si ya está emparejada. Más una variable para llevar la cuenta de los movimientos del jugador.</p>\n<pre><code class=\"language-javascript\">const SIMBOLOS = [&quot;🐶&quot;, &quot;🐱&quot;, &quot;🦊&quot;, &quot;🐻&quot;, &quot;🐼&quot;, &quot;🐨&quot;, &quot;🦁&quot;, &quot;🐸&quot;];\n\nlet cartas = [];\nlet primeraCarta = null;\nlet segundaCarta = null;\nlet bloqueado = false;\nlet movimientos = 0;\n</code></pre>\n<p>Las variables <code>primeraCarta</code> y <code>segundaCarta</code> guardan referencias a las cartas que el jugador acaba de levantar. La variable <code>bloqueado</code> evita que el jugador pueda seguir levantando cartas mientras se está comprobando una pareja: durante el segundo de espera entre que levanta dos cartas que no coinciden y vuelven a girarse, no debe poder tocar nada más. Sin esa variable, un jugador rápido podría hacer click en una tercera carta antes de que las dos primeras se hayan resuelto, y el código se confundiría.</p>\n<h2>Inicializar el juego</h2>\n<p>Para empezar una partida necesitamos crear las parejas, mezclarlas y pintarlas en pantalla en orden aleatorio. Es la operación más larga del juego y la única que tiene un poquito de chicha, pero tampoco mucha.</p>\n<pre><code class=\"language-javascript\">function inicializar() {\n\tcartas = [];\n\tprimeraCarta = null;\n\tsegundaCarta = null;\n\tbloqueado = false;\n\tmovimientos = 0;\n\n\tconst parejas = [...SIMBOLOS, ...SIMBOLOS];\n\tparejas.sort(() =&gt; Math.random() - 0.5);\n\n\tparejas.forEach((simbolo, indice) =&gt; {\n\t\tcartas.push({\n\t\t\tindice,\n\t\t\tsimbolo,\n\t\t\tgirada: false,\n\t\t\temparejada: false,\n\t\t});\n\t});\n\n\tpintar();\n\tactualizarMarcador();\n}\n</code></pre>\n<p>La línea <code>[...SIMBOLOS, ...SIMBOLOS]</code> duplica el array de símbolos para tener dieciséis cartas (ocho parejas). Después, <code>sort(() =&gt; Math.random() - 0.5)</code> mezcla el array. Este truco de mezcla no es matemáticamente perfecto —los matemáticos prefieren el algoritmo de Fisher-Yates—, pero para un juego como este es más que suficiente y se escribe en una sola línea. Si te interesa la pureza estadística, busca <em>Fisher-Yates shuffle</em> y lo aplicas en lugar de esta línea. Aquí no notarás la diferencia.</p>\n<h2>Pintar el tablero</h2>\n<p>La función que pinta el tablero recorre el array de cartas y crea un <code>&lt;div&gt;</code> para cada una, con su anverso, su reverso y su comportamiento al hacer click.</p>\n<pre><code class=\"language-javascript\">function pintar() {\n\tconst tablero = document.getElementById(&quot;tablero&quot;);\n\ttablero.innerHTML = &quot;&quot;;\n\n\tcartas.forEach((carta) =&gt; {\n\t\tconst div = document.createElement(&quot;div&quot;);\n\t\tdiv.className = &quot;carta&quot;;\n\t\tif (carta.girada || carta.emparejada) div.classList.add(&quot;girada&quot;);\n\t\tdiv.dataset.indice = carta.indice;\n\n\t\tdiv.innerHTML = `\n      &lt;div class=&quot;carta-interior&quot;&gt;\n        &lt;div class=&quot;cara reverso&quot;&gt;?&lt;/div&gt;\n        &lt;div class=&quot;cara anverso&quot;&gt;${carta.simbolo}&lt;/div&gt;\n      &lt;/div&gt;\n    `;\n\n\t\tdiv.addEventListener(&quot;click&quot;, () =&gt; clickCarta(carta));\n\t\ttablero.appendChild(div);\n\t});\n}\n</code></pre>\n<p>Lo más interesante aquí es la lógica de la clase <code>girada</code>. Una carta se muestra girada si su estado lo indica (porque el jugador acaba de hacer click en ella) o si ya está emparejada (porque su pareja ya fue encontrada y queda visible). Esto significa que la misma clase CSS sirve para los dos estados visuales, simplificando el código.</p>\n<h2>La lógica del click</h2>\n<p>Aquí está el núcleo del juego. Cuando el jugador hace click en una carta, hay que decidir qué pasa, y eso depende del estado actual.</p>\n<pre><code class=\"language-javascript\">function clickCarta(carta) {\n\tif (bloqueado) return;\n\tif (carta.emparejada) return;\n\tif (carta === primeraCarta) return;\n\n\tcarta.girada = true;\n\tpintar();\n\n\tif (!primeraCarta) {\n\t\tprimeraCarta = carta;\n\t\treturn;\n\t}\n\n\tsegundaCarta = carta;\n\tbloqueado = true;\n\tmovimientos++;\n\tactualizarMarcador();\n\n\tif (primeraCarta.simbolo === segundaCarta.simbolo) {\n\t\tprimeraCarta.emparejada = true;\n\t\tsegundaCarta.emparejada = true;\n\t\tresetSeleccion();\n\t\tif (cartas.every((c) =&gt; c.emparejada)) {\n\t\t\tsetTimeout(\n\t\t\t\t() =&gt; alert(`¡Has ganado en ${movimientos} movimientos!`),\n\t\t\t\t400,\n\t\t\t);\n\t\t}\n\t} else {\n\t\tsetTimeout(() =&gt; {\n\t\t\tprimeraCarta.girada = false;\n\t\t\tsegundaCarta.girada = false;\n\t\t\tpintar();\n\t\t\tresetSeleccion();\n\t\t}, 900);\n\t}\n}\n\nfunction resetSeleccion() {\n\tprimeraCarta = null;\n\tsegundaCarta = null;\n\tbloqueado = false;\n}\n</code></pre>\n<p>La función empieza con tres comprobaciones que cortan el flujo si el click no debería tener efecto. Si el juego está bloqueado (porque hay una comprobación en curso), si la carta ya está emparejada o si es la misma carta que el jugador acaba de levantar, no hacemos nada. Estas tres líneas son fundamentales, porque sin ellas el código se rompe en cuanto el jugador hace clicks rápidos o desordenados.</p>\n<p>Después gira la carta y comprueba si es la primera o la segunda del turno. Si es la primera, simplemente la guarda y termina. Si es la segunda, compara los símbolos. Si coinciden, marca ambas como emparejadas. Si no coinciden, espera 900 milisegundos y las vuelve a girar boca abajo, dejando al jugador tiempo suficiente para memorizar dónde estaban.</p>\n<p>Cuando las dos cartas coinciden, además, comprobamos si ya están todas emparejadas. Si lo están, mostramos el mensaje de victoria. Esto se hace después de un pequeño retraso para que el jugador tenga tiempo de ver la última pareja girarse antes de que aparezca el mensaje.</p>\n<h2>Actualizar el marcador</h2>\n<p>Esta es la función más sencilla del tutorial entero, pero conviene tenerla aparte para poder llamarla desde varios sitios.</p>\n<pre><code class=\"language-javascript\">function actualizarMarcador() {\n\tdocument.getElementById(&quot;movimientos&quot;).textContent = movimientos;\n}\n\ninicializar();\n</code></pre>\n<p>Con la última línea arrancamos el juego al cargar la página. Y con esto, ya tenemos un juego de parejas funcional. Diecisiete líneas de CSS para el efecto del giro, unas cuarenta de JavaScript para toda la lógica, y una página HTML mínima. Suma todo y no llegas a las cien líneas. Una de las cosas que más me gusta de este proyecto es justamente esa: el cociente entre lo divertido que resulta jugarlo y lo poco que cuesta programarlo es enorme.</p>\n<h2>Cosas que se pueden añadir</h2>\n<p>A partir del esqueleto que hemos construido, hay muchas mejoras posibles. Un cronómetro que mida cuánto tarda el jugador en completar una partida. Un sistema de niveles con tableros más grandes y más parejas. Diferentes conjuntos de símbolos: animales, frutas, banderas, elementos químicos. Un sonido suave cuando se gira una carta y otro distinto cuando se completa una pareja. Un récord de mejor tiempo o menor número de movimientos guardado en <code>localStorage</code>. Un modo de dos jugadores donde los jugadores se turnan y el que más parejas encuentra gana. Animaciones más elaboradas: que las cartas emparejadas hagan un pequeño efecto de celebración antes de quedarse fijas.</p>\n<p>Todas son mejoras razonables que caben en pocas líneas adicionales. Pero como pasa con todos los juegos de esta serie, lo importante es que lo básico ya funciona. A partir de aquí, lo que añadas o no es decisión tuya según las ganas que tengas y el tiempo que quieras dedicarle.</p>\n<h2>El prototipo funcional</h2>\n<p>Aquí abajo dejo el juego completo y funcionando, con un cronómetro que se inicia automáticamente al primer click, animación de celebración cuando se completa una pareja, un botón para empezar partida nueva y diseño adaptado para que funcione bien en móvil. Está todo aislado bajo un id propio para que no afecte al resto del blog.</p>\n<p><strong>Otros tutoriales de la serie</strong>: <a href=\"https://paigar.eu/juego-2048-tutorial/\">2048</a> · <a href=\"https://paigar.eu/juego-pong-tutorial/\">Pong</a> · <a href=\"https://paigar.eu/juego-serpiente-tutorial/\">Serpiente</a> · <a href=\"https://paigar.eu/juego-ladrillos-tutorial/\">Ladrillos</a>.</p>\n","date_published":"2026-02-09T00:00:00.000Z","image":"https://paigar.eu/juego-parejas.png"},{"id":"https://paigar.eu/envejecer-en-la-web/","url":"https://paigar.eu/envejecer-en-la-web/","title":"Cómo envejecer en un oficio que nunca había envejecido","language":"es","content_html":"<p>Tengo más de cincuenta años y sigo escribiendo código para vivir. Esto, en cualquier otra profesión, sería la cosa más aburrida del mundo. Un abogado de cincuenta está en su mejor momento. Un médico, lo mismo. Un arquitecto, ni te cuento, que muchos a esa edad ni siquiera han terminado su edificio importante. Pero en desarrollo web la cosa cambia. Cuando dices que llevas en esto desde finales de los noventa y que sigues activo, la mirada que recibes oscila entre la compasión y la curiosidad antropológica. <em>Vaya, otro de los que sobrevivió</em>. Como si fueras la última cuagga del zoológico.</p>\n<p>He pensado bastante sobre por qué pasa esto, y creo que la respuesta es bastante divertida cuando uno la mira con calma. La razón es que somos los primeros en hacer esto. Los primeros en envejecer programando webs. La web tiene poco más de treinta años en su versión utilizable, y los que entramos al oficio cuando todo era HTML estático y tablas con bordes invisibles para maquetar somos, literalmente, la primera generación que llega a la cincuentena habiendo dedicado su vida laboral a esto. Antes de nosotros, nadie. No hay abuelos del oficio. No hay maestros jubilados. No hay un Picasso del <em>div</em> al que peregrinar. Estamos inventando cómo se envejece en esta profesión sobre la marcha.</p>\n<h2>Pioneros sin querer serlo</h2>\n<p>La cosa es que nadie firmó para esto. Cuando empezamos a finales de los noventa, lo último que pensábamos era que estábamos abriendo camino a algo. Nos limitábamos a maquetar páginas con FrontPage o, los más valientes, con Notepad y un libro de HTML al lado. Vimos aparecer Netscape, sufrimos a Internet Explorer 6 con la dignidad del que sufre una desgracia natural, navegamos por la web con Lynx desde una terminal de Linux porque no había otra cosa, y enviamos formularios cuyos resultados llegaban por correo electrónico porque los formularios bien hechos en servidor eran ciencia avanzada. Todo lo que hacíamos era cacharreo. Pero ese cacharreo, sin que nadie nos avisara, se convirtió con los años en una profesión, y luego en un sector entero, y luego en una parte central de la economía mundial. Y nosotros, sin movernos demasiado del sitio, pasamos de ser los frikis del barrio a ser veteranos de un oficio millonario.</p>\n<p>Lo curioso es que la profesión, al haber crecido tan deprisa, no ha tenido tiempo de desarrollar una tradición. En medicina hay toda una jerarquía cultural construida durante siglos: residentes, adjuntos, jefes de servicio, eméritos. En arquitectura hay maestros, estudios consolidados, premios honoríficos al final de la carrera. En el desarrollo web, lo más parecido a un emérito es un señor con barba canosa que mantiene un proyecto de software libre desde 2003 y al que pocos saben colocar en el mapa. La estructura simplemente no existe. Nos hemos quedado fuera de cuadro porque nunca se dibujó el cuadro.</p>\n<h2>Haber estado ahí, que no es poco</h2>\n<p>Si me paro a pensarlo con calma, una de las cosas que más satisfacción me da a estas alturas es la sensación de haber formado parte, aunque sea modestamente, del crecimiento de internet. No hablo de haber inventado nada importante, ni de aparecer en ninguna historia oficial del medio. Hablo de algo más pequeño y más bonito: haber estado ahí cuando todo se estaba inventando. Haber montado las primeras webs de algún cliente que no tenía ni idea de qué era una página web. Haber visto cómo aquello pasó de ser una curiosidad para frikis a ser la herramienta principal con la que se comunican empresas, instituciones, comunidades enteras.</p>\n<p>Cada página que dejé funcionando hace veinte años, cada cliente al que ayudé a entender que internet no era una moda pasajera, cada formulario que conseguí que llegara correctamente al destinatario después de pelearme media tarde con caracteres especiales, todo eso forma parte, en una proporción microscópica pero real, del internet que tenemos hoy. La web que ahora todos usamos para todo se ha construido sumando millones de pequeñas contribuciones de gente como yo, programadores anónimos que fueron levantando una pieza tras otra sin saber que estaban levantando algo enorme. Eso, mirado desde la cincuentena, es bastante para sentirse orgulloso. No es poesía, es contabilidad emocional honesta: he dedicado media vida a esto y la cosa funciona. Cuando veo a un crío usando una web para hacer los deberes pienso, sin ninguna grandilocuencia, <em>yo también puse un par de ladrillos en este edificio</em>. Y me parece suficiente.</p>\n<h2>En mi mejor momento, que leches</h2>\n<p>Y dicho todo esto, hay otra cosa que conviene decir bien clara. A los cincuenta y tantos, en la mayoría de oficios serios, uno está en su mejor momento. Un abogado con esta trayectoria es el que llevan los casos importantes. Un arquitecto con esta edad es el que firma los proyectos que de verdad cuentan. Un cirujano de esta edad es al que pides cuando el asunto es delicado. Y la pregunta razonable es: ¿por qué un desarrollador web con veinticinco años de oficio no iba a estar exactamente en el mismo sitio? Pues lo está. Lo estoy. No voy a ir por la vida disculpándome por seguir ejerciendo cuando, francamente, nunca había trabajado mejor.</p>\n<p>Hago en seis horas lo que antes me costaba doce, no porque haya menos energía sino porque no me pierdo en rodeos. Diagnostico problemas en minutos que a cualquier persona con menos kilómetros le costarían días, no por listo sino por base de datos personal acumulada bug a bug. Hablo con clientes de cualquier sector con la naturalidad de quien ha visto pasar empresas, modelos de negocio, modas y desgracias variadas. Sé qué tecnologías merecen la pena y cuáles van a desaparecer en tres años. Sé cuándo aceptar un proyecto y cuándo decir que mejor no, que no encaja. Sé escribir código limpio, mantener webs ligeras, decirle que no a un page builder que va a generar basura, recomendar un sitio estático cuando lo razonable es un sitio estático. Si todo esto, sumado, no es estar en el mejor momento, no sé qué será.</p>\n<h2>La buena noticia</h2>\n<p>Cuando uno se para a pensarlo en serio, lo que estamos haciendo los veteranos del oficio en este momento histórico es bastante interesante. Estamos descubriendo, sobre la marcha y sin instrucciones, qué significa envejecer haciendo este trabajo. Estamos probando opciones, improvisando trayectorias, mezclando oficios. Algunos abren agencias. Otros se especializan tanto que se vuelven imprescindibles para tres clientes muy concretos. Otros enseñan, escriben, mantienen proyectos personales, dan charlas. Cada uno improvisa su manera de seguir aquí.</p>\n<p>Esa improvisación, vista en perspectiva, es el manual que les estamos dejando a los que vienen detrás. La generación que tiene ahora treinta años va a poder mirarnos a nosotros dentro de quince y decirse <em>ah, así se hace, así se sobrevive, así se sigue siendo útil sin perder la cabeza</em>. Habremos sido sus maestros sin querer, igual que fuimos pioneros sin querer. Es una responsabilidad bonita y también un poco cómica, porque en el fondo la mayoría seguimos haciendo lo mismo que hacíamos a los veintitantos: cacharrear con código, intentar que las webs carguen rápido y discutir de vez en cuando si esta vez sí ha llegado el momento de aprender Rust o vamos a esperar otros dos años. Hay peores maneras de cumplir años. Esta, la verdad, la recomiendo bastante.</p>\n","date_published":"2026-02-02T00:00:00.000Z","image":"https://paigar.eu/envejecer-en-la-web.png"},{"id":"https://paigar.eu/tecnologias-que-marcaron-la-web/","url":"https://paigar.eu/tecnologias-que-marcaron-la-web/","title":"Las tecnologías que marcaron la web (vistas desde dentro)","language":"es","content_html":"<p>Llevo en internet desde antes de que internet tuviera imágenes. No es una exageración: mis primeras sesiones fueron con Lynx sobre Linux, un navegador de texto puro donde la web era párrafos, enlaces subrayados y poco más. No había colores, no había layout, no había nada que se pareciera a lo que hoy entendemos por &quot;página web&quot;. Y sin embargo, la sensación de acceder a información de un servidor al otro lado del mundo a través de una terminal ya era extraordinaria.</p>\n<p>Desde ahí hasta hoy he visto pasar tecnologías que en su momento parecían definitivas y que desaparecieron sin dejar rastro. Este es un recorrido por las que más me marcaron, no como lista histórica, sino como experiencia vivida.</p>\n<h2>Lynx y la web antes de la web</h2>\n<p>Antes de que los navegadores gráficos llegaran a los escritorios, la web se navegaba en modo texto. Lynx renderizaba HTML como texto plano con enlaces numerados. No había CSS. No había JavaScript. No había imágenes inline. Solo contenido.</p>\n<p>Es fácil idealizar esa época desde la nostalgia, pero la realidad es que era limitada. Lo importante es lo que demuestra: que la web nació como un sistema de documentos enlazados, no como una plataforma de aplicaciones. Ese origen importa, porque muchos de los problemas que tenemos hoy vienen de forzar la web a ser algo que no fue diseñada para ser.</p>\n<h2>Netscape: cuando la web se hizo visual</h2>\n<p>La aparición de Netscape Navigator lo cambió todo. De repente había imágenes, había color, había la posibilidad de que una página tuviera personalidad visual. Recuerdo la primera vez que vi una web con fondo de color y texto formateado — después de meses con Lynx, era como pasar del blanco y negro al color.</p>\n<p>Netscape no solo fue un navegador. Fue el catalizador que convirtió la web en un medio de masas. Antes de Netscape, internet era de universidades y técnicos. Después de Netscape, era de todos. También nos trajo JavaScript — un lenguaje que Marc Andreessen encargó crear en diez días y que hoy domina el ecosistema entero. Nadie habría apostado por eso en 1995.</p>\n<p>Y nos trajo la etiqueta <code>&lt;blink&gt;</code>. Esa sí que nadie la echa de menos.</p>\n<h2>Las guerras de los navegadores</h2>\n<p>Netscape contra Internet Explorer. La primera gran guerra del software. Microsoft empezó a incluir IE gratis con Windows y en pocos años Netscape pasó de dominar el mercado a desaparecer. Para los desarrolladores fue un desastre: cada navegador implementaba HTML y CSS a su manera, y escribir código que funcionara en ambos era un ejercicio de paciencia y hacks.</p>\n<p>Recuerdo los conditional comments de IE, los underscore hacks, el <code>* html</code>. Recuerdo tener que probar cada página en cuatro navegadores diferentes. Recuerdo la frustración de que algo perfecto en Netscape se rompiera en IE, y viceversa.</p>\n<p>Los que empiezan ahora a desarrollar no saben la suerte que tienen con la compatibilidad actual entre navegadores. Que Firefox, Chrome, Safari y Edge rendericen prácticamente igual el mismo código es un lujo que se construyó sobre años de frustración colectiva y esfuerzos de estandarización.</p>\n<h2>La tiranía de Internet Explorer 6</h2>\n<p>IE6 merece su propia sección. Fue el navegador más odiado de la historia del desarrollo web, y al mismo tiempo el más longevo. Cuando Microsoft ganó la guerra de los navegadores, dejó de actualizar IE durante años. IE6 se quedó congelado en 2001 mientras la web avanzaba a su alrededor.</p>\n<p>El problema no era solo que IE6 fuera malo. El problema es que millones de personas seguían usándolo — algunas hasta bien entrado 2010 — y los clientes te decían &quot;tiene que funcionar en IE6&quot;. Eso significaba renunciar a casi todo lo que CSS podía hacer y vivir en un mundo de floats, clearfix y <code>zoom: 1</code>.</p>\n<p>El día que los grandes sitios dejaron de dar soporte a IE6 fue un día de celebración real en la comunidad web. No exagero. Hubo contadores de cuenta atrás y campañas enteras dedicadas a enterrarlo.</p>\n<h2>Geocities: la web personal en estado puro</h2>\n<p>Entre todas las tecnologías y plataformas que he visto, Geocities ocupa un lugar especial. No porque fuera bueno — técnicamente era horrible. Fondos de estrellas, textos parpadeantes, GIFs animados de obras en construcción, contadores de visitas que nunca llegaban a tres cifras.</p>\n<p>Pero Geocities representaba algo que hemos perdido: la web como espacio personal sin pretensiones. La gente hacía páginas porque quería, no porque tuviera que posicionar una marca o monetizar una audiencia. Eran feas, desordenadas, a veces ilegibles. Pero eran honestas. Cada página era alguien diciendo &quot;esto soy yo, esto me gusta, esto quiero compartir&quot;.</p>\n<p>Cuando Yahoo cerró Geocities en 2009, se llevó millones de páginas personales. Se perdió una parte de la historia de la web que no vamos a recuperar. Y lo que vino después — Facebook, Instagram, Twitter — nos dio alcance a cambio de quitarnos propiedad. Ya no son tus páginas. Son sus plataformas.</p>\n<h2>Flash: el rey que se creía inmortal</h2>\n<p>Si has trabajado en la web entre 2000 y 2010, has sufrido Flash. No como desarrollador — como usuario. Esas intros animadas que tardaban un minuto en cargar antes de dejarte ver el contenido. Esos menús que no respondían al botón derecho. Esas webs enteras donde no podías copiar texto ni usar el botón de atrás.</p>\n<p>Como herramienta, Flash era impresionante. Permitía cosas que HTML y CSS no podían ni soñar: animaciones complejas, vídeo integrado, interactividad rica. Hubo una época en que los mejores portfolios y las webs más creativas estaban hechos en Flash, y parecía que ese era el futuro inevitable de la web.</p>\n<p>Entonces llegaron los smartphones. Steve Jobs publicó su carta explicando por qué el iPhone no soportaría Flash, y en pocos años el ecosistema entero se desmoronó. Flash pasó de ser imprescindible a ser historia en menos de una década.</p>\n<p>La lección es clara: si tu tecnología no es un estándar abierto, por muy dominante que sea, puede desaparecer cuando una empresa decide que ya no le conviene. Es una lección que sigo aplicando hoy cada vez que alguien me sugiere construir sobre la plataforma propietaria del momento.</p>\n<h2>Las tablas para layout</h2>\n<p>Antes de que CSS supiera maquetar — y tardó en aprender —, el layout se hacía con tablas HTML. Tablas anidadas dentro de tablas, con celdas vacías para crear espacios, imágenes cortadas en trozos para simular bordes redondeados, spacer GIFs transparentes de 1×1 píxel.</p>\n<p>Era espantoso desde el punto de vista semántico. Pero funcionaba. Y durante años fue la única forma fiable de conseguir un diseño con columnas que se viera igual en todos los navegadores.</p>\n<p>La transición a CSS para layout fue lenta y dolorosa. Primero los floats, que nunca fueron diseñados para eso. Luego los frameworks como Blueprint y 960gs. Después Flexbox. Y finalmente CSS Grid, que es lo que las tablas para layout intentaban ser pero bien hecho.</p>\n<p>Hoy diseño layouts en minutos que hace veinte años habrían requerido horas de tablas anidadas. Y cada vez que uso <code>grid-template-columns</code>, agradezco silenciosamente no tener que volver a escribir <code>&lt;td width=&quot;1&quot; bgcolor=&quot;transparent&quot;&gt;</code>.</p>\n<h2>jQuery: el gran igualador</h2>\n<p>Es difícil explicar a alguien que empezó a programar después de 2015 lo que significó jQuery. No fue solo una librería — fue la solución a un problema que hoy ya no existe: la incompatibilidad brutal entre navegadores.</p>\n<p>Escribir <code>$('.menu').slideToggle()</code> y que funcionara en IE6, Firefox 2 y Safari 3 sin cambiar una línea era revolucionario. jQuery abstraía las diferencias entre navegadores y nos dejaba centrarnos en lo que queríamos hacer en vez de en cómo hacerlo funcionar en cada uno.</p>\n<p>Hoy jQuery es innecesario. Los navegadores modernos implementan las mismas APIs, <code>querySelector</code> existe de forma nativa, y CSS hace animaciones mejor que JavaScript. Pero durante una década, jQuery fue la diferencia entre poder hacer tu trabajo y querer tirarte por la ventana.</p>\n<h2>Lo que se repite</h2>\n<p>Mirando atrás, lo que más me llama la atención no son las tecnologías en sí, sino cómo se repite el patrón: algo aparece, se vuelve dominante, la gente dice que es el futuro, y cinco o diez años después ha desaparecido o se ha vuelto irrelevante.</p>\n<p>Pasó con Netscape. Pasó con IE6. Pasó con Flash. Pasó con jQuery. Pasó con cada framework de CSS que prometía ser el último. Y está pasando ahora mismo con herramientas que hoy parecen imprescindibles.</p>\n<p>Lo único que no ha desaparecido en todos estos años es la plataforma web en sí: HTML, CSS y JavaScript. Las tres tecnologías base siguen ahí, mejorando cada año, sin breaking changes, sin versiones incompatibles. Es la constante en un mar de modas.</p>\n<p>Después de haber navegado con Lynx, haber sufrido IE6, haber esperado intros de Flash y haber maquetado con tablas, esa constancia es lo que más valoro. Y es lo que elijo como base de mi trabajo: tecnología que no caduca.</p>\n","date_published":"2026-01-12T00:00:00.000Z","image":"https://paigar.eu/tecnologias-que-marcaron-la-web.png"},{"id":"https://paigar.eu/juego-pong-tutorial/","url":"https://paigar.eu/juego-pong-tutorial/","title":"Cómo programar Pong desde cero","language":"es","content_html":"<p>Pong es, casi con seguridad, el videojuego más sencillo que se haya programado jamás. Salió en 1972 de la mano de Atari, fue el primer videojuego comercialmente exitoso de la historia, y su mecánica entera cabe en una sola frase: dos paletas, una pelota, gana el primero que llegue a una puntuación acordada. No tiene niveles, no tiene power-ups, no tiene jefes finales. Tiene física, tiene reflejos y tiene la satisfacción inexplicable de oír el sonido de la pelota chocando contra la paleta. Y por todo eso es uno de los mejores proyectos para aprender cómo funciona un videojuego con movimiento continuo, que es algo que en Snake o 2048 no aparecía.</p>\n<p>En este tutorial vamos a construir Pong en HTML, CSS y JavaScript, sin frameworks ni dependencias. Como con los anteriores, todo cabe en un único archivo y al final del artículo dejo el prototipo embebido para que puedas jugarlo. La diferencia conceptual respecto a Snake y 2048 es que aquí los objetos no se mueven por casillas discretas: se mueven con velocidad real, en píxeles por fotograma, y eso introduce el concepto de física básica de juego. No te asustes, que es física de la fácil.</p>\n<h2>La idea general antes de tocar código</h2>\n<p>Pong consiste en una pelota que rebota indefinidamente entre dos paletas, una a la izquierda y otra a la derecha. La pelota tiene una velocidad horizontal y una velocidad vertical, y en cada fotograma del juego se desplaza exactamente esos píxeles. Cuando la pelota toca el borde superior o el inferior, su velocidad vertical se invierte (rebota). Cuando toca una paleta, su velocidad horizontal se invierte (rebota hacia el otro lado). Si la pelota sale del campo por uno de los laterales sin que la paleta correspondiente la haya tocado, el otro jugador gana un punto y la pelota vuelve al centro.</p>\n<p>Las dos paletas se mueven solo en vertical. La del jugador humano responde a las teclas, y la del rival —que en nuestra versión va a ser una IA muy básica— intenta seguir la posición vertical de la pelota con un poco de retraso para que no sea invencible. Esto es todo. Si entiendes estos cuatro o cinco conceptos, has entendido Pong entero.</p>\n<h2>El esqueleto HTML y CSS</h2>\n<p>Empezamos por la página. Vamos a usar un <code>&lt;canvas&gt;</code> para dibujar, igual que en Snake, porque la pelota se va a mover en píxeles y el canvas es la herramienta natural para eso. Las paletas las podríamos hacer con divs absolutos pero es más limpio dibujarlas también en el canvas, así todo el juego está contenido en un solo elemento.</p>\n<pre><code class=\"language-html\">&lt;!DOCTYPE html&gt;\n&lt;html lang=&quot;es&quot;&gt;\n\t&lt;head&gt;\n\t\t&lt;meta charset=&quot;UTF-8&quot; /&gt;\n\t\t&lt;title&gt;Pong&lt;/title&gt;\n\t\t&lt;style&gt;\n\t\t\tbody {\n\t\t\t\tdisplay: flex;\n\t\t\t\tflex-direction: column;\n\t\t\t\talign-items: center;\n\t\t\t\tbackground: #1a1d24;\n\t\t\t\tcolor: #e8e8e8;\n\t\t\t\tfont-family: system-ui, sans-serif;\n\t\t\t}\n\t\t\tcanvas {\n\t\t\t\tbackground: #000;\n\t\t\t\tborder: 1px solid #2a2e36;\n\t\t\t}\n\t\t&lt;/style&gt;\n\t&lt;/head&gt;\n\t&lt;body&gt;\n\t\t&lt;h1&gt;Pong&lt;/h1&gt;\n\t\t&lt;div&gt;\n\t\t\tTú: &lt;span id=&quot;puntos-jugador&quot;&gt;0&lt;/span&gt; · Rival:\n\t\t\t&lt;span id=&quot;puntos-rival&quot;&gt;0&lt;/span&gt;\n\t\t&lt;/div&gt;\n\t\t&lt;canvas id=&quot;lienzo&quot; width=&quot;600&quot; height=&quot;400&quot;&gt;&lt;/canvas&gt;\n\t\t&lt;script&gt;\n\t\t\t// aquí irá todo el código del juego\n\t\t&lt;/script&gt;\n\t&lt;/body&gt;\n&lt;/html&gt;\n</code></pre>\n<p>El canvas mide 600 por 400, una proporción 3:2 que para Pong funciona bien. Suficiente espacio horizontal para que la pelota tarde algo en cruzar de un lado a otro y suficiente espacio vertical para que las paletas tengan recorrido.</p>\n<h2>El estado del juego</h2>\n<p>Pong necesita guardar la posición y velocidad de la pelota, la posición vertical de cada paleta y la puntuación de cada jugador. Todo eso se representa con variables sencillas.</p>\n<pre><code class=\"language-javascript\">const ANCHO = 600;\nconst ALTO = 400;\nconst ANCHO_PALETA = 10;\nconst ALTO_PALETA = 80;\nconst TAMANO_PELOTA = 10;\n\nlet pelota = {\n\tx: ANCHO / 2,\n\ty: ALTO / 2,\n\tvx: 4,\n\tvy: 3,\n};\n\nlet paletaJugador = { y: ALTO / 2 - ALTO_PALETA / 2 };\nlet paletaRival = { y: ALTO / 2 - ALTO_PALETA / 2 };\n\nlet puntosJugador = 0;\nlet puntosRival = 0;\n</code></pre>\n<p>La pelota tiene posición <code>x</code>, <code>y</code> y velocidad <code>vx</code>, <code>vy</code>. Que la velocidad inicial sea 4 horizontal y 3 vertical no es casualidad: si fuera puramente horizontal o puramente vertical el juego sería aburrido, y los valores 4 y 3 dan una trayectoria diagonal con un ángulo razonable. Las paletas solo necesitan su posición vertical porque la horizontal es fija (siempre están pegadas al borde correspondiente).</p>\n<h2>Mover la pelota y rebotes en bordes</h2>\n<p>El movimiento de la pelota es la operación más sencilla del mundo: en cada fotograma, sumamos la velocidad a la posición. Si la pelota toca el borde superior o el inferior, invertimos la velocidad vertical. Eso es todo.</p>\n<pre><code class=\"language-javascript\">function actualizarPelota() {\n\tpelota.x += pelota.vx;\n\tpelota.y += pelota.vy;\n\n\tif (pelota.y &lt; 0) {\n\t\tpelota.y = 0;\n\t\tpelota.vy = -pelota.vy;\n\t}\n\tif (pelota.y + TAMANO_PELOTA &gt; ALTO) {\n\t\tpelota.y = ALTO - TAMANO_PELOTA;\n\t\tpelota.vy = -pelota.vy;\n\t}\n}\n</code></pre>\n<p>Hay un detalle importante en cómo manejamos el rebote. Cuando la pelota se sale por arriba, no nos limitamos a invertir la velocidad: también la &quot;encajamos&quot; de vuelta dentro del campo poniendo <code>y = 0</code>. Sin esto, si la pelota se ha pasado del borde por dos píxeles, en el siguiente fotograma rebota pero todavía está fuera, y al siguiente vuelve a estar fuera, y entra en un bucle visual desagradable de temblequeo. Con la corrección, el rebote es siempre limpio.</p>\n<h2>Detectar colisión con las paletas</h2>\n<p>La parte más delicada de Pong es detectar cuándo la pelota toca una paleta. La forma estándar es comprobar si dos rectángulos se solapan: el rectángulo de la pelota y el rectángulo de la paleta. Dos rectángulos se solapan si y solo si se solapan en el eje X y también en el eje Y. Si en cualquiera de los dos ejes están separados, no hay colisión.</p>\n<pre><code class=\"language-javascript\">function chocaConPaleta(paletaX, paletaY) {\n\treturn (\n\t\tpelota.x &lt; paletaX + ANCHO_PALETA &amp;&amp;\n\t\tpelota.x + TAMANO_PELOTA &gt; paletaX &amp;&amp;\n\t\tpelota.y &lt; paletaY + ALTO_PALETA &amp;&amp;\n\t\tpelota.y + TAMANO_PELOTA &gt; paletaY\n\t);\n}\n</code></pre>\n<p>Esta función comprueba las cuatro condiciones de solapamiento entre el rectángulo de la pelota y el rectángulo de la paleta. Si las cuatro se cumplen a la vez, los rectángulos se están tocando. La aplicamos a la paleta del jugador (que está pegada al borde izquierdo, <code>x = 0</code>) y a la paleta del rival (que está pegada al borde derecho, <code>x = ANCHO - ANCHO_PALETA</code>):</p>\n<pre><code class=\"language-javascript\">function comprobarColisiones() {\n\tif (chocaConPaleta(0, paletaJugador.y) &amp;&amp; pelota.vx &lt; 0) {\n\t\tpelota.vx = -pelota.vx;\n\t\tconst centroPaleta = paletaJugador.y + ALTO_PALETA / 2;\n\t\tconst centroPelota = pelota.y + TAMANO_PELOTA / 2;\n\t\tpelota.vy = (centroPelota - centroPaleta) * 0.15;\n\t}\n\tif (chocaConPaleta(ANCHO - ANCHO_PALETA, paletaRival.y) &amp;&amp; pelota.vx &gt; 0) {\n\t\tpelota.vx = -pelota.vx;\n\t\tconst centroPaleta = paletaRival.y + ALTO_PALETA / 2;\n\t\tconst centroPelota = pelota.y + TAMANO_PELOTA / 2;\n\t\tpelota.vy = (centroPelota - centroPaleta) * 0.15;\n\t}\n}\n</code></pre>\n<p>Aquí hay dos detalles importantes. El primero es la condición <code>pelota.vx &lt; 0</code> (o <code>&gt; 0</code> en el caso del rival). Sin ella, una pelota que ya rebotó pero todavía está dentro del rectángulo de la paleta volvería a invertir su velocidad en el siguiente fotograma y se quedaría pegada. Con esta condición, solo invertimos la velocidad si la pelota se está acercando a la paleta, no si ya se está alejando.</p>\n<p>El segundo detalle es más bonito y es lo que separa un Pong jugable de un Pong soso. Cuando la pelota golpea la paleta, no se limita a rebotar: cambiamos también su velocidad vertical en función de qué parte de la paleta ha tocado. Si la pelota golpea cerca del centro, sale casi horizontal. Si golpea cerca del extremo superior, sale hacia arriba. Si golpea cerca del extremo inferior, sale hacia abajo. Esto le da al jugador control real sobre la trayectoria de la pelota, y convierte el juego en algo estratégico en lugar de en una sucesión de rebotes aburridos. La constante 0.15 controla cuánto influye el punto de impacto: con valores más altos la pelota gira mucho, con valores más bajos casi nada.</p>\n<h2>Detectar puntos y reiniciar pelota</h2>\n<p>Si la pelota se sale por la izquierda, el rival ha marcado un punto. Si se sale por la derecha, el punto es para el jugador. En ambos casos sumamos el punto correspondiente y devolvemos la pelota al centro con una velocidad inicial.</p>\n<pre><code class=\"language-javascript\">function comprobarPuntos() {\n\tif (pelota.x + TAMANO_PELOTA &lt; 0) {\n\t\tpuntosRival++;\n\t\treiniciarPelota(1);\n\t} else if (pelota.x &gt; ANCHO) {\n\t\tpuntosJugador++;\n\t\treiniciarPelota(-1);\n\t}\n}\n\nfunction reiniciarPelota(direccion) {\n\tpelota.x = ANCHO / 2;\n\tpelota.y = ALTO / 2;\n\tpelota.vx = 4 * direccion;\n\tpelota.vy = (Math.random() - 0.5) * 6;\n}\n</code></pre>\n<p>El parámetro <code>direccion</code> controla hacia qué lado sale la pelota tras cada punto. La idea es que la pelota salga hacia el jugador que acaba de recibir el punto, dándole una pequeña ventaja para reaccionar. La velocidad vertical inicial es aleatoria pero con un rango limitado, para que cada saque sea ligeramente distinto y el juego no se vuelva monótono.</p>\n<h2>La paleta del jugador</h2>\n<p>El jugador controla su paleta con las flechas arriba y abajo. La forma más sencilla es escuchar los eventos de teclado y guardar qué teclas están actualmente pulsadas, para luego mover la paleta en consecuencia en cada fotograma.</p>\n<pre><code class=\"language-javascript\">const teclas = { arriba: false, abajo: false };\n\ndocument.addEventListener(&quot;keydown&quot;, (e) =&gt; {\n\tif (e.key === &quot;ArrowUp&quot; || e.key === &quot;w&quot; || e.key === &quot;W&quot;)\n\t\tteclas.arriba = true;\n\tif (e.key === &quot;ArrowDown&quot; || e.key === &quot;s&quot; || e.key === &quot;S&quot;)\n\t\tteclas.abajo = true;\n});\n\ndocument.addEventListener(&quot;keyup&quot;, (e) =&gt; {\n\tif (e.key === &quot;ArrowUp&quot; || e.key === &quot;w&quot; || e.key === &quot;W&quot;)\n\t\tteclas.arriba = false;\n\tif (e.key === &quot;ArrowDown&quot; || e.key === &quot;s&quot; || e.key === &quot;S&quot;)\n\t\tteclas.abajo = false;\n});\n\nfunction moverJugador() {\n\tconst VELOCIDAD = 6;\n\tif (teclas.arriba) paletaJugador.y -= VELOCIDAD;\n\tif (teclas.abajo) paletaJugador.y += VELOCIDAD;\n\tpaletaJugador.y = Math.max(0, Math.min(ALTO - ALTO_PALETA, paletaJugador.y));\n}\n</code></pre>\n<p>Este patrón de escuchar <code>keydown</code> y <code>keyup</code> para mantener un estado del teclado es la forma estándar de gestionar movimiento continuo en juegos. Si en lugar de eso movieras la paleta solo dentro del manejador de <code>keydown</code>, el navegador te daría un primer movimiento, una pausa de medio segundo y luego empezaría a repetir, que es como funcionan los teclados cuando escribes texto. Para un juego eso es horrible. Con el patrón de arriba, en cada fotograma comprobamos si la tecla está pulsada y movemos en consecuencia, dando una respuesta inmediata y constante.</p>\n<p>La última línea del bloque encaja la posición de la paleta dentro de los límites del canvas. Sin esa línea, el jugador podría sacar su paleta fuera de la pantalla, lo cual no tiene sentido.</p>\n<h2>La paleta del rival: una IA muy básica</h2>\n<p>El rival necesita moverse solo. Hay muchas formas de programar una IA para Pong, desde la más sencilla (seguir la pelota verticalmente) hasta sistemas que predicen dónde va a llegar la pelota teniendo en cuenta los rebotes futuros. Para este tutorial vamos a hacer la versión sencilla, pero con un par de detalles que la hacen jugable.</p>\n<pre><code class=\"language-javascript\">function moverRival() {\n\tconst VELOCIDAD = 4.5;\n\tconst centroPaleta = paletaRival.y + ALTO_PALETA / 2;\n\tconst objetivo = pelota.y + TAMANO_PELOTA / 2;\n\tif (Math.abs(centroPaleta - objetivo) &gt; 10) {\n\t\tif (centroPaleta &lt; objetivo) paletaRival.y += VELOCIDAD;\n\t\telse paletaRival.y -= VELOCIDAD;\n\t}\n\tpaletaRival.y = Math.max(0, Math.min(ALTO - ALTO_PALETA, paletaRival.y));\n}\n</code></pre>\n<p>La paleta del rival intenta alinear su centro con la altura de la pelota. La velocidad es ligeramente menor que la del jugador (4.5 frente a 6), para que el rival no sea perfecto y el jugador pueda ganarle si es lo suficientemente hábil. Y solo se mueve si la diferencia entre el centro de la paleta y la pelota es mayor que diez píxeles. Esto evita que la paleta vibre constantemente cuando la pelota está casi alineada con su centro, que sería antiestético y haría a la IA fácil de leer.</p>\n<p>Si quieres un rival más difícil, sube la velocidad. Si quieres uno más fácil, bájala o reduce la zona muerta de diez píxeles. Si quieres uno realmente espectacular, tendrías que predecir la trayectoria de la pelota teniendo en cuenta los rebotes futuros contra los bordes, pero eso ya es harina de otro tutorial.</p>\n<h2>Dibujar todo</h2>\n<p>La función de dibujado limpia el canvas y vuelve a pintar los tres elementos del juego: la línea central, las dos paletas y la pelota.</p>\n<pre><code class=\"language-javascript\">const lienzo = document.getElementById(&quot;lienzo&quot;);\nconst ctx = lienzo.getContext(&quot;2d&quot;);\n\nfunction dibujar() {\n\tctx.fillStyle = &quot;#000&quot;;\n\tctx.fillRect(0, 0, ANCHO, ALTO);\n\n\tctx.fillStyle = &quot;#444&quot;;\n\tfor (let y = 0; y &lt; ALTO; y += 20) {\n\t\tctx.fillRect(ANCHO / 2 - 1, y, 2, 10);\n\t}\n\n\tctx.fillStyle = &quot;#fff&quot;;\n\tctx.fillRect(0, paletaJugador.y, ANCHO_PALETA, ALTO_PALETA);\n\tctx.fillRect(ANCHO - ANCHO_PALETA, paletaRival.y, ANCHO_PALETA, ALTO_PALETA);\n\tctx.fillRect(pelota.x, pelota.y, TAMANO_PELOTA, TAMANO_PELOTA);\n}\n</code></pre>\n<p>La línea central es un detalle estético clásico de Pong: una serie de pequeños rectángulos verticales que dividen visualmente el campo en dos mitades. No tiene función jugable, pero sin ella el juego se ve raro. Es uno de esos elementos heredados del original que conviene respetar.</p>\n<h2>El bucle principal</h2>\n<p>A diferencia de Snake o 2048, Pong tiene movimiento continuo, así que el bucle del juego se ejecuta a sesenta fotogramas por segundo. Para esto la mejor herramienta es <code>requestAnimationFrame</code>, que sincroniza el bucle con la frecuencia de refresco de la pantalla y se pausa automáticamente si el usuario cambia de pestaña.</p>\n<pre><code class=\"language-javascript\">function bucle() {\n\tmoverJugador();\n\tmoverRival();\n\tactualizarPelota();\n\tcomprobarColisiones();\n\tcomprobarPuntos();\n\tdibujar();\n\tdocument.getElementById(&quot;puntos-jugador&quot;).textContent = puntosJugador;\n\tdocument.getElementById(&quot;puntos-rival&quot;).textContent = puntosRival;\n\trequestAnimationFrame(bucle);\n}\n\nbucle();\n</code></pre>\n<p>Este patrón —cada fotograma actualiza la lógica y luego dibuja— es la estructura estándar de cualquier juego con movimiento continuo. Dentro del bucle cabe la complejidad que quieras, pero la estructura general no cambia. Si vas a programar más juegos en el futuro, te conviene memorizar esta estructura porque la vas a usar muchas veces.</p>\n<h2>Cosas que se pueden añadir</h2>\n<p>A partir de este esqueleto, las posibilidades son muchas. Un sonido de &quot;pong&quot; cada vez que la pelota golpea una paleta, usando la Web Audio API. Una pantalla de fin de partida cuando alguno llegue a once puntos. Un modo de dos jugadores humanos en el mismo teclado, donde uno controla la paleta izquierda con W y S y el otro la paleta derecha con las flechas. Una IA mejor que prediga dónde va a llegar la pelota. Aceleración progresiva: que la pelota vaya un poquito más rápido después de cada rebote, hasta que el juego se vuelva imposible. Soporte táctil para móvil, donde la paleta del jugador siga el dedo arrastrado por la pantalla.</p>\n<p>La gracia de Pong es que sigue siendo divertido incluso en su versión más mínima. Cualquier mejora que le añadas es accesoria. El núcleo del juego —dos paletas, una pelota, rebotes— ya lo hemos terminado, y cabe en menos de cien líneas de código si lo escribes apretado.</p>\n<h2>El prototipo funcional</h2>\n<p>Aquí abajo dejo el juego completo y funcionando. Mismo código del tutorial pero un poco más cuidado, con marcador grande, soporte para WASD además de las flechas, control táctil para móvil (mueve el dedo arriba y abajo en la mitad izquierda del tablero) y una zona de marcadores al estilo retro. Está todo aislado bajo un id propio para que no afecte al resto del blog.</p>\n<p><strong>Otros tutoriales de la serie</strong>: <a href=\"https://paigar.eu/juego-2048-tutorial/\">2048</a> · <a href=\"https://paigar.eu/juego-serpiente-tutorial/\">Serpiente</a> · <a href=\"https://paigar.eu/juego-parejas-tutorial/\">Parejas</a> · <a href=\"https://paigar.eu/juego-ladrillos-tutorial/\">Ladrillos</a>.</p>\n","date_published":"2026-01-06T00:00:00.000Z","image":"https://paigar.eu/juego-pong.png"},{"id":"https://paigar.eu/tom-of-finland/","url":"https://paigar.eu/tom-of-finland/","title":"Tom of Finland: de la clandestinidad al sello postal","language":"es","content_html":"<p>Amsterdam en otoño de 1999 tenía ese aire de ciudad que ya sabía perfectamente lo que era sin necesidad de explicárselo a nadie. Yo llevaba unos meses viviendo en Eindhoven, a poco más de una hora al sur, y escaparme al norte cada vez que podía era una forma de compensar la escala más contenida de mi ciudad de adopción. En aquella época internet existía, sí, pero de aquella manera: conexiones de módem, páginas que tardaban siglos en cargar y un acceso a contenido de cualquier tipo que, comparado con lo de ahora, era prácticamente medieval. Encontrar según qué cosas requería moverse, buscar, entrar en sitios físicos. Las tiendas especializadas todavía tenían sentido de una forma que hoy cuesta imaginar.</p>\n<p>Fue en una de esas tiendas, de temática gay, en algún rincón del centro de Amsterdam que ya no sabría localizar con precisión, donde cogí por primera vez un libro de ilustraciones de Tom of Finland. Lo hojeé de pie, entre estanterías, con esa mezcla de curiosidad y concentración que se reserva para los descubrimientos inesperados. Y lo compré.</p>\n<h2>Un nombre para algo que quizás ya había visto sin saberlo</h2>\n<p>El nombre en sí, Tom of Finland, tiene esa cadencia que suena a seudónimo con historia, y lo es. Detrás estaba Touko Valio Laaksonen, un finlandés nacido en 1920 que empezó a dibujar durante y después de la Segunda Guerra Mundial, en un país donde la homosexualidad era todavía ilegal. Sus ilustraciones circulaban de forma semiclandestina antes de que la revista americana Physique Pictorial las publicara a finales de los años cincuenta y le diera proyección internacional. Firmaba como Tom of Finland desde entonces.</p>\n<p>Es posible que antes de aquel librito de Amsterdam yo ya hubiera visto alguna imagen suya sin relacionarla con un autor concreto. Su estética es tan reconocible, tan absorbida por la cultura visual gay del siglo XX, que resulta casi imposible haber llegado a cierta edad sin haberse cruzado con algo que bebiese de ella. Pero ese día en Amsterdam fue la primera vez que puse nombre y firma a todo aquello.</p>\n<h2>Lo que te golpea no es solo el erotismo</h2>\n<p>Sería deshonesto no mencionar que la obra de Tom of Finland es explícitamente sexual. Sus ilustraciones son eróticas sin ambigüedad y sin disculpa, y eso forma parte de lo que son y de lo que significan. Pero reducirlas a eso sería quedarse en la superficie, porque lo que a mí me paró los pies fue otra cosa: la estética. La coherencia visual brutal de un lenguaje que él inventó y perfeccionó durante décadas.</p>\n<p>Los cuerpos en sus dibujos son imposibles en la escala habitual de lo posible. Hombros que no caben en una puerta, cinturas que contrastan con torsos de proporciones hercúleas, mandíbulas cuadradas, uniformes tensos hasta el límite de lo que permite la tela. Todo exagerado de forma deliberada y sistemática, como si la hiperrealidad fuera la única manera honesta de representar el deseo. Hay algo en esa exageración que no es grotesco sino celebratorio, una suerte de himno dibujado a una masculinidad que en la época en que empezó a hacerlo no tenía casi ningún otro espacio de representación positiva.</p>\n<p>La línea es limpia, segura, sin titubeos. Los escenarios son esquemáticos, apenas esbozados, porque lo que importa es la figura. Y las figuras tienen siempre esa cualidad extraña de parecer a la vez idealizadas y completamente reales en su energía, como si captaran algo verdadero sobre la forma en que el deseo construye sus objetos.</p>\n<h2>Una influencia que está en todas partes aunque no siempre se cite</h2>\n<p>Poco después de aquel descubrimiento empecé a ver su huella en sitios donde antes no la habría reconocido. La estética del cuero gay, que él no inventó del todo pero sí codificó visualmente con más fuerza que nadie. La forma en que ciertas subculturas construyeron su iconografía en las décadas siguientes. El modo en que la masculinidad gay encontró un lenguaje propio para representarse a sí misma con potencia en lugar de con vergüenza.</p>\n<p>Su impacto trascendió con el tiempo las comunidades para las que originalmente dibujó. Las ilustraciones de Tom of Finland llegaron al mundo del diseño, de la moda, del arte contemporáneo. Finlandia, el país que en sus años de formación criminalizaba su identidad, terminó emitiéndole sellos postales en 1992, el año de su muerte. La Tom of Finland Foundation en Los Ángeles —donde pasó parte de sus últimos años— preserva su archivo. Sus obras están en colecciones de museos como el MoMA. El arco completo de ese recorrido, de la clandestinidad al sello postal, dice bastante sobre cómo cambia la historia cuando alguien decide representar lo que quiere representar sin pedir permiso.</p>\n<h2>El librito y el libro grande</h2>\n<p>Aquel primer libro que compré en Amsterdam es pequeño, manejable, el tipo de edición que cabe en la mochila y que tiene las páginas un poco amarillentas ya. Lo conservo. Tiene esa calidad de los objetos que acumulan significado no por su valor material sino por lo que representan en el momento en que llegaron a tus manos.</p>\n<p>Con el tiempo compré algún otro libro suyo, incluyendo una edición en gran formato que le hace mucha más justicia a su trabajo. Cuando tienes las ilustraciones a ese tamaño, con esa presencia en el papel, la calidad del dibujo se impone de otra manera. Los detalles que en una edición pequeña se intuyen, en el formato grande se hacen evidentes: la precisión del trazo, el control sobre la luz y la sombra, la composición de cada imagen como un objeto autónomo.</p>\n<p>No sé si en 1999, con el internet que había, habría llegado a él de otra forma. Probablemente no con esa misma inmediatez del objeto físico entre las manos, en una tienda de Amsterdam, en un otoño que ahora tiene más de veinticinco años. Que para conocer ciertas cosas hubiera que ir a buscarlas tiene algo que no me parece del todo mal en retrospectiva.</p>\n<p>Si aún no conoces su obra, o la has descartado de un vistazo, te animo a buscarla con curiosidad y sin prevenciones. No hace falta que todo te resulte cómodo para reconocer que estás ante algo genuinamente extraordinario. Laaksonen dibujó lo que quiso durante décadas, sin pedir permiso ni disculpas, y eso se ve en cada trazo. Vale la pena tomarse el tiempo de verlo.</p>\n","date_published":"2025-12-22T00:00:00.000Z","image":"https://paigar.eu/tom-of-finland.png"}]}