Kami menghasilkan placeholder SVG yang indah di Node.js


Menggunakan gambar SVG sebagai pengganti adalah ide yang sangat bagus, terutama di dunia kita, ketika hampir semua situs terdiri dari banyak gambar yang kami coba muat secara tidak sinkron. Semakin banyak gambar dan semakin banyak gambarnya, semakin tinggi kemungkinan mendapatkan berbagai masalah, mulai dari kenyataan bahwa pengguna tidak cukup memahami apa yang dimuat di sana, dan diakhiri dengan lompatan terkenal dari seluruh antarmuka setelah memuat gambar. Terutama pada Internet yang buruk dari ponsel Anda - ia dapat terbang di beberapa layar. Pada saat itulah bertopik datang untuk menyelamatkan. Opsi lain untuk penggunaannya adalah sensor. Ada saat-saat ketika Anda perlu menyembunyikan gambar dari pengguna, tetapi saya ingin menjaga gaya keseluruhan halaman, warna dan tempat gambar itu ditempati.


Tetapi di sebagian besar artikel, semua orang berbicara tentang teori, bahwa akan lebih baik untuk memasukkan semua gambar rintisan ini ke halaman-halaman di-line, dan hari ini kita akan melihat dalam praktiknya bagaimana Anda dapat menghasilkan mereka sesuai dengan selera dan warna Anda menggunakan Node.js. Kami akan membuat template setang dari gambar SVG dan mengisinya dengan cara yang berbeda, dari pengisian sederhana dengan warna atau gradien hingga triangulasi, mosaik Voronoi, dan menggunakan filter. Semua tindakan akan disortir dalam langkah-langkah. Saya percaya artikel ini akan menarik bagi pemula yang tertarik pada bagaimana hal ini dilakukan dan memerlukan analisis tindakan yang terperinci, tetapi pengembang yang berpengalaman mungkin juga menyukai beberapa ide.


Persiapan


Untuk memulainya, kita akan pergi ke repositori tanpa dasar dari semua jenis barang yang disebut NPM. Karena tugas menghasilkan gambar rintisan kami melibatkan pembuatan satu kali gambar di sisi server (atau bahkan di mesin pengembang, jika kita berbicara tentang situs yang kurang lebih statis), kami tidak akan berurusan dengan optimasi prematur. Kami akan menghubungkan semua yang kami suka. Jadi, kita mulai dengan mantra npm init dan melanjutkan dengan pemilihan dependensi.


Sebagai permulaan, ini adalah ColorThief . Anda mungkin sudah pernah mendengar tentang dia. Perpustakaan indah yang dapat mengisolasi palet warna dari warna yang paling sering digunakan dalam gambar. Kami hanya butuh sesuatu seperti itu sebagai permulaan.


 npm i --save color-thief 

Saat memasang paket ini di Linux, ada masalah - beberapa paket cairo hilang, yang tidak ada di direktori NPM. Kesalahan aneh ini diselesaikan dengan menginstal versi pengembangan beberapa perpustakaan:


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

Cara kerja alat ini akan ditonton dalam proses. Tetapi tidak akan berlebihan untuk segera menghubungkan paket rgb-hex untuk mengubah format warna dari RGB ke Hex, yang jelas dari namanya. Kami tidak akan terlibat dalam bersepeda dengan fungsi sederhana seperti itu.


 npm i --save rgb-hex 

Dari sudut pandang pelatihan, adalah berguna untuk menulis hal-hal seperti itu sendiri, tetapi ketika ada tugas untuk dengan cepat mengumpulkan prototipe yang berfungsi minimal, maka menghubungkan segala sesuatu yang berasal dari katalog NPM adalah ide yang bagus. Menghemat banyak waktu.

Salah satu parameter terpenting untuk busi adalah proporsi. Mereka harus cocok dengan proporsi gambar asli. Oleh karena itu, kita perlu mengetahui ukurannya. Kami akan menggunakan paket ukuran gambar untuk menyelesaikan masalah ini.


 npm i --save image-size 

