1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SVGコードを簡単に編集するアプリを作成した

1
Posted at

こんにちは🌤 自称、駆け出しフルスタックエンジニアのここあです!

普段は WezTerm + Neovim でターミナルに引きこもり、マウスをできるだけ触らない生活を送っています。

そんな私が長年悩んでいたのが「スライドの図問題」です。 Obsidian で Marp を使ってスライドを作っているのですが、ガントチャートだけパワポで作って画像として貼り付けるという微妙なワークフローを続けていました。

「パワポのためだけにパワポを残すのはやめたい」

その一念で、SVG コードをキーボードのみで快適に編集できるツール「VSvgCode」を作りました。


背景:なぜ SVG エディタが必要だったのか

Marp でスライドを作るエンジニアの悩み

スライドの作成はMarpとObsidianのプラグインを用いて行っております。
こちらを参照ください。

このプラグインを用いて発表までは出来るようになったのですが、一方でスライドの作成時には、こんな問題にぶつかります。

  • Marp は Mermaid のガントチャートに未対応
  • Mermaid 感あふれるガントチャートはデザイン的にイマイチ
  • 図のためだけにパワポを使い続けるのが苦痛

そこで SVG を手書きするようにしたのですが、今度は別の問題が生まれました。

「SVG コードとプレビューを行き来する作業がつらい」

VSCode でコードを書いてブラウザでプレビューを確認する…という往復が地味に面倒で、しかも「どのタグがどの要素に対応しているのか」がすぐわからない。

既存の SVG エディタ(draw.io など)はマウス操作前提で、ターミナルから離れたくない自分には合いませんでした。

解決策:双方向ハイライト付き SVG エディタを自作する

ないなら作る。ということで VSvgCode を作りました。

作ったものはこちら


VSvgCode とは

リアルタイムプレビュー + 双方向ハイライト が特徴の、キーボード完結 SVG コードエディタです。

デモはこちら👇
VSvgCodeを試す

ローカルで使いたい場合は、ビルド済みの index.html を1ファイルダウンロードするだけでOKです。Node.js 不要。


主な機能

① リアルタイムプレビューと双方向ハイライト

エディタでコードを編集すると即座にプレビューへ反映されます。 さらに以下の双方向ハイライトが動作します。

操作 動作
エディタでカーソルを移動する プレビュー上の対応要素がハイライト
プレビュー上の要素にホバーする エディタの対応行がハイライト
プレビュー上の要素をクリックする エディタのカーソルがその行にジャンプ

「この要素を直したい」と思ったとき、ソースを探す手間がゼロになります。

