Add repeat field to animation tags (#1275, #1740)

This commit is contained in:
David Capello 2022-10-19 12:09:27 -03:00
parent 17a5b3f3fc
commit 4f96d37b1f
33 changed files with 1455 additions and 211 deletions

View File

@ -14,6 +14,7 @@ right_click = Right-click: Show playback options
forward = Forward
reverse = Reverse
ping_pong = Ping-pong
ping_pong_reverse = Ping-pong Reverse
[ask_for_color_profile]
title = Color Profile
@ -1941,6 +1942,7 @@ from = From:
to = To:
color = Color:
ani_dir = Animation Direction:
repeat = Repeat:
[tga_options]
title = TGA Options

View File

@ -6,17 +6,22 @@
<window id="tag_properties" text="@.title">
<vbox>
<grid id="properties_grid" columns="3">
<label text="@.name" />
<entry maxsize="256" id="name" magnet="true" cell_align="horizontal" expansive="true" />
<button id="user_data" icon="icon_user_data" maxsize="32" tooltip="@general.user_data" />
<label text="@.from" />
<expr id="from" cell_hspan="2" />
<label text="@.to" />
<expr id="to" cell_hspan="2" />
<label text="@.ani_dir" />
<combobox id="anidir" cell_hspan="2" />
<check text="@.repeat" id="limit_repeat" />
<vbox id="repeat_placeholder" cell_hspan="2" />
</grid>
<grid columns="2">
<separator horizontal="true" cell_hspan="2" minwidth="180" />

View File

@ -292,7 +292,15 @@ for each tag.
0 = Forward
1 = Reverse
2 = Ping-pong
BYTE[8] For future (set to zero)
3 = Ping-pong Reverse
WORD Repeat N times. Play this animation section N times:
0 = Doesn't specify (plays infinite in UI, once on export,
for ping-pong it plays once in each direction)
1 = Plays once (for ping-pong, it plays just in one direction)
2 = Plays twice (for ping-pong, it plays once in one direction,
and once in reverse)
n = Plays N times
BYTE[6] For future (set to zero)
BYTE[3] RGB values of the tag color
Deprecated, used only for backward compatibility with Aseprite v1.2.x
The color of the tag is the one in the user data field following

View File

@ -521,6 +521,7 @@ add_library(app-lib
cmd/set_tag_color.cpp
cmd/set_tag_name.cpp
cmd/set_tag_range.cpp
cmd/set_tag_repeat.cpp
cmd/set_tileset_base_index.cpp
cmd/set_tileset_name.cpp
cmd/set_total_frames.cpp

View File

@ -0,0 +1,38 @@
// Aseprite
// Copyright (C) 2021 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "app/cmd/set_tag_repeat.h"
#include "doc/tag.h"
namespace app {
namespace cmd {
SetTagRepeat::SetTagRepeat(Tag* tag, int repeat)
: WithTag(tag)
, m_oldRepeat(tag->repeat())
, m_newRepeat(repeat)
{
}
void SetTagRepeat::onExecute()
{
tag()->setRepeat(m_newRepeat);
tag()->incrementVersion();
}
void SetTagRepeat::onUndo()
{
tag()->setRepeat(m_oldRepeat);
tag()->incrementVersion();
}
} // namespace cmd
} // namespace app

View File

@ -0,0 +1,38 @@
// Aseprite
// Copyright (C) 2021 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifndef APP_CMD_SET_TAG_REPEAT_H_INCLUDED
#define APP_CMD_SET_TAG_REPEAT_H_INCLUDED
#pragma once
#include "app/cmd.h"
#include "app/cmd/with_tag.h"
namespace app {
namespace cmd {
using namespace doc;
class SetTagRepeat : public Cmd
, public WithTag {
public:
SetTagRepeat(Tag* tag, int repeat);
protected:
void onExecute() override;
void onUndo() override;
size_t onMemSize() const override {
return sizeof(*this);
}
private:
int m_oldRepeat;
int m_newRepeat;
};
} // namespace cmd
} // namespace app
#endif

View File

@ -13,6 +13,7 @@
#include "app/cmd/set_tag_color.h"
#include "app/cmd/set_tag_name.h"
#include "app/cmd/set_tag_range.h"
#include "app/cmd/set_tag_repeat.h"
#include "app/cmd/set_user_data.h"
#include "app/color.h"
#include "app/commands/command.h"
@ -108,6 +109,10 @@ void FrameTagPropertiesCommand::onExecute(Context* context)
if (tag->aniDir() != anidir)
tx(new cmd::SetTagAniDir(tag, anidir));
const int repeat = window.repeatValue();
if (tag->repeat() != repeat)
tx(new cmd::SetTagRepeat(tag, repeat));
// Change user data
doc::UserData userData = window.userDataValue();
if (tag->userData() != userData) {

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2020 Igara Studio S.A.
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -69,6 +69,7 @@ void NewFrameTagCommand::onExecute(Context* context)
tag->setFrameRange(from, to);
tag->setName(window.nameValue());
tag->setAniDir(window.aniDirValue());
tag->setRepeat(window.repeatValue());
tag->setUserData(window.userDataValue());
{

View File

@ -202,6 +202,10 @@ void SaveFileBaseCommand::saveDocumentInBackground(
case AniDir::PING_PONG:
m_selFrames = m_selFrames.makePingPong();
break;
case AniDir::PING_PONG_REVERSE:
m_selFrames = m_selFrames.makePingPong();
m_selFrames = m_selFrames.makeReverse();
break;
}
}

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2020 Igara Studio S.A.
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2018 David Capello
//
// This program is distributed under the terms of
@ -67,7 +67,8 @@ DocDiff compare_docs(const Doc* a,
aTag->toFrame() != bTag->toFrame() ||
aTag->name() != bTag->name() ||
aTag->color() != bTag->color() ||
aTag->aniDir() != bTag->aniDir()) {
aTag->aniDir() != bTag->aniDir() ||
aTag->repeat() != bTag->repeat()) {
diff.anything = diff.tags = true;
}
}

View File

@ -1435,6 +1435,9 @@ void DocExporter::createDataFile(const Samples& samples,
<< " \"from\": " << (tag->fromFrame()) << ","
<< " \"to\": " << (tag->toFrame()) << ","
" \"direction\": \"" << escape_for_json(convert_anidir_to_string(tag->aniDir())) << "\"";
if (tag->repeat() > 0) {
os << ", \"repeat\": \"" << tag->repeat() << "\"";
}
os << tag->userData() << " }";
}
}

View File