Karena kami akan mencoba membuat versi gambar yang berbeda dan semuanya akan dalam format SVG, dengan satu atau lain cara akan muncul pertanyaan tentang templat untuk mereka. Anda tentu saja bisa mengelak dengan string pola di JS, tetapi mengapa semua ini? Lebih baik menggunakan mesin templat "normal". Misalnya, setang . Sederhana dan enak, karena tugas kita akan tepat.


 npm i --save handlebars 

Kami tidak akan segera mengatur semacam arsitektur kompleks untuk percobaan ini. Kami membuat file main.js dan mengimpor semua dependensi kami di sana, serta modul untuk bekerja dengan sistem file.


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

ColorThief membutuhkan inisialisasi tambahan


 const thief = new ColorThief(); 

Menggunakan dependensi yang kami sambungkan, menyelesaikan masalah "mengunggah gambar ke skrip" dan "mendapatkan ukurannya" tidak sulit. Katakanlah kita memiliki gambar 1.jpg:


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

Bagi orang yang tidak terbiasa dengan Node.js, ada baiknya mengatakan bahwa hampir semua yang terkait dengan sistem file dapat terjadi secara sinkron atau asinkron. Untuk metode sinkron, "Sinkronkan" ditambahkan di akhir nama. Kami akan menggunakannya agar tidak mengalami komplikasi yang tidak perlu dan tidak memutar otak kami secara tiba-tiba.


Mari kita beralih ke contoh pertama.


Isi warna



Untuk memulainya, kita akan memecahkan masalah pengisian persegi panjang sederhana. Gambar kami akan memiliki tiga parameter - lebar, tinggi dan warna isi. Kami membuat gambar SVG dengan persegi panjang, tetapi alih-alih nilai-nilai ini kami mengganti pasangan tanda kurung dan nama bidang yang akan berisi data yang dikirimkan dari skrip. Anda mungkin sudah melihat sintaks ini dengan HTML tradisional (misalnya, Vue menggunakan sesuatu yang serupa), tetapi tidak ada yang mengganggu untuk menggunakannya dengan gambar SVG - mesin template tidak peduli apa yang akan terjadi dalam jangka panjang. Teksnya adalah dia dan teks di 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> 

Lebih lanjut ColorThief memberi kita salah satu warna yang paling umum, dalam contoh itu adalah abu-abu. Untuk menggunakan templat, kita membaca file dengan itu, katakan setang sehingga perpustakaan ini mengkompilasinya dan kemudian kita menghasilkan garis dengan rintisan SVG yang sudah jadi. Mesin template itu sendiri menggantikan data kami (warna dan ukuran) di tempat yang tepat.


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

Tetap hanya menulis hasilnya ke file. Seperti yang Anda lihat, bekerja dengan SVG cukup bagus - semua file adalah teks, Anda dapat dengan mudah membaca dan menulisnya. Hasilnya adalah gambar persegi panjang. Tidak ada yang menarik, tetapi setidaknya kami memastikan bahwa pendekatan itu berhasil (tautan ke sumber lengkap akan ada di akhir artikel).


Isi gradien


Menggunakan gradien adalah pendekatan yang lebih menarik. Di sini kita dapat menggunakan beberapa warna umum dari gambar dan membuat transisi yang mulus dari satu ke yang lain. Ini kadang-kadang dapat ditemukan di situs yang memuat gambar pita panjang.



Template SVG kami sekarang telah diperluas dengan gradien ini. Sebagai contoh, kita akan menggunakan gradien linier biasa. Kami hanya tertarik pada dua parameter - warna di awal dan warna di akhir:


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

Warna itu sendiri diperoleh dengan menggunakan ColorThief yang sama. Ini memiliki dua mode operasi - baik itu memberi kita satu warna primer, atau palet dengan jumlah warna yang kita tentukan. Cukup nyaman. Untuk gradien, kita perlu dua warna.


Kalau tidak, contoh ini mirip dengan yang sebelumnya:


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

