ملخص المقال

  • حاولت تطوير محرر SVG الخاص بالموقع (واجهته الحالية باليابانية) ليعمل في المتصفح فقط بالتعاون الوثيق مع ChatGPT، وتعثر المشروع مراراً قبل أن يكتمل، لذلك سجّلت هنا نقاط التعثر.
  • ما كان ليتحقق بمفردي أبداً؛ قوة التكويد لدى الذكاء الاصطناعي التوليدي لا تزال مدهشة.
  • وفي الوقت نفسه لم يكن التفويض الكامل كافياً؛ كان لزاماً عليّ تنسيق الصورة الكبيرة، وتصحيح أخطاء المتطلبات وسوء الفهم، وتحديد اتجاه معالجة الأعطال—بمعنى آخر لعب دور المدير الميداني.
  • لذلك وفّر المشروع أيضاً درساً عملياً حول كيفية العيش في عصر الذكاء الاصطناعي.

شرح تنفيذ ومواصفات محرر SVG يعمل بالكامل داخل المتصفح

ما هو SVG؟

SVG (رسوميات المتجهات القابلة للتحجيم) هو صيغة رسوميات تعتمد على XML وتخضع لمعيار W3C. وعلى خلاف الصور النقطية مثل PNG أو JPEG، لا تتدهور جودة الرسم عند التكبير أو التصغير. وبما أن SVG جزء من DOM يمكن تضمينه داخل مستند HTML مباشرة، يصبح بالإمكان التحكم فيه ديناميكياً باستخدام JavaScript وCSS. يعتمد المواصفة على معيار W3C لـSVG ويتيح طيفاً واسعاً من العناصر مثل الأشكال النصية والمسارات والتدرجات والمرشحات.

تُستعمل رسومات SVG على نطاق واسع في الحالات التالية:

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

نظرة عامة على محرر SVG الحالي

المحرر هنا يعمل بالكامل على الواجهة العميلة داخل المتصفح. يمكن للمستخدم رسم الأشكال الأساسية مثل المستطيل والقطع الناقص، إضافة إلى النجوم والفقاعات والغيوم والقلوب، وحتى رسم منحنيات حرة باستخدام وضع القلم. يدعم البرنامج تحريك العناصر وتغيير حجمها وتدويرها، كما يوفّر محاذاة، ووظائف الالتقاط (Snap)، وتجميع العناصر، وقائمة للطبقات. ويمكن تصدير الرسومات بصيغة SVG أو PNG.

فيما يلي شرح تفصيلي لكيفية إسقاط مواصفة SVG على تنفيذ JavaScript عملي.

نقاط المواصفة والتعثر أثناء التنفيذ

إضافة الأشكال والتعامل مع السمات

تحدد مواصفة SVG عناصر وسمات خاصة بكل شكل. فالمستطيل يمثَّل بالعنصر <rect>، والقطع الناقص بـ<ellipse>، والمضلعات بـ<polygon>. ويجب ضبط السمات مثل x وy وwidth وheight أو cx وcy وrx وry وpoints بدقة. أما الأشكال المعقدة مثل النجوم والقلوب فأنشأناها باستخدام عنصر <path> مع مزيج من المنحنيات البِزيَّة وأوامر القوس.

مثال تنفيذي (إضافة مستطيل)

function addRectangle(x, y, width, height) {
  const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
  rect.setAttribute("x", x);
  rect.setAttribute("y", y);
  rect.setAttribute("width", width);
  rect.setAttribute("height", height);
  rect.setAttribute("fill", "transparent");
  rect.setAttribute("stroke", "#000");
  svg.appendChild(rect);
}

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

function createStar(cx, cy, spikes, outerR, innerR) {
  let points = "";
  const step = Math.PI / spikes;
  for (let i = 0; i < 2 * spikes; i++) {
    const r = i % 2 === 0 ? outerR : innerR;
    const x = cx + r * Math.cos(i * step);
    const y = cy + r * Math.sin(i * step);
    points += `${x},${y} `;
  }
  const polygon = document.createElementNS("http://www.w3.org/2000/svg", "polygon");
  polygon.setAttribute("points", points.trim());
  polygon.setAttribute("stroke", "#000");
  polygon.setAttribute("fill", "transparent");
  svg.appendChild(polygon);
}

التحويلات والتعامل مع أنظمة الإحداثيات

