From 99e6e7bd8256cfdbb0d0c3b0ab28098ddb8a038a Mon Sep 17 00:00:00 2001 From: Gaspar Capello Date: Mon, 8 Apr 2024 19:01:27 -0300 Subject: [PATCH] Fix nested tag with "Ping-Pong" repeat mode causes to skip the first frame of the parent tag (fix #4271) --- src/doc/playback.cpp | 116 ++++++++- src/doc/playback_tests.cpp | 495 +++++++++++++++++++++++++++++++++++-- 2 files changed, 582 insertions(+), 29 deletions(-) diff --git a/src/doc/playback.cpp b/src/doc/playback.cpp index 2eaa9b287..8e50616b8 100644 --- a/src/doc/playback.cpp +++ b/src/doc/playback.cpp @@ -1,5 +1,5 @@ // Aseprite Document Library -// Copyright (C) 2021-2022 Igara Studio S.A. +// Copyright (C) 2021-2024 Igara Studio S.A. // // This file is released under the terms of the MIT license. // Read LICENSE.txt for more information. @@ -185,8 +185,19 @@ void Playback::handleEnterFrame(const frame_t frameDelta, const bool firstTime) } else { addTag(t, false, forward); - if (!firstTime) + if (!firstTime) { goToFirstTagFrame(t); + // Handle cases where inner tags will jump to different + // frames several times recursively (e.g. one reverse + // inside other reverse). + // + // Consideration for tests: + // Playback.OnePingPongInsidePingPongReverse + // Playback.OneReverseInsidePingPongReverse + // Playback.OnePingPongReverseInsideReverse + if (frame != m_frame) + handleEnterFrame(frameDelta, false); + } } } } @@ -457,10 +468,11 @@ bool Playback::decrementRepeat(const frame_t frameDelta) // New frame outside the tag frame_t newFrame; - if (rewind) { + if (rewind && !m_playing.empty()) { newFrame = firstTagFrame(m_playing.back()->tag); } else { + // Note that 'tag' means 'the last tag removed from m_playing' newFrame = (frameDelta * forward < 0 ? tag->fromFrame()-1: tag->toFrame()+1); } @@ -473,10 +485,100 @@ bool Playback::decrementRepeat(const frame_t frameDelta) stop(); return false; } - if (newFrame < 0) - newFrame = m_sprite->lastFrame(); - else if (newFrame > m_sprite->lastFrame()) - newFrame = 0; + if (newFrame < 0) { + // m_playing.empty() should never happen, because the only + // way to have "newFrame < 0" is if we have a tag on + // m_playing in REVERSE or PING_PONG_REVERSE which frame 0 + // is contained into that tag. + ASSERT(!m_playing.empty()); + if (m_playing.empty()) { + newFrame = m_sprite->lastFrame(); + } + else { + // Special cases arise with PING_PONG_REVERSE aniDir and when + // the begining of the tag range matches with the first frame of + // the sprite. + // Consideration for tests inside: + // Playback.OnePingPongInsideOther + // A A + // >-------< >-------< + // B B + // <---> >---< + // 0 1 2 3 4 0 1 2 3 4 + PlayTag* parentPlaying = m_playing.back().get(); + // When parentPlaying is PING_PONG_REVERSE + // the next frame will be defined according: + // 1. The playloop has more repetitions to decrement + // --> go to the next frame of the 'tag' + // 2. The playloop has no more repetitions to decrement + // --> Start all the playloop again. + if (parentPlaying->repeat > 1) { + if (parentPlaying->tag->aniDir() == AniDir::PING_PONG_REVERSE) + parentPlaying->invertForward(); + --parentPlaying->repeat; + newFrame = tag->toFrame() + 1; + } + else + continue; + } + } + else if (newFrame > m_sprite->lastFrame()) { + // If all the tags were played and + // the 'tag' range == timeline range and + // the 'tag' is PING_PONG_REVERSE --> + // The playloop has to start on the last frame of + // the timeline, or the first frame of the most nested + // tag (on reverse direction). + // Consideration for tests: + // Playback.WithTagRepetitions + // Playback.OnePingPongInsideOther + // Playback.OnePingPongInsideOther14, 15, 18 and 19 + // A <-- last tag removed from 'm_playing', i.e. 'tag' + // >-----< + // B <-- most nested tag + // ***-*** + // 0 1 2 3 + if (m_playing.empty() && + tag->aniDir() == AniDir::PING_PONG_REVERSE && + tag->fromFrame() == 0 && + tag->toFrame() == m_sprite->lastFrame()) { + m_frame = m_sprite->lastFrame(); + handleEnterFrame(frameDelta, false); + if (m_playing.size() > 1) { + m_playing.back()->invertForward(); + goToFirstTagFrame(m_playing.back()->tag); + } + return false; + } + + // 'tag' is contained by other tag and the last frame of each tag + // matches in the last frame of the sprite + if (!m_playing.empty() && + tag->toFrame() == m_playing.back()->tag->toFrame()) { + PlayTag* parentPlaying = m_playing.back().get(); + // The parentPlaying has no more repetitions to decrement + // --> continue to remove the 'parentTag' + if (parentPlaying->repeat <= 1) + continue; + // Consideration for test: + // Playback.OnePingPongInsideOther + if (parentPlaying->tag->aniDir() == AniDir::PING_PONG || + parentPlaying->tag->aniDir() == AniDir::PING_PONG_REVERSE) { + parentPlaying->invertForward(); + newFrame = tag->fromFrame() - 1; + } + // Consideration for test: + // Playback.OnePingPongInsideForward2 + else if (parentPlaying->tag->aniDir() == AniDir::FORWARD) { + --parentPlaying->repeat; + newFrame = parentPlaying->tag->fromFrame(); + } + else + newFrame = 0; + } + else + newFrame = 0; + } } m_frame = newFrame; diff --git a/src/doc/playback_tests.cpp b/src/doc/playback_tests.cpp index 957097483..dba3cb8b2 100644 --- a/src/doc/playback_tests.cpp +++ b/src/doc/playback_tests.cpp @@ -1,5 +1,5 @@ // Aseprite Document Library -// Copyright (c) 2021-2022 Igara Studio S.A. +// Copyright (c) 2021-2024 Igara Studio S.A. // // This file is released under the terms of the MIT license. // Read LICENSE.txt for more information. @@ -152,6 +152,47 @@ TEST(Playback, WithTagRepetitions) play = Playback(sprite.get(), 0, Playback::Mode::PlayAll); expect_frames(play, {0,1,2,1,2,3,0,0,0}); EXPECT_TRUE(play.isStopped()); + + Tag* b = make_tag("B", 0, 3, AniDir::PING_PONG, 2); + sprite = make_sprite(4, { b }); + play = Playback(sprite.get(), 0, Playback::Mode::PlayInLoop); + expect_frames(play, {0,1,2,3,2,1,0, + 0,1,2,3,2,1,0, + 0,1,2,3,2,1,0}); + EXPECT_FALSE(play.isStopped()); + + Tag* c = make_tag("C", 0, 3, AniDir::PING_PONG_REVERSE, 2); + sprite = make_sprite(4, { c }); + play = Playback(sprite.get(), 0, Playback::Mode::PlayInLoop); + expect_frames(play, {0,1,2,3, + 3,2,1,0,1,2,3, + 3,2,1,0,1,2,3}); + EXPECT_FALSE(play.isStopped()); + + Tag* d = make_tag("D", 0, 3, AniDir::PING_PONG_REVERSE, 2); + sprite = make_sprite(4, { d }); + play = Playback(sprite.get(), 1, Playback::Mode::PlayInLoop); + expect_frames(play, {1,0,1,2,3, + 3,2,1,0,1,2,3, + 3,2,1,0,1,2,3}); + EXPECT_FALSE(play.isStopped()); + + Tag* e = make_tag("E", 0, 3, AniDir::PING_PONG_REVERSE, 1); + sprite = make_sprite(4, { e }); + play = Playback(sprite.get(), 0, Playback::Mode::PlayInLoop); + expect_frames(play, {0, + 3,2,1,0, + 3,2,1,0, + 3,2,1,0}); + EXPECT_FALSE(play.isStopped()); + + Tag* f = make_tag("F", 0, 3, AniDir::REVERSE, 2); + sprite = make_sprite(4, { f }); + play = Playback(sprite.get(), 0, Playback::Mode::PlayInLoop); + expect_frames(play, {0,3,2,1,0, + 3,2,1,0, 3,2,1,0, + 3,2,1,0, 3,2,1,0}); + EXPECT_FALSE(play.isStopped()); } TEST(Playback, LoopTagInfinite) @@ -464,39 +505,359 @@ TEST(Playback, PingPongWithInnerReverse) EXPECT_FALSE(play.isStopped()); } +// OnePingPongInsideOther series +static std::vector goRight(const int a, const int b) { + std::vector out; + if (a > b) + return out; + for (int i=a; i<=b ; ++i) + out.push_back(i); + return out; +} + +static std::vector goLeft(const int a, const int b) { + std::vector out; + if (a > b) + return out; + for (int i=b; i>=a ; --i) + out.push_back(i); + return out; +} + +static void concat(std::vector& a, const std::vector& b) +{ + for (size_t i=0; i - // B - // >---< - // 0 1 2 3 4 + // A repeat = 2 ; B repeat = 2 + // + // A A A + // *-------* *-------* *-------* + // B B B + // *---* *---* *---* + // 0 1 2 3 4 0 1 2 3 4 0 1 2 3 4 + const int lastFrame = 4; + std::vector A_AniDirs = {AniDir::PING_PONG, AniDir::PING_PONG_REVERSE}; + std::vector B_AniDirs = {AniDir::PING_PONG, AniDir::PING_PONG_REVERSE}; + std::vector A_Range = {0,lastFrame}; + std::vector> rangeBs = {{0,2}, {1,3}, {2,4}}; + std::vector> pingPongSeq1 = {{0,1,2,1,0}, {2,1,0,1,2}}; + std::vector> pingPongSeq2 = {{1,2,3,2,1}, {3,2,1,2,3}}; + std::vector> pingPongSeq3 = {{2,3,4,3,2}, {4,3,2,3,4}}; + std::vector right012 = {0,1,2}; - Tag* tagA = make_tag("A", 0, 4, AniDir::PING_PONG, 2); - Tag* tagB = make_tag("B", 1, 3, AniDir::PING_PONG_REVERSE, 3); - auto sprite = make_sprite(5, { tagA, tagB }); + for (auto A_aniDir : A_AniDirs) { + for (auto B_aniDir : B_AniDirs) { + for (auto B_Range : rangeBs) { + std::vector expected; + std::vector temp; + // A A A + // <-------> <-------> <-------> + // B B B + // *---* *---* *---* + // 0 1 2 3 4 0 1 2 3 4 0 1 2 3 4 + if (A_aniDir == doc::AniDir::PING_PONG) { + + // Start + temp = goRight(0, B_Range[0]-1); + concat(expected, temp); + + // Tag B playback + if (B_Range[0] == 0) + concat(expected, B_aniDir == doc::AniDir::PING_PONG ? pingPongSeq1[0] : right012); + else if (B_Range[0] == 1) + concat(expected, B_aniDir == doc::AniDir::PING_PONG ? pingPongSeq2[0] : pingPongSeq2[1]); + else if (B_Range[0] == 2) + concat(expected, B_aniDir == doc::AniDir::PING_PONG ? pingPongSeq3[0] : pingPongSeq3[1]); + + // Reproduce right side of the tag A + temp = goRight(B_Range[1]+1, lastFrame); + concat(expected, temp); + temp = goLeft(B_Range[1]+1, lastFrame-1); + concat(expected, temp); + + // Tag B playback (only if tag B last frame doesn't match with the tag A last frame + if (B_Range[1] != A_Range[1]) { + if (B_Range[1] == lastFrame - 1) + concat(expected, B_aniDir == doc::AniDir::PING_PONG ? pingPongSeq2[1] : pingPongSeq2[0]); + else if (B_Range[1] == lastFrame - 2) + concat(expected, B_aniDir == doc::AniDir::PING_PONG ? pingPongSeq1[1] : pingPongSeq1[0]); + } + + // Reproduce right side of the tag A + temp = goLeft(0, B_Range[0]-1); + concat(expected, temp); + // Sequence end + } + // A A A + // >-------< >-------< >-------< + // B B B + // *---* *---* *---* + // 0 1 2 3 4 0 1 2 3 4 0 1 2 3 4 + else { + + // Start + temp = goRight(0, B_Range[0]-1); + concat(expected, temp); + + // Tag B playback + if (B_Range[0] == 0) + concat(expected, B_aniDir == doc::AniDir::PING_PONG ? pingPongSeq1[0] : right012); + else if (B_Range[0] == 1) + concat(expected, B_aniDir == doc::AniDir::PING_PONG ? pingPongSeq2[0] : pingPongSeq2[1]); + else if (B_Range[0] == 2) + concat(expected, B_aniDir == doc::AniDir::PING_PONG ? pingPongSeq3[0] : pingPongSeq3[1]); + + // Reproduce right side of the tag A + temp = goRight(B_Range[1]+1, lastFrame); + concat(expected, temp); + // Sequence end + + // New Start + temp = goLeft(B_Range[1]+1, lastFrame); + concat(expected, temp); + + // Tag B playback + if (B_Range[1] == lastFrame) + concat(expected, B_aniDir == doc::AniDir::PING_PONG ? pingPongSeq3[1] : pingPongSeq3[0]); + else if (B_Range[1] == lastFrame-1) + concat(expected, B_aniDir == doc::AniDir::PING_PONG ? pingPongSeq2[1] : pingPongSeq2[0]); + else if (B_Range[1] == lastFrame-2) + concat(expected, B_aniDir == doc::AniDir::PING_PONG ? pingPongSeq1[1] : pingPongSeq1[0]); + + // Reproduce left side of the tag A + temp = goLeft(0, B_Range[0]-1); + concat(expected, temp); + temp = goRight(1, B_Range[0]-1); + concat(expected, temp); + + // Tag B playback (only if tag B first frame doesn't match with the tag A first frame + if (B_Range[0] == 1) + concat(expected, B_aniDir == doc::AniDir::PING_PONG ? pingPongSeq2[0] : pingPongSeq2[1]); + else if (B_Range[0] == 2) + concat(expected, B_aniDir == doc::AniDir::PING_PONG ? pingPongSeq3[0] : pingPongSeq3[1]); + + + // Reproduce right side of the tag A + temp = goRight(B_Range[1]+1, lastFrame); + concat(expected, temp); + // Sequence end + } + + // Test + Tag* tagA = make_tag("A", 0, 4, A_aniDir, 2); + Tag* tagB = make_tag("B", B_Range[0], B_Range[1], B_aniDir, 2); + auto sprite = make_sprite(lastFrame + 1, { tagA, tagB }); + Playback play(sprite.get(), 0, Playback::Mode::PlayInLoop); + + expect_frames(play, expected); + EXPECT_FALSE(play.isStopped()); + } + } + } +} + +TEST(Playback, OnePingPongInsideOther1Repeat) +{ + // A repeat = 1 ; B repeat = 1 + // + // A A A + // *-------* *-------* *-------* + // B B B + // *---* *---* *---* + // 0 1 2 3 4 0 1 2 3 4 0 1 2 3 4 + + const int lastFrame = 4; + std::vector A_AniDirs = {AniDir::PING_PONG, AniDir::PING_PONG_REVERSE}; + std::vector B_AniDirs = {AniDir::PING_PONG, AniDir::PING_PONG_REVERSE}; + std::vector A_Range = {0,lastFrame}; + std::vector> rangeBs = {{0,2}, {1,3}, {2,4}}; + std::vector> pingPongSeq1 = {{0,1,2}, {2,1,0}}; + std::vector> pingPongSeq2 = {{1,2,3}, {3,2,1}}; + std::vector> pingPongSeq3 = {{2,3,4}, {4,3,2}}; + + for (auto A_aniDir : A_AniDirs) { + for (auto B_aniDir : B_AniDirs) { + for (auto B_Range : rangeBs) { + std::vector expected; + std::vector temp; + // A A A + // <-------> <-------> <-------> + // B B B + // *---* *---* *---* + // 0 1 2 3 4 0 1 2 3 4 0 1 2 3 4 + if (A_aniDir == doc::AniDir::PING_PONG) { + + // Start + temp = goRight(0, B_Range[0]-1); + concat(expected, temp); + // Tag B playback + if (B_Range[0] == 0) { + temp = {0}; + concat(expected, B_aniDir == doc::AniDir::PING_PONG ? pingPongSeq1[0] : temp); + } + else if (B_Range[0] == 1) + concat(expected, B_aniDir == doc::AniDir::PING_PONG ? pingPongSeq2[0] : pingPongSeq2[1]); + else if (B_Range[0] == 2) + concat(expected, B_aniDir == doc::AniDir::PING_PONG ? pingPongSeq3[0] : pingPongSeq3[1]); + // Reproduce right side of the tag A + temp = goRight(B_Range[1]+1, lastFrame); + concat(expected, temp); + // Sequence end + + // Fresh sequence start + temp = goRight(0, B_Range[0]-1); + concat(expected, temp); + // Tag B playback + if (B_Range[0] == 0) + concat(expected, B_aniDir == doc::AniDir::PING_PONG ? pingPongSeq1[0] : pingPongSeq1[1]); + else if (B_Range[0] == 1) + concat(expected, B_aniDir == doc::AniDir::PING_PONG ? pingPongSeq2[0] : pingPongSeq2[1]); + else if (B_Range[0] == 2) + concat(expected, B_aniDir == doc::AniDir::PING_PONG ? pingPongSeq3[0] : pingPongSeq3[1]); + // Reproduce right side of the tag A + temp = goRight(B_Range[1]+1, lastFrame); + concat(expected, temp); + // Sequence end + + } + // A A A + // >-------< >-------< >-------< + // B B B + // *---* *---* *---* + // 0 1 2 3 4 0 1 2 3 4 0 1 2 3 4 + else { + // Start + temp = {0}; + // Tag B playback + if (B_Range[0] == 0 && B_aniDir == doc::AniDir::PING_PONG) + concat(expected, pingPongSeq1[0]); + else + concat(expected, temp); + // Sequence end + + // Fresh sequence start + // Reproduce right side of the tag A + temp = goLeft(B_Range[1]+1, lastFrame); + concat(expected, temp); + + // Tag B playback + if (B_Range[1] == lastFrame) + concat(expected, B_aniDir == doc::AniDir::PING_PONG ? pingPongSeq3[1] : pingPongSeq3[0]); + else if (B_Range[1] == lastFrame-1) + concat(expected, B_aniDir == doc::AniDir::PING_PONG ? pingPongSeq2[1] : pingPongSeq2[0]); + else if (B_Range[1] == lastFrame-2) + concat(expected, B_aniDir == doc::AniDir::PING_PONG ? pingPongSeq1[1] : pingPongSeq1[0]); + + // Reproduce left side of the tag A + temp = goLeft(0, B_Range[0]-1); + concat(expected, temp); + // Sequence end + } + + // Test + Tag* tagA = make_tag("A", 0, 4, A_aniDir, 1); + Tag* tagB = make_tag("B", B_Range[0], B_Range[1], B_aniDir, 1); + auto sprite = make_sprite(lastFrame + 1, { tagA, tagB }); + Playback play(sprite.get(), 0, Playback::Mode::PlayInLoop); + + expect_frames(play, expected); + EXPECT_FALSE(play.isStopped()); + } + } + } +} + +TEST(Playback, OnePingPongInsideForward) +{ + // A + // --------> + // B + // <---> + // 0 1 2 3 4 + + Tag* tagA = make_tag("A", 0, 4, AniDir::FORWARD, 2); + Tag* tagB = make_tag("B", 2, 4, AniDir::PING_PONG, 2); + auto sprite = make_sprite(5, { tagA, tagB }); + + Playback play(sprite.get(), 0, Playback::Mode::PlayInLoop); + expect_frames(play, {0,1 , 2,3,4,3,2, + 0,1 , 2,3,4,3,2}); + EXPECT_FALSE(play.isStopped()); +} + +TEST(Playback, OnePingPongInsideForward2) +{ + // A + // --------> + // B + // <---> + // 0 1 2 3 4 5 + + Tag* tagA = make_tag("A", 1, 5, AniDir::FORWARD, 2); + Tag* tagB = make_tag("B", 3, 5, AniDir::PING_PONG, 2); + auto sprite = make_sprite(6, { tagA, tagB }); Playback play(sprite.get(), 0, Playback::Mode::PlayInLoop); - expect_frames(play, {0, 3,2,1,2,3,2,1, 4, 1,2,3,2,1,2,3, 0, - 0, 3,2,1,2,3,2,1, 4, 1,2,3,2,1,2,3, 0, }); + expect_frames(play, {0 , 1,2 , 3,4,5,4,3, 1,2 , 3,4,5,4,3, + 0 , 1,2 , 3,4,5,4,3, 1,2 , 3,4,5,4,3}); EXPECT_FALSE(play.isStopped()); } -TEST(Playback, OnePingPongInsideOther3) +TEST(Playback, OnePingPongInsidePingPongReverse) { - // A - // <-------> - // B - // >---< - // 0 1 2 3 4 + // A + // >-------< + // B + // <---> + // 0 1 2 3 4 5 - Tag* tagA = make_tag("A", 0, 4, AniDir::PING_PONG, 3); - Tag* tagB = make_tag("B", 1, 3, AniDir::PING_PONG_REVERSE, 2); - auto sprite = make_sprite(5, { tagA, tagB }); + Tag* tagA = make_tag("A", 1, 5, AniDir::PING_PONG_REVERSE, 2); + Tag* tagB = make_tag("B", 3, 5, AniDir::PING_PONG, 2); + auto sprite = make_sprite(6, { tagA, tagB }); Playback play(sprite.get(), 0, Playback::Mode::PlayInLoop); - expect_frames(play, {0, 3,2,1,2,3, 4, 1,2,3,2,1, 0, 3,2,1,2,3, 4, - 0, 3,2,1,2,3, 4, 1,2,3,2,1, 0, 3,2,1,2,3, 4, 0 }); + expect_frames(play, {0 , 5,4,3,4,5 , 2,1,2 , 3,4,5,4,3, + 0 , 5,4,3,4,5 , 2,1,2 , 3,4,5,4,3}); + EXPECT_FALSE(play.isStopped()); +} + +TEST(Playback, OneReverseInsidePingPongReverse) +{ + // A + // >-------< + // B + // <---- + // 0 1 2 3 4 5 + + Tag* tagA = make_tag("A", 1, 5, AniDir::PING_PONG_REVERSE, 2); + Tag* tagB = make_tag("B", 3, 5, AniDir::REVERSE, 2); + auto sprite = make_sprite(6, { tagA, tagB }); + + Playback play(sprite.get(), 0, Playback::Mode::PlayInLoop); + expect_frames(play, {0 , 3,4,5,3,4,5 , 2,1,2 , 5,4,3,5,4,3, + 0 , 3,4,5,3,4,5 , 2,1,2 , 5,4,3,5,4,3}); + EXPECT_FALSE(play.isStopped()); +} + +TEST(Playback, OnePingPongReverseInsideReverse) +{ + // A + // <-------- + // B + // >---< + // 0 1 2 3 4 5 + + Tag* tagA = make_tag("A", 1, 5, AniDir::REVERSE, 2); + Tag* tagB = make_tag("B", 3, 5, AniDir::PING_PONG_REVERSE, 2); + auto sprite = make_sprite(6, { tagA, tagB }); + + Playback play(sprite.get(), 0, Playback::Mode::PlayInLoop); + expect_frames(play, {0 , 3,4,5,4,3 , 2,1, 3,4,5,4,3 , 2,1, + 0 , 3,4,5,4,3 , 2,1, 3,4,5,4,3 , 2,1}); EXPECT_FALSE(play.isStopped()); } @@ -536,6 +897,96 @@ TEST(Playback, TwoLoopsInCascadeReverse) EXPECT_FALSE(play.isStopped()); } +TEST(Playback, TwoLoopsInCascadeReversePingPongReverse1) +{ + // A + // <---- + // B + // >---< + // 0 1 2 3 4 + + Tag* a = make_tag("A", 1, 3, AniDir::REVERSE, 2); + Tag* b = make_tag("B", 2, 4, AniDir::PING_PONG_REVERSE, 2); + auto sprite = make_sprite(5, { a, b }); + + Playback play(sprite.get(), 0, Playback::Mode::PlayInLoop); + expect_frames(play, {0, 3,2,1,3,2,1, 4,3,2,3,4, + 0, 3,2,1,3,2,1, 4,3,2,3,4, 0 }); + EXPECT_FALSE(play.isStopped()); +} + +TEST(Playback, TwoLoopsInCascadeReversePingPongReverse2) +{ + // A + // <------ + // B + // >---< + // 0 1 2 3 4 + + Tag* a = make_tag("A", 0, 3, AniDir::REVERSE, 2); + Tag* b = make_tag("B", 2, 4, AniDir::PING_PONG_REVERSE, 2); + auto sprite = make_sprite(5, { a, b }); + + Playback play(sprite.get(), 0, Playback::Mode::PlayInLoop); + expect_frames(play, {0, 3,2,1,0, 4,3,2,3,4, + 3,2,1,0,3,2,1,0, 4,3,2,3,4 }); + EXPECT_FALSE(play.isStopped()); +} + +TEST(Playback, TwoLoopsInCascadeReversePingPongReverse3) +{ + // A + // <-------- + // B + // >---< + // 0 1 2 3 4 + + Tag* a = make_tag("A", 0, 4, AniDir::REVERSE, 2); + Tag* b = make_tag("B", 2, 4, AniDir::PING_PONG_REVERSE, 2); + auto sprite = make_sprite(5, { a, b }); + + Playback play(sprite.get(), 0, Playback::Mode::PlayInLoop); + expect_frames(play, {0, 2,3,4,3,2, 1,0, + 2,3,4,3,2, 1,0, 2,3,4,3,2, 1,0,}); + EXPECT_FALSE(play.isStopped()); +} + +TEST(Playback, TwoLoopsInCascadePingPongReverseReverse1) +{ + // A + // >-----< + // B + // <---- + // 0 1 2 3 4 + + Tag* a = make_tag("A", 0, 3, AniDir::PING_PONG_REVERSE, 2); + Tag* b = make_tag("B", 2, 4, AniDir::REVERSE, 2); + auto sprite = make_sprite(5, { a, b }); + + Playback play(sprite.get(), 0, Playback::Mode::PlayInLoop); + expect_frames(play, {0, 1,2,3, 4,3,2,4,3,2, + 3,2,1,0,1,2,3, 4,3,2,4,3,2 }); + EXPECT_FALSE(play.isStopped()); +} + +TEST(Playback, TwoLoopsInCascadePingPongReverseReverse2) +{ + // A + // >---< + // B + // <---- + // 0 1 2 3 4 + + Tag* a = make_tag("A", 1, 3, AniDir::PING_PONG_REVERSE, 2); + Tag* b = make_tag("B", 2, 4, AniDir::REVERSE, 2); + auto sprite = make_sprite(5, { a, b }); + + Playback play(sprite.get(), 0, Playback::Mode::PlayInLoop); + expect_frames(play, {0, 3,2,1,2,3, 4,3,2,4,3,2, + 0, 3,2,1,2,3, 4,3,2,4,3,2, 0 }); + EXPECT_FALSE(play.isStopped()); +} + TEST(Playback, ThreeLoopsInCascade) { // A