<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Blog de Lonepixell</title>
    <link>https://lonepixell.com/blog/</link>
    <description>Notas de taller del estudio: diseño, código y las razones detrás de cada decisión.</description>
    <language>es-mx</language>
    <atom:link href="https://lonepixell.com/rss.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Un campo de píxeles sin una sola librería</title>
      <link>https://lonepixell.com/blog/un-campo-de-pixeles-sin-libreria/</link>
      <guid isPermaLink="true">https://lonepixell.com/blog/un-campo-de-pixeles-sin-libreria/</guid>
      <pubDate>Sat, 22 Aug 2026 12:00:00 GMT</pubDate>
      <description>El fondo animado del hero pesa menos que el icono de una librería de animación. Así funciona, y por qué no usamos GSAP ni Three.js.</description>
      <category>código</category>
      <category>movimiento</category>
      <category>rendimiento</category>
      <content:encoded><![CDATA[<p>El hero de esta página tiene un campo de píxeles que crece desde el punto naranja
del titular y se asienta solo. Es el único momento de firma del sitio. No usa
GSAP, ni Three.js, ni Lenis, ni una librería de partículas. Son unas cuarenta
líneas de Canvas 2D y un <code>requestAnimationFrame</code> que se apaga a los dos segundos.</p>
<p>Esto no es purismo. Es aritmética: GSAP con ScrollTrigger son ~45 KB
comprimidos, Three.js pasa de 150 KB. <strong>Toda esta página pesa 57 KB.</strong> Meter una
librería de movimiento habría triplicado el peso del sitio para animar una cosa.</p>
<h2 id="aleatorio-pero-siempre-el-mismo"><a class="anchor" href="#aleatorio-pero-siempre-el-mismo">Aleatorio, pero siempre el mismo</a></h2>
<p>El primer requisito es raro: el campo tiene que verse aleatorio pero ser
<strong>determinista</strong>. Si cada recarga dibuja un patrón distinto, deja de ser una
identidad y pasa a ser ruido. Y <code>Math.random()</code> no sirve, porque no se puede
preguntar &quot;¿qué valor te tocaría en la celda 12, 7?&quot;.</p>
<p>La solución es una función hash: mismas coordenadas, mismo número, siempre, sin
guardar nada.</p>
<figure class="code">
  <figcaption class="code-bar label">
    <span class="code-name">src/js/hero-pixels.js</span>
    <button class="code-copy label" type="button" hidden>
      <svg class="icon" aria-hidden="true"><use href="#i-copy" /></svg>
      <span aria-live="polite">Copiar</span>
    </button>
  </figcaption>
  <pre class="shiki lonepixell" style="background-color:var(--sh-background);color:var(--sh-foreground)" tabindex="0"><code><span class="line"><span style="color:var(--sh-token-keyword)">function</span><span style="color:var(--sh-token-function)"> hash</span><span style="color:var(--sh-foreground)">(x</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-foreground)"> y) {</span></span>
<span class="line"><span style="color:var(--sh-token-keyword)">  let</span><span style="color:var(--sh-foreground)"> h </span><span style="color:var(--sh-token-keyword)">=</span><span style="color:var(--sh-foreground)"> (x </span><span style="color:var(--sh-token-keyword)">*</span><span style="color:var(--sh-token-constant)"> 374761393</span><span style="color:var(--sh-token-keyword)"> +</span><span style="color:var(--sh-foreground)"> y </span><span style="color:var(--sh-token-keyword)">*</span><span style="color:var(--sh-token-constant)"> 668265263</span><span style="color:var(--sh-foreground)">) </span><span style="color:var(--sh-token-keyword)">^</span><span style="color:var(--sh-token-constant)"> 0x5bf03635</span></span>
<span class="line"><span style="color:var(--sh-foreground)">  h </span><span style="color:var(--sh-token-keyword)">=</span><span style="color:var(--sh-token-constant)"> Math</span><span style="color:var(--sh-token-function)">.imul</span><span style="color:var(--sh-foreground)">(h </span><span style="color:var(--sh-token-keyword)">^</span><span style="color:var(--sh-foreground)"> (h </span><span style="color:var(--sh-token-keyword)">>>></span><span style="color:var(--sh-token-constant)"> 13</span><span style="color:var(--sh-foreground)">)</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-token-constant)"> 1274126177</span><span style="color:var(--sh-foreground)">)</span></span>
<span class="line"><span style="color:var(--sh-token-keyword)">  return</span><span style="color:var(--sh-foreground)"> ((h </span><span style="color:var(--sh-token-keyword)">^</span><span style="color:var(--sh-foreground)"> (h </span><span style="color:var(--sh-token-keyword)">>>></span><span style="color:var(--sh-token-constant)"> 16</span><span style="color:var(--sh-foreground)">)) </span><span style="color:var(--sh-token-keyword)">>>></span><span style="color:var(--sh-token-constant)"> 0</span><span style="color:var(--sh-foreground)">) </span><span style="color:var(--sh-token-keyword)">/</span><span style="color:var(--sh-token-constant)"> 4294967296</span></span>
<span class="line"><span style="color:var(--sh-foreground)">}</span></span></code></pre>
</figure><p>Devuelve un número entre 0 y 1 a partir de dos enteros. Es el mismo truco de los
generadores de terreno procedural, en su versión más pequeña posible.</p>
<h2 id="crecer-no-aparecer"><a class="anchor" href="#crecer-no-aparecer">Crecer, no aparecer</a></h2>
<p>El campo no se dibuja de golpe: se propaga desde una celda semilla, como algo que
se extiende. Un recorrido en anchura sobre la retícula, donde cada celda vecina
entra o no según su propio hash y con una probabilidad que baja con la
distancia, para que el campo se apague en los bordes en vez de cortarse.</p>
<figure class="code">
  <figcaption class="code-bar label">
    <span class="code-name">js</span>
    <button class="code-copy label" type="button" hidden>
      <svg class="icon" aria-hidden="true"><use href="#i-copy" /></svg>
      <span aria-live="polite">Copiar</span>
    </button>
  </figcaption>
  <pre class="shiki lonepixell" style="background-color:var(--sh-background);color:var(--sh-foreground)" tabindex="0"><code><span class="line"><span style="color:var(--sh-token-keyword)">const</span><span style="color:var(--sh-token-constant)"> queue</span><span style="color:var(--sh-token-keyword)"> =</span><span style="color:var(--sh-foreground)"> [[seedX</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-foreground)"> seedY</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-token-constant)"> 0</span><span style="color:var(--sh-foreground)">]]</span></span>
