Klíčové body článku

  • Společně s ChatGPT – většinu času jsem dělal projektového manažera – jsme stavěli SVG editor (/cs/tools/svgedit/). Tady popisuji všechna klopýtnutí.
  • Sám bych projekt nikdy nedotáhl. Generativní AI je v programování stále ohromně silná.
  • Zároveň „přenechat všechno AI“ nefungovalo. Musel jsem hlídat celkový design, opravovat nepochopení a udávat směr při řešení potíží – tedy přesně roli hrajícího manažera.
  • Z tohoto pohledu mi projekt naznačil, co znamená spolupracovat s AI v praxi.

Návrh a implementace SVG editoru, který běží jen v prohlížeči

Co je SVG

SVG (Scalable Vector Graphics) je vektorový obrazový formát založený na XML, který standardizuje W3C. Na rozdíl od rastrových obrázků (PNG, JPEG atd.) se dá bez ztráty kvality zvětšovat i zmenšovat. Protože SVG lze vložit přímo do HTML dokumentu jako součást DOM, lze je dynamicky ovládat přes JavaScript a CSS. Specifikace vychází z doporučení W3C SVG a podporuje tvary, text, cesty, přechody, filtry a mnoho dalšího.

SVG se široce používá v situacích, jako jsou:

  • zobrazování ikon a diagramů na webu,
  • generování dynamických grafů a schémat,
  • kreslicí aplikace reagující na vstup uživatele,
  • tvorba podkladů pro tisk a UI design.

Přehled editoru

Editor, který zde popisuji, je plně klientský nástroj běžící v prohlížeči. Uživatel může kreslit nejen obdélníky a elipsy, ale i hvězdy, bubliny, obláčky, srdce a volné křivky v režimu pera. Objekty lze přesouvat, měnit jejich velikost i otáčet. Uživatelské rozhraní navíc nabízí zarovnávání, přichytávání, seskupování i seznam vrstev. Hotovou práci lze exportovat jako SVG nebo PNG.

V následujících sekcích vysvětluji, jak jsem převáděl specifikaci SVG do JavaScriptu a na jakých místech jsem zakopával.

Implementace specifikace a úskalí na cestě

Přidávání tvarů a práce s atributy

Specifikace SVG definuje pro každý tvar samostatné elementy a atributy. Obdélník používá <rect>, elipsa <ellipse>, mnohoúhelník <polygon>; u každého je nutné správně nastavit atributy x, y, width, height, cx, cy, rx, ry či points. Složitější tvary, jako hvězdy nebo srdce, vznikají pomocí elementu <path>, který kombinuje Bézierovy křivky a příkazy oblouků.

Ukázka implementace (přidání obdélníku)

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

Obdélníky a elipsy jsou jednoduché, ale u mnohoúhelníků a vlastních tvarů je nutné počítat souřadnice. Kreslení hvězdy například vyžaduje rovnoměrně rozdělit středový úhel a spočítat vrcholy, které se pak zapíší do atributu 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);
}

Transformace a souřadnicové soustavy

SVG umožňuje kombinovat translate, rotate a scale pomocí atributu transform. Náročné bylo získat ohraničující rámeček vybraného tvaru tak, aby zohlednil rozdíl mezi lokální a globální soustavou souřadnic. Řešení spočívalo v tom, že jsem nejprve zavolal getScreenCTM() a vše převedl do globálního souřadnicového systému.

Použití převodu souřadnic

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

Díky tomu lze správně vykreslit výběrový rámeček i po aplikaci rotace nebo změny měřítka.

Přichytávání k mřížce

Mřížku jsem vykreslil do pozadí pomocí elementu <pattern>. Samotné přichytávání spočívá v tom, že JavaScript přepočítá souřadnice a zaokrouhlí je na daný krok.

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

Při objektovém přichytávání se prochází středové body a krajní body ostatních tvarů, hledá se nejbližší a zobrazuje se pomocná čára, aby se objekt snadněji „přilepil“. Výsledkem je mnohem přesnější práce s UI.

