نقوم بإنشاء عناصر نائبة SVG جميلة على Node.js


يعد استخدام صور SVG كعناصر نائبة فكرة جيدة جدًا ، خاصة في عالمنا ، عندما تتكون جميع المواقع تقريبًا من مجموعة من الصور التي نحاول تحميلها بشكل غير متزامن. كلما زادت الصور وكلما زاد حجمها ، زادت احتمالية الحصول على مشاكل مختلفة ، بدءًا من حقيقة أن المستخدم لا يفهم تمامًا ما يتم تحميله هناك ، وينتهي بالقفزة الشهيرة للواجهة بأكملها بعد تحميل الصور. خاصة على الإنترنت السيئ من هاتفك - يمكن أن يطير على شاشات متعددة. في مثل هذه اللحظات تأتي الإنقاذ. خيار آخر لاستخدامها هو الرقابة. هناك أوقات تحتاج فيها إلى إخفاء صورة عن المستخدم ، لكني أرغب في الحفاظ على النمط العام للصفحة والألوان والمكان الذي تشغله الصورة.


ولكن في معظم المقالات ، يتحدث الجميع عن النظرية ، أنه سيكون من اللطيف إدراج جميع هذه الصور العنيدة في الصفحات في السطر ، واليوم سنرى عمليًا كيف يمكنك توليدها حسب ذوقك ولونك باستخدام Node.js. سننشئ قوالب المقاود من صور SVG ونملأها بطرق مختلفة ، من التعبئة البسيطة بالألوان أو التدرج إلى التثليث ، فسيفساء Voronoi واستخدام الفلاتر. سيتم فرز جميع الإجراءات في خطوات. أعتقد أن هذه المقالة ستكون مثيرة للاهتمام للمبتدئين المهتمين بكيفية القيام بذلك ويحتاجون إلى تحليل مفصل للإجراءات ، ولكن المطورين ذوي الخبرة قد يحبون أيضًا بعض الأفكار.


التحضير


بادئ ذي بدء ، سوف نذهب إلى مستودع لا نهاية له لجميع أنواع الأشياء تسمى NPM. نظرًا لأن مهمة إنشاء صور كعب الروتين تتضمن جيلًا لمرة واحدة منها على جانب الخادم (أو حتى على جهاز المطور ، إذا كنا نتحدث عن موقع ثابت تقريبًا) ، فلن نتعامل مع التحسين المبكر. سنربط كل ما نحب. لذا ، نبدأ npm init اختيار التبعيات.


بالنسبة للمبتدئين ، هذا هو ColorThief . ربما سمعت عنه بالفعل. مكتبة رائعة يمكنها عزل لوحة ألوان الألوان الأكثر استخدامًا في الصورة. نحن فقط بحاجة إلى شيء من هذا القبيل كبداية.


 npm i --save color-thief 

عند تثبيت هذه الحزمة في Linux ، كانت هناك مشكلة - بعض حزمة القاهرة المفقودة ، والتي ليست في دليل NPM. تم حل هذا الخطأ الغريب عن طريق تثبيت إصدارات التطوير لبعض المكتبات:


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

ستتم مراقبة كيفية عمل هذه الأداة في هذه العملية. ولكن لن يكون من غير الضروري توصيل حزمة rgb-hex على الفور لتحويل تنسيق الألوان من RGB إلى Hex ، وهو أمر واضح من اسمه. لن نشارك في ركوب الدراجات بهذه الوظائف البسيطة.


 npm i --save rgb-hex 

من وجهة نظر التدريب ، من المفيد أن تكتب مثل هذه الأشياء بنفسك ، ولكن عندما تكون المهمة هي تجميع نموذج أولي يعمل بشكل ضئيل ، فإن ربط كل شيء من كتالوج NPM فكرة جيدة. يوفر الكثير من الوقت.

واحدة من أهم المعلمات للنسب هي النسب. يجب أن تتطابق مع نسب الصورة الأصلية. وبناءً على ذلك ، نحتاج إلى معرفة حجمه. سنستخدم حزمة حجم الصورة لحل هذه المشكلة.


 npm i --save image-size 

