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-rectangle
やsvg-circle
、svg-path
1など)を適用していくことで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オブジェクトに要素が追加されるたびに描画がリフレッシュされる(ウレシイ)。
挿入した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-pp
やdom-by-id
、dom-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で傾けさせるタイマー関数を使うことで、SVGをEmacs上でグルグルさせることができる。
グルグルさせてみた 🙂
(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*")))
タイマー関数内ではsmile-svg
に要素を追加しているわけではないので、再描画するためにsvg-possibly-update-image
を明示的に呼び出す必要がある。
タイマー関数がクロージャになることで、増えていく角度を保持するのにグローバル変数を使わずに済んでいる。ウレシイ! 3🙂
おしまい
svg.elを使ってEmacs上でSVGをあれやこれやしてみましょう 🙃
実用的な例→ GNU ELPA - svg-clock