Generamos marcadores de posición SVG hermosos en Node.js


Usar imágenes SVG como marcadores de posición es una muy buena idea, especialmente en nuestro mundo, cuando casi todos los sitios consisten en un montón de imágenes que estamos tratando de cargar de forma asincrónica. Cuantas más imágenes y más voluminosas sean, mayor es la probabilidad de tener varios problemas, comenzando por el hecho de que el usuario no comprende del todo lo que se carga allí y terminando con el conocido salto de toda la interfaz después de cargar las imágenes. Especialmente en Internet deficiente desde su teléfono, puede volar en varias pantallas. Es en esos momentos que los trozos vienen al rescate. Otra opción para su uso es la censura. Hay momentos en los que necesita ocultar una imagen al usuario, pero me gustaría mantener el estilo general de la página, los colores y el lugar que ocupa la imagen.


Pero en la mayoría de los artículos, todo el mundo habla de la teoría, que sería bueno insertar todas estas imágenes en trozos en las páginas en línea, y hoy veremos en la práctica cómo puede generarlas a su gusto y color usando Node.js. Crearemos plantillas de manillar a partir de imágenes SVG y las rellenaremos de diferentes maneras, desde un simple color o relleno de degradado hasta triangulación, mosaico de Voronoi y el uso de filtros. Todas las acciones se ordenarán por pasos. Creo que este artículo será interesante para los principiantes que estén interesados ​​en cómo se hace y necesitan un análisis detallado de las acciones, pero a los desarrolladores experimentados también les pueden gustar algunas ideas.


Preparación


Para empezar, iremos a un repositorio sin fondo de todo tipo de cosas llamado NPM. Dado que la tarea de generar nuestras imágenes de código auxiliar implica una generación única de ellas en el lado del servidor (o incluso en la máquina del desarrollador, si estamos hablando de un sitio más o menos estático), no nos ocuparemos de la optimización prematura. Conectaremos todo lo que nos guste. Entonces, comenzamos con el hechizo de npm init y procedemos con la selección de dependencias.


Para empezar, este es ColorThief . Probablemente ya hayas oído hablar de él. Una biblioteca maravillosa que puede aislar la paleta de colores de los colores más utilizados en la imagen. Solo necesitamos algo así para empezar.


 npm i --save color-thief 

Al instalar este paquete en Linux, había un problema: faltaba un paquete de El Cairo, que no está en el directorio NPM. Este extraño error se resolvió instalando versiones de desarrollo de algunas bibliotecas:


 sudo apt install libcairo2-dev libjpeg-dev libgif-dev 

El funcionamiento de esta herramienta se verá en el proceso. Pero no será superfluo conectar inmediatamente el paquete rgb-hex para convertir el formato de color de RGB a Hex, lo cual es obvio por su nombre. No participaremos en el ciclismo con funciones tan simples.


 npm i --save rgb-hex 

Desde el punto de vista de la capacitación, es útil escribir tales cosas usted mismo, pero cuando hay una tarea de ensamblar rápidamente un prototipo que funcione mínimamente, entonces es una buena idea conectar todo lo que es del catálogo de NPM. Ahorra un montón de tiempo.

Uno de los parámetros más importantes para los enchufes son las proporciones. Deben coincidir con las proporciones de la imagen original. En consecuencia, necesitamos saber su tamaño. Utilizaremos el paquete de tamaño de imagen para resolver este problema.


 npm i --save image-size 

Como intentaremos hacer diferentes versiones de las imágenes y todas estarán en formato SVG, de una forma u otra surgirá la cuestión de las plantillas para ellas. Por supuesto, puede esquivar con cadenas de patrones en JS, pero ¿por qué todo esto? Es mejor tomar un motor de plantillas "normal". Por ejemplo, manillares . Simple y de buen gusto, para nuestra tarea será la correcta.


 npm i --save handlebars 