نظرًا لأننا سنحاول إنشاء إصدارات مختلفة من الصور وستكون جميعها بتنسيق SVG ، بطريقة أو بأخرى ، سيظهر سؤال عن النماذج الخاصة بهم. يمكنك بالطبع المراوغة مع سلاسل الأنماط في JS ، ولكن لماذا كل هذا؟ من الأفضل أن تأخذ محرك نموذج "عادي". على سبيل المثال ، المقاود . بسيطة ولذيذة ، لأن مهمتنا ستكون صحيحة.


 npm i --save handlebars 

لن نقوم على الفور بترتيب نوع من العمارة المعقدة لهذه التجربة. نقوم بإنشاء ملف main.js واستيراد جميع تبعياتنا هناك ، بالإضافة إلى وحدة للعمل مع نظام الملفات.


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

يتطلب ColorThief تهيئة إضافية


 const thief = new ColorThief(); 

ليس من الصعب استخدام التبعيات التي قمنا بتوصيلها ، وحل مشاكل "تحميل صورة إلى برنامج نصي" و "الحصول على حجمها". لنفترض أن لدينا صورة 1.jpg:


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

بالنسبة للأشخاص غير المعتادين على Node.js ، تجدر الإشارة إلى أن كل شيء متعلق بنظام الملفات يمكن أن يحدث بشكل متزامن أو غير متزامن. بالنسبة للطرق المتزامنة ، تتم إضافة "Sync" في نهاية الاسم. سنستخدمها حتى لا نواجه تعقيدات لا داعي لها ولا نخرج عقولنا من فراغ.


دعنا ننتقل إلى المثال الأول.


تعبئة اللون



بادئ ذي بدء ، سوف نحل مشكلة ملء مستطيل بسيط. ستحتوي صورتنا على ثلاث معلمات - العرض والارتفاع ولون التعبئة. نقوم بعمل صورة SVG مع مستطيل ، ولكن بدلاً من هذه القيم نستبدل أزواج الأقواس وأسماء الحقول التي ستحتوي على البيانات المرسلة من البرنامج النصي. ربما تكون قد شاهدت بالفعل بناء الجملة هذا مع HTML التقليدي (على سبيل المثال ، يستخدم Vue شيئًا مشابهًا) ، ولكن لا أحد يزعج استخدامه مع صورة SVG - لا يهتم محرك القالب بما سيكون عليه على المدى الطويل. النص هو والنص في إفريقيا.


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

يعطينا هذا اللون أحد أكثر الألوان شيوعًا ، في المثال هو اللون الرمادي. من أجل استخدام القالب ، نقرأ الملف معه ، على سبيل المثال المقاود بحيث تقوم هذه المكتبة بتجميعه ثم ننشئ خطًا مع كعب SVG النهائي. يستبدل محرك القالب نفسه بياناتنا (اللون والحجم) في الأماكن المناسبة.


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

يبقى فقط لكتابة النتيجة إلى ملف. كما ترى ، فإن العمل مع SVG أمر رائع - كل الملفات نصية ، يمكنك قراءتها وكتابتها بسهولة. والنتيجة هي صورة مستطيل. لا يوجد شيء مثير للاهتمام ، ولكن على الأقل تأكدنا من أن النهج كان يعمل (سيكون هناك رابط إلى المصادر الكاملة في نهاية المقالة).


تعبئة متدرجة


استخدام التدرجات هو نهج أكثر إثارة للاهتمام. هنا يمكننا استخدام زوجين من الألوان الشائعة من الصورة وإجراء انتقال سلس من واحد إلى آخر. يمكن العثور على هذا في بعض الأحيان على المواقع التي تقوم بتحميل أشرطة طويلة من الصور.



تم توسيع قالب SVG الخاص بنا الآن مع هذا التدرج. على سبيل المثال ، سنستخدم التدرج الخطي المعتاد. نحن مهتمون بمعلمتين فقط - اللون في البداية واللون في النهاية:


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

يتم الحصول على الألوان نفسها باستخدام ColorThief نفسه. يحتوي على وضعين للتشغيل - إما أنه يمنحنا لونًا أساسيًا واحدًا ، أو لوحة تحتوي على عدد الألوان التي نحددها. مريح بما فيه الكفاية. بالنسبة إلى التدرج ، نحتاج إلى لونين.


خلاف ذلك ، هذا المثال مشابه للمثال السابق:


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

