2021-07-24 ,

月の満ち欠け画像をアジェンダに表示する

以前月の出、月の入り、月齢をorg-modeのアジェンダに表示する仕組みを作った(moonrise-el)のですが、その時に月の満ち欠けを画像で表示できたらなぁと思っていたのでした。

というのも Emacs で時の流れを感じる - Qiita という記事でモードラインに月の満ち欠けを表示しているのを見ていいなーと思っていたからです。

その記事で紹介しているコードではU+1F311からU+1F318にある絵文字を使用して月の満ち欠けを表現しているのですが、私の使っているEmacsではフォントの設定が十分ではなく絵文字が表示できないのでその方法は使えませんでした(Windowsではカラー絵文字が使えないというのもあります)。

最近all-the-iconsを導入したのですが、その中に入っていた Weather Icons というフォントに月の満ち欠けのグリフが入っているのを見つけました。

Weather Icons内のグリフ
図1: Weather Icons内のグリフ

これを使うと次のようなコードで月の満ち欠けを表現できます。

(insert (all-the-icons-wicon "moon-14")) ;;0から27まで指定可能

これは使えると思い次のコードを作成しました。

(require 'all-the-icons)
(setq moonrise-moon-age-format
      (lambda (age)
        (concat (format "(%.2f) " age)
                 (all-the-icons-wicon
                  (format "moon-%d"
                          (mod (round (* (/ age 29.53) 28)) 28))
                  :v-adjust -0.1))))

本当は太陽と月の黄経差から求めた方が良いのですがmoonrise-elにはその機能が無いのでとりあえず月齢を使って計算しています。(2021-07-25追記:moonrise-elに月相を計算する機能を追加しました。現在は何も設定しなくても画像が出ます)

早速実行してみたのですが、org-modeのアジェンダでは正しいグリフが表示されません。calendarからL m(calendar-moonrise-moonset)やL d(calendar-moonrise-moonset-month)したときは正しく表示されます。

org-agendaで正しく表示されないグリフ
図2: org-agendaで正しく表示されないグリフ
calendarで正しく表示されるグリフ
図3: calendarで正しく表示されるグリフ

調べてみたところorg-agenda.el内でテキストプロパティfaceを'org-agenda-calendar-sexpに設定してしまうのが原因でした。all-the-iconsはテキストプロパティのfont-lock-faceにフォントファミリーを指定することで狙ったフォントでグリフを表示させるの仕組みなので、faceが指定されると違うフォントが選択されて正しいグリフで表示されなくなってしまいます。

困りました。このためだけにorg-agenda.elに手を入れるのは気が進みません。

何か良い方法は無いものか。describe-charでテキストプロパティの状態を眺めているとall-the-iconsが出力したfont-lock-faceプロパティ自体はバッファに反映されている点に気がつきました(faceプロパティが追加されたのでそちらが優先されているわけです)。つまり上の設定のlambda関数の中で生成したテキストプロパティ自体は正しくアジェンダバッファに反映されているわけです。テキストプロパティが使えるのなら画像を表示するのは簡単です。displayプロパティを使えば良いのです。

まず月の満ち欠けを表す画像を用意します。たまたまPCに入っていたBlenderでサクッと作ってみました。球と平行光源とカメラを置いて光源の向きをグルッと回すアニメーションを設定してレンダリングするだけです。

Blenderで画像を作る
図4: Blenderで画像を作る
レンダリングされた画像
図5: レンダリングされた画像

次に変数moonrise-moon-age-formatにはその画像を使うdisplayプロパティ付きの文字列を生成する関数を指定します。

(setq moonrise-moon-age-format
      (lambda (age)
        (concat (format "(%.2f) " age)
                (propertize
                 "A" 'display
                 (create-image
                  (format "~/tmp/moonphases/moonphase-%02d.png"
                          (min 30 (round (* (/ age 29.53) 30))))
                  nil nil
                  :ascent 'center)))))

すると狙い通りagendaバッファ上に月の満ち欠けが画像で表示されました。

org-agendaで正しく表示された月画像
図6: org-agendaで正しく表示された月画像

無事目的は達成できたのですが画像ファイルを管理するのが面倒です。よく考えてみたらSVGが使えるのですからEmacs内で生成してしまえば良さそうなものです。球の光が当たる境目は三角関数で簡単に求められるでしょう。

(require 'svg)
(defun moonrise-create-moon-svg (age size)
  (let* ((svg (svg-create size size))
         ;; 円の中心と半径
         (cx (* 0.5 size))
         (cy (* 0.5 size))
         (radius (* 0.48 size))
         ;; 円の分割数
         (ndiv 32)
         (2pi/ndiv (* 2.0 (/ pi ndiv)))
         ;; 月齢をラジアンへ
         (age-rad (* 2.0 pi (/ (min (max age 0.0) 29.53) 29.53)))
         ;; 光が当たる部分の右側と左側の位置
         (right-edge (if (<= age-rad pi) 1.0 (- (cos age-rad))))
         (left-edge (if (>= age-rad pi) 1.0 (- (cos age-rad)))))
    ;; 光が当たっていない月の全球
    (svg-circle svg cx cy radius :fill "#000")
    ;; 光が当たっている部分
    (svg-polygon
     svg
     (cl-loop for i from 0 to ndiv
              collect (let ((i-rad (* i 2pi/ndiv))
                            (edge (if (< (* 2 i) ndiv) right-edge left-edge)))
                        (cons (+ cx (* (sin i-rad) radius edge))
                              (- cy (* (cos i-rad) radius)))))
     :fill "#ffc")
    (svg-image svg :ascent 'center)))

試しに画像をバッファに挿入してみると正しく表示されます。

(cl-loop for i from 0 to 28 do
         (insert-image (moonrise-create-moon-svg i 16)))
EmacsのSVGでレンダリングされた月画像
図7: EmacsのSVGでレンダリングされた月画像

これを使う設定は次のようになります。

(setq moonrise-moon-age-format
      (lambda (age)
        (concat (format "(%.2f) " age)
                (propertize
                 "A" 'display
                 (moonrise-create-moon-svg age 16)))))

ちゃんと狙い通り表示されました。

org-agendaで正しく表示された月SVG画像
図8: org-agendaで正しく表示された月SVG画像

Emacsでは、設定に文字列をフォーマットする関数を指定することが良くあると思います。そういった場所では今回と同様の手法でSVG画像を使える可能性があります(返した文字列のテキストプロパティがそのままバッファにinsertされていることが必要です)。他の用途でも応用が利くかもしれないなと思いましたので紹介してみました。

#モードラインなんて全体をSVGで書いちゃえば良いんじゃないの?(テキストまわりの制御が難しいか?)

Pingback / Trackback