Crear patrones con filtros SVG

Durante años, mi dolor ha sido no poder crear un patrón de apariencia algo natural en CSS. Quiero decir, a veces todo lo que necesito es una textura de madera. La única solución compatible con la producción que conocía era utilizar una imagen externa, pero las imágenes externas son una dependencia adicional e introducen una nueva complejidad.

Ahora sé que una buena parte de estos problemas podrían resolverse con unas pocas líneas de SVG.

Tl;dr: ¡Llévame a la galería!

Hay una primitiva de filtro en SVG llamada feTurbulence. Es especial en el sentido de que no necesita ninguna imagen de entrada: la primitiva de filtro genera una imagen. Produce el llamado ruido Perlin, que es un tipo de gradiente de ruido. El ruido Perlin se utiliza mucho en gráficos generados por computadora para crear todo tipo de texturas. feTurbulenceviene con opciones para crear múltiples tipos de texturas de ruido y millones de variaciones por tipo.

"¿Así que lo que?" podrías preguntar. Estas texturas son ciertamente ruidosas, pero también contienen patrones ocultos que podemos descubrir emparejándolos con otros filtros. Eso es a lo que estamos a punto de saltar.

Creando filtros SVG

Un filtro personalizado normalmente consta de múltiples primitivas de filtro encadenadas para lograr el resultado deseado. En SVG, podemos describirlos de forma declarativa con el filterelemento y una serie de fe{PrimitiveName}elementos. Luego, un filtro declarado se puede aplicar a un elemento renderizable (como rect, circle, path, textetc.) haciendo referencia al filtro id. El siguiente fragmento muestra un filtro vacío identificado como coolEffecty aplicado en todo su ancho y alto rect.

svg   filter    !-- Filter primitives will be written here --  /filter  rect filter="url(#coolEffect)"//svg  

SVG ofrece más de una docena de primitivas de filtro diferentes, pero comencemos con una relativamente sencilla: feFlood. Hace exactamente lo que dice: inunda un área objetivo. (Esto tampoco necesita una imagen de entrada). El área de destino es técnicamente la subregión de la primitiva de filtro dentro de la filterregión.

Tanto la región del filtro como la subregión de la primitiva del filtro se pueden personalizar. Sin embargo, usaremos los valores predeterminados a lo largo de este artículo, que es prácticamente el área completa de nuestro rectángulo. El siguiente fragmento hace que nuestro rectángulo sea rojo y semitransparente configurando los atributos flood-color( red) y flood-opacity( 0.5).

svg   filter    feFlood flood-color="red" flood-opacity="0.5"/  /filter  rect filter="url(#coolEffect)"//svg 

Ahora veamos el feBlendprimitivo. Se utiliza para combinar múltiples entradas. Una de nuestras entradas puede ser SourceGraphic, una palabra clave que representa el gráfico original sobre el que se aplica el filtro.

fillNuestro gráfico original es un rectángulo negro; eso se debe a que no hemos especificado el rectcolor de relleno predeterminado y es negro. Nuestra otra entrada es el resultado de la feFloodprimitiva. Como puede ver a continuación, hemos agregado el resultatributo para feFloodnombrar su salida. Estamos haciendo referencia a esta salida feBlendcon el inatributo y SourceGraphiccon el in2atributo.

El modo de fusión predeterminado es normaly el orden de entrada es importante. Yo describiría nuestra operación de fusión como colocar el rectángulo rojo semitransparente encima del rectángulo negro.

svg   filter    feFlood flood-color="red" flood-opacity="0.5" result="flood"/    feBlend in="flood" in2="SourceGraphic"/  /filter  rect filter="url(#coolEffect)"//svg  

Encadenar primitivas de filtro es una operación bastante frecuente y, afortunadamente, tiene valores predeterminados útiles que se han estandarizado. En nuestro ejemplo anterior, podríamos haber omitido el resultatributo in feFloodasí como el inatributo in feBlend, porque cualquier filtro posterior utilizará el resultado del filtro anterior como entrada. Usaremos este atajo con bastante frecuencia a lo largo de este artículo.

Generando patrones aleatorios confeTurbulence

feTurbulencetiene algunos atributos que determinan el patrón de ruido que produce. Repasemos estos, uno por uno.

baseFrequency