بهذه الطريقة ، يمكنك إنشاء جميع أنواع التدرجات - وليس بالضرورة أن تكون خطية. ولكن لا تزال هذه نتيجة مملة إلى حد ما. سيكون من الرائع إنشاء نوع من الفسيفساء يشبه عن بعد الصورة الأصلية.


فسيفساء مستطيل


للبدء ، دعنا فقط نصنع الكثير من المستطيلات ونملأها بألوان من اللوحة التي ستقدمها لنا نفس المكتبة.



يمكن أن تؤدي المقاود العديد من الأشياء المختلفة ، على وجه الخصوص لها دورات. سنمرره بمجموعة من الإحداثيات والألوان ، وبعد ذلك سوف يكتشفها. نحن فقط نلف المستطيل الخاص بنا في القالب في كل:


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

وفقًا لذلك ، في البرنامج النصي نفسه ، لدينا الآن لوحة ألوان كاملة ، حلقة من خلال إحداثيات X / Y وإنشاء مستطيل بلون عشوائي من اللوحة. كل شيء بسيط للغاية:


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

من الواضح أن الفسيفساء ، على الرغم من تشابهها في اللون مع الصورة ، ولكن مع ترتيب الألوان ، كل شيء ليس على الإطلاق كما نود. قدرات ColorThief في هذا المجال محدودة. أود الحصول على فسيفساء يتم فيها تخمين الصورة الأصلية ، وليس مجرد مجموعة من الطوب من نفس الألوان تقريبًا.


تحسين الفسيفساء


هنا يجب أن نتعمق قليلاً ونحصل على الألوان من البكسل في الصورة ...



نظرًا لأنه من الواضح أنه ليس لدينا لوحة في وحدة التحكم التي نحصل عادةً على هذه البيانات منها ، فسنستخدم المساعدة في شكل حزمة get-pixels. يمكنه سحب المعلومات اللازمة من المخزن المؤقت مع صورة لدينا بالفعل.


 npm i --save get-pixels 

سيبدو شيء من هذا القبيل:


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

نحصل على كائن يحتوي على حقل البيانات - صفيف من البكسل ، نفس ما نحصل عليه من قماش. دعني أذكرك أنه من أجل الحصول على لون بكسل بواسطة الإحداثيات (X ، Y) ، تحتاج إلى إجراء حسابات بسيطة:


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

وبالتالي ، لكل مستطيل ، يمكننا أخذ اللون ليس من اللوحة ، ولكن مباشرة من الصورة ، واستخدامه. ستحصل على شيء مثل هذا (الشيء الرئيسي هنا هو ألا تنسى أن الإحداثيات في الصورة تختلف عن "المعدل" لدينا من 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 }); } } // . . . 

لمزيد من الجمال ، يمكننا زيادة عدد "الطوب" قليلاً ، مما يقلل حجمه. نظرًا لأننا لا نمرر هذا الحجم إلى القالب (بالطبع ، سيكون من المفيد جعله نفس المعلمة مثل عرض أو ارتفاع الصورة) ، سنقوم بتغيير قيم الحجم في القالب نفسه:


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

الآن لدينا فسيفساء تشبه الصورة الأصلية حقًا ، ولكن في نفس الوقت تأخذ ترتيبًا بحجم أقل.


لا تنس أن GZIP يضغط مثل هذه التسلسلات المتكررة في الملفات النصية جيدًا ، بحيث عند النقل إلى المتصفح ، سيصبح حجم هذه المعاينة أصغر.

لكن دعنا ننتقل.


التثليث



المستطيلات جيدة ، لكن المثلثات تعطي نتائج أكثر إثارة للاهتمام. لذا دعونا نحاول صنع فسيفساء من كومة من المثلثات. هناك طرق مختلفة لهذه المسألة ، سنستخدم تثليث Delaunay :


 npm i --save delaunay-triangulate 

الميزة الرئيسية للخوارزمية التي سنستخدمها هي أنها تتجنب المثلثات ذات الزوايا الحادة جدًا والمنفرجة كلما أمكن ذلك. للحصول على صورة جميلة ، لا نحتاج إلى مثلثات ضيقة وطويلة.


