blob: a2debece223486b104b2568836b385520a1ddd85 [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/animation/keyframe.hpp"
#include "rive/animation/keyframe_double.hpp"
#include "rive/animation/keyframe_color.hpp"
#include "rive/animation/keyframe_bool.hpp"
#include "rive/animation/keyframe_string.hpp"
#include "rive/artboard.hpp"
#include "rive/assets/script_asset.hpp"
#include "rive/data_bind/data_bind.hpp"
#include "rive/data_bind/bindable_property.hpp"
#include "rive/data_bind/bindable_property_number.hpp"
#include "rive/data_bind/bindable_property_color.hpp"
#include "rive/data_bind/bindable_property_boolean.hpp"
#include "rive/data_bind/bindable_property_string.hpp"
#include "rive/data_bind/converters/data_converter.hpp"
#include "rive/profiler/profiler_macros.h"
#include "rive/scripted/scripted_interpolator.hpp"
#include "rive/animation/linear_animation_instance_extras.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_noKeyFrameBinds(lhs.m_noKeyFrameBinds)
, m_loopValue(lhs.m_loopValue)
{}
LinearAnimationInstance::~LinearAnimationInstance()
{
if (m_bindingExtras == nullptr)
{
return;
}
auto& extras = *m_bindingExtras;
// Critical teardown order, mirroring the SMI pattern at
// state_machine_instance.cpp:2011-2044: every cloned data bind must leave
// the artboard and be deleted BEFORE the object owning its target dies.
// Without this, the next Artboard::updateDataBinds() (which runs every
// frame from updatePass) reads through DataBind::target() into freed
// memory.
//
// The containers share one struct, so declaration order alone would
// decide this. Do it explicitly instead — the ordering is load bearing and
// must not be silently broken by reordering a field.
//
// ~Artboard deletes m_Objects — which own the LAIs (nested animations,
// joysticks) — before deleteDataBinds(), so the artboard's bind list is
// still valid here.
//
// Each StatefulScriptedInterpolator removes + deletes the binds it parked
// on the artboard before it destroys its clone.
extras.scriptedInterpolators.clear();
if (m_artboardInstance != nullptr)
{
// Keyframe value bind clones target the holders below, so remove +
// delete them from the artboard BEFORE freeing the holders.
for (auto& pair : extras.keyFrameValueBinds)
{
m_artboardInstance->removeDataBind(pair.second);
delete pair.second;
}
}
extras.keyFrameValueBinds.clear();
// Keyframe value holders are owned here; safe to delete now that the clones
// targeting them were removed above.
for (auto& pair : extras.keyFrameValueHolders)
{
delete pair.second;
}
extras.keyFrameValueHolders.clear();
m_bindingExtras.reset();
}
// The BindableProperty value property key matching a keyframe's value type, or
// 0 for unsupported keyframe types (e.g. id/uint), which are left unbound.
static uint32_t keyFrameHolderPropertyKey(uint16_t keyFrameType)
{
switch (keyFrameType)
{
case KeyFrameDoubleBase::typeKey:
return BindablePropertyNumberBase::propertyValuePropertyKey;
case KeyFrameColorBase::typeKey:
return BindablePropertyColorBase::propertyValuePropertyKey;
case KeyFrameBoolBase::typeKey:
return BindablePropertyBooleanBase::propertyValuePropertyKey;
case KeyFrameStringBase::typeKey:
return BindablePropertyStringBase::propertyValuePropertyKey;
default:
return 0;
}
}
// Creates the BindableProperty holder matching a keyframe's value type.
static BindableProperty* makeKeyFrameValueHolder(uint16_t keyFrameType)
{
switch (keyFrameType)
{
case KeyFrameDoubleBase::typeKey:
return new BindablePropertyNumber();
case KeyFrameColorBase::typeKey:
return new BindablePropertyColor();
case KeyFrameBoolBase::typeKey:
return new BindablePropertyBoolean();
case KeyFrameStringBase::typeKey:
return new BindablePropertyString();
default:
return nullptr;
}
}
LAIBindingExtras& LinearAnimationInstance::ensureBindingExtras() const
{
if (m_bindingExtras == nullptr)
{
m_bindingExtras = std::make_unique<LAIBindingExtras>();
}
return *m_bindingExtras;
}
#ifdef TESTING
uint64_t LinearAnimationInstance::sm_keyFrameHolderResolves = 0;
#endif
BindableProperty* LinearAnimationInstance::resolveKeyFrameValueHolder(
const KeyFrame* keyframe) const
{
#ifdef TESTING
sm_keyFrameHolderResolves++;
#endif
// Already resolved for this LAI (holder cached).
if (m_bindingExtras != nullptr)
{
auto& holders = m_bindingExtras->keyFrameValueHolders;
auto it = holders.find(keyframe);
if (it != holders.end())
{
// Refresh the holder from its source now (if the source changed
// this frame) so the value is current at read time, regardless of
// where the batched artboard updateDataBinds() falls relative to
// the animation apply that's calling us. No-op when the bind isn't
// dirty.
auto& binds = m_bindingExtras->keyFrameValueBinds;
auto bindIt = binds.find(keyframe);
if (bindIt != binds.end() && m_artboardInstance != nullptr)
{
m_artboardInstance->flushDataBind(bindIt->second);
}
return it->second;
}
}
// Lazily resolve from the source artboard's keyframe data binds. The gate
// keeps playback in files without any keyframe binds free of per-keyframe
// work (hasKeyFrameSourceBinds() is a cached O(1) check).
if (m_artboardInstance == nullptr)
{
return nullptr;
}
const Artboard* source = m_artboardInstance->artboardSource();
if (source == nullptr || !source->hasKeyFrameSourceBinds())
{
#ifndef WITH_RIVE_EDITOR
m_noKeyFrameBinds = true;
#endif
return nullptr;
}
DataBind* sourceBind = source->keyFrameSourceBind(keyframe);
if (sourceBind == nullptr)
{
return nullptr;
}
return buildKeyFrameValueHolder(keyframe, sourceBind);
}
BindableProperty* LinearAnimationInstance::buildKeyFrameValueHolder(
const KeyFrame* keyframe,
DataBind* sourceBind) const
{
uint32_t propertyKey = keyFrameHolderPropertyKey(keyframe->coreType());
if (propertyKey == 0)
{
return nullptr;
}
BindableProperty* holder = makeKeyFrameValueHolder(keyframe->coreType());
ensureBindingExtras().keyFrameValueHolders[keyframe] = holder;
// Clone the source bind, retarget it at the per-instance holder, and park
// it on the artboard's data-bind container so it's advanced each frame.
// addDataBind primes the holder synchronously when the data context is set.
auto* clone = static_cast<DataBind*>(sourceBind->clone());
clone->file(sourceBind->file());
clone->target(holder);
clone->propertyKey(propertyKey);
clone->initialize();
if (sourceBind->converter() != nullptr)
{
clone->converter(sourceBind->converter()->clone()->as<DataConverter>());
}
m_artboardInstance->addDataBind(clone);
m_bindingExtras->keyFrameValueBinds[keyframe] = clone;
return holder;
}
// 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.
ScriptedInterpolator* LinearAnimationInstance::statefulInterpolator(
const InterpolatingKeyFrame* keyframe,
const ScriptedInterpolator* shared) const
{
if (shared == nullptr || keyframe == nullptr)
{
return nullptr;
}
auto& map = ensureBindingExtras().scriptedInterpolators;
auto it = map.find(keyframe);
if (it != map.end())
{
return it->second->get();
}
auto stateful = std::unique_ptr<StatefulScriptedInterpolator>(
new StatefulScriptedInterpolator(
shared,
m_artboardInstance,
m_artboardInstance->dataContext()));
auto* clone = stateful->get();
if (clone == nullptr)
{
return nullptr;
}
map.emplace(keyframe, std::move(stateful));
return clone;
}
bool LinearAnimationInstance::advanceAndApply(float seconds)
{
RIVE_PROF_SCOPE_L(1)
// Scripted interpolators run in apply(), before Artboard::advance polls.
m_artboardInstance->pollAsyncWork();
bool more = this->advance(seconds, this);
this->apply();
if (m_artboardInstance->advance(seconds))
{
more = true;
}
// A host that stops ticking a finished animation would never poll its
// scripts' async work in.
return more || keepGoing() || m_artboardInstance->hasPendingAsyncWork();
}
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);
}