Este es el atributo más importante porque es necesario para crear un patrón. Acepta uno o dos valores numéricos. Especificar dos números define la frecuencia a lo largo de los ejes x e y, respectivamente. Si solo se proporciona un número, define la frecuencia a lo largo de ambos ejes. Un intervalo razonable para los valores es entre 0.001y 1, donde un valor bajo da como resultado "características" grandes y un valor alto da como resultado "características" más pequeñas. Cuanto mayor es la diferencia entre las frecuencias x e y, más "estirado" se vuelve el patrón.

svg   filter    feTurbulence baseFrequency="0.001 1"/  /filter  rect filter="url(#coolEffect)"//svg

type

El typeatributo toma uno de dos valores: turbulence(el predeterminado) o fractalNoise, que es lo que suelo utilizar. fractalNoiseproduce el mismo tipo de patrón en los canales rojo, verde, azul y alfa (RGBA), mientras que turbulenceen el canal alfa es diferente de los de RGB. Me resulta difícil describir la diferencia, pero es mucho más fácil de ver al comparar los resultados visuales.

svg   filter    feTurbulence baseFrequency="0.1" type="fractalNoise"/  /filter  rect filter="url(#coolEffect)"//svg

numOctaves

El concepto de octavas puede que te resulte familiar por la música o la física. Una octava alta duplica la frecuencia. Y, feTurbulenceen SVG, el numOctavesatributo define el número de octavas a representar en el archivo baseFrequency.

numOctavesEl valor predeterminado es 1, lo que significa que genera ruido en la frecuencia base. Cualquier octava adicional duplica la frecuencia y reduce a la mitad la amplitud. Cuanto mayor sea este número, menos visible será su efecto. Además, más octavas significan más cálculo, lo que posiblemente perjudique el rendimiento. Normalmente uso valores entre 1 y 5 y solo los uso para refinar un patrón.

svg   filter    feTurbulence baseFrequency="0.1" type="fractalNoise" numOctaves="2"/  /filter  rect filter="url(#coolEffect)"//svg 

seed

El seedatributo crea diferentes instancias de ruido y sirve como número inicial para el generador de ruido, que produce números pseudoaleatorios bajo el capó. Si se define el valor semilla, aparecerá una instancia diferente de ruido, pero con las mismas cualidades. Su valor predeterminado es 0y se interpretan los números enteros positivos (aunque 0y 1se consideran la misma semilla). Los flotadores están truncados.

Este atributo es mejor para agregar un toque único a un patrón. Por ejemplo, se puede generar una semilla aleatoria en una visita a una página para que cada visitante obtenga un patrón ligeramente diferente. Un intervalo práctico para generar semillas aleatorias es de 0a 9999999debido a algunos detalles técnicos y flotadores de precisión simple. Pero aún así, son 10 millones de casos diferentes, que con suerte cubren la mayoría de los casos.

svg   filter    feTurbulence baseFrequency="0.1" type="fractalNoise" numOctaves="2" seed="7329663"/  /filter  rect filter="url(#coolEffect)"//svg 

stitchTiles

¡Podemos crear mosaicos en mosaico de la misma manera que podemos usar background-repeat: repeaten CSS! Todo lo que necesitamos es el stitchTilesatributo, que acepta uno de dos valores de palabras clave: noStitchy stitch, donde noStitchestá el valor predeterminado. stitchrepite el patrón sin problemas a lo largo de ambos ejes.

Tenga en cuenta que feTurbulencetambién produce ruido en el canal Alfa, lo que significa que las imágenes son semitransparentes, en lugar de completamente opacas.

Galería de patrones

¡Veamos un montón de patrones increíbles hechos con filtros SVG y descubramos cómo funcionan!

Cielo estrellado

Este patrón consta de dos efectos de filtro encadenados en un rectángulo de ancho y alto completo. feTurbulenceEs el primer filtro, responsable de generar ruido. feColorMatrixes el segundo efecto de filtro y altera la imagen de entrada, píxel a píxel. Podemos decir específicamente cuál debe ser el valor de cada canal de salida en función de una constante y todos los valores del canal de entrada dentro de un píxel. La fórmula por canal se ve así:

  • es el valor del canal de salida
  • son los valores del canal de entrada
  • son los pesos

