16
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

EmacsAdvent Calendar 2017

Day 16

Emacs 内にペイントツールを実装する方法

Last updated at Posted at 2017-12-15

#はじめに

まずは、↓このツイートをご覧ください。

Emacs 内でマウス使ってお絵描き出来てます!
これは合成ではありません!
NTEmacs64 を使ってますが、別に公式ビルドでもいいですし、Linux 版 Emacs でも動いています。
ただ、macOS 版については手元に動作環境が無い為未確認です。
プラットフォーム依存コードは無い(!)ので、もし macOS で動いたら動作したよとコメントやリツイートしてくれると嬉しいです。

ちなみに、本気でペイントツールを作ろうとしてましたが、現在は完成度が10%未満の時点で放置されています…。
画像の保存や読み込みにも対応してますが、十分バグが取れてるとは思えないので、お試しの域を越えて使用する事はお勧め出来ません。

#動作環境

GUI 版 Emacs24.4 以上

端末エミュ上でしか動かない Emacs では(当然ですが…)動作しません。
ただ、GUI 版としてビルドされていれば、標準で XBM と PNM 形式の画像を扱えるので JPEG やら TIFF 等の追加の画像サポートは必要ありません。

#起動方法

MELPA にアップされていないので、以下からソース一式をクローン等してダウンロードしてください。

落としたディレクトリ内で以下のコマンドでバイトコンパイルします。
バイトコンパイルしないと、まともな速度で動きません。
若干のワーニングが表示されますが気にしないでください…。
emacs にパスを通してなければ絶対パスで指定してください。

