Selainpohjaisen SVG-editorin toteutus ja spesifikaation tulkinta
Artikkelin pääkohdat
- Yritin rakentaa SVG-editorin (/fi/tools/svgedit/) jälleen kerran käsi kädessä ChatGPT:n kanssa – käytännössä projektipäällikön roolissa – ja tämä on kooste niistä kohdista, joihin takeltelin.
- En olisi saanut työkalua valmiiksi yksin. Generatiivisen tekoälyn koodauskyvykkyys on edelleen vaikuttava.
- Pelkkä “annetaan kaiken olla AI:n hoidossa” ei kuitenkaan toiminut. Jouduin koordinoimaan kokonaisuuden, korjaamaan väärinymmärryksiä ja näyttämään suunnan ongelmatilanteissa – juuri sitä, mitä pelaava esimies tekee.
- Tässä mielessä kokemus tarjosi vihjeen siitä, millaista on työskennellä AI:n rinnalla käytännön elämässä.
Selainpohjaisen SVG-editorin toteutus ja spesifikaation tulkinta
Mikä SVG on?
SVG (Scalable Vector Graphics) on XML-pohjainen vektorigrafiikkamuoto, jonka W3C on standardoinut. Toisin kuin rasterikuvat kuten PNG tai JPEG, SVG:tä voi suurentaa ja pienentää laadun heikkenemättä. Koska SVG voidaan upottaa suoraan HTML-dokumenttiin osaksi DOMia, sitä voi käsitellä ja tyylitellä dynaamisesti JavaScriptilla ja CSS:llä. Spesifikaatio noudattaa W3C:n SVG-suositusta ja tarjoaa laajan tuen muodolle, tekstille, poluille, liukuväreille, suodattimille ja paljolle muulle.
SVG:tä hyödynnetään muun muassa seuraavasti:
- Ikonien ja kaavioiden esittämiseen verkossa
- Dynaamisten diagrammien ja kaavioiden generointiin
- Piirto-ohjelmiin, jotka reagoivat käyttäjän toimintaan
- Tulostus- ja käyttöliittymämateriaalien tuottamiseen
Editorin yleiskuvaus
Tässä kuvattu editori on täysin selaimessa toimiva työkalu. Käyttäjä voi piirtää suorakulmioiden ja ellipsien lisäksi tähtiä, puhekuplia, pilvenmuotoja, sydämiä ja vapaakäyräisiä viivoja kynätilassa. Muotoja voi siirtää, skaalata ja pyörittää. Käyttöliittymä tarjoaa myös kohdistuksen, snap-kiinnityksen, ryhmittelyn ja kerroslistan. Valmiin kuvan voi viedä sekä SVG- että PNG-muodossa.
Alla käyn läpi, miten tulkitsin SVG-spesifikaation JavaScriptiksi ja missä kohdissa kompastuin.
Spesifikaation sovittaminen toteutukseen ja kompastuskivet
Muotojen lisääminen ja attribuuttien hallinta
SVG-spesifikaatio määrittelee jokaiselle muodolle omat elementtinsä ja attribuuttinsa. Suorakulmiot käyttävät <rect>
-elementtiä, ellipsit <ellipse>
-elementtiä ja monikulmiot <polygon>
-elementtiä. Näille on asetettava oikeat attribuutit kuten x
, y
, width
, height
, cx
, cy
, rx
, ry
tai points
. Monimutkaisemmat muodot, kuten tähdet ja sydämet, luodaan <path>
-elementillä yhdistämällä Bézier-käyriä ja kaarikomentoja.
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);
}
Suorakulmiot ja ellipsit ovat helppoja, mutta monikulmiot ja räätälöidyt muodot vaativat koordinaattilaskentaa. Tähtiä piirtäessä jakoimme keskuskulman tasavälein, laskimme kärkien sijainnit ja syötimme ne points
-attribuuttiin.
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);
}
Muunnokset ja koordinaatistot
transform
-attribuutilla voi yhdistää translate
-, rotate
- ja scale
-muunnoksia, joilla muotoja manipuloidaan. Haasteeksi nousi valitun muodon rajaavan laatikon laskeminen: paikallisen ja globaalin koordinaatiston erot piti kuroa umpeen. Ratkaisimme tämän käyttämällä getScreenCTM()
-metodia, joka muuntaa pisteet globaaleihin koordinaatteihin.
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));
}
Näin saimme piirrettäväksi oikean valintakehyksen myös kiertojen ja skaalausten jälkeen.
Snap-toiminto ja ruudukko
Piirsimme ruudukon taustalle SVG:n <pattern>
-elementillä. Snap-toiminto pyöristää koordinaatit JavaScriptilla määriteltyyn väliin.
function snapToGrid(value, gridSize) {
return Math.round(value / gridSize) * gridSize;
}
Objektisnappi käy läpi muiden muotojen keskipisteet ja päätepisteet, etsii lähimmän ja näyttää ohjausviivan, jolloin kohdistus helpottuu ja tarkkuus paranee selvästi.
Tekstin muokkaus
Koska SVG:n <text>
-elementtiä ei voi muokata suoraan, lisäsimme hetkellisesti HTML:n <input>
-kentän tekstin päälle inline-editointia varten.
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);
});
}
Polut ja vapaakäyrät
SVG:n <path>
yhdistää M-, L-, C- ja A-komentoja. Aloitimme suoraviivaisesta muokkauksesta ja lisäsimme jokaisella klikkauksella uuden koordinaatin.
let pathData = "M100,100";
function addPathPoint(x, y) {
pathData += ` L${x},${y}`;
path.setAttribute("d", pathData);
}
Vienti (SVG/PNG)
Tallennusvaiheessa poistamme apuelementit ennen vientiä. PNG-muunnos tehdään Canvasin kautta.
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;
}
Työskentely suurilla piirroksilla
Suuria SVG-kuvia käsiteltäessä kiedoimme piirtoalueen <div style="overflow:auto">
-elementtiin, jolloin selaimen vierityspalkit ratkaisevat näyttöongelman.
Yhteenveto
SVG:n spesifikaatio on monipuolinen, mutta toteutuksessa kompastuskiviä ovat etenkin koordinaatiston hallinta ja polkujen määrittely. Tässä editorissa kokosimme asiat hallittavaksi kokonaisuudeksi ja saimme aikaan seuraavat:
- Muotoelementtien attribuuttien manipulointi (rect, ellipse, polygon, path)
transform
-muunnosten ja koordinaatistojen oikea käsittely- Ruudukon ja snap-toiminnon toteutus
- Inline-tekstin editointi
- Polkueditori (suoraviivainen tapaus)
- Vienti sekä PNG- että SVG-muotoon
- Vieritettävä käyttöliittymä suurille piirroksille
SVG:n ilmaisukykyä hyödyntämällä pystyimme rakentamaan tehokkaan editorin, joka toimii kokonaan selaimessa.