Vimでお絵描きプログラミング

この記事はVim Advent Calender 2012,第23日目の記事です.前日は@y_uukiさんのPerl屋さんに便利なVim Pluginを2つ書いた - ゆううきブログでした.


pietというプログラミング言語があります.pietは普通の言語とはちょっと変わっていて画像を使ってプログラミングをおこないます.例えば"Hello World"を出力するプログラムの1つは次のようになります(DM's Esoteric Programming Languages - Piet Samplesより)

http://www.dangermouse.net/esoteric/piet/Piet_hello_big.png

さて,このpietのプログラムを作成するにはペイントとかいうエディタを使ったりするらしいですがプログラムを書くのはvimの本分ですからね.vimで書けない訳ないですよね.ということで作りました.

GitHub - mmisono/piet.vim: vim plugin for piet

実行例:

pietについて

pietについては公式wikipedia(日本語)を参考にしてください.

pietのプログラムを普通に実行したいときにはnpietがおすすめです.npietはppm,png,gifに対応しています.


pietは簡単にいってしまうといわゆるスタックマシンです.どの命令を実行するのかということを色の明度(lightness)と色相(hue)の違いによって決定します.同じ色でつながっている領域のセルの数がpushしたときにスタックに積まれる値となります.本来ならば1ピクセルがそれぞれ意味を持ちますが,1ピクセルだとプログラミングをするときに不便なので,実際は10x10などを一まとまりとして扱います.これをpietではcodelと呼んでいます.


次に移動する場所を決定するDPとCCの動きが少し分かりにくいと思うので以下に少し説明します.DPが取りうる値は"right","down","light","up"の4つ,CCが取り売る値は"left","right"の2つです.初期状態ではDP="right",CC="left",初期位置は左上です.プログラムが実行されると,現在の位置からまずDPの方向に最も遠いcodelを探します.もし,DPの方向に最も遠いcodelが複数あるならば,自分がDPの方向に向かってるとして,その向きからCCの方向に最も遠いcodelが選択されます,そして,その選択したcodelからDP側にあるcodelが次に移動する位置となり,そのcodelとの色の違いで実行する命令が決定されます.もしも移動先のcodelが黒or端ならば,まずCCがトグルされ,再び次の移動先を探します.もしもまた移動先のcodelが黒or端ならば,今度はDPを時計回りに変えます.このように,移動先が黒or端ならばCC->DP->CC->DP...のようにCCやDPの値を変えて次の移動先を探します.もし8回試しても見つからないようならばどこにも移動することがないということなのでプログラムは終了します.


仮に移動先codelが白だった場合は,そのままDPの方向に白以外のcodelにぶつかるまで進みます.この動作は通常の移動先を決定する動作とは違いCCは考慮されません.もし白以外のcodelが見つかればそこに移動します(何も命令は実行しません).もしこの時黒のcodelや端にぶつかってしまったときはDPを時計周りに変化させかつCCをトグルします.そして再びDPの方向に直進し,移動先を探します.これを移動先が見つかるまでおこないます.もしも移動先が見つからずループするようならばプログラムの実行は終了します.ただし,npietのデフォルトの動作は白から黒や端に遷移したときはぶつかる手前の位置に移動し,CCとDPを変化させるようになっています.npietでこの動作をするよう帰るには"-v11"オプションを指定しますpiet.vimではデフォルトでここで説明した動作です.もしnpietのデフォルトと
同じ動作をさせるためには"-stay"オプションをつけます.

piet.vimについて

piet.vimPPM(P3)にのみ対応しています.
以下のコマンドがあります.

・ReadFromPPM
PPM(P3)のファイルからpiet.vimが扱える形式に変換して読み込みます.ちなみにpngやgifからPPM(P3)の画像に変換するにはimage magicを使って以下のようにできます.

 $ convert -compress none foo.png foo.ppm

・PietEdit
pietの編集を開始します.ReadFromPPMを実行するとこのコマンドは自動的に実行されます.piet.vimでは2文字が1codelになります.このコマンドを実行すると以下のマッピングが定義されます.

ノーマルモード
・ H : カーソル下を現在の色に変更し左へ移動
・ J : カーソル下を現在の色に変更し下へ移動
・ K : カーソル下を現在の色に変更し右へ移動
・ L : カーソル下を現在の色に変更し右へ移動
・ U : 現在の色をpushに対応する色へ変更
・ OO : 現在の色をpopに対応する色へ変更
・ + : 現在の色をaddに対応する色へ変更
・ - : 現在の色をsubtractに対応する色へ変更
・ * : 現在の色をmultiplyに対応する色へ変更
・ / : 現在の色をdivideに対応する色へ変更
・ % : 現在の色をmodに対応する色へ変更
・ N : 現在の色をnotに対応する色へ変更
・ > : 現在の色をgreaterに対応する色へ変更
・ P : 現在の色をpointerに対応する色へ変更
・ S : 現在の色をswitchに対応する色へ変更
・ D : 現在の色をduplicateに対応する色へ変更
・ R : 現在の色をrollに対応する色へ変更
・ IN : 現在の色をin(number)に対応する色へ変更
・ IC : 現在の色をin(char)に対応する色へ変更
・ ON : 現在の色をout(number)に対応する色へ変更
・ OC : 現在の色をout(char)に対応する色へ変更
・ w : 現在の色を白へ変更
・ k : 現在の色を黒へ変更
・ <C-c>r : 現在の色を赤へ変更
・ <C-c>y : 現在の色を黄へ変更
・ <C-c>g : 現在の色を緑へ変更
・ <C-c>c : 現在の色をシアンへ変更
・ <C-c>b : 現在の色を青へ変更
・ <C-c>m : 現在の色をマゼンダへ変更
・c : 現在の色をカーソルしたの色に変更
また,<C-c><C-b> とすると明るい青,<C-c>Bとすると暗い青へ現在の色を変更します.他も同様です.
対応する色に変更というのは,例えば現在の色が赤のとき'-'を押すと現在の色が暗い黄色になります(色相を1ステップ,明るさを1つ暗くするのがsubtractなので)

インサートモード
・r : 赤
・y : 黄
・g : 緑
・c : シアン
・b : 青
・m : マゼンダ
また,ノーマルモードで色を変更するのと同様に<C-b>で明るい青,Bで暗い青を入力します.他も同様です.
まぁ,通常であればノーマルモードでの編集で十分だと思います.


このモードになるとエコーラインに以下の表示が出ます.

ここで左側が現在の色,右側がカーソル下の色です.また,移動した時にカーソル下の色が変化した場合にはその変化に対応するコマンドが右端に出ます.

・PietEditOff
pietの編集を終了します.実態はバッファローカルなマッピングやautocmdを消すだけ..


・SaveAsPPM
現在のファイルをPPMとしてエクスポートします.保存する場合のデフォルトのcodelサイズは1x1です.引数で指定することもできます.


・PietRun
現在編集しているpietのファイルを実行します.領域の面積を計算するのに再帰を使っていますが,大きいプログラムだと再帰が深すぎてエラーがでることがあります.
とりあえず

 :setl mfd=500

とかすれば一応対処できますがmfdの値を変えても無理なようならnpietを使ってください..





以下適当な補足など
・pietの存在を知った瞬間,直感的にvimならいける,と思い勢いそのままに作ってみましたがあんまり操作性よくないかも(あれ..
vim advent calender 2012 13日目の記事を書かれた@ne_sachirouさんがその記事の少し前でpietについて書かれていたのでちょっとびっくりしました.時代はpietなんですかね
・ターミナルでも色が対応してればいけます
インタプリタは碌にテストしてません.バグあるかもしれません
・quickrunがあるならば以下のようにしてnpietで実行できます(やっつけ)

if !exists('g:quickrun_config')
    let g:quickrun_config = {}
endif
let g:quickrun_config.piet = 
    \ { 
    \   'command' : 'npiet',
    \   'cmdopt' : '-v11',
    \   'exec' : ['%c %o %S:p:r.ppm'],
    \   'tmpfile' : '%{tempname()}.ppm',
    \   'runner' : 'system',
    \   'hook/sweep/files' : '%S:p:r.ppm',
    \   'hook/saveasppm' : ''
    \ }
let s:hook = {}
let s:hook.kind = "hook"
let s:hook.name = "saveasppm"
function! s:hook.on_hook_loaded(session,context)
    exe "SaveAsPPM ".expand("%:p:r").".ppm"
endfunction
call quickrun#module#register(s:hook)

動作はこちらの方が確実です
・ビジュアルモードちゃんと対応してないですorz (うまくいかない..
・というかソースひどいorz


次はもっと実用的なもの作るようがんばります..


明日は@kozo2さんです.