| #include <rive/file.hpp> | |
| #include <rive/animation/state_machine_bool.hpp> | |
| #include <rive/animation/state_machine_trigger.hpp> | |
| #include <rive/animation/state_machine_layer.hpp> | |
| #include <rive/animation/animation_state.hpp> | |
| #include <rive/animation/entry_state.hpp> | |
| #include <rive/animation/state_transition.hpp> | |
| #include <rive/animation/state_machine_instance.hpp> | |
| #include <rive/animation/state_machine_input_instance.hpp> | |
| #include <rive/animation/blend_state_1d.hpp> | |
| #include <rive/animation/blend_animation_1d.hpp> | |
| #include <rive/animation/blend_state_direct.hpp> | |
| #include <rive/animation/blend_state_transition.hpp> | |
| #include <rive/animation/animation_reset_factory.hpp> | |
| #include <rive/shapes/paint/solid_color.hpp> | |
| #include <rive/shapes/paint/stroke.hpp> | |
| #include <rive/shapes/shape.hpp> | |
| #include <rive/viewmodel/viewmodel_instance_viewmodel.hpp> | |
| #include <rive/viewmodel/viewmodel_instance_number.hpp> | |
| #include <rive/viewmodel/viewmodel_instance_string.hpp> | |
| #include <rive/viewmodel/viewmodel_instance_boolean.hpp> | |
| #include <rive/viewmodel/viewmodel.hpp> | |
| #include "catch.hpp" | |
| #include "rive_file_reader.hpp" | |
| #include "utils/serializing_factory.hpp" | |
| #include <cstdio> | |
| using namespace rive; | |
| TEST_CASE("file with state machine be read", "[file]") | |
| { | |
| auto file = ReadRiveFile("assets/rocket.riv"); | |
| auto artboard = file->artboard(); | |
| REQUIRE(artboard != nullptr); | |
| REQUIRE(artboard->animationCount() == 3); | |
| REQUIRE(artboard->stateMachineCount() == 1); | |
| auto stateMachine = artboard->stateMachine("Button"); | |
| REQUIRE(stateMachine != nullptr); | |
| REQUIRE(stateMachine->layerCount() == 1); | |
| REQUIRE(stateMachine->inputCount() == 2); | |
| auto hover = stateMachine->input("Hover"); | |
| REQUIRE(hover != nullptr); | |
| REQUIRE(hover->is<rive::StateMachineBool>()); | |
| auto press = stateMachine->input("Press"); | |
| REQUIRE(press != nullptr); | |
| REQUIRE(press->is<rive::StateMachineBool>()); | |
| auto layer = stateMachine->layer(0); | |
| REQUIRE(layer->stateCount() == 6); | |
| REQUIRE(layer->anyState() != nullptr); | |
| REQUIRE(layer->entryState() != nullptr); | |
| REQUIRE(layer->exitState() != nullptr); | |
| int foundAnimationStates = 0; | |
| for (int i = 0; i < layer->stateCount(); i++) | |
| { | |
| auto state = layer->state(i); | |
| if (state->is<rive::AnimationState>()) | |
| { | |
| foundAnimationStates++; | |
| REQUIRE(state->as<rive::AnimationState>()->animation() != nullptr); | |
| } | |
| } | |
| REQUIRE(foundAnimationStates == 3); | |
| REQUIRE(layer->entryState()->transitionCount() == 1); | |
| auto stateTo = layer->entryState()->transition(0)->stateTo(); | |
| REQUIRE(stateTo != nullptr); | |
| REQUIRE(stateTo->is<rive::AnimationState>()); | |
| REQUIRE(stateTo->as<rive::AnimationState>()->animation() != nullptr); | |
| REQUIRE(stateTo->as<rive::AnimationState>()->animation()->name() == "idle"); | |
| auto idleState = stateTo->as<rive::AnimationState>(); | |
| REQUIRE(idleState->transitionCount() == 2); | |
| for (int i = 0; i < idleState->transitionCount(); i++) | |
| { | |
| auto transition = idleState->transition(i); | |
| if (transition->stateTo() | |
| ->as<rive::AnimationState>() | |
| ->animation() | |
| ->name() == "Roll_over") | |
| { | |
| // Check the condition | |
| REQUIRE(transition->conditionCount() == 1); | |
| } | |
| } | |
| auto abi = artboard->instance(); | |
| rive::StateMachineInstance smi(artboard->stateMachine("Button"), abi.get()); | |
| REQUIRE(smi.getBool("Hover")->name() == "Hover"); | |
| REQUIRE(smi.getBool("Press")->name() == "Press"); | |
| REQUIRE(smi.getBool("Hover") != nullptr); | |
| REQUIRE(smi.getBool("Press") != nullptr); | |
| REQUIRE(smi.stateChangedCount() == 0); | |
| REQUIRE(smi.currentAnimationCount() == 0); | |
| } | |
| TEST_CASE("file with blend states loads correctly", "[file]") | |
| { | |
| auto file = ReadRiveFile("assets/blend_test.riv"); | |
| auto artboard = file->artboard(); | |
| REQUIRE(artboard != nullptr); | |
| REQUIRE(artboard->animationCount() == 4); | |
| REQUIRE(artboard->stateMachineCount() == 2); | |
| auto stateMachine = artboard->stateMachine("blend"); | |
| REQUIRE(stateMachine != nullptr); | |
| REQUIRE(stateMachine->layerCount() == 1); | |
| auto layer = stateMachine->layer(0); | |
| REQUIRE(layer->stateCount() == 5); | |
| REQUIRE(layer->anyState() != nullptr); | |
| REQUIRE(layer->entryState() != nullptr); | |
| REQUIRE(layer->exitState() != nullptr); | |
| REQUIRE(layer->state(1)->is<rive::BlendState1D>()); | |
| REQUIRE(layer->state(2)->is<rive::BlendState1D>()); | |
| auto blendStateA = layer->state(1)->as<rive::BlendState1D>(); | |
| auto blendStateB = layer->state(2)->as<rive::BlendState1D>(); | |
| REQUIRE(blendStateA->animationCount() == 3); | |
| REQUIRE(blendStateB->animationCount() == 3); | |
| auto animation = blendStateA->animation(0); | |
| REQUIRE(animation->is<rive::BlendAnimation1D>()); | |
| auto animation1D = animation->as<rive::BlendAnimation1D>(); | |
| REQUIRE(animation1D->animation() != nullptr); | |
| REQUIRE(animation1D->animation()->name() == "horizontal"); | |
| REQUIRE(animation1D->value() == 0.0f); | |
| animation = blendStateA->animation(1); | |
| REQUIRE(animation->is<rive::BlendAnimation1D>()); | |
| animation1D = animation->as<rive::BlendAnimation1D>(); | |
| REQUIRE(animation1D->animation() != nullptr); | |
| REQUIRE(animation1D->animation()->name() == "vertical"); | |
| REQUIRE(animation1D->value() == 100.0f); | |
| animation = blendStateA->animation(2); | |
| REQUIRE(animation->is<rive::BlendAnimation1D>()); | |
| animation1D = animation->as<rive::BlendAnimation1D>(); | |
| REQUIRE(animation1D->animation() != nullptr); | |
| REQUIRE(animation1D->animation()->name() == "rotate"); | |
| REQUIRE(animation1D->value() == 0.0f); | |
| REQUIRE(blendStateA->transitionCount() == 1); | |
| REQUIRE(blendStateA->transition(0)->is<rive::BlendStateTransition>()); | |
| REQUIRE(blendStateA->transition(0) | |
| ->as<rive::BlendStateTransition>() | |
| ->exitBlendAnimation() != nullptr); | |
| } | |
| TEST_CASE("animation state with no animation doesn't crash", "[file]") | |
| { | |
| auto file = ReadRiveFile("assets/multiple_state_machines.riv"); | |
| auto artboard = file->artboard(); | |
| REQUIRE(artboard != nullptr); | |
| REQUIRE(artboard->animationCount() == 1); | |
| REQUIRE(artboard->stateMachineCount() == 4); | |
| auto stateMachine = artboard->stateMachine("two"); | |
| REQUIRE(stateMachine != nullptr); | |
| REQUIRE(stateMachine->layerCount() == 1); | |
| auto layer = stateMachine->layer(0); | |
| REQUIRE(layer->stateCount() == 4); | |
| REQUIRE(layer->anyState() != nullptr); | |
| REQUIRE(layer->entryState() != nullptr); | |
| REQUIRE(layer->exitState() != nullptr); | |
| REQUIRE(layer->state(3)->is<rive::AnimationState>()); | |
| auto animationState = layer->state(3)->as<rive::AnimationState>(); | |
| REQUIRE(animationState->animation() == nullptr); | |
| auto abi = artboard->instance(); | |
| rive::StateMachineInstance(stateMachine, abi.get()).advance(0.0f); | |
| } | |
| TEST_CASE("1D blend state keeps keepsGoing true even when animations " | |
| "themselves have stopped", | |
| "[file]") | |
| { | |
| auto file = ReadRiveFile("assets/oneshotblend.riv"); | |
| auto artboard = file->artboard(); | |
| auto stateMachine = artboard->stateMachine("State Machine 1"); | |
| auto abi = artboard->instance(); | |
| rive::StateMachineInstance* stateMachineInstance = | |
| new rive::StateMachineInstance(stateMachine, abi.get()); | |
| stateMachineInstance->advance(0.0f); | |
| REQUIRE(stateMachineInstance->needsAdvance() == true); | |
| // after advancing into the 1DBlendState we still need to keep going. | |
| stateMachineInstance->advance(0.5f); | |
| REQUIRE(stateMachineInstance->needsAdvance() == true); | |
| // even after advancing past the duration of the animations in the blend | |
| // states we need to keep going. | |
| stateMachineInstance->advance(1.0f); | |
| REQUIRE(stateMachineInstance->needsAdvance() == true); | |
| delete stateMachineInstance; | |
| } | |
| TEST_CASE("Transitions with duration completes the state correctly before " | |
| "changing states", | |
| "[file]") | |
| { | |
| auto file = ReadRiveFile("assets/state_machine_transition.riv"); | |
| auto black_color = 0xFF000000; | |
| auto white_color = 0xFFFFFFFF; | |
| auto artboard = file->artboard(); | |
| auto stateMachine = artboard->stateMachine("State-Machine-Test"); | |
| REQUIRE(artboard != nullptr); | |
| REQUIRE(artboard->animationCount() == 3); | |
| REQUIRE(artboard->stateMachineCount() == 1); | |
| REQUIRE(stateMachine->layerCount() == 1); | |
| auto layer = stateMachine->layer(0); | |
| REQUIRE(layer->stateCount() == 6); | |
| auto abi = artboard->instance(); | |
| REQUIRE(abi->children()[0]->is<rive::Shape>()); | |
| REQUIRE(abi->children()[0]->name() == "Star-Stroke"); | |
| auto shape = abi->children()[0]->as<rive::Shape>(); | |
| REQUIRE(shape->children()[1]->is<rive::Stroke>()); | |
| auto stroke = shape->children()[1]->as<rive::Stroke>(); | |
| REQUIRE(stroke->paint()->is<rive::SolidColor>()); | |
| auto solidColor = stroke->paint()->as<rive::SolidColor>(); | |
| // Before the transition, the color has to be full black | |
| REQUIRE(solidColor->colorValue() == black_color); | |
| rive::StateMachineInstance* stateMachineInstance = | |
| new rive::StateMachineInstance(stateMachine, abi.get()); | |
| stateMachineInstance->advanceAndApply(0.1f); | |
| abi->advance(0.1f); | |
| REQUIRE(stateMachineInstance->currentAnimationByIndex(0)->name() == | |
| "State-2"); | |
| // After the transition has passed, the color has to be full white. | |
| stateMachineInstance->advanceAndApply(2.0f); | |
| abi->advance(2.0f); | |
| REQUIRE(stateMachineInstance->currentAnimationByIndex(0)->name() == | |
| "State-3"); | |
| REQUIRE(solidColor->colorValue() == white_color); | |
| delete stateMachineInstance; | |
| } | |
| TEST_CASE("Blend state animations with reset applied to them.", "[file]") | |
| { | |
| auto file = ReadRiveFile("assets/animation_reset_cases.riv"); | |
| auto artboard = file->artboard(); | |
| auto stateMachine = artboard->stateMachine("blend-states-state-machine"); | |
| // We empty all factory reset resources to start the test clean | |
| rive::AnimationResetFactory::releaseResources(); | |
| REQUIRE(rive::AnimationResetFactory::resourcesCount() == 0); | |
| REQUIRE(artboard != nullptr); | |
| REQUIRE(artboard->animationCount() == 9); | |
| REQUIRE(artboard->stateMachineCount() == 1); | |
| auto abi = artboard->instance(); | |
| rive::StateMachineInstance* stateMachineInstance = | |
| new rive::StateMachineInstance(stateMachine, abi.get()); | |
| stateMachineInstance->advanceAndApply(0.1f); | |
| abi->advance(0.1f); | |
| auto blendValueNumber = stateMachineInstance->getNumber("blend-value"); | |
| REQUIRE(blendValueNumber != nullptr); | |
| REQUIRE(blendValueNumber->value() == 0); | |
| blendValueNumber->value(50); | |
| REQUIRE(blendValueNumber->value() == 50); | |
| stateMachineInstance->advanceAndApply(0.1f); | |
| abi->advance(0.1f); | |
| auto rect1 = abi->children()[9]->as<rive::Shape>(); | |
| REQUIRE(rect1->name() == "rect1"); | |
| auto rect2 = abi->children()[7]->as<rive::Shape>(); | |
| REQUIRE(rect2->name() == "rect2"); | |
| auto triangle = abi->children()[5]->as<rive::Shape>(); | |
| REQUIRE(triangle->name() == "triangle"); | |
| // This blend rotates 2 * Pi. At 50% it should have rotated 1 * Pi | |
| REQUIRE(rect1->rotation() == Approx(3.141592f)); | |
| auto state2Bool = stateMachineInstance->getBool("state-2"); | |
| REQUIRE(state2Bool != nullptr); | |
| REQUIRE(state2Bool->value() == false); | |
| state2Bool->value(true); | |
| blendValueNumber->value(50); | |
| stateMachineInstance->advanceAndApply(0.1f); | |
| abi->advance(0.1f); | |
| stateMachineInstance->advanceAndApply(0.1f); | |
| REQUIRE(rive::AnimationResetFactory::resourcesCount() == 0); | |
| REQUIRE(state2Bool->value() == true); | |
| // X and Y interpolation in this state ranges are [75; 425] and [50; 450] | |
| // so at 50% they should be at 250, 250 | |
| REQUIRE(rect1->x() == 250.0f); | |
| REQUIRE(rect1->y() == 250.0f); | |
| // rect2 rotation range is [0; -360] | |
| // so at 50% it should be at -180 | |
| REQUIRE(rect2->rotation() == Approx(-3.141592f)); | |
| auto state3Bool = stateMachineInstance->getBool("state-3"); | |
| REQUIRE(state3Bool != nullptr); | |
| REQUIRE(state3Bool->value() == false); | |
| state3Bool->value(true); | |
| REQUIRE(rive::AnimationResetFactory::resourcesCount() == 0); | |
| stateMachineInstance->advanceAndApply(0.1f); | |
| abi->advance(0.1f); | |
| stateMachineInstance->advanceAndApply(0.1f); | |
| blendValueNumber->value(100); | |
| abi->advance(0.1f); | |
| stateMachineInstance->advanceAndApply(0.1f); | |
| REQUIRE(state3Bool->value() == true); | |
| REQUIRE(triangle->y() == Approx(43.13281f)); | |
| REQUIRE(rive::AnimationResetFactory::resourcesCount() == 1); | |
| auto state4Bool = stateMachineInstance->getBool("state-4"); | |
| REQUIRE(state4Bool != nullptr); | |
| REQUIRE(state4Bool->value() == false); | |
| state4Bool->value(true); | |
| stateMachineInstance->advanceAndApply(0.1f); | |
| abi->advance(0.1f); | |
| REQUIRE(state4Bool->value() == true); | |
| REQUIRE(rive::AnimationResetFactory::resourcesCount() == 2); | |
| state4Bool->value(false); | |
| stateMachineInstance->advanceAndApply(0.1f); | |
| abi->advance(0.1f); | |
| stateMachineInstance->advanceAndApply(0.1f); | |
| REQUIRE(state4Bool->value() == false); | |
| // After switching states mutiple times resources stay at 2 because they are | |
| // released and retrieved from the pool | |
| REQUIRE(rive::AnimationResetFactory::resourcesCount() == 2); | |
| delete stateMachineInstance; | |
| } | |
| TEST_CASE("Transitions with reset applied to them.", "[file]") | |
| { | |
| auto file = ReadRiveFile("assets/animation_reset_cases.riv"); | |
| auto artboard = file->artboard("transitions"); | |
| auto stateMachine = artboard->stateMachine("transitions-state-machine"); | |
| // We empty all factory reset resources to start the test clean | |
| rive::AnimationResetFactory::releaseResources(); | |
| REQUIRE(rive::AnimationResetFactory::resourcesCount() == 0); | |
| REQUIRE(artboard != nullptr); | |
| REQUIRE(artboard->animationCount() == 5); | |
| REQUIRE(artboard->stateMachineCount() == 1); | |
| auto abi = artboard->instance(); | |
| rive::StateMachineInstance* stateMachineInstance = | |
| new rive::StateMachineInstance(stateMachine, abi.get()); | |
| stateMachineInstance->advanceAndApply(0.1f); | |
| abi->advance(0.1f); | |
| auto rect = abi->children()[7]->as<rive::Shape>(); | |
| REQUIRE(rect->name() == "rectangle"); | |
| REQUIRE(rect->x() == 50); | |
| auto ellipse = abi->children()[5]->as<rive::Shape>(); | |
| REQUIRE(ellipse->name() == "ellipse"); | |
| REQUIRE(ellipse->x() == Approx(440.31241)); | |
| auto stateNumber = stateMachineInstance->getNumber("Number 1"); | |
| REQUIRE(stateNumber != nullptr); | |
| REQUIRE(stateNumber->value() == 0); | |
| stateNumber->value(1); | |
| REQUIRE(stateNumber->value() == 1); | |
| stateMachineInstance->advanceAndApply(0.1f); | |
| abi->advance(0.1f); | |
| stateMachineInstance->advanceAndApply(1.25f); | |
| // rect transitions in 2.5 secs from x->50 to x->433 | |
| // so if the translation is linear, after 1.25s it should have | |
| // traversed half the path | |
| REQUIRE(rect->x() == 241.5f); | |
| stateNumber->value(2); | |
| REQUIRE(stateNumber->value() == 2); | |
| stateMachineInstance->advanceAndApply(0.1f); | |
| abi->advance(0.1f); | |
| stateMachineInstance->advanceAndApply(1.25f); | |
| // range is [440.31241; 42.69] | |
| // half if the path is 42.69 + (440.21241 - 42.60) = 241.4962 | |
| REQUIRE(ellipse->x() == Approx(241.49992f)); | |
| // Transitions release their instance immediately so it's available for the | |
| // next instance to use | |
| REQUIRE(rive::AnimationResetFactory::resourcesCount() == 0); | |
| stateNumber->value(3); | |
| REQUIRE(stateNumber->value() == 3); | |
| stateMachineInstance->advanceAndApply(0.1f); | |
| abi->advance(0.1f); | |
| stateMachineInstance->advanceAndApply(1.25f); | |
| REQUIRE(rive::AnimationResetFactory::resourcesCount() == 0); | |
| stateNumber->value(4); | |
| REQUIRE(stateNumber->value() == 4); | |
| stateMachineInstance->advanceAndApply(0.1f); | |
| abi->advance(0.1f); | |
| stateMachineInstance->advanceAndApply(1.25f); | |
| // The last two states don't have a transition with duration set so the | |
| // instance is released and available | |
| REQUIRE(rive::AnimationResetFactory::resourcesCount() == 1); | |
| delete stateMachineInstance; | |
| } | |
| TEST_CASE("Triggers will only be used on allowed state changes.", "[file]") | |
| { | |
| auto file = ReadRiveFile("assets/state_machine_triggers.riv"); | |
| auto artboard = file->artboard("main"); | |
| // We empty all factory reset resources to start the test clean | |
| rive::AnimationResetFactory::releaseResources(); | |
| REQUIRE(rive::AnimationResetFactory::resourcesCount() == 0); | |
| REQUIRE(artboard != nullptr); | |
| REQUIRE(artboard->animationCount() == 1); | |
| REQUIRE(artboard->stateMachineCount() == 1); | |
| auto stateMachine = artboard->stateMachine("State Machine 1"); | |
| REQUIRE(stateMachine->layerCount() == 1); | |
| REQUIRE(stateMachine->inputCount() == 1); | |
| auto abi = artboard->instance(); | |
| rive::StateMachineInstance* stateMachineInstance = | |
| new rive::StateMachineInstance(stateMachine, abi.get()); | |
| stateMachineInstance->advanceAndApply(0.1f); | |
| abi->advance(0.1f); | |
| auto trigger = stateMachine->input("Trigger 1"); | |
| REQUIRE(trigger != nullptr); | |
| REQUIRE(trigger->is<rive::StateMachineTrigger>()); | |
| auto layer = stateMachine->layer(0); | |
| REQUIRE(layer->stateCount() == 5); | |
| REQUIRE(layer->anyState() != nullptr); | |
| REQUIRE(layer->entryState() != nullptr); | |
| REQUIRE(layer->exitState() != nullptr); | |
| rive::AnimationState* animationState1 = nullptr; | |
| rive::AnimationState* animationState2 = nullptr; | |
| int foundAnimationStates = 0; | |
| for (int i = 0; i < layer->stateCount(); i++) | |
| { | |
| auto state = layer->state(i); | |
| if (state->is<rive::AnimationState>()) | |
| { | |
| foundAnimationStates++; | |
| REQUIRE(state->as<rive::AnimationState>()->animation() != nullptr); | |
| if (foundAnimationStates == 1) | |
| { | |
| animationState1 = state->as<rive::AnimationState>(); | |
| } | |
| else | |
| { | |
| animationState2 = state->as<rive::AnimationState>(); | |
| } | |
| } | |
| } | |
| REQUIRE(foundAnimationStates == 2); | |
| REQUIRE(animationState1 != nullptr); | |
| REQUIRE(animationState2 != nullptr); | |
| auto triggerInstance = stateMachineInstance->getTrigger("Trigger 1"); | |
| REQUIRE(triggerInstance != nullptr); | |
| triggerInstance->fire(); | |
| stateMachineInstance->advanceAndApply(0.1f); | |
| auto currentLayerState = stateMachineInstance->layerState(0); | |
| REQUIRE(currentLayerState == animationState2); | |
| triggerInstance->fire(); | |
| stateMachineInstance->advanceAndApply(0.1f); | |
| currentLayerState = stateMachineInstance->layerState(0); | |
| REQUIRE(currentLayerState == animationState1); | |
| delete stateMachineInstance; | |
| } | |
| TEST_CASE("Transition with list index can be compared to a number", "[silver]") | |
| { | |
| SerializingFactory silver; | |
| auto file = ReadRiveFile("assets/transition_index_condition.riv", &silver); | |
| auto artboard = file->artboardDefault(); | |
| silver.frameSize(artboard->width(), artboard->height()); | |
| auto stateMachine = artboard->stateMachineAt(0); | |
| int viewModelId = artboard.get()->viewModelId(); | |
| auto vmi = viewModelId == -1 | |
| ? file->createViewModelInstance(artboard.get()) | |
| : file->createViewModelInstance(viewModelId, 0); | |
| stateMachine->bindViewModelInstance(vmi); | |
| stateMachine->advanceAndApply(0.0f); | |
| auto renderer = silver.makeRenderer(); | |
| artboard->draw(renderer.get()); | |
| CHECK(silver.matches("transition_index_condition")); | |
| } | |
| TEST_CASE("Listeners are sorted in the right order", "[silver]") | |
| { | |
| SerializingFactory silver; | |
| auto file = ReadRiveFile("assets/sorted_listeners.riv", &silver); | |
| auto artboard = file->artboardDefault(); | |
| silver.frameSize(artboard->width(), artboard->height()); | |
| auto stateMachine = artboard->stateMachineAt(0); | |
| auto vmi = file->createViewModelInstance(artboard.get()); | |
| stateMachine->bindViewModelInstance(vmi); | |
| stateMachine->advanceAndApply(0.0f); | |
| auto renderer = silver.makeRenderer(); | |
| artboard->draw(renderer.get()); | |
| // Down (Move + Down happen on the same frame, but down happens after move) | |
| silver.addFrame(); | |
| stateMachine->pointerDown(Vec2D(250.0f, 250.0f), 0); | |
| stateMachine->advanceAndApply(0.016f); | |
| artboard->draw(renderer.get()); | |
| // Click (Up + Click on same frame, but click happens after up) | |
| silver.addFrame(); | |
| stateMachine->pointerUp(Vec2D(250.0f, 250.0f), 0); | |
| stateMachine->advanceAndApply(0.016f); | |
| artboard->draw(renderer.get()); | |
| // Exit | |
| silver.addFrame(); | |
| stateMachine->pointerMove(Vec2D(0.0f, 0.0f), 0, 0); | |
| stateMachine->advanceAndApply(0.016f); | |
| artboard->draw(renderer.get()); | |
| // Drag gesture | |
| silver.addFrame(); | |
| stateMachine->pointerDown(Vec2D(250.0f, 250.0f), 0); | |
| stateMachine->pointerMove(Vec2D(251.0f, 251.0f), 0); | |
| stateMachine->advanceAndApply(0.016f); | |
| artboard->draw(renderer.get()); | |
| // Up | |
| silver.addFrame(); | |
| stateMachine->pointerUp(Vec2D(251.0f, 251.0f), 0); | |
| stateMachine->advanceAndApply(0.016f); | |
| artboard->draw(renderer.get()); | |
| // Exit | |
| silver.addFrame(); | |
| stateMachine->pointerMove(Vec2D(0.0f, 0.0f), 0); | |
| stateMachine->advanceAndApply(0.016f); | |
| artboard->draw(renderer.get()); | |
| // (Move): Enter + Move, but move happens after enter | |
| silver.addFrame(); | |
| stateMachine->pointerMove(Vec2D(251.0f, 251.0f), 0); | |
| stateMachine->advanceAndApply(0.016f); | |
| artboard->draw(renderer.get()); | |
| CHECK(silver.matches("sorted_listeners")); | |
| } | |
| // The test asset carries Luau bytecode scripts, which only the Luau | |
| // backend runs. | |
| #ifdef WITH_RIVE_SCRIPTING_LUAU | |
| TEST_CASE("Listeners with multiple types of events", "[silver]") | |
| { | |
| SerializingFactory silver; | |
| auto file = ReadRiveFile("assets/multi_listeners.riv", &silver); | |
| auto artboard = file->artboardDefault(); | |
| silver.frameSize(artboard->width(), artboard->height()); | |
| auto stateMachine = artboard->stateMachineAt(0); | |
| auto vmi = file->createDefaultViewModelInstance(artboard.get()); | |
| stateMachine->bindViewModelInstance(vmi); | |
| auto renderer = silver.makeRenderer(); | |
| stateMachine->advanceAndApply(0.016f); | |
| artboard->draw(renderer.get()); | |
| silver.addFrame(); | |
| // Trigger first timeline events | |
| stateMachine->advanceAndApply(0.5f); | |
| artboard->draw(renderer.get()); | |
| silver.addFrame(); | |
| // Trigger second timeline events | |
| stateMachine->advanceAndApply(0.5f); | |
| artboard->draw(renderer.get()); | |
| silver.addFrame(); | |
| // Process second timeline events | |
| stateMachine->advanceAndApply(0.016f); | |
| artboard->draw(renderer.get()); | |
| silver.addFrame(); | |
| stateMachine->pointerMove(rive::Vec2D(490.0f, 10.0f)); | |
| stateMachine->advanceAndApply(0.016f); | |
| artboard->draw(renderer.get()); | |
| silver.addFrame(); | |
| stateMachine->pointerDown(rive::Vec2D(490.0f, 10.0f)); | |
| stateMachine->advanceAndApply(0.016f); | |
| artboard->draw(renderer.get()); | |
| silver.addFrame(); | |
| stateMachine->pointerUp(rive::Vec2D(490.0f, 10.0f)); | |
| stateMachine->advanceAndApply(0.016f); | |
| artboard->draw(renderer.get()); | |
| CHECK(silver.matches("multi_listeners")); | |
| } | |
| // The test asset carries Luau bytecode scripts, which only the Luau | |
| // backend runs. | |
| #ifdef WITH_RIVE_SCRIPTING_LUAU | |
| #endif | |
| TEST_CASE("Listeners with multiple types of events and rebinding", "[silver]") | |
| { | |
| SerializingFactory silver; | |
| auto file = ReadRiveFile("assets/multi_listeners.riv", &silver); | |
| auto artboard = file->artboardNamed("Rebind"); | |
| silver.frameSize(artboard->width(), artboard->height()); | |
| auto stateMachine = artboard->stateMachineAt(0); | |
| auto vmi = file->createDefaultViewModelInstance(artboard.get()); | |
| auto vmiChildProp = | |
| vmi->propertyValue("child")->as<ViewModelInstanceViewModel>(); | |
| auto vmiChild = vmiChildProp->referenceViewModelInstance(); | |
| stateMachine->bindViewModelInstance(vmi); | |
| auto renderer = silver.makeRenderer(); | |
| stateMachine->advanceAndApply(0.016f); | |
| artboard->draw(renderer.get()); | |
| silver.addFrame(); | |
| { | |
| auto num1 = | |
| vmiChild->propertyValue("num")->as<ViewModelInstanceNumber>(); | |
| auto num2 = | |
| vmiChild->propertyValue("num2")->as<ViewModelInstanceNumber>(); | |
| num1->propertyValue(2); | |
| stateMachine->advanceAndApply(0.016f); | |
| artboard->draw(renderer.get()); | |
| silver.addFrame(); | |
| num2->propertyValue(2); | |
| stateMachine->advanceAndApply(0.016f); | |
| artboard->draw(renderer.get()); | |
| silver.addFrame(); | |
| } | |
| // Create a new view model instance for RebindChild that will be used to | |
| // replace the vm tree | |
| auto vmiChild2 = file->createViewModelInstance("RebindChild"); | |
| if (vmiChild2) | |
| { | |
| auto num1 = | |
| vmiChild2->propertyValue("num")->as<ViewModelInstanceNumber>(); | |
| auto num2 = | |
| vmiChild2->propertyValue("num2")->as<ViewModelInstanceNumber>(); | |
| vmi->replaceViewModelByName("child", vmiChild2); | |
| stateMachine->advanceAndApply(0.016f); | |
| artboard->draw(renderer.get()); | |
| silver.addFrame(); | |
| num1->propertyValue(2); | |
| stateMachine->advanceAndApply(0.016f); | |
| artboard->draw(renderer.get()); | |
| silver.addFrame(); | |
| num2->propertyValue(2); | |
| stateMachine->advanceAndApply(0.016f); | |
| artboard->draw(renderer.get()); | |
| silver.addFrame(); | |
| // Nothing should change in this case | |
| auto num1Old = | |
| vmiChild->propertyValue("num")->as<ViewModelInstanceNumber>(); | |
| num1Old->propertyValue(3); | |
| stateMachine->advanceAndApply(0.016f); | |
| artboard->draw(renderer.get()); | |
| } | |
| CHECK(silver.matches("multi_listeners-rebind")); | |
| } | |
| #endif | |
| TEST_CASE("Transition duration in nested state machines is bindable", | |
| "[silver]") | |
| { | |
| SerializingFactory silver; | |
| auto file = | |
| ReadRiveFile("assets/transition_duration_bind_nested.riv", &silver); | |
| auto artboard = file->artboardDefault(); | |
| silver.frameSize(artboard->width(), artboard->height()); | |
| auto stateMachine = artboard->stateMachineAt(0); | |
| int viewModelId = artboard.get()->viewModelId(); | |
| auto vmi = viewModelId == -1 | |
| ? file->createViewModelInstance(artboard.get()) | |
| : file->createViewModelInstance(viewModelId, 0); | |
| stateMachine->bindViewModelInstance(vmi); | |
| stateMachine->advanceAndApply(0.0f); | |
| auto renderer = silver.makeRenderer(); | |
| int frames = 86; | |
| for (int i = 0; i < frames; i++) | |
| { | |
| silver.addFrame(); | |
| stateMachine->advanceAndApply(0.016f); | |
| artboard->draw(renderer.get()); | |
| } | |
| CHECK(silver.matches("transition_duration_bind_nested")); | |
| } | |
| TEST_CASE("Transition duration in artboard list state machines is bindable", | |
| "[silver]") | |
| { | |
| SerializingFactory silver; | |
| auto file = | |
| ReadRiveFile("assets/transition_duration_bind_list.riv", &silver); | |
| auto artboard = file->artboardDefault(); | |
| silver.frameSize(artboard->width(), artboard->height()); | |
| auto stateMachine = artboard->stateMachineAt(0); | |
| int viewModelId = artboard.get()->viewModelId(); | |
| auto vmi = viewModelId == -1 | |
| ? file->createViewModelInstance(artboard.get()) | |
| : file->createViewModelInstance(viewModelId, 0); | |
| stateMachine->bindViewModelInstance(vmi); | |
| stateMachine->advanceAndApply(0.0f); | |
| auto renderer = silver.makeRenderer(); | |
| int frames = 86; | |
| for (int i = 0; i < frames; i++) | |
| { | |
| silver.addFrame(); | |
| stateMachine->advanceAndApply(0.016f); | |
| artboard->draw(renderer.get()); | |
| } | |
| CHECK(silver.matches("transition_duration_bind_list")); | |
| } | |
| TEST_CASE("Replace view model instances with multiple view model levels", | |
| "[silver]") | |
| { | |
| SerializingFactory silver; | |
| auto file = | |
| ReadRiveFile("assets/rebind_with_nested_viewmodel.riv", &silver); | |
| auto artboard = file->artboardDefault(); | |
| REQUIRE(artboard != nullptr); | |
| silver.frameSize(artboard->width(), artboard->height()); | |
| auto stateMachine = artboard->stateMachineAt(0); | |
| auto vmi = file->createDefaultViewModelInstance(artboard.get()); | |
| stateMachine->bindViewModelInstance(vmi); | |
| stateMachine->advanceAndApply(0.0f); | |
| auto renderer = silver.makeRenderer(); | |
| stateMachine->advanceAndApply(0.016f); | |
| artboard->draw(renderer.get()); | |
| silver.addFrame(); | |
| { | |
| auto second = | |
| vmi->propertyValue("sec")->as<ViewModelInstanceViewModel>(); | |
| auto secInstance = second->referenceViewModelInstance(); | |
| auto third = | |
| secInstance->propertyValue("thi")->as<ViewModelInstanceViewModel>(); | |
| auto thirdInstance = third->referenceViewModelInstance(); | |
| thirdInstance->propertyValue("label") | |
| ->as<ViewModelInstanceString>() | |
| ->propertyValue("updated"); | |
| } | |
| stateMachine->advanceAndApply(0.016f); | |
| artboard->draw(renderer.get()); | |
| silver.addFrame(); | |
| // Create a new view model with a new child view model | |
| auto thirdVM = file->viewModel("Third"); | |
| auto thirdInstance = thirdVM->createInstance(); | |
| thirdInstance->propertyValue("label") | |
| ->as<ViewModelInstanceString>() | |
| ->propertyValue("new updated"); | |
| auto secondVM = file->viewModel("Second"); | |
| auto secondInstance = secondVM->createInstance(); | |
| secondInstance->replaceViewModelByName("thi", thirdInstance); | |
| vmi->replaceViewModelByName("sec", secondInstance); | |
| stateMachine->advanceAndApply(0.016f); | |
| artboard->draw(renderer.get()); | |
| CHECK(silver.matches("rebind_with_nested_viewmodel")); | |
| } | |
| TEST_CASE("Component based transition conditions", "[silver]") | |
| { | |
| SerializingFactory silver; | |
| auto file = ReadRiveFile("assets/component_based_conditions.riv", &silver); | |
| auto artboard = file->artboardDefault(); | |
| REQUIRE(artboard != nullptr); | |
| silver.frameSize(artboard->width(), artboard->height()); | |
| auto stateMachine = artboard->stateMachineAt(0); | |
| REQUIRE(stateMachine != nullptr); | |
| auto vmi = file->createViewModelInstance(artboard.get()); | |
| REQUIRE(vmi != nullptr); | |
| auto numberProperty = | |
| vmi->propertyValue("numberProperty")->as<ViewModelInstanceNumber>(); | |
| stateMachine->bindViewModelInstance(vmi); | |
| stateMachine->advanceAndApply(0.1f); | |
| auto renderer = silver.makeRenderer(); | |
| artboard->draw(renderer.get()); | |
| silver.addFrame(); | |
| numberProperty->propertyValue(1.5); | |
| stateMachine->advanceAndApply(0.016f); | |
| artboard->draw(renderer.get()); | |
| int frames = (int)(2.5f / 0.1f); | |
| for (int i = 0; i < frames; i++) | |
| { | |
| silver.addFrame(); | |
| stateMachine->advanceAndApply(0.1f); | |
| artboard->draw(renderer.get()); | |
| } | |
| CHECK(silver.matches("component_based_conditions")); | |
| } | |
| TEST_CASE("Component based transition conditions with other props", "[silver]") | |
| { | |
| SerializingFactory silver; | |
| auto file = ReadRiveFile("assets/component_based_conditions.riv", &silver); | |
| auto artboard = file->artboardNamed("Artboard2"); | |
| REQUIRE(artboard != nullptr); | |
| silver.frameSize(artboard->width(), artboard->height()); | |
| auto stateMachine = artboard->stateMachineAt(0); | |
| REQUIRE(stateMachine != nullptr); | |
| auto vmi = file->createViewModelInstance(artboard.get()); | |
| REQUIRE(vmi != nullptr); | |
| auto boolProperty = | |
| vmi->propertyValue("vmBool")->as<ViewModelInstanceBoolean>(); | |
| stateMachine->bindViewModelInstance(vmi); | |
| stateMachine->advanceAndApply(0.1f); | |
| auto renderer = silver.makeRenderer(); | |
| artboard->draw(renderer.get()); | |
| silver.addFrame(); | |
| boolProperty->propertyValue(true); | |
| stateMachine->advanceAndApply(0.016f); | |
| artboard->draw(renderer.get()); | |
| int frames = (int)(2.5f / 0.1f); | |
| for (int i = 0; i < frames; i++) | |
| { | |
| silver.addFrame(); | |
| stateMachine->advanceAndApply(0.1f); | |
| artboard->draw(renderer.get()); | |
| } | |
| CHECK(silver.matches("component_based_conditions-Artboard2")); | |
| } | |
| TEST_CASE("Transitions and state layers can trigger actions", "[silver]") | |
| { | |
| SerializingFactory silver; | |
| auto file = ReadRiveFile("assets/transition_actions.riv", &silver); | |
| auto artboard = file->artboardDefault(); | |
| REQUIRE(artboard != nullptr); | |
| silver.frameSize(artboard->width(), artboard->height()); | |
| auto stateMachine = artboard->stateMachineAt(0); | |
| REQUIRE(stateMachine != nullptr); | |
| int viewModelId = artboard.get()->viewModelId(); | |
| auto vmi = viewModelId == -1 | |
| ? file->createViewModelInstance(artboard.get()) | |
| : file->createViewModelInstance(viewModelId, 0); | |
| REQUIRE(vmi != nullptr); | |
| stateMachine->bindViewModelInstance(vmi); | |
| stateMachine->advanceAndApply(0.1f); | |
| auto renderer = silver.makeRenderer(); | |
| artboard->draw(renderer.get()); | |
| int frames = (int)(1.5f / 0.5f); | |
| for (int i = 0; i < frames; i++) | |
| { | |
| silver.addFrame(); | |
| stateMachine->advanceAndApply(0.5f); | |
| artboard->draw(renderer.get()); | |
| } | |
| stateMachine->pointerDown(Vec2D(250.0f, 250.0f)); | |
| stateMachine->pointerUp(Vec2D(250.0f, 250.0f)); | |
| stateMachine->advanceAndApply(0.016f); | |
| artboard->draw(renderer.get()); | |
| for (int i = 0; i < frames; i++) | |
| { | |
| silver.addFrame(); | |
| stateMachine->advanceAndApply(0.5f); | |
| artboard->draw(renderer.get()); | |
| } | |
| CHECK(silver.matches("transition_actions")); | |
| } | |
| TEST_CASE( | |
| "Paused state machine changes opacity and relayouts when parent's opacity or layout changes", | |
| "[silver]") | |
| { | |
| SerializingFactory silver; | |
| auto file = | |
| ReadRiveFile("assets/paused_nested_artboard_opacity.riv", &silver); | |
| auto artboard = file->artboardDefault(); | |
| REQUIRE(artboard != nullptr); | |
| silver.frameSize(artboard->width(), artboard->height()); | |
| auto stateMachine = artboard->stateMachineAt(0); | |
| auto vmi = file->createDefaultViewModelInstance(artboard.get()); | |
| stateMachine->bindViewModelInstance(vmi); | |
| stateMachine->advanceAndApply(0.1f); | |
| auto renderer = silver.makeRenderer(); | |
| artboard->draw(renderer.get()); | |
| silver.addFrame(); | |
| stateMachine->advanceAndApply(0.1f); | |
| artboard->draw(renderer.get()); | |
| int frames = (int)(2.0f / 0.05f); | |
| for (int i = 0; i < frames; i++) | |
| { | |
| silver.addFrame(); | |
| stateMachine->advanceAndApply(0.05f); | |
| artboard->draw(renderer.get()); | |
| } | |
| CHECK(silver.matches("paused_nested_artboard_opacity")); | |
| } |