Dengan cara ini, Anda dapat membuat semua jenis gradien - tidak harus linier. Tapi tetap saja ini hasil yang agak membosankan. Akan lebih bagus untuk membuat semacam mosaik yang akan menyerupai gambar aslinya.


Mosaik persegi panjang


Untuk memulai, mari kita membuat banyak persegi panjang dan mengisinya dengan warna dari palet yang akan diberikan perpustakaan yang sama kepada kita.



Setang dapat melakukan banyak hal berbeda, khususnya memiliki siklus. Kami akan memberinya array koordinat dan warna, dan kemudian dia akan mengetahuinya. Kami hanya membungkus persegi panjang kami di templat di masing-masing:


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

Dengan demikian, dalam skrip itu sendiri, kita sekarang memiliki palet warna penuh, loop melalui koordinat X / Y dan membuat persegi panjang dengan warna acak dari palet. Semuanya cukup sederhana:


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

Tentunya, mozaik, meski warnanya mirip dengan gambar, tetapi dengan penataan warna, semuanya tidak sama sekali seperti yang kita inginkan. Kemampuan ColorThief di bidang ini terbatas. Saya ingin mendapatkan mosaik di mana gambar aslinya akan ditebak, dan bukan hanya satu set batu bata dengan warna yang kurang lebih sama.


Memperbaiki mosaik


Di sini kita harus sedikit lebih dalam dan mendapatkan warna dari piksel dalam gambar ...



Karena kami jelas tidak memiliki kanvas di konsol tempat kami biasanya mendapatkan data ini, kami akan menggunakan bantuan dalam bentuk paket get-pixel. Dia dapat menarik informasi yang diperlukan dari buffer dengan gambar yang sudah kita miliki.


 npm i --save get-pixels 

Akan terlihat seperti ini:


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

Kami mendapatkan objek yang berisi bidang data - array piksel, sama seperti yang kami dapatkan dari kanvas. Biarkan saya mengingatkan Anda bahwa untuk mendapatkan warna piksel dengan koordinat (X, Y), Anda perlu membuat perhitungan sederhana:


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

Jadi, untuk setiap persegi panjang kita dapat mengambil warna bukan dari palet, tetapi langsung dari gambar, dan menggunakannya. Anda akan mendapatkan sesuatu seperti ini (hal utama di sini adalah jangan lupa bahwa koordinat dalam gambar berbeda dari "dinormalisasi" kami dari 0 hingga 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 }); } } // . . . 

Untuk kecantikan yang lebih besar, kita dapat sedikit meningkatkan jumlah "batu bata", mengurangi ukurannya. Karena kami tidak meneruskan ukuran ini ke templat (tentu saja, ada baiknya menjadikannya parameter yang sama dengan lebar atau tinggi gambar), kami akan mengubah nilai ukuran dalam templat itu sendiri:


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

Sekarang kami memiliki mosaik yang benar-benar terlihat seperti gambar asli, tetapi pada saat yang sama memakan banyak ruang lebih sedikit.


Jangan lupa bahwa GZIP memampatkan urutan berulang dalam file teks dengan baik, sehingga saat mentransfer ke browser, ukuran pratinjau seperti itu akan menjadi lebih kecil.

Tapi mari kita lanjutkan.


Triangulasi



Persegi panjang itu baik, tetapi segitiga biasanya memberikan hasil yang jauh lebih menarik. Jadi mari kita coba membuat mosaik dari tumpukan segitiga. Ada beberapa pendekatan untuk masalah ini, kami akan menggunakan triangulasi Delaunay :


 npm i --save delaunay-triangulate 

Keuntungan utama dari algoritma yang akan kita gunakan adalah bahwa ia menghindari segitiga dengan sudut yang sangat tajam dan tumpul bila memungkinkan. Untuk gambar yang indah, kita tidak perlu segitiga yang sempit dan panjang.


Ini adalah salah satu momen ketika berguna untuk mengetahui algoritma matematika apa yang ada di bidang kita dan apa perbedaannya. Tidak perlu mengingat semua implementasi mereka, tetapi setidaknya berguna untuk mengetahui apa yang harus google.