$ emacs -Q --batch -L . -f batch-byte-compile *.el plug-ins/*.el

このまま load-path を通して起動している Emacs 内で (require 'epaint) (epaint) してもいいですが、自分の環境だと helm を常駐させていると epaint の描画中に激しくガベージコレクションが発生してとても重くなるので、以下のように新規の Emacs を起動して実行する事をお勧めします。

$ emacs -r -Q -L . -l epaint --eval "(epaint)"

-r は黒背景で起動するオプションですが、初期のキャンバスは白なので、白背景に白地だとキャンバスの大きさが分からないので指定します。

これで新規の Emacs が起動して白地のキャンバスが表示されているはずです。
キャンバス内でマウスの左ボタンを押しながらマウスを動かしてみてください。
黒い線が引かれましたか?
線が引かれた方はおめでとうございます!
そうでない方は…起動しなかったとコメントにエラーメッセージ等を添えて報告してみてください。出来る限り対応します。

#操作方法

キー 説明
B ベンチマーク(フィルレートの測定) 実行後 Messages バッファにキャンバスクリア(epaint-clear)とキャンバス全体を epaint-set-pixel で塗り潰し時間が表示されている
C キャンバスをクリア
E 消しゴムとペンをトグル
R 赤→緑→青→赤→…とペンの色変更
u アンドゥ
C-r リドゥ
f フリーハンド
c
e 楕円
r 矩形
l ライン
a 矢印
1 ペン先のサイズを 1 pixel にする
2 ペン先のサイズを 3 pixel にする
3 ペン先のサイズを 5 pixel にする
4 ペン先のサイズを 7 pixel にする
5 ペン先のサイズを 9 pixel にする
6 ペン先のサイズを 11 pixel にする
7 ペン先のサイズを 13 pixel にする
8 ペン先のサイズを 15 pixel にする
9 ペン先のサイズを 17 pixel にする
0 ペン先のサイズを 19 pixel にする
C-x C-s 保存
C-x C-w 書き出し (名前を付ける際に必ず .xbm または .ppm の拡張子を付けて保存してください)
C-x C-f XBM 形式(.xbm) または PNM(PPM P6) 形式(.ppm) のファイルのみ

#実装解説

##マウスに追従させる方法

実装していた当初は、非常に高い志しで作成していたので、ソースは無駄に複雑になっています…すみません。
肝の部分だけを分かり易く抜き出せば良かったですが、そこまでするモチベーションが無かったので…今あるソースのここの部分がこうという感じで解説して行きます。

今迄も GUI 版 Emacs なら elisp で画像を生成出来る事は良く知られていましたが(それでもフルカラーの画像まで生成しているのは見た記憶が無いですが)、マウスでラインを引いたりする elisp は見た事ありませんでした。

なので、どのようにマウスに追従させてるのかを先に解説します。
まずは以下がその該当メソッドです。(抜粋)

epaint.el
(defun epaint-canvas-down-mouse-1 (ev)
  (interactive "@e")
  (epaint-down-mouse-1 epaint-canvas ev))
epaint-canvas.el
(cl-defmethod epaint-down-mouse-1 ((this epaint-canvas-class) ev)
  (let* ((position (event-start ev))
         (x-y (posn-object-x-y position))
         (x0 (car x-y))
         (y0 (cdr x-y))
         (sx x0)
         (sy y0)
         x1 y1)
    (track-mouse
      (while (or (mouse-movement-p ev)
                 (member 'down (event-modifiers ev)))
        (setq position (event-start ev)
              x-y (posn-object-x-y position)
              x1 (car x-y)
              y1 (cdr x-y))

        (epaint-draw-funcall drawable gc
                             (epaint-history-current-data history)
                             sx sy x0 y0 x1 y1)
        (epaint-force-window-update this)

        (setq x0 x1
              y0 y1)
        (setq ev (read-event))))))

このコードを作成する前にまず参考にしたのは Emacs 上での元祖お絵描きモードの artist-mode です。
artist-mode はテキスト用に文字単位で絵を書くお絵描きモードですが、マウスの使い方は参考になります。

最終的に分かった事は、マウスイベントを取得する為に (interactive "@e") をして、連続でマウスイベントを取得する為に track-mouse が必要で、オブジェクト(ここではキャンバス)相対座標を取得する為に posn-object-x-y を使うという感じです。

以外とシンプルだなと思いましたか?
これで絵が描ければ苦労は無かったんですが…2つ問題が発生します。

  1. マウスがキャンバスを外れた時のクリッピングの問題

  2. track-mouse 中はキャンバスに描画した内容が反映されない (画面が更新されない)

1はマウスがキャンバスを外れた時に正しい座標を返さなくなる事に問題があります。
色々解決方を模索しましたがどうにも解決出来ず、現状はマウスがキャンバス外に行った際には前回の位置からのラインは引かないようにしてお茶を濁しています。
キャンバス内から外へマウスを速く動かすと線が途切れるので分かると思います。
これはショボい処理をしてるからではなく、Emacs の制限から来るものだという事をご理解ください…。

2は自分の理解不足なのか分からないですが、マウスを追い掛けるには track-mouse 内で while ループを回す必要がある為、ループ中は Emacs に処理が戻る事も無く当然画面も更新されるわけないわな、という状況です。
なので、どうにかして画面を更新する方法を模索する必要があるわけですが、そこで編み出したハックが epaint-force-window-update です。

epaint-canvas.el
(cl-defmethod epaint-force-window-update ((this epaint-canvas-class) &optional redisplay)
  (image-flush (image this))
  (force-window-update (get-buffer-window))
  (when redisplay
    (redisplay)))

要は image-flush した後に force-window-update します。
これで(半ば強引に)画面を更新出来ます。(ラインがマウスに追従して描画されます)

ちなみに、NTEmacs ではこれをするとメニューバーとツールバーが激しくチラつきます(笑)
Linux では問題無いですが、NTEmacs ではメニューバーとツールバーを非表示にしておいた方がいいでしょう。

以上が Emacs でペイントツールを実装する為の肝である、マウスに追従させる方法と同時に画面更新する方法についての解説でした。

##フルカラー画像を elisp で生成する方法

elisp で画像を生成する方法は Emacs で普通にサポートしている事なので特に目新しい事も無いですが、Emacs のドキュメントで触れているのは白黒の XBM 画像の生成だけです。
ただ、やっぱりフルカラー画像(PPM P6 形式)を生成したかったので、やってみたら出来てしまったので軽く触れておきます。

画像生成コードは諸事情により色々なソースに散在してしまっていて分かり難いですが、それぞれ抜粋してくると…

まずは、PPM P6 形式のヘッダーと make-string した文字列を concat して string-make-unibyte したものを保存しておきます。(ここでは epaint--bitmap)
ヘッダーの長さも保存しておきます。(ここでは epaint--offset)
make-string の大きさは、width × height × RGBの3byte 分です。255 は初期値です。(要するに白)

epaint-internal-format-pbm.el
(defun epaint-drawable-create (width height)
  (let ((drawable (make-epaint-drawable))
        (header (concat "P6\n"
                        (number-to-string width)
                        " "
                        (number-to-string height)
                        "\n"
                        "255\n")))
    (setf (epaint--bitmap drawable) (string-make-unibyte
                                     (concat header (make-string (* width height 3) 255)))
          (epaint--offset drawable) (length header)
          (epaint--width drawable) width
          (epaint--height drawable) height)
    drawable))

上記で作成した文字列を使って create-image します。

epaint-context.el
(defvar epaint-internal-format 'pbm)

(defun epaint-drawable-create-image (drawable)
  (create-image (epaint--bitmap drawable) epaint-internal-format t
                :width (epaint--width drawable)
                :height (epaint--height drawable)
                ;; text (or nil), arrow, hand, vdrag, hdrag, modeline, hourglass
                :pointer 'arrow
                :foreground "black"
                :background "white"))

create-image の戻り値を使って put-image します。

epaint-canvas.el
(cl-defmethod epaint-create-image ((this epaint-canvas-class) &key point)
  (setf (image this) (epaint-drawable-create-image (drawable this)))
  (epaint-put-image this :point point)
  (epaint-push-history this t))

(cl-defmethod epaint-put-image ((this epaint-canvas-class) &key point)
  (remove-images (point-min) (point-max))
  (setf (overlay this) (put-image (image this) (or point (point-min))))
  (image-flush (image this)))

ピクセルを打ち込むには、RGB の値を表わす3つの整数を前述の epaint--bitmap に保存した文字列に書き込みます。
offset はヘッダーの長さで、epaint--offset に保存しておいた値です。
color[r g b] と整数が3つ格納された vector ですが、これはアプリの都合上そうしてるだけです。

epaint-internal-format-pbm.el
(defsubst epaint-bitmap-set-pixel-linear (bitmap offset i color)
  (let* ((r (+ offset (* i 3)))
         (g (1+ r))
         (b (1+ g)))
    (aset bitmap r (aref color 0))
    (aset bitmap g (aref color 1))
    (aset bitmap b (aref color 2))))

これで、打ち込んだピクセルは前出の epaint-force-window-update がコールされた際に画面に反映されます。

#最後に

epaint 実装の動機ですが、artist-mode ってそういえばマウスの入力を見てるよなぁ…なら、画像でやれば絵が描けるじゃんという思い付きからでした。
意外とすんなり実装出来たんですが、その後フルカラー対応や 2D 描画アルゴリズムに手を出してこれにガッツリ時間を取られたり、度重なる内部構造のリファクタリング等を経て途中で燃え尽き絶賛放置中という状態です(笑)

とはいえ、またいつか再開したいとは思います。
勿論、コードのフォーク等大歓迎です!

この記事を読んで Emacs の更なる可能性を感じてもらえたら幸いです。

おわり

16
8
3

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
16
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?