pattern y las texturas
Los dos sistemas de coordenadas de un patrón (que son distintos entre sí por omisión), el viewBox del patrón, patternTransform, y las costuras que aparecen al escalar.
pattern es el elemento más confuso de SVG y hay una razón concreta: tiene dos atributos de unidades, y cada uno tiene un valor por omisión distinto. patternUnits vale objectBoundingBox y patternContentUnits vale userSpaceOnUse, así que la celda del patrón se mide en fracciones de la forma y su contenido en unidades absolutas del lienzo. Nadie espera eso, y de ahí sale la sensación de que los patrones «no se dejan controlar».
- Distinguir
patternUnitsdepatternContentUnitsy decir qué controla cada uno. - Escribir un patrón que se comporte de forma predecible y explicar por qué.
- Usar el
viewBoxdel patrón como tercera vía que simplifica las dos anteriores. - Diagnosticar y evitar las costuras entre celdas.
Los dos sistemas de un patrón
Un pattern define una celda que se repite en mosaico. Hay dos decisiones independientes: dónde está y cuánto mide la celda, y en qué coordenadas está dibujado su contenido.
patternUnits controla lo primero, es decir x, y, width y height del pattern. Su valor por omisión es objectBoundingBox, así que un width="0.2" significa «una quinta parte del ancho de la forma que use el patrón».
patternContentUnits controla lo segundo. Su valor por omisión es userSpaceOnUse, así que un circle cx="5" dentro del patrón está a 5 unidades del lienzo, no a 5 veces el ancho de la forma.
La mezcla por omisión es la peor combinación posible: la celda se dimensiona en relativo y el contenido se dibuja en absoluto, así que al cambiar el tamaño de la forma la celda crece y el contenido no. El patrón se ve distinto en cada forma que lo use, y no de una manera controlable.
La configuración que funciona es poner los dos en userSpaceOnUse, o dicho de otra forma, poner patternUnits="userSpaceOnUse" y dejar el otro en su valor por omisión:
<svg viewBox="0 0 240 100" width="480">
<defs>
<pattern id="puntos" patternUnits="userSpaceOnUse"
x="0" y="0" width="12" height="12">
<circle cx="6" cy="6" r="2" fill="#89b4fa" />
</pattern>
</defs>
<rect x="10" y="10" width="100" height="80" fill="url(#puntos)" />
<rect x="130" y="10" width="40" height="80" fill="url(#puntos)" />
</svg>
Ahora los dos rectángulos tienen puntos del mismo tamaño y con el mismo paso, que es lo que cualquiera esperaría. La celda mide 12 por 12 unidades del lienzo, el círculo está en el centro de esa celda, y todo es predecible.
Con la configuración por omisión, el rectángulo estrecho tendría una celda de otro tamaño y los círculos aparecerían recortados o dispersos según el caso.
El viewBox del patrón
Hay una tercera vía que evita pensar en los dos sistemas: dar al pattern su propio viewBox. Cuando lo tiene, el contenido se dibuja en las coordenadas de ese viewBox y se escala para encajar en la celda, exactamente igual que un symbol. patternContentUnits se ignora por completo.
<pattern id="cuadros" patternUnits="userSpaceOnUse"
width="20" height="20" viewBox="0 0 10 10">
<rect width="5" height="5" fill="#313244" />
<rect x="5" y="5" width="5" height="5" fill="#313244" />
</pattern>
El contenido se dibuja en una rejilla de 10 y se escala a una celda de 20. Cambiar el tamaño de la celda escala el dibujo entero sin tocar el contenido, que es justo lo que quieres cuando el patrón viene de un diseñador que lo dibujó en su propia rejilla.
Esta es la forma que recomiendo por defecto: patternUnits="userSpaceOnUse" más viewBox. Dos atributos y el patrón deja de sorprender.
patternTransform
patternTransform aplica una transformación al espacio del patrón entero: rotación, escala, sesgado. Es lo que permite rayas diagonales sin dibujarlas diagonales:
<pattern id="rayas" patternUnits="userSpaceOnUse"
width="10" height="10" patternTransform="rotate(45)">
<line x1="0" y1="0" x2="0" y2="10" stroke="#45475a" stroke-width="4" />
</pattern>
Una línea vertical, un patrón rotado 45 grados, y salen rayas diagonales con el paso correcto. Dibujarlas diagonales dentro de la celda exigiría calcular las intersecciones con los bordes para que casen entre celdas, que es exactamente el problema que patternTransform evita.
Con una escala en patternTransform puedes también hacer que el patrón se adapte a un nivel de zoom sin regenerarlo.
Las costuras
Un patrón produce costuras cuando el contenido de la celda toca sus bordes. La causa es el antialiasing: el borde de la celda cae entre dos píxeles, cada celda pinta su mitad con cobertura parcial, y la suma de las dos mitades no llega a la cobertura completa. Aparece una retícula de líneas más claras.
Tres soluciones, en orden de preferencia.
Uno: que el contenido no toque los bordes. Es la solución de verdad. Un patrón de puntos con el círculo centrado y de radio menor que la mitad de la celda no tiene costuras porque no hay nada que cortar.
Dos: dibujar el contenido cortado por los cuatro bordes explícitamente. Si el motivo tiene que atravesar el borde, dibújalo cuatro veces desplazado (arriba, abajo, izquierda, derecha) para que cada celda pinte también los trozos que le entran de las vecinas. Es lo que hace un editor de texturas sin costuras.
Tres: alinear la celda con la rejilla de píxeles. Si el width de la celda en píxeles finales es un entero, los bordes caen entre píxeles y no hay reparto. Funciona, es frágil, y se rompe en cuanto el usuario hace zoom o el elemento cambia de tamaño.
Un patrón no se dibuja n veces: el motor rasteriza la celda a una textura y la repite. Eso tiene dos consecuencias que separan a pattern del resto de SVG.
La primera es de rendimiento, y a favor: un patrón que cubre un área enorme cuesta prácticamente lo mismo que uno pequeño, porque solo se rasteriza la celda. Un fondo de puntos hecho con un patrón es infinitamente más barato que el mismo fondo hecho con diez mil elementos circle.
La segunda es de calidad, y en contra: la textura se rasteriza a una resolución concreta, y si después el SVG se escala (por zoom del navegador, por un transform, o simplemente porque el svg es responsivo y ha crecido), esa textura puede reescalarse en lugar de volver a rasterizarse a la resolución nueva. En ese caso el patrón sale borroso mientras el resto del dibujo sigue nítido, que es la señal inconfundible de que estás mirando una textura y no geometría.
Cuándo ocurre exactamente depende del motor y del momento en que decide invalidar la caché de la textura. En la práctica se ve más en animaciones que escalan un elemento con patrón, y casi nunca en un zoom estático del navegador, porque ahí se rerrenderiza todo.
La lección operativa es doble. Si el patrón es puramente decorativo y a baja frecuencia, no te preocupes. Si el patrón es la textura de una forma que se anima escalando, y la nitidez importa, la alternativa es un mask con la forma y un fondo generado por otra vía, o aceptar el coste y dibujar la geometría de verdad.
Patrones útiles de verdad
Tres que se usan más de lo que parece.
Rayado para la accesibilidad de un gráfico. Cuando dos series se distinguen solo por color, un patrón de rayas en una de ellas las distingue también para quien no percibe ese contraste de color. Es una recomendación concreta, y se retoma en el nivel 29.
Rejilla de fondo. Dos líneas por celda, en userSpaceOnUse, con la celda alineada con las marcas del eje. Un solo elemento en lugar de cien líneas.
Imagen como textura. Un image dentro del patrón permite rellenar una forma con una fotografía. Es la manera de hacer un mapa coropleto con textura, o una máscara de recorte con contenido. Ojo: si el SVG se va a servir como imagen, la referencia externa no se carga, y hay que incrustar la imagen como data URI.
Crea un patrón de rejilla isométrica (tres familias de líneas a 0, 60 y 120 grados) usando un solo pattern con patternTransform. Comprueba si aparecen costuras en las intersecciones y arréglalas con la técnica que corresponda. Después ponlo de fondo en un rectángulo grande y compara el tiempo de pintado con la versión hecha con líneas sueltas, usando el panel de rendimiento del navegador.