Nous générons de beaux espaces réservés SVG sur Node.js


L'utilisation d'images SVG comme espaces réservés est une très bonne idée, en particulier dans notre monde, lorsque presque tous les sites sont constitués d'un tas d'images que nous essayons de charger de manière asynchrone. Plus elles sont volumineuses et volumineuses, plus la probabilité d'obtenir divers problèmes est élevée, à commencer par le fait que l'utilisateur ne comprend pas très bien ce qui y est chargé, et se terminant par le fameux saut de l'interface entière après le chargement des images. Surtout sur un mauvais Internet à partir de votre téléphone - il peut s'envoler sur plusieurs écrans. C'est à de tels moments que les talons viennent à la rescousse. Une autre option pour leur utilisation est la censure. Il y a des moments où vous devez cacher une image à l'utilisateur, mais je voudrais garder le style général de la page, les couleurs et la place que l'image occupe.


Mais dans la plupart des articles, tout le monde parle de théorie, qu'il serait bien d'insérer toutes ces images de talon dans des pages en ligne, et aujourd'hui nous verrons en pratique comment vous pouvez les générer à votre goût et couleur en utilisant Node.js. Nous allons créer des modèles de guidons à partir d'images SVG et les remplir de différentes manières, du simple remplissage avec couleur ou dégradé à la triangulation, en mosaïque Voronoi et en utilisant des filtres. Toutes les actions seront triées par étapes. Je crois que cet article sera intéressant pour les débutants qui sont intéressés par la façon dont cela se fait et qui ont besoin d'une analyse détaillée des actions, mais les développeurs expérimentés peuvent également aimer certaines idées.


La préparation


Pour commencer, nous irons à un référentiel sans fond de toutes sortes de choses appelé NPM. Étant donné que la tâche de générer nos images de stub implique une génération unique d'entre elles côté serveur (ou même sur la machine du développeur, si nous parlons d'un site plus ou moins statique), nous ne traiterons pas d'optimisation prématurée. Nous connecterons tout ce que nous aimons. Donc, nous commençons par le sort npm init et procédons à la sélection des dépendances.


Pour commencer, c'est ColorThief . Vous en avez probablement déjà entendu parler. Une merveilleuse bibliothèque qui peut isoler la palette de couleurs des couleurs les plus utilisées dans l'image. Nous avons juste besoin de quelque chose comme ça pour commencer.


 npm i --save color-thief 

Lors de l'installation de ce paquet sous Linux, il y avait un problème - un paquet cairo manquant, qui n'est pas dans le répertoire NPM. Cette étrange erreur a été résolue en installant des versions de développement de certaines bibliothèques:


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

Le fonctionnement de cet outil sera surveillé au cours du processus. Mais il ne sera pas superflu de connecter immédiatement le paquet rgb-hex pour convertir le format de couleur de RGB en Hex, ce qui est évident d'après son nom. Nous ne nous engagerons pas dans le cyclisme avec des fonctions aussi simples.


 npm i --save rgb-hex 

Du point de vue de la formation, il est utile d'écrire de telles choses vous-même, mais quand il y a une tâche pour assembler rapidement un prototype fonctionnant au minimum, puis connecter tout ce qui est du catalogue NPM est une bonne idée. Économise une tonne de temps.

Un des paramètres les plus importants pour les bouchons est les proportions. Ils doivent correspondre aux proportions de l'image d'origine. En conséquence, nous devons connaître sa taille. Nous utiliserons le package de taille d'image pour résoudre ce problème.


 npm i --save image-size 

Comme nous essaierons de créer différentes versions des images et qu'elles seront toutes au format SVG, d'une manière ou d'une autre la question des modèles se posera. Vous pouvez bien sûr esquiver avec des chaînes de motifs en JS, mais pourquoi tout cela? Il est préférable de prendre un moteur de modèle «normal». Par exemple, le guidon . Simple et de bon goût, car notre tâche sera parfaite.


 npm i --save handlebars 

Nous n'organiserons pas immédiatement une sorte d'architecture complexe pour cette expérience. Nous créons le fichier main.js et y importons toutes nos dépendances, ainsi qu'un module pour travailler avec le système de fichiers.


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

ColorThief nécessite une initialisation supplémentaire


 const thief = new ColorThief(); 

En utilisant les dépendances que nous avons connectées, résoudre les problèmes de «téléchargement d'une image vers un script» et «obtenir sa taille» n'est pas difficile. Disons que nous avons une image 1.jpg:


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

Pour les personnes qui ne connaissent pas Node.js, il convient de dire que presque tout ce qui concerne le système de fichiers peut se produire de manière synchrone ou asynchrone. Pour les méthodes synchrones, «Sync» est ajouté à la fin du nom. Nous les utiliserons pour ne pas rencontrer de complications inutiles et ne nous creusons pas la tête.


Passons au premier exemple.