<span class="line"><span style="color:var(--sh-token-keyword)">let</span><span style="color:var(--sh-foreground)"> head </span><span style="color:var(--sh-token-keyword)">=</span><span style="color:var(--sh-token-constant)"> 0</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--sh-token-keyword)">while</span><span style="color:var(--sh-foreground)"> (head </span><span style="color:var(--sh-token-keyword)">&#x3C;</span><span style="color:var(--sh-token-constant)"> queue</span><span style="color:var(--sh-foreground)">.</span><span style="color:var(--sh-token-constant)">length</span><span style="color:var(--sh-token-keyword)"> &#x26;&#x26;</span><span style="color:var(--sh-foreground)"> head </span><span style="color:var(--sh-token-keyword)">&#x3C;</span><span style="color:var(--sh-foreground)"> budget) {</span></span>
<span class="line"><span style="color:var(--sh-token-keyword)">  const</span><span style="color:var(--sh-foreground)"> [</span><span style="color:var(--sh-token-constant)">x</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-token-constant)"> y</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-token-constant)"> depth</span><span style="color:var(--sh-foreground)">] </span><span style="color:var(--sh-token-keyword)">=</span><span style="color:var(--sh-foreground)"> queue[head</span><span style="color:var(--sh-token-keyword)">++</span><span style="color:var(--sh-foreground)">]</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--sh-token-keyword)">  for</span><span style="color:var(--sh-foreground)"> (</span><span style="color:var(--sh-token-keyword)">const</span><span style="color:var(--sh-foreground)"> [</span><span style="color:var(--sh-token-constant)">nx</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-token-constant)"> ny</span><span style="color:var(--sh-foreground)">] </span><span style="color:var(--sh-token-keyword)">of</span><span style="color:var(--sh-foreground)"> [[x </span><span style="color:var(--sh-token-keyword)">+</span><span style="color:var(--sh-token-constant)"> 1</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-foreground)"> y]</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-foreground)"> [x </span><span style="color:var(--sh-token-keyword)">-</span><span style="color:var(--sh-token-constant)"> 1</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-foreground)"> y]</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-foreground)"> [x</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-foreground)"> y </span><span style="color:var(--sh-token-keyword)">+</span><span style="color:var(--sh-token-constant)"> 1</span><span style="color:var(--sh-foreground)">]</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-foreground)"> [x</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-foreground)"> y </span><span style="color:var(--sh-token-keyword)">-</span><span style="color:var(--sh-token-constant)"> 1</span><span style="color:var(--sh-foreground)">]]) {</span></span>
<span class="line"><span style="color:var(--sh-token-keyword)">    if</span><span style="color:var(--sh-foreground)"> (seen[ny </span><span style="color:var(--sh-token-keyword)">*</span><span style="color:var(--sh-foreground)"> cols </span><span style="color:var(--sh-token-keyword)">+</span><span style="color:var(--sh-foreground)"> nx]) </span><span style="color:var(--sh-token-keyword)">continue</span></span>
<span class="line"><span style="color:var(--sh-token-comment)">    // El umbral sube con la distancia: el campo se disuelve, no se recorta.</span></span>
<span class="line"><span style="color:var(--sh-token-keyword)">    if</span><span style="color:var(--sh-foreground)"> (</span><span style="color:var(--sh-token-function)">hash</span><span style="color:var(--sh-foreground)">(nx</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-foreground)"> ny) </span><span style="color:var(--sh-token-keyword)">></span><span style="color:var(--sh-token-constant)"> 0.93</span><span style="color:var(--sh-token-keyword)"> -</span><span style="color:var(--sh-foreground)"> depth </span><span style="color:var(--sh-token-keyword)">*</span><span style="color:var(--sh-token-constant)"> 0.0085</span><span style="color:var(--sh-foreground)">) </span><span style="color:var(--sh-token-keyword)">continue</span></span>
<span class="line"><span style="color:var(--sh-foreground)">    seen[ny </span><span style="color:var(--sh-token-keyword)">*</span><span style="color:var(--sh-foreground)"> cols </span><span style="color:var(--sh-token-keyword)">+</span><span style="color:var(--sh-foreground)"> nx] </span><span style="color:var(--sh-token-keyword)">=</span><span style="color:var(--sh-token-constant)"> 1</span></span>
<span class="line"><span style="color:var(--sh-token-constant)">    queue</span><span style="color:var(--sh-token-function)">.push</span><span style="color:var(--sh-foreground)">([nx</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-foreground)"> ny</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-foreground)"> depth </span><span style="color:var(--sh-token-keyword)">+</span><span style="color:var(--sh-token-constant)"> 1</span><span style="color:var(--sh-foreground)">])</span></span>
<span class="line"><span style="color:var(--sh-foreground)">  }</span></span>
<span class="line"><span style="color:var(--sh-foreground)">}</span></span></code></pre>
</figure><p>El <code>budget</code> es el detalle que hace que esto sea viable en un teléfono: en
escritorio son ~1250 celdas, en móvil 420. No es una degradación visual
perceptible; es la diferencia entre 60 fps y un ventilador.</p>
<h2 id="el-truco-de-rendimiento-que-si-importa"><a class="anchor" href="#el-truco-de-rendimiento-que-si-importa">El truco de rendimiento que sí importa</a></h2>
<p>Un <code>fillRect</code> por celda con un <code>fillStyle</code> distinto cada vez es lento, porque
cada cambio de estilo es un cambio de estado del contexto. La solución no es
dibujar menos: es <strong>agrupar</strong>.</p>
<p>Las opacidades se cuantizan en diez cubos, y se pinta cubo por cubo. Diez cambios
de <code>fillStyle</code> en total, en lugar de mil doscientos:</p>
<figure class="code">
  <figcaption class="code-bar label">
    <span class="code-name">js</span>
    <button class="code-copy label" type="button" hidden>
      <svg class="icon" aria-hidden="true"><use href="#i-copy" /></svg>
      <span aria-live="polite">Copiar</span>
    </button>
  </figcaption>
  <pre class="shiki lonepixell" style="background-color:var(--sh-background);color:var(--sh-foreground)" tabindex="0"><code><span class="line"><span style="color:var(--sh-token-keyword)">const</span><span style="color:var(--sh-token-constant)"> buckets</span><span style="color:var(--sh-token-keyword)"> =</span><span style="color:var(--sh-token-constant)"> Array</span><span style="color:var(--sh-token-function)">.from</span><span style="color:var(--sh-foreground)">({ length</span><span style="color:var(--sh-token-keyword)">:</span><span style="color:var(--sh-token-constant)"> 10</span><span style="color:var(--sh-foreground)"> }</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-foreground)"> () </span><span style="color:var(--sh-token-keyword)">=></span><span style="color:var(--sh-foreground)"> [])</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--sh-token-keyword)">for</span><span style="color:var(--sh-foreground)"> (</span><span style="color:var(--sh-token-keyword)">const</span><span style="color:var(--sh-token-constant)"> cell</span><span style="color:var(--sh-token-keyword)"> of</span><span style="color:var(--sh-foreground)"> cells) {</span></span>
<span class="line"><span style="color:var(--sh-foreground)">  buckets[</span><span style="color:var(--sh-token-constant)">Math</span><span style="color:var(--sh-token-function)">.min</span><span style="color:var(--sh-foreground)">(</span><span style="color:var(--sh-token-constant)">9</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-foreground)"> (</span><span style="color:var(--sh-token-constant)">cell</span><span style="color:var(--sh-foreground)">.alpha </span><span style="color:var(--sh-token-keyword)">*</span><span style="color:var(--sh-token-constant)"> 10</span><span style="color:var(--sh-foreground)">) </span><span style="color:var(--sh-token-keyword)">|</span><span style="color:var(--sh-token-constant)"> 0</span><span style="color:var(--sh-foreground)">)]</span><span style="color:var(--sh-token-function)">.push</span><span style="color:var(--sh-foreground)">(cell)</span></span>
<span class="line"><span style="color:var(--sh-foreground)">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--sh-token-constant)">buckets</span><span style="color:var(--sh-token-function)">.forEach</span><span style="color:var(--sh-foreground)">((cells</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-foreground)"> i) </span><span style="color:var(--sh-token-keyword)">=></span><span style="color:var(--sh-foreground)"> {</span></span>
<span class="line"><span style="color:var(--sh-token-keyword)">  if</span><span style="color:var(--sh-foreground)"> (</span><span style="color:var(--sh-token-keyword)">!</span><span style="color:var(--sh-token-constant)">cells</span><span style="color:var(--sh-foreground)">.</span><span style="color:var(--sh-token-constant)">length</span><span style="color:var(--sh-foreground)">) </span><span style="color:var(--sh-token-keyword)">return</span></span>
<span class="line"><span style="color:var(--sh-token-constant)">  ctx</span><span style="color:var(--sh-foreground)">.fillStyle </span><span style="color:var(--sh-token-keyword)">=</span><span style="color:var(--sh-token-string-expression)"> `rgba(20, 20, 18, </span><span style="color:var(--sh-token-keyword)">${</span><span style="color:var(--sh-foreground)">(i </span><span style="color:var(--sh-token-keyword)">+</span><span style="color:var(--sh-token-constant)"> 0.5</span><span style="color:var(--sh-foreground)">) </span><span style="color:var(--sh-token-keyword)">/</span><span style="color:var(--sh-token-constant)"> 10</span><span style="color:var(--sh-token-keyword)">}</span><span style="color:var(--sh-token-string-expression)">)`</span></span>
<span class="line"><span style="color:var(--sh-token-keyword)">  for</span><span style="color:var(--sh-foreground)"> (</span><span style="color:var(--sh-token-keyword)">const</span><span style="color:var(--sh-token-constant)"> c</span><span style="color:var(--sh-token-keyword)"> of</span><span style="color:var(--sh-foreground)"> cells) </span><span style="color:var(--sh-token-constant)">ctx</span><span style="color:var(--sh-token-function)">.fillRect</span><span style="color:var(--sh-foreground)">(</span><span style="color:var(--sh-token-constant)">c</span><span style="color:var(--sh-foreground)">.x</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-token-constant)"> c</span><span style="color:var(--sh-foreground)">.y</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-foreground)"> size</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-foreground)"> size)</span></span>
<span class="line"><span style="color:var(--sh-foreground)">})</span></span></code></pre>
</figure><p>Nadie nota la diferencia entre 1000 opacidades distintas y 10. Todo el mundo
nota un scroll a 30 fps.</p>
<h2 id="sembrado-en-la-posicion-real"><a class="anchor" href="#sembrado-en-la-posicion-real">Sembrado en la posición real</a></h2>
<p>El campo tiene que nacer exactamente del punto naranja del <code>&lt;h1&gt;</code>. No de una
coordenada aproximada escrita a mano: del punto <strong>renderizado</strong>, que cambia según
la tipografía cargada, el ancho de pantalla y el tamaño de fuente del sistema.</p>
<figure class="code">
  <figcaption class="code-bar label">
    <span class="code-name">src/js/main.js</span>
    <button class="code-copy label" type="button" hidden>
      <svg class="icon" aria-hidden="true"><use href="#i-copy" /></svg>
      <span aria-live="polite">Copiar</span>
    </button>
  </figcaption>
  <pre class="shiki lonepixell" style="background-color:var(--sh-background);color:var(--sh-foreground)" tabindex="0"><code><span class="line"><span style="color:var(--sh-token-keyword)">const</span><span style="color:var(--sh-token-constant)"> fonts</span><span style="color:var(--sh-token-keyword)"> =</span><span style="color:var(--sh-token-constant)"> document</span><span style="color:var(--sh-foreground)">.</span><span style="color:var(--sh-token-constant)">fonts</span><span style="color:var(--sh-foreground)">?.ready </span><span style="color:var(--sh-token-keyword)">??</span><span style="color:var(--sh-token-constant)"> Promise</span><span style="color:var(--sh-token-function)">.resolve</span><span style="color:var(--sh-foreground)">()</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--sh-token-constant)">Promise</span><span style="color:var(--sh-token-function)">.race</span><span style="color:var(--sh-foreground)">([fonts</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-token-keyword)"> new</span><span style="color:var(--sh-token-constant)"> Promise</span><span style="color:var(--sh-foreground)">((r) </span><span style="color:var(--sh-token-keyword)">=></span><span style="color:var(--sh-token-function)"> setTimeout</span><span style="color:var(--sh-foreground)">(r</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-token-constant)"> 400</span><span style="color:var(--sh-foreground)">))])</span><span style="color:var(--sh-token-function)">.then</span><span style="color:var(--sh-foreground)">(</span><span style="color:var(--sh-token-keyword)">async</span><span style="color:var(--sh-foreground)"> () </span><span style="color:var(--sh-token-keyword)">=></span><span style="color:var(--sh-foreground)"> {</span></span>
<span class="line"><span style="color:var(--sh-token-keyword)">  const</span><span style="color:var(--sh-foreground)"> { </span><span style="color:var(--sh-token-constant)">initHeroPixels</span><span style="color:var(--sh-foreground)"> } </span><span style="color:var(--sh-token-keyword)">=</span><span style="color:var(--sh-token-keyword)"> await</span><span style="color:var(--sh-token-keyword)"> import</span><span style="color:var(--sh-foreground)">(</span><span style="color:var(--sh-token-string-expression)">'./hero-pixels.js'</span><span style="color:var(--sh-foreground)">)</span></span>
<span class="line"><span style="color:var(--sh-token-function)">  initHeroPixels</span><span style="color:var(--sh-foreground)">(canvas</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-foreground)"> seed</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-foreground)"> { staticOnly</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-foreground)"> hover</span><span style="color:var(--sh-token-keyword)">:</span><span style="color:var(--sh-token-constant)"> env</span><span style="color:var(--sh-foreground)">.hover })</span></span>
<span class="line"><span style="color:var(--sh-foreground)">})</span></span></code></pre>
</figure><p>Dos cosas pasan aquí. La primera: se espera a <code>document.fonts.ready</code>, porque
antes de que cargue Instrument Sans el punto está en otro sitio y el campo
nacería descolocado. La segunda: se espera <strong>con techo</strong>. Si una fuente falla, el
<code>Promise.race</code> cede a los 400 ms y el hero se dibuja igual. Una promesa sin
timeout es un bug esperando a una mala conexión.</p>
<p>El módulo además se importa dinámicamente, así que su peso no entra en el paquete
inicial. Y quien pidió menos movimiento, tiene modo de ahorro de datos o está en
un equipo de gama baja recibe el campo ya asentado, sin animación:</p>
<figure class="code">
  <figcaption class="code-bar label">
    <span class="code-name">src/js/env.js</span>
    <button class="code-copy label" type="button" hidden>
      <svg class="icon" aria-hidden="true"><use href="#i-copy" /></svg>
      <span aria-live="polite">Copiar</span>
    </button>
  </figcaption>
  <pre class="shiki lonepixell" style="background-color:var(--sh-background);color:var(--sh-foreground)" tabindex="0"><code><span class="line"><span style="color:var(--sh-token-keyword)">export</span><span style="color:var(--sh-token-keyword)"> const</span><span style="color:var(--sh-token-constant)"> staticOnly</span><span style="color:var(--sh-token-keyword)"> =</span><span style="color:var(--sh-token-constant)"> env</span><span style="color:var(--sh-foreground)">.reduced </span><span style="color:var(--sh-token-keyword)">||</span><span style="color:var(--sh-token-constant)"> env</span><span style="color:var(--sh-foreground)">.saveData </span><span style="color:var(--sh-token-keyword)">||</span><span style="color:var(--sh-token-constant)"> env</span><span style="color:var(--sh-foreground)">.lowEnd</span></span></code></pre>
