Wir generieren wunderschöne SVG-Platzhalter auf Node.js.


Die Verwendung von SVG-Bildern als Platzhalter ist eine sehr gute Idee, insbesondere in unserer Welt, wenn fast alle Websites aus einer Reihe von Bildern bestehen, die wir asynchron laden möchten. Je mehr Bilder und je umfangreicher sie sind, desto höher ist die Wahrscheinlichkeit, dass verschiedene Probleme auftreten, angefangen bei der Tatsache, dass der Benutzer nicht genau versteht, was dort geladen wird, bis hin zum berühmten Sprung der gesamten Benutzeroberfläche nach dem Laden der Bilder. Besonders bei schlechtem Internet von Ihrem Telefon aus kann es auf mehreren Bildschirmen wegfliegen. In solchen Momenten kommen Stummel zur Rettung. Eine weitere Option für ihre Verwendung ist die Zensur. Es gibt Zeiten, in denen Sie ein Bild vor dem Benutzer verbergen müssen, aber ich möchte den Gesamtstil der Seite, die Farben und den Platz, den das Bild einnimmt, beibehalten.


In den meisten Artikeln wird jedoch über die Theorie gesprochen, dass es schön wäre, all diese Stichbilder in Seiten in Reihe einzufügen, und heute werden wir in der Praxis sehen, wie Sie sie mit Node.js nach Ihrem Geschmack und Ihrer Farbe erzeugen können. Wir werden Lenkervorlagen aus SVG-Bildern erstellen und auf verschiedene Arten ausfüllen, von der einfachen Füllung mit Farbe oder Farbverlauf bis zur Triangulation, dem Voronoi-Mosaik und der Verwendung von Filtern. Alle Aktionen werden in Schritten sortiert. Ich glaube, dieser Artikel wird für Anfänger interessant sein, die daran interessiert sind, wie dies gemacht wird, und eine detaillierte Analyse der Aktionen benötigen, aber erfahrene Entwickler mögen möglicherweise auch einige Ideen.


Vorbereitung


Zunächst werden wir zu einem bodenlosen Repository aller Arten von Dingen gehen, die NPM genannt werden. Da die Generierung unserer Stub-Images eine einmalige Generierung auf der Serverseite (oder sogar auf dem Computer des Entwicklers, wenn es sich um eine mehr oder weniger statische Site handelt) umfasst, werden wir uns nicht mit vorzeitiger Optimierung befassen. Wir werden alles verbinden, was wir mögen. Wir beginnen also mit dem npm init Zauber und fahren mit der Auswahl der Abhängigkeiten fort.


Für den Anfang ist dies ColorThief . Sie haben wahrscheinlich schon von ihm gehört. Eine wunderbare Bibliothek, die die Farbpalette der am häufigsten verwendeten Farben im Bild isolieren kann. Wir brauchen nur so etwas für den Anfang.


 npm i --save color-thief 

Bei der Installation dieses Pakets unter Linux gab es ein Problem - ein fehlendes Cairo-Paket, das sich nicht im NPM-Verzeichnis befindet. Dieser seltsame Fehler wurde durch die Installation von Entwicklungsversionen einiger Bibliotheken behoben:


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

Wie dieses Tool funktioniert, wird dabei beobachtet. Es ist jedoch nicht überflüssig, das rgb-hex-Paket sofort anzuschließen, um das Farbformat von RGB in Hex zu konvertieren, was aus dem Namen hervorgeht. Wir werden uns nicht mit so einfachen Funktionen auf das Radfahren einlassen.


 npm i --save rgb-hex 

Unter dem Gesichtspunkt der Schulung ist es nützlich, solche Dinge selbst zu schreiben. Wenn es jedoch darum geht, schnell einen minimal funktionierenden Prototyp zusammenzubauen, ist es eine gute Idee, alles aus dem NPM-Katalog zu verbinden. Spart eine Menge Zeit.

Einer der wichtigsten Parameter für Stecker sind die Proportionen. Sie müssen den Proportionen des Originalbildes entsprechen. Dementsprechend müssen wir seine Größe kennen. Wir werden das Paket in Bildgröße verwenden, um dieses Problem zu beheben.


 npm i --save image-size 