② Vim モード(Ctrl+Shift+V

Ctrl+Shift+V で Vim キーバインドのオン・オフを切り替えられます。 Vim モード時は**相対行番号(relative line numbers)**にも自動で切り替わります。 hjkl で動きたい人向けのこだわりポイントです。

③ アーカイブ(Ctrl+Shift+A

作成した SVG を名前をつけて localStorage に保存・管理できます。 ページを閉じる前にドラフトを自動保存し、次回起動時に復元します。

④ エクスポート(SVG / PNG / JPEG)

形式 補足
SVG ソースコードをそのままダウンロード
PNG 2x 高解像度でラスタライズ
JPEG 白背景を自動塗り、2x 高解像度でラスタライズ

技術構成

シングル HTML ファイルにすべてをバンドルしている点が最大の特徴です。

Vite + vite-plugin-singlefile → index.html(完全単体)
TypeScript(フレームワークなし)
CodeMirror 6(エディタ本体)
  ├─ @codemirror/lang-xml(SVG/XML シンタックスハイライト)
  ├─ @codemirror/theme-one-dark
  └─ @replit/codemirror-vim(Vim キーバインド)

React を使わなかった理由

React や Vue を入れるほど状態管理が複雑ではなく、バンドルサイズを抑えたかったため、フレームワークなしの TypeScript にしました。コアロジックは以下のモジュールに分離しています。

モジュール 役割
editor.ts CodeMirror エディタの構築・管理
preview.ts SVG レンダリング・要素ハイライト
highlighter.ts エディタ ↔ プレビューの双方向同期
line-mapper.ts ソース行 ↔ DOM 要素のマッピング
export.ts SVG/PNG/JPEG エクスポート
archive.ts localStorage による保存・復元

シングルファイルにした理由

社内で使うことを想定して、ダウンロードして即使えるツールにしたかったためです。

vite-plugin-singlefile を使うと CSS・JS・フォントをすべて HTML に埋め込んだ単体ファイルが生成されます。

// vite.config.ts
import { defineConfig } from "vite";
import { viteSingleFile } from "vite-plugin-singlefile";

export default defineConfig({
  plugins: [viteSingleFile()],
});

実装でハマったポイント

1. SVG ソースと DOM 要素のマッピング

双方向ハイライトを実現するには「ソースの何行目が DOM のどの要素か」を知る必要があります。

SVG ソースをパースして <タグ> の出現位置を走査し、レンダリング後の DOM 要素と突き合わせる line-mapper.ts を実装しました。

コメント・CDATA・DOCTYPE を除外しながらバイナリサーチでオフセットから行番号を求める処理が地味に面倒でした。

// バイナリサーチで文字オフセット → 行番号を求める
function getLineAt(charOffset: number): number {
  let lo = 0, hi = lineOffsets.length - 1;
  while (lo < hi) {
    const mid = (lo + hi + 1) >> 1;
    if (lineOffsets[mid] <= charOffset) lo = mid;
    else hi = mid - 1;
  }
  return lo + 1; // 1-indexed
}

2. Vim モードと相対行番号の連動

@replit/codemirror-vim を使うと Vim キーバインドは比較的簡単に導入できますが、「Vim モードのときだけ相対行番号にしたい」という要件が少し手間でした。

CodeMirror 6 の Compartment を使うと拡張機能を動的に差し替えられます。vimCompartmentlineNumberCompartment をそれぞれ独立して管理し、トグル時に dispatch で入れ替えます。

const vimCompartment = new Compartment();
const lineNumberCompartment = new Compartment();

// Vim モードのオン・オフ
editorView.dispatch({
  effects: [
    vimCompartment.reconfigure(vimExtension(enabled)),
    lineNumberCompartment.reconfigure(
      enabled ? relativeLineNumbers() : lineNumbers()
    ),
  ],
});

3. PNG/JPEG エクスポートの透過処理

SVG を Canvas 経由でラスタライズするとき、JPEG は透過を扱えないため、drawImage の前に白背景を塗る必要があります。これを忘れると透過部分が真っ黒になります。

if (mimeType === "image/jpeg") {
  ctx.fillStyle = "#ffffff";
  ctx.fillRect(0, 0, width, height); // 先に白背景を塗る
}
ctx.drawImage(img, 0, 0, width, height);

高解像度化のために 2x スケールで Canvas を確保し、ctx.scale(2, 2) で描画しています。

4. localStorage の容量とアーカイブ設計

localStorage は 5MB 前後の制限があり、大量の SVG を保存しようとすると詰まります。書き込み失敗時にアラートを出す最低限のガードを入れています。

function writeArchives(archives: Archive[]) {
  try {
    localStorage.setItem(ARCHIVES_KEY, JSON.stringify(archives));
  } catch {
    alert("localStorage is full. Could not save archives.");
  }
}

使い方

Web で使う

VSvgCodeを試してみる

デモサイトにアクセスするだけで使えます。

ローカルで使う(社内配布など)

GitHub Releases から index.html をダウンロードして、ブラウザで開くだけです。

# ダウンロード後、ダブルクリック or ブラウザにドラッグするだけ
index.html

Node.js・npm・インターネット接続は一切不要です。

自分でビルドする

git clone https://github.com/cocoaai-it/VSvgCode.git
cd VSvgCode
pnpm install
pnpm build
# dist/index.html が生成される

キーボードショートカット一覧

ショートカット 動作
Ctrl+S SVG ファイルとして保存
Ctrl+Shift+V Vim モードのオン・オフ
Ctrl+Shift+A アーカイブパネルの開閉

おわりに

「パワポを捨てたい」という動機から始まったツールですが、実装してみると CodeMirror のカスタマイズ、SVG のパース、DOM とのマッピングなど学びが多かったです。

普段のターミナル環境から離れずに図を作れるようになって、スライド制作の工程がだいぶスッキリしました。

同じ課題(Marp で図が作れない問題・パワポを捨てたい問題)を感じている方の参考になれば嬉しいです!

ソースコードはこちら👇
https://github.com/cocoaai-it/VSvgCode


この記事が役に立ったらいいね👍やシェアをお願いします!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?