هذه واحدة من تلك اللحظات عندما يكون من المفيد معرفة ما هي الخوارزميات الرياضية الموجودة في مجالنا وما الفرق بينهما. ليس من الضروري تذكر جميع عمليات التنفيذ الخاصة بها ، ولكن على الأقل من المفيد معرفة ما يجب البحث عنه.

تقسيم مهمتنا إلى مهام أصغر. تحتاج أولاً إلى إنشاء نقاط لرؤوس المثلثات. وسيكون من اللطيف إضافة بعض العشوائية إلى إحداثياتهم:


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

بعد مراجعة هيكل المصفوفة مع المثلثات (console.log لمساعدتنا) ، نجد أنفسنا نقاطًا نأخذ فيها لون البكسل. يمكنك ببساطة حساب المتوسط ​​الحسابي لإحداثيات رؤوس المثلثات. ثم ننقل النقاط الإضافية من الحدود القصوى بحيث لا تخرج في أي مكان ، وبعد تلقي إحداثيات حقيقية غير طبيعية ، نحصل على لون البكسل ، والذي سيصبح لون المثلث.


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

يبقى فقط لجمع إحداثيات النقاط المطلوبة في سلسلة وإرسالها مع اللون إلى المقاود للمعالجة ، كما فعلنا من قبل.


في القالب نفسه ، لن يكون لدينا الآن مستطيلات ، ولكن مضلعات:


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

التثليث شيء مثير للاهتمام. من خلال زيادة عدد المثلثات ، يمكنك الحصول على صور جميلة فقط ، لأنه لا أحد يقول أنه يجب علينا استخدامها فقط كعنصر.


فسيفساء Voronoi


هناك مشكلة ، مرآة السابقة - قسم أو فسيفساء Voronoi . لقد استخدمناه بالفعل عند العمل مع تظليل ، ولكن هنا يمكن أن يكون مفيدًا أيضًا.



كما هو الحال مع الخوارزميات المعروفة الأخرى ، لدينا تنفيذ جاهز:


 npm i --save voronoi 

ستكون الإجراءات الأخرى مشابهة جدًا لما فعلناه في المثال السابق. الفرق الوحيد هو أنه لدينا الآن بنية مختلفة - بدلاً من مجموعة من المثلثات لدينا كائن معقد. والخيارات مختلفة قليلاً. خلاف ذلك ، كل شيء هو نفسه تقريبا. يتم إنشاء مصفوفة من النقاط الأساسية بنفس الطريقة ، تخطيها حتى لا تجعل القائمة طويلة جدًا:


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

نتيجة لذلك ، نحصل على فسيفساء من المضلعات المحدبة. أيضا نتيجة مثيرة جدا للاهتمام.


من المفيد تقريب جميع الأرقام إلى أعداد صحيحة أو على الأقل إلى منزلين عشريين. الدقة المفرطة في SVG غير ضرورية تمامًا هنا ، فهي ستزيد فقط من حجم الصور.

فسيفساء غير واضحة


المثال الأخير الذي سنراه هو فسيفساء ضبابية. لدينا كل قوة SVG في أيدينا ، فلماذا لا تستخدم المرشحات؟



خذ الفسيفساء الأولى للمستطيلات وأضف مرشح "التمويه" القياسي إليها:


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

والنتيجة هي معاينة ضبابية وواضحة لصورتنا ، فهي تشغل مساحة أقل 10 مرات تقريبًا (بدون ضغط) ومتجه وتمتد إلى أي حجم للشاشة. بنفس الطريقة ، يمكنك طمس بقية الفسيفساء لدينا.


عند تطبيق مثل هذا المرشح على فسيفساء منتظمة من المستطيلات ، يمكن أن يتحول "تأثير الجيب" ، لذلك إذا كنت تستخدم شيئًا كهذا في الإنتاج ، خاصة على الصور كبيرة الحجم ، فقد يكون من الأفضل تطبيق التمويه ليس عليه ، ولكن على تقسيم Voronoi.

بدلا من الاستنتاج


في هذه المقالة ، نظرنا في كيفية إنشاء جميع أنواع صور كعب SVG على Node.js وتأكدنا من أن هذه ليست مهمة صعبة إذا لم تكتب كل شيء يدويًا ، وإذا أمكن ، قم بتجميع الوحدات الجاهزة. تتوفر مصادر كاملة على جيثب .

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


All Articles