No organizaremos de inmediato algún tipo de arquitectura compleja para este experimento. Creamos el archivo main.js e importamos todas nuestras dependencias allí, así como un módulo para trabajar con el sistema de archivos.


 const ColorThief = require('color-thief'); const Handlebars = require('handlebars'); const rgbHex = require('rgb-hex'); const sizeOf = require('image-size'); const fs = require('fs'); 

ColorThief requiere inicialización adicional


 const thief = new ColorThief(); 

Usando las dependencias que conectamos, resolver los problemas de "subir una imagen a un script" y "obtener su tamaño" no es difícil. Digamos que tenemos una imagen 1.jpg:


 const image = fs.readFileSync('1.jpg'); const size = sizeOf('1.jpg'); const height = size.height; const width = size.width; 

Para las personas que no están familiarizadas con Node.js, vale la pena decir que casi todo lo relacionado con el sistema de archivos puede ocurrir de forma síncrona o asincrónica. Para los métodos sincrónicos, se agrega "Sincronización" al final del nombre. Los usaremos para no encontrar complicaciones innecesarias y no nos saquemos los sesos de la nada.


Pasemos al primer ejemplo.


Relleno de color



Para comenzar, resolveremos el problema del simple llenado de un rectángulo. Nuestra imagen tendrá tres parámetros: ancho, alto y color de relleno. Hacemos una imagen SVG con un rectángulo, pero en lugar de estos valores, sustituimos los pares de corchetes y los nombres de los campos que contendrán los datos transmitidos desde el script. Probablemente ya haya visto esta sintaxis con HTML tradicional (por ejemplo, Vue usa algo similar), pero nadie se molesta en usarla con una imagen SVG: al motor de plantillas no le importa lo que será a largo plazo. El texto es él y el texto en África.


 <svg version='1.1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none' height='{{ height }}' width='{{ width }}'> <rect x='0' y='0' height='100' width='100' fill='{{ color }}' /> </svg> 

Además ColorThief nos da uno de los colores más comunes, en el ejemplo es el gris. Para usar la plantilla, leemos el archivo con ella, digamos manillares para que esta biblioteca la compile y luego generamos una línea con el código auxiliar SVG terminado. El motor de plantillas sustituye nuestros datos (color y tamaño) en los lugares correctos.


 function generateOneColor() { const rgb = thief.getColor(image); const color = '#' + rgbHex(...rgb); const template = Handlebars.compile(fs.readFileSync('template-one-color.svg', 'utf-8')); const svg = template({ height, width, color }); fs.writeFileSync('1-one-color.svg', svg, 'utf-8'); } 

Solo queda escribir el resultado en un archivo. Como puede ver, trabajar con SVG es bastante bueno: todos los archivos son texto, puede leerlos y escribirlos fácilmente. El resultado es una imagen rectangular. Nada interesante, pero al menos nos aseguramos de que el enfoque funcionara (al final del artículo encontrará un enlace a las fuentes completas).


Relleno de degradado


Usar gradientes es un enfoque más interesante. Aquí podemos usar un par de colores comunes de la imagen y hacer una transición suave de uno a otro. Esto a veces se puede encontrar en sitios que cargan largas cintas de imágenes.



Nuestra plantilla SVG ahora se ha ampliado con este mismo gradiente. Por ejemplo, usaremos el gradiente lineal habitual. Solo nos interesan dos parámetros: el color al principio y el color al final:


 <defs> <linearGradient id='my-gradient' x1='0%' y1='0%' x2='100%' y2='0%' gradientTransform='rotate(45)'> <stop offset='0%' style='stop-color:{{ startColor }};stop-opacity:1' /> <stop offset='100%' style='stop-color:{{ endColor }};stop-opacity:1' /> </linearGradient> </defs> <rect x='0' y='0' height='100' width='100' fill='url(#my-gradient)' /> 

Los colores mismos se obtienen usando el mismo ColorThief. Tiene dos modos de operación: nos da un color primario o una paleta con el número de colores que especificamos. Lo suficientemente cómodo Para el gradiente, necesitamos dos colores.


