人はスーパーサイヤ人に憧れるものです、しかし現実ではなれる人は限られています、日夜修行していつかスーパーサイヤ人になれる日を夢見るのもいいですが、ここでは変わりにVimにスーパーサイヤエディタになってもらいます。
そんなプラグインを作りました。
kuririn-no-kotoka.vim
wordijp/kuririn-no-kotoka.vim(GitHub)
インストール方法や使い方はREADME.mdに記載しています。
※動作確認はWindowsのみで行っているため、Linunx/Macでの確認ができていません
デモ
このプラグインはこちらの動画(Youtube)を見ながら作成しています。
あなたがサイヤ人ならクリリンを殺された怒りでVimがスーパーサイヤエディタへと覚醒します
デモでは音が出ませんが、シュインシュインの効果音も出ています。
苦労した点
スーパーサイヤ人らしさ
特徴といえば「逆立った髪」「金色のエフェクト」「シュインシュイン」だと思います。
このうち、「逆立った髪」はエディタで何が逆立つのか分からなかったので却下、「金色のエフェクト」はカラースキームをリアルタイムに変更しようとしたら編集に支障が出るレベルで遅いので却下。
最終的に、金色っぽいカラースキームに変更後、「シュインシュイン」だけ発生させてスーパーサイヤエディタっぽくするようにしました、見辛くてもいけないのでカラースキームも「golden.vim」という名前が金色なもので妥協しました。
技術的な話
以下は今回のプラグインを作成した際の技術的な話です、読み飛ばしてもらっても構いません。
利用したVim8の新機能
exepath
あなたがサイヤ人か、はたまたへたれ王子なのかの判定に利用しています。
それぞれの専用ディレクトリへパスを通したあと、ユーザー名コマンドを作成し、exepathで取得したフルパスに「saiyajin」か「prince」が含まれているかで判定しています。
kuririn-no-kotoka.vim
`-- autoload
|-- kuririn_no_kotoka.vim # princeとsaiyajinへパスを通す
|-- prince
`-- saiyajin
`-- wordi # 作成されたユーザー名コマンド
タイマー(Timer)
タイマーはアニメーション処理で利用しています、Vimがスーパーサイヤエディタになるには、いくつかの過程を経る必要があるため、それらをアニメーションで処理しています。
アニメーション処理用の関数はこちらです
" 一定間隔で指定回数funcを読んだあと、最後にnext_funcを呼ぶ
function! s:regist_animate(interval, max_count, func, next_func) abort
" クロージャ動作をさせるバインド
let l:Func = a:func
let l:Next_func = a:next_func
let l:max_count = a:max_count
let l:interval = a:interval
let l:count = 1
" ループコールバック
function! s:regist_animate_internal_loop(local, timer) abort
call a:local.Func() " max_count回数呼ばれる
if a:local.max_count == 0
" 無限ループ
return
endif
let a:local.count += 1
if a:local.count > a:local.max_count
call timer_stop(a:timer)
call s:regist_animate_internal_next(a:local.Next_func)
endif
endfunction
call timer_start(a:interval, function('s:regist_animate_internal_loop', [l:]), {'repeat': -1})
" 終了時コールバック
function! s:regist_animate_internal_next(next_func) abort
let l:Next_func = a:next_func
function! s:regist_animate_internal_next_internal(local, timer) abort
call a:local.Next_func() " max_count回数後に呼ばれる
endfunction
" NOTE : 非同期でa:local.Next_funcを呼び出し、ループコールバック関数の使用中を解除する
call timer_start(0, function('s:regist_animate_internal_next_internal', [l:]), {'repeat': 1})
endfunction
endfunction
この関数を利用して、過程を経てスーパーサイヤエディタになります。
過程は、Stateパターン(のようなもの)で実装しています。
※解説用にシンプルにしています。
" Phase 1
function! s:phase_1() abort
let l:foo = 'local'
function! s:phase_1_loop(local) abort
" ここにアニメーション処理を書く
echo a:local.foo
endfunction
function! s:phase_1_done() abort
" 次のPhaseへ
call s:phase_2()
endfunction
" 50ms間隔でloop関数を40回実行後、done関数を呼び出す
call s:regist_animate(50, 40, function('s:phase_1_loop', [l:]), function('s:phase_1_done'))
endfunction
" ----------------------------------
" Phase 2
function! s:phase_2() abort
function! s:phase_2_loop() abort
" ここにアニメーション処理を書く
endfunction
function! s:phase_2_loop2() abort
" ここにアニメーション処理を書く
endfunction
let l:remain = 0
function! s:phase_2_done(local) abort
let a:local.remain -= 1
if a:local.remain <= 0
" no-op : 最終Phaseなので何もしない
endif
endfunction
" アニメーションを合成する
call s:regist_animate(50, 40, function('s:phase_2_loop'), function('s:phase_2_done', [l:]))
let l:remain += 1
" 異なるFPSでもOK
call s:regist_animate(100, 20, function('s:phase_2_loop2'), function('s:phase_2_done', [l:]))
let l:remain += 1
endfunction
アニメーションは合成が出来ます、今回のプラグインではセリフと震えを合成しています、アニメーションをそれぞれの専用関数に実装して合成すると、FPSの違いを考慮しながら実装する手間がなくなり楽に実装できました。
作ってみた感想
ウインドウの振動を表現するために乱数が欲しいなと思いました!
https://github.com/vim-jp/issues/issues/983
おわりに
悟空が「クリリンのことかー!」と叫ぶのはスーパーサイヤ人になったあとなんですが、勢いがあるので気にしないことにしました。