كيفية إنشاء ضربة قلم عريضة الحافة بتمرير شكل ثابت على طول مسار
لإنشاء ضربة قلم عريضة الحافة، حدّد سنًا مستطيلًا ضيقًا، وثبّت زاويته، وحرّكه على طول مسار. تكون الضربة اتحاد جميع المواضع التي يشغلها ذلك المستطيل. ويتغير سمكها مع اتجاه الحركة، رغم أن حجم السن نفسه لا يتغير أبدًا.
يوضح هذا الدليل كيفية إنشاء هذه الهندسة لرسم متجهي أو لمسة تزيينية لشخصية. ستحدّد السن، وتمرّره باستمرار على طول خط متعدد الأضلاع، وتنشئ SVG باستخدام برنامج Python كامل. ويتجنب الإنشاء الأساسي الفجوات التي قد تظهر عند محاكاة الفرشاة بطوابع منفصلة.
ما المقصود بتمرير شكل ثابت؟
قسّم الرسم إلى مدخلين: مسار يصف حركة النقطة المرجعية للسن، وبصمة تصف المساحة التي يغطيها السن في موضع واحد. ضع النقطة المرجعية للبصمة عند نقطة الأصل. وفي كل موضع على المسار، أضف ذلك الموضع إلى كل نقطة في البصمة.
رياضيًا، المجموعة الناتجة هي S = {p + n | p belongs to P, n belongs to N}، حيث P هو المسار وN هو السن. وهذا هو مجموع مينكوفسكي، وفق التعريف النقطي في دليل مستخدم CGAL لمجاميع مينكوفسكي ثنائية الأبعاد.
في هذا الإنشاء، تعني كلمة «ثابت» أن كلًا من البصمة واتجاهها يظلان ثابتين في إحداثيات الرسم. يتحرك السن من دون أن يدور لمتابعة المسار. ويجعل إبقاء النقطة المرجعية في مركز المستطيل المسارَ يصف مركز السن المتحرك.
كيف تحدد زاوية السن واتجاه الحركة السمك؟
اختر ثلاث قيم: البعد الطويل للمستطيل w، والبعد القصير h، وزاوية الحافة الطويلة α. تمنح قيمة موجبة صغيرة لـ h السنَّ سمكًا أدنى مرئيًا. أما سن القطعة المستقيمة المثالي فيكون سمكه صفرًا، وقد يترك علامة عديمة المساحة إذا تحرك بالضبط في اتجاهه نفسه.
لقطعة مستقيمة من المسار ذات زاوية اتجاه θ، يكون عرض السن المستطيل المقاس عموديًا على الحركة كما يلي:
width across the stroke = w × |sin(θ − α)| + h × |cos(θ − α)|
تنتج هذه الصيغة من إسقاط بُعدَي المستطيل المتعامدين على العمود على المسار. وهي تصف قطعة مستقيمة، أو إسقاط البصمة المحلي على طول منحنى أملس؛ وقد تجعل الأجزاء المتداخلة من ضربة منحنية محيطها الظليلي أعرض.
لنفترض سنًا توضيحيًا أبعاده 24 وحدة في 3 وحدات. تؤدي الحركة الموازية للحافة الطويلة إلى شريط عرضه 3 وحدات. أما الحركة العمودية فتؤدي إلى شريط عرضه 24 وحدة. وعند فرق زاوية قدره 45°، يكون العرض نحو (24 + 3) / √2، أي 19.09 وحدة. هذه أبعاد محسوبة وليست نتائج مقاسة للفرشاة.
استخدم هذه الاتجاهات الثلاثة كاختبار معايرة قبل رسم منحنى معقد. فهي تبيّن ما إذا كانت الزاوية والأبعاد تتصرف كما ينبغي، من دون الحاجة إلى تفسير عدة انحناءات متداخلة.
كيف تمرّر الشكل باستمرار من دون ترك فجوات بين الطوابع؟
لتمرير سن محدب على قطعة مستقيمة واحدة، خذ نسخة من السن عند كل نقطة نهاية، واحسب الغلاف المحدب للرؤوس المجمعة. وهذا الغلاف هو بالضبط المساحة التي يمسحها السن أثناء انتقاله على طول القطعة.
السبب واضح: كل موضع وسيط هو استيفاء خطي بين موضعي النهاية. وبما أن السن محدب، فإن الغلاف يحتوي هذه المواضع المستوفاة بدقة. وينتج إنشاء القطعة هذا مباشرةً عن تعريف مجموع مينكوفسكي، وليس عن ميزة منفصلة للفرشاة.
في حالة المستطيل، توفر كل قطعة ثماني نقاط مرشحة فقط: أربع زوايا عند البداية وأربع عند النهاية. احسب غلافها، وكرّر ذلك لكل زوج متتالٍ من نقاط المسار، ثم اجمع المضلعات الناتجة. تتشارك المساحات الممسوحة المتجاورة موضع السن الكامل عند نقطة النهاية المشتركة، لذا توفر الهندسة تغطية متصلة.
لا تحسب غلافًا محدبًا واحدًا حول المسار بأكمله، لأن ذلك سيملأ المساحة عبر الانحناءات ويمحو التجاويف المقصودة. ينتمي الغلاف إلى كل قطعة على حدة؛ أما الضربة الكاملة فهي اتحاد هذه الأغلفة.
يتطلب السن المقعر معالجة مختلفة، إذ إن أخذ غلافه سيملأ تجاويفه. قسّمه إلى أجزاء محدبة، ومرّر كل جزء، ثم اجمع النتائج. يوثّق CGAL أسلوبًا مرتبطًا يعتمد على التقسيم والاتحاد في مجاميع مينكوفسكي للمضلعات غير المحدبة.
كيف تنفّذ التمرير في Python؟
يتطلب البرنامج الكامل التالي Python 3 ومكتبته القياسية. يعرّف مسارًا نموذجيًا من خمس نقاط، وينشئ سنًا مستطيلًا، ويطبع مستند SVG يحتوي على أربعة مضلعات ممتلئة للمساحة الممسوحة. احفظه باسم broad_edge.py، ثم شغّل python broad_edge.py > stroke.svg لإنشاء الرسم.
تتبع الإحداثيات هنا اصطلاح SVG: تزداد قيمة x نحو اليمين، وتزداد قيمة y نحو الأسفل. وبناءً على ذلك، تميل الزاوية الموجبة في هذا البرنامج السنَّ باتجاه عقارب الساعة على الشاشة. وقد تستخدم واجهات الرسم الأخرى اصطلاحًا مختلفًا للزوايا.
from math import cos, sin, radians
def hull(points):
pts = sorted(set(points))
if len(pts) <= 1:
return pts
def cross(o, a, b):
return ((a[0] - o[0]) * (b[1] - o[1])
- (a[1] - o[1]) * (b[0] - o[0]))
def half(sequence):
out = []
for p in sequence:
while len(out) >= 2 and cross(out[-2], out[-1], p) <= 0:
out.pop()
out.append(p)
return out
return half(pts)[:-1] + half(reversed(pts))[:-1]
def sweep(path, width=24, thickness=3, angle=30):
if not path or width <= 0 or thickness <= 0:
raise ValueError(
'Provide a nonempty path and positive nib dimensions'
)
a = radians(angle)
u = (cos(a), sin(a))
v = (-sin(a), cos(a))
nib = [
(s * width * u[0] / 2 + t * thickness * v[0] / 2,
s * width * u[1] / 2 + t * thickness * v[1] / 2)
for s, t in [(-1, -1), (1, -1), (1, 1), (-1, 1)]
]
pairs = list(zip(path, path[1:])) or [(path[0], path[0])]
return [
hull([(p[0] + q[0], p[1] + q[1])
for p in (start, end) for q in nib])
for start, end in pairs
]
path = [(40, 45), (85, 125), (140, 150), (195, 110), (245, 45)]
polygons = sweep(path)
all_points = [p for polygon in polygons for p in polygon]
x0, y0 = (min(p[i] for p in all_points) - 5 for i in (0, 1))
x1, y1 = (max(p[i] for p in all_points) + 5 for i in (0, 1))
svg = [
'<svg xmlns="http://www.w3.org/2000/svg" '
f'viewBox="{x0} {y0} {x1 - x0} {y1 - y0}">',
'<g fill="#23364d" stroke="none">'
]
for polygon in polygons:
coordinates = ' '.join(f'{x:.6f},{y:.6f}' for x, y in polygon)
svg.append(f'<polygon points="{coordinates}"/>')
svg.extend(['</g>', '</svg>'])
print('\n'.join(svg))
يتعامل البرنامج مع نقطة مسار واحدة برسم بصمة سن واحدة. كما تنتج النقاط المتتالية المتكررة بصمةً بدلًا من الحاجة إلى حساب اتجاه. وتؤدي المسارات الفارغة وأبعاد السن غير الموجبة إلى ظهور خطأ. قدّم أزواج إحداثيات رقمية محدودة؛ فهذا المثال الموجز لا يتحقق من بيانات المسار المستوردة.
شُغّل الإنشاء باستخدام إحداثيات العينة: فأنتج أربعة مضلعات ممسوحة، لكل منها ستة رؤوس، واجتاز SVG الناتج تحليل XML بنجاح. كما جرى التحقق من حالتي النقطة الواحدة والنقاط المتكررة. تتحقق هذه الفحوص من الإنشاء وبنية المستند، لكنها لا تُعد اختبارًا للعرض المرئي.
كيف تتعامل مع المنحنيات والزوايا ونقاط النهاية؟
المثال دقيق بالنسبة إلى خطه متعدد الأضلاع، مع مراعاة حساب الفاصلة العائمة وتقريب الإحداثيات. لاستخدام منحنى بيزييه، قرّبه أولًا إلى قطع مستقيمة، ثم طبّق التمرير نفسه. أضف مزيدًا من القطع حيث ينحني المسار بشدة بدلًا من توزيع جميع العينات بالتساوي على معامل المنحنى.
اختر سماحية التقريب وفق حجم الإخراج المقصود. على سبيل المثال، يمكن أن يكون انحراف ربع بكسل هدفًا أوليًا للعرض، على أن يتبعه فحص عند أكبر مقياس مخطط له. هذا خيار هندسي توضيحي وليس حدًا عالميًا للجودة. ومن المهم أن إضافة تقسيمات على طول قطعة مستقيمة أصلًا لا تغير المساحة التي يمسحها السن.
أبقِ المسارات المنفصلة التي رُسمت مع ملامسة القلم منفصلة. فتمرير نقاط من ضربتين غير متصلتين في استدعاء واحد سينشئ مساحة وصل غير مرغوب فيها. أنشئ كل ضربة على حدة، واجمع مضلعاتها في الناتج.
عند نقطة النهاية، يحتفظ السن ببصمته كاملة. وعند الزاوية، تتداخل المساحتان الممسوحتان المتجاورتان. لا توجد في هذا النموذج نهاية مائلة إضافية أو نهاية مستديرة. تحدد خصائص الضربة العادية في SVG إنشاءات النهايات والوصلات الخاصة بها؛ لذا فإن إضافتها ستُدخل هندسة تتجاوز التمرير بسن ثابت. راجع تعريفات نهايات الضربات ووصلاتها في SVG 2.
متى تحتاج إلى دمج المضلعات؟
يصدّر البرنامج مضلعات ممتلئة متداخلة. وتمثل هذه المضلعات تغطية المساحة الممسوحة في معاينة بلون موحد، من دون الحاجة إلى مكتبة لاتحاد المضلعات. لكنها تظل كائنات منفصلة قابلة للتحرير؛ فالبرنامج لا يحسب حدًا واحدًا موحدًا.
إذا كنت تحتاج إلى محيط ظليلي نظيف لمتابعة تحرير المخطط الخارجي، فاحسب اتحادًا منطقيًا لجميع مضلعات القطع. حافظ على أي ثقوب تبقى في النتيجة. فقد يحيط مسار ملتف بمنطقة غير مطلية، وسيؤدي ملء كل المحيطات المغلقة إلى تغيير الضربة.
يتطلب الشفاف أيضًا معالجة مقصودة. فتطبيق fill-opacity جزئي على المضلعات المتداخلة كلٍّ على حدة قد يجعل مناطق التداخل أغمق. للحصول على شفافية موحدة، طبّق العتامة على المجموعة الحاوية، أو ادمج الهندسة قبل تطبيق عتامة التعبئة. ويميز SVG بين عتامة تعبئة الكائن وعتامة المجموعة في مواصفات الرسم.
| النتيجة المطلوبة | الخطوة التالية المناسبة |
|---|---|
| معاينة بلون موحد | أبقِ مضلعات المساحة الممسوحة ممتلئة |
| معاينة شفافة بعتامة موحدة | طبّق العتامة على المجموعة الحاوية لها |
| محيط ظليلي واحد قابل للتحرير | أجرِ اتحادًا منطقيًا لمضلعات المساحة الممسوحة |
| سن مخصص مقعر | قسّم السن قبل تمريره |
| ضربة منحنية سلسة | قرّب المنحنى، ثم مرّر أجزاءه |
كيف تشخّص ضربة غير متوقعة؟
ابدأ باتجاهات المعايرة، ثم افحص المسار وإنشاء الناتج. غيّر مدخلًا واحدًا كل مرة كي يظل مصدر الفرق المرئي واضحًا.
- يبقى السمك شبه ثابت: تحقق مما إذا كان السن يدور مع المسار. يجب أن تظل زاويته ثابتة في إحداثيات الرسم.
- تظهر فجوات مرئية بين العلامات: استبدل الطوابع المنفردة بإنشاء الغلاف لكل قطعة.
- يمتلئ داخل الانحناء بإسفين عريض: تحقق مما إذا كنت قد حسبت غلافًا حول عدة قطع من المسار معًا عن طريق الخطأ.
- تبدو النهايات مستديرة: تحقق من إضافة ضربة عادية أو استخدام بصمة سن مختلفة.
- يبدو المنحنى الأصلي السلس زاويًا: حسّن تقريب الخط متعدد الأضلاع حول الانحناءات.
- تظهر وصلات غير متوقعة: أبقِ المسارات غير المتصلة ضمن استدعاءات تمرير منفصلة.
إذا كنت تقارن النتيجة بأداة الخطاطية في Inkscape، فتقدم وثائقها فحوصًا مفيدة للمعلمات: تثبّت قيمة Fixation 100 الزاوية، وتزيل قيمة Thinning 0 التغير في العرض الناتج عن السرعة، ويمكن لعناصر التحكم في الضغط والميل أن تغيّر العرض والزاوية. عطّل هذه المدخلات المتغيرة عند فحص سلوك السن الثابت. تصف هذه الإعدادات عناصر التحكم في الأداة؛ ولا تثبت أن ناتجها مطابق لهذا الإنشاء المستطيل. راجع وثائق شريط أدوات الخطاطية في Inkscape.
تبدأ ضربة الحافة العريضة الموثوقة بمسار معايرة بسيط وبصمة محددة بوضوح. وبعد أن تعمل اختبارات التوازي والتعامد والقطر كما ينبغي، استخدم تمرير القطع نفسه على مسارك المختار، وحسّن الأجزاء المنحنية بما يناسب مقياس الإخراج، وادمج المضلعات عندما تتطلب خطوة التحرير التالية محيطًا ظليليًا واحدًا.
