ベジェ曲線の算出過程をPlotlyのスライダーを使用してインタラクティブに描画させてみる


## 概要
ベジェ曲線の算出過程をPlotlyのスライダーを使用してインタラクティブに描画させてみる．

### 環境
- Windows 10
- Python 3.6.3 (Anaconda)
- NumPy 1.14.2
- Plotly 3.4.1

## 参考
- Plotly User Guide for Python | https://plot.ly/python/user-guide/
- Python Slider Controls | plotly | https://plot.ly/python/sliders/
- ベジェ曲線 - Wikipedia | https://ja.wikipedia.org/wiki/ベジェ曲線


## 出力結果
### 3次
以下で解説のコードで描画したもの．
![BezierCurveAnimation.gif](https://qiita-image-store.s3.amazonaws.com/0/43136/cf90fc43-dcb6-c2bd-7186-b45bfd25e2b7.gif)
### 5次
入力点数を増やすと面白い．
![BezierCurveAnimation5.gif](https://qiita-image-store.s3.amazonaws.com/0/43136/75c9a226-3bc8-d994-e473-79725801a62d.gif)


## コードと解説
### 1. ライブラリのインポート
```python
import numpy as np
import plotly.offline as py  # オフライン描画用Plotlyライブラリ
import plotly.graph_objs as go  # グラフオブジェクト（辞書データ）作成用Plotlyライブラリ

py.init_notebook_mode()  # jupyter notebook 上でplotlyを使用する場合
```

### 2. ベジェ曲線を算出するための関数を定義
```python
def bezier(p_in, t, sep=False):
    """ベジェ曲線上の点および算出途中で生成された点群を出力
    Wikipedia ベジェ曲線作図法に倣う
    Parameters
    ----------
    p_in: list like object
        入力点群（点数で次数を決定）
    t: float
        媒介変数
    sep: bool
        各回での算出結果にNoneを含ませる (default False)

    Returns
    -------
    list
        出力点群
    """
    p_out = []
    for i in range(len(p_in) - 1):
        p_out.append((1 - t) * p_in[i] + t * p_in[i + 1])

    if len(p_in) > 2:
        p = bezier(p_out, t, sep)  # 再帰
        if sep:
            p_out += [None]  # Plotlyで区切って描画させるため
        p_out += p

    return p_out  # リストの最後がベジェ曲線上の点
```

### 3. 入力点群データの作成
```python
ps_x = np.random.rand(4)
# array([0.36265296, 0.00793013, 0.76807413, 0.64266143])

ps_y = np.random.rand(4)
# array([0.08902042, 0.47218923, 0.36629385, 0.84850309])

# ベジェ曲線上の点の算出テスト
bezier(ps_x, 0.5, True)
# [0.18529154541040233,
#  0.38800212828554853,
#  0.705367778887567,
#  None,
#  0.28664683684797543,
#  0.5466849535865578,
#  None,
#  0.4166658952172666]  # ベジェ曲線上の点
```

### 4. Plotlyグラフ作成
- 大まかな流れ
    - data（traceの集合体）の作成
        - ステップごとの点群を描画するためのtraceを作成（21個）
        - ベジェ曲線描画のためのtraceを作成
        - 入力点群描画のためのtraceを作成
    - レイアウトの作成
        - スライダー設定を作成
            - ステップごとの描画設定を作成
    - figure作成
    - 描画

```python
# traceを格納するための空のリストを準備
data = []

# ステップごとに，描画するためのtraceデータを作成し，dataリストに格納する
t_step = np.linspace(0, 1, 21)  # ステップ分の媒介変数tのリストを作成（0.05ピッチ）．
for t in t_step:
    # あるtでのベジェ曲線上の点および算出途中で生成された点群を取得
    p_x = bezier(ps_x, t, sep=True)
    p_y = bezier(ps_y, t, sep=True)

    # ベジェ曲線上の点および算出途中で生成された点群を描画するためのtraceを作成
    trace = go.Scatter(
        visible=False,  # さしあたり全てのフレームを非表示設定とする
        mode='lines+markers',  # ラインプロット + 点プロット
        name='t = %.2f' % t,
        x=p_x,
        y=p_y,
        line=dict(dash='dot'))
    data.append(trace)  # traceをdataリストに格納

# 初回表示するステップの設定
step0 = int(len(t_step) / 2)  # 初回表示ステップ番号を定義
data[step0]['visible'] = True  # 初回表示ステップを表示設定する

# 滑らかな曲線を描画するため，ベジェ曲線上の点リストを算出（0.01ピッチ）．
curve_x = [bezier(ps_x, t)[-1] for t in np.linspace(0, 1, 101)]
curve_y = [bezier(ps_y, t)[-1] for t in np.linspace(0, 1, 101)]

# ベジェ曲線を描画するためのtraceを作成
trace_line = go.Scatter(
    visible=True,
    x=curve_x,
    y=curve_y,
    mode='lines',  # ラインプロット
    name='Bézier curve')
data.append(trace_line)  # traceをdataリストに格納

# 入力点群データを描画するためのtraceを作成
trace_point = go.Scatter(
    visible=True,
    x=ps_x,
    y=ps_y,
    mode='lines+markers+text',  # ラインプロット + 点プロット + テキストプロット
    name='input',
    line=dict(dash='dash'),
    text=['P%d' % i for i in range(len(ps_x))],
    textposition='middle right')
data.append(trace_point)  # traceをdataリストに格納

# 各ステップの描画方法を設定した設定リストを作成
steps = []
for i in range(len(t_step)):
    step = dict(
        method='restyle',
        args=['visible', [False] * len(data)])  # さしあたり全てのフレームを非表示設定とする
    step['args'][1][i] = True   # iステップを表示設定とする
    step['args'][1][-2] = True  # ベジェ曲線は常に描画
    step['args'][1][-1] = True  # 入力データ点は常に描画
    steps.append(step)

# スライダー設定辞書データを作成
sliders = [
    dict(
        active=step0,  # 初期表示ステップの指定
        currentvalue={'prefix': 't: '},  # 現在値表示エリアに表示する文字列の定義
        pad={'t': 20},  # スライダー描画位置の設定（padding-top: 20）
        steps=steps)]  # ステップ設定リストを指定

# レイアウト設定
layout = go.Layout(
    sliders=sliders,  # 作成したスライダー設定辞書データをレイアウトに織り込み
    xaxis=dict(range=[0, 1]),  # x軸表示範囲の設定
    yaxis=dict(range=[0, 1]))   # y軸表示範囲の設定

# fig, plot
fig = go.Figure(data=data, layout=layout)
py.iplot(fig)
```