Inti artikel ini

  • Saya kembali berduet dengan ChatGPT—kali ini lebih berperan sebagai manajer proyek—untuk membuat editor SVG (/id/tools/svgedit/), dan ini adalah catatan tentang berbagai titik tempat saya tersandung.
  • Mustahil saya menyelesaikan proyek ini sendirian; kemampuan pemrograman AI generatif memang luar biasa.
  • Namun menyerahkan semuanya pada AI juga tidak berhasil. Saya tetap harus mengoordinasikan desain keseluruhan, meluruskan salah tafsir, dan menunjukkan arah saat terjadi masalah—persis seperti playing manager.
  • Dalam pengertian itu, pengalaman ini memberi petunjuk tentang bagaimana bertahan di era AI.

Merancang dan mengimplementasikan editor SVG yang sepenuhnya berjalan di browser

Apa itu SVG?

SVG (Scalable Vector Graphics) adalah format gambar vektor berbasis XML yang distandardisasi oleh W3C. Berbeda dengan gambar raster seperti PNG atau JPEG, SVG dapat diperbesar dan diperkecil tanpa kehilangan kualitas. Karena SVG bisa disematkan langsung ke dalam dokumen HTML sebagai bagian dari DOM, kita dapat memanipulasi dan mengubah gayanya secara dinamis dengan JavaScript maupun CSS. Spesifikasinya mengikuti rekomendasi SVG W3C dan mendukung bentuk, teks, path, gradasi, filter, dan masih banyak lagi.

SVG digunakan secara luas untuk kebutuhan berikut:

  • Menampilkan ikon atau diagram di halaman web
  • Menghasilkan diagram atau grafik yang dinamis
  • Membangun aplikasi gambar dengan interaksi pengguna
  • Membuat aset untuk materi cetak atau desain antarmuka

Gambaran umum editor

Editor yang saya bahas di sini sepenuhnya berjalan di sisi klien. Pengguna dapat menggambar bukan hanya persegi dan elips, tetapi juga bintang, gelembung percakapan, bentuk awan, hati, dan kurva bebas melalui mode pena. Setiap bentuk bisa dipindah, diubah ukuran, dan diputar. UI-nya juga mendukung perataan, snapping, pengelompokan, serta daftar layer. Karya yang sudah selesai dapat diekspor sebagai SVG maupun PNG.

Bagian berikut menjelaskan bagaimana saya menerjemahkan spesifikasi SVG ke dalam JavaScript dan di mana saja saya tersandung sepanjang prosesnya.

Menerapkan spesifikasi dan rintangan yang muncul

Menambahkan bentuk dan menangani atribut

Spesifikasi SVG mendefinisikan elemen dan atribut khusus untuk setiap jenis bentuk. Persegi menggunakan <rect>, elips menggunakan <ellipse>, poligon menggunakan <polygon>, dan masing-masing menuntut atribut seperti x, y, width, height, cx, cy, rx, ry, atau points yang harus diisi dengan benar. Bentuk kompleks seperti bintang atau hati dibuat dengan elemen <path> yang menggabungkan kurva Bézier dan perintah busur.

Contoh implementasi (penambahan persegi)

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

Persegi dan elips tergolong sederhana, tetapi poligon dan bentuk kustom menuntut perhitungan koordinat. Ketika menggambar bintang, misalnya, saya membagi sudut pusat secara merata, menghitung koordinat setiap titik, lalu mengumpulkannya ke atribut 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);
}

Transformasi dan sistem koordinat

Dalam SVG, atribut transform memungkinkan kita menggabungkan translate, rotate, scale, dan lain-lain untuk memanipulasi bentuk. Tantangannya muncul saat perlu mendapatkan bounding box dari bentuk yang dipilih: saya harus menjembatani perbedaan antara koordinat lokal dan global. Untuk itu saya menggunakan getScreenCTM() guna mengubah koordinat ke sistem global dan menghitung persegi pembatasnya.

Contoh implementasi (menerapkan transformasi koordinat)

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

Dengan cara ini, saya bisa menggambar kotak seleksi yang benar meskipun bentuknya sudah diputar atau diskalakan.

Fitur snap dan grid

Saya menggambar grid di latar belakang menggunakan elemen <pattern> pada SVG. Proses snapping dilakukan di JavaScript dengan membulatkan koordinat ke interval yang ditentukan.

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

Untuk object snap, saya memindai titik tengah dan titik ujung milik bentuk lain, mencari kandidat terdekat, lalu menampilkan garis bantu agar bentuk menempel di titik tersebut. Pendekatan ini sangat meningkatkan ketepatan UI.

Penyuntingan teks

Elemen <text> pada SVG tidak bisa diedit langsung, jadi saya menumpuk elemen HTML <input> sementara di atasnya untuk pengeditan inline.

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 dan kurva bebas

Elemen <path> pada SVG menggabungkan perintah M, L, C, A, dan lain-lain. Saya memulai dari penyuntingan berbasis garis lurus: setiap klik menambahkan koordinat baru.

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

Ekspor (SVG/PNG)

Saat menyimpan, saya membuang elemen bantu kemudian mengekspor hasilnya. Konversi PNG dilakukan lewat 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;
}

Dukungan untuk kanvas yang dapat digulir

Untuk menangani SVG berukuran besar, saya membungkusnya dengan <div style="overflow:auto"> agar scrollbar muncul bila diperlukan.

Ringkasan

Spesifikasi SVG sangat kuat, tetapi implementasinya membawa tantangan seperti transformasi koordinat dan definisi path. Dalam editor ini saya berhasil merapikan semuanya dan mencapai hal-hal berikut:

  • Mengelola atribut elemen bentuk (rect, ellipse, polygon, path)
  • Menangani transform dan sistem koordinat dengan benar
  • Mengimplementasikan grid dan snapping
  • Menghadirkan penyuntingan teks inline
  • Mengelola penyuntingan path berbasis garis
  • Mendukung ekspor ke PNG maupun SVG
  • Menyediakan kanvas yang dapat digulir untuk layar besar

Dengan memaksimalkan kemampuan SVG, saya dapat membangun editor kaya fitur yang berjalan sepenuhnya di dalam browser.