Da wir versuchen werden, verschiedene Versionen der Bilder zu erstellen, die alle im SVG-Format vorliegen, stellt sich auf die eine oder andere Weise die Frage nach den Vorlagen für sie. Sie können natürlich mit Musterzeichenfolgen in JS ausweichen, aber warum das alles? Es ist besser, eine "normale" Vorlagen-Engine zu verwenden. Zum Beispiel Lenker . Einfach und geschmackvoll, denn unsere Aufgabe wird genau richtig sein.


 npm i --save handlebars 

Wir werden für dieses Experiment nicht sofort eine komplexe Architektur arrangieren. Wir erstellen die Datei main.js und importieren dort alle unsere Abhängigkeiten sowie ein Modul für die Arbeit mit dem Dateisystem.


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

ColorThief erfordert eine zusätzliche Initialisierung


 const thief = new ColorThief(); 

Mit den Abhängigkeiten, die wir verbunden haben, ist es nicht schwierig, die Probleme zu lösen, ein Bild in ein Skript hochzuladen und seine Größe zu ermitteln. Nehmen wir an, wir haben ein Bild 1.jpg:


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

Für Leute, die mit Node.js nicht vertraut sind, ist es erwähnenswert, dass fast alles, was mit dem Dateisystem zu tun hat, synchron oder asynchron ablaufen kann. Bei synchronen Methoden wird am Ende des Namens "Sync" hinzugefügt. Wir werden sie verwenden, um nicht auf unnötige Komplikationen zu stoßen und unser Gehirn nicht aus heiterem Himmel zu zerbrechen.


Fahren wir mit dem ersten Beispiel fort.


Farbfüllung



Zunächst lösen wir das Problem des einfachen Füllens des Rechtecks. Unser Bild hat drei Parameter - Breite, Höhe und Füllfarbe. Wir erstellen ein SVG-Bild mit einem Rechteck, ersetzen jedoch anstelle dieser Werte Klammerpaare und die Namen der Felder, die die vom Skript übertragenen Daten enthalten. Sie haben diese Syntax wahrscheinlich bereits mit herkömmlichem HTML gesehen (zum Beispiel verwendet Vue etwas Ähnliches), aber niemand stört sich daran, sie mit einem SVG-Bild zu verwenden - der Template-Engine ist es egal, wie sie auf lange Sicht aussehen wird. Der Text ist er und der Text in Afrika.


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

Weiter ColorThief gibt uns eine der häufigsten Farben, im Beispiel ist es grau. Um die Vorlage zu verwenden, lesen wir die Datei damit, sagen wir Lenker, damit diese Bibliothek sie kompiliert, und generieren dann eine Zeile mit dem fertigen SVG-Stub. Die Template-Engine selbst ersetzt unsere Daten (Farbe und Größe) an den richtigen Stellen.


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

Es bleibt nur das Ergebnis in eine Datei zu schreiben. Wie Sie sehen können, ist die Arbeit mit SVG ziemlich gut - alle Dateien sind Textdateien, Sie können sie leicht lesen und schreiben. Das Ergebnis ist ein Rechteckbild. Nichts Interessantes, aber zumindest haben wir sichergestellt, dass der Ansatz funktioniert (ein Link zu den vollständigen Quellen befindet sich am Ende des Artikels).


Verlaufsfüllung


Die Verwendung von Verläufen ist ein interessanterer Ansatz. Hier können wir einige gängige Farben aus dem Bild verwenden und einen reibungslosen Übergang von einer zur anderen vornehmen. Dies kann manchmal auf Websites gefunden werden, die lange Bildbänder laden.



Unsere SVG-Vorlage wurde jetzt mit genau diesem Farbverlauf erweitert. Als Beispiel verwenden wir den üblichen linearen Gradienten. Wir interessieren uns nur für zwei Parameter - die Farbe am Anfang und die Farbe am Ende:


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

Die Farben selbst werden mit demselben ColorThief erhalten. Es gibt zwei Betriebsarten - entweder eine Primärfarbe oder eine Palette mit der von uns angegebenen Anzahl von Farben. Bequem genug. Für den Verlauf benötigen wir zwei Farben.


Ansonsten ähnelt dieses Beispiel dem vorherigen:


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

