mirror of
https://github.com/aseprite/aseprite.git
synced 2025-03-01 01:13:40 +00:00
parent
17a5b3f3fc
commit
4f96d37b1f
@ -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
|
||||
|
@ -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" />
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
38
src/app/cmd/set_tag_repeat.cpp
Normal file
38
src/app/cmd/set_tag_repeat.cpp
Normal 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
|
38
src/app/cmd/set_tag_repeat.h
Normal file
38
src/app/cmd/set_tag_repeat.h
Normal 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
|
@ -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) {
|
||||
|
@ -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());
|
||||
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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() << " }";
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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
|
@ -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
500
src/doc/playback.cpp
Normal 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
136
src/doc/playback.h
Normal 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
540
src/doc/playback_tests.cpp
Normal 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();
|
||||
}
|
@ -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
|
||||
|
@ -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&);
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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 ||
|
||||
|
Loading…
x
Reference in New Issue
Block a user