| // Makes ure gl3w is included before glfw3 | |
| #include "GL/gl3w.h" | |
| #define SK_GL | |
| #include "GLFW/glfw3.h" | |
| #include "GrBackendSurface.h" | |
| #include "GrDirectContext.h" | |
| #include "SkCanvas.h" | |
| #include "SkColorSpace.h" | |
| #include "SkSurface.h" | |
| #include "SkTypes.h" | |
| #include "gl/GrGLInterface.h" | |
| #include "rive/animation/linear_animation_instance.hpp" | |
| #include "rive/animation/state_machine_instance.hpp" | |
| #include "rive/animation/state_machine_input_instance.hpp" | |
| #include "rive/animation/state_machine_number.hpp" | |
| #include "rive/animation/state_machine_bool.hpp" | |
| #include "rive/animation/state_machine_trigger.hpp" | |
| #include "rive/artboard.hpp" | |
| #include "rive/file.hpp" | |
| #include "rive/layout.hpp" | |
| #include "rive/math/aabb.hpp" | |
| #include "skia_renderer.hpp" | |
| #include "imgui/backends/imgui_impl_glfw.h" | |
| #include "imgui/backends/imgui_impl_opengl3.h" | |
| #include <cmath> | |
| #include <stdio.h> | |
| std::string filename; | |
| rive::File* currentFile = nullptr; | |
| rive::Artboard* artboard = nullptr; | |
| rive::StateMachineInstance* stateMachineInstance = nullptr; | |
| rive::LinearAnimationInstance* animationInstance = nullptr; | |
| uint8_t* fileBytes = nullptr; | |
| unsigned int fileBytesLength = 0; | |
| int animationIndex = 0; | |
| int stateMachineIndex = -1; | |
| void initStateMachine(int index) { | |
| stateMachineIndex = index; | |
| animationIndex = -1; | |
| assert(fileBytes != nullptr); | |
| auto reader = rive::BinaryReader(fileBytes, fileBytesLength); | |
| rive::File* file = nullptr; | |
| auto result = rive::File::import(reader, &file); | |
| if (result != rive::ImportResult::success) { | |
| delete[] fileBytes; | |
| fprintf(stderr, "failed to import file\n"); | |
| return; | |
| } | |
| auto sourceArtboard = file->artboard(); | |
| // Artboard should always be instance and hence must be deleted. | |
| delete artboard; | |
| artboard = sourceArtboard->instance(); | |
| artboard->advance(0.0f); | |
| delete animationInstance; | |
| delete stateMachineInstance; | |
| delete currentFile; | |
| animationInstance = nullptr; | |
| stateMachineInstance = nullptr; | |
| auto stateMachine = index >= 0 && index < artboard->stateMachineCount() | |
| ? artboard->stateMachine(index) | |
| : nullptr; | |
| if (stateMachine != nullptr) { | |
| stateMachineInstance = new rive::StateMachineInstance(stateMachine); | |
| } | |
| currentFile = file; | |
| } | |
| void initAnimation(int index) { | |
| animationIndex = index; | |
| stateMachineIndex = -1; | |
| assert(fileBytes != nullptr); | |
| auto reader = rive::BinaryReader(fileBytes, fileBytesLength); | |
| rive::File* file = nullptr; | |
| auto result = rive::File::import(reader, &file); | |
| if (result != rive::ImportResult::success) { | |
| delete[] fileBytes; | |
| fprintf(stderr, "failed to import file\n"); | |
| return; | |
| } | |
| auto sourceArtboard = file->artboard(); | |
| // Artboard should always be instance and hence must be deleted. | |
| delete artboard; | |
| artboard = sourceArtboard->instance(); | |
| artboard->advance(0.0f); | |
| delete animationInstance; | |
| delete stateMachineInstance; | |
| delete currentFile; | |
| animationInstance = nullptr; | |
| stateMachineInstance = nullptr; | |
| auto animation = | |
| index >= 0 && index < artboard->animationCount() ? artboard->animation(index) : nullptr; | |
| if (animation != nullptr) { | |
| animationInstance = new rive::LinearAnimationInstance(animation); | |
| } | |
| currentFile = file; | |
| } | |
| void glfwErrorCallback(int error, const char* description) { puts(description); } | |
| void glfwDropCallback(GLFWwindow* window, int count, const char** paths) { | |
| // Just get the last dropped file for now... | |
| filename = paths[count - 1]; | |
| FILE* fp = fopen(filename.c_str(), "r"); | |
| fseek(fp, 0, SEEK_END); | |
| fileBytesLength = ftell(fp); | |
| fseek(fp, 0, SEEK_SET); | |
| delete[] fileBytes; | |
| fileBytes = new uint8_t[fileBytesLength]; | |
| if (fread(fileBytes, 1, fileBytesLength, fp) != fileBytesLength) { | |
| delete[] fileBytes; | |
| fprintf(stderr, "failed to read all of %s\n", filename.c_str()); | |
| return; | |
| } | |
| initAnimation(0); | |
| } | |
| // returns the mouse position, transforming it through the inverse of | |
| // the canvas' CTM -- which may have been altered to scale/translate | |
| // the artboard into the window. | |
| // | |
| static void post_mouse_event(rive::Artboard* artboard, const SkMatrix& ctm) { | |
| static ImVec2 gPrevMousePos = {-1000, -1000}; | |
| const auto mouse = ImGui::GetMousePos(); | |
| static bool gPrevMouseButtonDown = false; | |
| const bool isDown = ImGui::IsMouseDown(ImGuiMouseButton_Left); | |
| if (mouse.x == gPrevMousePos.x && | |
| mouse.y == gPrevMousePos.y && | |
| isDown == gPrevMouseButtonDown) | |
| { | |
| return; | |
| } | |
| auto evtType = rive::PointerEventType::move; | |
| if (isDown && !gPrevMouseButtonDown) { | |
| evtType = rive::PointerEventType::down; // we just went down | |
| } else if (!isDown && gPrevMouseButtonDown) { | |
| evtType = rive::PointerEventType::up; // we just went up | |
| } | |
| gPrevMousePos = mouse; | |
| gPrevMouseButtonDown = isDown; | |
| SkMatrix inv; | |
| (void)ctm.invert(&inv); | |
| // scale by 2 for the DPI of a high-res monitor | |
| const auto pt = inv.mapXY(mouse.x * 2, mouse.y * 2); | |
| const int pointerIndex = 0; // til we track more than one button/mouse | |
| rive::PointerEvent evt = { | |
| evtType, | |
| {pt.fX, pt.fY}, | |
| pointerIndex, | |
| }; | |
| artboard->postPointerEvent(evt); | |
| } | |
| static void test_messages(rive::Artboard* artboard) { | |
| rive::Message msg; | |
| int i = 0; | |
| bool hasAny = artboard->hasMessages(); | |
| while (artboard->nextMessage(&msg)) { | |
| printf("-- message[%d]: '%s'\n", i, msg.m_Str.c_str()); | |
| i += 1; | |
| } | |
| assert((hasAny && i > 0) || (!hasAny && i == 0)); | |
| } | |
| int main() { | |
| if (!glfwInit()) { | |
| fprintf(stderr, "Failed to initialize glfw.\n"); | |
| return 1; | |
| } | |
| glfwSetErrorCallback(glfwErrorCallback); | |
| glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); | |
| glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); | |
| GLFWwindow* window = glfwCreateWindow(1280, 720, "Rive Viewer", NULL, NULL); | |
| if (window == nullptr) { | |
| fprintf(stderr, "Failed to make window or GL.\n"); | |
| glfwTerminate(); | |
| return 1; | |
| } | |
| glfwSetDropCallback(window, glfwDropCallback); | |
| glfwMakeContextCurrent(window); | |
| if (gl3wInit() != 0) { | |
| fprintf(stderr, "Failed to make initialize gl3w.\n"); | |
| glfwTerminate(); | |
| return 1; | |
| } | |
| // Enable VSYNC. | |
| glfwSwapInterval(1); | |
| // Setup ImGui | |
| ImGui::CreateContext(); | |
| ImGuiIO& io = ImGui::GetIO(); | |
| (void)io; | |
| ImGui::StyleColorsDark(); | |
| ImGui_ImplGlfw_InitForOpenGL(window, true); | |
| ImGui_ImplOpenGL3_Init("#version 150"); | |
| io.Fonts->AddFontDefault(); | |
| // Setup Skia | |
| GrContextOptions options; | |
| sk_sp<GrDirectContext> context = GrDirectContext::MakeGL(nullptr, options); | |
| GrGLFramebufferInfo framebufferInfo; | |
| framebufferInfo.fFBOID = 0; | |
| framebufferInfo.fFormat = GL_RGBA8; | |
| SkSurface* surface = nullptr; | |
| SkCanvas* canvas = nullptr; | |
| // Render loop. | |
| int width = 0, height = 0; | |
| int lastScreenWidth = 0, lastScreenHeight = 0; | |
| double lastTime = glfwGetTime(); | |
| while (!glfwWindowShouldClose(window)) { | |
| glfwGetFramebufferSize(window, &width, &height); | |
| // Update surface. | |
| if (surface == nullptr || width != lastScreenWidth || height != lastScreenHeight) { | |
| lastScreenWidth = width; | |
| lastScreenHeight = height; | |
| SkColorType colorType = | |
| kRGBA_8888_SkColorType; // GrColorTypeToSkColorType(GrPixelConfigToColorType(kRGBA_8888_GrPixelConfig)); | |
| // | |
| // if (kRGBA_8888_GrPixelConfig == kSkia8888_GrPixelConfig) | |
| // { | |
| // colorType = kRGBA_8888_SkColorType; | |
| // } | |
| // else | |
| // { | |
| // colorType = kBGRA_8888_SkColorType; | |
| // } | |
| GrBackendRenderTarget backendRenderTarget(width, | |
| height, | |
| 0, // sample count | |
| 0, // stencil bits | |
| framebufferInfo); | |
| delete surface; | |
| surface = SkSurface::MakeFromBackendRenderTarget(context.get(), | |
| backendRenderTarget, | |
| kBottomLeft_GrSurfaceOrigin, | |
| colorType, | |
| nullptr, | |
| nullptr) | |
| .release(); | |
| if (surface == nullptr) { | |
| fprintf(stderr, "Failed to create Skia surface\n"); | |
| return 1; | |
| } | |
| canvas = surface->getCanvas(); | |
| } | |
| double time = glfwGetTime(); | |
| float elapsed = (float)(time - lastTime); | |
| lastTime = time; | |
| // Clear screen. | |
| SkPaint paint; | |
| paint.setColor(SK_ColorDKGRAY); | |
| canvas->drawPaint(paint); | |
| if (artboard != nullptr) { | |
| if (animationInstance != nullptr) { | |
| animationInstance->advance(elapsed); | |
| animationInstance->apply(artboard); | |
| } else if (stateMachineInstance != nullptr) { | |
| stateMachineInstance->advance(artboard, elapsed); | |
| } | |
| artboard->advance(elapsed); | |
| rive::SkiaRenderer renderer(canvas); | |
| renderer.save(); | |
| renderer.align(rive::Fit::contain, | |
| rive::Alignment::center, | |
| rive::AABB(0, 0, width, height), | |
| artboard->bounds()); | |
| post_mouse_event(artboard, canvas->getTotalMatrix()); | |
| artboard->draw(&renderer); | |
| renderer.restore(); | |
| } | |
| context->flush(); | |
| ImGui_ImplOpenGL3_NewFrame(); | |
| ImGui_ImplGlfw_NewFrame(); | |
| ImGui::NewFrame(); | |
| if (artboard != nullptr) { | |
| ImGui::Begin(filename.c_str(), nullptr); | |
| if (ImGui::ListBox( | |
| "Animations", | |
| &animationIndex, | |
| [](void* data, int index, const char** name) { | |
| const char* animationName = artboard->animation(index)->name().c_str(); | |
| *name = animationName; | |
| return true; | |
| }, | |
| artboard, | |
| artboard->animationCount(), | |
| 4)) | |
| { | |
| stateMachineIndex = -1; | |
| initAnimation(animationIndex); | |
| } | |
| if (ImGui::ListBox( | |
| "State Machines", | |
| &stateMachineIndex, | |
| [](void* data, int index, const char** name) { | |
| const char* machineName = artboard->stateMachine(index)->name().c_str(); | |
| *name = machineName; | |
| return true; | |
| }, | |
| artboard, | |
| artboard->stateMachineCount(), | |
| 4)) | |
| { | |
| animationIndex = -1; | |
| initStateMachine(stateMachineIndex); | |
| } | |
| if (stateMachineInstance != nullptr) { | |
| ImGui::Columns(2); | |
| ImGui::SetColumnWidth(0, ImGui::GetWindowWidth() * 0.6666); | |
| for (int i = 0; i < stateMachineInstance->inputCount(); i++) { | |
| auto inputInstance = stateMachineInstance->input(i); | |
| if (inputInstance->input()->is<rive::StateMachineNumber>()) { | |
| // ImGui requires names as id's, use ## to hide the | |
| // label but still give it an id. | |
| char label[256]; | |
| snprintf(label, 256, "##%u", i); | |
| auto number = static_cast<rive::SMINumber*>(inputInstance); | |
| float v = number->value(); | |
| ImGui::InputFloat(label, &v, 1.0f, 2.0f, "%.3f"); | |
| number->value(v); | |
| ImGui::NextColumn(); | |
| } else if (inputInstance->input()->is<rive::StateMachineTrigger>()) { | |
| // ImGui requires names as id's, use ## to hide the | |
| // label but still give it an id. | |
| char label[256]; | |
| snprintf(label, 256, "Fire##%u", i); | |
| if (ImGui::Button(label)) { | |
| auto trigger = static_cast<rive::SMITrigger*>(inputInstance); | |
| trigger->fire(); | |
| } | |
| ImGui::NextColumn(); | |
| } else if (inputInstance->input()->is<rive::StateMachineBool>()) { | |
| // ImGui requires names as id's, use ## to hide the | |
| // label but still give it an id. | |
| char label[256]; | |
| snprintf(label, 256, "##%u", i); | |
| auto boolInput = static_cast<rive::SMIBool*>(inputInstance); | |
| bool value = boolInput->value(); | |
| ImGui::Checkbox(label, &value); | |
| boolInput->value(value); | |
| ImGui::NextColumn(); | |
| } | |
| ImGui::Text("%s", inputInstance->input()->name().c_str()); | |
| ImGui::NextColumn(); | |
| } | |
| ImGui::Columns(1); | |
| } | |
| ImGui::End(); | |
| test_messages(artboard); | |
| } else { | |
| ImGui::Text("Drop a .riv file to preview."); | |
| } | |
| ImGui::Render(); | |
| ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); | |
| glfwSwapBuffers(window); | |
| glfwPollEvents(); | |
| } | |
| delete currentFile; | |
| delete[] fileBytes; | |
| // Cleanup Skia. | |
| delete surface; | |
| context = nullptr; | |
| ImGui_ImplGlfw_Shutdown(); | |
| // Cleanup GLFW. | |
| glfwDestroyWindow(window); | |
| glfwTerminate(); | |
| return 0; | |
| } |