</figure><h2 id="la-animacion-que-se-apaga-sola"><a class="anchor" href="#la-animacion-que-se-apaga-sola">La animación que se apaga sola</a></h2>
<p>Lo último, y lo más fácil de olvidar: el bucle <strong>termina</strong>. A los dos segundos el
campo está asentado, se cancela el <code>rAF</code> y la pestaña vuelve a costar cero. Una
animación ambiental que corre para siempre es una batería que se vacía para
siempre.</p>
<p>Un fondo animado no tiene por qué costar 200 KB y un núcleo de CPU. Casi siempre
cuesta eso porque nadie preguntó cuánto tenía que costar.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Medir el espaciado en vez de opinarlo</title>
      <link>https://lonepixell.com/blog/medir-el-espaciado-en-vez-de-opinarlo/</link>
      <guid isPermaLink="true">https://lonepixell.com/blog/medir-el-espaciado-en-vez-de-opinarlo/</guid>
      <pubDate>Thu, 20 Aug 2026 12:00:00 GMT</pubDate>
      <description>Un script de Playwright recorre la página, mide los huecos verticales reales y convierte una discusión de gustos en un número.</description>
      <category>proceso</category>
      <category>css</category>
      <content:encoded><![CDATA[<p>La conversación más cara del diseño web es esta:</p>
<blockquote>
<p>—Le falta aire.
—Yo lo veo bien.</p>
</blockquote>
<p>No hay forma de ganarla, porque nadie está midiendo nada. Los dos están
recordando la página, no viéndola. Y un recuerdo de una página no tiene números.</p>
<h2 id="el-problema-real"><a class="anchor" href="#el-problema-real">El problema real</a></h2>
<p>Al construir esta página apareció el síntoma clásico: en el monitor grande todo
respiraba, y en el teléfono algunas secciones parecían desconectadas entre sí. No
&quot;apretadas&quot; — al revés: <strong>tan separadas que el ojo perdía la relación</strong> entre un
titular y su párrafo.</p>
<p>Ese es el matiz que casi siempre se pierde. Generosidad no es lo mismo que vacío.
Un hueco tan grande que rompe la jerarquía de proximidad no se lee como lujo, se
lee como un error de maquetación.</p>
<h2 id="la-escala-primero"><a class="anchor" href="#la-escala-primero">La escala primero</a></h2>
<p>Antes de medir hay que tener contra qué medir. Todo el espaciado del sitio sale
de una escala fluida, y vive en un solo archivo:</p>
<figure class="code">
  <figcaption class="code-bar label">
    <span class="code-name">src/styles/tokens.css</span>
    <button class="code-copy label" type="button" hidden>
      <svg class="icon" aria-hidden="true"><use href="#i-copy" /></svg>
      <span aria-live="polite">Copiar</span>
    </button>
  </figcaption>
  <pre class="shiki lonepixell" style="background-color:var(--sh-background);color:var(--sh-foreground)" tabindex="0"><code><span class="line"><span style="color:var(--sh-foreground)">--space-2xs: clamp(0</span><span style="color:var(--sh-token-function)">.5rem</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-foreground)"> 0</span><span style="color:var(--sh-token-function)">.45rem</span><span style="color:var(--sh-token-keyword)"> +</span><span style="color:var(--sh-foreground)"> 0</span><span style="color:var(--sh-token-function)">.2vw</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-foreground)"> 0</span><span style="color:var(--sh-token-function)">.65rem</span><span style="color:var(--sh-foreground)">);</span></span>
<span class="line"><span style="color:var(--sh-foreground)">--space-xs:  clamp(0</span><span style="color:var(--sh-token-function)">.75rem</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-foreground)"> 0</span><span style="color:var(--sh-token-function)">.65rem</span><span style="color:var(--sh-token-keyword)"> +</span><span style="color:var(--sh-foreground)"> 0</span><span style="color:var(--sh-token-function)">.4vw</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-foreground)"> 1rem);</span></span>
<span class="line"><span style="color:var(--sh-foreground)">--space-s:   clamp(1</span><span style="color:var(--sh-token-function)">.15rem</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-foreground)"> 1rem </span><span style="color:var(--sh-token-keyword)">+</span><span style="color:var(--sh-foreground)"> 0</span><span style="color:var(--sh-token-function)">.6vw</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-foreground)"> 1</span><span style="color:var(--sh-token-function)">.6rem</span><span style="color:var(--sh-foreground)">);</span></span>
<span class="line"><span style="color:var(--sh-foreground)">--space-m:   clamp(1</span><span style="color:var(--sh-token-function)">.75rem</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-foreground)"> 1</span><span style="color:var(--sh-token-function)">.4rem</span><span style="color:var(--sh-token-keyword)"> +</span><span style="color:var(--sh-foreground)"> 1</span><span style="color:var(--sh-token-function)">.4vw</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-foreground)"> 2</span><span style="color:var(--sh-token-function)">.75rem</span><span style="color:var(--sh-foreground)">);</span></span>
<span class="line"><span style="color:var(--sh-foreground)">--space-l:   clamp(2</span><span style="color:var(--sh-token-function)">.75rem</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-foreground)"> 2</span><span style="color:var(--sh-token-function)">.1rem</span><span style="color:var(--sh-token-keyword)"> +</span><span style="color:var(--sh-foreground)"> 2</span><span style="color:var(--sh-token-function)">.6vw</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-foreground)"> 4</span><span style="color:var(--sh-token-function)">.5rem</span><span style="color:var(--sh-foreground)">);</span></span>
<span class="line"><span style="color:var(--sh-foreground)">--space-xl:  clamp(4rem</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-foreground)"> 2</span><span style="color:var(--sh-token-function)">.9rem</span><span style="color:var(--sh-token-keyword)"> +</span><span style="color:var(--sh-foreground)"> 4</span><span style="color:var(--sh-token-function)">.4vw</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-foreground)"> 7rem);</span></span>
<span class="line"><span style="color:var(--sh-foreground)">--space-2xl: clamp(5rem</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-foreground)"> 3</span><span style="color:var(--sh-token-function)">.4rem</span><span style="color:var(--sh-token-keyword)"> +</span><span style="color:var(--sh-foreground)"> 5</span><span style="color:var(--sh-token-function)">.6vw</span><span style="color:var(--sh-token-punctuation)">,</span><span style="color:var(--sh-foreground)"> 8rem);</span></span></code></pre>
</figure><p>La regla que hace que esto funcione no es técnica, es social: <strong>si un valor no
está en la escala, es un valor puesto a ojo y hay que justificarlo en un
comentario</strong>. Sin esa regla, la escala se convierte en decoración y a la tercera
semana el CSS vuelve a tener <code>margin-top: 37px</code>.</p>
<p>Encima de la escala hay dos roles con nombre, para no elegir un escalón cada vez:</p>
<figure class="code">
  <figcaption class="code-bar label">
    <span class="code-name">css</span>
    <button class="code-copy label" type="button" hidden>
      <svg class="icon" aria-hidden="true"><use href="#i-copy" /></svg>
      <span aria-live="polite">Copiar</span>
    </button>
  </figcaption>
  <pre class="shiki lonepixell" style="background-color:var(--sh-background);color:var(--sh-foreground)" tabindex="0"><code><span class="line"><span style="color:var(--sh-foreground)">--section-space: var(--space-2xl);   </span><span style="color:var(--sh-token-comment)">/* entre secciones */</span></span>