يتيح SVG التحكم بالعناصر عبر السمة transform التي تجمع بين translate وrotate وscale. أما التحدي فكان في الحصول على الصندوق المحيط (Bounding Box) عند اختيار عنصر بعد تطبيق التحويلات، حيث يلزم توحيد الفرق بين الإحداثيات المحلية والعالمية. استخدمنا لذلك getScreenCTM() لتحويل الإحداثيات إلى النظام العام ثم حساب المستطيل.

مثال تنفيذي (تطبيق تحويل الإحداثيات)

function getTransformedBBox(element) {
  const bbox = element.getBBox();
  const matrix = element.getScreenCTM();
  const points = [
    svg.createSVGPoint(), svg.createSVGPoint(),
    svg.createSVGPoint(), svg.createSVGPoint()
  ];
  points[0].x = bbox.x; points[0].y = bbox.y;
  points[1].x = bbox.x + bbox.width; points[1].y = bbox.y;
  points[2].x = bbox.x; points[2].y = bbox.y + bbox.height;
  points[3].x = bbox.x + bbox.width; points[3].y = bbox.y + bbox.height;
  return points.map(p => p.matrixTransform(matrix));
}

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

ميزة الالتقاط والشبكة

رسمنا الشبكة باستخدام عنصر <pattern> في الخلفية. أما الالتقاط فطبقناه في JavaScript من خلال تقريب الإحداثيات إلى أقرب مضاعف لقيمة الشبكة.

function snapToGrid(value, gridSize) {
  return Math.round(value / gridSize) * gridSize;
}

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

تحرير النص

لا يمكن تحرير عنصر <text> مباشرة، لذلك استخدمنا عنصر HTML من نوع <input> يوضع مؤقتاً فوق النص لتحريره بشكل مريح.

function editTextElement(textElem) {
  const input = document.createElement("input");
  input.type = "text";
  input.value = textElem.textContent;
  document.body.appendChild(input);
  const bbox = textElem.getBoundingClientRect();
  input.style.position = "absolute";
  input.style.left = bbox.x + "px";
  input.style.top = bbox.y + "px";
  input.focus();
  input.addEventListener("blur", () => {
    textElem.textContent = input.value;
    document.body.removeChild(input);
  });
}

المسارات والمنحنيات الحرة

يجمع عنصر <path> بين الأوامر M وL وC وA وغيرها. بدأنا بتحرير يعتمد على الخطوط المستقيمة، حيث نضيف إحداثيات مع كل نقرة.

let pathData = "M100,100";
function addPathPoint(x, y) {
  pathData += ` L${x},${y}`;
  path.setAttribute("d", pathData);
}

التصدير (SVG/PNG)

قبل الحفظ نزيل العناصر المساعدة ثم نصدر الملف. ولتحويله إلى PNG نمر عبر عنصر Canvas.

function exportToPNG(svgElement) {
  const serializer = new XMLSerializer();
  const source = serializer.serializeToString(svgElement);
  const img = new Image();
  const svgBlob = new Blob([source], { type: "image/svg+xml;charset=utf-8" });
  const url = URL.createObjectURL(svgBlob);
  img.onload = () => {
    const canvas = document.createElement("canvas");
    canvas.width = img.width;
    canvas.height = img.height;
    const ctx = canvas.getContext("2d");
    ctx.drawImage(img, 0, 0);
    canvas.toBlob(blob => {
      const a = document.createElement("a");
      a.href = URL.createObjectURL(blob);
      a.download = "drawing.png";
      a.click();
    });
  };
  img.src = url;
}

دعم تمرير مساحة الرسم

لمعالجة الرسومات الكبيرة وضعنا عنصر SVG داخل <div style="overflow:auto"> لإظهار أشرطة التمرير عند الحاجة.

الخلاصة

تمتاز مواصفة SVG بقوة هائلة، لكن التنفيذ العملي يواجه تحديات مثل تحويل الإحداثيات أو تعريف المسارات. وفي هذا المحرر نجحنا في تنظيم التحديات وتنفيذ ما يلي:

  • التحكم بسمات عناصر الأشكال (rect وellipse وpolygon وpath)
  • التعامل الصحيح مع transform وأنظمة الإحداثيات
  • تنفيذ الشبكة وميزة الالتقاط
  • تحرير النص داخل الواجهة
  • تحرير المسارات (مبني على الخطوط المستقيمة في هذه النسخة)
  • التصدير بصيغتي PNG وSVG
  • دعم التمرير للعمل على مساحات كبيرة

بهذا استفدنا من قدرات SVG إلى أقصى حد لبناء محرر غني يعمل بالكامل داخل المتصفح.