Search code examples
c++winapidirect2d

The animation will stop before the transition is almost done because of the elapsed time of the animation


I will explain the problem very briefly: In order to get a color animation, the lerp algorithm must be used to change the colors smoothly, which requires the progress of the animation (std::min)((deltaTime / duration), 1.0f) But there is a problem, that by using this animation technique, it runs during the animation duration, but the progress does not reach 1, so that I can kill the timer to avoid additional cpu consumption.

Note that I am using the Direct2D API and the Win32 API for handling my window class.

Before I show you my code, I'll show you how I update my window which you will see in the following codes:

void Window::CreateTimer(bool update, std::uint16_t frameRate, std::uint64_t timerId) 
{ 
    SetTimer(m_Hwnd, timerId, 1000 / frameRate, nullptr); 
    if (update) 
    { 
        HandleTimerEvent([&](const Event& event)
            {
                InvalidateRect(m_hWnd, nullptr, false); 
            });
    }
}

If the update flag was true, we want to handle WM_TIMER to invalidate the window based on the frame rate, which in our case is 60Hz.

Here's the ColorTransition implementaion:

class Transition
{
protected:
    Transition(Window* window, float duration)
        : m_Window(window), m_Duration(duration)
    {
        srand(static_cast<std::uint32_t>(time(nullptr)));
        QueryPerformanceFrequency(&m_Frequency);
        QueryPerformanceCounter(&m_LastFrameTime);
        SetTimerId();
        SetFrameRate();
        m_Window->CreateTimer(true, m_FrameRate, m_TimerId);
    }

public:
    virtual void Update() = 0;

public:
    void SetLastFrameTime()
    {
        m_LastFrameTime = m_CurrentTime;
    }

    void SetTimerId()
    {
        m_TimerId = static_cast<std::uint32_t>(rand());
    }

    void SetFrameRate(std::uint32_t frameRate = 60)
    {
        m_FrameRate = frameRate;
        KillTimer(m_Window->GetWindow(), m_TimerId);
        m_Window->CreateTimer(true, m_FrameRate, m_TimerId);
    }

protected:
    float m_Duration;
    Window* m_Window;
    LARGE_INTEGER m_Frequency, m_LastFrameTime, m_CurrentTime;
    std::uint32_t m_FrameRate, m_TimerId;
    bool m_IsDone;
};

class ColorTransition : public Transition
{
public:
    ColorTransition(Window* window, float duration, D2D1::ColorF start, D2D1::ColorF end)
        : Transition(window, duration), m_Start(start), m_End(end), m_Color(start)
    {
    }

    D2D1::ColorF GetColor() const
    {
        return D2D1::ColorF(
                        m_Color.r / 255.0f,
                        m_Color.g / 255.0f,
                        m_Color.b / 255.0f,
                        m_Color.a / 255.0f);
    }

    void Update() override
    {
        QueryPerformanceCounter(&m_CurrentTime);

        float deltaTime = static_cast<float>(m_CurrentTime.QuadPart - m_LastFrameTime.QuadPart) /
            static_cast<float>(m_Frequency.QuadPart);
        float progress = (std::min)((deltaTime / m_Duration), 1.0f);
        
        m_Color.r = Lerp(m_Color.r, m_End.r, progress);
        m_Color.g = Lerp(m_Color.g, m_End.g, progress);
        m_Color.b = Lerp(m_Color.b, m_End.b, progress);
        m_Color.a = Lerp(m_Color.a, m_End.a, progress);

        if (progress >= 1.0f)
        {
            KillTimer(m_Window->GetWindow(), m_TimerId);
            m_IsDone = true;
        }
    }

    float Lerp(float start, float end, float progress)
    {
        return start + (end - start) * progress;
    }

private:
    D2D1::ColorF m_Start, m_End, m_Color;
};

main.cpp which includes ColorTransition.h:

ColorTransition transition(&window, 1.0f, D2D1::ColorF(0, 55, 255), D2D1::ColorF(255, 0, 0));

window.HandlePaintEvent[&](const Event& event)
    {
        ID2D1HwndRenderTarget* renderTarget = event.GetRenderTarget();
        ID2D1SolidColorBrush* brush = nullptr;
        renderTarget->CreateSolidColorBrush(
            transition.GetColor(),
            &brush
        );

        transition.Update();
        renderTarget->FillRectangle(
            D2D1::RectF(10, 10, 110, 110),
            brush
        );
        transition.SetLastFrameTime();
    });

I guess the code is pretty simple to understand, so I won't explain the code And I guess I've provided enough code for you to understand what my problem really is.

I can use a variable called m_ElapsedTime for example to store the elapsed time since the last frame and check if it is equal to the transition animation which works but stops before the transition is almost complete.

Here's the Update method with the technique I said:

void Update() override
{
    QueryPerformanceCounter(&m_CurrentTime);

    float deltaTime = static_cast<float>(m_CurrentTime.QuadPart - m_LastFrameTime.QuadPart) /
        static_cast<float>(m_Frequency.QuadPart);
    m_Elapsed += deltaTime;
    float progress = (std::min)((deltaTime / m_Duration), 1.0f);

    m_Current.r = Lerp(m_Current.r, m_End.r, progress);
    m_Current.g = Lerp(m_Current.g, m_End.g, progress);
    m_Current.b = Lerp(m_Current.b, m_End.b, progress);
    m_Current.a = Lerp(m_Current.a, m_End.a, progress);

    if (m_Elapsed >= m_Duration)
    {
        Stop();
    }
}

This works but as I said it stops before the transition is almost complete.

I have not been able to find any solution for it. There aren't many details about this kind of animation in my case because I've been researching for a while to find a solution, so I really had to ask a question here. If you know a better algorithm or a solution for it, please help me And please explain why my code doesn't work with the things I provided.

Thank y'all for help.


Solution

  • Actually, the problem I had, had nothing to do with delta time or elpsed time, the problem was that as the first parameter of the lerp function I was passing m_Color, which made the animation end much earlier, and instead of elpsed time I had to use delta time, which is a wrong method. I should have passed m_Start instead of the first parameter of the lerp method.

    Here's the working version of the Update method:

    void Update() override
    {
        QueryPerformanceCounter(&m_CurrentTime);
    
        float deltaTime = static_cast<float>(m_CurrentTime.QuadPart - m_LastFrameTime.QuadPart) /
            static_cast<float>(m_Frequency.QuadPart);
        m_ElapsedTime += deltaTime;
        float progress = (std::min)((m_ElapsedTime / m_Duration), 1.0f);
        
        m_Color.r = Lerp(m_Start.r, m_End.r, progress);
        m_Color.g = Lerp(m_Start.g, m_End.g, progress);
        m_Color.b = Lerp(m_Start.b, m_End.b, progress);
        m_Color.a = Lerp(m_Start.a, m_End.a, progress);
    
        if (progress >= 1.0f)
        {
            KillTimer(m_Window->GetWindow(), m_TimerId);
            m_IsDone = true;
        }
    }
    

    So after all the problem was really simple but an annoying one too.