Auf diese Weise können Sie alle Arten von Verläufen erstellen - nicht unbedingt linear. Trotzdem ist dies ein ziemlich langweiliges Ergebnis. Es wäre großartig, eine Art Mosaik zu erstellen, das dem Originalbild aus der Ferne ähnelt.


Rechteckmosaik


Lassen Sie uns zunächst viele Rechtecke erstellen und sie mit Farben aus der Palette füllen, die dieselbe Bibliothek uns geben wird.



Lenker können viele verschiedene Dinge tun, insbesondere hat er Zyklen. Wir werden ihm eine Reihe von Koordinaten und Farben übergeben, und dann wird er es herausfinden. Wir wickeln einfach unser Rechteck in die Vorlage in jedem:


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

Dementsprechend haben wir im Skript selbst jetzt eine vollwertige Farbpalette, durchlaufen die X / Y-Koordinaten und erstellen ein Rechteck mit einer zufälligen Farbe aus der Palette. Alles ist ganz einfach:


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

Offensichtlich ist das Mosaik zwar farblich ähnlich wie das Bild, aber mit der Anordnung der Farben ist überhaupt nicht alles so, wie wir es gerne hätten. Die Funktionen von ColorThief in diesem Bereich sind begrenzt. Ich hätte gerne ein Mosaik, in dem das Originalbild erraten würde, und nicht nur ein Satz Steine ​​mit mehr oder weniger gleichen Farben.


Das Mosaik verbessern


Hier müssen wir etwas tiefer gehen und die Farben aus den Pixeln im Bild erhalten ...



Da wir offensichtlich keinen Canvas in der Konsole haben, von dem wir normalerweise diese Daten erhalten, werden wir die Hilfe in Form eines get-pixels-Pakets verwenden. Er kann die notwendigen Informationen mit einem Bild, das wir bereits haben, aus dem Puffer ziehen.


 npm i --save get-pixels 

Es wird ungefähr so ​​aussehen:


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

Wir erhalten ein Objekt, das das Datenfeld enthält - ein Array von Pixeln, genau wie wir es von der Leinwand erhalten. Ich möchte Sie daran erinnern, dass Sie einfache Berechnungen durchführen müssen, um die Farbe eines Pixels anhand der Koordinaten (X, Y) zu erhalten:


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

Somit können wir für jedes Rechteck die Farbe nicht aus der Palette, sondern direkt aus dem Bild nehmen und verwenden. Sie erhalten so etwas (die Hauptsache hier ist nicht zu vergessen, dass sich die Koordinaten im Bild von unseren "normalisierten" von 0 bis 100 unterscheiden):


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

Für mehr Schönheit können wir die Anzahl der "Steine" leicht erhöhen und ihre Größe verringern. Da wir diese Größe nicht an die Vorlage übergeben (es lohnt sich natürlich, sie als Parameter wie die Breite oder Höhe des Bildes festzulegen), ändern wir die Größenwerte in der Vorlage selbst:


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

Jetzt haben wir ein Mosaik, das wirklich wie das Originalbild aussieht, aber gleichzeitig eine Größenordnung weniger Platz einnimmt.


Vergessen Sie nicht, dass GZIP solche sich wiederholenden Sequenzen in Textdateien gut komprimiert, damit beim Übertragen in den Browser die Größe einer solchen Vorschau noch kleiner wird.

Aber lass uns weitermachen.


Triangulation



Rechtecke sind gut, aber Dreiecke liefern normalerweise viel interessantere Ergebnisse. Versuchen wir also, aus einem Stapel Dreiecke ein Mosaik zu machen. Es gibt verschiedene Ansätze für dieses Problem. Wir werden die Delaunay-Triangulation verwenden :


 npm i --save delaunay-triangulate 

Der Hauptvorteil des Algorithmus, den wir verwenden werden, besteht darin, dass Dreiecke mit sehr scharfen und stumpfen Winkeln nach Möglichkeit vermieden werden. Für ein schönes Bild benötigen wir keine schmalen und langen Dreiecke.


Dies ist einer dieser Momente, in denen es nützlich ist zu wissen, welche mathematischen Algorithmen auf unserem Gebiet existieren und was der Unterschied darin ist. Es ist nicht notwendig, sich alle Implementierungen zu merken, aber es ist zumindest nützlich zu wissen, was zu googeln ist.

