Help us understand the problem. What is going on with this article?

AnimatedAppComponentクラスを使ってアニメーションさせてみた

More than 1 year has passed since last update.

はじめに

去年に続いて今年も JUCE Advent Callender に参加しました。kawaです。
今回は "AnimationAppComponent" クラスを使ってアニメーション描画に挑戦してみたいと思います。
中身の内容で間違っている場所や、おかしなところがあるかもしれないですが、その時はコメント欄にて教えていただけると嬉しいです。
よろしくお願いします。

テスト環境 JUCE v5.4.1
Windows 10 64bit

※ この記事はopenGLを使わずに描画しています。より本格的なアニメーションをする場合はCPUの負荷が大きくなる場合があります。内容に合わせてFPSを下げたり、OpenGLAppComponentを使った方が良いかもしれません。

AnimationAppComponentについて

Juce_2018_projucer.png

Projucerで Animated Application を選択し、プロジェクトを作成すると AnimationAppComponent クラスを継承した MainComponentクラスが作られます。このクラスを使ってアニメーションを描画していきます。

AnimationAppComponentクラスで追加された関数は AnimatedAppComponent Class Referenceによると以下の4つのようでした。

void          setFramesPerSecond (int framesPerSecond); // 更新間隔を設定する(FPS)
virtual void  update () =0; // 純粋仮想関数
int           getFrameCounter () const noexcept; // コンポーネントの実行が開始されてからupdate()が実行された回数を取得する
int           getMillisecondsSinceLastUpdate () const noexcept; // 前回update()呼び出してからの時間をミリ秒単位で取得する

処理の流れとして、
設定したFPSのタイミングで update()が実行され、その後に描画を更新する。
という流れになっているようです。( setFramePerSecond(int) 関数でFPSを設定しないと親クラスのTimerがスタートせずアニメーションがスタートしないようです。)

今回は、MainComponentクラスのコンストラクタ で setFramePerSecond(int) でFPSを60に設定し
update() 関数の中で getMillisecondsSinceLastUpdate() 関数を使ってアニメーションに使う時刻、変数を計算して paint()関数で使っていきたいと思います。

アニメーション時刻を計算する。

MainComponentクラスに アニメーション時刻として使う変数を追加して、update() 関数の中で計算します。"0.0 ~ 1.0" の範囲に調節することで、計算がとてもしやすくなります。

// MainComponentクラス
// アニメーション時刻用の変数をクラスに追加
double m_animationTime; // コンストラクタで、0.0の値で初期化しておく
void MainComponent::update()
{
    //=========================================================================
    int deltaTimeMs = this->getMillisecondsSinceLastUpdate(); // ミリ秒単位で取得
    this->m_animationTime += (deltaTimeMs / 1000.0);// 1000.で割って秒単位にしたものを足していく。
    //=========================================================================
    if( this->m_animationTime > 1.0 ) // 1.0を超えた瞬間 == 1秒経過 
        this->m_animationTime = 0.0;  // 0.0にしてリセットする。
    //=========================================================================
}

Juce_2018_anim01.gif

paint関数の source code ( クリックで折りたたみを開く )
void MainComponent::paint (Graphics& g)
{
    const auto bounds = this->getLocalBounds(); // 描画エリア
    //=========================================================================
    if ( g.clipRegionIntersects( bounds ) == false )
        return;
    //=========================================================================

    g.fillAll ( juce::Colours::white );               // わかりやすいように背景を白で塗りつぶし。
    const auto mousePos = this->getMouseXYRelative(); // マウスの座標
    //=========================================================================
    // update()で計算したアニメーション時刻を使って サイン波形を生成。
    const float animateSin = (float)sin(juce::MathConstants<float>::twoPi * this->m_animationTime);
    //=========================================================================
    // 生成したサイン波形を使って直径を計算。マイナスの値にならない様にする。
    float circleWidth = 1 + 200 * fabs(animateSin);
    // 円を描画 ( マウスの位置が円の中心になるように半分だけずらす )
    g.fillEllipse( mousePos.getX() - circleWidth / 2  
                 , mousePos.getY() - circleWidth / 2 
                 , circleWidth,circleWidth);
    //=========================================================================

    //=========================================================================
    // テスト用にアニメーション時刻とマウス座標を画面左上に描画
    //=========================================================================
    g.setColour( juce::Colours::black);
    const float fontSize = 28.f;
    g.setFont( fontSize);
    //=========================================================================
    g.drawText( juce::String::formatted( "Animation Time %0.4f" , this->m_animationTime)
              , bounds, juce::Justification::topLeft);
    //=========================================================================
    g.drawText( juce::String::formatted( "Mouse Position %d:%d" , mousePos.getX(),mousePos.getY())
              , bounds.translated(0,fontSize), juce::Justification::topLeft);
    //=========================================================================
    if (this->isMouseButtonDown() == true)
    {  g.drawText( "Mouse Button Down"
                 , bounds.translated(0,fontSize*2), juce::Justification::topLeft);
    }
    //=========================================================================
}