Remplissage de couleur



Pour commencer, nous allons résoudre le problème du simple remplissage du rectangle. Notre image aura trois paramètres - largeur, hauteur et couleur de remplissage. Nous faisons une image SVG avec un rectangle, mais au lieu de ces valeurs, nous substituons des paires de crochets et les noms des champs qui contiendront les données transmises par le script. Vous avez probablement déjà vu cette syntaxe avec du HTML traditionnel (par exemple, Vue utilise quelque chose de similaire), mais personne ne prend la peine de l'utiliser avec une image SVG - le moteur de modèle ne se soucie pas de ce que ce sera à long terme. Le texte c'est lui et le texte en Afrique.


 <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> 

De plus, ColorThief nous donne l'une des couleurs les plus courantes, dans l'exemple, c'est le gris. Afin d'utiliser le modèle, nous lisons le fichier avec, disons le guidon pour que cette bibliothèque le compile puis nous générons une ligne avec le stub SVG fini. Le moteur de modèle lui-même substitue nos données (couleur et taille) aux bons endroits.


 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'); } 

Il ne reste plus qu'à écrire le résultat dans un fichier. Comme vous pouvez le voir, travailler avec SVG est assez agréable - tous les fichiers sont du texte, vous pouvez facilement les lire et les écrire. Le résultat est une image rectangulaire. Rien d'intéressant, mais au moins nous nous sommes assurés que l'approche fonctionnait (un lien vers les sources complètes sera à la fin de l'article).


Remplissage dégradé


L'utilisation de dégradés est une approche plus intéressante. Ici, nous pouvons utiliser quelques couleurs courantes de l'image et effectuer une transition en douceur de l'une à l'autre. Cela peut parfois être trouvé sur des sites qui chargent de longs rubans d'images.



Notre modèle SVG a maintenant été étendu avec ce dégradé très. Pour un exemple, nous utiliserons le gradient linéaire habituel. Nous ne sommes intéressés que par deux paramètres - la couleur au début et la couleur à la fin:


 <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)' /> 

Les couleurs elles-mêmes sont obtenues en utilisant le même ColorThief. Il a deux modes de fonctionnement - soit il nous donne une couleur primaire, soit une palette avec le nombre de couleurs que nous spécifions. Assez confortable. Pour le dégradé, nous avons besoin de deux couleurs.


Sinon, cet exemple est similaire au précédent:


 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 cette façon, vous pouvez créer toutes sortes de dégradés - pas nécessairement linéaires. Mais c'est quand même un résultat plutôt ennuyeux. Ce serait formidable de faire une sorte de mosaïque qui ressemblerait à distance à l'image originale.


Mosaïque rectangle


Pour commencer, faisons simplement beaucoup de rectangles et remplissons-les de couleurs de la palette que la même bibliothèque nous donnera.



Le guidon peut faire beaucoup de choses différentes, en particulier il a des cycles. Nous lui passerons un tableau de coordonnées et de couleurs, puis il le découvrira. Nous enveloppons simplement notre rectangle dans le modèle de chacun:


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

En conséquence, dans le script lui-même, nous avons maintenant une palette de couleurs à part entière, parcourons les coordonnées X / Y et faisons un rectangle avec une couleur aléatoire à partir de la palette. Tout est assez 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 }); // . . . 

Évidemment, la mosaïque, bien que similaire en couleur à l'image, mais avec la disposition des couleurs, tout n'est pas du tout comme nous le souhaiterions. Les capacités de ColorThief dans ce domaine sont limitées. Je voudrais obtenir une mosaïque dans laquelle on devinerait l'image originale, et pas seulement un ensemble de briques de plus ou moins les mêmes couleurs.


Améliorer la mosaïque


Ici, nous devons aller un peu plus loin et obtenir les couleurs des pixels de l'image ...



Comme nous n'avons évidemment pas de toile dans la console à partir de laquelle nous obtenons généralement ces données, nous utiliserons l'aide sous la forme d'un package get-pixels. Il peut extraire les informations nécessaires du tampon avec une image que nous avons déjà.


 npm i --save get-pixels 

Cela ressemblera à ceci:


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

Nous obtenons un objet qui contient le champ de données - un tableau de pixels, le même que nous obtenons du canevas. Permettez-moi de vous rappeler que pour obtenir la couleur d'un pixel par coordonnées (X, Y), vous devez effectuer des calculs simples:


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

Ainsi, pour chaque rectangle, nous pouvons prendre la couleur non pas de la palette, mais directement de l'image, et l'utiliser. Vous obtiendrez quelque chose comme ça (l'essentiel ici n'est pas d'oublier que les coordonnées dans l'image diffèrent de nos "normalisées" de 0 à 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 }); } } // . . . 

