A cikk fő tanulságai

  • Ismét együtt dolgoztam a ChatGPT-vel—most inkább projektmenedzserként—egy SVG-szerkesztő (/hu/tools/svgedit/) megépítésén, és ez a napló arról szól, hol akadtam el.
  • Egyedül soha nem tudtam volna befejezni; a generatív MI valóban kiemelkedő segítség a kódolásban.
  • Ugyanakkor nem működött az, hogy mindent ráhagyjak az MI-re. Nekem kellett összefogni a teljes tervet, helyreigazítani a félreértéseket és irányítani a hibakeresést, pont úgy, mint egy pályaedzőnek.
  • Ebben az értelemben ez a tapasztalat jó ízelítőt adott abból, milyen együtt dolgozni egy MI-vel a valóságban.

Egy kizárólag böngészőben futó SVG-szerkesztő tervezése és megvalósítása

Mi az az SVG?

Az SVG (Scalable Vector Graphics) egy XML-alapú vektoros képformátum, amelyet a W3C szabványosított. A PNG vagy JPEG típusú raszteres képekkel ellentétben az SVG grafikák veszteség nélkül nagyíthatók és kicsinyíthetők. Mivel az SVG közvetlenül beágyazható egy HTML-dokumentumba a DOM részeként, JavaScripttel és CSS-sel dinamikusan kezelhető és formázható. A specifikáció követi a W3C SVG Recommendation előírásait, és támogatja a formákat, szövegeket, útvonalakat, színátmeneteket, szűrőket és még sok mást.

Az SVG rengeteg helyen felbukkan:

  • Ikonok és diagramok megjelenítése a weben
  • Dinamikus grafikonok és ábrák generálása
  • Felhasználói inputra reagáló rajzolóalkalmazások építése
  • Nyomdai vagy UI-tervezési elemek készítése

A szerkesztő áttekintése

Az itt bemutatott szerkesztő teljes egészében kliensoldali eszköz, a böngészőben fut. A felhasználó nemcsak téglalapokat és ellipsziseket rajzolhat, hanem csillagokat, beszédbuborékokat, felhőket, szíveket és szabadkézi görbéket is toll módban. Az alakzatok mozgathatók, átméretezhetők, forgathatók. A felület támogatja az igazítást, az illesztést, a csoportosítást és a rétegek listáját. A kész munka SVG-ként vagy PNG-ként exportálható.

Az alábbi szakaszok arról szólnak, hogyan fordítottam le az SVG specifikációt JavaScriptre, és milyen akadályokba botlottam útközben.

A specifikáció megvalósítása és az útközben felbukkanó akadályok

Alakzatok hozzáadása és attribútumok kezelése

Az SVG specifikáció minden alakzattípushoz dedikált elemeket és attribútumokat határoz meg. Téglalaphoz <rect>, ellipszishez <ellipse>, sokszöghez <polygon> kell, mindegyikhez olyan attribútumokat kell helyesen beállítani, mint az x, y, width, height, cx, cy, rx, ry vagy points. Az összetettebb alakzatok, például a csillagok vagy a szívek <path> elemekből állnak, amelyek Bézier-görbéket és ívparancsokat kombinálnak.

Példa megvalósítás (téglalap hozzáadása)

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

A téglalapok és ellipszisek egyszerűek, de a sokszögek és az egyedi alakzatok koordinátaszámítást igényelnek. Csillag rajzolásakor például egyenletesen kell osztani a középső szöget, kiszámolni a csúcsokat, majd beírni őket a points attribútumba.

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

Transzformációk és koordinátarendszerek

Az SVG-ben a transform attribútummal kombinálható a translate, rotate és scale. A legtrükkösebb rész az volt, hogyan számoljam ki a kijelölt alakzat határoló dobozát úgy, hogy figyelembe veszem a lokális és globális koordinátarendszert. A megoldás az lett, hogy minden értéket a globális rendszerbe konvertáltam getScreenCTM() hívással, mielőtt a téglalapot kiszámoltam.

A koordináta-transzformáció alkalmazása

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

Ezzel a kiválasztási keret a forgatás vagy méretezés után is a helyén marad.

Rács és illesztés

A rács egy háttérben kirajzolt SVG <pattern> elem. Az illesztés JavaScriptben egy egyszerű segédfüggvénnyel kerekíti a koordinátákat.

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

Az objektum-illesztéshez a szerkesztő végigpásztázza a többi alakzatot középpontok és végpontok után kutatva, megkeresi a legközelebbi jelöltet, majd segédvonalakat jelenít meg, hogy „odarántsa” a kijelölt elemet. Ez rengeteget javított a felület pontosságán.

Szövegszerkesztés

Mivel egy SVG <text> elem nem szerkeszthető közvetlenül, a szerkesztő ideiglenesen egy HTML <input> mezőt tesz fölé az inline szerkesztéshez.

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

Útvonalak és szabadkézi görbék

A <path> elem olyan parancsokat használ, mint az M, L, C és A. Kezdetnek egyenes szakaszokkal dolgoztam, ahol minden kattintás új koordinátát ad a rajzhoz.

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

Exportálás SVG-be vagy PNG-be

Mentéskor a szerkesztő eltávolítja a segédelemeket, majd exportál. A PNG-konverzió vásznon keresztül történik.

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

Görgethető vásznak kezelése

Nagy munkaterületeknél elegendő volt az SVG-t egy <div style="overflow:auto"> elembe csomagolni, és szükség esetén görgetősávokat megjeleníteni, így a felület használható maradt.

Záró gondolatok

Az SVG specifikáció rendkívül erős, de a megvalósításához meg kell küzdeni a koordináta-transzformációkkal, az útvonalak definiálásával és még sok mással. Ez a szerkesztő úgy szervezi ezeket a feladatokat, hogy a következő funkciókat nyújtja:

  • Attribútumkezelés téglalapokhoz, ellipszisekhez, sokszögekhez és útvonalakhoz
  • A transform és a koordinátarendszerek helyes használata
  • Rács és illesztési segédlet
  • Inline szövegszerkesztés
  • Útvonalszerkesztés (egyelőre egyenes szegmensek)
  • Exportálás PNG-be és SVG-be
  • Nagy vásznak támogatása görgetéssel

Az SVG kifejezőereje tette lehetővé, hogy egy teljes értékű szerkesztőt építsek, amely kizárólag a böngészőben fut.