| #include "rive/animation/state_transition.hpp" |
| #include "rive/importers/import_stack.hpp" |
| #include "rive/importers/layer_state_importer.hpp" |
| #include "rive/animation/layer_state.hpp" |
| #include "rive/animation/transition_condition.hpp" |
| #include "rive/animation/animation_state.hpp" |
| #include "rive/animation/linear_animation.hpp" |
| #include "rive/animation/state_machine_input_instance.hpp" |
| #include "rive/animation/state_machine_trigger.hpp" |
| #include "rive/animation/animation_state_instance.hpp" |
| #include "rive/animation/transition_trigger_condition.hpp" |
| |
| using namespace rive; |
| |
| StateTransition::~StateTransition() { |
| for (auto condition : m_Conditions) { |
| delete condition; |
| } |
| } |
| |
| StatusCode StateTransition::onAddedDirty(CoreContext* context) { |
| StatusCode code; |
| for (auto condition : m_Conditions) { |
| if ((code = condition->onAddedDirty(context)) != StatusCode::Ok) { |
| return code; |
| } |
| } |
| return StatusCode::Ok; |
| } |
| |
| StatusCode StateTransition::onAddedClean(CoreContext* context) { |
| StatusCode code; |
| for (auto condition : m_Conditions) { |
| if ((code = condition->onAddedClean(context)) != StatusCode::Ok) { |
| return code; |
| } |
| } |
| return StatusCode::Ok; |
| } |
| |
| StatusCode StateTransition::import(ImportStack& importStack) { |
| auto stateImporter = importStack.latest<LayerStateImporter>(LayerState::typeKey); |
| if (stateImporter == nullptr) { |
| return StatusCode::MissingObject; |
| } |
| stateImporter->addTransition(this); |
| return Super::import(importStack); |
| } |
| |
| void StateTransition::addCondition(TransitionCondition* condition) { |
| m_Conditions.push_back(condition); |
| } |
| |
| float StateTransition::mixTime(const LayerState* stateFrom) const { |
| if (duration() == 0) { |
| return 0; |
| } |
| if ((transitionFlags() & StateTransitionFlags::DurationIsPercentage) == |
| StateTransitionFlags::DurationIsPercentage) |
| { |
| float animationDuration = 0.0f; |
| if (stateFrom->is<AnimationState>()) { |
| auto animation = stateFrom->as<AnimationState>()->animation(); |
| if (animation != nullptr) { |
| animationDuration = animation->durationSeconds(); |
| } |
| } |
| return duration() / 100.0f * animationDuration; |
| } else { |
| return duration() / 1000.0f; |
| } |
| } |
| |
| float StateTransition::exitTimeSeconds(const LayerState* stateFrom, bool absolute) const { |
| if ((transitionFlags() & StateTransitionFlags::ExitTimeIsPercentage) == |
| StateTransitionFlags::ExitTimeIsPercentage) |
| { |
| float animationDuration = 0.0f; |
| float start = 0.0f; |
| |
| auto exitAnimation = exitTimeAnimation(stateFrom); |
| if (exitAnimation != nullptr) { |
| start = absolute ? exitAnimation->startSeconds() : 0.0f; |
| animationDuration = exitAnimation->durationSeconds(); |
| } |
| |
| return start + exitTime() / 100.0f * animationDuration; |
| } |
| return exitTime() / 1000.0f; |
| } |
| |
| const LinearAnimationInstance* |
| StateTransition::exitTimeAnimationInstance(const StateInstance* from) const { |
| return from != nullptr && from->state()->is<AnimationState>() |
| ? static_cast<const AnimationStateInstance*>(from)->animationInstance() |
| : nullptr; |
| } |
| |
| const LinearAnimation* StateTransition::exitTimeAnimation(const LayerState* from) const { |
| return from != nullptr && from->is<AnimationState>() ? from->as<AnimationState>()->animation() |
| : nullptr; |
| } |
| |
| AllowTransition |
| StateTransition::allowed(StateInstance* stateFrom, Span<SMIInput*> inputs, bool ignoreTriggers) const { |
| if (isDisabled()) { |
| return AllowTransition::no; |
| } |
| |
| for (auto condition : m_Conditions) { |
| // N.B. state machine instance sanitizes these for us... |
| auto input = inputs[condition->inputId()]; |
| |
| if ((ignoreTriggers && condition->is<TransitionTriggerCondition>()) || |
| !condition->evaluate(input)) { |
| return AllowTransition::no; |
| } |
| } |
| |
| if (enableExitTime()) { |
| auto exitAnimation = exitTimeAnimationInstance(stateFrom); |
| if (exitAnimation != nullptr) { |
| // Exit time is specified in a value less than a single loop, so we |
| // want to allow exiting regardless of which loop we're on. To do |
| // that we bring the exit time up to the loop our lastTime is at. |
| auto lastTime = exitAnimation->lastTotalTime(); |
| auto time = exitAnimation->totalTime(); |
| auto exitTime = exitTimeSeconds(stateFrom->state()); |
| auto animationFrom = exitAnimation->animation(); |
| auto duration = animationFrom->durationSeconds(); |
| |
| // TODO: there are some considerations to have when exit time is |
| // combined with another condition (like trigger) |
| // - not sure how to get this to make sense with pingPing |
| // animations |
| // - also if exit time is, say 50% on a loop, this will be happy |
| // to fire |
| // - when time is anywhere in 50%-100%, 150%-200%. as opposed |
| // to just at 50% |
| // .... makes you wonder if we need some kind of exit |
| // after/exit before time |
| // .... but i suspect that will introduce some more issues? |
| |
| // There's only one iteration in oneShot, |
| if (exitTime <= duration && animationFrom->loop() != Loop::oneShot) { |
| // Get exit time relative to the loop lastTime was in. |
| exitTime += std::floor(lastTime / duration) * duration; |
| } |
| |
| if (time < exitTime) { |
| return AllowTransition::waitingForExit; |
| } |
| } |
| } |
| return AllowTransition::yes; |
| } |
| |
| bool StateTransition::applyExitCondition(StateInstance* from) const { |
| // Hold exit time when the user has set to pauseOnExit on this condition |
| // (only valid when exiting from an Animation). |
| bool useExitTime = enableExitTime() && (from != nullptr && from->state()->is<AnimationState>()); |
| if (pauseOnExit() && useExitTime) { |
| static_cast<AnimationStateInstance*>(from)->animationInstance()->time( |
| exitTimeSeconds(from->state(), true)); |
| return true; |
| } |
| return useExitTime; |
| } |