Úprava textu

Element <text> v SVG nelze upravovat přímo, proto jsem použil metodu, kdy se dočasně překryje HTML <input> pro inline editaci.

function editTextElement(textElem) {
  const input = document.createElement("input");
  input.type = "text";
  input.value = textElem.textContent;

<input> umístím na stejné souřadnice, přebere stylování a po potvrzení změny hodnotu zapíšu zpět do elementu <text>. Tím lze nabídnout známé chování, které uživatelé očekávají.

Export SVG a PNG

Export do formátu SVG řeším pouhým serializováním DOMu do textu a vytvořením souboru. V případě PNG je potřeba vykreslit aktuální stav do <canvas> pomocí drawImage. Nejprve tedy vytvořím off-screen <canvas>, do něj vykreslím SVG pomocí createImageBitmap a výsledek následně uloží canvas.toBlob.

async function exportPNG(svgElement) {
  const data = new XMLSerializer().serializeToString(svgElement);
  const blob = new Blob([data], { type: 'image/svg+xml' });
  const url = URL.createObjectURL(blob);
  const bitmap = await createImageBitmap(await fetch(url).then(r => r.blob()));
  const canvas = document.createElement('canvas');
  canvas.width = bitmap.width;
  canvas.height = bitmap.height;
  canvas.getContext('2d').drawImage(bitmap, 0, 0);
  bitmap.close();
  return await new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
}

PNG export je náročnější na paměť, takže při větších kresbách je dobré upozornit uživatele, aby chvíli počkal.

Jak jsem rozkládal řízení stavu

Celý editor běží na kombinaci petite-vue a jednoduchých vlastních obchodních objektů. Každý tvar drží stav v plain-objektu, zatímco historie operací je uložená ve stacku. Undo/redo se řeší přidáváním a odebíráním snímků, které popisují změny atributů.

const history = [];
let pointer = -1;

function pushHistory(action) {
  history.splice(pointer + 1);
  history.push(action);
  pointer = history.length - 1;
}

function undo() {
  if (pointer < 0) return;
  history[pointer].undo();
  pointer--;
}

function redo() {
  if (pointer >= history.length - 1) return;
  pointer++;
  history[pointer].redo();
}

Díky tomu není potřeba ukládat celé SVG při každém kroku a editor reaguje svižně i s desítkami objektů.

Proč byla spolupráce s AI nutná

Generativní AI jsem využíval hlavně jako „pair programmer“. Dokázala připravit kostru kódu, navrhnout datové struktury nebo zkontrolovat, zda určité API existuje ve všech prohlížečích. Když narazila na slepou uličku, musel jsem ručně zasáhnout, přenastavit zadání nebo prompt a zkontrolovat, zda řešení opravdu odpovídá specifikaci.

Nefungovalo to, pokud jsem jen napsal „udělej SVG editor“. Výsledkem byl obvykle nekonzistentní kód bez ohledu na reálné UX. Fungovat začalo až tehdy, když jsem dával konkrétní pokyny: „teď potřebujeme zarovnání k mřížce, které respektuje transformace“, „přidej operaci pro seskupení a rozdělení“ apod.

Co jsem si z projektu odnesl

  • SVG je nesmírně flexibilní a jeho možnosti využiji jen tehdy, pokud rozumím jednotlivým atributům.
  • Přichytávání, transformace a historie změn jsou funkce, které se zdají samozřejmé, ale implementačně zaberou většinu času.
  • Generativní AI má ohromnou sílu, pokud jí člověk dokáže zadávat správné úkoly a hlídat výsledek.

Celkově byl projekt připomínkou, že AI není náhrada za zodpovědného koordinátora. Potřebuje někoho, kdo drží směr, vyhodnocuje kvalitu a rozhoduje, co je „dost dobré“. V tom spočívá role hrajícího manažera ve věku AI.


Další zdroje