מימוש והסבר של עורך SVG שמסתמך רק על הדפדפן
עיקרי הדברים בכתבה
- כמו תמיד עבדתי יד ביד עם ChatGPT — או יותר נכון, זרקתי הוראות — כדי לבנות את עורך ה-SVG /tools/svgedit/, אבל ההתנגשויות בדרך היו רבות.
- אין מצב שהייתי מצליח לבד. הכישרון של AI בכתיבת קוד עדיין מרשים מאוד.
- ובכל זאת, אי אפשר היה להסתמך עליו בעיניים עצומות. הייתי חייב לתאם את הכול, לתקן דרישות שגויות והבנות מוטעות, ולהכווין בזמן תקלות — בדיוק כמו מנהל-מבצע.
- במובן הזה, החוויה הזו גם נתנה הצצה לאיך שורדים בעידן ה-AI.
מימוש והסבר של עורך SVG שמסתמך רק על הדפדפן
מהו SVG
SVG (Scalable Vector Graphics) הוא פורמט תמונה וקטורי מבוסס XML שתקנן ארגון ה-W3C. בשונה מתמונות רסטר כמו PNG או JPEG, ניתן להגדיל ולהקטין אותו בלי אובדן איכות. מאחר שהוא חלק מה-DOM, אפשר להטמיע אותו ישירות ב-HTML ולשלוט בו דינמית בעזרת JavaScript ו-CSS. המפרט הרשמי הוא מפרט SVG של W3C, וכולל צורות, טקסט, מסלולים, מעברי צבע, פילטרים ועוד.
SVG נפוץ במגוון שימושים:
- הצגת אייקונים ותרשימים באתרי אינטרנט
- יצירת דיאגרמות וגרפים דינמיים
- אפליקציות ציור עם אינטראקציה למשתמש
- חומרי הדפסה ועיצוב ממשקים
סקירה של עורך ה-SVG שבניתי
העורך פועל כולו בצד הלקוח, בתוך הדפדפן. המשתמש יכול לצייר צורות בסיסיות כמו מלבנים ואליפסות, וגם כוכבים, בועות דיבור, צורות ענן, לבבות ואפילו מסלולים חופשיים באמצעות מצב עט. ניתן להזיז, לשנות גודל ולסובב כל צורה, קיימים יישור ו-Snap, תמיכה בקיבוץ וברשימת שכבות. אפשר לייצא את התוצאה כ-SVG או כ-PNG.
בהמשך אסביר איך מימשתי בפועל את היכולות שמציע המפרט של SVG, ומה היו המהמורות בדרך.
יישום המפרט והאתגרים שפגשתי
הוספת צורות וטיפול בתכונות
במפרט 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);
}
טרנספורמציות ומערכות צירים
המאפיין transform
מאפשר לשלב translate
, rotate
, scale
ועוד. האתגר היה בחישוב תיבת הגבול של צורה שנבחרה, משום שיש להמיר בין קואורדינטות מקומיות לגלובליות. לשם כך השתמשתי ב-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));
}
כך ניתן לצייר מסגרת בחירה נכונה גם אחרי סיבוב או שינוי גודל.
פונקציית Snap ורשת עזר
את הרשת ציירתי ברקע בעזרת אלמנט <pattern>
. מנגנון ה-Snap מעוגל את הקואורדינטות לצד הקרוב בקפיצות קבועות בחישוב JavaScript.
function snapToGrid(value, gridSize) {
return Math.round(value / gridSize) * gridSize;
}
ב-Snap לאובייקטים אני סורק את מרכזי הצורות ואת הקצוות שלהן, מוצא את הנקודה הקרובה ומציג קווי עזר שמדביקים את האובייקט. זה שדרג משמעותית את הדיוק בממשק.
עריכת טקסט
אלמנט <text>
של SVG אינו תומך בעריכה ישירה, ולכן יצרתי <input>
HTML שמונח זמנית מעליו בעת העריכה.
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>
של SVG מורכב מפקודות כמו 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
ובמערכות צירים - מימוש רשת ו-Snap
- עריכת טקסט אינליין
- עריכת מסלול (מבוססת קווים ישרים)
- ייצוא ל-PNG ול-SVG
- תמיכה בעבודה על קנבס גדול וניתן לגלילה
הצלחתי לנצל את העוצמה של SVG כדי לבנות עורך עשיר שנשען כולו על הדפדפן.