🎆

NeovimのFloating Windowで花火を打ち上げる

2023/09/04に公開

NeovimにはFloating Windowという機能があります。
https://neovim.io/doc/user/api.html#api-floatwin

この機能で花火を打ち上げて遊んでみたので紹介します!

(Neovim: v0.9.1)

Floating Windowとは

Floating Windowはv0.4あたりで入った機能です。
通常のウィンドウ内ではなく、ポップアップのように重ねて配置できるウィンドウです。
なんでもできる。

できたもの

fireworks.vim

花火の点一つ一つがひとつのFloating Windowです。

:call Firework()

で画面におさまるランダムな位置に花火を打ち上げます。
ついでに

:call Fireworks(n)

でn回連続で花火を打ちげられるようにしました。

それではざっくりと流れを解説していきます!

Floating Windowで点を打つ

点(横2,縦1のFloating Window)を打つ(nvim_open_win)

半角2文字分、高さ1で大体正方形になるのでこれを1ドットとします。

" Floating Window用の空のバッファ
let empty_buf = nvim_create_buf(v:false, v:true)

" Floating Window生成
" Floating WindowのIDが返ってくるので背景色の設定や削除するときのために保持する
let window_id = nvim_open_win(empty_buf, v:true,
  \ {
  \   'row': 10, 'col': 20, 'width': 2, 'height': 1,
  \   'relative': 'editor', 'style': 'minimal',
  \ })

点の色(Floating Windowの背景色)を設定する(nvim_win_set_option)

" 適当な色を追加
highlight FireworkRed guibg=#FF0000

" window_idで指定したWindowの`winhighlight`を`FireworkRed`に設定
call nvim_win_set_option(window_id, 'winhighlight', 'Normal:FireworkRed')

指定時間で点を消す(timer_start, nvim_win_close)

指定時間後に点を消すことで、花火のアニメーションを実現します。

" window_idで指定したWindowを閉じる関数
function! s:remove_window(window_id, _timer_id) abort
  call nvim_win_close(a:window_id, v:true)
endfunction

" 3秒後(3000ms)に`s:remove_window`を実行. 引数に`window_id`を渡す
call timer_start(3000, function('s:remove_window', [window_id]))

花火を打ち上げる

上記の工程を1つの関数にまとめる

指定した座標に指定した色で点を表示し、指定時間後に点を消す関数です。

let s:current_window_id = win_getid()
let s:empty_buf = nvim_create_buf(v:false, v:true)
let s:winconf = { 'width': 2, 'height': 1, 'relative': 'editor' }

" x, y: 座標を指定
" color_name: 色名を文字列で渡す
" ms: 何ms後に消すか
function! s:FireworkFlash(x, y, color_name, ms) abort
  let l:firework_window_id = nvim_open_win(
        \ s:empty_buf, v:true,
        \ extend(s:winconf, { 'col': a:x, 'row': a:y })
        \ )
  call nvim_win_set_option(l:firework_window_id, 'winhighlight', a:color_name)
  call nvim_win_set_option(l:firework_window_id, 'winblend', 40)
  function! s:remove_window(firework_window_id, _timer_id) abort
    call nvim_win_close(a:firework_window_id, v:true)
  endfunction
  call nvim_set_current_win(s:current_window_id)
  call timer_start(a:ms, function('s:remove_window', [l:firework_window_id]))
endfunction

スプレッドシートで花火のドット絵を作る

花火の座標、色などのデータを用意する

highlight FireworkYellow guibg=#FFF100
highlight FireworkOrange guibg=#FFA500
highlight FireworkBlue guibg=#0000FF
highlight FireworkGreen guibg=#7CFC00
highlight FireworkRed guibg=#FF0000
let s:color_sets = [
    \   ['Normal:FireworkYellow', 'Normal:FireworkOrange'],
    \   ['Normal:FireworkGreen', 'Normal:FireworkBlue'],
    \   ['Normal:FireworkYellow', 'Normal:FireworkBlue'],
    \   ['Normal:FireworkGreen', 'Normal:FireworkOrange'],
    \   ['Normal:FireworkYellow', 'Normal:FireworkRed'],
    \   ['Normal:FireworkGreen', 'Normal:FireworkRed'],
    \ ]

