تقليد الرسم الحر على مثال RoughJS

RoughJS عبارة عن مكتبة رسومات JavaScript صغيرة (<9 كيلوبايت ) تسمح لك برسم نمط مكتوب بخط اليد . يسمح لك بالاعتماد عليها <canvas>ومعها SVG. في هذا المنشور ، أريد الإجابة على السؤال الأكثر شيوعًا حول RoughJS: كيف يعمل؟


القليل من التاريخ


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

بعد دراسة القضية لفترة وجيزة ، وجدت مقالًا لجو وود وزملائه بعنوان Sketchy rendering for visualization information . أصبحت التقنيات الموصوفة فيه أساس المكتبة ، وخاصة في رسم الخطوط والقطع الناقص.

في عام 2017 ، كتبت النسخة الأولى من المكتبة ، التي عملت فقط على Canvas. بعد حل المشكلة ، فقدت الاهتمام بها. بعد مرور عام ، عملت كثيرًا مع SVG ، وقررت تكييف RoughJS للعمل مع SVG. لقد قمت أيضًا بتغيير بنية واجهة برمجة التطبيقات (API) لجعلها أبسط ، وركزت على بدائية رسومات المتجهات البسيطة. تحدثت عن الإصدار 2.0 على Hacker News وفجأة اكتسبت شعبية هائلة. في عام 2018 ، كان ثاني أكثر المنشورات شعبية ShowHN .

منذ ذلك الحين ، ابتكر أشخاص آخرون أشياء أكثر إثارة استنادًا إلى RoughJS ، على سبيل المثال ، Excalidraw ، لماذا القطط والكلاب ... ، مكتبة الرسومات التقريبية .

الآن لنتحدث عن الخوارزميات ...

التفاوت


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


تخيل نقطة Aودائرة حولها. الآن Aاستبدل بنقطة عشوائية داخل هذه الدائرة. يتم التحكم في منطقة هذه الدائرة من العشوائية بالقيمة roughness.

خطوط


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


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


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

انظر إلى سطح القماش هذا. تغير معلمة الخشونة مظهر الخطوط:


في المقالة الأصلية على القماش ، يمكنك رسم نفسك.

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


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


الحذف (والدوائر)


خذ ورقة وارسم بضع دوائر بأسرع ما يمكن في حركة واحدة مستمرة. إليك ما حصلت عليه:


لاحظ أن نقاط البداية والنهاية للحلقة لا تتطابق دائمًا. يحاول RoughJS تقليد هذا ، بينما يجعل المظهر أكثر اكتمالاً (يتم تكييف التقنية من مقالة giCenter ).

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


يتم أيضًا رسم شكل بيضاوي ثانٍ بحيث تكون الحلقة مغلقة أكثر ولها تأثير رسم إضافي.

في المقالة الأصلية ، يمكنك رسم علامات الحذف على سطح قماش تفاعلي. اختلف الخشونة وشاهد كيف يتغير الشكل:


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


يتم ضبط الخوارزمية تلقائيًا بناءً على حجم الشكل ، مما يزيد من عدد النقاط في الدائرة ( n). يوجد أدناه نفس مجموعة الدوائر التي تم إنشاؤها باستخدام الضبط التلقائي.


ملء


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


من السهل ملء المربعات الموضحة أعلاه ، ولكن في حالة الأشكال الأخرى ، يمكن أن تحدث جميع أنواع المشاكل. على سبيل المثال ، غالبًا ما تسبب المضلعات المقعرة (التي يمكن أن تتجاوز فيها الزوايا 180 درجة) مثل هذه المشاكل:


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

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


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

تفاصيل تنفيذ خوارزمية مسح السلسلة
() .

— (Edge Table, ET), , Ymin. Ymin, Xmin.

— (Active Edge Table, AET), , .

:

interface EdgeTableEntry {
  ymin: number;
  ymax: number;
  x: number; // Initialized to Xmin
  iSlope: number; // Inverse of the slope of the line: 1/m
}

interface ActiveEdgeTableEntry {
  scanlineY: number; // The y value of the scanline
  edge: EdgeTableEntry;
}

, :

1. y y ET. .

2. AET .

3. , AET, ET :

(a) ET y AET , ymin ≤ y.

(b) AET , y = ymax, AET x.

() y, x AET.

(d) y , , .. .

(e) , AET, x y (edge.x = edge.x + edge.iSlope)

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


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

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


ليس مجرد ملء السكتات الدماغية


يدعم RoughJS أيضًا أنماط التعبئة الأخرى ، لكنها مشتقة من نفس خوارزمية الفقس. يتكون الفقس المتقاطع في رسم خطوط متقطعة بزاوية angle، ثم خطوط أخرى بزاوية angle + 90°. يسعى Zigzag لربط خط متقطع مع الخط السابق. للحصول على نمط نقطي ، ارسم دوائر صغيرة على طول الخطوط المتقطعة.


المنحنيات


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

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


ملء المنحنى


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

يمكنك أخذ عينات من المنحنى بالتردد المطلوب باستخدام معادلة منحنى Bezier التكعيبي .


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

يتم حساب نعومة المنحنى باستخدام الطريقة الموضحة في هذا المنشور . تتم مقارنة قيمة النعومة مع قيمة التسامح ، وبعد ذلك يتم اتخاذ قرار بشأن تقسيم المنحنى أم لا.

هذا هو نفس المنحنى مع مستوى التسامح 0.7:


استنادًا إلى التسامح وحده ، توفر الخوارزمية نقاطًا كافية لتمثيل المنحنى. ومع ذلك ، فإنه لا يسمح لك بالتخلص بشكل فعال من النقاط الاختيارية. هذا سيساعد المعلمة الثانية تسمى المسافة . لتقليل عدد النقاط في هذه الطريقة ، يتم استخدام خوارزمية Ramer-Douglas-Pecker .

يظهر بعد نقطة ولدت مع قيم المسافة، أي ما يعادل 0.15، 0.75، 1.5و 3.0.


بناءً على خشونة الشكل ، يمكنك تعيين قيمة المسافة المناسبة . بعد استلام جميع رؤوس المضلع ، يمكننا ملء الأشكال المنحنية بشكل جميل:


دوائر SVG


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

يعمل RoughJS على تحليل المسار وتطبيعه في ثلاث عمليات فقط: Move و Line و Cubic Curve . ( محلل بيانات المسار ). بعد التطبيع ، يمكن رسم الشكل باستخدام الطرق المذكورة أعلاه لرسم الخطوط والمنحنيات. تجمع

حزمة النقاط على المسار تطبيع المسارات وأخذ عينات من نقاط المنحنى لحساب نقاط المسار المقابلة.

فيما يلي مثال على حساب نقطة لمسار M240,100c50,0,0,125,50,100s0,-125,50,-150s175,50,50,100s-175,50,-300,0s0,-125,50,-100s0,125,50,150s0,-100,50,-100:


آخر SVG سبيل المثال أنني أحب أن المعرض هو مخطط خريطة للولايات المتحدة:


جرب RoughJS


تحقق من موقع الويب أو المستودع على Github أو وثائق API . تابع تويتر RoughLib للحصول على معلومات المشروع .

All Articles