@ -1109,7 +1109,8 @@ static void ase_file_write_tags_chunk(FILE* f,
fputw(to, f);
fputc((int)tag->aniDir(), f);
fputl(0, f); // 8 reserved bytes
fputw(std::clamp(tag->repeat(), 0, Tag::kMaxRepeat), f); // repeat
fputw(0, f); // 6 reserved bytes
fputl(0, f);
fputc(doc::rgba_getr(tag->color()), f);

View File

@ -275,6 +275,7 @@ Engine::Engine()
setfield_integer(L, "FORWARD", doc::AniDir::FORWARD);
setfield_integer(L, "REVERSE", doc::AniDir::REVERSE);
setfield_integer(L, "PING_PONG", doc::AniDir::PING_PONG);
setfield_integer(L, "PING_PONG_REVERSE", doc::AniDir::PING_PONG_REVERSE);
lua_pop(L, 1);
lua_newtable(L);

View File

@ -21,7 +21,6 @@
#include "app/ui/editor/scrolling_state.h"
#include "app/ui/skin/skin_theme.h"
#include "app/ui_context.h"
#include "doc/handle_anidir.h"
#include "doc/tag.h"
#include "ui/manager.h"
#include "ui/message.h"
@ -39,7 +38,6 @@ PlayState::PlayState(const bool playOnce,
, m_toScroll(false)
, m_playTimer(10)
, m_nextFrameTime(-1)
, m_pingPongForward(true)
, m_refFrame(0)
, m_tag(nullptr)
{
@ -85,10 +83,17 @@ void PlayState::onEnterState(Editor* editor)
m_editor->setFrame(frame);
}
m_playback = doc::Playback(m_editor->sprite(),
TagsList(), // TODO add support to follow subtags
m_editor->frame(),
m_playOnce ? doc::Playback::PlayOnce:
m_playAll ? doc::Playback::PlayWithoutTagsInLoop:
doc::Playback::PlayInLoop,
m_tag);
m_toScroll = false;
m_nextFrameTime = getNextFrameTime();
m_curFrameTick = base::current_tick();
m_pingPongForward = true;
// Maybe we came from ScrollingState and the timer is already
// running.
@ -195,40 +200,12 @@ void PlayState::onPlaybackTick()
m_nextFrameTime -= (base::current_tick() - m_curFrameTick);
doc::Sprite* sprite = m_editor->sprite();
while (m_nextFrameTime <= 0) {
doc::frame_t frame = m_editor->frame();
if (m_playOnce) {
bool atEnd = false;
if (m_tag) {
switch (m_tag->aniDir()) {
case AniDir::FORWARD:
atEnd = (frame == m_tag->toFrame());
break;
case AniDir::REVERSE:
atEnd = (frame == m_tag->fromFrame());
break;
case AniDir::PING_PONG:
atEnd = (!m_pingPongForward &&
frame == m_tag->fromFrame());
break;
}
}
else {
atEnd = (frame == sprite->lastFrame());
}
if (atEnd) {
m_editor->stop();
break;
}
doc::frame_t frame = m_playback.nextFrame();
if (m_playback.isStopped()) {
m_editor->stop();
break;
}
frame = calculate_next_frame(
sprite, frame, frame_t(1), m_tag,
m_pingPongForward);
m_editor->setFrame(frame);
m_nextFrameTime += getNextFrameTime();
}

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2020-2021 Igara Studio S.A.
// Copyright (C) 2020-2022 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -12,6 +12,7 @@
#include "app/ui/editor/state_with_wheel_behavior.h"
#include "base/time.h"
#include "doc/frame.h"
#include "doc/playback.h"
#include "obs/connection.h"
#include "ui/timer.h"
@ -50,6 +51,7 @@ namespace app {
double getNextFrameTime();
Editor* m_editor;
doc::Playback m_playback;
bool m_playOnce;
bool m_playAll;
bool m_toScroll;

View File

@ -141,11 +141,13 @@ void fill_anidir_combobox(ui::ComboBox* anidir, doc::AniDir defAnidir)
static_assert(
int(doc::AniDir::FORWARD) == 0 &&
int(doc::AniDir::REVERSE) == 1 &&
int(doc::AniDir::PING_PONG) == 2, "doc::AniDir has changed");
int(doc::AniDir::PING_PONG) == 2 &&
int(doc::AniDir::PING_PONG_REVERSE) == 3, "doc::AniDir has changed");
anidir->addItem(Strings::anidir_combo_forward());
anidir->addItem(Strings::anidir_combo_reverse());
anidir->addItem(Strings::anidir_combo_ping_pong());
anidir->addItem(Strings::anidir_combo_ping_pong_reverse());
anidir->setSelectedItemIndex(int(defAnidir));
}

View File

@ -15,14 +15,41 @@
#include "app/pref/preferences.h"
#include "app/ui/layer_frame_comboboxes.h"
#include "app/ui/user_data_view.h"
#include "base/convert_to.h"
#include "doc/sprite.h"
#include "doc/tag.h"
#include "ui/manager.h"
#include "ui/message.h"
#include <algorithm>
namespace app {
const char* kInfiniteSymbol = "\xE2\x88\x9E"; // Infinite symbol (UTF-8)
TagWindow::Repeat::Repeat()
{
}
bool TagWindow::Repeat::onProcessMessage(ui::Message* msg)
{
switch (msg->type()) {
case ui::kFocusEnterMessage: {
if (text() == kInfiniteSymbol)
setText("");
break;
}
}
return ExprEntry::onProcessMessage(msg);
}
void TagWindow::Repeat::onFormatExprFocusLeave(std::string& buf)
{
ExprEntry::onFormatExprFocusLeave(buf);
if (buf.empty() || base::convert_to<int>(buf) == 0)
buf = kInfiniteSymbol;
}
TagWindow::TagWindow(const doc::Sprite* sprite, const doc::Tag* tag)
: m_sprite(sprite)
, m_base(Preferences::instance().document(
@ -32,11 +59,25 @@ TagWindow::TagWindow(const doc::Sprite* sprite, const doc::Tag* tag)
{
m_userDataView.configureAndSet(m_userData, propertiesGrid());
repeatPlaceholder()->addChild(&m_repeat);
name()->setText(tag->name());
from()->setTextf("%d", tag->fromFrame()+m_base);
to()->setTextf("%d", tag->toFrame()+m_base);
if (tag->repeat() > 0) {
limitRepeat()->setSelected(true);
repeat()->setTextf("%d", tag->repeat());
}
else {
limitRepeat()->setSelected(false);
repeat()->setText(kInfiniteSymbol);
}
fill_anidir_combobox(anidir(), tag->aniDir());
limitRepeat()->Click.connect([this]{ onLimitRepeat(); });
repeat()->Change.connect([this]{ onRepeatChange(); });
userData()->Click.connect([this]{ onToggleUserData(); });
}
@ -46,12 +87,12 @@ bool TagWindow::show()
return (closer() == ok());
}
std::string TagWindow::nameValue()
std::string TagWindow::nameValue() const
{
return name()->text();
}
void TagWindow::rangeValue(doc::frame_t& from, doc::frame_t& to)
void TagWindow::rangeValue(doc::frame_t& from, doc::frame_t& to) const
{
doc::frame_t first = 0;
doc::frame_t last = m_sprite->lastFrame();
@ -62,11 +103,33 @@ void TagWindow::rangeValue(doc::frame_t& from, doc::frame_t& to)
to = std::clamp(to, from, last);
}
doc::AniDir TagWindow::aniDirValue()
doc::AniDir TagWindow::aniDirValue() const
{
return (doc::AniDir)anidir()->getSelectedItemIndex();
}
int TagWindow::repeatValue() const
{
return repeat()->textInt();
}
void TagWindow::onLimitRepeat()
{
if (limitRepeat()->isSelected())
repeat()->setText("1");
else
repeat()->setText(kInfiniteSymbol);
}
void TagWindow::onRepeatChange()
{
if (repeat()->text().empty() ||
repeat()->textInt() == 0)
limitRepeat()->setSelected(false);
else
limitRepeat()->setSelected(true);
}
void TagWindow::onToggleUserData()
{
m_userDataView.toggleVisibility();

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2020 Igara Studio S.A.
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -10,6 +10,7 @@
#pragma once
#include "app/ui/color_button.h"
#include "app/ui/expr_entry.h"
#include "app/ui/user_data_view.h"
#include "doc/anidir.h"
#include "doc/frame.h"
@ -30,18 +31,33 @@ namespace app {
bool show();
std::string nameValue();
void rangeValue(doc::frame_t& from, doc::frame_t& to);
doc::AniDir aniDirValue();
std::string nameValue() const;
void rangeValue(doc::frame_t& from, doc::frame_t& to) const;
doc::AniDir aniDirValue() const;
int repeatValue() const;
const doc::UserData& userDataValue() const { return m_userDataView.userData(); }
private:
class Repeat : public ExprEntry {
public:
Repeat();
private:
bool onProcessMessage(ui::Message* msg) override;
void onFormatExprFocusLeave(std::string& buf) override;
};
const Repeat* repeat() const { return &m_repeat; }
Repeat* repeat() { return &m_repeat; }
void onLimitRepeat();
void onRepeatChange();
void onToggleUserData();
const doc::Sprite* m_sprite;
int m_base;
doc::UserData m_userData;
UserDataView m_userDataView;
Repeat m_repeat;
};
}

View File

@ -584,7 +584,7 @@ void Timeline::activateClipboardRange()
}
Tag* Timeline::getTagByFrame(const frame_t frame,
const bool getLoopTagIfNone)
const bool getLoopTagIfNone)
{
if (!m_sprite)
return nullptr;

View File

@ -986,11 +986,13 @@ void AsepriteDecoder::readTagsChunk(doc::Tags* tags)
int aniDir = read8();
if (aniDir != int(doc::AniDir::FORWARD) &&
aniDir != int(doc::AniDir::REVERSE) &&
aniDir != int(doc::AniDir::PING_PONG)) {
aniDir != int(doc::AniDir::PING_PONG) &&
aniDir != int(doc::AniDir::PING_PONG_REVERSE)) {
aniDir = int(doc::AniDir::FORWARD);
}
read32(); // 8 reserved bytes
int repeat = read16(); // Number of times we repeat this tag
read16(); // 6 reserved bytes
read32();
int r = read8();
@ -1009,6 +1011,7 @@ void AsepriteDecoder::readTagsChunk(doc::Tags* tags)
tag->setName(name);
tag->setAniDir((doc::AniDir)aniDir);
tag->setRepeat(repeat);
tags->add(tag);
}
}

View File

@ -39,7 +39,6 @@ add_library(doc-lib
file/pal_file.cpp
grid.cpp
grid_io.cpp
handle_anidir.cpp
image.cpp
image_impl.cpp
image_io.cpp
@ -55,6 +54,7 @@ add_library(doc-lib
octree_map.cpp
palette.cpp
palette_io.cpp
playback.cpp
primitives.cpp
remap.cpp
rgbmap_rgb5a3.cpp

View File

@ -1,4 +1,5 @@
// Aseprite Document Library
// Copyright (c) 2021 Igara Studio S.A.
// Copyright (c) 2001-2018 David Capello
//
// This file is released under the terms of the MIT license.
@ -18,6 +19,7 @@ std::string convert_anidir_to_string(AniDir anidir)
case AniDir::FORWARD: return "forward";
case AniDir::REVERSE: return "reverse";
case AniDir::PING_PONG: return "pingpong";
case AniDir::PING_PONG_REVERSE: return "pingpong_reverse";
}
return "";
}
@ -27,6 +29,7 @@ doc::AniDir convert_string_to_anidir(const std::string& s)
if (s == "forward") return AniDir::FORWARD;
if (s == "reverse") return AniDir::REVERSE;
if (s == "pingpong") return AniDir::PING_PONG;
if (s == "pingpong_reverse") return AniDir::PING_PONG_REVERSE;
return AniDir::FORWARD;
}