Entonces, por ejemplo, podemos escribir una fórmula para el canal Rojo que solo considere el canal Verde estableciendo en 1y estableciendo los otros pesos en 0. Podemos escribir fórmulas similares para los canales Verde y Azul que solo consideren los canales Azul y Rojo, respectivamente. Para el canal Alfa, podemos establecer (la constante) en 1y los otros pesos en 0para crear una imagen completamente opaca. Estas cuatro fórmulas realizan una rotación de tono.

Las fórmulas también se pueden escribir como multiplicación de matrices, que es el origen del nombre feColorMatrix. Aunque feColorMatrixse puede utilizar sin comprender las operaciones matriciales, debemos tener en cuenta que nuestra matriz de 4 × 5 son los pesos de 4 × 5 de las cuatro fórmulas.

  • es el peso de la contribución del canal Rojo al canal Rojo.
  • es el peso de la contribución del canal Rojo al canal Verde.
  • es el peso de la contribución del canal Verde al canal Rojo.
  • es el peso de la contribución del canal Verde al canal Verde.
  • La descripción de los 16 pesos restantes se omite por motivos de brevedad.

La rotación de tono mencionada anteriormente se escribe así:

Es importante tener en cuenta que los valores RGBA son flotantes que van de 0 a 1, inclusive (en lugar de números enteros que van de 0 a 255, como cabría esperar). Los pesos pueden ser cualquier flotante, aunque al final de los cálculos cualquier resultado inferior a 0 se fija en 0, y cualquier valor superior a 1 se fija en 1. El patrón del cielo estrellado se basa en esta fijación, ya que su matriz es esta:

Estamos usando la misma fórmula para los canales RGB, lo que significa que estamos produciendo una imagen en escala de grises. La fórmula consiste en multiplicar el valor del canal Alfa por nueve y luego quitarle cuatro. Recuerde, incluso los valores Alpha varían en la salida de feTurbulence. La mayoría de los valores resultantes no estarán dentro del rango de 0 a 1; así quedarán sujetos. Entonces, nuestra imagen es principalmente negra o blanca: el negro es el cielo y el blanco son las estrellas más brillantes; los pocos valores intermedios restantes son estrellas tenues. Estamos configurando el canal Alfa en una constante de 1en la cuarta fila, lo que significa que la imagen es completamente opaca.

Madera de pino

Este código no es muy diferente de lo que acabamos de ver en Starry Sky. En realidad, es solo algo de generación de ruido y transformación de la matriz de color. Un patrón de madera típico tiene características que son más largas en una dimensión que en la otra. Para imitar este efecto, estamos creando ruido "estirado" feTurbulenceconfigurando baseFrequency="0.1 0.01". Además, estamos estableciendo type="fractalNoise".

Con feColorMatrix, simplemente estamos recoloreando nuestro patrón más largo. Y, una vez más, el canal Alfa se utiliza como entrada para la variación. Esta vez, sin embargo, compensamos los canales RGB mediante pesos constantes que son mayores que los pesos aplicados en la entrada Alfa. Esto asegura que todos los píxeles de la imagen permanezcan dentro de un rango de color determinado. Encontrar la mejor gama de colores requiere jugar un poco con los valores.

