この記事の要点

  • ChatGptと例によって二人三脚というか指示出すだけの人になりながらSVGエディタ/tools/svgedit/を作ろうとしたものの、躓きまくりなかなかできなかった苦労ポイントのまとめ。
  • 一人では絶対できなかった。生成AIのコーディング力はやはりすごい。
  • 一方でお任せだけでもできなかった。全体をコーディネートしたり要件誤りや勘違いをただしたり、トラブル対応の方向性を示すなど、まさにプレイングマネージャーとしての役割が求められた。
  • そういう意味でもAI時代を生き抜く示唆にもなったかも。

ブラウザだけで完結するSVGエディタの実装と仕様の解説

SVGとは何か

SVG(Scalable Vector Graphics)は、W3Cによって標準化されているXMLベースのベクター画像フォーマットである。ラスター画像(PNGやJPEGなど)と異なり、拡大・縮小しても画質が劣化しないという特長を持つ。また、DOMの一部としてHTML文書に直接埋め込めるため、JavaScriptやCSSを用いた動的な操作・スタイル変更が可能である。仕様はW3C SVG仕様に準拠しており、図形、テキスト、パス、グラデーション、フィルターなど幅広い表現力を備えている。

SVGは次のような用途で広く活用されている。

  • Webページ上のアイコンや図表の表示
  • ダイアグラムやチャートの動的生成
  • ユーザーインタラクションを含む図形描画アプリケーション
  • 印刷物やUIデザインの素材作成

今回のSVGエディタの概要

本エディタは、ブラウザだけで動作する完全クライアントサイドのツールである。ユーザーは矩形や楕円といった基本図形だけでなく、星形、吹き出し、雲形、ハート、さらには自由曲線を描けるペンモードを用いて多彩な図形を作成できる。作成した図形は移動・拡縮・回転が可能で、整列やスナップ機能、グループ化やレイヤー一覧もサポートしている。完成した図はSVG形式やPNG形式でエクスポート可能である。

以下では、SVGの仕様をどのようにJavaScriptで実装に落とし込んだかを具体的に解説する。

実装における仕様反映と躓いた点

図形の追加と属性の扱い

SVG仕様では、図形ごとに固有の要素・属性が定義されている。矩形は<rect>、楕円は<ellipse>、多角形は<polygon>を利用し、それぞれx,y,width,heightcx,cy,rx,rypointsといった属性を正しく設定する必要がある。星形やハートといった複雑形状は<path>要素でベジェ曲線や弧コマンドを組み合わせて生成した。

実装例(矩形追加処理)

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

矩形や楕円は単純だが、多角形やカスタムシェイプでは座標計算が必要になる。例えば星形を描く際は、中心角を等分して頂点を算出し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);
}

変形と座標系の扱い

SVGではtransform属性によりtranslaterotatescaleなどを組み合わせて図形を操作できる。問題は、選択した図形のバウンディングボックスを取得する際に、ローカル座標とワールド座標の違いを吸収する必要があった点である。これにはgetScreenCTM()を利用し、グローバル座標系に変換して矩形を計算した。

実装例(座標変換の適用)

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

これにより、回転や拡縮を適用した後でも正しい選択枠を描画できるようになった。

スナップ機能とグリッド

グリッドはSVGの<pattern>要素を使って背景に描画した。スナップ処理はJavaScriptで座標を計算し、指定間隔に丸める方式とした。

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

オブジェクトスナップは他図形の中心点や端点を走査して最近傍を検出し、補助線を表示して吸着させる仕組みを導入した。これによりUIの精度が大幅に向上した。

テキスト編集

SVGの<text>要素は直接編集できないため、インライン編集用にHTMLの<input>を一時的に重ねる方式を採用した。

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

パスと自由曲線

SVGの<path>はM, L, C, Aといったコマンドを組み合わせて描画する。まずは直線ベースの編集から始め、クリックごとに座標を追加していく。

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

出力(SVG/PNGエクスポート)

保存時には補助要素を除去してエクスポートする。PNG変換は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;
}

キャンバスのスクロール対応

大きなSVGを扱う場合は<div style="overflow:auto">でラップし、必要に応じてスクロールバーを表示することで解決した。

まとめ

SVGは仕様が強力である一方、実装上は座標変換やパス定義などのハードルが高い。本エディタではそれらを整理し、以下を実現した。

  • 図形要素の属性操作(rect, ellipse, polygon, path)
  • transformと座標系の正しい扱い
  • グリッド・スナップの実装
  • インラインテキスト編集
  • パス編集(直線ベース)
  • PNG/SVG両対応のエクスポート
  • スクロール可能な大画面対応

SVGの持つ表現力を最大限に活かし、ブラウザだけで動作する高機能なエディタを構築できた。