Điểm chính của bài viết

  • Tôi phối hợp cùng ChatGPT, đóng vai trò người giao việc, để xây dựng trình chỉnh sửa SVG /tools/svgedit/ nhưng vấp phải vô số trở ngại; bài viết tổng hợp lại những điểm khó ấy.
  • Một mình tôi chắc chắn không làm nổi; khả năng viết mã của AI sinh nội dung vẫn rất đáng nể.
  • Dẫu vậy cũng không thể phó mặc cho AI: tôi phải điều phối tổng thể, sửa sai các yêu cầu và định hướng xử lý sự cố — đúng nghĩa một người quản lý kiêm thực thi.
  • Kinh nghiệm đó cũng gợi ý cách sinh tồn trong thời đại AI.

Phân tích triển khai và đặc tả của trình chỉnh sửa SVG chạy hoàn toàn trên trình duyệt

SVG là gì

SVG (Scalable Vector Graphics) là định dạng hình ảnh vector dựa trên XML được W3C chuẩn hóa. Khác với ảnh raster (PNG hay JPEG), SVG không bị giảm chất lượng khi phóng to thu nhỏ. Ngoài ra, SVG có thể nhúng trực tiếp vào tài liệu HTML như một phần của DOM, nên ta có thể thao tác động và thay đổi kiểu bằng JavaScript hoặc CSS. Định dạng này tuân theo đặc tả SVG của W3C và hỗ trợ nhiều khả năng biểu đạt như hình khối, văn bản, đường path, gradient, bộ lọc.

SVG thường được dùng trong những tình huống sau:

  • Hiển thị biểu tượng và biểu đồ trên trang web
  • Sinh biểu đồ, sơ đồ động
  • Ứng dụng vẽ hình có tương tác với người dùng
  • Tạo tư liệu in ấn và thiết kế giao diện

Tổng quan về trình chỉnh sửa SVG lần này

Trình chỉnh sửa này chạy hoàn toàn phía trình duyệt, không cần máy chủ. Người dùng có thể tạo nhiều hình dạng: từ hình chữ nhật, hình elip đến ngôi sao, bóng thoại, đám mây, trái tim, và cả đường cong tự do bằng chế độ bút. Mỗi hình đều có thể kéo thả, phóng to thu nhỏ, xoay; công cụ còn hỗ trợ căn chỉnh, bắt dính, gom nhóm và danh sách lớp. Bản vẽ hoàn thiện có thể xuất ra SVG hoặc PNG.

Phần dưới đây giải thích chi tiết cách tôi chuyển đặc tả SVG thành triển khai JavaScript.

Cách hiện thực đặc tả và những vấp váp

Thêm hình và xử lý thuộc tính

Đặc tả SVG định nghĩa phần tử và thuộc tính riêng cho từng loại hình. Hình chữ nhật dùng <rect>, hình elip dùng <ellipse>, đa giác dùng <polygon>; mỗi loại phải gán đúng các thuộc tính như x, y, width, height, cx, cy, rx, ry hoặc points. Những hình phức tạp như ngôi sao hay trái tim được tạo bằng <path> với các lệnh bezier và cung.

Ví dụ triển khai (thêm hình chữ nhật)

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);
}

Hình chữ nhật hay hình elip tương đối đơn giản, nhưng đa giác và hình tự do đòi hỏi tính toán tọa độ. Ví dụ để vẽ ngôi sao, tôi chia đều góc ở tâm, tính các đỉnh rồi đưa vào thuộc tính 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);
}

Biến đổi và hệ tọa độ

Trong SVG, thuộc tính transform cho phép kết hợp translate, rotate, scale để điều khiển hình. Điểm khó là khi lấy bounding box của hình được chọn, ta phải triệt tiêu sai khác giữa tọa độ cục bộ và toàn cục. Tôi dùng getScreenCTM() để chuyển sang hệ tọa độ toàn cục rồi mới tính hình chữ nhật bao quanh.

Ví dụ triển khai (áp dụng chuyển đổi tọa độ)

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));
}

Nhờ vậy khung chọn vẫn hiển thị chính xác kể cả khi đã xoay hoặc phóng to thu nhỏ.

Bắt dính và lưới

Lưới nền được vẽ bằng phần tử <pattern> trong SVG. Việc bắt dính được tính toán bằng JavaScript: làm tròn tọa độ theo bước đã định.

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

Bắt dính theo đối tượng thì duyệt qua tâm và mép của các hình khác, tìm điểm gần nhất rồi hiển thị đường phụ trợ để hút vào. Điều này làm độ chính xác của giao diện tăng lên rõ rệt.

Chỉnh sửa văn bản

Phần tử <text> của SVG không thể sửa trực tiếp, nên tôi tạm thời phủ lên một <input> HTML để chỉnh sửa nội dung.

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);
  });
}

Đường path và đường cong tự do

Phần tử <path> kết hợp các lệnh M, L, C, A để vẽ hình. Tôi khởi đầu bằng cách dựng các đoạn thẳng nối tiếp: mỗi lần nhấp chuột sẽ bổ sung một tọa độ vào đường path.

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

Xuất dữ liệu (SVG/PNG)

Khi lưu, tôi loại bỏ các phần tử phụ trợ rồi xuất dữ liệu. Việc chuyển sang PNG được thực hiện qua Canvas trung gian.

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;
}

Hỗ trợ cuộn canvas

Đối với bản vẽ kích thước lớn, tôi bọc vùng làm việc trong <div style="overflow:auto"> để hiện thanh cuộn khi cần.

Tổng kết

SVG rất mạnh mẽ về đặc tả nhưng triển khai thực tế gặp nhiều thách thức về hệ tọa độ và định nghĩa đường path. Trình chỉnh sửa này đã sắp xếp lại các vấn đề và đạt được những điểm sau:

  • Điều khiển thuộc tính của các hình (rect, ellipse, polygon, path)
  • Xử lý chính xác transform và hệ tọa độ
  • Triển khai lưới và bắt dính
  • Chỉnh sửa văn bản trực tiếp trên màn hình
  • Chỉnh sửa đường path dựa trên đoạn thẳng
  • Xuất ra PNG và SVG
  • Hỗ trợ vùng vẽ lớn có thể cuộn

Nhờ tận dụng tối đa sức mạnh biểu đạt của SVG, tôi đã xây dựng được một trình chỉnh sửa giàu tính năng chạy hoàn toàn trên trình duyệt.