| #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); | |
| } |