Teilen Sie unsere Aufgabe in kleinere auf. Zuerst müssen Sie Punkte für die Eckpunkte der Dreiecke generieren. Und es wäre schön, ihren Koordinaten etwas Zufälligkeit hinzuzufügen:


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

Nachdem wir die Struktur des Arrays mit Dreiecken überprüft haben (console.log, um uns zu helfen), finden wir uns Punkte, an denen wir die Farbe des Pixels annehmen werden. Sie können einfach das arithmetische Mittel für die Koordinaten der Eckpunkte der Dreiecke berechnen. Dann verschieben wir die zusätzlichen Punkte vom äußersten Rand, damit sie nicht herauskriechen, und nachdem wir echte, nicht normalisierte Koordinaten erhalten haben, erhalten wir die Farbe des Pixels, die zur Farbe des Dreiecks wird.


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

Es bleibt nur, die Koordinaten der gewünschten Punkte in einer Zeichenfolge zu sammeln und sie zusammen mit der Farbe zur Verarbeitung an den Lenker zu senden, wie wir es zuvor getan haben.


In der Vorlage selbst haben wir jetzt keine Rechtecke mehr, sondern Polygone:


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

Triangulation ist eine sehr interessante Sache. Wenn Sie die Anzahl der Dreiecke erhöhen, erhalten Sie nur schöne Bilder, da niemand sagt, dass wir sie nur als Stummel verwenden dürfen.


Mosaik von Voronoi


Es gibt ein Problem, den Spiegel des vorherigen - eine Trennwand oder ein Mosaik von Voronoi . Wir haben es bereits bei der Arbeit mit Shadern verwendet , aber hier kann es auch nützlich sein.



Wie bei anderen bekannten Algorithmen haben wir eine vorgefertigte Implementierung:


 npm i --save voronoi 

Weitere Aktionen werden denen im vorherigen Beispiel sehr ähnlich sein. Der einzige Unterschied besteht darin, dass wir jetzt eine andere Struktur haben - anstelle einer Anordnung von Dreiecken haben wir ein komplexes Objekt. Und die Optionen sind etwas anders. Ansonsten ist fast alles gleich. Ein Array von Basispunkten wird auf die gleiche Weise generiert. Überspringen Sie es, um die Auflistung nicht zu lang zu machen:


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

Als Ergebnis erhalten wir ein Mosaik aus konvexen Polygonen. Auch ein sehr interessantes Ergebnis.


Es ist nützlich, alle Zahlen entweder auf ganze Zahlen oder auf mindestens ein paar Dezimalstellen abzurunden. Eine übermäßige Genauigkeit in SVG ist hier völlig unnötig, sondern vergrößert nur die Bilder.

Verschwommenes Mosaik


Das letzte Beispiel, das wir sehen werden, ist ein verschwommenes Mosaik. Wir haben die ganze Kraft von SVG in unseren Händen. Warum also nicht Filter verwenden?



Nehmen Sie das erste Mosaik aus Rechtecken und fügen Sie den Standardfilter "Unschärfe" hinzu:


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

Das Ergebnis ist eine verschwommene, „zensierte“ Vorschau unseres Bildes. Es nimmt fast zehnmal weniger Platz (ohne Komprimierung), Vektor und erstreckt sich auf jede Bildschirmgröße. Auf die gleiche Weise können Sie den Rest unserer Mosaike verwischen.


Wenn Sie einen solchen Filter auf ein normales Mosaik aus Rechtecken anwenden, kann sich der "Jeep-Effekt" herausstellen. Wenn Sie also in der Produktion so etwas verwenden, insbesondere bei großen Bildern, ist es möglicherweise schöner, Unschärfe nicht darauf, sondern auf Voronois Aufteilung anzuwenden.

Anstelle einer Schlussfolgerung


In diesem Artikel haben wir uns angesehen, wie Sie auf Node.js alle Arten von SVG-Stub-Bildern generieren können, und sichergestellt, dass dies keine so schwierige Aufgabe ist, wenn Sie nicht alles von Hand schreiben und wenn möglich vorgefertigte Module zusammenstellen. Vollständige Quellen sind auf Github verfügbar .

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


All Articles