Es esencial comprender que la matriz opera en el espacio de color RGB linealizado de forma predeterminada. El color violeta ( #800080), por ejemplo, está representado por los valores , y . Puede parecer extraño al principio, pero hay una buena razón para usar RGB linealizado para algunas transformaciones. Este artículo proporciona una buena respuesta al por qué y es excelente para profundizar en el cómo.

Al final del día, todo lo que significa es que necesitamos convertir nuestros #RRGGBBvalores habituales al espacio RGB linealizado. Utilicé esta herramienta de espacio de color para hacer eso. Ingrese los valores RGB en la primera línea y luego use los valores de la tercera línea. En el caso de nuestro ejemplo morado, ingresaríamos,, en la primera línea y presionaríamos el sRGB8botón para obtener los valores linealizados en la tercera línea.

Si elegimos nuestros valores correctamente y realizamos las conversiones correctamente, terminaremos con algo que se asemeja a los colores de la madera de pino.

Manchas dálmatas

Este ejemplo condimenta un poco las cosas al introducir feComponentTransferun filtro. Este efecto nos permite definir funciones de transferencia personalizadas por canal de color (también conocido como componente de color). Solo estamos definiendo una función de transferencia personalizada en esta demostración para el canal Alfa y dejamos los otros canales sin definir (lo que significa que se aplicará la función de identidad). Usamos el discretetipo para establecer una función de paso. Los pasos se describen mediante valores numéricos separados por espacios en el tableValuesatributo. tableValuescontrolar el número de pasos y la altura de cada paso.

Consideremos ejemplos en los que jugamos con el tableValuesvalor. Nuestro objetivo es crear un patrón "irregular" a partir del ruido. Esto es lo que sabemos:

  • tableValues="1"transfiere cada valor a 1.
  • tableValues="0"transfiere cada valor a 0.
  • tableValues="0 1"transfiere los valores siguientes 0.5a 0y los valores desde 0.5a 1.
  • tableValues="1 0"transfiere los valores siguientes 0.5a 1y los valores desde 0.5a 0.

Vale la pena jugar con este atributo para comprender mejor sus capacidades y la calidad de nuestro ruido. Después de experimentar un poco llegamos a tableValues="0 1 0"lo que traduce los valores del rango medio a 1 y otros a 0.

El último efecto de filtro en este ejemplo es feColorMatrixel que se utiliza para cambiar el color del patrón. Específicamente, hace que las partes transparentes (Alfa = 0) sean negras y las partes opacas (Alfa = 1) blancas.

Finalmente, afinamos el patrón con feTurbulence. El ajuste numOctaves="2"ayuda a que las manchas sean un poco más "irregulares" y reduce las manchas alargadas. Básicamente baseFrequency="0.06"establece un nivel de zoom que creo que es mejor para este patrón.

Camuflaje ERDL

El patrón ERDL fue desarrollado para disfrazar personal, equipo e instalaciones militares. En las últimas décadas, llegó a la ropa. El patrón consta de cuatro colores: un verde más oscuro para el fondo, marrón para las formas, un verde amarillento para los parches y negro salpicado como pequeñas manchas.

De manera similar al ejemplo de Dalmatian Spots que vimos, estamos encadenando feComponentTransferal ruido, aunque esta vez las discretefunciones están definidas para los canales RGB.

Imaginemos que los canales RGBA son cuatro capas de la imagen. Creamos blobs en tres capas definiendo funciones de un solo paso. El paso comienza en diferentes posiciones para cada función, lo que produce una cantidad diferente de manchas en cada capa. Los recortes para Rojo, Verde y Azul son del 66,67%, 60% y 50%, respectivamente.

feFuncR type="discrete" tableValues="0 0 0 0 1 1"/feFuncG type="discrete" tableValues="0 0 0 1 1"/feFuncB type="discrete" tableValues="0 1"/

En este punto, las manchas de cada capa se superponen en algunos lugares, lo que da como resultado colores que no queremos. Estos otros colores hacen que sea más difícil transformar nuestro patrón en un camuflaje ERDL, así que eliminémoslos:

  • Para Red, definimos la función de identidad.
  • Para Verde, nuestro punto de partida es la función identidad pero le restamos el Rojo.
  • Para el azul, nuestro punto de partida también es la función de identidad, pero le restamos el rojo y el verde.

Estas reglas significan que el Rojo permanece donde antes se superponían el Rojo, el Verde y/o el Azul; El verde permanece donde se superpusieron el verde y el azul. La imagen resultante contiene cuatro tipos de píxeles: rojo, verde, azul o negro.

El segundo encadenado feColorMatrixcambia el color de todo:

  • Las partes negras se vuelven de color verde oscuro con pesos constantes.
  • Las partes rojas se vuelven negras al negar los pesos constantes.
  • Las partes verdes adquieren ese color amarillo verdoso gracias a los pesos adicionales del canal Verde.
  • Las partes azules se vuelven marrones gracias a los pesos adicionales del canal azul.

Grupo de islas

Este ejemplo es básicamente un mapa de altura. Es bastante fácil producir un mapa de altura de apariencia realista feTurbulence: solo necesitamos enfocarnos en un canal de color y ya lo tenemos. Centrémonos en el canal Rojo. Con la ayuda de feColorMatrix, convertimos el ruido colorido en un mapa de altura en escala de grises sobrescribiendo los canales Verde y Azul con el valor del canal Rojo.

Ahora podemos confiar en el mismo valor para cada canal de color por píxel. Esto facilita recolorear nuestra imagen nivel por nivel con la ayuda de feComponentTransfer, aunque esta vez usamos un tabletipo de función. tablees similar a discrete, pero cada paso es una rampa hacia el siguiente paso. Esto permite una transición mucho más suave entre niveles.

El número de tableValuesdetermina cuántas rampas hay en la función de transferencia. Tenemos que considerar dos cosas para encontrar el número óptimo de rampas. Uno es la distribución de intensidad en la imagen. Las diferentes intensidades están distribuidas de manera desigual, aunque los anchos de las rampas son siempre iguales. La otra cosa a considerar es la cantidad de niveles que nos gustaría ver. Y, por supuesto, también debemos recordar que estamos en un espacio RGB linealizado. Podríamos entrar en las matemáticas de todo esto, pero es mucho más fácil simplemente jugar y sentir los valores correctos.

Usamos valores de color azul intenso y aguamarina desde las intensidades más bajas hasta algún punto intermedio para representar el agua. Luego, utilizamos algunos sabores de amarillo para las partes arenosas. Finalmente, el verde y el verde oscuro en las intensidades más altas crean el bosque.


No hemos visto el seedatributo en ninguno de estos ejemplos, pero te invito a probarlo agregándolo allí. Piense en un número aleatorio entre 1 y 10 millones, luego use ese número como seedvalor de atributo en feTurbulence, comofeTurbulence seed="3761593" ...

¡Ahora tienes tu propia variación del patrón!

Uso de producción

Hasta ahora, lo que hemos hecho es observar un montón de patrones SVG geniales y cómo se hacen. Mucho de lo que hemos visto es una excelente prueba de concepto, pero el beneficio real es poder utilizar los patrones en producción de manera responsable.

A mi modo de ver, hay tres caminos fundamentales para elegir.

Método 1: usar un URI de datos en línea en CSS o HTML

Mi forma favorita de usar SVG es alinearlos, siempre que sean lo suficientemente pequeños. Para mí, "suficientemente pequeño" significa unos pocos kilobytes o menos, pero realmente depende del caso de uso particular. La ventaja de la inserción es que se garantiza que la imagen estará ahí en su archivo CSS o HTML, lo que significa que no hay necesidad de esperar hasta que se descargue.

La desventaja es tener que codificar el marcado. Afortunadamente, existen algunas herramientas excelentes diseñadas exclusivamente para este propósito. El codificador de URL de Yoksel para SVG es una de esas herramientas que proporciona una interfaz de usuario con forma de copiar y pegar para generar el código. Si está buscando un enfoque programático, como parte de un proceso de compilación, le sugiero que busque mini-svg-data-uri. No lo he usado personalmente, pero parece bastante popular.

Independientemente del enfoque, el URI de datos codificados va directamente a su CSS o HTML (o incluso JavaScript). CSS es mejor debido a su reutilización, pero HTML tiene un tiempo de entrega mínimo. Si está utilizando algún tipo de técnica de representación del lado del servidor, también puede introducir un seedvalor aleatorio dentro feTurbulencedel URI de datos para mostrar una variación única para cada usuario.

Aquí está el ejemplo de Starry Sky utilizado como imagen de fondo con su URI de datos en línea en CSS:

.your-selector {  background-image: url("data:image/svg+xml,%3Csvg %3E%3Cfilter id='filter'%3E%3CfeTurbulence baseFrequency='0.2'/%3E%3CfeColorMatrix values='0 0 0 9 -4 0 0 0 9 -4 0 0 0 9 -4 0 0 0 0 1'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23filter)'/%3E%3C/svg%3E%0A");}

Y así es como se ve usado imgen HTML:

img/

Método 2: usar el marcado SVG en HTML

Simplemente podemos poner el código SVG en HTML. Aquí está el marcado de Starry Sky, que se puede colocar en un archivo HTML:

div  svg     filter      feTurbulence baseFrequency="0.2"/      feColorMatrix values="0 0 0 9 -4                             0 0 0 9 -4                             0 0 0 9 -4                             0 0 0 0 1"/    /filter    rect filter="url(#filter)"/  /svg/div

Es un enfoque súper simple, pero conlleva un gran inconveniente, especialmente con los ejemplos que hemos visto.

¿Ese inconveniente? Colisión de identidad.

Observe que el marcado SVG utiliza una #filteridentificación. Imagine agregar los otros ejemplos en el mismo archivo HTML. Si también usan una #filterID, eso provocaría que las ID colisionen donde la primera instancia anula a las demás.

Personalmente, solo usaría esta técnica en páginas hechas a mano donde el alcance es lo suficientemente pequeño como para tener en cuenta todos los SVG incluidos y sus ID. Existe la opción de generar identificaciones únicas durante una compilación, pero esa es otra historia.

Método 3: usar un SVG independiente

Esta es la forma "clásica" de hacer SVG. De hecho, es como usar cualquier otro archivo de imagen. Suelte el archivo SVG en un servidor, luego use la URL en una imgetiqueta HTML o en algún lugar de CSS como una imagen de fondo.

Entonces, volvamos al ejemplo del cielo estrellado. Aquí está nuevamente el contenido del archivo SVG, pero esta vez el archivo en sí va al servidor.

svg   filter    feTurbulence baseFrequency="0.2"/    feColorMatrix values="0 0 0 9 -4                           0 0 0 9 -4                           0 0 0 9 -4                           0 0 0 0 1"/  /filter  rect filter="url(#filter)"//svg

Ahora podemos usarlo en HTML, digamos como imagen:

img src="https://example.com/starry-sky.svg"/

Y es igual de conveniente usar la URL en CSS, como una imagen de fondo:

.your-selector {  background-image: url("https://example.com/starry-sky.svg");}

Teniendo en cuenta la compatibilidad actual con HTTP2 y lo relativamente pequeños que son los archivos SVG en comparación con las imágenes rasterizadas, esta no es una mala solución en absoluto. Alternativamente, el archivo se puede colocar en una CDN para una entrega aún mejor. La ventaja de tener archivos SVG como archivos separados es que se pueden almacenar en caché en varias capas.

Advertencias

Si bien disfruto mucho creando estos pequeños patrones, también debo reconocer algunas de sus imperfecciones.

La imperfección más importante es que pueden crear con bastante rapidez una cadena de filtros “monstruosa” computacionalmente pesada. Los efectos de filtro individuales son muy similares a las operaciones únicas en el software de edición de fotografías. Básicamente, estamos "haciendo photoshop" con código, y cada vez que el navegador muestra este tipo de SVG, tiene que representar cada operación. Por lo tanto, si termina teniendo una cadena de filtros larga, sería mejor capturar y entregar el resultado como JPEG o PNG para ahorrar tiempo de CPU a los usuarios. "Mejorar el rendimiento en tiempo de ejecución de SVG" de Taylor Hunt tiene muchos otros consejos excelentes para aprovechar al máximo el rendimiento de SVG.

En segundo lugar, tenemos que hablar sobre la compatibilidad con el navegador. En términos generales, SVG tiene buen soporte, especialmente en los navegadores modernos. Sin embargo, encontré un problema con Safari al trabajar con estos patrones. Intenté crear un patrón circular repetido usando radialGradientwith spreadMethod="repeat". Funcionó bien en Chrome y Firefox, pero Safari no estaba contento con él. Safari muestra el degradado radial como si estuviera spreadMethodconfigurado en pad. Puedes verificarlo directamente en la documentación de MDN.

Es posible que diferentes navegadores muestren el mismo SVG de forma diferente. Considerando todas las complejidades de renderizar SVG, es bastante difícil lograr una consistencia perfecta. Dicho esto, sólo encontré una diferencia entre los navegadores que vale la pena mencionar y es cuando se cambia a la vista de “pantalla completa”. Cuando Firefox pasa a pantalla completa, no muestra el SVG en la parte extendida de la ventana gráfica. Sin embargo, Chrome y Safari son buenos. Puede verificarlo abriendo este lápiz en el navegador de su elección y luego accediendo al modo de pantalla completa. Es un caso extremo poco común que probablemente podría solucionarse con algo de JavaScript y la API de pantalla completa.

¡Gracias!

¡Uf, eso es todo! No solo pudimos ver algunos patrones interesantes, sino que aprendimos muchísimo sobre feTurbulenceSVG, incluidos los diversos filtros que requiere y cómo manipularlos de maneras interesantes.

Como la mayoría de las cosas en la web, existen desventajas y posibles inconvenientes en los conceptos que cubrimos juntos, pero es de esperar que ahora tenga una idea de lo que es posible y a qué debe prestar atención. ¡Tienes el poder de crear algunos patrones increíbles en SVG!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Subir