Tkinterでメダカっぽい動きを表現する

# これは何？

![fish.gif](https://qiita-image-store.s3.amazonaws.com/0/175214/765618e8-11a5-6257-9205-1560fe7162b8.gif)

こんなやつです。なんとなくメダカっぽい動きをします。

# ソースコードと簡単な説明

```Python
import math
import random
import time
import numpy as np

# ベクトル正規化
def norm( a):
    x = np.linalg.norm(a)
    if x == 0.0:
        x = 1
    return  a / x

#
# 魚
#
class Fish(object):

    VISCOSITY = 2.2 # 粘性係数[1/sec]
    RUSH_ACL = 15.0 # 突進加速度[m/sec^2]
    RUSH_TIME= 2    # 突進加速する時間間隔[sec]
    MAX_POS = 1     # 空間の広さ[m]

    fishes = []     # 魚群

    def __init__(self,id=-1,pos_x=0,pos_y=0):
        self.id = id
        self.pt_vel = np.array([0.0,0.0])
        self.pt_pos = np.array([pos_x,pos_y])
        self.rush_time = 0 # 次回突進までの残り時間

    # 更新
    def update(self, delta_t,mode='MODE_RAND'):

        # 一定時間がたったら突進加速
        self.rush_time -= delta_t
        if self.rush_time < 0:
            acl,tim = 1,1 # 先頭魚用の係数

            # 各自ランダムに泳ぐ
            if mode == 'MODE_RAND':
                pt_dir = norm( np.array([random.uniform(-1,1),random.uniform(-1,1)]))
            # 先頭魚を追う
            elif mode == 'MODE_HEAD':
                if self.id == 0: # 先頭魚
                    pt_dir = norm( np.array([random.uniform(-1,1),random.uniform(-1,1)]))
                    acl,tim = 2,0.1 # 10倍の頻度で２倍の加速
                else:            # その他の魚
                    head = Fish.fishes[0]
                    pt_dir = norm( head.pt_pos - self.pt_pos)

            pt_acl = pt_dir * acl * Fish.RUSH_ACL * random.uniform(0.8,1.2)
            self.rush_time = tim * Fish.RUSH_TIME * random.uniform(0.5,1.2)
        else:
            pt_acl = self.pt_vel * -Fish.VISCOSITY

        # 速度、位置の計算
        self.pt_vel += pt_acl * delta_t
        self.pt_pos += self.pt_vel * delta_t

        self.limitWall()

        return (self.pt_pos,self.pt_vel)

    # 壁（境界）処理
    def limitWall(self):
        bLim = False
        if   self.pt_pos[0] >= Fish.MAX_POS:self.pt_pos[0] = Fish.MAX_POS; bLim = True
        elif self.pt_pos[0] < 0:            self.pt_pos[0] = 0;            bLim = True
        elif self.pt_pos[1] >= Fish.MAX_POS:self.pt_pos[1] = Fish.MAX_POS; bLim = True
        elif self.pt_pos[1] < 0:            self.pt_pos[1] = 0;            bLim = True
        if bLim: self.pt_vel *= -1 # 反転


#
# main
#
if __name__ == '__main__':

    import tkinter as tk

    SZ = 500 # 画面サイズ

    def toRect(vec,size=5):
        x = vec[0] * SZ; y = vec[1] * SZ
        return (x-size,y-size,x+size,y+size)

    def toColor(c1,c2,i,N):
        c = [ int(c1[k]+i/N*(c2[k]-c1[k])) for k in range(3)]
        return '#{:02X}{:02X}{:02X}'.format(*c)

    def getId_pos(idx):
        return 'pos{}'.format(idx+1)

    def getId_vel(idx):
        return 'vel{}'.format(idx+1)

    N_FISH = 15
    DELTA_T = 0.033

    cv = tk.Canvas(width=SZ,height=SZ,bg='white')
    cv.pack()

    # 魚群の生成
    Fish.fishes = []
    for idx in range(N_FISH):
        fill = toColor([255,0,0],[255,255,0],idx,N_FISH)
        cv.create_oval( 0,0,0,0, tags=getId_pos(idx), fill=fill,outline=fill) # 本体
        cv.create_oval( 0,0,0,0, tags=getId_vel(idx), fill=fill,outline=fill) # しっぽ

        fish = Fish(idx,random.uniform(0,1),random.uniform(0,1))
        Fish.fishes.append( fish)

    cv.create_text(10, 10, anchor=tk.NW,text = '', font = ('FixedSys', 20),tags='tMes')

    # イベントループ
    cur_mode = 'MODE_HEAD'
    mode_time = 0
    while True:
        # 動作モードの切り替え
        mode_time -= DELTA_T
        if mode_time < 0:
            if cur_mode == 'MODE_RAND':
                cur_mode = 'MODE_HEAD'
                mode_time = 15
            elif cur_mode == 'MODE_HEAD':
                cur_mode = 'MODE_RAND'
                mode_time = 30

            cv.itemconfigure('tMes', text=cur_mode)

        # 魚が泳ぐ
        for idx,fish in enumerate(Fish.fishes):
            pt_pos,pt_vel = fish.update(DELTA_T,cur_mode)
            # 本体
            cv.coords(getId_pos(idx), *toRect(pt_pos,10))
            # しっぽ
            pt_vel = norm(pt_vel) * (-15/SZ) + pt_pos
            cv.coords(getId_vel(idx), *toRect(pt_vel,5))

        cv.update_idletasks()
        cv.update()

        time.sleep(DELTA_T)

    tk.mainloop()
```
画面全体にメダカをランダム位置にばらまきます。
メダカの見た目は大小の円だけでシンプルに表現しています（手抜き）
メダカは約２秒間隔でランダム方向に加速します。
メダカに、進行方向とは逆に速度に比例した粘性加速度をかけます。
３０秒に１回、１５秒間だけ、先頭のメダカが狂ったように泳ぎます。
このとき、他のメダカは先頭メダカを追います。

# 感想など

比較的簡単なコードですが、それっぽい動きができました。
パラメータを変えると動きがいろいろ変化します。