<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" version="2.0">
  <channel>
    <title>PAIGAR</title>
    <link>https://paigar.eu/</link>
    <atom:link href="https://paigar.eu/feed.xml" rel="self" type="application/rss+xml"/>
    <description>Portal de Apuntes, Ideas, Garabatos, Artilugios y Retrofuturismo</description>
    <lastBuildDate>Tue, 12 May 2026 08:55:57 GMT</lastBuildDate>
    <language>es</language>
    <generator>Lume v3.2.4</generator>
    <item>
      <title>Cuando el gusto personal choca con la accesibilidad web</title>
      <link>https://paigar.eu/accesibilidad-paleta-pastel/</link>
      <guid isPermaLink="false">https://paigar.eu/accesibilidad-paleta-pastel/</guid>
      <description>
        Una paleta de colores preciosa, una normativa de accesibilidad implacable y la conversación incómoda que tarde o temprano hay que tener con el cliente.
      </description>
      <content:encoded>
        <![CDATA[<p>Un terracota casi invisible, un verde que susurra y un gris que apenas se distingue del blanco. Bonito, sí. Accesible, no.</p>
<h2>La clienta y su paleta de acuarela</h2>
<p>Algunos proyectos llegan con una identidad visual ya bien formada, y este era uno de ellos. Mi clienta tiene buen ojo, personalidad clara y gustos muy definidos. Llegó con su logotipo bajo el brazo y una paleta cromática que lo decía todo sobre ella: tierra, crema, verde salvia, un terracota casi imperceptible. Colores suaves como una mañana de domingo. Nada estridentes, nada agresivos. Una estética coherente y honesta que, desde el punto de vista puramente visual, tiene todo el sentido del mundo.</p>
<p>Su idea para la web era clara: fondo blanco, textos en gris suave —porque el negro le resultaba demasiado agresivo—, acentos en crema y esas pinceladas de verde y terracota que definen su marca. Una página tranquila, sin ruido, que transmitiera exactamente lo que ella quería transmitir.</p>
<p>Y desde el punto de vista estético, ¿quién soy yo para llevarle la contraria?</p>
<h2>El gusto es el gusto, y no hay vuelta de hoja</h2>
<p>Podría pensar —y lo pienso— que una paleta tan desaturada corre el riesgo de resultar sosa. Que le falta tensión visual, que los elementos importantes se pierden, que el ojo del usuario no sabe muy bien dónde mirar. Pero eso es mi opinión, y mi opinión no paga la factura ni toma las decisiones.</p>
<p>El gusto personal no se discute. Podemos educar, podemos proponer, podemos mostrar alternativas. Pero si una clienta llega con una identidad consolidada que le funciona, que le representa y que le gusta, nuestra labor no es convencerla de que se equivoca, sino entender qué necesita y ayudarla a trasladarlo al entorno digital de la mejor manera posible.</p>
<p>El problema, claro, es que a veces lo que nos llega tiene que pasar por ciertos filtros antes de llegar a la pantalla del usuario final. Y uno de esos filtros, en 2026, ya no es opcional.</p>
<h2>La accesibilidad web no es una opinión</h2>
<p>Aquí es donde la conversación se complica. Porque lo que hasta hace poco era una recomendación —las pautas WCAG de accesibilidad web— ha pasado a ser una obligación legal en Europa a través de la European Accessibility Act (EAA), cuya aplicación efectiva llegó en 2025.</p>
<p>Y las WCAG tienen algo muy claro que decir sobre el color: el contraste entre texto y fondo debe superar una ratio de 4,5:1 para texto normal y de 3:1 para texto grande. Esto no es una preferencia estética. Es un número. Un número que se puede medir, calcular y verificar con herramientas en cuestión de segundos.</p>
<p>Cuando coges esa paleta tan delicada —el gris clarito sobre blanco, el verde suave sobre crema, el terracota ligerísimo sobre cualquier cosa— y la metes en un comprobador de contraste, el resultado es siempre el mismo: <em>fail</em>, <em>fail</em>, <em>fail</em>. El gris que no quería perturbar perturba precisamente a quienes más necesitan que los textos sean legibles: personas con baja visión, usuarios mayores, quienes leen en condiciones de iluminación adversas. La delicadeza visual se convierte, sin quererlo, en una barrera de acceso.</p>
<h2>La conversación que nadie quiere tener</h2>
<p>Explicar esto a alguien que no quiere tocar su paleta es uno de los momentos más delicados de este trabajo. No se trata de decirle que sus colores son feos —que no lo son— ni de que su criterio estético está equivocado —que no lo está—. Se trata de explicarle que hay una normativa que protege a una parte de los usuarios, y que ignorarla no es una opción que esté sobre la mesa.</p>
<p>La reacción más habitual es la resistencia. &quot;Pero es que así no parece mi marca.&quot; &quot;Si oscurezco el texto pierde la sutileza.&quot; &quot;Es que el negro sobre blanco es muy agresivo.&quot; Y uno entiende esas reacciones, porque vienen de un lugar genuino. Nadie quiere que le digan que lo que ha construido con tanto cuidado tiene un problema.</p>
<p>La clave está en no presentarlo como una crítica al gusto ajeno, sino como una restricción técnica y legal, del mismo tipo que las que aplican al etiquetado de productos o a la señalización de emergencias. La ley no opina sobre la estética de una señal de salida de incendios, pero sí exige que sea visible. En la web pasa exactamente lo mismo, y el argumento suele aterrizar mejor cuando se plantea así: no es que sus colores estén mal, es que la norma no distingue entre bonito y funcional.</p>
<h2>Compromisos que no deberían serlo</h2>
<p>La buena noticia es que casi siempre existe margen de maniobra. El truco está en encontrar versiones de los colores elegidos que, con ajustes mínimos de luminosidad o saturación, pasen el umbral de contraste sin traicionar el espíritu de la paleta original.</p>
<p>Un terracota algo más intenso, pero todavía cálido. Un verde un poco más oscuro, que siga siendo vegetal y tranquilo. Un gris de texto que, en lugar de rozar el blanco, se sitúe en una zona que garantice legibilidad sin necesidad de llegar al negro puro. Son cambios que muchas veces resultan imperceptibles para quien no tiene el ojo entrenado, pero que marcan la diferencia entre una web accesible y una que incumple la normativa.</p>
<p>La clave es trabajar con la clienta en esos ajustes, no imponérselos. Mostrarle los dos colores juntos, explicarle qué cambia y qué no. A veces, cuando ve que el verde &quot;accesible&quot; sigue siendo verde —solo un par de puntos más oscuro en la escala HSL—, la resistencia se diluye. Otras veces hay que llegar a acuerdos más estructurados: reservar los colores delicados para elementos puramente decorativos —ilustraciones, fondos de sección, separadores— y usar las versiones con mayor contraste solo donde hay texto. Es más trabajo, pero es una solución honesta para ambas partes.</p>
<h2>Lo que nos toca a nosotros</h2>
<p>Al final, la accesibilidad web no es un extra que se añade si al cliente le apetece. Es parte de lo que significa hacer bien nuestro trabajo. Y eso incluye la parte incómoda: decirle a alguien, con toda la delicadeza del mundo, que hay algo en su propuesta que necesita ajustarse.</p>
<p>No es un juicio sobre el gusto ajeno. Es una responsabilidad compartida. La nuestra, como profesionales, es conocer la normativa y aplicarla. La del cliente, como propietario de un sitio web, es tener un espacio que no excluya a nadie.</p>
<p>Ese verde tan suave puede seguir siendo suyo. Solo necesita ser, también, de todos.</p>
]]>
      </content:encoded>
      <pubDate>Wed, 06 May 2026 00:00:00 GMT</pubDate>
      <meta property="og:image" content="https://paigar.eu/accesibilidad-paleta-pastel.jpg"/>
    </item>
    <item>
      <title>Leyendecker: un genio escondido en plain sight</title>
      <link>https://paigar.eu/leyendecker/</link>
      <guid isPermaLink="false">https://paigar.eu/leyendecker/</guid>
      <description>
        Hace poco descubrí a Joseph Christian Leyendecker. Hizo más portadas del Saturday Evening Post que Norman Rockwell, inventó el bebé de Año Nuevo y el Arrow Collar Man. Y su nombre lleva décadas enterrado.
      </description>
      <content:encoded>
        <![CDATA[<p>Hace poco descubrí a Joseph Christian Leyendecker. Alguien lo nombró, fui a buscar su obra en internet, y pasé un buen rato mirando ilustraciones con esa atención sostenida que solo se le presta a lo que realmente te detiene.</p>
<figure><div class="lqip-wrap" style="background-image:url('https://imagenes.paigar.es/paigar/leyendecker-01-16.jpg')"><picture><source type="image/avif" srcset="https://imagenes.paigar.es/paigar/leyendecker-01-480.avif 480w, https://imagenes.paigar.es/paigar/leyendecker-01-800.avif 800w" sizes="(max-width: 600px) 100vw, 720px"><source type="image/webp" srcset="https://imagenes.paigar.es/paigar/leyendecker-01-480.webp 480w, https://imagenes.paigar.es/paigar/leyendecker-01-800.webp 800w" sizes="(max-width: 600px) 100vw, 720px"><img src="https://imagenes.paigar.es/paigar/leyendecker-01-800.jpg" srcset="https://imagenes.paigar.es/paigar/leyendecker-01-480.jpg 480w, https://imagenes.paigar.es/paigar/leyendecker-01-800.jpg 800w" sizes="(max-width: 600px) 100vw, 720px" alt="Ilustración de J.C. Leyendecker: figura masculina elegante con postura escultórica, pincelada suelta y paleta cálida" loading="lazy" width="800" height="533" onload="this.closest('.lqip-wrap').classList.add('loaded')"></picture></div><figcaption>J.C. Leyendecker</figcaption></figure>
<h2>El hombre que hizo más portadas que Rockwell</h2>
<p>Leyendecker nació en Alemania en 1874 y emigró a Estados Unidos de niño. Se formó en Chicago y París, y a principios del siglo XX ya era el ilustrador comercial más solicitado del país. Hizo más de cuatrocientas portadas del <em>Saturday Evening Post</em>, la revista de mayor tirada de América en aquella época. Más que Norman Rockwell, que vivía cerca de él en parte precisamente para aprender de su trabajo y que reconoció abiertamente su influencia.</p>
<p>También fue Leyendecker quien fijó visualmente algunas de las imágenes más arraigadas en el imaginario americano: el bebé que representa el año nuevo sustituyendo al anciano del año anterior, las escenas navideñas, las estampas de Acción de Gracias. Imágenes que han sido reproducidas y reinterpretadas tantas veces que ya nadie recuerda quién las inventó. Esa es una forma peculiar de inmortalidad — que tu obra sobreviva completamente desvinculada de tu nombre.</p>
<figure><div class="lqip-wrap" style="background-image:url('https://imagenes.paigar.es/paigar/leyendecker-02-16.jpg')"><picture><source type="image/avif" srcset="https://imagenes.paigar.es/paigar/leyendecker-02-480.avif 480w, https://imagenes.paigar.es/paigar/leyendecker-02-800.avif 800w" sizes="(max-width: 600px) 100vw, 720px"><source type="image/webp" srcset="https://imagenes.paigar.es/paigar/leyendecker-02-480.webp 480w, https://imagenes.paigar.es/paigar/leyendecker-02-800.webp 800w" sizes="(max-width: 600px) 100vw, 720px"><img src="https://imagenes.paigar.es/paigar/leyendecker-02-800.jpg" srcset="https://imagenes.paigar.es/paigar/leyendecker-02-480.jpg 480w, https://imagenes.paigar.es/paigar/leyendecker-02-800.jpg 800w" sizes="(max-width: 600px) 100vw, 720px" alt="Portada de J.C. Leyendecker para el Saturday Evening Post: escena de la vida americana con esa presencia visual que convirtió la revista en fenómeno de masas" loading="lazy" width="800" height="533" onload="this.closest('.lqip-wrap').classList.add('loaded')"></picture></div><figcaption>J.C. Leyendecker</figcaption></figure>
<h2>El Arrow Collar Man y el amor escondido en plain sight</h2>
<p>La campaña que lo hizo más famoso en su época fue la del Arrow Collar Man, una serie de ilustraciones publicitarias para una marca de camisas que se convirtieron en fenómeno cultural. El personaje — apuesto, elegante, con esa mandíbula perfecta y esa postura de quien sabe exactamente dónde está — generó una respuesta que hoy llamaríamos viral. Las mujeres americanas enviaban cartas de amor a un hombre de papel. La empresa recibía más correspondencia dirigida al Arrow Collar Man que muchos actores de carne y hueso.</p>
<figure><div class="lqip-wrap" style="background-image:url('https://imagenes.paigar.es/paigar/leyendecker-03-16.jpg')"><picture><source type="image/avif" srcset="https://imagenes.paigar.es/paigar/leyendecker-03-480.avif 480w, https://imagenes.paigar.es/paigar/leyendecker-03-800.avif 800w" sizes="(max-width: 600px) 100vw, 720px"><source type="image/webp" srcset="https://imagenes.paigar.es/paigar/leyendecker-03-480.webp 480w, https://imagenes.paigar.es/paigar/leyendecker-03-800.webp 800w" sizes="(max-width: 600px) 100vw, 720px"><img src="https://imagenes.paigar.es/paigar/leyendecker-03-800.jpg" srcset="https://imagenes.paigar.es/paigar/leyendecker-03-480.jpg 480w, https://imagenes.paigar.es/paigar/leyendecker-03-800.jpg 800w" sizes="(max-width: 600px) 100vw, 720px" alt="Ilustración publicitaria de J.C. Leyendecker para Arrow Collar: figura masculina apuesta y elegante que generó una respuesta cultural sin precedentes en la publicidad americana" loading="lazy" width="800" height="533" onload="this.closest('.lqip-wrap').classList.add('loaded')"></picture></div><figcaption>J.C. Leyendecker</figcaption></figure>
<p>Lo que el público no sabía, y que tardó décadas en documentarse, es que el modelo era Charles Beach, la pareja de Leyendecker durante casi cincuenta años. Vivían juntos, viajaban juntos, Beach gestionaba su carrera y sus finanzas. En la América de principios del siglo XX eso era un secreto a voces que nadie nombraba en voz alta. Leyendecker llevó su vida entera escondida en plain sight: el hombre al que amaba era literalmente la cara más reconocida de la publicidad americana, y nadie hacía la pregunta obvia.</p>
<figure><div class="lqip-wrap" style="background-image:url('https://imagenes.paigar.es/paigar/leyendecker-04-16.jpg')"><picture><source type="image/avif" srcset="https://imagenes.paigar.es/paigar/leyendecker-04-480.avif 480w, https://imagenes.paigar.es/paigar/leyendecker-04-800.avif 800w" sizes="(max-width: 600px) 100vw, 720px"><source type="image/webp" srcset="https://imagenes.paigar.es/paigar/leyendecker-04-480.webp 480w, https://imagenes.paigar.es/paigar/leyendecker-04-800.webp 800w" sizes="(max-width: 600px) 100vw, 720px"><img src="https://imagenes.paigar.es/paigar/leyendecker-04-800.jpg" srcset="https://imagenes.paigar.es/paigar/leyendecker-04-480.jpg 480w, https://imagenes.paigar.es/paigar/leyendecker-04-800.jpg 800w" sizes="(max-width: 600px) 100vw, 720px" alt="Ilustración de J.C. Leyendecker del Arrow Collar Man: el modelo era Charles Beach, su pareja durante casi cincuenta años, retratado con una atención y admiración que va más allá del encargo comercial" loading="lazy" width="800" height="533" onload="this.closest('.lqip-wrap').classList.add('loaded')"></picture></div><figcaption>J.C. Leyendecker</figcaption></figure>
<p>Cuando ves las ilustraciones del Arrow Collar Man sabiendo esto, algo cambia en la forma de mirarlas. Hay una calidad en cómo están dibujados esos hombres — una atención, una admiración contenida en cada trazo — que va más allá del encargo comercial.</p>
<h2>Lo que te para los pies</h2>
<p>Pero lo que más me llamó la atención al descubrir su obra no fue el subtexto biográfico sino algo más inmediato: la elegancia. No en el sentido de la ropa o los escenarios, aunque ambos la tienen, sino en algo más difícil de definir — las posturas corporales, la forma en que los cuerpos ocupan el espacio, la seguridad del trazo.</p>
<p>Los personajes de Leyendecker tienen una calidad escultórica que hace pensar más en una estatua bien iluminada que en una ilustración comercial. Los hombros, los giros de cabeza, el peso de un cuerpo apoyado en algo — todo tiene una presencia física que es inusual en el trabajo de encargo. La pincelada es suelta pero precisa, con esa paradoja técnica de los grandes dibujantes que consiguen que algo muy trabajado parezca fácil.</p>
<figure><div class="lqip-wrap" style="background-image:url('https://imagenes.paigar.es/paigar/leyendecker-05-16.jpg')"><picture><source type="image/avif" srcset="https://imagenes.paigar.es/paigar/leyendecker-05-480.avif 480w, https://imagenes.paigar.es/paigar/leyendecker-05-800.avif 800w" sizes="(max-width: 600px) 100vw, 720px"><source type="image/webp" srcset="https://imagenes.paigar.es/paigar/leyendecker-05-480.webp 480w, https://imagenes.paigar.es/paigar/leyendecker-05-800.webp 800w" sizes="(max-width: 600px) 100vw, 720px"><img src="https://imagenes.paigar.es/paigar/leyendecker-05-800.jpg" srcset="https://imagenes.paigar.es/paigar/leyendecker-05-480.jpg 480w, https://imagenes.paigar.es/paigar/leyendecker-05-800.jpg 800w" sizes="(max-width: 600px) 100vw, 720px" alt="Ilustración de J.C. Leyendecker: detalle que muestra la calidad escultórica de sus figuras, con pincelada suelta pero precisa y una presencia física inusual en el trabajo comercial" loading="lazy" width="800" height="533" onload="this.closest('.lqip-wrap').classList.add('loaded')"></picture></div><figcaption>J.C. Leyendecker</figcaption></figure>
<p>Es un estilo totalmente distinto al de <a href="https://paigar.eu/tom-of-finland/">Tom of Finland</a>, con quien comparte sin embargo esa misma cualidad de ser inconfundible desde el primer vistazo. Donde Laaksonen era línea pura, contraste radical, exageración deliberada, Leyendecker es pintura, volumen, luz cálida, contención. Dos formas completamente distintas de mirar al cuerpo masculino, dos lenguajes visuales sin ningún punto de contacto formal, y los dos igual de marcados.</p>
<h2>El eclipse y la reivindicación tardía</h2>
<p>Murió en 1951 en circunstancias económicas muy distintas a las de su apogeo. Charles Beach había muerto unos años antes, y sin él Leyendecker perdió buena parte de su capacidad de gestionar una carrera que ya llevaba tiempo en declive. Rockwell, que le debía tanto, se había convertido en el nombre que todo el mundo conocía. Leyendecker quedó en un segundo plano del que tardó décadas en salir.</p>
<p>La reivindicación llegó despacio, impulsada en parte por el interés en recuperar la historia gay oculta en la cultura americana del siglo XX. Sus obras están ahora en colecciones importantes, su influencia se estudia, y el Arrow Collar Man ha pasado de anuncio de camisas a objeto de análisis cultural. Es el tipo de justicia póstuma que llega demasiado tarde para el interesado pero que al menos corrige el registro.</p>
<p>Lo descubrí hace poco, y la obra aguanta perfectamente el tiempo y la distancia. Eso es lo que importa de un artista — que cuando por fin lo encuentras, no te decepcione.</p>
<hr>
<p><em>Las ilustraciones reproducidas en este artículo son obra de J.C. Leyendecker (1874–1951).</em></p>
]]>
      </content:encoded>
      <pubDate>Tue, 05 May 2026 00:00:00 GMT</pubDate>
      <meta property="og:image" content="https://paigar.eu/leyendecker-00.jpg"/>
    </item>
    <item>
      <title>La patinadora entera no cabe</title>
      <link>https://paigar.eu/patinadora-encuadre-imposible/</link>
      <guid isPermaLink="false">https://paigar.eu/patinadora-encuadre-imposible/</guid>
      <description>
        Llevamos décadas con pantallas rectangulares y seguimos sin ponernos de acuerdo en qué dirección va el rectángulo. Una reflexión sobre proporciones, encuadres imposibles y la ceguera espacial que todos tenemos en algún sitio.
      </description>
      <content:encoded>
        <![CDATA[<p>Imagina que te pido que metas una sandía en un tarro de mermelada. No una sandía pequeña, no un tarro grande: una sandía normal y un tarro estándar de noventa y tantos gramos. Y que cuando te digo que no cabe, me miras con cara de no entender muy bien el problema. Eso, más o menos, es lo que vivo cada vez que un cliente me manda una foto en vertical para usarla como imagen de cabecera en su web.</p>
<p>He perdido la cuenta de las veces que ha pasado. Diseño una maquetación con una cabecera cinematográfica, proporciones 16:9 o incluso 21:9, esa franja ancha y baja que en pantalla tiene una presencia brutal. El cliente la aprueba. Le gusta. Le parece perfecta. Y entonces le pido la foto para esa cabecera y me llega un retrato vertical, con el sujeto centrado en un formato que mide más de alto que de ancho.</p>
<h2>El problema, explicado como si hubiera que explicarlo</h2>
<p>Una imagen horizontal necesita ser ancha y poco alta. Una imagen vertical es exactamente lo contrario. No son intercambiables, del mismo modo que una bufanda no es un cinturón aunque estén hechos del mismo tejido. Si quiero rellenar un espacio horizontal con una foto vertical, solo tengo dos opciones: añado franjas a los lados para completar el ancho que falta, o recorto la imagen hasta quedarme con el fragmento que sí encaja en el espacio disponible.</p>
<p>No hay tercera opción. Es geometría, no una decisión de diseño. El espacio tiene unas proporciones. La imagen tiene otras. Si no coinciden, algo tiene que ceder.</p>
<h2>La patinadora y sus tres versiones</h2>
<p>Hubo una vez una patinadora. El cliente tenía una escuela de patinaje sobre hielo y me mandó una foto magnífica de una de sus alumnas: vertical, bien iluminada, la chica en plena pirouette con los brazos extendidos y los patines brillando. Una foto preciosa. Completamente inútil para mi cabecera panorámica.</p>
<p>Hice lo que me parecía más lógico: recorté por el centro, me quedé con el torso y la cara, que era donde estaba la expresión, la energía, el momento. Presenté la propuesta. El cliente la miró y me dijo que lo importante era que se viera que era una patinadora, y que con ese recorte no quedaba claro. Argumento válido, no voy a negarlo. Me pidió que se vieran los patines.</p>
<p>Así que rehíce el recorte por el otro extremo. Patines, tobilleras, el arranque de las piernas, el suelo de hielo. Quedaba raro, sí, una cabecera de web que empezaba en las rodillas y terminaba en el suelo, sin cara, sin contexto, con un protagonismo inesperado del calzado. Presenté la propuesta. Al cliente no le gustó. Y entonces llegó la pregunta que ya sabía que iba a llegar: ¿y no se puede ver la patinadora entera?</p>
<p>No. No se puede ver la patinadora entera. Porque la patinadora entera no cabe.</p>
<h2>Lo que hoy haría en diez minutos</h2>
<p>El remate irónico de aquella historia es que hoy ese problema tiene solución, o al menos una salida razonablemente digna. Cualquier herramienta de generación de imagen con capacidad de outpainting —extender una foto más allá de sus bordes originales— podría rellenar los laterales de esa imagen vertical con hielo generado, con pista de fondo, con ambiente de pabellón. No sería perfecto, pero sería creíble. La patinadora entera cabría en la cabecera.</p>
<p>Hace cinco años eso era ciencia ficción. En aquel momento la única opción era buscar otra foto, convencer al cliente de un encuadre diferente, o rendirse y poner franjas. Hoy tenemos IA generativa que rellena los huecos que la física no permite. Lo cual es fascinante y un poco perturbador a partes iguales, porque significa que el problema que intento explicar desde hace años —que una foto vertical no puede cubrir un espacio horizontal sin perder información— está a punto de dejar de ser un problema técnico para convertirse en una decisión estética. Y no sé si eso me alegra o me entristece un poco.</p>
<h2>Mi propio tarro de mermelada</h2>
<p>Lo que me fascina, y llevo años intentando entenderlo, es que la incapacidad del cliente no es un problema de vocabulario. No es que no sepa qué es una relación de aspecto. Es que no consigue ver la incompatibilidad. Se sienta delante de una pantalla apaisada, mira una foto en retrato, y su cerebro no activa ninguna alarma. No hay colisión cognitiva. Para él, una foto es una foto y debería poder ir donde le digas que vaya.</p>
<p>Me pregunto si existe algún término en psicología para eso. Alguna variante de ceguera espacial aplicada a proporciones en pantalla. Porque la habilidad que falta es muy concreta: la misma que usas cuando intentas aparcar en un hueco y calibras si tu coche cabe o no cabe. La mayoría de la gente lo hace de forma automática. Pero trasladada a imágenes, esa automatización desaparece en un porcentaje de personas que, en mi experiencia, es más elevado de lo razonablemente esperable.</p>
<p>Y entonces, justo cuando estoy más enrocado en mi incomprensión ante la incomprensión del cliente, me acuerdo. Me acuerdo de que yo, con toda mi convicción sobre lo obvio que es que una foto vertical no cabe en un espacio horizontal, soy incapaz de distinguir mi derecha de mi izquierda de forma espontánea. Tengo que pensar. Tengo que buscar algún truco mental, alguna referencia física. Hay gente que nace sabiendo cuál es cuál y hay gente como yo, que a los cincuenta años todavía duda un segundo antes de señalar.</p>
<p>Claro, al resto de la gente le parece increíble que yo tenga ese problema. Le parece que debería ser evidente. Y yo les explico que lo sé, que claro que lo sé, pero que en el momento de la acción mi cerebro no lo procesa de forma automática. Y ellos asienten despacio, con esa expresión de educación y perplejidad que yo conozco tan bien desde el otro lado.</p>
<p>Así que al final parece que todos tenemos nuestra sandía y nuestro tarro de mermelada. Los míos solo están orientados en otra dirección.</p>
]]>
      </content:encoded>
      <pubDate>Sun, 03 May 2026 00:00:00 GMT</pubDate>
      <meta property="og:image" content="https://paigar.eu/patinadora-encuadre-imposible.jpg"/>
    </item>
    <item>
      <title>Flash Gordon, la película que el tiempo se tragó sin avisar</title>
      <link>https://paigar.eu/flash-gordon-1980/</link>
      <guid isPermaLink="false">https://paigar.eu/flash-gordon-1980/</guid>
      <description>
        En 1980 llegó a los cines una película con Queen, con Max von Sydow, con un universo visual absolutamente delirante. Y sin embargo, casi nadie la recuerda. Esto es una reivindicación, aunque no sé muy bien si tengo autoridad para hacerla.
      </description>
      <content:encoded>
        <![CDATA[<p>Si le preguntas a alguien de cuarenta y tantos años por Flash Gordon, la respuesta más habitual es un silencio breve seguido de &quot;¿la de Queen?&quot;. Y sí, es la de Queen. Pero también es mucho más que eso, al menos en mi opinión.</p>
<h2>La película que nadie recuerda bien</h2>
<p>Flash Gordon se estrenó en 1980. La dirigió Mike Hodges —que venía de hacer Get Carter, una película de culto del cine negro británico, lo cual en retrospectiva es una combinación bastante extraña— y la produjo Dino De Laurentiis, que era el tipo de productor al que le gustaba apostar fuerte y a veces le salía bien y a veces no.</p>
<p>El caso es que la película existe, tiene cuarenta y tantos años, y casi nadie la ha visto. No está en las listas de clásicos de los ochenta. No sale en los artículos de &quot;las mejores películas de ciencia ficción&quot;. Aparece de vez en cuando en algún hilo de nostalgia ochentera y poco más. Para una película con ese presupuesto, esa banda sonora y ese reparto, eso es bastante triste.</p>
<h2>Lo que te encuentras cuando por fin la pones</h2>
<p>La primera vez que la ves lo primero que piensas es que alguien se gastó una cantidad absurda de dinero en telas, pinturas y purpurina. Mongo, el planeta donde transcurre todo, es un universo de colores imposibles. Rojos, dorados, azules que no existen en la naturaleza. Los decorados parecen pintados a mano porque en muchos casos lo están, y tiene esa textura de teatro que hoy resulta rara pero también encantadora.</p>
<p>Y luego está el vestuario. Danilo Donati —que había trabajado con Fellini, con Zeffirelli— hizo los trajes, y se nota que se lo pasó en grande. Cada personaje lleva algo absolutamente ridículo y absolutamente perfecto al mismo tiempo. Los hombres halcón con sus alas, Ming con su túnica dorada y las cejas depiladas, Flash con su camiseta de los New York Jets que en algún momento decide que ya no necesita... es todo demasiado, en el mejor sentido.</p>
<h2>Max von Sydow haciendo de Max von Sydow</h2>
<p>Hay que hablar de Ming el Despiadado. O más concretamente, de Max von Sydow haciendo de Ming el Despiadado.</p>
<p>Von Sydow era en 1980 un actor de primerísimo nivel. Había trabajado con Bergman. Había jugado al ajedrez con la Muerte en El séptimo sello. Y aquí está, con la cabeza depilada, las cejas pintadas y una expresión de desprecio absoluto hacia toda la humanidad, completamente entregado al papel sin el menor asomo de ironía. No está haciendo el payaso. Está siendo Ming.</p>
<p>Siempre me ha parecido que un villano de cómic funciona o no dependiendo de si el actor que lo interpreta se lo cree de verdad. Von Sydow se lo cree absolutamente. Es lo mejor de la película, sin discusión.</p>
<h2>Sam J. Jones, o el héroe que no termina de arrancar</h2>
<p>Aquí hay que ser honesto. Flash Gordon es un quarterback de fútbol americano que acaba en el espacio prácticamente por accidente, y Sam J. Jones lo interpreta con una presencia física indudable y una profundidad emocional que... bueno. No es lo suyo.</p>
<p>Lo que hace más comprensible la cosa es que el rodaje fue un desastre en ese frente. Jones tuvo un conflicto con De Laurentiis, se fue antes de terminar, y parte de su doblaje lo hizo otra persona. Eso explica esa sensación extraña que tienes en algunas escenas de que el protagonista parece estar en una película diferente a la del resto del reparto.</p>
<p>No es el único protagonista del cine de los ochenta con ese problema, pero en Flash Gordon se nota más porque alrededor suyo todo el mundo está dándolo todo y él parece de visita.</p>
<h2>El problema de la banda sonora, que es demasiado buena</h2>
<p>Queen hizo la banda sonora. Esto todo el mundo lo sabe. Lo que quizás no se ha pensado mucho es que eso, siendo una cosa maravillosa, también es parte del problema.</p>
<p>Flash's Theme es un himno. The Hero es grandiosa. El disco entero es buenísimo y ha sobrevivido décadas perfectamente. El problema es que funciona sin la película. La música de Queen para Flash Gordon es tan buena por sí sola que se ha convertido en un objeto independiente que ya no necesita las imágenes que acompañaba.</p>
<p>Cuando alguien dice &quot;Flash Gordon&quot; y la primera reacción es tararear la canción, algo ha salido torcido en la relación entre la película y su banda sonora. No es culpa de Queen, obviamente. Pero el resultado es que la película ha quedado reducida a ser el contexto de la canción, en lugar de al revés. Y eso no le ha hecho ningún favor.</p>
<h2>El momento equivocado</h2>
<p>Flash Gordon llegó a los cines tres años y medio después de Star Wars. Eso importa más de lo que parece, porque Star Wars había cambiado completamente lo que el público esperaba de la ciencia ficción: efectos especiales que se creyeran, mundos que parecieran reales aunque fueran inventados.</p>
<p>Flash Gordon iba en dirección contraria: teatro, artificio, colores imposibles, una convención explícita de que todo es un espectáculo. En otro momento histórico eso habría sido una propuesta interesante. En 1980, después de Lucas, desorientó a todo el mundo.</p>
<p>El público que quería épica espacial encontró Mongo demasiado de juguete. El que disfrutaba del camp encontró la película demasiado seria para reírse con ella. Y así quedó en tierra de nadie.</p>
<h2>Por qué vale la pena verla igual</h2>
<p>Con todos esos problemas encima de la mesa, sigo pensando que Flash Gordon merece más atención de la que recibe. Tiene esa generosidad visual que hoy casi no existe: cada plano está lleno, cada decorado tiene detalle, cada traje tiene trabajo detrás. Es una película que se nota que la hizo gente que se lo estaba pasando bien, aunque luego todo se complicara.</p>
<p>Y tiene a Max von Sydow siendo Ming. Que ya es motivo suficiente.</p>
<p>Si nunca la has visto, ponla un viernes por la noche sin demasiadas expectativas. Si la viste hace veinte años y solo recuerdas la canción de Queen, dale otra oportunidad. Es imperfecta, es rara, a veces no sabes muy bien qué estás viendo. Pero es genuina. Y eso en el cine de hoy no abunda tanto como debería.</p>
]]>
      </content:encoded>
      <pubDate>Fri, 01 May 2026 00:00:00 GMT</pubDate>
      <meta property="og:image" content="https://paigar.eu/flash-gordon.jpg"/>
    </item>
    <item>
      <title>Cómo programar el juego de Tetris desde cero</title>
      <link>https://paigar.eu/juego-tetris-tutorial/</link>
      <guid isPermaLink="false">https://paigar.eu/juego-tetris-tutorial/</guid>
      <description>
        Un tutorial paso a paso para construir el clásico Tetris en una sola página web. Es la pieza más completa de la serie hasta ahora: rotación de matrices, gravedad por turnos, y la lógica de eliminación de líneas, que es el corazón del juego y sigue siendo, cuarenta años después, una de las invenciones más limpias de la historia de los videojuegos.
      </description>
      <content:encoded>
        <![CDATA[<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>
<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>
<h2>La idea general antes de tocar código</h2>
<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>
<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>
<h2>La cuadrícula como matriz 2D</h2>
<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>
<pre><code class="language-js">const COLUMNAS = 10;
const FILAS = 20;
const TAMANO = 30; // píxeles por celda

const ANCHO = COLUMNAS * TAMANO; // 300
const ALTO = FILAS * TAMANO; // 600

let tablero = Array.from({ length: FILAS }, () =&gt; Array(COLUMNAS).fill(null));
</code></pre>
<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>
<h2>Las siete piezas como matrices</h2>
<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>
<pre><code class="language-js">const PIEZAS = {
	I: [[1, 1, 1, 1]],
	O: [
		[1, 1],
		[1, 1],
	],
	T: [
		[0, 1, 0],
		[1, 1, 1],
	],
	L: [
		[0, 0, 1],
		[1, 1, 1],
	],
	J: [
		[1, 0, 0],
		[1, 1, 1],
	],
	S: [
		[0, 1, 1],
		[1, 1, 0],
	],
	Z: [
		[1, 1, 0],
		[0, 1, 1],
	],
};
const COLORES = {
	I: &quot;#00d4d4&quot;,
	O: &quot;#d4d400&quot;,
	T: &quot;#a040c0&quot;,
	L: &quot;#d48028&quot;,
	J: &quot;#3050d0&quot;,
	S: &quot;#40c440&quot;,
	Z: &quot;#d44040&quot;,
};
</code></pre>
<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>
<pre><code class="language-js">function clonarMatriz(m) {
	return m.map((fila) =&gt; [...fila]);
}

function nuevaPieza() {
	const TIPOS = Object.keys(PIEZAS);
	const tipo = TIPOS[Math.floor(Math.random() * TIPOS.length)];
	pieza = { tipo, forma: clonarMatriz(PIEZAS[tipo]), color: COLORES[tipo] };
	x = Math.floor((COLUMNAS - pieza.forma[0].length) / 2);
	y = 0;
}
</code></pre>
<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>
<h2>La función <code>colisiona</code>: el corazón de todo</h2>
<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>
<pre><code class="language-js">function colisiona(forma, px, py) {
	for (let f = 0; f &lt; forma.length; f++) {
		for (let c = 0; c &lt; forma[f].length; c++) {
			if (!forma[f][c]) continue; // celda vacía de la pieza, ignorar
			const tx = px + c;
			const ty = py + f;
			if (tx &lt; 0 || tx &gt;= COLUMNAS) return true; // fuera por los lados
			if (ty &gt;= FILAS) return true; // fuera por abajo
			if (ty &gt;= 0 &amp;&amp; tablero[ty][tx]) return true; // pisa un bloque fijo
		}
	}
	return false;
}
</code></pre>
<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>
<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>
<h2>Movimiento y caída</h2>
<p>Con <code>colisiona</code> definida, mover lateralmente es trivial:</p>
<pre><code class="language-js">function moverHorizontal(dx) {
	if (!colisiona(pieza.forma, x + dx, y)) x += dx;
}
</code></pre>
<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>
<pre><code class="language-js">function moverAbajo() {
	if (!colisiona(pieza.forma, x, y + 1)) {
		y++;
		return true;
	}
	fijarPieza();
	return false;
}
</code></pre>
<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>
<h2>La rotación como transformación de matriz</h2>
<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>
<pre><code>rotada[c][N - 1 - f] = original[f][c]
</code></pre>
<p>donde <code>N</code> es el número de filas de la matriz original. Implementado en código:</p>
<pre><code class="language-js">function rotar() {
	if (pieza.tipo === &quot;O&quot;) return; // El cuadrado no cambia al rotar
	const N = pieza.forma.length;
	const M = pieza.forma[0].length;
	const nueva = Array.from({ length: M }, () =&gt; Array(N).fill(0));
	for (let f = 0; f &lt; N; f++) {
		for (let c = 0; c &lt; M; c++) {
			nueva[c][N - 1 - f] = pieza.forma[f][c];
		}
	}
	if (!colisiona(nueva, x, y)) pieza.forma = nueva;
}
</code></pre>
<p>Tres detalles de los que merece la pena hablar:</p>
<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>
<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>
<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>
<h2>Fijar la pieza al tablero</h2>
<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>
<pre><code class="language-js">function fijarPieza() {
	for (let f = 0; f &lt; pieza.forma.length; f++) {
		for (let c = 0; c &lt; pieza.forma[f].length; c++) {
			if (pieza.forma[f][c]) {
				if (y + f &lt; 0) continue; // fuera del tablero por arriba: ignorar
				tablero[y + f][x + c] = pieza.color;
			}
		}
	}
	eliminarLineas();
	nuevaPieza();
}
</code></pre>
<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>
<h2>La eliminación de líneas</h2>
<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>
<pre><code class="language-js">function eliminarLineas() {
	let eliminadas = 0;
	for (let f = FILAS - 1; f &gt;= 0; f--) {
		if (tablero[f].every((c) =&gt; c)) {
			tablero.splice(f, 1);
			tablero.unshift(Array(COLUMNAS).fill(null));
			eliminadas++;
			f++; // el splice ha desplazado todo hacia abajo, hay que re-mirar esta f
		}
	}
	if (eliminadas &gt; 0) {
		const puntosPorN = [0, 100, 300, 500, 800];
		puntos += puntosPorN[eliminadas] * nivel;
		lineas += eliminadas;
		nivel = Math.floor(lineas / 10) + 1;
		intervaloCaida = Math.max(80, 800 - (nivel - 1) * 60);
	}
}
</code></pre>
<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>
<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>
<h2>El bucle del juego con tiempo real</h2>
<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>
<pre><code class="language-js">let ultimoTiempo = 0;
let contadorCaida = 0;
let intervaloCaida = 800;

function bucle(tiempo) {
	if (!ultimoTiempo) ultimoTiempo = tiempo;
	const delta = tiempo - ultimoTiempo;
	ultimoTiempo = tiempo;

	contadorCaida += delta;
	if (contadorCaida &gt;= intervaloCaida) {
		moverAbajo();
		contadorCaida = 0;
	}

	dibujar();
	requestAnimationFrame(bucle);
}
</code></pre>
<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>
<h2>Dibujado</h2>
<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>
<pre><code class="language-js">function dibujarCelda(cx, cy, color) {
	ctx.fillStyle = color;
	ctx.fillRect(cx * TAMANO, cy * TAMANO, TAMANO, TAMANO);
	ctx.fillStyle = &quot;rgba(255,255,255,0.18)&quot;;
	ctx.fillRect(cx * TAMANO, cy * TAMANO, TAMANO, 3); // brillo arriba
	ctx.fillStyle = &quot;rgba(0,0,0,0.25)&quot;;
	ctx.fillRect(cx * TAMANO, cy * TAMANO + TAMANO - 3, TAMANO, 3); // sombra abajo
}
</code></pre>
<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>
<h2>Controles</h2>
<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>
<pre><code class="language-js">document.addEventListener(&quot;keydown&quot;, (e) =&gt; {
	if (e.key === &quot;ArrowLeft&quot;) moverHorizontal(-1);
	if (e.key === &quot;ArrowRight&quot;) moverHorizontal(1);
	if (e.key === &quot;ArrowUp&quot;) rotar();
	if (e.key === &quot;ArrowDown&quot;) {
		moverAbajo();
		contadorCaida = 0;
	}
	if (e.key === &quot; &quot;) caidaInstantanea();
});
</code></pre>
<p><code>caidaInstantanea</code> es un loop simple: mientras la pieza pueda bajar, baja; cuando ya no, fijamos:</p>
<pre><code class="language-js">function caidaInstantanea() {
	while (!colisiona(pieza.forma, x, y + 1)) y++;
	fijarPieza();
}
</code></pre>
<h2>Cierre</h2>
<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>
<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>
]]>
      </content:encoded>
      <pubDate>Thu, 30 Apr 2026 00:00:00 GMT</pubDate>
      <meta property="og:image" content="https://paigar.eu/juego-tetromino.png"/>
    </item>
    <item>
      <title>Autónomo societario: la trampa perfecta del sistema</title>
      <link>https://paigar.eu/autonomo-societario-discriminacion/</link>
      <guid isPermaLink="false">https://paigar.eu/autonomo-societario-discriminacion/</guid>
      <description>
        Montas una sociedad para simplificarte la vida y descubres que has entrado en una categoría invisible que el sistema se encarga de penalizar en cada esquina.
      </description>
      <content:encoded>
        <![CDATA[<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>
<h2>El embrollo del IRPF que nadie sabe gestionar</h2>
<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>
<p>En teoría.</p>
<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>
<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>
<p>Y entonces descubres que eres autónomo societario.</p>
<h2>Una categoría que nadie te anuncia</h2>
<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>
<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>
<h2>La base mínima de cotización: pagando por lo que no ganas</h2>
<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>
<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>
<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>
<h2>La tarifa plana: un derecho recuperado a regañadientes</h2>
<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>
<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>
<h2>La jubilación activa: el cierre que nadie esperaba</h2>
<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>
<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>
<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>
<h2>El paro que existe pero no existe</h2>
<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>
<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>
<p>Y cuando se agota esa prestación, o cuando nunca se cumplieron los requisitos para acceder a ella, ¿qué queda?</p>
<h2>El subsidio de los 52 que nunca llegará</h2>
<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>
<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>
<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>
<h2>¿Qué queda, entonces?</h2>
<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>
<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>
<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>
<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>
]]>
      </content:encoded>
      <pubDate>Wed, 29 Apr 2026 00:00:00 GMT</pubDate>
      <meta property="og:image" content="https://paigar.eu/autonomo-societario-discriminacion.png"/>
    </item>
    <item>
      <title>Cómo validar un NIF español en un formulario web (con CIF incluido)</title>
      <link>https://paigar.eu/validar-nif-cif-formulario/</link>
      <guid isPermaLink="false">https://paigar.eu/validar-nif-cif-formulario/</guid>
      <description>
        El NIF no es solo el DNI con otro nombre. Cuando tu formulario tiene que aceptar tanto personas físicas como empresas, el algoritmo del CIF añade una capa de complejidad que merece su propio artículo.
      </description>
      <content:encoded>
        <![CDATA[<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>
<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>
<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>
<h2>DNI y NIE: terreno conocido</h2>
<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>
<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>
<h2>El CIF: una letra de tipo y un carácter de control que puede ser letra o dígito</h2>
<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>
<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>
<pre><code>[Letra de tipo] + [7 dígitos] + [Letra o dígito de control]

Ejemplo: A  1 2 3 4 5 6 7  4
         ^  ───────────── ^
    tipo S.A.   dígitos   control
</code></pre>
<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>
<h2>El algoritmo del CIF, paso a paso</h2>
<p>El cálculo del carácter de control opera sobre los siete dígitos centrales y sigue este proceso:</p>
<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>
<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>
<pre><code class="language-javascript">const LETRAS_CIF = &quot;JABCDEFGHI&quot;;

function calcularControlCIF(tipo, digitos) {
	let suma = 0;
	for (let i = 0; i &lt; 7; i++) {
		let d = parseInt(digitos[i], 10);
		if ((i + 1) % 2 !== 0) {
			// posición impar (base 1)
			d *= 2;
			if (d &gt;= 10) d = Math.floor(d / 10) + (d % 10);
		}
		suma += d;
	}
	const idx = (10 - (suma % 10)) % 10;
	return { digit: String(idx), letter: LETRAS_CIF[idx] };
}
</code></pre>
<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>
<pre><code class="language-javascript">function validateCIF(value) {
	const cif = normalizar(value);
	const match = cif.match(/^([ABCDEFGHJNPQRSTUVW])(\d{7})([0-9A-J])$/);
	if (!match) return { valid: false, type: &quot;CIF&quot;, error: &quot;formato_incorrecto&quot; };

	const [, tipo, digitos, control] = match;
	const { digit, letter } = calcularControlCIF(tipo, digitos);

	if (&quot;PQSW&quot;.includes(tipo) &amp;&amp; /\d/.test(control)) {
		return { valid: false, type: &quot;CIF&quot;, error: &quot;control_debe_ser_letra&quot; };
	}

	const ok = control === digit || control === letter;
	return {
		valid: ok,
		type: &quot;CIF&quot;,
		value: cif,
		entityType: tipo,
		entityName: TIPOS_CIF[tipo] || null,
		error: ok ? undefined : &quot;caracter_control_incorrecto&quot;,
	};
}
</code></pre>
<h2>Detectar automáticamente qué tipo de NIF es</h2>
<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>
<pre><code class="language-javascript">function validate(raw) {
	const doc = normalizar(raw);
	if (!doc) return { valid: false, type: null, error: &quot;vacio&quot; };

	if (/^\d{8}[A-Z]$/.test(doc)) return validateDNI(doc);
	if (/^[XYZ]\d{7}[A-Z]$/.test(doc)) return validateNIE(doc);
	if (/^[ABCDEFGHJNPQRSTUVW]\d{7}[0-9A-J]$/.test(doc)) return validateCIF(doc);

	return { valid: false, type: null, value: doc, error: &quot;tipo_no_reconocido&quot; };
}
</code></pre>
<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>
<h2>Un resultado rico: más que un booleano</h2>
<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>
<pre><code class="language-javascript">const resultado = IdeFormsNIF.validate(campo.value);

if (resultado.valid &amp;&amp; resultado.type === &quot;CIF&quot;) {
	mostrarInfo(`Empresa registrada como: ${resultado.entityName}`);
}

if (!resultado.valid &amp;&amp; resultado.error === &quot;caracter_control_incorrecto&quot;) {
	mostrarError(&quot;La letra o dígito de control no es correcto para este NIF&quot;);
}
</code></pre>
<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>
<h2>Qué cubre esta validación y qué no</h2>
<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>
<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>
<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>
]]>
      </content:encoded>
      <pubDate>Mon, 27 Apr 2026 00:00:00 GMT</pubDate>
      <meta property="og:image" content="https://paigar.eu/validar-nif-cif-formulario.png"/>
    </item>
    <item>
      <title>LQIP en Lume: placeholders inline generados en build</title>
      <link>https://paigar.eu/lqip-en-lume/</link>
      <guid isPermaLink="false">https://paigar.eu/lqip-en-lume/</guid>
      <description>
        LQIP llena el hueco que deja una imagen mientras descarga: una versión diminuta y borrosa que se sustituye con un cross-fade cuando la real entra. Aquí cuento cómo lo implementé en Idenautas con un script Deno de cien líneas que genera los placeholders en build y los incrusta inline en el HTML, sin un solo byte de JavaScript de cliente más allá del onload del propio img.
      </description>
      <content:encoded>
        <![CDATA[<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>
<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>
<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>
<h2>La idea</h2>
<p>Tres decisiones que conviene fijar antes de escribir nada:</p>
<ol>
<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>
<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>
<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>
</ol>
<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>
<h2>El script: <code>scripts/lqip.ts</code></h2>
<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>
<h3>Descubrir las imágenes referenciadas</h3>
<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>
<ul>
<li>Llamadas a los shortcodes <code>{{ img(&quot;ruta&quot;) }}</code> y <code>{{ cardPicture(&quot;ruta&quot;) }}</code>.</li>
<li>La clave <code>heroImage:</code> en el frontmatter.</li>
</ul>
<pre><code class="language-typescript">const SHORTCODE_RE =
	/(?:\{%[-\s]*|\{\{[-\s]*(?:await\s+)?)(?:img|cardPicture)\s*\(?\s*[&quot;']([^&quot;']+)[&quot;']/g;
const HERO_RE = /^heroImage:\s*(.+)$/m;

async function findImagePaths(): Promise&lt;string[]&gt; {
	const paths = new Set&lt;string&gt;();
	for await (const entry of walk(SRC_DIR, {
		exts: [&quot;.md&quot;, &quot;.vto&quot;, &quot;.njk&quot;, &quot;.ts&quot;],
		skip: [/node_modules/, /_data\/lqip\.json$/],
	})) {
		if (!entry.isFile) continue;
		const content = await Deno.readTextFile(entry.path);
		let m: RegExpExecArray | null;
		SHORTCODE_RE.lastIndex = 0;
		while ((m = SHORTCODE_RE.exec(content)) !== null) paths.add(m[1]);
		const hero = content.match(HERO_RE);
		if (hero) paths.add(hero[1].trim().replace(/^[&quot;']|[&quot;']$/g, &quot;&quot;));
	}
	return [...paths];
}
</code></pre>
<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>
<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>
<h3>Descargar los placeholders del CDN</h3>
<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>
<pre><code class="language-typescript">function imgBase(imgPath: string): string {
	const i = imgPath.lastIndexOf(&quot;.&quot;);
	return i &gt;= 0 ? imgPath.slice(0, i) : imgPath;
}

// imgBase(&quot;portada.jpg&quot;) + &quot;-16.jpg&quot; → &quot;portada-16.jpg&quot;
</code></pre>
<p>Para cada imagen, descargo <code>${CDN}${base}-16.jpg</code> y la convierto a data URI:</p>
<pre><code class="language-typescript">async function fetchBase64(url: string): Promise&lt;string&gt; {
	const res = await fetch(url);
	if (!res.ok) throw new Error(`HTTP ${res.status}`);
	const type = res.headers.get(&quot;content-type&quot;) ?? &quot;image/jpeg&quot;;
	const bytes = new Uint8Array(await res.arrayBuffer());
	return `data:${type};base64,${encodeBase64(bytes)}`;
}
</code></pre>
<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>
<h3>El cache: <code>src/_data/lqip.json</code></h3>
<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>
<pre><code class="language-typescript">let existing: Record&lt;string, string&gt; = {};
try {
	existing = JSON.parse(await Deno.readTextFile(OUTPUT));
} catch {
	// primera ejecución, no hay cache
}

const lqip: Record&lt;string, string&gt; = {};
let downloaded = 0;
for (const img of images) {
	if (existing[img]) {
		lqip[img] = existing[img];
		continue;
	}
	const url = `${CDN}${imgBase(img)}-16.jpg`;
	try {
		lqip[img] = await fetchBase64(url);
		downloaded++;
	} catch (err) {
		console.error(`  [lqip] ✗ ${img}: ${(err as Error).message}`);
	}
}
</code></pre>
<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>
<pre><code class="language-typescript">const prevJson = JSON.stringify(existing, null, 2);
const nextJson = JSON.stringify(lqip, null, 2);
if (prevJson !== nextJson) {
	await Deno.writeTextFile(OUTPUT, nextJson);
}
</code></pre>
<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>
<h2>Integración con Lume</h2>
<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>
<pre><code class="language-typescript">import { generateLQIP } from &quot;./scripts/lqip.ts&quot;;

let lqipData: Record&lt;string, string&gt; = {};
try {
	lqipData = JSON.parse(await Deno.readTextFile(&quot;./src/_data/lqip.json&quot;));
} catch {
	// primer build, todavía no hay cache
}

site.addEventListener(&quot;beforeBuild&quot;, async () =&gt; {
	lqipData = await generateLQIP({ quiet: false });
});
</code></pre>
<p>Dos detalles aquí:</p>
<ul>
<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>
<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>
</ul>
<p>Con <code>lqipData</code> en memoria, los shortcodes que generan el HTML pueden consultarlo:</p>
<pre><code class="language-typescript">site.data(&quot;img&quot;, function (imgPath: string, alt: string, ...) {
  const lqip = lqipData[imgPath] || imgUrl(imgPath, 16, &quot;jpg&quot;);
  return `&lt;div class=&quot;lqip-wrap&quot; style=&quot;background-image:url('${lqip}')&quot;&gt;
    &lt;picture&gt;...&lt;/picture&gt;
  &lt;/div&gt;`;
});
</code></pre>
<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>
<h2>El HTML resultante</h2>
<p>Para cada imagen, el shortcode produce este HTML:</p>
<pre><code class="language-html">&lt;div
	class=&quot;lqip-wrap&quot;
	style=&quot;background-image:url('data:image/jpeg;base64,/9j/4AAQ...')&quot;&gt;
	&lt;picture&gt;
		&lt;source type=&quot;image/avif&quot; srcset=&quot;foto-480.avif 480w, ...&quot; sizes=&quot;...&quot; /&gt;
		&lt;source type=&quot;image/webp&quot; srcset=&quot;foto-480.webp 480w, ...&quot; sizes=&quot;...&quot; /&gt;
		&lt;img
			src=&quot;foto-1200.jpg&quot;
			srcset=&quot;foto-480.jpg 480w, ...&quot;
			sizes=&quot;...&quot;
			alt=&quot;...&quot;
			loading=&quot;lazy&quot;
			width=&quot;1200&quot;
			height=&quot;800&quot;
			onload=&quot;this.parentNode.classList.add('loaded')&quot; /&gt;
	&lt;/picture&gt;
&lt;/div&gt;
</code></pre>
<p>Tres piezas:</p>
<ul>
<li><strong>El wrapper lleva el placeholder como <code>background-image</code>.</strong> Aparece instantáneo: ya viaja en el HTML.</li>
<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>
<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>
</ul>
<h2>El CSS: cross-fade sin JavaScript</h2>
<pre><code class="language-css">.lqip-wrap {
	position: relative;
	background-size: cover;
	background-position: center;
	background-repeat: no-repeat;
	overflow: hidden;
	width: 100%;
	height: 100%;
}

.lqip-wrap &gt; img {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
	opacity: 0;
	transition: opacity 0.4s ease;
}

.lqip-wrap.loaded &gt; img {
	opacity: 1;
}
</code></pre>
<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>
<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>
<h2>Por qué 16 píxeles y por qué JPG</h2>
<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>
<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>
<h2>Por qué no BlurHash, Plaiceholder o transform_images</h2>
<p>Existen alternativas conocidas:</p>
<ul>
<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>
<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>
<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>
</ul>
<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>
<h2>Lo que cuesta y lo que aporta</h2>
<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>
<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>
<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>
]]>
      </content:encoded>
      <pubDate>Thu, 23 Apr 2026 00:00:00 GMT</pubDate>
      <meta property="og:image" content="https://paigar.eu/lqip-en-lume.png"/>
    </item>
    <item>
      <title>Cómo programar el juego de ladrillos desde cero</title>
      <link>https://paigar.eu/juego-ladrillos-tutorial/</link>
      <guid isPermaLink="false">https://paigar.eu/juego-ladrillos-tutorial/</guid>
      <description>
        Un tutorial paso a paso para construir un clásico juego de ladrillos —de la familia de Breakout y Arkanoid— en una sola página web. Es el juego más completo de la serie hasta ahora: combina la física de Pong con la gestión de muchos objetos a la vez y una lógica de niveles que da mucho juego.
      </description>
      <content:encoded>
        <![CDATA[<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>
<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>
<h2>La idea general antes de tocar código</h2>
<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>
<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>
<h2>El esqueleto HTML y CSS</h2>
<p>Vamos con canvas, igual que con Snake o Pong, porque tenemos movimiento continuo y muchos objetos que pintar.</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;es&quot;&gt;
	&lt;head&gt;
		&lt;meta charset=&quot;UTF-8&quot; /&gt;
		&lt;title&gt;Ladrillos&lt;/title&gt;
		&lt;style&gt;
			body {
				display: flex;
				flex-direction: column;
				align-items: center;
				background: #1a1d24;
				color: #e8e8e8;
				font-family: system-ui, sans-serif;
			}
			canvas {
				background: #0f1115;
				border: 1px solid #2a2e36;
			}
		&lt;/style&gt;
	&lt;/head&gt;
	&lt;body&gt;
		&lt;h1&gt;Ladrillos&lt;/h1&gt;
		&lt;div&gt;
			Puntos: &lt;span id=&quot;puntos&quot;&gt;0&lt;/span&gt; · Vidas: &lt;span id=&quot;vidas&quot;&gt;3&lt;/span&gt;
		&lt;/div&gt;
		&lt;canvas id=&quot;lienzo&quot; width=&quot;480&quot; height=&quot;600&quot;&gt;&lt;/canvas&gt;
		&lt;script&gt;
			// aquí irá todo el código del juego
		&lt;/script&gt;
	&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<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>
<h2>El estado del juego</h2>
<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>
<pre><code class="language-javascript">const ANCHO = 480;
const ALTO = 600;
const ANCHO_PALETA = 80;
const ALTO_PALETA = 12;
const TAMANO_PELOTA = 10;
const FILAS_LADRILLOS = 5;
const COLUMNAS_LADRILLOS = 8;
const ANCHO_LADRILLO = 54;
const ALTO_LADRILLO = 18;
const MARGEN_LADRILLOS = 6;

let pelota = { x: 0, y: 0, vx: 0, vy: 0 };
let paleta = { x: ANCHO / 2 - ANCHO_PALETA / 2 };
let ladrillos = [];
let puntos = 0;
let vidas = 3;
let pegada = true;
</code></pre>
<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>
<h2>Generar los ladrillos</h2>
<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>
<pre><code class="language-javascript">function generarLadrillos() {
	ladrillos = [];
	const colores = [&quot;#e74c3c&quot;, &quot;#e67e22&quot;, &quot;#f1c40f&quot;, &quot;#27ae60&quot;, &quot;#3498db&quot;];
	const offsetX =
		(ANCHO -
			(COLUMNAS_LADRILLOS * (ANCHO_LADRILLO + MARGEN_LADRILLOS) -
				MARGEN_LADRILLOS)) /
		2;
	const offsetY = 60;

	for (let f = 0; f &lt; FILAS_LADRILLOS; f++) {
		for (let c = 0; c &lt; COLUMNAS_LADRILLOS; c++) {
			ladrillos.push({
				x: offsetX + c * (ANCHO_LADRILLO + MARGEN_LADRILLOS),
				y: offsetY + f * (ALTO_LADRILLO + MARGEN_LADRILLOS),
				color: colores[f],
				puntos: (FILAS_LADRILLOS - f) * 10,
				vivo: true,
			});
		}
	}
}
</code></pre>
<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>
<h2>Mover la pelota y rebotes en bordes</h2>
<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>
<pre><code class="language-javascript">function actualizarPelota() {
	if (pegada) {
		pelota.x = paleta.x + ANCHO_PALETA / 2 - TAMANO_PELOTA / 2;
		pelota.y = ALTO - ALTO_PALETA - TAMANO_PELOTA - 5;
		return;
	}

	pelota.x += pelota.vx;
	pelota.y += pelota.vy;

	if (pelota.x &lt; 0) {
		pelota.x = 0;
		pelota.vx = -pelota.vx;
	}
	if (pelota.x + TAMANO_PELOTA &gt; ANCHO) {
		pelota.x = ANCHO - TAMANO_PELOTA;
		pelota.vx = -pelota.vx;
	}
	if (pelota.y &lt; 0) {
		pelota.y = 0;
		pelota.vy = -pelota.vy;
	}
	if (pelota.y &gt; ALTO) {
		perderVida();
	}
}
</code></pre>
<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>
<h2>Colisión con la paleta</h2>
<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>
<pre><code class="language-javascript">function comprobarColisionPaleta() {
	const paletaY = ALTO - ALTO_PALETA - 10;
	if (
		pelota.x &lt; paleta.x + ANCHO_PALETA &amp;&amp;
		pelota.x + TAMANO_PELOTA &gt; paleta.x &amp;&amp;
		pelota.y + TAMANO_PELOTA &gt; paletaY &amp;&amp;
		pelota.y &lt; paletaY + ALTO_PALETA &amp;&amp;
		pelota.vy &gt; 0
	) {
		pelota.y = paletaY - TAMANO_PELOTA;
		const centroPaleta = paleta.x + ANCHO_PALETA / 2;
		const centroPelota = pelota.x + TAMANO_PELOTA / 2;
		const offset = (centroPelota - centroPaleta) / (ANCHO_PALETA / 2);
		const velocidad = Math.sqrt(pelota.vx ** 2 + pelota.vy ** 2);
		const angulo = offset * (Math.PI / 3);
		pelota.vx = velocidad * Math.sin(angulo);
		pelota.vy = -velocidad * Math.cos(angulo);
	}
}
</code></pre>
<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>
<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>
<h2>Colisión con los ladrillos</h2>
<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>
<pre><code class="language-javascript">function comprobarColisionLadrillos() {
	for (const l of ladrillos) {
		if (!l.vivo) continue;
		if (
			pelota.x &lt; l.x + ANCHO_LADRILLO &amp;&amp;
			pelota.x + TAMANO_PELOTA &gt; l.x &amp;&amp;
			pelota.y &lt; l.y + ALTO_LADRILLO &amp;&amp;
			pelota.y + TAMANO_PELOTA &gt; l.y
		) {
			l.vivo = false;
			puntos += l.puntos;

			const centroPelota = {
				x: pelota.x + TAMANO_PELOTA / 2,
				y: pelota.y + TAMANO_PELOTA / 2,
			};
			const centroLadrillo = {
				x: l.x + ANCHO_LADRILLO / 2,
				y: l.y + ALTO_LADRILLO / 2,
			};
			const dx =
				Math.abs(centroPelota.x - centroLadrillo.x) - ANCHO_LADRILLO / 2;
			const dy =
				Math.abs(centroPelota.y - centroLadrillo.y) - ALTO_LADRILLO / 2;

			if (dx &gt; dy) {
				pelota.vx = -pelota.vx;
			} else {
				pelota.vy = -pelota.vy;
			}
			return;
		}
	}
}
</code></pre>
<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>
<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>
<h2>Mover la paleta</h2>
<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>
<pre><code class="language-javascript">const teclas = { izquierda: false, derecha: false };

document.addEventListener(&quot;keydown&quot;, (e) =&gt; {
	if (e.key === &quot;ArrowLeft&quot; || e.key === &quot;a&quot; || e.key === &quot;A&quot;)
		teclas.izquierda = true;
	if (e.key === &quot;ArrowRight&quot; || e.key === &quot;d&quot; || e.key === &quot;D&quot;)
		teclas.derecha = true;
	if (e.key === &quot; &quot; &amp;&amp; pegada) lanzarPelota();
});

document.addEventListener(&quot;keyup&quot;, (e) =&gt; {
	if (e.key === &quot;ArrowLeft&quot; || e.key === &quot;a&quot; || e.key === &quot;A&quot;)
		teclas.izquierda = false;
	if (e.key === &quot;ArrowRight&quot; || e.key === &quot;d&quot; || e.key === &quot;D&quot;)
		teclas.derecha = false;
});

function moverPaleta() {
	const VELOCIDAD = 7;
	if (teclas.izquierda) paleta.x -= VELOCIDAD;
	if (teclas.derecha) paleta.x += VELOCIDAD;
	paleta.x = Math.max(0, Math.min(ANCHO - ANCHO_PALETA, paleta.x));
}

function lanzarPelota() {
	pegada = false;
	pelota.vx = (Math.random() - 0.5) * 4;
	pelota.vy = -5;
}
</code></pre>
<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>
<h2>Perder y reiniciar</h2>
<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>
<pre><code class="language-javascript">function perderVida() {
	vidas--;
	pegada = true;
	pelota.vx = 0;
	pelota.vy = 0;
	if (vidas &lt;= 0) {
		setTimeout(() =&gt; alert(`Fin del juego. Puntos: ${puntos}`), 100);
		reiniciarJuego();
	}
}

function reiniciarJuego() {
	puntos = 0;
	vidas = 3;
	pegada = true;
	generarLadrillos();
}
</code></pre>
<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>
<pre><code class="language-javascript">function comprobarVictoria() {
	if (ladrillos.every((l) =&gt; !l.vivo)) {
		setTimeout(() =&gt; alert(`¡Nivel completado! Puntos: ${puntos}`), 100);
		reiniciarJuego();
	}
}
</code></pre>
<h2>Dibujar todo</h2>
<p>La función de dibujado limpia el canvas y pinta la paleta, la pelota y todos los ladrillos vivos.</p>
<pre><code class="language-javascript">const lienzo = document.getElementById(&quot;lienzo&quot;);
const ctx = lienzo.getContext(&quot;2d&quot;);

function dibujar() {
	ctx.fillStyle = &quot;#0f1115&quot;;
	ctx.fillRect(0, 0, ANCHO, ALTO);

	for (const l of ladrillos) {
		if (!l.vivo) continue;
		ctx.fillStyle = l.color;
		ctx.fillRect(l.x, l.y, ANCHO_LADRILLO, ALTO_LADRILLO);
	}

	ctx.fillStyle = &quot;#e8e8e8&quot;;
	ctx.fillRect(paleta.x, ALTO - ALTO_PALETA - 10, ANCHO_PALETA, ALTO_PALETA);
	ctx.fillRect(pelota.x, pelota.y, TAMANO_PELOTA, TAMANO_PELOTA);
}
</code></pre>
<h2>El bucle principal</h2>
<p>Igual que en Pong, usamos <code>requestAnimationFrame</code> para sincronizar el bucle con la frecuencia de refresco de la pantalla.</p>
<pre><code class="language-javascript">function bucle() {
	moverPaleta();
	actualizarPelota();
	comprobarColisionPaleta();
	comprobarColisionLadrillos();
	comprobarVictoria();
	dibujar();
	document.getElementById(&quot;puntos&quot;).textContent = puntos;
	document.getElementById(&quot;vidas&quot;).textContent = vidas;
	requestAnimationFrame(bucle);
}

generarLadrillos();
bucle();
</code></pre>
<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>
<h2>Cosas que se pueden añadir</h2>
<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>
<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>
<h2>El prototipo funcional</h2>
<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>
<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>
]]>
      </content:encoded>
      <pubDate>Sat, 18 Apr 2026 00:00:00 GMT</pubDate>
      <meta property="og:image" content="https://paigar.eu/juego-ladrillos.png"/>
    </item>
    <item>
      <title>Cómo validar un código IBAN en un formulario web</title>
      <link>https://paigar.eu/validar-iban-formulario/</link>
      <guid isPermaLink="false">https://paigar.eu/validar-iban-formulario/</guid>
      <description>
        El IBAN tiene un algoritmo de validación elegante, una longitud que varía según el país y un problema con números enormes que tiene solución. Aquí va todo lo que necesitas para no rechazar cuentas bancarias válidas.
      </description>
      <content:encoded>
        <![CDATA[<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>
<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>
<h2>La estructura del IBAN</h2>
<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>
<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>
<h2>El algoritmo: mover, convertir, dividir</h2>
<p>El método de validación es elegante en su sencillez. Se llama MOD-97-10 y funciona en tres pasos.</p>
<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>
<pre><code class="language-javascript">function mod97(numStr) {
	let resto = 0;
	for (const digito of numStr) {
		resto = (resto * 10 + parseInt(digito)) % 97;
	}
	return resto;
}

function validarIBAN(raw) {
	const iban = raw
		.trim()
		.toUpperCase()
		.replace(/[\s\-]/g, &quot;&quot;);

	if (!/^[A-Z]{2}\d{2}[A-Z0-9]{4,}$/.test(iban)) {
		return { valido: false, error: &quot;formato&quot; };
	}

	const reordenado = iban.substring(4) + iban.substring(0, 4);
	const numerico = reordenado.replace(/[A-Z]/g, (c) =&gt;
		(c.charCodeAt(0) - 55).toString(),
	);

	return { valido: mod97(numerico) === 1, pais: iban.substring(0, 2) };
}
</code></pre>
<h2>El problema de los números enormes</h2>
<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>
<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>
<pre><code class="language-javascript">const valido = BigInt(numerico) % 97n === 1n;
</code></pre>
<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>
<h2>Validar la longitud según el país</h2>
<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>
<pre><code class="language-javascript">const LONGITUDES_IBAN = {
	AL: 28,
	AD: 24,
	AT: 20,
	BE: 16,
	BA: 20,
	BR: 29,
	BG: 22,
	HR: 21,
	CY: 28,
	CZ: 24,
	DK: 18,
	EE: 20,
	FI: 18,
	FR: 27,
	DE: 22,
	GI: 23,
	GR: 27,
	HU: 28,
	IS: 26,
	IE: 22,
	IT: 27,
	LV: 21,
	LI: 21,
	LT: 20,
	LU: 20,
	MT: 31,
	MC: 27,
	NL: 18,
	NO: 15,
	PL: 28,
	PT: 25,
	RO: 24,
	SM: 27,
	SK: 24,
	SI: 19,
	ES: 24,
	SE: 24,
	CH: 21,
	GB: 22,
};
</code></pre>
<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>
<h2>La función completa, con mensajes detallados</h2>
<p>Juntando todo —limpieza de entrada, validación de formato, comprobación de longitud y algoritmo MOD-97— queda algo así:</p>
<pre><code class="language-javascript">const LONGITUDES_IBAN = {
	ES: 24,
	DE: 22,
	FR: 27,
	GB: 22,
	IT: 27,
	PT: 25,
	NL: 18,
	BE: 16 /* ... */,
};

function mod97(numStr) {
	let resto = 0;
	for (const d of numStr) resto = (resto * 10 + parseInt(d)) % 97;
	return resto;
}

function validarIBAN(raw) {
	const iban = raw
		.trim()
		.toUpperCase()
		.replace(/[\s\-]/g, &quot;&quot;);
	const pais = iban.substring(0, 2);

	if (!/^[A-Z]{2}\d{2}[A-Z0-9]{4,}$/.test(iban)) {
		return { valido: false, mensaje: &quot;Formato no reconocido&quot; };
	}

	const longEsperada = LONGITUDES_IBAN[pais];
	if (longEsperada &amp;&amp; iban.length !== longEsperada) {
		return {
			valido: false,
			mensaje: `El IBAN de ${pais} debe tener ${longEsperada} caracteres (este tiene ${iban.length})`,
		};
	}

	const numerico = (iban.substring(4) + iban.substring(0, 4)).replace(
		/[A-Z]/g,
		(c) =&gt; (c.charCodeAt(0) - 55).toString(),
	);

	return {
		valido: mod97(numerico) === 1,
		pais,
		control: iban.substring(2, 4),
		bban: iban.substring(4),
		mensaje:
			mod97(numerico) === 1
				? &quot;IBAN válido&quot;
				: &quot;Los dígitos de control no son correctos&quot;,
	};
}
</code></pre>
<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>
<h2>Integración en un formulario con feedback en tiempo real</h2>
<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>
<pre><code class="language-javascript">const campo = document.getElementById(&quot;iban&quot;);
const aviso = document.getElementById(&quot;aviso-iban&quot;);

campo.addEventListener(&quot;input&quot;, () =&gt; {
	const valor = campo.value;

	if (!valor.trim()) {
		aviso.textContent = &quot;&quot;;
		campo.removeAttribute(&quot;aria-invalid&quot;);
		return;
	}

	const resultado = validarIBAN(valor);
	aviso.textContent = resultado.mensaje;
	aviso.className = resultado.valido ? &quot;aviso ok&quot; : &quot;aviso error&quot;;
	campo.setAttribute(&quot;aria-invalid&quot;, resultado.valido ? &quot;false&quot; : &quot;true&quot;);
});
</code></pre>
<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>
<h2>Lo que esta validación no puede decirte</h2>
<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>
<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>
<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>
]]>
      </content:encoded>
      <pubDate>Tue, 14 Apr 2026 00:00:00 GMT</pubDate>
      <meta property="og:image" content="https://paigar.eu/validar-iban-formulario.png"/>
    </item>
    <item>
      <title>La España que exportaba videojuegos</title>
      <link>https://paigar.eu/software-espanol-80s/</link>
      <guid isPermaLink="false">https://paigar.eu/software-espanol-80s/</guid>
      <description>
        A mediados de los ochenta, un puñado de chavales en Madrid estaban programando juegos que se vendían en el Reino Unido y aparecían reseñados en la prensa especializada británica. Se llamaban Dinamic, Opera Soft, Topo Soft. Casi nadie los recuerda.
      </description>
      <content:encoded>
        <![CDATA[<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>
<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>
<h2>Por qué España y por qué entonces</h2>
<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>
<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>
<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>
<h2>Dinamic y el oficio de construir desde la nada</h2>
<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>
<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>
<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>
<h2>Opera Soft y el argumento definitivo</h2>
<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>
<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>
<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>
<h2>El dato que cambia la perspectiva</h2>
<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>
<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>
<h2>El precipicio de los 16 bits</h2>
<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>
<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>
<h2>Lo que queda</h2>
<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>
<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>
<p>No está mal para un país que, según el relato oficial, llegó tarde a la tecnología.</p>
]]>
      </content:encoded>
      <pubDate>Tue, 07 Apr 2026 00:00:00 GMT</pubDate>
      <meta property="og:image" content="https://paigar.eu/software-espanol-80s.png"/>
    </item>
    <item>
      <title>Cómo construir una herramienta de armonías de color con JavaScript</title>
      <link>https://paigar.eu/color-harmony-article/</link>
      <guid isPermaLink="false">https://paigar.eu/color-harmony-article/</guid>
      <description>
        El círculo cromático lleva siglos explicando por qué ciertos colores funcionan juntos. Te cuento cómo convertir esa teoría en una pequeña utilidad web que calcule paletas armónicas a partir de cualquier color.
      </description>
      <content:encoded>
        <![CDATA[<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>
<h2>El círculo cromático, o por qué algunos colores se llevan bien</h2>
<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>
<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>
<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>
<h2>Los cinco tipos de armonía que vale la pena conocer</h2>
<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>
<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>
<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>
<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>
<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>
<h2>HSL: el espacio de color que hace todo esto natural</h2>
<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>
<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>
<pre><code class="language-javascript">function complementario(h, s, l) {
	return [(h + 180) % 360, s, l];
}
</code></pre>
<p>Y el triádico, añadir 120 y 240:</p>
<pre><code class="language-javascript">function triadica(h, s, l) {
	return [
		[h, s, l],
		[(h + 120) % 360, s, l],
		[(h + 240) % 360, s, l],
	];
}
</code></pre>
<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>
<h2>Convertir entre formatos: de hex a HSL y vuelta</h2>
<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>
<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>
<pre><code class="language-javascript">function hexToHsl(hex) {
	const r = parseInt(hex.slice(1, 3), 16) / 255;
	const g = parseInt(hex.slice(3, 5), 16) / 255;
	const b = parseInt(hex.slice(5, 7), 16) / 255;

	const max = Math.max(r, g, b);
	const min = Math.min(r, g, b);
	const l = (max + min) / 2;
	let h = 0,
		s = 0;

	if (max !== min) {
		const d = max - min;
		s = l &gt; 0.5 ? d / (2 - max - min) : d / (max + min);
		if (max === r) h = ((g - b) / d + (g &lt; b ? 6 : 0)) / 6;
		else if (max === g) h = ((b - r) / d + 2) / 6;
		else h = ((r - g) / d + 4) / 6;
	}

	return [Math.round(h * 360), Math.round(s * 100), Math.round(l * 100)];
}
</code></pre>
<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>
<pre><code class="language-javascript">function hslToHex(h, s, l) {
	h = ((h % 360) + 360) % 360;
	s /= 100;
	l /= 100;
	const a = s * Math.min(l, 1 - l);
	const f = (n) =&gt; {
		const k = (n + h / 30) % 12;
		const c = l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
		return Math.round(255 * c)
			.toString(16)
			.padStart(2, &quot;0&quot;);
	};
	return &quot;#&quot; + f(0) + f(8) + f(4);
}
</code></pre>
<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>
<h2>La interfaz: selector de color, swatches y rueda visual</h2>
<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>
<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>
<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>
<h2>Lo que me llevé de todo esto</h2>
<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>
<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>
]]>
      </content:encoded>
      <pubDate>Fri, 03 Apr 2026 00:00:00 GMT</pubDate>
      <meta property="og:image" content="https://paigar.eu/color-harmony-tool.png"/>
    </item>
    <item>
      <title>Venus Marítima, el relato que me voló la cabeza</title>
      <link>https://paigar.eu/venus-maritima-gripari/</link>
      <guid isPermaLink="false">https://paigar.eu/venus-maritima-gripari/</guid>
      <description>
        Un cuento corto de ciencia ficción publicado en 1972 que mezcla venusianos, una distopía socialista mundial y una bahía francesa donde sucede algo que nadie nombra durante quince años. Lo leí hace tiempo, lo he releído ahora y sigue funcionando.
      </description>
      <content:encoded>
        <![CDATA[<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>
<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>
<h2>El planteamiento, que ya te coloca de entrada</h2>
<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>
<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>
<p>Pero lo bueno viene después.</p>
<h2>Cuando el sexo pasa a ser propiedad del Estado</h2>
<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>
<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>
<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>
<h2>Y entonces aparece el microbio rosa</h2>
<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>
<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>
<h2>El secreto mejor guardado del mundo</h2>
<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>
<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>
<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>
<h2>El perrito que lo arruina todo</h2>
<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>
<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>
<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>
<h2>Por qué me parece que el cuento sigue dando en el clavo</h2>
<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>
<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>
<h2>Por qué conviene buscarlo</h2>
<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>
<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>
]]>
      </content:encoded>
      <pubDate>Tue, 24 Mar 2026 00:00:00 GMT</pubDate>
      <meta property="og:image" content="https://paigar.eu/venus-maritima-gripari.png"/>
    </item>
    <item>
      <title>Cómo programar el juego de la serpiente desde cero</title>
      <link>https://paigar.eu/juego-serpiente-tutorial/</link>
      <guid isPermaLink="false">https://paigar.eu/juego-serpiente-tutorial/</guid>
      <description>
        Un tutorial paso a paso para construir el clásico Snake en una sola página web, sin frameworks, sin dependencias y sin más de doscientas líneas de código. Al final del artículo te dejo el prototipo funcional para que juegues.
      </description>
      <content:encoded>
        <![CDATA[<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>
<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>
<h2>La idea general antes de tocar código</h2>
<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>
<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>
<h2>El esqueleto HTML y CSS</h2>
<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>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;es&quot;&gt;
	&lt;head&gt;
		&lt;meta charset=&quot;UTF-8&quot; /&gt;
		&lt;title&gt;Snake&lt;/title&gt;
		&lt;style&gt;
			body {
				display: flex;
				flex-direction: column;
				align-items: center;
				background: #1a1d24;
				color: #e8e8e8;
				font-family: system-ui, sans-serif;
			}
			canvas {
				background: #0f1115;
				border: 1px solid #2a2e36;
			}
		&lt;/style&gt;
	&lt;/head&gt;
	&lt;body&gt;
		&lt;h1&gt;Snake&lt;/h1&gt;
		&lt;div&gt;Puntuación: &lt;span id=&quot;puntuacion&quot;&gt;0&lt;/span&gt;&lt;/div&gt;
		&lt;canvas id=&quot;lienzo&quot; width=&quot;400&quot; height=&quot;400&quot;&gt;&lt;/canvas&gt;
		&lt;script&gt;
			// aquí irá todo el código del juego
		&lt;/script&gt;
	&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<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>
<h2>El estado del juego</h2>
<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>
<pre><code class="language-javascript">const TAMANO_CASILLA = 20;
const COLUMNAS = 20;
const FILAS = 20;

let serpiente = [
	{ x: 10, y: 10 },
	{ x: 9, y: 10 },
	{ x: 8, y: 10 },
];
let direccion = { x: 1, y: 0 };
let manzana = { x: 5, y: 5 };
let puntuacion = 0;
let terminado = false;
</code></pre>
<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>
<h2>Mover la serpiente</h2>
<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>
<pre><code class="language-javascript">function paso() {
	if (terminado) return;

	const cabeza = serpiente[0];
	const nuevaCabeza = {
		x: cabeza.x + direccion.x,
		y: cabeza.y + direccion.y,
	};

	serpiente.unshift(nuevaCabeza);

	if (nuevaCabeza.x === manzana.x &amp;&amp; nuevaCabeza.y === manzana.y) {
		puntuacion += 10;
		colocarManzana();
	} else {
		serpiente.pop();
	}
}
</code></pre>
<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>
<h2>Detectar colisiones</h2>
<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>
<pre><code class="language-javascript">const fueraDeLimites =
	nuevaCabeza.x &lt; 0 ||
	nuevaCabeza.x &gt;= COLUMNAS ||
	nuevaCabeza.y &lt; 0 ||
	nuevaCabeza.y &gt;= FILAS;

const seMuerde = serpiente.some(
	(s) =&gt; s.x === nuevaCabeza.x &amp;&amp; s.y === nuevaCabeza.y,
);

if (fueraDeLimites || seMuerde) {
	terminado = true;
	return;
}
</code></pre>
<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>
<h2>Colocar la manzana</h2>
<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>
<pre><code class="language-javascript">function colocarManzana() {
	while (true) {
		const candidata = {
			x: Math.floor(Math.random() * COLUMNAS),
			y: Math.floor(Math.random() * FILAS),
		};
		const colisiona = serpiente.some(
			(s) =&gt; s.x === candidata.x &amp;&amp; s.y === candidata.y,
		);
		if (!colisiona) {
			manzana = candidata;
			return;
		}
	}
}
</code></pre>
<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>
<h2>Capturar el teclado</h2>
<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>
<pre><code class="language-javascript">document.addEventListener(&quot;keydown&quot;, (e) =&gt; {
	const teclas = {
		ArrowUp: { x: 0, y: -1 },
		ArrowDown: { x: 0, y: 1 },
		ArrowLeft: { x: -1, y: 0 },
		ArrowRight: { x: 1, y: 0 },
	};
	if (teclas[e.key]) {
		const nueva = teclas[e.key];
		if (nueva.x === -direccion.x &amp;&amp; nueva.y === -direccion.y) return;
		direccion = nueva;
	}
});
</code></pre>
<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>
<h2>Dibujar en el canvas</h2>
<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>
<pre><code class="language-javascript">const lienzo = document.getElementById(&quot;lienzo&quot;);
const ctx = lienzo.getContext(&quot;2d&quot;);

function dibujar() {
	ctx.fillStyle = &quot;#0f1115&quot;;
	ctx.fillRect(0, 0, lienzo.width, lienzo.height);

	ctx.fillStyle = &quot;#e74c3c&quot;;
	ctx.fillRect(
		manzana.x * TAMANO_CASILLA,
		manzana.y * TAMANO_CASILLA,
		TAMANO_CASILLA,
		TAMANO_CASILLA,
	);

	ctx.fillStyle = &quot;#3ecf8e&quot;;
	serpiente.forEach((segmento) =&gt; {
		ctx.fillRect(
			segmento.x * TAMANO_CASILLA,
			segmento.y * TAMANO_CASILLA,
			TAMANO_CASILLA,
			TAMANO_CASILLA,
		);
	});
}
</code></pre>
<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>
<h2>El bucle principal</h2>
<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>
<pre><code class="language-javascript">function bucle() {
	paso();
	dibujar();
}

setInterval(bucle, 110);
</code></pre>
<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>
<h2>Cosas que se pueden añadir si te apetece seguir</h2>
<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>
<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>
<h2>El prototipo funcional</h2>
<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>
<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>
]]>
      </content:encoded>
      <pubDate>Tue, 17 Mar 2026 00:00:00 GMT</pubDate>
      <meta property="og:image" content="https://paigar.eu/juego-serpiente.png"/>
    </item>
    <item>
      <title>Cuando tu herramienta favorita cambia de rumbo</title>
      <link>https://paigar.eu/cuando-tu-herramienta-favorita-cambia-de-rumbo/</link>
      <guid isPermaLink="false">https://paigar.eu/cuando-tu-herramienta-favorita-cambia-de-rumbo/</guid>
      <description>
        Eleventy —el generador de sitios estáticos al que tantos hemos sido fieles— ha confirmado que se renombra a Build Awesome y entra al catálogo freemium de Font Awesome. La versión gratuita sigue existiendo, pero la promesa original —proyecto pequeño, independiente, sin agenda comercial— deja de estar sobre la mesa. Toca empezar a mirar el horizonte.
      </description>
      <content:encoded>
        <![CDATA[<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>
<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>
<h2>El patrón conocido</h2>
<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>
<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>
<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>
<h2>Lo que busco en un generador estático</h2>
<p>Antes de mirar alternativas, conviene definir el punto de partida. No todo el mundo necesita lo mismo.</p>
<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>
<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>
<h2>Lume: el sucesor espiritual</h2>
<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>
<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>
<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>
<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>
<h2>La decisión</h2>
<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>
<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>
<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>
<p>A veces las herramientas que más quieres son las que te empujan a mirar más allá de ellas.</p>
]]>
      </content:encoded>
      <pubDate>Thu, 12 Mar 2026 00:00:00 GMT</pubDate>
      <meta property="og:image" content="https://paigar.eu/hta-cambio-rumbo.png"/>
    </item>
    <item>
      <title>Cómo validar un DNI, NIE o Pasaporte en un formulario web</title>
      <link>https://paigar.eu/validar-dni-nie-pasaporte/</link>
      <guid isPermaLink="false">https://paigar.eu/validar-dni-nie-pasaporte/</guid>
      <description>
        Antes de rechazar a un usuario con un mensaje de error críptico, al menos asegúrate de que el rechazo está justificado. Aquí te explico cómo validar documentos de identidad españoles con JavaScript.
      </description>
      <content:encoded>
        <![CDATA[<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>
<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>
<h2>La anatomía del DNI español</h2>
<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>
<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>
<pre><code class="language-javascript">const LETRAS = &quot;TRWAGMYFPDXBNJZSQVHLCKE&quot;;

function validarDNI(dni) {
	const match = dni.toUpperCase().match(/^(\d{8})([A-Z])$/);
	if (!match) return false;
	const numero = parseInt(match[1], 10);
	const letra = match[2];
	return LETRAS[numero % 23] === letra;
}
</code></pre>
<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>
<h2>El NIE, primo hermano con letra por delante</h2>
<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>
<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>
<pre><code class="language-javascript">function validarNIE(nie) {
	const match = nie.toUpperCase().match(/^([XYZ])(\d{7})([A-Z])$/);
	if (!match) return false;
	const prefijo = { X: &quot;0&quot;, Y: &quot;1&quot;, Z: &quot;2&quot; };
	const numero = parseInt(prefijo[match[1]] + match[2], 10);
	const letra = match[3];
	return LETRAS[numero % 23] === letra;
}
</code></pre>
<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>
<h2>El pasaporte, el pariente sin algoritmo público</h2>
<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>
<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>
<pre><code class="language-javascript">function validarPasaporte(pasaporte) {
	return /^[A-Z]{2,3}\d{5,6}$/i.test(pasaporte);
}
</code></pre>
<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>
<h2>Juntarlo todo: detección automática del tipo de documento</h2>
<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>
<pre><code class="language-javascript">const LETRAS = &quot;TRWAGMYFPDXBNJZSQVHLCKE&quot;;

function validarDNI(doc) {
	const m = doc.match(/^(\d{8})([A-Z])$/);
	if (!m) return { valido: false };
	const ok = LETRAS[parseInt(m[1], 10) % 23] === m[2];
	return { tipo: &quot;DNI&quot;, valido: ok };
}

function validarNIE(doc) {
	const m = doc.match(/^([XYZ])(\d{7})([A-Z])$/);
	if (!m) return { valido: false };
	const pref = { X: &quot;0&quot;, Y: &quot;1&quot;, Z: &quot;2&quot; };
	const ok = LETRAS[parseInt(pref[m[1]] + m[2], 10) % 23] === m[3];
	return { tipo: &quot;NIE&quot;, valido: ok };
}

function validarPasaporte(doc) {
	const ok = /^[A-Z]{2,3}\d{5,6}$/.test(doc);
	return { tipo: &quot;Pasaporte&quot;, valido: ok };
}

function validarDocumento(raw) {
	const doc = raw
		.trim()
		.toUpperCase()
		.replace(/[\s\-\.]/g, &quot;&quot;);

	if (/^\d{8}[A-Z]$/.test(doc)) return validarDNI(doc);
	if (/^[XYZ]\d{7}[A-Z]$/.test(doc)) return validarNIE(doc);
	if (/^[A-Z]{2,3}\d{5,6}$/.test(doc)) return validarPasaporte(doc);

	return { tipo: &quot;desconocido&quot;, valido: false };
}
</code></pre>
<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>
<h2>Integración en un formulario real</h2>
<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>
<pre><code class="language-javascript">const campo = document.getElementById(&quot;documento&quot;);
const aviso = document.getElementById(&quot;aviso-documento&quot;);

campo.addEventListener(&quot;input&quot;, () =&gt; {
	const resultado = validarDocumento(campo.value);

	if (!campo.value.trim()) {
		aviso.textContent = &quot;&quot;;
		campo.removeAttribute(&quot;aria-invalid&quot;);
		return;
	}

	if (resultado.valido) {
		aviso.textContent = `${resultado.tipo} válido ✓`;
		aviso.className = &quot;aviso ok&quot;;
		campo.setAttribute(&quot;aria-invalid&quot;, &quot;false&quot;);
	} else {
		aviso.textContent =
			resultado.tipo === &quot;desconocido&quot;
				? &quot;Formato no reconocido&quot;
				: `La letra de control no es correcta`;
		aviso.className = &quot;aviso error&quot;;
		campo.setAttribute(&quot;aria-invalid&quot;, &quot;true&quot;);
	}
});
</code></pre>
<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>
<h2>Una última consideración antes de confiar ciegamente en esto</h2>
<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>
<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>
<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>
]]>
      </content:encoded>
      <pubDate>Tue, 10 Mar 2026 00:00:00 GMT</pubDate>
      <meta property="og:image" content="https://paigar.eu/validar-dni-nie-pasaporte.png"/>
    </item>
    <item>
      <title>Imágenes Open Graph automáticas con Lume</title>
      <link>https://paigar.eu/imagenes-og-lume/</link>
      <guid isPermaLink="false">https://paigar.eu/imagenes-og-lume/</guid>
      <description>
        Cuando compartes un enlace en redes sociales, lo primero que ves es una imagen. Crear esas imágenes a mano para cada post es tedioso, y conectar un servicio externo es sobredimensionar el problema. La solución está en el propio build: un generador TypeScript produce un SVG por post, y resvg-wasm los convierte a PNG durante la compilación. Sin servicios externos, sin imágenes que mantener a mano.
      </description>
      <content:encoded>
        <![CDATA[<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>
<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>
<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>
<h2>La estrategia</h2>
<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>
<p>El flujo completo:</p>
<ol>
<li>Un generador TypeScript (<code>og-images.page.ts</code>) produce un archivo SVG por cada post</li>
<li>El SVG contiene el título del post, la sección y el branding del sitio</li>
<li>Después del build, un evento <code>afterBuild</code> en <code>_config.ts</code> convierte todos los SVG a PNG con resvg</li>
<li>Las meta tags <code>og:image</code> apuntan a las imágenes PNG generadas</li>
</ol>
<p>Todo ocurre en el build. No hay servicios externos, no hay APIs, no hay imágenes que mantener a mano.</p>
<h2>El generador: og-images.page.ts</h2>
<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>
<p>El generador empieza recopilando todos los posts de ambas secciones con <code>search.pages()</code>:</p>
<pre><code class="language-typescript">export default function* ({ search }: Lume.Data) {
	const posts = [...search.pages(&quot;bitacora&quot;), ...search.pages(&quot;reflexiones&quot;)];

	for (const post of posts) {
		const title = post.title as string;
		const tags = (post.tags || []) as string[];
		// ...
	}
}
</code></pre>
<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>
<h3>Partir el título en líneas</h3>
<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>
<pre><code class="language-typescript">const parts = title.split(&quot; &quot;);
const titleLines: string[] = parts.reduce((prev: string[], current: string) =&gt; {
	if (!prev.length) return [current];
	const lastLine = prev[prev.length - 1];
	if (lastLine.length + 1 + current.length &gt; 36) {
		return [...prev, current];
	}
	prev[prev.length - 1] = lastLine + &quot; &quot; + current;
	return prev;
}, []);
</code></pre>
<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>
<h3>Posición vertical del título</h3>
<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>
<pre><code class="language-typescript">const lineCount = titleLines.length;
let titleY: number;
if (lineCount === 1) titleY = 310;
else if (lineCount === 2) titleY = 280;
else if (lineCount === 3) titleY = 240;
else titleY = 200;
</code></pre>
<h3>Sección y slug</h3>
<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>
<pre><code class="language-typescript">const seccion = tags.includes(&quot;bitacora&quot;) ? &quot;BITACORA&quot; : &quot;REFLEXIONES&quot;;

const urlParts = (post.url as string).split(&quot;/&quot;).filter(Boolean);
const slug = urlParts[urlParts.length - 1];
</code></pre>
<h3>El SVG</h3>
<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>
<pre><code class="language-typescript">const tspans = titleLines
	.map(
		(line: string, i: number) =&gt;
			`    &lt;tspan x=&quot;80&quot; y=&quot;${titleY + i * 62}&quot;&gt;${escapeXml(line)}&lt;/tspan&gt;`,
	)
	.join(&quot;\n&quot;);
</code></pre>
<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>
<p>Finalmente, el generador produce el archivo:</p>
<pre><code class="language-typescript">yield {
  url: `/og-images/${slug}.svg`,
  content: svg,
};
</code></pre>
<h2>Por qué PNG y no JPEG o WebP</h2>
<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>
<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>
<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>
<h2>La conversión: SVG a PNG con resvg-wasm</h2>
<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>
<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>
<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>
<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>
<pre><code class="language-typescript">import { render as renderSvgToPng } from &quot;https://deno.land/x/resvg_wasm@0.2.0/mod.ts&quot;;

site.addEventListener(&quot;afterBuild&quot;, async () =&gt; {
	const ogDir = site.dest() + &quot;/og-images&quot;;

	try {
		const entries = [...Deno.readDirSync(ogDir)];
		const svgFiles = entries.filter((e) =&gt; e.name.endsWith(&quot;.svg&quot;));

		if (svgFiles.length === 0) return;

		let converted = 0;
		for (const entry of svgFiles) {
			const svgPath = `${ogDir}/${entry.name}`;
			const pngPath = svgPath.replace(&quot;.svg&quot;, &quot;.png&quot;);
			const svgContent = await Deno.readTextFile(svgPath);

			const pngBuffer = await renderSvgToPng(svgContent);
			await Deno.writeFile(pngPath, pngBuffer);
			await Deno.remove(svgPath);
			converted++;
		}

		console.log(`[og-images] ${converted} SVG convertidos a PNG`);
	} catch (err) {
		if (!(err instanceof Deno.errors.NotFound)) {
			console.error(&quot;[og-images] Error:&quot;, err);
		}
	}
});
</code></pre>
<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>
<h2>Las meta tags</h2>
<p>Solo queda apuntar las meta tags a las imágenes generadas. En el layout base:</p>
<pre><code class="language-html">{{ if tags &amp;&amp; (tags.includes(&quot;bitacora&quot;) || tags.includes(&quot;reflexiones&quot;)) }}
&lt;meta
	property=&quot;og:image&quot;
	content=&quot;{{ metadata.url }}/og-images/{{ page.src.slug }}.png&quot; /&gt;
{{ else }}
&lt;meta property=&quot;og:image&quot; content=&quot;{{ metadata.url }}/og-images/default.png&quot; /&gt;
{{ /if }}
&lt;meta property=&quot;og:image:width&quot; content=&quot;1200&quot; /&gt;
&lt;meta property=&quot;og:image:height&quot; content=&quot;630&quot; /&gt;
&lt;meta name=&quot;twitter:card&quot; content=&quot;summary_large_image&quot; /&gt;
</code></pre>
<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>
<h2>Sobre las fuentes</h2>
<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>
<h2>El archivo completo</h2>
<p>Para referencia, este es el <code>og-images.page.ts</code> completo tal como funciona en producción:</p>
<pre><code class="language-typescript">export default function* ({ search }: Lume.Data) {
	const posts = [...search.pages(&quot;bitacora&quot;), ...search.pages(&quot;reflexiones&quot;)];

	for (const post of posts) {
		const title = post.title as string;
		const tags = (post.tags || []) as string[];

		const parts = title.split(&quot; &quot;);
		const titleLines: string[] = parts.reduce(
			(prev: string[], current: string) =&gt; {
				if (!prev.length) return [current];
				const lastLine = prev[prev.length - 1];
				if (lastLine.length + 1 + current.length &gt; 36) {
					return [...prev, current];
				}
				prev[prev.length - 1] = lastLine + &quot; &quot; + current;
				return prev;
			},
			[],
		);

		const lineCount = titleLines.length;
		let titleY: number;
		if (lineCount === 1) titleY = 310;
		else if (lineCount === 2) titleY = 280;
		else if (lineCount === 3) titleY = 240;
		else titleY = 200;

		const seccion = tags.includes(&quot;bitacora&quot;) ? &quot;BITACORA&quot; : &quot;REFLEXIONES&quot;;

		const urlParts = (post.url as string).split(&quot;/&quot;).filter(Boolean);
		const slug = urlParts[urlParts.length - 1];

		const tspans = titleLines
			.map(
				(line: string, i: number) =&gt;
					`    &lt;tspan x=&quot;80&quot; y=&quot;${titleY + i * 62}&quot;&gt;${escapeXml(line)}&lt;/tspan&gt;`,
			)
			.join(&quot;\n&quot;);

		const svg = `&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;
&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;

  &lt;!-- Fondo --&gt;
  &lt;rect width=&quot;1200&quot; height=&quot;630&quot; fill=&quot;#111118&quot;/&gt;

  &lt;!-- Barra naranja lateral --&gt;
  &lt;rect x=&quot;0&quot; y=&quot;0&quot; width=&quot;8&quot; height=&quot;630&quot; fill=&quot;#f86624&quot;/&gt;

  &lt;!-- Seccion --&gt;
  &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;

  &lt;!-- Titulo --&gt;
  &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;
${tspans}
  &lt;/text&gt;

  &lt;!-- Linea separadora --&gt;
  &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;

  &lt;!-- Branding --&gt;
  &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;

  &lt;!-- Punto naranja --&gt;
  &lt;circle cx=&quot;1120&quot; cy=&quot;568&quot; r=&quot;6&quot; fill=&quot;#f86624&quot;/&gt;

&lt;/svg&gt;`;

		yield {
			url: `/og-images/${slug}.svg`,
			content: svg,
		};
	}
}

function escapeXml(str: string): string {
	return str
		.replace(/&amp;/g, &quot;&amp;amp;&quot;)
		.replace(/&lt;/g, &quot;&amp;lt;&quot;)
		.replace(/&gt;/g, &quot;&amp;gt;&quot;)
		.replace(/&quot;/g, &quot;&amp;quot;&quot;)
		.replace(/'/g, &quot;&amp;apos;&quot;);
}
</code></pre>
<h2>La alternativa oficial: el plugin og_images</h2>
<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>
<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>
<ul>
<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>
<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>
<li><strong>Menos dependencias</strong> — el generador es un único archivo TypeScript de 89 líneas, sin configuración JSX ni paquetes adicionales.</li>
</ul>
<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>
<h2>El resultado</h2>
<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>
<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>
]]>
      </content:encoded>
      <pubDate>Tue, 03 Mar 2026 00:00:00 GMT</pubDate>
      <meta property="og:image" content="https://paigar.eu/imagenes-og-lume.png"/>
    </item>
    <item>
      <title>Construí una app con IA sin saber programar y gano 10.000 dólares al mes (y tú también puedes)</title>
      <link>https://paigar.eu/ia-apps-youtube-sin-programar/</link>
      <guid isPermaLink="false">https://paigar.eu/ia-apps-youtube-sin-programar/</guid>
      <description>
        YouTube está lleno de vídeos donde alguien construye una app sin saber programar, usando IA, y la vende por miles de dólares. ¿Es verdad? ¿Y si lo es, por qué lo cuentan?
      </description>
      <content:encoded>
        <![CDATA[<p>Llevas semanas viendo el mismo vídeo con distinto thumbnail. Un chico joven, o a veces no tan joven, con cara de no poder creérselo a sí mismo, señalando una pantalla en la que se ve una cifra con muchos ceros. El título varía poco: <em>&quot;Construí una app con IA sin saber programar y gano 12.000 dólares al mes&quot;</em>, <em>&quot;Cómo monté un SaaS en un fin de semana usando ChatGPT&quot;</em>, o la variante más aspiracional: <em>&quot;No sé código. Esta app me da libertad financiera.&quot;</em> Y tú, que llevas veinte años trabajando en lo tuyo, te quedas mirando la pantalla con una mezcla de curiosidad, escepticismo y esa incomodísima duda de si te estás perdiendo algo.</p>
<p>Vamos a hablar de todo esto.</p>
<h2>El patrón es siempre el mismo</h2>
<p>El formato está tan pulido que parece un producto en sí mismo. Comienza con el gancho: los ingresos, el número, la prueba de que funciona. Luego viene la historia de origen —normalmente alguien que no tiene formación técnica, que lo intentó con otras cosas, que encontró esta herramienta casi por casualidad—. Después, el proceso: capturas de pantalla de Claude o ChatGPT escribiendo código, un Bubble o un Cursor haciendo magia, una interfaz que parece profesional surgida de la nada. Y al final, los ingresos otra vez, esta vez desglosados por plataforma, con un gráfico que sube hacia la derecha como si fuera la ley natural del universo.</p>
<p>El vídeo dura entre ocho y quince minutos, tiene música de fondo que transmite urgencia sin ser molesta, y termina con una llamada a la acción: <em>suscríbete</em>, <em>apúntate a mi newsletter</em>, <em>compra mi curso de 97 dólares donde te lo explico todo</em>.</p>
<p>Ese último detalle es importante. Volvemos a él.</p>
<h2>¿Es real? Sí. Y también no</h2>
<p>La respuesta honesta es que el fenómeno es real pero las cifras son una selección muy conveniente de la realidad. Sí, existen personas que han construido herramientas funcionales con poca o ninguna experiencia en programación usando los modelos actuales de IA. Eso es verdad y es genuinamente notable. La barrera técnica para crear algo que funcione se ha reducido de forma dramática en los últimos dos años. Alguien con paciencia, criterio para entender qué quiere construir y disposición a iterar puede llegar bastante lejos.</p>
<p>Lo que ya no es tan claro es lo de los miles de dólares mensuales. No porque sea imposible —hay casos reales documentados— sino porque lo que ves en YouTube es el 0,1% que funcionó, contado por alguien que tiene incentivos muy concretos para contarlo de esa manera. El 99,9% de las apps construidas así no llegan a diez usuarios de pago, se abandonan en dos meses, o generan ingresos tan modestos que no justifican el tiempo invertido. Pero esos vídeos no se hacen porque no tienen thumbnail atractivo.</p>
<p>Hay también una cuestión de sincronización. Muchos de los casos que funcionaron lo hicieron en ventanas de tiempo muy específicas, cuando una categoría de herramienta no existía y había demanda sin cubrir. Llegar tarde a ese nicho, cuando ya hay cinco competidores haciendo lo mismo con más recursos, es otra historia.</p>
<h2>La paradoja que nadie responde</h2>
<p>Aquí está la pregunta que me parece más interesante y que curiosamente casi nadie hace en los comentarios: si realmente estás ganando doce mil dólares al mes con una app, ¿por qué lo cuentas?</p>
<p>No es una pregunta retórica ni un ataque. Es genuina. Si descubres una veta de oro, el comportamiento racional no es colgar un cartel en YouTube para que vengan otros a picar en el mismo sitio. Los negocios que funcionan de verdad tienden a no publicitarse a sí mismos como recetas replicables, precisamente porque la replicabilidad los destruye. Si mil personas ven tu vídeo y el 5% lo intenta, acabas de crear cincuenta competidores directos en tu nicho.</p>
<p>La respuesta, claro, es que el vídeo no es un gesto de generosidad ni un diario íntimo de éxito. Es parte del negocio. Y el negocio no es la app.</p>
<h2>El negocio de verdad es el canal</h2>
<p>Lo que están vendiendo no es la app. Están vendiendo la narrativa de que la app es posible. Y eso lo monetizan de cinco formas distintas que a menudo se solapan: los ingresos de YouTube por publicidad, los programas de afiliados de las herramientas que mencionan (Cursor, Bubble, Supabase, y cualquier SaaS que pague comisiones), los cursos y mentorías, la newsletter con upselling, y en algunos casos el propio prestigio que luego se convierte en consultoría o inversión.</p>
<p>Un canal de YouTube con cien mil suscriptores en el nicho de <em>indie hacking</em> o <em>no-code</em> puede generar entre dos mil y diez mil dólares al mes solo en publicidad, y otro tanto en afiliados si los links están bien colocados. Añade un curso de noventa y siete euros que se vende a trescientas personas al mes y tienes un negocio muy sólido que no tiene nada que ver con ninguna app.</p>
<p>¿Y la app? La app es el MacGuffin. Es lo que hace que el vídeo sea creíble, lo que da autoridad al creador, lo que convierte el canal en algo más que opiniones. Puede que exista, puede que incluso gane algo de dinero. Pero no es la fuente principal de ingresos. Es el argumento de venta del verdadero producto, que eres tú aprendiendo a hacer lo que él ya hizo.</p>
<h2>¿Merece la pena montar esto si ya tienes ingresos?</h2>
<p>Hay una última capa en todo esto que me parece fascinante desde el punto de vista del esfuerzo versus retorno. Imagina que tienes un negocio digital que te genera varios miles de euros al mes de forma más o menos estable, sin necesidad de crear contenido constantemente, sin algoritmos de por medio, sin tener que estar delante de una cámara con cara de entusiasmo cada semana.</p>
<p>¿Tiene sentido en ese contexto montar un canal de YouTube que te exige producir vídeos con regularidad, optimizar thumbnails, gestionar comentarios, depender del humor del algoritmo de Google y construir una audiencia desde cero? La respuesta no es automáticamente no, pero tampoco es automáticamente sí. Depende de si te gusta crear ese tipo de contenido por sí mismo, no solo como medio para un fin.</p>
<p>Porque eso es lo que muchos de estos creadores no dicen: hacer un canal de YouTube que funcione de verdad es un trabajo a tiempo completo, y hacerlo bien durante meses sin ingresos iniciales requiere una tolerancia a la incertidumbre bastante alta. El que ya tiene sus ingresos cubiertos por otra vía puede hacerlo desde una posición más cómoda, claro. Pero entonces tampoco tiene la urgencia narrativa del <em>&quot;lo construí desde cero sin nada&quot;</em> que hace que los vídeos enganchen.</p>
<p>La trampa es sutil: admiras a alguien que parece que ganó dos veces al mismo tiempo, con la app y con el canal, cuando en realidad probablemente perdió bastante tiempo y dinero con la app antes de que el canal se convirtiera en lo que realmente paga las facturas. El orden importa, y el orden raramente aparece en el thumbnail.</p>
]]>
      </content:encoded>
      <pubDate>Sat, 28 Feb 2026 00:00:00 GMT</pubDate>
      <meta property="og:image" content="https://paigar.eu/ia-apps-youtube.jpg"/>
    </item>
    <item>
      <title>La pequeña estafa cotidiana del pago con tarjeta</title>
      <link>https://paigar.eu/pago-con-tarjeta-comisiones/</link>
      <guid isPermaLink="false">https://paigar.eu/pago-con-tarjeta-comisiones/</guid>
      <description>
        Hemos normalizado pagar todo con tarjeta porque es cómodo, rápido y limpio. Pero hay un detalle del que casi nadie habla y que conviene mirar con calma: cada transacción deja un trozo del dinero en el banco, y al final del recorrido la cuenta sale rara.
      </description>
      <content:encoded>
        <![CDATA[<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>
<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>
<h2>El ejemplo del billete de cincuenta euros</h2>
<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>
<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>
<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>
<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>
<h2>Un peaje invisible que no existía con el efectivo</h2>
<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>
<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>
<h2>Y encima te cobran por usar tu propio dinero</h2>
<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>
<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>
<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>
<h2>El servicio que recibimos a cambio de toda esta sangría</h2>
<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>
<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>
<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>
<h2>Lo que estoy haciendo, sin pretender heroísmos</h2>
<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>
<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>
<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>
]]>
      </content:encoded>
      <pubDate>Mon, 23 Feb 2026 00:00:00 GMT</pubDate>
      <meta property="og:image" content="https://paigar.eu/pago-con-tarjeta-comisiones.png"/>
    </item>
    <item>
      <title>Mataora: la obra de arte que el Benidorm Fest 2026 no supo premiar</title>
      <link>https://paigar.eu/mataora-benidorm-fest-2026/</link>
      <guid isPermaLink="false">https://paigar.eu/mataora-benidorm-fest-2026/</guid>
      <description>
        Rosalinda Galán llegó al Benidorm Fest 2026 con una canción sobre Carmen, un plano secuencia que es pura artesanía televisiva y una actuación que debería estar en los libros de historia del festival. Pero no ganó. Y eso todavía me duele un poco.
      </description>
      <content:encoded>
        <![CDATA[<p>El Benidorm Fest 2026 ya es historia y lo voy a decir sin trampa: técnicamente ha sido el mejor de su historia. Producción impecable, realización de primera, puesta en escena que en varios momentos te hacía olvidar que estabas viendo la tele pública española un sábado por la noche. Hasta ahí, todo bien.</p>
<p><strong>Ahora lo otro. Porque hay un pero enorme, y no me lo puedo guardar.</strong></p>
<p>Algunas canciones que en disco sonaban muy bien hicieron un poco de aguas sobre el escenario. No voy a hacer leña del árbol caído, que para eso ya hay críticos con mucho tiempo libre. Pero sí voy a hablar —necesito hablar— de lo que me tiene todavía dándole vueltas días después de la final. De lo que, siendo honesto, me parece una injusticia bastante gorda.</p>
<h2>Rosalinda Galán y tres minutos que me dejaron sin palabras</h2>
<p>No sabía muy bien quién era Rosalinda Galán antes del festival. Lo reconozco. Sevillana, cantante de copla desde pequeña, se define a sí misma como &quot;folclórica estrafalaria&quot; —ya solo por eso merece todo mi respeto—, y llegó al Benidorm Fest con una canción llamada &quot;Mataora&quot; que, la primera vez que la escuché en disco, pensé: <em>qué cosa más rara y qué cosa más buena</em>.</p>
<p>La canción está construida sobre el personaje de Carmen, la de Mérimée, sí, la de la ópera de Bizet, esa Carmen que llevan siglos contando los demás. Pero aquí Carmen habla ella. Se presenta, reivindica su libertad, corta sus propias trenzas cuando intentan retenerla. &quot;Me agarran fuerte del pelo / y yo me corto las trenzas.&quot; Cuatro palabras por verso y ahí está todo dicho. No es una canción fácil ni pegadiza ni de esas que te tarareas en la ducha. Es una canción que te agarra de algún sitio y no te suelta.</p>
<p>Pero lo de la actuación en directo ya fue otra cosa.</p>
<h2>Un plano secuencia que no me esperaba ver en televisión</h2>
<p>No soy ningún experto en realización televisiva ni en técnica audiovisual. Lo que sé es lo que me gusta y lo que no. Y lo que vi en esa actuación me pareció de una categoría diferente a todo lo demás de la noche, y probablemente de todo lo que he visto en el festival desde que existe.</p>
<p>La filmaron en un único plano sin cortes —lo que se llama un plano secuencia, que ya es complicado de por sí— pero además jugando con pasar del blanco y negro al color según la música respiraba de una manera u otra. Los momentos más copleros, más de raíz, en blanco y negro. Y cuando la canción se abría y se electrificaba, el color aparecía con rojos y sombras que se proyectaban sobre Rosalinda y el fondo como si el escenario estuviera vivo. No sé cómo lo hicieron. Bueno, sí sé algo: en un momento dado la cámara subió por encima de ella en un plano desde arriba que me pareció imposible de conseguir en un directo, y resulta que lo lograron colgando literalmente a un operador del techo del pabellón. Del techo. En directo. En una final de televisión.</p>
<p>Cuando vi ese plano, paré. Me quedé mirando la pantalla sin hacer nada. Eso no me pasa muy a menudo.</p>
<h2>Lo que pasó después todavía no lo entiendo</h2>
<p>Ganó &quot;T Amaré&quot; de Tony Grox y LUCYCALYS. Que es un temazo, que tiene un estribillo enorme, que la gente lo coreaba y se lo pasaba en grande, y que tiene todo el derecho del mundo a existir y a gustar. No tengo nada en contra.</p>
<p>Pero es que Rosalinda Galán estaba ahí. Con esa canción. Con esa actuación. Con ese plano secuencia y esas sombras y esa voz y esa Carmen que por fin se contaba a sí misma. Y no ganó.</p>
<p>Los premios secundarios —el de Spotify, el de Univisión— me parecieron razonables para lo que eran. Pero la Sirenita de Oro, el premio gordo, ese era el de Rosalinda. O al menos eso pensé yo, y al parecer lo pensaba mucha gente, porque era una de las favoritas y terminó sin nada.</p>
<p>Entiendo que los festivales de televisión no los ganan siempre las propuestas más arriesgadas. Lo entiendo perfectamente. Llevo suficientes años viendo este tipo de cosas como para no sorprenderme. Pero entenderlo no me impide pensar que fue una injusticia. Una de esas injusticias menores, de las que no cambian el mundo, pero que duelen un poco de todas formas.</p>
<p>Rosalinda Galán subió a ese escenario con una canción sobre una mujer libre, con un homenaje muy discreto a su padre recién fallecido, y con una puesta en escena que demostraba que cuando alguien cree de verdad en lo que está haciendo, eso se ve. Vaya si se ve.</p>
<p>Que no haya ganado es cosa del festival. Pero que la actuación exista y que podamos volver a ella cuando queramos, eso ya es cosa de Rosalinda. Y de Carmen. Y de todas las que se cortan las trenzas.</p>
<hr>
<p><em>Si no la has visto: busca &quot;Mataora – Rosalinda Galán – Benidorm Fest 2026 Final&quot; en YouTube. Merece los tres minutos.</em></p>
]]>
      </content:encoded>
      <pubDate>Wed, 18 Feb 2026 00:00:00 GMT</pubDate>
      <meta property="og:image" content="https://paigar.eu/mataora-rosalinda-galan.jpg"/>
    </item>
  </channel>
</rss>