De lo contrario, este ejemplo es similar al anterior:


 function generateGradient() { const palette = thief.getPalette(image, 2); const startColor = '#' + rgbHex(...palette[0]); const endColor = '#' + rgbHex(...palette[1]); const template = Handlebars.compile(fs.readFileSync('template-gradient.svg', 'utf-8')); const svg = template({ height, width, startColor, endColor }); // . . . 

De esta manera, puede hacer todo tipo de gradientes, no necesariamente lineales. Pero aún así, este es un resultado bastante aburrido. Sería genial hacer algún tipo de mosaico que se pareciera remotamente a la imagen original.


Mosaico rectángulo


Para comenzar, hagamos muchos rectángulos y complételos con los colores de la paleta que nos dará la misma biblioteca.



Los manillares pueden hacer muchas cosas diferentes, en particular tiene ciclos. Le pasaremos una serie de coordenadas y colores, y luego lo resolverá. Simplemente envolvemos nuestro rectángulo en la plantilla en cada uno:


 {{# each rects }} <rect x='{{ x }}' y='{{ y }}' height='11' width='11' fill='{{ color }}' /> {{/each }} 

En consecuencia, en el script en sí, ahora tenemos una paleta de colores completa, recorremos las coordenadas X / Y y hacemos un rectángulo con un color aleatorio de la paleta. Todo es bastante simple:


 function generateMosaic() { const palette = thief.getPalette(image, 16); palette.forEach(function(color, index) { palette[index] = '#' + rgbHex(...color); }); const rects = []; for (let x = 0; x < 100; x += 10) { for (let y = 0; y < 100; y += 10) { const color = palette[Math.floor(Math.random() * 15)]; rects.push({ x, y, color }); } } const template = Handlebars.compile(fs.readFileSync('template-mosaic.svg', 'utf-8')); const svg = template({ height, width, rects }); // . . . 

Obviamente, el mosaico, aunque es similar en color a la imagen, pero con la disposición de los colores, no todo es lo que quisiéramos. Las capacidades de ColorThief en esta área son limitadas. Me gustaría obtener un mosaico en el que se adivinara la imagen original, y no solo un conjunto de ladrillos de más o menos los mismos colores.


Mejorando el mosaico


Aquí tenemos que ir un poco más profundo y obtener los colores de los píxeles de la imagen ...



Como obviamente no tenemos un lienzo en la consola del que usualmente obtenemos estos datos, utilizaremos la ayuda en forma de un paquete get-pixels. Puede extraer la información necesaria del búfer con una imagen que ya tenemos.


 npm i --save get-pixels 

Se verá más o menos así:


 getPixels(image, 'image/jpg', (err, pixels) => { // . . . }); 

Obtenemos un objeto que contiene el campo de datos: una matriz de píxeles, lo mismo que obtenemos del lienzo. Permítame recordarle que para obtener el color de un píxel por coordenadas (X, Y), debe hacer cálculos simples:


 const pixelPosition = 4 * (y * width + x); const rgb = [ pixels.data[pixelPosition], pixels.data[pixelPosition + 1], pixels.data[pixelPosition + 2] ]; 

Por lo tanto, para cada rectángulo podemos tomar el color no de la paleta, sino directamente de la imagen, y usarlo. Obtendrá algo como esto (lo principal aquí es no olvidar que las coordenadas en la imagen difieren de nuestra "normalizada" de 0 a 100):


 function generateImprovedMosaic() { getPixels(image, 'image/jpg', (err, pixels) => { if (err) { console.log(err); return; } const rects = []; for (let x = 0; x < 100; x += 5) { const realX = Math.floor(x * width / 100); for (let y = 0; y < 100; y += 5) { const realY = Math.floor(y * height / 100); const pixelPosition = 4 * (realY * width + realX); const rgb = [ pixels.data[pixelPosition], pixels.data[pixelPosition + 1], pixels.data[pixelPosition + 2] ]; const color = '#' + rgbHex(...rgb); rects.push({ x, y, color }); } } // . . . 

Para una mayor belleza, podemos aumentar ligeramente la cantidad de "ladrillos", reduciendo su tamaño. Como no pasamos este tamaño a la plantilla (por supuesto, valdría la pena que sea el mismo parámetro que el ancho o la altura de la imagen), cambiaremos los valores de tamaño en la plantilla en sí:


 {{# each rects }} <rect x='{{ x }}' y='{{ y }}' height='6' width='6' fill='{{ color }}' /> {{/each }} 

Ahora tenemos un mosaico que realmente se parece a la imagen original, pero al mismo tiempo ocupa un orden de magnitud menos espacio.


No olvide que GZIP comprime bien tales secuencias repetitivas en archivos de texto, de modo que cuando se transfiere al navegador, el tamaño de dicha vista previa será aún más pequeño.

Pero sigamos adelante.


Triangulación



Los rectángulos son buenos, pero los triángulos generalmente dan resultados mucho más interesantes. Así que tratemos de hacer un mosaico a partir de una pila de triángulos. Existen diferentes enfoques para este problema, utilizaremos la triangulación de Delaunay :


 npm i --save delaunay-triangulate 

La principal ventaja del algoritmo que usaremos es que evita triángulos con ángulos muy agudos y obtusos siempre que sea posible. Para una imagen hermosa, no necesitamos triángulos estrechos y largos.


Este es uno de esos momentos en los que es útil saber qué algoritmos matemáticos existen en nuestro campo y cuál es la diferencia en ellos. No es necesario recordar todas sus implementaciones, pero al menos es útil saber qué buscar en google.

Divide nuestra tarea en otras más pequeñas. Primero necesitas generar puntos para los vértices de los triángulos. Y sería bueno agregar algo de aleatoriedad a sus coordenadas:


 function generateTriangulation() { // . . . const basePoints = []; for (let x = 0; x <= 100; x += 5) { for (let y = 0; y <= 100; y += 5) { const point = [x, y]; if ((x >= 5) && (x <= 95)) { point[0] += Math.floor(10 * Math.random() - 5); } if ((y >= 5) && (y <= 95)) { point[1] += Math.floor(10 * Math.random() - 5); } basePoints.push(point); } } const triangles = triangulate(basePoints); // . . . 

Después de revisar la estructura de la matriz con triángulos (console.log para ayudarnos), nos encontramos con puntos en los que tomaremos el color del píxel. Simplemente puede calcular la media aritmética de las coordenadas de los vértices de los triángulos. Luego, movemos los puntos adicionales desde el borde extremo para que no salgan a ningún lado y, después de haber recibido coordenadas reales, no normalizadas, obtenemos el color del píxel, que se convertirá en el color del triángulo.


 const polygons = []; triangles.forEach((triangle) => { let x = Math.floor((basePoints[triangle[0]][0] + basePoints[triangle[1]][0] + basePoints[triangle[2]][0]) / 3); let y = Math.floor((basePoints[triangle[0]][1] + basePoints[triangle[1]][1] + basePoints[triangle[2]][1]) / 3); if (x === 100) { x = 99; } if (y === 100) { y = 99; } const realX = Math.floor(x * width / 100); const realY = Math.floor(y * height / 100); const pixelPosition = 4 * (realY * width + realX); const rgb = [ pixels.data[pixelPosition], pixels.data[pixelPosition + 1], pixels.data[pixelPosition + 2] ]; const color = '#' + rgbHex(...rgb); const points = ' ' + basePoints[triangle[0]][0] + ',' + basePoints[triangle[0]][1] + ' ' + basePoints[triangle[1]][0] + ',' + basePoints[triangle[1]][1] + ' ' + basePoints[triangle[2]][0] + ',' + basePoints[triangle[2]][1]; polygons.push({ points, color }); }); 

Solo queda recopilar las coordenadas de los puntos deseados en una cadena y enviarla junto con el color a los Manillares para su procesamiento, como lo hicimos antes.


En la plantilla en sí, ahora no tendremos rectángulos, sino polígonos:


 {{# each polygons }} <polygon points='{{ points }}' style='stroke-width:0.1;stroke:{{ color }};fill:{{ color }}' /> {{/each }} 

La triangulación es una cosa muy interesante. Al aumentar el número de triángulos, puede obtener imágenes hermosas, porque nadie dice que debemos usarlas solo como trozos.


Mosaico de voronoi


Hay un problema, el espejo del anterior: una partición o un mosaico de Voronoi . Ya lo hemos usado cuando trabajamos con sombreadores , pero aquí también puede ser útil.



Al igual que con otros algoritmos conocidos, tenemos una implementación lista para usar:


 npm i --save voronoi 

Otras acciones serán muy similares a lo que hicimos en el ejemplo anterior. La única diferencia es que ahora tenemos una estructura diferente: en lugar de una matriz de triángulos, tenemos un objeto complejo. Y las opciones son ligeramente diferentes. De lo contrario, todo es casi lo mismo. Se genera una matriz de puntos base de la misma manera, omítala para no hacer que la lista sea demasiado larga:


 function generateVoronoi() { // . . . const box = { xl: 0, xr: 100, yt: 0, yb: 100 }; const diagram = voronoi.compute(basePoints, box); const polygons = []; diagram.cells.forEach((cell) => { let x = cell.site.x; let y = cell.site.y; if (x === 100) { x = 99; } if (y === 100) { y = 99; } const realX = Math.floor(x * width / 100); const realY = Math.floor(y * height / 100); const pixelPosition = 4 * (realY * width + realX); const rgb = [ pixels.data[pixelPosition], pixels.data[pixelPosition + 1], pixels.data[pixelPosition + 2] ]; const color = '#' + rgbHex(...rgb); let points = ''; cell.halfedges.forEach((halfedge) => { const endPoint = halfedge.getEndpoint(); points += endPoint.x.toFixed(2) + ',' + endPoint.y.toFixed(2) + ' '; }); polygons.push({ points, color }); }); // . . . 

Como resultado, obtenemos un mosaico de polígonos convexos. También un resultado muy interesante.


Es útil redondear todos los números a enteros o al menos a un par de decimales. La precisión excesiva en SVG es completamente innecesaria aquí, solo aumentará el tamaño de las imágenes.

Mosaico borroso


El último ejemplo que veremos es un mosaico borroso. Tenemos todo el poder de SVG en nuestras manos, entonces, ¿por qué no usar filtros?



Tome el primer mosaico de rectángulos y agregue el filtro estándar de "desenfoque":


 <defs> <filter id='my-filter' x='0' y='0'> <feGaussianBlur in='SourceGraphic' stdDeviation='2' /> </filter> </defs> <g filter='url(#my-filter)'> {{# each rects }} <rect x='{{ x }}' y='{{ y }}' height='6' width='6' fill='{{ color }}' /> {{/each }} </g> 

El resultado es una vista previa borrosa, "censurada" de nuestra imagen, ocupa casi 10 veces menos espacio (sin compresión), vector y se extiende a cualquier tamaño de pantalla. Del mismo modo, puede difuminar el resto de nuestros mosaicos.


Al aplicar dicho filtro a un mosaico regular de rectángulos, el "efecto jeep" puede resultar, por lo que si usa algo como esto en la producción, especialmente en imágenes de gran tamaño, puede ser más hermoso aplicar el desenfoque no a él, sino a la división de Voronoi.

En lugar de una conclusión


En este artículo, analizamos cómo puede generar todo tipo de imágenes de código auxiliar SVG en Node.js y nos aseguramos de que no sea una tarea tan difícil si no escribe todo a mano y, si es posible, ensambla módulos listos para usar. Las fuentes completas están disponibles en github .

Source: https://habr.com/ru/post/es431232/


All Articles