実際にビルドして試してみたところ、アニメーションするスピードが速すぎるようです。このままだと目がチカチカしてくるのでアニメーション時刻がゆっくり進むように調整したいと思います。(※ 計算したアニメーション時刻の値がわかりやすいように左上に表示してみました。)

動きが早いので遅くしてみる

void MainComponent::update()
{
    //=========================================================================
    int deltaTimeMs = this->getMillisecondsSinceLastUpdate();
    // 秒単位にしたものを 5で割ったものを足していき、5秒で1.0になるようにする。
    this->m_animationTime += (deltaTimeMs / 1000.0) / 5.0; 
    //=========================================================================
    if( this->m_animationTime > 1.0 ) // 1.0を超えた瞬間 == 5秒経過
        this->m_animationTime = 0.0;  // リセット //下のgif画像はこのタイミングで描画確認用の時刻を保存
    //=========================================================================
}

Juce_2018_anim_07.gif

paint関数の source code ( クリックで折りたたみを開く )
void MainComponent::paint (Graphics& g)
{
    // ... 省略
    //=========================================================================
    // update()で計算したアニメーション時刻を使って サイン波形を生成。
    float animateSin = (float)sin(juce::MathConstants<float>::twoPi * this->m_animationTime);
    //=========================================================================
    // 生成したサイン波形を使って色をアニメーションする。
    juce::Colour animateColor ( fabs(animateSin) // hue ( 0.0 ~ 1.0) 
                              , 0.8f             // saturation
                              , 0.8f             // bright
                              , 1.0f );          // alpha
    // アニメーションさせたカラーをセットする
    g.setColour(animateColor);
    //=========================================================================
    float circleWidth = 1 + 200 * fabs(animateSin);
    g.fillEllipse( mousePos.getX() - circleWidth / 2
                 , mousePos.getY() - circleWidth / 2
                 , circleWidth,circleWidth);
    //=========================================================================
    // テスト用 テキスト .. 省略
}

アニメーション速度が遅くなりました。今回は一つのアニメーション時刻しか使っていないのですが同時に複数のアニメーション時刻を使いたい場合はそれぞれ変数を準備してアニメーション時刻を計算した方がよさそうです。
(※ gif画像は5秒を確認するためにm_animationTimeが1.0を超えた瞬間の時刻を表示しています。)

サイン波アニメーションさせてみた

準備が整ったので、いくつかアニメーションに挑戦してみたいと思います。
計算したアニメーション時刻の変数を使ってMainComponentクラスのpaint関数内で描画していきます。
まずは、サイン波形を使ったアニメーションを作ってみました。

Juce_2018_anim_03.gif

