只用瀏覽器的 SVG 編輯器:實作與規格解說
本文重點
- 和往常一樣,我與 ChatGPT 兩人三腳──嚴格說我幾乎只是負責下指示的人──想打造 SVG 編輯器/zh-hant/tools/svgedit/,結果一路跌跌撞撞。這篇整理所有的卡關點。
- 我一個人絕對做不到。生成式 AI 的寫程式能力確實驚人。
- 但也不是「交給 AI 就會完成」。必須由我來統籌全貌、修正需求誤解、決定排障方向,徹頭徹尾扮演行動型主管。
- 從這層意義看,這次經驗也提供了與 AI 共事時如何生存的提示。
只用瀏覽器的 SVG 編輯器:實作與規格解說
什麼是 SVG?
SVG(Scalable Vector Graphics,可縮放向量圖形)是 W3C 制定的 XML 為基礎的向量影像格式。與 PNG、JPEG 等點陣圖不同,放大或縮小都不會失真。此外,因為 SVG 可以作為 DOM 的一部分直接嵌入 HTML 文件,因此能透過 JavaScript 與 CSS 進行動態操作與套用樣式。規格遵循 W3C SVG 規格,具備圖形、文字、路徑、漸層、濾鏡等強大表現力。
SVG 廣泛應用於下列情境:
- 網頁上的圖示與圖表顯示
- 動態產生流程圖或圖解
- 具備使用者互動的圖形繪圖應用程式
- 製作印刷品或 UI 設計素材
本次 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
等變形。棘手的是,選取圖形的外框時必須彌補本地座標與全域座標的差異。為了解決這點,我利用 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));
}
如此一來,即便套用了旋轉或縮放,也能畫出正確的選取框。
貼齊功能與格線
格線使用 SVG 的 <pattern>
元素描繪在背景。貼齊(snap)則由 JavaScript 計算座標並四捨五入到指定間距。
function snapToGrid(value, gridSize) {
return Math.round(value / gridSize) * gridSize;
}
物件貼齊會掃描其他圖形的中心與端點,找出最近點並顯示輔助線,讓圖形吸附對齊。這大幅提升了操作精度。
文字編輯
由於 SVG 的 <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);
});
}
路徑與自由曲線
SVG 的 <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 的表現力,打造出只需瀏覽器就能運行的高機能編輯器。