Pour plus de beauté, on peut augmenter légèrement le nombre de "briques", en réduisant leur taille. Puisque nous ne transmettons pas cette taille au modèle (bien sûr, cela vaudrait la peine de lui donner le même paramètre que la largeur ou la hauteur de l'image), nous changerons les valeurs de taille dans le modèle lui-même:


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

Nous avons maintenant une mosaïque qui ressemble vraiment à l'image originale, mais qui prend en même temps un ordre de grandeur moins d'espace.


N'oubliez pas que GZIP compresse bien ces séquences répétitives dans les fichiers texte, de sorte que lors du transfert vers le navigateur, la taille d'un tel aperçu deviendra encore plus petite.

Mais passons à autre chose.


Triangulation



Les rectangles sont bons, mais les triangles donnent généralement des résultats beaucoup plus intéressants. Essayons donc de faire une mosaïque à partir d'un tas de triangles. Il existe différentes approches à ce problème, nous utiliserons la triangulation de Delaunay :


 npm i --save delaunay-triangulate 

Le principal avantage de l'algorithme que nous utiliserons est qu'il évite autant que possible les triangles avec des angles très nets et obtus. Pour une belle image, nous n'avons pas besoin de triangles étroits et longs.


C'est l'un de ces moments où il est utile de savoir quels algorithmes mathématiques existent dans notre domaine et quelle est leur différence. Il n'est pas nécessaire de se souvenir de toutes leurs implémentations, mais au moins il est utile de savoir quoi google.

Divisez notre tâche en plus petites. Vous devez d'abord générer des points pour les sommets des triangles. Et ce serait bien d'ajouter un peu de hasard à leurs coordonnées:


 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); // . . . 

Après avoir revu la structure du tableau avec des triangles (console.log pour nous aider), nous nous trouvons des points sur lesquels nous prendrons la couleur du pixel. Vous pouvez simplement calculer la moyenne arithmétique des coordonnées des sommets des triangles. Ensuite, nous déplaçons les points supplémentaires de la frontière extrême pour qu'ils ne rampent pas et, après avoir reçu des coordonnées réelles et non normalisées, nous obtenons la couleur du pixel, qui deviendra la couleur du triangle.


 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 }); }); 

Il ne reste plus qu'à collecter les coordonnées des points souhaités dans une chaîne et à les envoyer avec la couleur au guidon pour traitement, comme nous l'avons fait auparavant.


Dans le modèle lui-même, nous n'aurons plus de rectangles, mais des polygones:


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

La triangulation est une chose très intéressante. En augmentant le nombre de triangles, vous pouvez obtenir de belles images, car personne ne dit que nous ne devons les utiliser que comme talons.


Mosaïque de Voronoi


Il y a un problème, le miroir du précédent - une partition ou une mosaïque de Voronoi . Nous l'avons déjà utilisé lorsque nous travaillons avec des shaders , mais ici, il peut également être utile.



Comme avec d'autres algorithmes connus, nous avons une implémentation prête à l'emploi:


 npm i --save voronoi 

D'autres actions seront très similaires à ce que nous avons fait dans l'exemple précédent. La seule différence est que nous avons maintenant une structure différente - au lieu d'un tableau de triangles, nous avons un objet complexe. Et les options sont légèrement différentes. Sinon, tout est presque le même. Un tableau de points de base est généré de la même manière, sautez-le pour ne pas allonger la liste:


 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 }); }); // . . . 

En conséquence, nous obtenons une mosaïque de polygones convexes. Aussi un résultat très intéressant.


Il est utile d'arrondir tous les nombres à des nombres entiers ou à au moins quelques décimales. Une précision excessive en SVG est complètement inutile ici, cela ne fera qu'augmenter la taille des images.

Mosaïque floue


Le dernier exemple que nous verrons est une mosaïque floue. Nous avons toute la puissance du SVG entre nos mains, alors pourquoi ne pas utiliser des filtres?



Prenez la première mosaïque de rectangles et ajoutez-y le filtre «flou» standard:


 <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> 

Le résultat est un aperçu flou et «censuré» de notre image, il prend presque 10 fois moins d'espace (sans compression), de vecteur et s'étire à n'importe quelle taille d'écran. De la même manière, vous pouvez brouiller le reste de nos mosaïques.


Lorsque vous appliquez un tel filtre à une mosaïque régulière de rectangles, «l'effet jeep» peut se produire, donc si vous utilisez quelque chose comme ça dans la production, en particulier pour les images de grande taille, il peut être plus beau d'appliquer un flou non pas à lui, mais au fractionnement de Voronoi.

Au lieu d'une conclusion


Dans cet article, nous avons examiné comment vous pouvez générer toutes sortes d'images de stub SVG sur Node.js et nous sommes assurés que ce n'est pas une tâche si difficile si vous n'écrivez pas tout à la main et, si possible, assemblez des modules prêts à l'emploi. Les sources complètes sont disponibles sur github .

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


All Articles