paint関数の source code ( クリックで折りたたみを開く )
void MainComponent::paint (Graphics& g)
{
    const auto bounds = this->getLocalBounds(); 
    //=========================================================================
    if ( g.clipRegionIntersects( bounds ) == false )
        return;
    //=========================================================================

    g.fillAll ( juce::Colours::white );
    const auto mousePos = this->getMouseXYRelative();
    //=========================================================================
    const int w = bounds.getWidth();
    const int h = bounds.getHeight();
    //=========================================================================
    const float twoPi = juce::MathConstants<float>::twoPi;
    const float animateSin = (float)sin( twoPi * this->m_animationTime);
    //=========================================================================
    juce::Colour animateColor ( fabs(animateSin) // hue ( 0.0 ~ 1.0)
                              , 0.8f             // saturation
                              , 0.8f             // bright
                              , 1.0f );          // alpha
    g.setColour(animateColor);
    //=========================================================================
    const int circleNum = 30;
    const int radiusMod = 8;
    const int sinWidth  = (h/4)+20;
    //=========================================================================
    for (int i = 0; i < circleNum; i++)
    {
        float phase   = ( twoPi / circleNum ) * i;
        float animSin = (float)sin( twoPi * (this->m_animationTime + phase) );
        // float animSin = (float)sin( twoPi * (this->m_animationTime ) + phase );
        float x = (w / circleNum) *i;
        float y = (sinWidth * animSin) + h/2;
        //=====================================================================
        float circleWidth = 1 + radiusMod * fabs(animSin);
        g.fillEllipse( x,y , circleWidth,circleWidth);
        //=====================================================================
    }
    //=========================================================================
    // テスト用テキスト描画省略 .. 省略
}

背景色をアニメーションさせてみた

今度は背景の色をアニメーションさせてみました。色相(hue)にアニメーション時刻、変数を掛けてアニメーションしています。だんだんと、斜めに動いているような気がしてきます。

Juce_2018_anim_06.gif

paint関数の source code ( クリックで折りたたみを開く )
void MainComponent::paint (Graphics& g)
{
    const auto bounds = this->getLocalBounds(); 
    //=========================================================================
    if ( g.clipRegionIntersects( bounds ) == false )
        return;
    //=========================================================================

    g.fillAll ( juce::Colours::white );
    const auto mousePos = this->getMouseXYRelative();
    //=========================================================================
    const int w = bounds.getWidth();
    const int h = bounds.getHeight();
    //=========================================================================
    const int w_Num = 4;// 描画する四角形の数、横
    const int h_Num = 4;// 描画する四角形の数、縦
    //=========================================================================
    const int box_w = w / w_Num;
    const int box_h = h / h_Num;
    //=========================================================================
    for (int row = 0; row < w_Num; row++)
    {
        for (int column = 0;column < w_Num;column++)
        {
            float phase    = (float)(row + column) / (float)(w_Num + h_Num);
            int animateHue = ( 360 * this->m_animationTime ) + (120 * phase) ;
            //=================================================================
            juce::Colour col( (float)( (animateHue % 360 )/360.0 ) // hue 
                            , 0.8f                                 // saturation
                            , 1.0f                                 // bright
                            , 1.0f);                               // alpha
            g.setColour(col);
            //=================================================================
            int x = box_w * column;
            int y = box_h * row;
            g.fillRect(x, y, box_w +1, box_h +1); // !
        }
    }
    //=========================================================================
    // テスト用テキスト描画省略 .. 省略
}

文字をアニメーションさせてみた

最後は文字を切り取って表示するタイピングアニメーション。
juce::Stringクラスのsubstring関数で文字を切り取って描画しています。
今回はやりませんでしたがフォントサイズをアニメーションさせるのも面白そうです。

Juce_2018_anim_05.gif

paint関数の source code ( クリックで折りたたみを開く )
void MainComponent::paint (Graphics& g)
{
    // サイン波形の描画 .. 省略    
    //=========================================================================
    const juce::String text = "Hello JUCE";
    //=========================================================================
    g.setFont(48.f);
    g.setColour(juce::Colours::black);
    //=========================================================================
    // substring 関数を使って文字を切り取る。
    int len = jmin(text.length(),(int)( text.length() * this->m_animationTime * 2 ) );
    g.drawText( text.substring( 0 , len ) 
              , bounds
              , juce::Justification::centred);
    //=========================================================================
    // テスト用テキスト描画 .. 省略
}

おわりに

長くなってしまいました。記事について見落としてる部分もあるかもしれないです。間違っているところやおかしなところがあればコメント欄にて教えていただけると嬉しいです。ソースコードを載せると縦に長くなるのでどうしようかな?と悩んでいたところ、Qiitaで折りたたみができることを知りました。これはもう、迷わずに使ってみました。見た目がすっきり。それでは、良い12月、年末、JUCEプログラミングをお過ごしください。

Link

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away