つーさにブログ

つうさにのメモ用ブログ

EmacsでSVGを描く、そして回す

回すとタノシイ!

環境: Emacs 27.1

はじめに

Emacsにはsvg.elというSVG (Scalable Vector Graphics) を作成するためのライブラリが標準で搭載されている。svg.elを使うことでEmacs Lispを使ってSVGインタラクティブに作成、描画することができる。

今回はそんなsvg.elのキホンと応用、そして最後にEmacs上でSVGをぐるぐる回すというお話 🙃

svg.elのバージョン

Emacs 27.1以降に搭載されているsvg.elを前提とする。また、Emacs 26.3以下の場合はGNU ELPA上のsvg-1.1.elを前提とする。

svg.elのキホン

svg-createしてできたSVGオブジェクトに対して、要素を追加する関数(svg-rectanglesvg-circlesvg-path1など)を適用していくことでSVG画像を作成することができる。

(require 'svg)

(setq smile-svg (svg-create 400 400))
(save-excursion (goto-char (point-max)) (svg-insert-image smile-svg))
(svg-circle smile-svg
            200 200 100
            :fill-color "orange")
(svg-ellipse smile-svg
             165 175 13 18
             :fill-color "maroon"
             :stroke-color "maroon")
(svg-ellipse smile-svg
             235 175 13 18
             :fill-color "maroon"
             :stroke-color "maroon")
(svg-path smile-svg
          '((moveto ((150 . 230)))
            (curveto ((180 250 220 250 250 230))))
          :stroke-linecap  "round"
          :stroke-width 10
          :fill-color "transparent"
          :stroke-color "maroon")

svg-insert-imageを使うことで、作成したSVGをバッファ上に表示することができる。また、単に表示されるだけでなく、svg-insert-imageで挿入されたSVG画像はSVGオブジェクトに要素が追加されるたびに描画がリフレッシュされる(ウレシイ)。

f:id:tsuu_mmj:20200914080153g:plain
`svg-insert-image`でSVGを表示する例

挿入したSVG画像上でoキーを押せば、画像をSVG形式のファイルに保存できる。また、SVGオブジェクトに対してsvg-printを使えば、SVG形式の文字列を見ることができる(横に長い)。

(svg-print smile-svg)
<svg width="400" height="400" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <circle cx="200" cy="200" r="100" fill="orange"></circle> <ellipse cx="165" cy="175" rx="13" ry="18" fill="maroon" stroke="maroon"></ellipse> <ellipse cx="235" cy="175" rx="13" ry="18" fill="maroon" stroke="maroon"></ellipse> <path d="M 150 230 C 180 250 220 250 250 230" stroke-linecap="round" fill="transparent" stroke="maroon" stroke-width="10"></path></svg>

svg.elの応用

svg-node関数

SVGではgタグを使って要素のグループ化をすることができる。svg.elにはg要素を追加する専用の関数は存在しないが、そういう場合はsvg-nodeを使うことで任意の要素を追加できる。

上の例では全ての描画要素を大元のsvg要素に子要素として追加していた。 今度は大元のsvg要素にはg要素を一つだけ追加し、そのg要素にcircleなどを子要素として追加していく。