<span class="line"><span style="color:var(--sh-foreground)">--copy-space:    var(--space-l);     </span><span style="color:var(--sh-token-comment)">/* dentro de un bloque de texto */</span></span></code></pre>
</figure><h2 id="y-luego-el-metro"><a class="anchor" href="#y-luego-el-metro">Y luego el metro</a></h2>
<p>Con la escala puesta, la pregunta deja de ser &quot;¿se ve bien?&quot; y pasa a ser &quot;¿qué
hueco hay realmente entre estos dos elementos, en este ancho?&quot;. Eso sí se puede
responder con un script.</p>
<figure class="code">
  <figcaption class="code-bar label">
    <span class="code-name">scripts/measure-spacing.mjs</span>
    <button class="code-copy label" type="button" hidden>
      <svg class="icon" aria-hidden="true"><use href="#i-copy" /></svg>
      <span aria-live="polite">Copiar</span>
    </button>
  </figcaption>
  <pre class="shiki lonepixell" style="background-color:var(--sh-background);color:var(--sh-foreground)" tabindex="0"><code><span class="line"><span style="color:var(--sh-token-keyword)">const</span><span style="color:var(--sh-token-constant)"> gaps</span><span style="color:var(--sh-token-keyword)"> =</span><span style="color:var(--sh-token-keyword)"> await</span><span style="color:var(--sh-token-constant)"> page</span><span style="color:var(--sh-token-function)">.evaluate</span><span style="color:var(--sh-foreground)">(() </span><span style="color:var(--sh-token-keyword)">=></span><span style="color:var(--sh-foreground)"> {</span></span>
<span class="line"><span style="color:var(--sh-token-keyword)">  const</span><span style="color:var(--sh-token-constant)"> out</span><span style="color:var(--sh-token-keyword)"> =</span><span style="color:var(--sh-foreground)"> []</span></span>
<span class="line"><span style="color:var(--sh-token-keyword)">  for</span><span style="color:var(--sh-foreground)"> (</span><span style="color:var(--sh-token-keyword)">const</span><span style="color:var(--sh-token-constant)"> section</span><span style="color:var(--sh-token-keyword)"> of</span><span style="color:var(--sh-token-constant)"> document</span><span style="color:var(--sh-token-function)">.querySelectorAll</span><span style="color:var(--sh-foreground)">(</span><span style="color:var(--sh-token-string-expression)">'section'</span><span style="color:var(--sh-foreground)">)) {</span></span>
<span class="line"><span style="color:var(--sh-token-keyword)">    const</span><span style="color:var(--sh-token-constant)"> kids</span><span style="color:var(--sh-token-keyword)"> =</span><span style="color:var(--sh-foreground)"> [</span><span style="color:var(--sh-token-keyword)">...</span><span style="color:var(--sh-token-constant)">section</span><span style="color:var(--sh-foreground)">.children]</span><span style="color:var(--sh-token-function)">.filter</span><span style="color:var(--sh-foreground)">((el) </span><span style="color:var(--sh-token-keyword)">=></span><span style="color:var(--sh-token-constant)"> el</span><span style="color:var(--sh-foreground)">.offsetHeight </span><span style="color:var(--sh-token-keyword)">></span><span style="color:var(--sh-token-constant)"> 0</span><span style="color:var(--sh-foreground)">)</span></span>
<span class="line"><span style="color:var(--sh-token-keyword)">    for</span><span style="color:var(--sh-foreground)"> (</span><span style="color:var(--sh-token-keyword)">let</span><span style="color:var(--sh-foreground)"> i </span><span style="color:var(--sh-token-keyword)">=</span><span style="color:var(--sh-token-constant)"> 1</span><span style="color:var(--sh-foreground)">; i </span><span style="color:var(--sh-token-keyword)">&#x3C;</span><span style="color:var(--sh-token-constant)"> kids</span><span style="color:var(--sh-foreground)">.</span><span style="color:var(--sh-token-constant)">length</span><span style="color:var(--sh-foreground)">; i</span><span style="color:var(--sh-token-keyword)">++</span><span style="color:var(--sh-foreground)">) {</span></span>
<span class="line"><span style="color:var(--sh-token-keyword)">      const</span><span style="color:var(--sh-token-constant)"> prev</span><span style="color:var(--sh-token-keyword)"> =</span><span style="color:var(--sh-foreground)"> kids[i </span><span style="color:var(--sh-token-keyword)">-</span><span style="color:var(--sh-token-constant)"> 1</span><span style="color:var(--sh-foreground)">]</span><span style="color:var(--sh-token-function)">.getBoundingClientRect</span><span style="color:var(--sh-foreground)">()</span></span>
<span class="line"><span style="color:var(--sh-token-keyword)">      const</span><span style="color:var(--sh-token-constant)"> curr</span><span style="color:var(--sh-token-keyword)"> =</span><span style="color:var(--sh-foreground)"> kids[i]</span><span style="color:var(--sh-token-function)">.getBoundingClientRect</span><span style="color:var(--sh-foreground)">()</span></span>
<span class="line"><span style="color:var(--sh-token-constant)">      out</span><span style="color:var(--sh-token-function)">.push</span><span style="color:var(--sh-foreground)">({</span></span>
<span class="line"><span style="color:var(--sh-foreground)">        section</span><span style="color:var(--sh-token-keyword)">:</span><span style="color:var(--sh-token-constant)"> section</span><span style="color:var(--sh-token-function)">.</span><span style="color:var(--sh-token-constant)">className</span><span style="color:var(--sh-token-function)">.split</span><span style="color:var(--sh-foreground)">(</span><span style="color:var(--sh-token-string-expression)">' '</span><span style="color:var(--sh-foreground)">)[</span><span style="color:var(--sh-token-constant)">0</span><span style="color:var(--sh-foreground)">]</span><span style="color:var(--sh-token-punctuation)">,</span></span>
<span class="line"><span style="color:var(--sh-foreground)">        between</span><span style="color:var(--sh-token-keyword)">:</span><span style="color:var(--sh-token-string-expression)"> `</span><span style="color:var(--sh-token-keyword)">${</span><span style="color:var(--sh-foreground)">kids[i </span><span style="color:var(--sh-token-keyword)">-</span><span style="color:var(--sh-token-constant)"> 1</span><span style="color:var(--sh-foreground)">].tagName</span><span style="color:var(--sh-token-keyword)">}</span><span style="color:var(--sh-token-string-expression)"> → </span><span style="color:var(--sh-token-keyword)">${</span><span style="color:var(--sh-foreground)">kids[i].tagName</span><span style="color:var(--sh-token-keyword)">}</span><span style="color:var(--sh-token-string-expression)">`</span><span style="color:var(--sh-token-punctuation)">,</span></span>
<span class="line"><span style="color:var(--sh-foreground)">        gap</span><span style="color:var(--sh-token-keyword)">:</span><span style="color:var(--sh-token-constant)"> Math</span><span style="color:var(--sh-token-function)">.round</span><span style="color:var(--sh-foreground)">(</span><span style="color:var(--sh-token-constant)">curr</span><span style="color:var(--sh-foreground)">.top </span><span style="color:var(--sh-token-keyword)">-</span><span style="color:var(--sh-token-constant)"> prev</span><span style="color:var(--sh-foreground)">.bottom)</span><span style="color:var(--sh-token-punctuation)">,</span></span>
<span class="line"><span style="color:var(--sh-foreground)">      })</span></span>
<span class="line"><span style="color:var(--sh-foreground)">    }</span></span>
<span class="line"><span style="color:var(--sh-foreground)">  }</span></span>
<span class="line"><span style="color:var(--sh-token-keyword)">  return</span><span style="color:var(--sh-foreground)"> out</span></span>
<span class="line"><span style="color:var(--sh-foreground)">})</span></span></code></pre>
</figure><p>Se corre contra el build servido —no contra el HTML crudo— en dos anchos, y
escupe una tabla. Ahí es donde aparecen las sorpresas: huecos de 140 px que en el
CSS parecían de 60, porque un <code>margin</code> colapsado y un <code>padding</code> de sección se
estaban sumando sin que nadie lo hubiera pedido.</p>
<h2 id="lo-que-cambio-de-verdad"><a class="anchor" href="#lo-que-cambio-de-verdad">Lo que cambió de verdad</a></h2>
<p>El script no toma decisiones de diseño. Lo único que hace es quitar la opinión
de la parte que no era opinable. Después de correrlo:</p>
<ul>
<li>Tres secciones tenían el doble de separación de la que decía su CSS.</li>
<li>El par <code>02 / La intención</code> estaba a <code>--space-m</code> de su propio titular, cuando es
una sola unidad y pedía <code>--space-2xs</code>.</li>
<li>El teléfono no necesitaba &quot;más aire&quot;, necesitaba <strong>menos</strong>, en los sitios
exactos donde la proximidad tenía que hacer su trabajo.</li>
</ul>
<p>Ninguno de esos tres se veía a ojo. Los tres eran evidentes en la tabla.</p>
<h2 id="lo-que-me-llevo"><a class="anchor" href="#lo-que-me-llevo">Lo que me llevo</a></h2>
<p>Medir no sustituye al criterio: sustituye a la discusión sobre los hechos, para
que el criterio se pueda gastar en lo que sí importa. Cuando los dos lados de la
conversación ven el mismo número, la pregunta vuelve a ser la buena —<em>¿este hueco
ayuda a leer?</em>— en vez de la inútil, que es <em>¿quién tiene razón?</em></p>
]]></content:encoded>
    </item>
  </channel>
</rss>
