blob: 623c2abaed86773f2a9d6af8edbd28fddea3e54c [file]
#include "rive/animation/linear_animation_instance.hpp"
#include "rive/animation/interpolating_keyframe.hpp"
#include "rive/animation/linear_animation.hpp"
#include "rive/animation/loop.hpp"
#include "rive/animation/keyed_callback_reporter.hpp"
#include "rive/assets/script_asset.hpp"
#include "rive/data_bind/data_bind.hpp"
#include "rive/profiler/profiler_macros.h"
#include "rive/scripted/scripted_interpolator.hpp"
#include <cmath>
#include <cassert>
using namespace rive;
LinearAnimationInstance::LinearAnimationInstance(
const LinearAnimation* animation,
ArtboardInstance* instance,
float speedMultiplier) :
Scene(instance),
m_animation((assert(animation != nullptr), animation)),
m_time((speedMultiplier >= 0) ? animation->startTime()
: animation->endTime()),
m_speedDirection((speedMultiplier >= 0) ? 1 : -1),
m_totalTime(0.0f),
m_lastTotalTime(0.0f),
m_spilledTime(0.0f),
m_direction(1)
{}
LinearAnimationInstance::LinearAnimationInstance(
LinearAnimationInstance const& lhs) :
Scene(lhs),
m_animation(lhs.m_animation),
m_time(lhs.m_time),
m_speedDirection(lhs.m_speedDirection),
m_totalTime(lhs.m_totalTime),
m_lastTotalTime(lhs.m_lastTotalTime),
m_spilledTime(lhs.m_spilledTime),
m_direction(lhs.m_direction),
m_didLoop(lhs.m_didLoop),
m_loopValue(lhs.m_loopValue)
{}
LinearAnimationInstance::~LinearAnimationInstance()
{
// Critical teardown order, mirroring the SMI pattern at
// state_machine_instance.cpp:2011-2044: pull cloned data binds out of
// the artboard and delete them BEFORE m_scriptedInterpolatorInstances
// destroys the clones whose CustomPropertys are those binds' targets.
// Without this, the next Artboard::updateDataBinds() (which runs every
// frame from updatePass) reads through DataBind::target() into freed
// memory.
if (m_artboardInstance != nullptr)
{
for (auto* bind : m_clonedArtboardDataBinds)
{
m_artboardInstance->removeDataBind(bind);
delete bind;
}
}
m_clonedArtboardDataBinds.clear();
// m_scriptedInterpolatorInstances destructs here via unique_ptr; safe now
// that no DataBind still points at the clones' CustomPropertys.
}
// Returns a per-(this LAI, keyframe) stateful clone of the given shared
// ScriptedInterpolator. Mirrors StateMachineInstance::scriptedObjects but
// keyed by the keyframe pointer so distinct keyframes that share a script
// each get their own Lua `self` table. Lazily allocates the map and the
// clone on first hit; subsequent calls return the cached clone. Cleanup
// runs in ~LinearAnimationInstance via unique_ptr.
ScriptedInterpolator* LinearAnimationInstance::statefulInterpolator(
const InterpolatingKeyFrame* keyframe,
const ScriptedInterpolator* shared) const
{
if (shared == nullptr || keyframe == nullptr)
{
return nullptr;
}
if (m_scriptedInterpolatorInstances == nullptr)
{
m_scriptedInterpolatorInstances = std::make_unique<
std::unordered_map<const InterpolatingKeyFrame*,
std::unique_ptr<ScriptedInterpolator>>>();
}
auto& map = *m_scriptedInterpolatorInstances;
auto it = map.find(keyframe);
if (it != map.end())
{
return it->second.get();
}
// cloneScriptedObject is non-const on the source; the shared template is
// logically read-only here so the const_cast is safe and matches the
// ScriptedListenerAction pattern.
auto* cloneAsObj =
const_cast<ScriptedInterpolator*>(shared)->cloneScriptedObject(
m_artboardInstance);
if (cloneAsObj == nullptr)
{
return nullptr;
}
auto* clone = static_cast<ScriptedInterpolator*>(cloneAsObj);
clone->dataContext(m_artboardInstance->dataContext());
// Walk the clone's ScriptInputs to discover the binds cloneProperties
// just parked on the artboard for this clone. The back-pointer is set
// inside cloneProperties (scripted_object.cpp) immediately after
// dataBindContainer->addDataBind(...). We track these so ~LAI can
// removeDataBind + delete each one BEFORE the clone — whose
// CustomProperty is the bind's target — is destroyed.
for (auto* prop : clone->customProperties())
{
if (auto* input = ScriptInput::from(prop))
{
if (auto* bind = input->dataBind())
{
m_clonedArtboardDataBinds.push_back(bind);
}
}
}
// Late-binding safety net: reinit() inside cloneScriptedObject is a no-op
// when scriptAsset() is null at clone time. If the asset became available
// later (editor live-edit), init now. ensureScriptInitialized
// short-circuits when already initialized so this stays cheap.
if (clone->scriptAsset() != nullptr && !clone->userLuaInitDone())
{
clone->scriptAsset()->initScriptedObject(clone);
clone->hydrateScriptInputs();
}
auto* raw = clone;
map.emplace(keyframe, std::unique_ptr<ScriptedInterpolator>(clone));
return raw;
}
bool LinearAnimationInstance::advanceAndApply(float seconds)
{
RIVE_PROF_SCOPE_L(1)
bool more = this->advance(seconds, this);
this->apply();
if (m_artboardInstance->advance(seconds))
{
more = true;
}
return more || keepGoing();
}
bool LinearAnimationInstance::advance(float elapsedSeconds,
KeyedCallbackReporter* reporter)
{
const LinearAnimation& animation = *m_animation;
float deltaSeconds = elapsedSeconds * animation.speed() * m_direction;
m_spilledTime = 0.0f;
if (deltaSeconds == 0)
{
m_didLoop = false;
return false;
}
m_lastTotalTime = m_totalTime;
m_totalTime += std::abs(deltaSeconds);
// NOTE:
// do not track spilled time, if our one shot loop is already completed.
// stop gap before we move spilled tracking into state machine logic.
bool killSpilledTime = !this->keepGoing(elapsedSeconds);
float lastTime = m_time;
m_time += deltaSeconds;
if (reporter != nullptr)
{
animation.reportKeyedCallbacks(reporter,
lastTime,
m_time,
m_speedDirection,
false);
}
float fps = (float)animation.fps();
float frames = m_time * fps;
float start =
animation.enableWorkArea() ? (float)animation.workStart() : 0.0f;
float end = animation.enableWorkArea() ? (float)animation.workEnd()
: (float)animation.duration();
float range = end - start;
bool didLoop = false;
// this has some issues when deltaSeconds is 0,
// right now we basically assume we default to going forwards in that case
//
int direction = deltaSeconds < 0 ? -1 : 1;
switch (loop())
{
case Loop::oneShot:
if (direction == 1 && frames > end)
{
// Account for the time dilation or contraction applied in the
// animation local time by its speed to calculate spilled time.
// Calculate the ratio of the time excess by the total elapsed
// time in local time (deltaFrames) and multiply the elapsed
// time by it.
auto deltaFrames = deltaSeconds * fps;
auto spilledFramesRatio = (frames - end) / deltaFrames;
m_spilledTime = spilledFramesRatio * elapsedSeconds;
frames = (float)end;
m_time = frames / fps;
didLoop = true;
}
else if (direction == -1 && frames < start)
{
auto deltaFrames = std::abs(deltaSeconds * fps);
auto spilledFramesRatio = (start - frames) / deltaFrames;
m_spilledTime = spilledFramesRatio * elapsedSeconds;
frames = (float)start;
m_time = frames / fps;
didLoop = true;
}
break;
case Loop::loop:
if (direction == 1 && frames >= end)
{
// How spilled time has to be calculated, given that local time
// can be scaled to a factor of the regular time:
// - for convenience, calculate the local elapsed time in frames
// (deltaFrames)
// - get the remainder of current frame position (frames) by
// duration (range)
// - use that remainder as the ratio of the original time that
// was not consumed by the loop (spilledFramesRatio)
// - multiply the original elapsedTime by the ratio to set the
// spilled time
auto deltaFrames = deltaSeconds * fps;
auto remainder = std::fmod(frames - start, (float)range);
auto spilledFramesRatio = remainder / deltaFrames;
m_spilledTime = spilledFramesRatio * elapsedSeconds;
frames = start + remainder;
m_time = frames / fps;
didLoop = true;
if (reporter != nullptr)
{
animation.reportKeyedCallbacks(reporter,
0.0f,
m_time,
m_speedDirection,
false);
}
}
else if (direction == -1 && frames <= start)
{
auto deltaFrames = deltaSeconds * fps;
auto remainder =
std::abs(std::fmod(start - frames, (float)range));
auto spilledFramesRatio = std::abs(remainder / deltaFrames);
m_spilledTime = spilledFramesRatio * elapsedSeconds;
frames = end - remainder;
m_time = frames / fps;
didLoop = true;
if (reporter != nullptr)
{
animation.reportKeyedCallbacks(reporter,
end / (float)fps,
m_time,
m_speedDirection,
false);
}
}
break;
case Loop::pingPong:
bool fromPong = true;
while (true)
{
if (direction == 1 && frames >= end)
{
m_spilledTime = (frames - end) / fps;
frames = end + (end - frames);
lastTime = end / (float)fps;
}
else if (direction == -1 && frames < start)
{
m_spilledTime = (start - frames) / fps;
frames = start + (start - frames);
lastTime = start / (float)fps;
}
else
{
// we're within the range, we can stop fixing. We do this in
// a loop to fix conditions when time has advanced so far
// that we've ping-ponged back and forth a few times in a
// single frame. We want to accomodate for this in cases
// where animations are not advanced on regular intervals.
break;
}
m_time = frames / fps;
m_direction *= -1;
direction *= -1;
didLoop = true;
if (reporter != nullptr)
{
animation.reportKeyedCallbacks(reporter,
lastTime,
m_time,
m_speedDirection,
fromPong);
}
fromPong = !fromPong;
}
break;
}
if (killSpilledTime)
{
m_spilledTime = 0;
}
m_didLoop = didLoop;
return this->keepGoing(elapsedSeconds);
}
void LinearAnimationInstance::time(float value)
{
if (m_time == value)
{
return;
}
m_time = value;
// Make sure to keep last and total in relative lockstep so state machines
// can track change even when setting time.
auto diff = m_totalTime - m_lastTotalTime;
float start =
(m_animation->enableWorkArea() ? (float)m_animation->workStart()
: 0.0f) *
(float)m_animation->fps();
m_totalTime = value - start;
m_lastTotalTime = m_totalTime - diff;
// leaving this RIGHT now. but is this required? it kinda messes up
// playing things backwards and seeking. what purpose does it solve?
m_direction = 1;
}
void LinearAnimationInstance::reset(float speedMultiplier = 1.0)
{
m_time = (speedMultiplier >= 0) ? m_animation->startTime()
: m_animation->endTime();
}
uint32_t LinearAnimationInstance::fps() const { return m_animation->fps(); }
uint32_t LinearAnimationInstance::duration() const
{
return m_animation->duration();
}
float LinearAnimationInstance::speed() const { return m_animation->speed(); }
float LinearAnimationInstance::startTime() const
{
return m_animation->startTime();
}
std::string LinearAnimationInstance::name() const
{
return m_animation->name();
}
bool LinearAnimationInstance::isTranslucent() const
{
return m_artboardInstance->isTranslucent(this);
}
// Returns either the animation's default or overridden loop values
int LinearAnimationInstance::loopValue() const
{
if (m_loopValue != -1)
{
return m_loopValue;
}
return m_animation->loopValue();
}
// Override the animation's loop value
void LinearAnimationInstance::loopValue(int value)
{
if (m_loopValue == value)
{
return;
}
if (m_loopValue == -1 && m_animation->loopValue() == value)
{
return;
}
m_loopValue = value;
}
float LinearAnimationInstance::durationSeconds() const
{
return m_animation->durationSeconds();
}
void LinearAnimationInstance::reportEvent(Event* event, float secondsDelay)
{
const std::vector<Event*> events{event};
notifyListeners(events);
}