Bagilah tugas kami menjadi yang lebih kecil. Pertama, Anda perlu menghasilkan poin untuk simpul segitiga. Dan akan menyenangkan untuk menambahkan beberapa keacakan pada koordinat mereka:


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

Setelah meninjau struktur array dengan segitiga (console.log untuk membantu kami), kami menemukan diri kami sendiri titik di mana kami akan mengambil warna piksel. Anda bisa menghitung rata-rata aritmatika untuk koordinat simpul segitiga. Kemudian kita memindahkan titik ekstra dari batas ekstrim sehingga mereka tidak keluar di mana pun dan, setelah menerima koordinat nyata, tidak dinormalisasi, kita mendapatkan warna piksel, yang akan menjadi warna segitiga.


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

Tetap hanya mengumpulkan koordinat titik yang diinginkan dalam string dan mengirimkannya bersama warna ke Setang untuk diproses, seperti yang kami lakukan sebelumnya.


Dalam templat itu sendiri, sekarang kita tidak akan memiliki persegi panjang, tetapi poligon:


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

Triangulasi adalah hal yang sangat menarik. Dengan menambah jumlah segitiga, Anda hanya bisa mendapatkan gambar yang indah, karena tidak ada yang mengatakan bahwa kita harus menggunakannya hanya sebagai potongan.


Mosaik Voronoi


Ada masalah, cermin yang sebelumnya - partisi atau mosaik Voronoi . Kami sudah menggunakannya saat bekerja dengan shader , tetapi di sini juga bisa bermanfaat.



Seperti halnya algoritma lain yang dikenal, kami memiliki implementasi yang siap pakai:


 npm i --save voronoi 

Tindakan selanjutnya akan sangat mirip dengan apa yang kami lakukan dalam contoh sebelumnya. Satu-satunya perbedaan adalah bahwa sekarang kita memiliki struktur yang berbeda - daripada array segitiga kita memiliki objek yang kompleks. Dan pilihannya sedikit berbeda. Kalau tidak, semuanya hampir sama. Array poin dasar dihasilkan dengan cara yang sama, lewati saja agar tidak membuat daftar terlalu lama:


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

Hasilnya, kami mendapatkan mosaik poligon cembung. Juga hasil yang sangat menarik.


Berguna untuk membulatkan semua angka ke bilangan bulat atau setidaknya beberapa tempat desimal. Keakuratan yang berlebihan dalam SVG sama sekali tidak perlu di sini, itu hanya akan meningkatkan ukuran gambar.

Mosaik buram


Contoh terakhir yang akan kita lihat adalah mosaik buram. Kami memiliki semua kekuatan SVG di tangan kami, jadi mengapa tidak menggunakan filter?



Ambil mosaik pertama persegi panjang dan tambahkan filter "blur" standar ke dalamnya:


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

Hasilnya adalah pratinjau buram, "disensor" dari gambar kami, ini memakan ruang hampir 10 kali lebih sedikit (tanpa kompresi), vektor, dan membentang ke ukuran layar apa pun. Dengan cara yang sama, Anda dapat mengaburkan sisa mosaik kami.


Ketika menerapkan filter seperti itu ke mosaik persegi panjang biasa, "efek jip" bisa berubah, jadi jika Anda menggunakan sesuatu seperti ini dalam produksi, terutama untuk gambar berukuran besar, mungkin lebih indah untuk menerapkan pengaburan bukan untuk itu, tetapi untuk pemecahan Voronoi.

Alih-alih sebuah kesimpulan


Dalam artikel ini, kami melihat bagaimana Anda dapat menghasilkan semua jenis gambar rintisan SVG di Node.js dan memastikan bahwa ini bukan tugas yang sulit jika Anda tidak menulis semuanya dengan tangan dan, jika mungkin, merakit modul yang sudah jadi. Sumber lengkap tersedia di github .

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


All Articles