Egy kizárólag böngészőben futó SVG-szerkesztő tervezése és megvalósítása
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.