" 上で作った花火の座標を、段階的に分ける
" color_index: 層の色として1つ目/2つ目どっちの色を使うか
" ms: 層の表示時間
" positions: 層のドットたちの、中心からの相対位置
let s:firework_matrix = [
    \   {
    \     'color_index': 1,
    \     'ms': 500,
    \     'positions': [
    \       { 'x': 0, 'y': -3 }, { 'x': 2, 'y': -2 }, { 'x': 3, 'y': 0 },
    \       { 'x': 2, 'y': 2 }, { 'x': 0, 'y': 3 }, { 'x': -2, 'y': 2 },
    \       { 'x': -3, 'y': 0 }, { 'x': -2, 'y': -2 },
    \     ],
    \   },
    \   {
    \     'color_index': 0,
    \     'ms': 300,
    \     'positions': [
    \       { 'x': 0, 'y': -6 }, { 'x': 2, 'y': -5 }, { 'x': 4, 'y': -4 },
    \       { 'x': 5, 'y': -2 }, { 'x': 6, 'y': 0 }, { 'x': 5, 'y': 2 },
    \       { 'x': 4, 'y': 4 }, { 'x': 2, 'y': 5 }, { 'x': 0, 'y': 6 },
    \       { 'x': -2, 'y': 5 }, { 'x': -4, 'y': 4 }, { 'x': -5, 'y': 2 },
    \       { 'x': -6, 'y': 0 }, { 'x': -5, 'y': -2 }, { 'x': -4, 'y': -4 },
    \       { 'x': -2, 'y': -5 },
    \     ],
    \   },
    \   {
    \     'color_index': 0,
    \     'ms': 200,
    \     'positions': [
    \       { 'x': 0, 'y': -7 }, { 'x': 5, 'y': -5 }, { 'x': 7, 'y': 0 },
    \       { 'x': 5, 'y': 5 }, { 'x': 0, 'y': 7 }, { 'x': -5, 'y': 5 },
    \       { 'x': -7, 'y': 0 }, { 'x': -5, 'y': -5 },
    \     ],
    \   },
    \   {
    \     'color_index': 1,
    \     'ms': 500,
    \     'positions': [
    \       { 'x': 0, 'y': -8 }, { 'x': 6, 'y': -6 }, { 'x': 8, 'y': 0 },
    \       { 'x': 6, 'y': 6 }, { 'x': 0, 'y': 8 }, { 'x': -6, 'y': 6 },
    \       { 'x': -8, 'y': 0 }, { 'x': -6, 'y': -6 },
    \     ],
    \   },
    \ ]

完成

let s:fw_vertical_radius = 8
let s:fw_horizontal_radius = 16
let s:margin = 8

function! Firework() abort
  " color_setsの中からランダムにセットを決定
  let l:color_set = s:color_sets[rand() % len(s:color_sets)]

  let l:max_y = nvim_get_option('lines')
  let l:max_x = nvim_get_option('columns')
  " 花火の中心を画面におさまる範囲でランダムに決定
  let l:center_y = rand() % (l:max_y - (s:fw_vertical_radius + s:margin) * 2) + s:fw_vertical_radius + s:margin
  let l:center_x = rand() % (l:max_x - (s:fw_horizontal_radius + s:margin) * 2) + s:fw_horizontal_radius + s:margin

  " 花火の中心に向かって打ち上げ
  for i in range(0, (l:max_y - l:center_y) / 2)
    let l:y = l:max_y - i * 2
    call s:FireworkFlash(l:center_x, l:y, l:color_set[0], 300)
    sleep 50m
  endfor
  sleep 100m


  " 花火の中心から層を段階的に表示していく
  for i in range(0, len(s:firework_matrix) - 1)
    let l:firework_circle = s:firework_matrix[i]
    for j in range(0, len(l:firework_circle['positions']) - 1)
      let l:pos = l:firework_circle['positions'][j]
      call s:FireworkFlash(
        \   l:center_x + l:pos['x'] * 2,
        \   l:center_y + l:pos['y'],
        \   l:color_set[l:firework_circle['color_index']],
        \   l:firework_circle['ms'],
        \ )
    endfor

    sleep 100m
  endfor
endfunction

おまけ

ループ

for i in range(0, n - 1)
  " n回実行したい処理
endfor

花火をn回連続で打ち上げる

function! Fireworks(n) abort
  for i in range(0, a:n - 1)
    call Firework()
  endfor
endfunction

スリープ

100msのスリープ

sleep 100ms

ランダム整数

0~(n-1)のランダムな整数を生成

echo rand() % n

変数スコープ

他にもあるけど今回使ったもの

let g:hoge = "hoge" " global-variable グローバル変数
let s:hoge = "hoge" " script-variable スクリプトに閉じた変数
let l:hoge = "hoge" " local-variable 関数に閉じた変数

function Hoge(hoge)
  echo a:hoge " 引数(アクセスするときに`a:`をつける)
endfunction

参考

さいごに

Vim scriptを設定以外で普段書かないので、やりたいことをやるために色々詰まることがありました。
変数/関数の書き方、呼び出し方、ループ処理、スリープ処理、ランダム、遅延処理などなど。
「Vim script勉強しよー」ではなく、やりたいこと(遊びだろうと業務だろうと)のためにコードを書いていると活きた知見になりますね。

皆さんもNeovimのFloating Windowでぜひ遊んでみてください!


株式会社クロスビットでは、デスクレスワーカーのためのHR管理プラットフォームを開発しています。
一緒に開発を行ってくれる各ポジションのエンジニアを募集中です。

https://x-bit.co.jp/recruit/
https://herp.careers/v1/xbit
https://note.com/xbit_recruit

クロスビットテックブログ

Discussion