| #include "fiddle_context.hpp" | |
| #include "rive/math/simd.hpp" | |
| #include "rive/artboard.hpp" | |
| #include "rive/file.hpp" | |
| #include "rive/layout.hpp" | |
| #include "rive/animation/linear_animation_instance.hpp" | |
| #include <cmath> | |
| #include <fstream> | |
| #include <iterator> | |
| #include <vector> | |
| #define GLFW_INCLUDE_NONE | |
| #include "GLFW/glfw3.h" | |
| #ifdef RIVE_WASM | |
| #include <emscripten/emscripten.h> | |
| #include <emscripten/html5.h> | |
| #include <sstream> | |
| #endif | |
| using namespace rive; | |
| GLFWwindow* g_window = nullptr; | |
| bool g_wireframe = false; | |
| bool g_disableFill = false; | |
| bool g_disableStroke = false; | |
| static std::unique_ptr<FiddleContext> s_fiddleContext; | |
| static float2 s_pts[] = {{260 + 2 * 100, 60 + 2 * 500}, | |
| {260 + 2 * 257, 60 + 2 * 233}, | |
| {260 + 2 * -100, 60 + 2 * 300}, | |
| {260 + 2 * 100, 60 + 2 * 200}, | |
| {260 + 2 * 250, 60 + 2 * 0}, | |
| {260 + 2 * 400, 60 + 2 * 200}, | |
| {260 + 2 * 213, 60 + 2 * 200}, | |
| {260 + 2 * 213, 60 + 2 * 300}, | |
| {260 + 2 * 391, 60 + 2 * 480}}; | |
| constexpr static int kNumInteractivePts = sizeof(s_pts) / sizeof(*s_pts); | |
| static float s_strokeWidth = 70; | |
| static float2 s_translate; | |
| static float s_scale = 1; | |
| static StrokeJoin s_join = StrokeJoin::miter; | |
| static StrokeCap s_cap = StrokeCap::butt; | |
| static bool s_doClose = false; | |
| static bool s_paused = false; | |
| static int s_dragIdx = -1; | |
| static float2 s_dragLastPos; | |
| static int s_animation = 0; | |
| static int s_horzRepeat = 0; | |
| static int s_upRepeat = 0; | |
| static int s_downRepeat = 0; | |
| std::unique_ptr<Scene> s_scene; | |
| #ifdef RIVE_WASM | |
| EM_JS(int, window_inner_width, (), { return window["innerWidth"]; }); | |
| EM_JS(int, window_inner_height, (), { return window["innerHeight"]; }); | |
| EM_JS(char*, get_location_hash_str, (), { | |
| var jsString = window.location.hash.substring(1); | |
| var lengthBytes = lengthBytesUTF8(jsString) + 1; | |
| var stringOnWasmHeap = _malloc(lengthBytes); | |
| stringToUTF8(jsString, stringOnWasmHeap, lengthBytes); | |
| return stringOnWasmHeap; | |
| }); | |
| #endif | |
| static void mouse_button_callback(GLFWwindow* window, int button, int action, int mods) | |
| { | |
| double x, y; | |
| glfwGetCursorPos(window, &x, &y); | |
| x *= s_fiddleContext->dpiScale(); | |
| y *= s_fiddleContext->dpiScale(); | |
| s_dragLastPos = float2{(float)x, (float)y}; | |
| if (button == GLFW_MOUSE_BUTTON_LEFT && action == GLFW_PRESS) | |
| { | |
| s_dragIdx = -1; | |
| if (!s_scene) | |
| { | |
| for (int i = 0; i < kNumInteractivePts; ++i) | |
| { | |
| if (simd::all(simd::abs(s_dragLastPos - (s_pts[i] + s_translate)) < 100)) | |
| { | |
| s_dragIdx = i; | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| static void mousemove_callback(GLFWwindow* window, double x, double y) | |
| { | |
| x *= s_fiddleContext->dpiScale(); | |
| y *= s_fiddleContext->dpiScale(); | |
| if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS) | |
| { | |
| float2 pos = float2{(float)x, (float)y}; | |
| if (s_dragIdx >= 0) | |
| { | |
| s_pts[s_dragIdx] += (pos - s_dragLastPos); | |
| } | |
| else | |
| { | |
| s_translate += (pos - s_dragLastPos); | |
| } | |
| s_dragLastPos = pos; | |
| } | |
| } | |
| static void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods) | |
| { | |
| bool shift = mods & GLFW_MOD_SHIFT; | |
| if (action == GLFW_PRESS) | |
| { | |
| switch (key) | |
| { | |
| case GLFW_KEY_ESCAPE: | |
| glfwSetWindowShouldClose(window, 1); | |
| break; | |
| case GLFW_KEY_D: | |
| printf("static float s_scale = %f;\n", s_scale); | |
| printf("static float2 s_translate = {%f, %f};\n", s_translate.x, s_translate.y); | |
| printf("static float2 s_pts[] = {"); | |
| for (int i = 0; i < kNumInteractivePts; i++) | |
| { | |
| printf("{%g, %g}", s_pts[i].x, s_pts[i].y); | |
| if (i < kNumInteractivePts - 1) | |
| { | |
| printf(", "); | |
| } | |
| else | |
| { | |
| printf("};\n"); | |
| } | |
| } | |
| fflush(stdout); | |
| break; | |
| case GLFW_KEY_Z: | |
| s_fiddleContext->toggleZoomWindow(); | |
| break; | |
| case GLFW_KEY_MINUS: | |
| s_strokeWidth /= 1.5f; | |
| break; | |
| case GLFW_KEY_EQUAL: | |
| s_strokeWidth *= 1.5f; | |
| break; | |
| case GLFW_KEY_W: | |
| g_wireframe = !g_wireframe; | |
| break; | |
| case GLFW_KEY_C: | |
| s_cap = static_cast<StrokeCap>((static_cast<int>(s_cap) + 1) % 3); | |
| break; | |
| case GLFW_KEY_O: | |
| s_doClose = !s_doClose; | |
| break; | |
| case GLFW_KEY_S: | |
| g_disableStroke = !g_disableStroke; | |
| break; | |
| case GLFW_KEY_F: | |
| g_disableFill = !g_disableFill; | |
| break; | |
| case GLFW_KEY_P: | |
| s_paused = !s_paused; | |
| break; | |
| case GLFW_KEY_H: | |
| if (!shift) | |
| ++s_horzRepeat; | |
| else if (s_horzRepeat > 0) | |
| --s_horzRepeat; | |
| break; | |
| case GLFW_KEY_K: | |
| if (!shift) | |
| ++s_upRepeat; | |
| else if (s_upRepeat > 0) | |
| --s_upRepeat; | |
| break; | |
| case GLFW_KEY_J: | |
| if (!s_scene) | |
| s_join = static_cast<StrokeJoin>((static_cast<int>(s_join) + 1) % 3); | |
| else if (!shift) | |
| ++s_downRepeat; | |
| else if (s_downRepeat > 0) | |
| --s_downRepeat; | |
| break; | |
| case GLFW_KEY_UP: | |
| { | |
| float oldScale = s_scale; | |
| s_scale *= 1.25; | |
| double x = 0, y = 0; | |
| glfwGetCursorPos(window, &x, &y); | |
| float2 cursorPos = float2{(float)x, (float)y} * s_fiddleContext->dpiScale(); | |
| s_translate = cursorPos + (s_translate - cursorPos) * s_scale / oldScale; | |
| break; | |
| } | |
| case GLFW_KEY_DOWN: | |
| { | |
| float oldScale = s_scale; | |
| s_scale /= 1.25; | |
| double x = 0, y = 0; | |
| glfwGetCursorPos(window, &x, &y); | |
| float2 cursorPos = float2{(float)x, (float)y} * s_fiddleContext->dpiScale(); | |
| s_translate = cursorPos + (s_translate - cursorPos) * s_scale / oldScale; | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| bool metal = false; | |
| bool angle = false; | |
| bool skia = false; | |
| int lastWidth = 0, lastHeight = 0; | |
| double fpsLastTime = glfwGetTime(); | |
| int fpsFrames = 0; | |
| std::unique_ptr<Factory> factory; | |
| std::unique_ptr<Renderer> renderer; | |
| std::unique_ptr<File> rivFile; | |
| std::unique_ptr<ArtboardInstance> artboard; | |
| void riveMainLoop(); | |
| int main(int argc, const char** argv) | |
| { | |
| // Cause stdout and stderr to print immediately without buffering. | |
| setvbuf(stdout, NULL, _IONBF, 0); | |
| setvbuf(stderr, NULL, _IONBF, 0); | |
| #ifdef RIVE_WASM | |
| emscripten_set_main_loop(riveMainLoop, 0, false); | |
| // Override argc/argv with the window location hash string. | |
| char* hash = get_location_hash_str(); | |
| std::stringstream ss(hash); | |
| std::vector<std::string> hashStrs; | |
| std::vector<const char*> hashArgs; | |
| std::string arg; | |
| hashStrs.push_back("index.html"); | |
| while (std::getline(ss, arg, ':')) | |
| { | |
| hashStrs.push_back(std::move(arg)); | |
| } | |
| for (const std::string& str : hashStrs) | |
| { | |
| hashArgs.push_back(str.c_str()); | |
| } | |
| argc = hashArgs.size(); | |
| argv = hashArgs.data(); | |
| free(hash); | |
| #endif | |
| const char* rivName = nullptr; | |
| for (int i = 1; i < argc; i++) | |
| { | |
| if (!strcmp(argv[i], "--gl")) | |
| { | |
| metal = false; | |
| } | |
| else if (!strcmp(argv[i], "--metal")) | |
| { | |
| metal = true; | |
| } | |
| else if (!strcmp(argv[i], "--skia")) | |
| { | |
| skia = true; | |
| } | |
| #ifdef RIVE_DESKTOP_GL | |
| else if (!strcmp(argv[i], "--angle_gl")) | |
| { | |
| glfwInitHint(GLFW_ANGLE_PLATFORM_TYPE, GLFW_ANGLE_PLATFORM_TYPE_OPENGL); | |
| angle = true; | |
| } | |
| else if (!strcmp(argv[i], "--angle_d3d")) | |
| { | |
| glfwInitHint(GLFW_ANGLE_PLATFORM_TYPE, GLFW_ANGLE_PLATFORM_TYPE_D3D11); | |
| angle = true; | |
| } | |
| else if (!strcmp(argv[i], "--angle_vk")) | |
| { | |
| glfwInitHint(GLFW_ANGLE_PLATFORM_TYPE, GLFW_ANGLE_PLATFORM_TYPE_VULKAN); | |
| angle = true; | |
| } | |
| else if (!strcmp(argv[i], "--angle_mtl")) | |
| { | |
| glfwInitHint(GLFW_ANGLE_PLATFORM_TYPE, GLFW_ANGLE_PLATFORM_TYPE_METAL); | |
| angle = true; | |
| } | |
| #endif | |
| else if (sscanf(argv[i], "-a%i", &s_animation)) | |
| {} | |
| else if (sscanf(argv[i], "-h%i", &s_horzRepeat)) | |
| {} | |
| else if (sscanf(argv[i], "-j%i", &s_downRepeat)) | |
| {} | |
| else if (sscanf(argv[i], "-k%i", &s_upRepeat)) | |
| {} | |
| else if (!strcmp(argv[i], "-p")) | |
| { | |
| s_paused = true; | |
| } | |
| else | |
| { | |
| rivName = argv[i]; | |
| } | |
| } | |
| if (!glfwInit()) | |
| { | |
| fprintf(stderr, "Failed to initialize glfw.\n"); | |
| return 1; | |
| } | |
| glfwWindowHint(GLFW_SAMPLES, 0); | |
| glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE); | |
| if (metal) | |
| { | |
| glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); | |
| glfwWindowHint(GLFW_COCOA_RETINA_FRAMEBUFFER, GLFW_TRUE); | |
| } | |
| else if (angle) | |
| { | |
| glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_EGL_CONTEXT_API); | |
| glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API); | |
| glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); | |
| glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); | |
| glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); | |
| } | |
| else | |
| { | |
| glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API); | |
| } | |
| g_window = glfwCreateWindow(1600, 1600, "Rive Renderer", nullptr, nullptr); | |
| if (!g_window) | |
| { | |
| glfwTerminate(); | |
| fprintf(stderr, "Failed to create window.\n"); | |
| return -1; | |
| } | |
| glfwSetWindowTitle(g_window, "Rive Renderer"); | |
| glfwSetMouseButtonCallback(g_window, mouse_button_callback); | |
| glfwSetCursorPosCallback(g_window, mousemove_callback); | |
| glfwSetKeyCallback(g_window, key_callback); | |
| glfwMakeContextCurrent(g_window); | |
| if (metal) | |
| { | |
| if (skia) | |
| { | |
| fprintf(stderr, "Skia not supported on Metal yet.\n"); | |
| exit(-1); | |
| } | |
| #ifdef __APPLE__ | |
| s_fiddleContext = FiddleContext::MakeMetalPLS(); | |
| #endif | |
| } | |
| else if (skia) | |
| { | |
| #ifdef RIVE_SKIA | |
| s_fiddleContext = FiddleContext::MakeGLSkia(); | |
| #endif | |
| } | |
| else | |
| { | |
| s_fiddleContext = FiddleContext::MakeGLPLS(); | |
| } | |
| if (!s_fiddleContext) | |
| { | |
| fprintf(stderr, "Failed to create a fiddle context.\n"); | |
| exit(-1); | |
| } | |
| factory = s_fiddleContext->makeFactory(); | |
| if (rivName) | |
| { | |
| std::ifstream rivStream(rivName, std::ios::binary); | |
| std::vector<uint8_t> rivBytes(std::istreambuf_iterator<char>(rivStream), {}); | |
| rivFile = File::import(rivBytes, factory.get()); | |
| artboard = rivFile->artboardDefault(); | |
| s_scene = artboard->animationAt(s_animation); | |
| s_scene->advanceAndApply(0); | |
| } | |
| #ifdef RIVE_DESKTOP_GL | |
| glfwSwapInterval(0); | |
| while (!glfwWindowShouldClose(g_window)) | |
| { | |
| riveMainLoop(); | |
| glfwSwapBuffers(g_window); | |
| if (s_scene) | |
| { | |
| glfwPollEvents(); | |
| } | |
| else | |
| { | |
| glfwWaitEvents(); | |
| } | |
| } | |
| glfwTerminate(); | |
| #endif | |
| return 0; | |
| } | |
| void riveMainLoop() | |
| { | |
| #ifdef RIVE_WASM | |
| { | |
| // Fit the canvas to the browser window size. | |
| int windowWidth = window_inner_width(); | |
| int windowHeight = window_inner_height(); | |
| double devicePixelRatio = emscripten_get_device_pixel_ratio(); | |
| int canvasExpectedWidth = windowWidth * devicePixelRatio; | |
| int canvasExpectedHeight = windowHeight * devicePixelRatio; | |
| int canvasWidth, canvasHeight; | |
| glfwGetFramebufferSize(g_window, &canvasWidth, &canvasHeight); | |
| if (canvasWidth != canvasExpectedWidth || canvasHeight != canvasExpectedHeight) | |
| { | |
| glfwSetWindowSize(g_window, canvasExpectedWidth, canvasExpectedHeight); | |
| emscripten_set_element_css_size("#canvas", windowWidth, windowHeight); | |
| } | |
| } | |
| #endif | |
| int width = 0, height = 0; | |
| glfwGetFramebufferSize(g_window, &width, &height); | |
| if (lastWidth != width || lastHeight != height) | |
| { | |
| printf("size changed to %ix%i\n", width, height); | |
| lastWidth = width; | |
| lastHeight = height; | |
| s_fiddleContext->onSizeChanged(width, height); | |
| renderer = s_fiddleContext->makeRenderer(width, height); | |
| } | |
| s_fiddleContext->begin(); | |
| int instances = 1; | |
| if (s_scene) | |
| { | |
| instances = (1 + s_horzRepeat * 2) * (1 + s_upRepeat + s_downRepeat); | |
| float iinstances = instances > 1 ? s_scene->durationSeconds() / instances : 0; | |
| Mat2D m = computeAlignment(rive::Fit::contain, | |
| rive::Alignment::center, | |
| rive::AABB(0, 0, width, height), | |
| artboard->bounds()); | |
| renderer->save(); | |
| m = Mat2D(s_scale, 0, 0, s_scale, s_translate.x, s_translate.y) * m; | |
| renderer->transform(m); | |
| float spacing = 200 / m.findMaxScale(); | |
| for (int j = 0; j < s_upRepeat + 1 + s_downRepeat; ++j) | |
| { | |
| renderer->save(); | |
| renderer->transform( | |
| Mat2D::fromTranslate(-spacing * s_horzRepeat, (j - s_upRepeat) * spacing)); | |
| for (int i = 0; i < s_horzRepeat * 2 + 1; ++i) | |
| { | |
| s_scene->draw(renderer.get()); | |
| renderer->transform(Mat2D::fromTranslate(spacing, 0)); | |
| if (!s_paused) | |
| { | |
| s_scene->advanceAndApply(iinstances); | |
| } | |
| } | |
| renderer->restore(); | |
| } | |
| renderer->restore(); | |
| if (!s_paused) | |
| { | |
| s_scene->advanceAndApply(1 / 120.f); | |
| } | |
| } | |
| else | |
| { | |
| float2 p[9]; | |
| for (int i = 0; i < 9; ++i) | |
| { | |
| p[i] = s_pts[i] + s_translate; | |
| } | |
| RawPath rawPath; | |
| rawPath.moveTo(p[0].x, p[0].y); | |
| rawPath.cubicTo(p[1].x, p[1].y, p[2].x, p[2].y, p[3].x, p[3].y); | |
| float2 c0 = simd::mix(p[3], p[4], float2(2 / 3.f)); | |
| float2 c1 = simd::mix(p[5], p[4], float2(2 / 3.f)); | |
| rawPath.cubicTo(c0.x, c0.y, c1.x, c1.y, p[5].x, p[5].y); | |
| rawPath.cubicTo(p[6].x, p[6].y, p[7].x, p[7].y, p[8].x, p[8].y); | |
| if (s_doClose) | |
| { | |
| rawPath.close(); | |
| } | |
| auto path = factory->makeRenderPath(rawPath, FillRule::nonZero); | |
| auto fillPaint = factory->makeRenderPaint(); | |
| fillPaint->style(RenderPaintStyle::fill); | |
| fillPaint->color(-1); | |
| auto strokePaint = factory->makeRenderPaint(); | |
| strokePaint->style(RenderPaintStyle::stroke); | |
| strokePaint->color(0x8000ffff); | |
| strokePaint->thickness(s_strokeWidth); | |
| strokePaint->join(s_join); | |
| strokePaint->cap(s_cap); | |
| renderer->drawPath(path.get(), fillPaint.get()); | |
| renderer->drawPath(path.get(), strokePaint.get()); | |
| // Draw the interactive points. | |
| auto pointPaint = factory->makeRenderPaint(); | |
| pointPaint->style(RenderPaintStyle::stroke); | |
| pointPaint->color(0xff0000ff); | |
| pointPaint->thickness(14); | |
| pointPaint->cap(StrokeCap::round); | |
| auto pointPath = factory->makeEmptyRenderPath(); | |
| for (int i : {1, 2, 4, 6, 7}) | |
| { | |
| float2 pt = s_pts[i] + s_translate; | |
| pointPath->moveTo(pt.x, pt.y); | |
| } | |
| renderer->drawPath(pointPath.get(), pointPaint.get()); | |
| } | |
| s_fiddleContext->end(); | |
| if (s_scene) | |
| { | |
| // Count FPS. | |
| ++fpsFrames; | |
| double time = glfwGetTime(); | |
| double fpsElapsed = time - fpsLastTime; | |
| if (fpsElapsed > 2) | |
| { | |
| int instances = (1 + s_horzRepeat * 2) * (1 + s_upRepeat + s_downRepeat); | |
| double fps = fpsFrames / fpsElapsed; | |
| // printf("%.1f FPS\n", fps); | |
| // fflush(stdout); | |
| char title[100]; | |
| if (instances > 1) | |
| { | |
| snprintf(title, | |
| sizeof(title), | |
| "[%.1f FPS] (x%i instances) | [%.1f ms] | %s Renderer | %i x %i", | |
| fps, | |
| instances, | |
| 1000 / fps, | |
| skia ? "Skia" : "Rive", | |
| width, | |
| height); | |
| } | |
| else | |
| { | |
| snprintf(title, | |
| sizeof(title), | |
| "[%.1f FPS] | [%.1f ms] | %s Renderer | %i x %i", | |
| fps, | |
| 1000 / fps, | |
| skia ? "Skia" : "Rive", | |
| width, | |
| height); | |
| } | |
| glfwSetWindowTitle(g_window, title); | |
| fpsFrames = 0; | |
| fpsLastTime = time; | |
| s_fiddleContext->shrinkGPUResourcesToFit(); | |
| } | |
| } | |
| } |