(setq smile-svg
      (let* ((svg (svg-create 400 400))
             (g (svg-node svg 'g :id "smile")))
        (svg-circle
         g
         200 200 100
         :fill-color "orange")
        (svg-ellipse
         g
         165 175 13 18
         :fill-color "maroon"
         :stroke-color "maroon")
        (svg-ellipse
         g
         235 175 13 18
         :fill-color "maroon"
         :stroke-color "maroon")
        (svg-path
         g
         '((moveto ((150 . 230)))
           (curveto ((180 250 220 250 250 230))))
         :stroke-linecap  "round"
         :stroke-width 10
         :fill-color "transparent"
         :stroke-color "maroon")
        svg))

dom.elの関数

svg.elのSVGオブジェクトは、同じくEmacs同梱のdom.elライブラリのdomオブジェクトである。よって、dom-ppdom-by-iddom-set-attributeといったdom.elの関数をSVGオブジェクトに対して使うことができる。

dom-ppはdomオブジェクトを綺麗に表示 (pretty-print) してくれる。ご覧のようにdomオブジェクト(やSVGオブジェクト)の実態はリストである。

(dom-pp smile-svg)
(svg ((width . 400)
      (height . 400)
      (version . "1.1")
      (xmlns . "http://www.w3.org/2000/svg")
      (xmlns:xlink . "http://www.w3.org/1999/xlink"))
 (g ((id . "smile"))
  (circle ((cx . 200)
           (cy . 200)
           (r . 100)
           (fill . "orange")))
  (ellipse ((cx . 165)
            (cy . 175)
            (rx . 13)
            (ry . 18)
            (fill . "maroon")
            (stroke . "maroon")))
  (ellipse ((cx . 235)
            (cy . 175)
            (rx . 13)
            (ry . 18)
            (fill . "maroon")
            (stroke . "maroon")))
  (path ((d . "M 150 230 C 180 250 220 250 250 230")
         (stroke-linecap . "round")
         (fill . "transparent")
         (stroke . "maroon")
         (stroke-width . 10)))))

dom-by-idは指定したidを持つ要素を返す。

(dom-by-id smile-svg "smile")
((g ((id . "smile")) (circle ((cx . 200) (cy . 200) (r . 100) (fill . "orange"))) (ellipse ((cx . 165) (cy . 175) (rx . 13) (ry . 18) (fill . "maroon") (stroke . "maroon"))) (ellipse ((cx . 235) (cy . 175) (rx . 13) (ry . 18) (fill . "maroon") (stroke . "maroon"))) (path ((d . "M 150 230 C 180 250 220 250 250 230") (stroke-linecap . "round") (fill . "transparent") (stroke . "maroon") (stroke-width . 10)))))

dom-set-attributeは名前の通り、指定した要素に対して属性をセットする。すでに同じ名前の属性がセットされていた場合は上書きする。

(dom-set-attribute
 (dom-by-id smile-svg "smile")
 'transform
 "rotate(180 200 200)")

回す

svg.elのキホンと応用を見てきた。最後に、SVGをグルグルさせて終わりにする 🙃

SVGでは描画要素のtransform属性にさまざまな変換関数をセットすることができる2。なかでも rotate(angle [x y]) という変換関数は描画要素を (x, y) を中心にangle度回転させる。ということで、実行するごとにrotateで傾けさせるタイマー関数を使うことで、SVGEmacs上でグルグルさせることができる。

グルグルさせてみた 🙂

(require 'svg)

(defvar smile-svg
  (let* ((svg (svg-create 400 400))
         (g   (svg-node svg 'g :id "smile")))
    (svg-circle
     g
     200 200 100
     :fill-color "orange")
    (svg-ellipse
     g
     165 175 13 18
     :fill-color "maroon"
     :stroke-color "maroon")
    (svg-ellipse
     g
     235 175 13 18
     :fill-color "maroon"
     :stroke-color "maroon")
    (svg-path
     g
     '((moveto ((150 . 230)))
       (curveto ((180 250 220 250 250 230))))
     :stroke-linecap  "round"
     :stroke-width 10
     :fill-color "transparent"
     :stroke-color "maroon")
    svg))

(defvar smile-timer nil)

(defun show-rotating-smile ()
  (interactive)
  (when (get-buffer "*smile*")
    (user-error "Smile already rotating"))
  (with-current-buffer (get-buffer-create "*smile*")
    (view-mode 0)
    (erase-buffer)
    (svg-insert-image smile-svg)
    (setq cursor-type nil)
    (view-mode 1)
    (switch-to-buffer (current-buffer)))
  (setq smile-timer
        (run-at-time t 0.05
                     (let ((x 0))
                       (lambda ()
                         (setq x (mod (+ x 20) 360))
                         (dom-set-attribute
                          (dom-by-id smile-svg "smile")
                          'transform
                          (format "rotate(%d 200 200)" x))
                         (let ((inhibit-read-only t))
                           (svg-possibly-update-image smile-svg)))))))

(defun kill-rotating-smile ()
  (interactive)
  (unless (get-buffer "*smile*")
    (user-error "Smile already killed"))
  (cancel-timer smile-timer)
  (setq smile-timer nil)
  (kill-buffer (get-buffer "*smile*")))

f:id:tsuu_mmj:20200914095047g:plain
グルグル笑顔

タイマー関数内ではsmile-svgに要素を追加しているわけではないので、再描画するためにsvg-possibly-update-imageを明示的に呼び出す必要がある。

タイマー関数がクロージャになることで、増えていく角度を保持するのにグローバル変数を使わずに済んでいる。ウレシイ! 3🙂

おしまい

svg.elを使ってEmacs上でSVGをあれやこれやしてみましょう 🙃

実用的な例→ GNU ELPA - svg-clock


  1. svg-path関数は他の関数と違い、引数に組み込みコマンドを要求する。組み込みコマンド一覧はGNU Emacs Lisp Reference Manualに書かれている。

  2. 参考: transform - SVG: Scalable Vector Graphics | MDN

  3. Emacs 27より*scratch* バッファでlexical-binding変数の値がデフォルトでtになった。(ただしM-x eval-bufferlexical-bindingの値を考慮しないので注意)