View File

@ -1,4 +1,5 @@
// Aseprite Document Library
// Copyright (c) 2021 Igara Studio S.A.
// Copyright (c) 2001-2018 David Capello
//
// This file is released under the terms of the MIT license.
@ -15,7 +16,8 @@ namespace doc {
enum class AniDir {
FORWARD = 0,
REVERSE = 1,
PING_PONG = 2,
PING_PONG = 2, // First playback is in forward
PING_PONG_REVERSE = 3, // First playback is in reverse
};
std::string convert_anidir_to_string(AniDir anidir);

View File

@ -1,101 +0,0 @@
// Aseprite Document Library
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2001-2016 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "doc/handle_anidir.h"
#include "doc/frame.h"
#include "doc/sprite.h"
#include "doc/tag.h"
namespace doc {
frame_t calculate_next_frame(
const Sprite* sprite,
frame_t frame,
frame_t frameDelta,
const Tag* tag,
bool& pingPongForward)
{
if (frameDelta == 0)
return frame;
frame_t first = frame_t(0);
frame_t last = sprite->lastFrame();
AniDir aniDir = AniDir::FORWARD;
if (tag) {
frame_t loopFrom, loopTo;
loopFrom = tag->fromFrame();
loopTo = tag->toFrame();
loopFrom = std::clamp(loopFrom, first, last);
loopTo = std::clamp(loopTo, first, last);
first = loopFrom;
last = loopTo;
aniDir = tag->aniDir();
}
frame_t frameRange = (last - first + 1);
switch (aniDir) {
case AniDir::REVERSE:
frameDelta = -frameDelta;
[[fallthrough]];
case AniDir::FORWARD:
frame += frameDelta;
while (frame > last) frame -= frameRange;
while (frame < first) frame += frameRange;
break;
case AniDir::PING_PONG: {
bool invertPingPong;
if (frameDelta < 0) {
frameDelta = -frameDelta;
pingPongForward = !pingPongForward;
invertPingPong = true;
}
else
invertPingPong = false;
while (--frameDelta >= 0) {
if (pingPongForward) {
++frame;
if (frame > last) {
frame = last-1;
if (frame < first)
frame = first;
pingPongForward = false;
}
}
else {
--frame;
if (frame < first) {
frame = first+1;
if (frame > last)
frame = last;
pingPongForward = true;
}
}
}
if (invertPingPong)
pingPongForward = !pingPongForward;
break;
}
}
return frame;
}
} // namespace doc

View File

@ -1,28 +0,0 @@
// Aseprite Document Library
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2001-2015 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifndef DOC_HANDLE_ANIDIR_H_INCLUDED
#define DOC_HANDLE_ANIDIR_H_INCLUDED
#pragma once
#include "doc/frame.h"
namespace doc {
class Sprite;
class Tag;
frame_t calculate_next_frame(
const Sprite* sprite,
frame_t frame,
frame_t frameDelta,
const Tag* tag,
bool& pingPongForward);
} // namespace doc
#endif

500
src/doc/playback.cpp Normal file
View File

@ -0,0 +1,500 @@
// Aseprite Document Library
// Copyright (C) 2021-2022 Igara Studio S.A.
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "doc/playback.h"
#include "doc/frame.h"
#include "doc/sprite.h"
#include "doc/tag.h"
#include <limits>
#define PLAY_TRACE(...) // TRACEARGS
namespace doc {
[[maybe_unused]]
static const char* mode_to_string(Playback::Mode mode)
{
switch (mode) {
case Playback::PlayAll: return "PlayAll";
case Playback::PlayInLoop: return "PlayInLoop";
case Playback::PlayWithoutTagsInLoop: return "PlayWithoutTagsInLoop";
case Playback::PlayOnce: return "PlayOnce";
case Playback::Stopped: return "Stopped";
}
return "";
}
Playback::PlayTag::PlayTag(const Tag* tag, int parentForward)
: tag(tag)
, forward(parentForward * (tag->aniDir() == AniDir::FORWARD ||
tag->aniDir() == AniDir::PING_PONG ? 1: -1))
{
if (tag->repeat() > 0) {
repeat = tag->repeat();
}
// Repeat=0 is a "infinite repeat", but we'll play the tag just
// once.
else {
if (tag->aniDir() == AniDir::PING_PONG ||
tag->aniDir() == AniDir::PING_PONG_REVERSE) {
repeat = 2;
}
else {
repeat = 1;
}
}
}
Playback::Playback(const Sprite* sprite,
const TagsList& tags,
const frame_t frame,
const Mode playMode,
const Tag* tag)
: m_sprite(sprite)
, m_tags(tags)
, m_initialFrame(frame)
, m_frame(frame)
, m_playMode(playMode)
{
PLAY_TRACE("--Playback-- tag=", (tag ? tag->name(): ""), "mode=", mode_to_string(m_playMode));
// Go to the first frame of the animation or active frame tag
if (playMode == Mode::PlayOnce) {
if (tag) {
m_frame = (tag->aniDir() == AniDir::REVERSE ||
tag->aniDir() == AniDir::PING_PONG_REVERSE ?
tag->toFrame():
tag->fromFrame());
addTag(tag, false, 1);
}
else {
m_frame = 0;
}
}
else if (playMode == Mode::PlayInLoop) {
if (tag) {
addTag(tag, false, 1);
// Loop the given tag in the constructor infite times
m_playing.back()->repeat = std::numeric_limits<int>::max();
}
}
if (m_sprite)
handleEnterFrame(frame, true);
}
Playback::Playback(const Sprite* sprite,
const frame_t frame,
const Mode playMode,
const Tag* tag)
: Playback(sprite,
(sprite ? sprite->tags().getInternalList(): TagsList()),
frame,
playMode,
tag)
{
}
frame_t Playback::nextFrame(frame_t frameDelta)
{
PLAY_TRACE(" Playback::nextFrame { frame=", m_frame, "+", frameDelta);
int step = (frameDelta > 0 ? +1: -1);
while (frameDelta != 0 && m_playMode != Stopped) {
bool move = handleExitFrame(step);
if (move)
handleMoveFrame(step);
handleEnterFrame(step, false);
if (frameDelta > 0)
--frameDelta;
else if (frameDelta < 0)
++frameDelta;
}
PLAY_TRACE(" } =", m_frame,
"(tag=", (tag() ? tag()->name(): "nullptr"),
", repeat=", (!m_playing.empty() ? m_playing.back()->repeat: -1), ")");
return m_frame;
}
void Playback::stop()
{
if (m_playMode == Mode::PlayAll ||
m_playMode == Mode::PlayOnce) {
m_frame = m_initialFrame;
}
m_playMode = Mode::Stopped;
}
Tag* Playback::tag() const
{
return (!m_playing.empty() ? const_cast<Tag*>(m_playing.back()->tag): nullptr);
}
void Playback::handleEnterFrame(const frame_t frameDelta, const bool firstTime)
{
PLAY_TRACE(" handleEnterFrame", m_frame, "+", frameDelta);
switch (m_playMode) {
case PlayAll:
case PlayInLoop: {
const Tag* tag = this->tag();
const frame_t frame = m_frame;
const int forward = getParentForward();
for (const Tag* t : m_tags) {
if (t->contains(frame)) {
// Ignored tags that were played
if (m_played.find(t) != m_played.end()) {
continue;
}
if (tag &&
(tag->toFrame() < t->toFrame() ||
tag->fromFrame() > t->fromFrame())) {
// Cascade
addTag(t, true, 1);
}
else {
addTag(t, false, forward);
if (!firstTime)
goToFirstTagFrame(t, frameDelta);
}
}
}
break;
}
case PlayWithoutTagsInLoop:
case PlayOnce:
// Do nothing
break;
}
}
bool Playback::handleExitFrame(const frame_t frameDelta)
{
PLAY_TRACE(" handleExitFrame", m_frame, "+", frameDelta);
switch (m_playMode) {
case PlayAll:
case PlayInLoop: {
if (auto tag = this->tag()) {
ASSERT(!m_playing.empty());
int forward = m_playing.back()->forward;
PLAY_TRACE("tag aniDir=", (int)tag->aniDir(),
"range=", (int)tag->fromFrame(), (int)tag->toFrame(),
"forward=", forward);
if ((tag->aniDir() == AniDir::FORWARD ||
tag->aniDir() == AniDir::REVERSE)
&& ((forward > 0 && m_frame == tag->toFrame())
||
(forward < 0 && m_frame == tag->fromFrame()))) {
decrementRepeat(frameDelta);
return false;
}
// Change ping-pong direction
else if ((tag->aniDir() == AniDir::PING_PONG ||
tag->aniDir() == AniDir::PING_PONG_REVERSE)
&& ((m_frame == tag->fromFrame() && forward < 0)
|| (m_frame == tag->toFrame() && forward > 0))) {
PLAY_TRACE(" Changing direction frame=", m_frame,
" forward=", forward, "->", -forward);
// Changing the direction of the ping-pong animation
m_playing.back()->invertForward();
return decrementRepeat(frameDelta);
}
}
if (frameDelta > 0 && m_frame == m_sprite->lastFrame()) {
if (m_playMode == PlayInLoop) {
PLAY_TRACE(" Going back to frame=0 (PlayInLoop)", m_frame,
m_sprite->lastFrame());
m_frame = 0;
return false;
}
else {
PLAY_TRACE(" Stop animation (PlayAll)");
stop();
return false;
}
}
else if (frameDelta < 0 && m_frame == 0) {
if (m_playMode == PlayInLoop) {
PLAY_TRACE(" Going back to frame=last frame (PlayInLoop)");
m_frame = m_sprite->lastFrame();
return false;
}
else {
PLAY_TRACE(" Stop animation in first frame (PlayAll)");
stop();
return false;
}
}
break;
}
case PlayWithoutTagsInLoop:
// Do nothing
break;
case PlayOnce: {
if (auto tag = this->tag()) {
ASSERT(m_playing.size() == 1);
int forward = m_playing.back()->forward;
if ((tag->aniDir() == AniDir::FORWARD && m_frame == tag->toFrame()) ||
(tag->aniDir() == AniDir::REVERSE && m_frame == tag->fromFrame()) ||
(tag->aniDir() == AniDir::PING_PONG && m_frame == tag->fromFrame() && forward < 0) ||
(tag->aniDir() == AniDir::PING_PONG_REVERSE && m_frame == tag->toFrame() && forward > 0)) {
stop();
return false;
}
else if ((tag->aniDir() == AniDir::PING_PONG &&
m_frame == tag->toFrame() && forward > 0)
|| (tag->aniDir() == AniDir::PING_PONG_REVERSE &&
m_frame == tag->fromFrame() && forward < 0)) {
PLAY_TRACE(" Changing direction frame=", m_frame,
" forward=", forward, "->", -forward);
// Changing the direction of the ping-pong animation
m_playing.back()->invertForward();
}
}
else if ((frameDelta > 0 && m_frame == m_sprite->lastFrame()) ||
(frameDelta < 0 && m_frame == 0)) {
stop();
return false;
}
break;
}
}
return true;
}
void Playback::handleMoveFrame(const frame_t frameDelta)
{
PLAY_TRACE(" handleMoveFrame", m_frame, "+", frameDelta);
switch (m_playMode) {
case PlayWithoutTagsInLoop: {
ASSERT(m_playing.empty());
frame_t first = 0;
frame_t last = m_sprite->lastFrame();
m_frame += frameDelta;
if (m_frame < 0) m_frame = last;
if (m_frame > last) m_frame = first;
break;
}
case PlayAll:
case PlayInLoop:
case PlayOnce: {
m_frame += frameDelta * getParentForward();
break;
}
}
}
void Playback::addTag(const Tag* tag,
const bool rewind,
const int forward)
{
auto playTag = std::make_unique<PlayTag>(tag, forward);
PLAY_TRACE(" addTag", tag->name(),
"rewind", rewind,
"new playTag forward", playTag->forward);
if (rewind) {
playTag->rewind = true;
// Delay the deletion of currentPlayTag to this new tag
PlayTag* currentPlayTag = m_playing.back().get();
PlayTag* delayed = currentPlayTag;
while (delayed->delayedDelete)
delayed = delayed->delayedDelete;
delayed->delayedDelete = playTag.get();
for (const Tag* otherTag : delayed->removeThese)
playTag->removeThese.push_back(otherTag);
playTag->removeThese.push_back(delayed->tag);
delayed->removeThese.clear();
auto it = m_playing.end(),
begin = m_playing.begin();
--it;
ASSERT(it->get() == currentPlayTag);
while (it != begin) {
if ((*it)->tag == delayed->tag)
break;
--it;
}
m_playing.insert(it, std::move(playTag));
}
else {
m_playing.push_back(std::move(playTag));
}
m_played.insert(tag);
}
void Playback::removeLastTagFromPlayed()
{
PlayTag* playTag = m_playing.back().get();
for (auto otherTag : playTag->removeThese) {
auto it = m_played.find(otherTag);
ASSERT(it != m_played.end());
if (it != m_played.end())
m_played.erase(it);
}
auto it = m_played.find(playTag->tag);
ASSERT(it != m_played.end());
if (it != m_played.end())
m_played.erase(it);
}
bool Playback::decrementRepeat(const frame_t frameDelta)
{
while (true) {
Tag* tag = this->tag();
PLAY_TRACE(" Decrement tag", tag->name(),
"repeat", m_playing.back()->repeat, "-1");
if (m_playing.back()->repeat > 1) {
--m_playing.back()->repeat;
goToFirstTagFrame(tag, frameDelta);
PLAY_TRACE(" Repeat tag", tag->name(), " frame=", m_frame,
"repeat=", m_playing.back()->repeat,
"forward=", m_playing.back()->forward);
return true;
}
else {
// Remove tag from played
if (!m_playing.back()->delayedDelete) {
PLAY_TRACE(" Removing played tag", tag->name());
removeLastTagFromPlayed();
}
else {
PLAY_TRACE(" Delaying the removal of played tag", tag->name());
}
// Delete and remove PlayTag
m_playing.pop_back();
// Forward direction of the parent tag
int forward = (m_playing.empty() ? +1: m_playing.back()->forward);
bool rewind = (m_playing.empty() ? false: m_playing.back()->rewind);
// New frame outside the tag
frame_t newFrame;
if (rewind) {
newFrame = firstTagFrame(m_playing.back()->tag, frameDelta);
}
else {
newFrame = (frameDelta * forward < 0 ? tag->fromFrame()-1: tag->toFrame()+1);
}
PLAY_TRACE(" After tag", tag->name(),
"possible new frame=", newFrame,
"forward", forward);
if (newFrame < 0 || newFrame > m_sprite->lastFrame()) {
if (m_playMode == PlayAll) {
stop();
return false;
}
if (newFrame < 0)
newFrame = m_sprite->lastFrame();
else if (newFrame > m_sprite->lastFrame())
newFrame = 0;
}
m_frame = newFrame;
if (auto newTag = this->tag()) {
if (newTag->contains(m_frame)) {
PLAY_TRACE(" Back to tag", newTag->name(), "frame=", m_frame);
return false;
}
else {
// Now we try to decrement this tag repeat counter...
}
}
else {
// Special case where a ping-pong animation ends in the 1st
// frame and we are playing in loop mode, so starting the
// animation again should continue in the 2nd frame
if (m_playing.empty() &&
m_playMode == PlayInLoop &&
(tag->aniDir() == AniDir::PING_PONG ||
tag->aniDir() == AniDir::PING_PONG_REVERSE) &&
tag->fromFrame() == 0 &&
tag->toFrame() == m_sprite->lastFrame()) {
PLAY_TRACE(" Re-adding ping-pong tag", tag->name(), "frame=", m_frame);
addTag(tag, false, getParentForward());
return false;
}
else {
PLAY_TRACE(" Going outside the tag", tag->name(), "frame=", m_frame);
return false;
}
}
}
}
}
frame_t Playback::firstTagFrame(const Tag* tag,
const frame_t frameDelta)
{
ASSERT(tag);
ASSERT(!m_playing.empty());
int forward = m_playing.back()->forward;
return (frameDelta * forward < 0 ? tag->toFrame():
tag->fromFrame());
}
void Playback::goToFirstTagFrame(const Tag* tag,
const frame_t frameDelta)
{
ASSERT(tag);
m_frame = firstTagFrame(tag, frameDelta);
PLAY_TRACE(" Go to first frame of tag", tag->name(), "frame=", m_frame);
}
int Playback::getParentForward() const
{
if (m_playing.empty())
return 1;
else
return m_playing.back()->forward;
}
} // namespace doc

136
src/doc/playback.h Normal file
View File

@ -0,0 +1,136 @@
// Aseprite Document Library
// Copyright (C) 2021-2022 Igara Studio S.A.
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifndef DOC_PLAYBACK_H_INCLUDED
#define DOC_PLAYBACK_H_INCLUDED
#pragma once
#include "doc/frame.h"
#include "doc/tag.h"
#include "doc/tags.h"
#include <memory>
#include <set>
#include <vector>
namespace doc {
class Sprite;
class Tag;
class Playback {
public:
enum Mode {
// Play all the animation until it ends (infinite loops just
// played once). Useful to export GIF files with tags+loops.
PlayAll,
// Regular playback mode for a sprite editor when we start
// playing in no tag (infinite loops are just played once, tags
// with repeat are respected). We start from the given frame on
// Playback() ctor.
PlayInLoop,
// Play all frames ignoring tags in loop from the first frame to
// the last one.
PlayWithoutTagsInLoop,
// Play once the full sprite or the given tag from beginning to
// end (ignoring starting frame, but we back to the starting
// frame when the animation stops). Useful to play the full
// sprite/current tag in one snapshot.
PlayOnce,
// We reached the end of the playback (generally we reach this
// state after a full PlayAll or PlayOnce, and never in a
// PlayInLoop).
Stopped,
};
Playback(const Sprite* sprite,
const TagsList& tagsList,
const frame_t frame,
const Mode playMode,
const Tag* tag);
Playback(const Sprite* sprite = nullptr,
const frame_t frame = 0,
const Mode playMode = PlayAll,
const Tag* tag = nullptr);
frame_t initialFrame() const { return m_initialFrame; }
frame_t frame() const { return m_frame; }
bool isStopped() const { return m_playMode == Mode::Stopped; }
void stop();
// Sets the list of possible tags to iterate/enter when we are
// playing. By default are all the available sprite tags.
void setTags(const TagsList& tags) {
m_tags = tags;
}
// If "delta" is +1, it's the next frame, if it's -1, it's the
// previous frame, etc.
frame_t nextFrame(frame_t frameDelta = frame_t(+1));
// The tag that is being played right now (can be nullptr).
Tag* tag() const;
private:
// Information about playing tags (and inner tags)
struct PlayTag {
const Tag* tag = nullptr;
int forward = 1;
int repeat = 0;
// True if we have to go to the first tag frame when we enter to
// this PlayTag. Used for overlapped tags, e.g.
// A
// ---> B
// ---->
// 0 1 2 3
//
// The tag "B" will have rewind=true, because when we finish
// "A", we should start from the beginning of "B".
bool rewind = false;
// Indicates what PlayTag deletes this PlayTag.
PlayTag* delayedDelete = nullptr;
// This PlayTag will remove the following tags from m_played.
std::vector<const Tag*> removeThese;
PlayTag(const Tag* tag, int parentForward);
void invertForward() { forward = -forward; }
};
void handleEnterFrame(const frame_t frameDelta, const bool firstTime);
bool handleExitFrame(const frame_t frameDelta);
void handleMoveFrame(const frame_t frameDelta);
void addTag(const Tag* tag,
const bool rewind,
const int forward);
void removeLastTagFromPlayed();
bool decrementRepeat(const frame_t frameDelta);
frame_t firstTagFrame(const Tag* tag,
const frame_t frameDelta);
void goToFirstTagFrame(const Tag* tag,
const frame_t frameDelta);
int getParentForward() const;
const Sprite* m_sprite;
// List of possible tags that can be played/iterated.
TagsList m_tags;
frame_t m_initialFrame;
frame_t m_frame;
Mode m_playMode;
// Queue of tags to play and tags that are being played
std::vector<std::unique_ptr<PlayTag>> m_playing;
std::set<const Tag*> m_played;
};
} // namespace doc
#endif

540
src/doc/playback_tests.cpp Normal file
View File

@ -0,0 +1,540 @@
// Aseprite Document Library
// Copyright (c) 2021-2022 Igara Studio S.A.
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <gtest/gtest.h>
#include "doc/playback.h"
#include "doc/sprite.h"
#include "doc/tag.h"
#include "doc/tags.h"
#include <iostream>
#include <memory>
#define PLAY_TRACE(...) // TRACEARGS
using namespace doc;
namespace std {
std::ostream& operator<<(std::ostream& os,
const std::vector<doc::frame_t>& frames)
{
os << "{ ";
for (int i=0; i<int(frames.size()); ++i)
os << "[" << i << "]=" << frames[i] << (i < frames.size()-1 ? ", ": " ");
os << "} ";
return os;
}
}
static std::unique_ptr<Sprite> make_sprite(frame_t nframes,
std::vector<Tag*> tags = {})
{
std::unique_ptr<Sprite> sprite(Sprite::MakeStdSprite(ImageSpec(ColorMode::RGB, 4, 4)));
sprite->setTotalFrames(nframes);
for (auto tag : tags)
sprite->tags().add(tag);
return sprite;
}
static Tag* make_tag(const char* name, frame_t from, frame_t to, AniDir aniDir, int repeat = 0)
{
Tag* tag = new Tag(from, to);
tag->setName(name);
tag->setAniDir(aniDir);
tag->setRepeat(repeat);
return tag;
}
static void expect_frames(Playback& play,
const std::vector<frame_t>& expected)
{
std::vector<frame_t> result;
result.push_back(play.frame());
for (int i=1; i<expected.size(); ++i) {
PLAY_TRACE("[", i, "]");
result.push_back(play.nextFrame());
}
for (int i=0; i<expected.size(); ++i) {
ASSERT_EQ(expected[i], result[i])
<< "[ " << i << " ]"
<< "\n expected=" << expected
<< "\n result =" << result;
}
}
TEST(Playback, OnceFullSprite)
{
auto sprite = make_sprite(5);
Playback play(sprite.get(), 2, Playback::Mode::PlayOnce);
expect_frames(play, {0,1,2,3,4,2,2,2,2,2});
EXPECT_TRUE(play.isStopped());
}
TEST(Playback, OnceTag)
{
// A
// ---->
// 0 1 2 3 4
Tag* a = make_tag("A", 1, 3, AniDir::FORWARD);
auto sprite = make_sprite(5, { a });
Playback play(sprite.get(), 2, Playback::Mode::PlayOnce, a);
expect_frames(play, {1,2,3,2,2,2,2,2});
EXPECT_TRUE(play.isStopped());
a->setAniDir(AniDir::REVERSE);
play = Playback(sprite.get(), 2, Playback::Mode::PlayOnce, a);
expect_frames(play, {3,2,1,2,2,2,2,2});
a->setAniDir(AniDir::PING_PONG);
play = Playback(sprite.get(), 0, Playback::Mode::PlayOnce, a);
expect_frames(play, {1,2,3,2,1,0,0,0,0});
a->setAniDir(AniDir::PING_PONG_REVERSE);
play = Playback(sprite.get(), 0, Playback::Mode::PlayOnce, a);
expect_frames(play, {3,2,1,2,3,0,0,0,0});
// Just check playing the full sprite when there is a tag (the tag must be ignored)
play = Playback(sprite.get(), 2, Playback::Mode::PlayOnce);
expect_frames(play, {0,1,2,3,4,2,2,2,2});
EXPECT_TRUE(play.isStopped());
play = Playback(sprite.get(), 2, Playback::Mode::PlayWithoutTagsInLoop);
expect_frames(play, {2,3,4,0,1,2,3,4,0,1,2,3,4,0});
EXPECT_FALSE(play.isStopped());
}
TEST(Playback, LoopSprite)
{
auto sprite = make_sprite(4);
Playback play(sprite.get(), 0, Playback::Mode::PlayInLoop);
expect_frames(play, {0,1,2,3,0,1,2,3,0,1,2,3,0});
EXPECT_FALSE(play.isStopped());
}
TEST(Playback, LoopSpriteStartFromFrame2)
{
auto sprite = make_sprite(4);
Playback play(sprite.get(), 2, Playback::Mode::PlayInLoop);
expect_frames(play, {2,3,0,1,2,3,0,1,2,3,0});
EXPECT_FALSE(play.isStopped());
}
TEST(Playback, WithTagRepetitions)
{
Tag* a = make_tag("A", 1, 2, AniDir::FORWARD, 2);
auto sprite = make_sprite(4, { a });
Playback play(sprite.get(), 0, Playback::Mode::PlayInLoop);
expect_frames(play, {0,1,2,1,2,3,0,1,2,1,2,3,0});
EXPECT_FALSE(play.isStopped());
play = Playback(sprite.get(), 0, Playback::Mode::PlayAll);
expect_frames(play, {0,1,2,1,2,3,0,0,0});
EXPECT_TRUE(play.isStopped());
}
TEST(Playback, LoopTagInfinite)
{
Tag* a = make_tag("A", 1, 2, AniDir::FORWARD, 0);
auto sprite = make_sprite(4, { a });
Playback play(sprite.get(), 0, Playback::Mode::PlayInLoop, a);
expect_frames(play, {0,1,2,1,2,1,2,1,2});
EXPECT_FALSE(play.isStopped());
}
TEST(Playback, LoopTagInfiniteAndFinite)
{
// A
// -->
// 0 1 2 3
Tag* a = make_tag("A", 1, 2, AniDir::FORWARD, 2);
auto sprite = make_sprite(4, { a });
Playback play(sprite.get(), 2, Playback::Mode::PlayInLoop, a);
expect_frames(play, {2,1,2,1,2,1,2});
EXPECT_FALSE(play.isStopped());
// This is not infinite because the tag is not specified in the
// Playback() ctor.
play = Playback(sprite.get(), 2, Playback::Mode::PlayInLoop);
expect_frames(play, {2,1,2,3,0,1,2,1,2,3,0});
EXPECT_FALSE(play.isStopped());
}
TEST(Playback, SimpleForward)
{
// A
// -->
// 0 1
Tag* a = make_tag("A", 0, 1, AniDir::FORWARD, 2);
auto sprite = make_sprite(2, { a });
Playback play(sprite.get(), 0, Playback::Mode::PlayInLoop);
expect_frames(play, {0,1,0,1,0,1,0,1,0,1});
EXPECT_FALSE(play.isStopped());
}
TEST(Playback, SimpleLoopBug)
{
// Loop
// -->
// 0 1 2 3
Tag* loop = make_tag("Loop", 1, 2, AniDir::FORWARD, 0);
auto sprite = make_sprite(4, { loop });
Playback play(sprite.get(), 0, Playback::Mode::PlayInLoop);
expect_frames(play, {0,1,2,3,0,1,2,3,0});
EXPECT_FALSE(play.isStopped());
play = Playback(sprite.get(), 0, Playback::Mode::PlayInLoop, loop);
expect_frames(play, {0,1,2,1,2,1,2,1,2});
EXPECT_FALSE(play.isStopped());
// Here we detected a bug where the playback kept playing 3,4,5,6,etc.
play = Playback(sprite.get(), 3, Playback::Mode::PlayInLoop, loop);
expect_frames(play, {3,0,1,2,1,2,1,2,1,2});
EXPECT_FALSE(play.isStopped());
}
TEST(Playback, TwoSimpleForwards)
{
// A
// -->
// B
// -->
// 0 1
Tag* a = make_tag("A", 0, 1, AniDir::FORWARD, 2);
Tag* b = make_tag("B", 0, 1, AniDir::FORWARD, 2);
auto sprite = make_sprite(2, { a, b });
Playback play(sprite.get(), 0, Playback::Mode::PlayInLoop);
expect_frames(play, {0,1,0,1,0,1,0,1,0,1});
EXPECT_FALSE(play.isStopped());
}
TEST(Playback, SimplePingPong2)
{
// A
// <->
// 0 1
Tag* a = make_tag("A", 0, 1, AniDir::PING_PONG, 2);
auto sprite = make_sprite(2, { a });
Playback play(sprite.get(), 0, Playback::Mode::PlayInLoop);
expect_frames(play, {0,1,0, 0,1,0, 0,1,0, 0,1,0});
EXPECT_FALSE(play.isStopped());
}
TEST(Playback, SimplePingPong3)
{
// A
// <->
// 0 1
Tag* a = make_tag("A", 0, 1, AniDir::PING_PONG, 3);
auto sprite = make_sprite(2, { a });
Playback play(sprite.get(), 0, Playback::Mode::PlayInLoop);
expect_frames(play, {0,1,0,1,0,1,0,1,0,1,0,1,0,1});
EXPECT_FALSE(play.isStopped());
}
TEST(Playback, SimplePingPong3Repeats)
{
// A
// <--->
// 0 1 2
Tag* a = make_tag("A", 0, 2, AniDir::PING_PONG, 3);
auto sprite = make_sprite(3, { a });
Playback play(sprite.get(), 0, Playback::Mode::PlayInLoop);
expect_frames(play, {0,1,2,1,0,1,2,
0,1,2,1,0,1,2});
EXPECT_FALSE(play.isStopped());
}
TEST(Playback, TagOneFrame)
{
// A
// ->
// 0 1
Tag* tagA = make_tag("A", 0, 0, AniDir::FORWARD, 2);
auto sprite = make_sprite(2, { tagA });
Playback play(sprite.get(), 1, Playback::Mode::PlayInLoop);
expect_frames(play, {1,0,0,1,0,0,1,0,0,1});
EXPECT_FALSE(play.isStopped());
}
TEST(Playback, FourTags)
{
// A B C D
// --> <---- <---> >------<
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13
Tag* a = make_tag("A", 1, 2, AniDir::FORWARD, 1);
Tag* b = make_tag("B", 3, 5, AniDir::REVERSE, 2);
Tag* c = make_tag("C", 6, 8, AniDir::PING_PONG, 3);
Tag* d = make_tag("D", 10, 12, AniDir::PING_PONG_REVERSE, 2);
auto sprite = make_sprite(14, { a, b, c, d });
Playback play(sprite.get(), 0, Playback::Mode::PlayWithoutTagsInLoop);
expect_frames(play, {0,1,2,3,4,5,6,7,8,9,10,11,12,13,0,1,2,3,4,5,6,7,8,9,10,11,12,13,0});
EXPECT_FALSE(play.isStopped());
play = Playback(sprite.get(), 0, Playback::Mode::PlayAll);
expect_frames(play, {0, 1,2, 5,4,3,5,4,3, 6,7,8,7,6,7,8, 9, 12,11,10,11,12, 13,0,0,0,0});
EXPECT_TRUE(play.isStopped());
}
TEST(Playback, ForwardTagWithInnerPingPong)
{
// A
// -------->
// B
// <--->
// 0 1 2 3 4 5 6
Tag* tagA = make_tag("A", 1, 5, AniDir::FORWARD, 2);
Tag* tagB = make_tag("B", 2, 4, AniDir::PING_PONG, 3);
auto sprite = make_sprite(7, { tagA, tagB });
Playback play(sprite.get(), 0, Playback::Mode::PlayInLoop);
expect_frames(play, {0, 1,2,3,4,3,2,3,4,5, 1,2,3,4,3,2,3,4,5, 6,
0, 1,2,3,4,3,2,3,4,5, 1,2,3,4,3,2,3,4,5, 6 });
EXPECT_FALSE(play.isStopped());
}
TEST(Playback, ForwardTagWithInnerForwardEndSameFrame)
{
// A
// ------>
// B
// ---->
// 0 1 2 3 4
Tag* tagA = make_tag("A", 1, 4, AniDir::FORWARD, 2);
Tag* tagB = make_tag("B", 2, 4, AniDir::FORWARD, 2);
auto sprite = make_sprite(5, { tagA, tagB });
Playback play(sprite.get(), 0, Playback::Mode::PlayInLoop);
expect_frames(play, {0, 1,2,3,4,2,3,4, 1,2,3,4,2,3,4,
0, 1,2,3,4,2,3,4, 1,2,3,4,2,3,4, 0 });
EXPECT_FALSE(play.isStopped());
}
TEST(Playback, ForwardTagWithInnerPingPongEndSameFrame)
{
// A
// ---->
// B
// <--->
// 0 1 2 3
Tag* tagA = make_tag("A", 1, 3, AniDir::FORWARD, 2);
Tag* tagB = make_tag("B", 1, 3, AniDir::PING_PONG, 4);
auto sprite = make_sprite(4, { tagA, tagB });
Playback play(sprite.get(), 0, Playback::Mode::PlayInLoop);
expect_frames(play, {0, 1,2,3,2,1,2,3,2,1, 1,2,3,2,1,2,3,2,1,
0, 1,2,3,2,1,2,3,2,1, 1,2,3,2,1,2,3,2,1, 0 });
EXPECT_FALSE(play.isStopped());
}
TEST(Playback, ForwardTagWithInnerReverse)
{
// A
// ------>
// B
// <----
// 0 1 2 3 4
Tag* tagA = make_tag("A", 1, 4, AniDir::FORWARD, 2);
Tag* tagB = make_tag("B", 1, 3, AniDir::REVERSE, 2);
auto sprite = make_sprite(5, { tagA, tagB });
Playback play(sprite.get(), 0, Playback::Mode::PlayInLoop);
expect_frames(play, {0, 3,2,1,3,2,1, 4, 3,2,1,3,2,1, 4,
0, 3,2,1,3,2,1, 4, 3,2,1,3,2,1, 4, 0 });
EXPECT_FALSE(play.isStopped());
}
TEST(Playback, PingPongWithInnerReverse)
{
// A
// <------->
// B
// <----
// 0 1 2 3 4
Tag* tagA = make_tag("A", 0, 4, AniDir::PING_PONG, 2);
Tag* tagB = make_tag("B", 1, 3, AniDir::REVERSE, 3);
auto sprite = make_sprite(5, { tagA, tagB });
Playback play(sprite.get(), 0, Playback::Mode::PlayInLoop);
expect_frames(play, {0, 3,2,1,3,2,1,3,2,1, 4, 1,2,3,1,2,3,1,2,3, 0 });
EXPECT_FALSE(play.isStopped());
}
TEST(Playback, OnePingPongInsideOther)
{
// A
// <------->
// B
// >---<
// 0 1 2 3 4
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 });
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_FALSE(play.isStopped());
}
TEST(Playback, OnePingPongInsideOther3)
{
// A
// <------->
// B
// >---<
// 0 1 2 3 4
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 });
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_FALSE(play.isStopped());
}
TEST(Playback, TwoLoopsInCascade)
{
// A
// ---->
// B
// ---->
// 0 1 2 3 4
Tag* tagA = make_tag("A", 1, 3, AniDir::FORWARD, 2);
Tag* tagB = make_tag("B", 2, 4, AniDir::FORWARD, 2);
auto sprite = make_sprite(5, { tagA, tagB });
Playback play(sprite.get(), 0, Playback::Mode::PlayInLoop);
expect_frames(play, {0, 1,2,3,1,2,3, 2,3,4,2,3,4,
0, 1,2,3,1,2,3, 2,3,4,2,3,4, 0 });
EXPECT_FALSE(play.isStopped());
}
TEST(Playback, TwoLoopsInCascadeReverse)
{
GTEST_SKIP() << "TODO not yet ready";
// 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::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,4,3,2,
0, 3,2,1,3,2,1, 4,3,2,4,3,2, 0 });
EXPECT_FALSE(play.isStopped());
}
TEST(Playback, ThreeLoopsInCascade)
{
// A
// ---->
// B
// ---->
// C
// ---->
// 0 1 2 3 4 5
Tag* a = make_tag("A", 1, 3, AniDir::FORWARD, 2);
Tag* b = make_tag("B", 2, 4, AniDir::FORWARD, 2);
Tag* c = make_tag("C", 3, 5, AniDir::FORWARD, 2);
auto sprite = make_sprite(6, { a, b, c });
Playback play(sprite.get(), 0, Playback::Mode::PlayInLoop);
expect_frames(play, {0, 1,2,3,1,2,3, 2,3,4,2,3,4, 3,4,5,3,4,5,
0, 1,2,3,1,2,3, 2,3,4,2,3,4, 3,4,5,3,4,5, 0});
EXPECT_FALSE(play.isStopped());
}
TEST(Playback, ThreeLoopsInCascadeDiffAniDirs)
{
// A
// ---->
// B
// <----
// C
// <--->
// 0 1 2 3 4 5 6
Tag* tagA = make_tag("A", 1, 3, AniDir::FORWARD, 2);
Tag* tagB = make_tag("B", 2, 4, AniDir::REVERSE, 2);
Tag* tagC = make_tag("C", 3, 5, AniDir::PING_PONG, 2);
auto sprite = make_sprite(7, { tagA, tagB, tagC });
Playback play(sprite.get(), 0, Playback::Mode::PlayInLoop);
expect_frames(play, {0, 1,2,3,1,2,3, 4,3,2,4,3,2, 3,4,5,4,3, 6,
0, 1,2,3,1,2,3, 4,3,2,4,3,2, 3,4,5,4,3, 6, 0});
EXPECT_FALSE(play.isStopped());
}
TEST(Playback, InnerCascades)
{
GTEST_SKIP() << "TODO not yet ready";
// A
// <--------->
// B
// <----
// C
// <--->
// 0 1 2 3 4 5 6
Tag* a = make_tag("A", 1, 6, AniDir::PING_PONG, 2);
Tag* b = make_tag("B", 2, 4, AniDir::REVERSE, 2);
Tag* c = make_tag("C", 3, 5, AniDir::PING_PONG, 2);
auto sprite = make_sprite(7, { a, b, c });
Playback play(sprite.get(), 0, Playback::Mode::PlayInLoop);
expect_frames(play, {0, 1, 4,3,2,4,3,2, 3,4,5,4,3, 6, 5,4,3,4,5, 2,3,4,2,3,4, 1,
0, 1, 4,3,2,4,3,2, 3,4,5,4,3, 6, 5,4,3,4,5, 2,3,4,2,3,4, 1, 0});
EXPECT_FALSE(play.isStopped());
}
int main(int argc, char** argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (C) 2019-2020 Igara Studio S.A.
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2001-2016 David Capello
//
// This file is released under the terms of the MIT license.
@ -14,6 +14,8 @@
#include "base/debug.h"
#include "doc/tags.h"
#include <algorithm>
namespace doc {
Tag::Tag(frame_t from, frame_t to)
@ -22,7 +24,6 @@ Tag::Tag(frame_t from, frame_t to)
, m_from(from)
, m_to(to)
, m_name("Tag")
, m_aniDir(AniDir::FORWARD)
{
color_t defaultColor = rgba_a_mask;// black color with full opacity.
userData().setColor(defaultColor);
@ -35,6 +36,7 @@ Tag::Tag(const Tag& other)
, m_to(other.m_to)
, m_name(other.m_name)
, m_aniDir(other.m_aniDir)
, m_repeat(other.m_repeat)
{
}
@ -83,9 +85,15 @@ void Tag::setAniDir(AniDir aniDir)
{
ASSERT(m_aniDir == AniDir::FORWARD ||
m_aniDir == AniDir::REVERSE ||
m_aniDir == AniDir::PING_PONG);
m_aniDir == AniDir::PING_PONG ||
m_aniDir == AniDir::PING_PONG_REVERSE);
m_aniDir = aniDir;
}
void Tag::setRepeat(int repeat)
{
m_repeat = std::clamp(repeat, 0, kMaxRepeat);
}
} // namespace doc

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (C) 2019-2020 Igara Studio S.A.
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2001-2016 David Capello
//
// This file is released under the terms of the MIT license.
@ -23,6 +23,8 @@ namespace doc {
class Tag : public WithUserData {
public:
static constexpr int kMaxRepeat = 65535;
Tag(frame_t from, frame_t to);
Tag(const Tag& other);
~Tag();
@ -35,19 +37,27 @@ namespace doc {
const std::string& name() const { return m_name; }
color_t color() const { return userData().color(); }
AniDir aniDir() const { return m_aniDir; }
int repeat() const { return m_repeat; }
void setFrameRange(frame_t from, frame_t to);
void setName(const std::string& name);
void setColor(color_t color);
void setAniDir(AniDir aniDir);
void setRepeat(int repeat);
void setOwner(Tags* owner);
bool contains(const frame_t frame) const {
return (frame >= m_from &&
frame <= m_to);
}
public:
Tags* m_owner;
frame_t m_from, m_to;
std::string m_name;
AniDir m_aniDir;
AniDir m_aniDir = AniDir::FORWARD;
int m_repeat = 0;
// Disable operator=
Tag& operator=(Tag&);

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (C) 2019-2020 Igara Studio S.A.
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This file is released under the terms of the MIT license.
@ -34,6 +34,7 @@ void write_tag(std::ostream& os, const Tag* tag)
write8(os, (int)tag->aniDir());
write_string(os, tag->name());
write_user_data(os, tag->userData());
write32(os, tag->repeat());
}
Tag* read_tag(std::istream& is,
@ -54,16 +55,21 @@ Tag* read_tag(std::istream& is,
UserData userData;
// If we are reading the new v1.3.x version, there is a user data with the color + text
if (!oldVersion)
int repeat = 0;
if (!oldVersion) {
userData = read_user_data(is);
repeat = read32(is);
}
std::unique_ptr<Tag> tag(new Tag(from, to));
auto tag = std::make_unique<Tag>(from, to);
tag->setAniDir(aniDir);
tag->setName(name);
if (oldVersion)
tag->setColor(color);
else
else {
tag->setUserData(userData);
tag->setRepeat(repeat);
}
if (setId)
tag->setId(id);
return tag.release();

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2001-2015 David Capello
//
// This file is released under the terms of the MIT license.
@ -21,12 +21,12 @@ namespace doc {
class Tag;
class Sprite;
class Tags {
typedef std::vector<Tag*> List;
using TagsList = std::vector<Tag*>;
class Tags {
public:
typedef List::iterator iterator;
typedef List::const_iterator const_iterator;
using iterator = TagsList::iterator;
using const_iterator = TagsList::const_iterator;
Tags(Sprite* sprite);
~Tags();
@ -50,9 +50,11 @@ namespace doc {
Tag* innerTag(const frame_t frame) const;
Tag* outerTag(const frame_t frame) const;
const TagsList& getInternalList() const { return m_tags; }
private:
Sprite* m_sprite;
List m_tags;
TagsList m_tags;
DISABLE_COPYING(Tags);
};

View File

@ -14,9 +14,9 @@
#include "doc/blend_internals.h"
#include "doc/blend_mode.h"
#include "doc/doc.h"
#include "doc/handle_anidir.h"
#include "doc/image_impl.h"
#include "doc/layer_tilemap.h"
#include "doc/playback.h"
#include "doc/tileset.h"
#include "doc/tilesets.h"
#include "gfx/clip.h"
@ -854,21 +854,16 @@ void Render::renderOnionskin(
Tag* loop = m_onionskin.loopTag();
Layer* onionLayer = (m_onionskin.layer() ? m_onionskin.layer():
m_sprite->root());
frame_t frameIn;
Playback play(m_sprite, frame,
loop ? Playback::PlayInLoop:
Playback::PlayOnce,
loop);
play.nextFrame(-m_onionskin.prevFrames());
for (frame_t frameOut = frame - m_onionskin.prevFrames();
frameOut <= frame + m_onionskin.nextFrames();
++frameOut) {
if (loop) {
bool pingPongForward = true;
frameIn =
calculate_next_frame(m_sprite,
frame, frameOut - frame,
loop, pingPongForward);
}
else {
frameIn = frameOut;
}
++frameOut, play.nextFrame()) {
const frame_t frameIn = play.frame();
if (frameIn == frame ||
frameIn < 0 ||