@@ -1,5 +1,8 @@ | |||
# DISTRHO CVCRack | |||
WORK IN PROGRESS | |||
WORK IN PROGRESS - nothing to see here | |||
Just an experiment to see how far we can go about a VCV plugin version that is opensource and uses original VCV source core directly. | |||
Maybe it will work, maybe not, hard to say at this point. | |||
This is a DPF'ied build of [VCVRack](https://github.com/VCVRack/Rack), allowing it to be used as an audio plugin. |
@@ -1 +1 @@ | |||
Subproject commit c4e1210897e804a19b3fa35d542765b3feb0236e | |||
Subproject commit 23f89562acbd637a23b9f0333877939ad26c0595 |
@@ -14,12 +14,102 @@ | |||
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | |||
*/ | |||
#include <asset.hpp> | |||
#include <audio.hpp> | |||
#include <context.hpp> | |||
#include <library.hpp> | |||
#include <keyboard.hpp> | |||
#include <midi.hpp> | |||
#include <plugin.hpp> | |||
#include <random.hpp> | |||
#include <settings.hpp> | |||
#include <system.hpp> | |||
#include <osdialog.h> | |||
#include "DistrhoPlugin.hpp" | |||
START_NAMESPACE_DISTRHO | |||
// ----------------------------------------------------------------------------------------------------------- | |||
struct Initializer { | |||
Initializer() | |||
{ | |||
using namespace rack; | |||
settings::devMode = true; | |||
system::init(); | |||
asset::init(); | |||
logger::init(); | |||
random::init(); | |||
// Log environment | |||
INFO("%s %s v%s", APP_NAME.c_str(), APP_EDITION.c_str(), APP_VERSION.c_str()); | |||
INFO("%s", system::getOperatingSystemInfo().c_str()); | |||
INFO("System directory: %s", asset::systemDir.c_str()); | |||
INFO("User directory: %s", asset::userDir.c_str()); | |||
INFO("System time: %s", string::formatTimeISO(system::getUnixTime()).c_str()); | |||
// Load settings | |||
settings::init(); | |||
try { | |||
settings::load(); | |||
} | |||
catch (Exception& e) { | |||
std::string message = e.what(); | |||
message += "\n\nResetting settings to default"; | |||
d_stdout(message.c_str()); | |||
/* | |||
if (!osdialog_message(OSDIALOG_WARNING, OSDIALOG_OK_CANCEL, msg.c_str())) { | |||
exit(1); | |||
} | |||
*/ | |||
} | |||
// Check existence of the system res/ directory | |||
std::string resDir = asset::system("res"); | |||
if (!system::isDirectory(resDir)) { | |||
std::string message = string::f("Rack's resource directory \"%s\" does not exist. Make sure Rack is correctly installed and launched.", resDir.c_str()); | |||
d_stderr2(message.c_str()); | |||
/* | |||
osdialog_message(OSDIALOG_ERROR, OSDIALOG_OK, message.c_str()); | |||
*/ | |||
exit(1); | |||
} | |||
INFO("Initializing environment"); | |||
// network::init(); | |||
audio::init(); | |||
// rtaudioInit(); | |||
midi::init(); | |||
// rtmidiInit(); | |||
keyboard::init(); | |||
plugin::init(); | |||
library::init(); | |||
// discord::init(); | |||
} | |||
~Initializer() | |||
{ | |||
using namespace rack; | |||
// discord::destroy(); | |||
library::destroy(); | |||
midi::destroy(); | |||
audio::destroy(); | |||
plugin::destroy(); | |||
INFO("Destroying logger"); | |||
logger::destroy(); | |||
} | |||
}; | |||
static Initializer& getInitializerInstance() | |||
{ | |||
static Initializer init; | |||
return init; | |||
} | |||
/** | |||
Plugin to demonstrate parameter outputs using meters. | |||
*/ | |||
@@ -130,6 +220,7 @@ private: | |||
Plugin* createPlugin() | |||
{ | |||
getInitializerInstance(); | |||
return new CVCRackPlugin(); | |||
} | |||
@@ -14,19 +14,92 @@ | |||
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | |||
*/ | |||
#include <app/common.hpp> | |||
#include <app/Scene.hpp> | |||
#include <context.hpp> | |||
#include <engine/Engine.hpp> | |||
#include <patch.hpp> | |||
#include <ui/common.hpp> | |||
#include <window/Window.hpp> | |||
#include "DistrhoUI.hpp" | |||
GLFWAPI const char* glfwGetClipboardString(GLFWwindow* window) { return nullptr; } | |||
GLFWAPI void glfwSetClipboardString(GLFWwindow* window, const char*) {} | |||
GLFWAPI const char* glfwGetKeyName(int key, int scancode) { return nullptr; } | |||
GLFWAPI int glfwGetKeyScancode(int key) { return 0; } | |||
namespace rack { | |||
namespace window { | |||
DISTRHO_NAMESPACE::UI* lastUI = nullptr; | |||
} | |||
} | |||
START_NAMESPACE_DISTRHO | |||
// ----------------------------------------------------------------------------------------------------------- | |||
struct Initializer { | |||
Initializer() | |||
{ | |||
using namespace rack; | |||
ui::init(); | |||
window::init(); | |||
} | |||
~Initializer() | |||
{ | |||
using namespace rack; | |||
window::destroy(); | |||
ui::destroy(); | |||
} | |||
}; | |||
static Initializer& getInitializerInstance() | |||
{ | |||
static Initializer init; | |||
return init; | |||
} | |||
class CVCRackUI : public UI | |||
{ | |||
public: | |||
CVCRackUI() | |||
: UI(128, 512) | |||
: UI(1280, 720) | |||
{ | |||
setGeometryConstraints(32, 128, false); | |||
using namespace rack; | |||
// Initialize context | |||
INFO("Initializing context"); | |||
window::lastUI = this; | |||
contextSet(new Context); | |||
APP->engine = new engine::Engine; | |||
APP->history = new history::State; | |||
APP->event = new widget::EventState; | |||
APP->scene = new app::Scene; | |||
APP->event->rootWidget = APP->scene; | |||
APP->patch = new patch::Manager; | |||
/*if (!settings::headless)*/ { | |||
APP->window = new window::Window; | |||
} | |||
window::lastUI = nullptr; | |||
APP->engine->startFallbackThread(); | |||
} | |||
~CVCRackUI() override | |||
{ | |||
using namespace rack; | |||
delete APP; | |||
contextSet(NULL); | |||
} | |||
void onNanoDisplay() override | |||
{ | |||
APP->window->step(); | |||
} | |||
protected: | |||
@@ -41,16 +114,6 @@ protected: | |||
{ | |||
} | |||
/* -------------------------------------------------------------------------------------------------------- | |||
* Widget Callbacks */ | |||
/** | |||
The drawing function. | |||
*/ | |||
void onDisplay() override | |||
{ | |||
} | |||
// ------------------------------------------------------------------------------------------------------- | |||
private: | |||
@@ -65,6 +128,7 @@ private: | |||
UI* createUI() | |||
{ | |||
getInitializerInstance(); | |||
return new CVCRackUI(); | |||
} | |||
@@ -28,6 +28,9 @@ | |||
#define DISTRHO_PLUGIN_WANT_DIRECT_ACCESS 1 | |||
// #define DISTRHO_PLUGIN_LV2_CATEGORY "lv2:AnalyserPlugin" | |||
// #define DISTRHO_PLUGIN_VST3_CATEGORIES "Fx|Analyzer" | |||
// #define DISTRHO_PLUGIN_HAS_EMBED_UI 1 | |||
// #define DISTRHO_PLUGIN_HAS_EXTERNAL_UI 1 | |||
#define DISTRHO_UI_USE_NANOVG 1 | |||
#define DISTRHO_UI_USER_RESIZABLE 1 | |||
enum Parameters { | |||
@@ -16,11 +16,14 @@ FILES_DSP = \ | |||
CVCRackPlugin.cpp | |||
FILES_UI = \ | |||
CVCRackUI.cpp | |||
CVCRackUI.cpp \ | |||
dep.cpp \ | |||
Window.cpp | |||
# -------------------------------------------------------------- | |||
# Import base definitions | |||
# UI_TYPE = external | |||
include ../../dpf/Makefile.base.mk | |||
# -------------------------------------------------------------- | |||
@@ -30,8 +33,6 @@ FILES_DSP += Rack/dep/pffft/pffft.c | |||
FILES_DSP += Rack/dep/pffft/fftpack.c | |||
FILES_UI += Rack/dep/oui-blendish/blendish.c | |||
FILES_UI += Rack/dep/nanovg/src/nanovg.c | |||
# FILES_UI += Rack/dep/glfw/deps/glad.c | |||
# FIXME dont use this | |||
FILES_UI += Rack/dep/osdialog/osdialog.c | |||
@@ -45,13 +46,15 @@ endif | |||
FILES_DSP += $(wildcard Rack/src/*.c) | |||
FILES_DSP += $(wildcard Rack/src/*/*.c) | |||
FILES_DSP += $(filter-out Rack/src/network.cpp Rack/src/rtaudio.cpp Rack/src/rtmidi.cpp, $(wildcard Rack/src/*.cpp)) | |||
FILES_DSP += $(wildcard Rack/src/*/*.cpp) | |||
FILES_DSP += $(filter-out Rack/src/dep.cpp Rack/src/gamepad.cpp Rack/src/rtaudio.cpp Rack/src/rtmidi.cpp, $(wildcard Rack/src/*.cpp)) | |||
FILES_DSP += $(filter-out Rack/src/window/Window.cpp, $(wildcard Rack/src/*/*.cpp)) | |||
# EXTRA_LIBS = Rack/dep/lib/libcurl.a | |||
EXTRA_LIBS += Rack/dep/lib/libglfw3.a | |||
EXTRA_LIBS = Rack/dep/lib/libcrypto.a | |||
EXTRA_LIBS += Rack/dep/lib/libcurl.a | |||
# EXTRA_LIBS += Rack/dep/lib/libglfw3.a | |||
EXTRA_LIBS += Rack/dep/lib/libjansson.a | |||
EXTRA_LIBS += Rack/dep/lib/libspeexdsp.a | |||
EXTRA_LIBS += Rack/dep/lib/libssl.a | |||
EXTRA_LIBS += Rack/dep/lib/libzstd.a | |||
ifeq ($(WINDOWS),true) | |||
@@ -76,16 +79,20 @@ endif | |||
Rack/dep/lib/%.a: | |||
$(MAKE) CMAKE="$(CMAKE) -DCMAKE_INSTALL_LIBDIR=lib -DCMAKE_INSTALL_PREFIX='$(abspath Rack/dep)'" -C Rack/dep lib/$*.a | |||
Rack/dep/lib/libcrypto.a: Rack/dep/lib/libssl.a | |||
# -------------------------------------------------------------- | |||
# Extra flags for VCV stuff | |||
BASE_FLAGS += -D_APP_VERSION=2.git.0 | |||
BASE_FLAGS += -I$(DPF_PATH)/dgl/src/nanovg | |||
BASE_FLAGS += -IRack/include | |||
BASE_FLAGS += -IRack/dep/include | |||
BASE_FLAGS += -IRack/dep/filesystem/include | |||
BASE_FLAGS += -IRack/dep/fuzzysearchdatabase/src | |||
BASE_FLAGS += -IRack/dep/glfw/deps | |||
BASE_FLAGS += -IRack/dep/nanovg/src | |||
BASE_FLAGS += -IRack/dep/glfw/include | |||
# BASE_FLAGS += -IRack/dep/nanovg/src | |||
BASE_FLAGS += -IRack/dep/nanosvg/src | |||
BASE_FLAGS += -IRack/dep/osdialog | |||
BASE_FLAGS += -IRack/dep/oui-blendish | |||
@@ -115,8 +122,11 @@ endif | |||
ifeq ($(MACOS),true) | |||
LINK_FLAGS += -framework IOKit | |||
# LINK_FLAGS += -Wl,-all_load | |||
endif | |||
# LINK_FLAGS += $(OPENGL_LIBS) | |||
# TODO needed on windows? need to check | |||
LINK_FLAGS += -lpthread | |||
@@ -0,0 +1,381 @@ | |||
#include <map> | |||
#include <queue> | |||
#include <thread> | |||
#include <stb_image_write.h> | |||
#include <osdialog.h> | |||
#include <window/Window.hpp> | |||
#include <asset.hpp> | |||
#include <widget/Widget.hpp> | |||
#include <app/Scene.hpp> | |||
#include <keyboard.hpp> | |||
#include <context.hpp> | |||
#include <patch.hpp> | |||
#include <settings.hpp> | |||
#include <plugin.hpp> // used in Window::screenshot | |||
#include <system.hpp> // used in Window::screenshot | |||
#include "DistrhoUI.hpp" | |||
namespace rack { | |||
namespace window { | |||
extern DISTRHO_NAMESPACE::UI* lastUI; | |||
static const math::Vec minWindowSize = math::Vec(640, 480); | |||
void Font::loadFile(const std::string& filename, NVGcontext* vg) { | |||
this->vg = vg; | |||
handle = nvgCreateFont(vg, filename.c_str(), filename.c_str()); | |||
if (handle < 0) | |||
throw Exception("Failed to load font %s", filename.c_str()); | |||
INFO("Loaded font %s", filename.c_str()); | |||
} | |||
Font::~Font() { | |||
// There is no NanoVG deleteFont() function yet, so do nothing | |||
} | |||
std::shared_ptr<Font> Font::load(const std::string& filename) { | |||
return APP->window->loadFont(filename); | |||
} | |||
void Image::loadFile(const std::string& filename, NVGcontext* vg) { | |||
this->vg = vg; | |||
handle = nvgCreateImage(vg, filename.c_str(), NVG_IMAGE_REPEATX | NVG_IMAGE_REPEATY); | |||
if (handle <= 0) | |||
throw Exception("Failed to load image %s", filename.c_str()); | |||
INFO("Loaded image %s", filename.c_str()); | |||
} | |||
Image::~Image() { | |||
// TODO What if handle is invalid? | |||
if (handle >= 0) | |||
nvgDeleteImage(vg, handle); | |||
} | |||
std::shared_ptr<Image> Image::load(const std::string& filename) { | |||
return APP->window->loadImage(filename); | |||
} | |||
struct Window::Internal { | |||
DISTRHO_NAMESPACE::UI* ui; | |||
std::string lastWindowTitle; | |||
int lastWindowX = 0; | |||
int lastWindowY = 0; | |||
int lastWindowWidth = 0; | |||
int lastWindowHeight = 0; | |||
int frame = 0; | |||
bool ignoreNextMouseDelta = false; | |||
int frameSwapInterval = -1; | |||
double monitorRefreshRate = 0.0; | |||
double frameTime = 0.0; | |||
double lastFrameDuration = 0.0; | |||
math::Vec lastMousePos; | |||
std::map<std::string, std::shared_ptr<Font>> fontCache; | |||
std::map<std::string, std::shared_ptr<Image>> imageCache; | |||
bool fbDirtyOnSubpixelChange = true; | |||
}; | |||
Window::Window() { | |||
internal = new Internal; | |||
internal->ui = lastUI; | |||
vg = lastUI->getContext(); | |||
int err; | |||
const GLubyte* vendor = glGetString(GL_VENDOR); | |||
const GLubyte* renderer = glGetString(GL_RENDERER); | |||
const GLubyte* version = glGetString(GL_VERSION); | |||
INFO("Renderer: %s %s", vendor, renderer); | |||
INFO("OpenGL: %s", version); | |||
INFO("UI pointer: %p", lastUI); | |||
// GLEW generates GL error because it calls glGetString(GL_EXTENSIONS), we'll consume it here. | |||
glGetError(); | |||
// Load default Blendish font | |||
uiFont = loadFont(asset::system("res/fonts/DejaVuSans.ttf")); | |||
bndSetFont(uiFont->handle); | |||
if (APP->scene) { | |||
widget::Widget::ContextCreateEvent e; | |||
APP->scene->onContextCreate(e); | |||
} | |||
} | |||
Window::~Window() { | |||
if (APP->scene) { | |||
widget::Widget::ContextDestroyEvent e; | |||
APP->scene->onContextDestroy(e); | |||
} | |||
// Fonts and Images in the cache must be deleted before the NanoVG context is deleted | |||
internal->fontCache.clear(); | |||
internal->imageCache.clear(); | |||
// nvgDeleteClone(fbVg); | |||
delete internal; | |||
} | |||
math::Vec Window::getSize() { | |||
return math::Vec(1280, 720); | |||
} | |||
void Window::setSize(math::Vec size) { | |||
size = size.max(minWindowSize); | |||
} | |||
void Window::run() { | |||
internal->frame = 0; | |||
} | |||
void Window::step() { | |||
double frameTime = system::getTime(); | |||
double lastFrameTime = internal->frameTime; | |||
internal->frameTime = frameTime; | |||
internal->lastFrameDuration = frameTime - lastFrameTime; | |||
// DEBUG("%.2lf Hz", 1.0 / internal->lastFrameDuration); | |||
double t1 = 0.0, t2 = 0.0, t3 = 0.0, t4 = 0.0, t5 = 0.0; | |||
// Make event handlers and step() have a clean NanoVG context | |||
nvgReset(vg); | |||
bndSetFont(uiFont->handle); | |||
nvgFillColor(vg, nvgRGBf(1, 1, 1)); | |||
nvgStrokeColor(vg, nvgRGBf(1, 1, 1)); | |||
// Poll events | |||
// Save and restore context because event handler set their own context based on which window they originate from. | |||
Context* context = contextGet(); | |||
// glfwPollEvents(); | |||
contextSet(context); | |||
// Set window title | |||
std::string windowTitle = APP_NAME + " " + APP_EDITION_NAME + " " + APP_VERSION; | |||
if (APP->patch->path != "") { | |||
windowTitle += " - "; | |||
if (!APP->history->isSaved()) | |||
windowTitle += "*"; | |||
windowTitle += system::getFilename(APP->patch->path); | |||
} | |||
if (windowTitle != internal->lastWindowTitle) { | |||
internal->ui->getWindow().setTitle(windowTitle.c_str()); | |||
internal->lastWindowTitle = windowTitle; | |||
} | |||
// Get desired pixel ratio | |||
float newPixelRatio = internal->ui->getScaleFactor(); | |||
if (newPixelRatio != pixelRatio) { | |||
pixelRatio = newPixelRatio; | |||
APP->event->handleDirty(); | |||
} | |||
// Get framebuffer/window ratio | |||
int winWidth = internal->ui->getWidth(); | |||
int winHeight = internal->ui->getHeight(); | |||
int fbWidth = winWidth;// * newPixelRatio; | |||
int fbHeight = winHeight;// * newPixelRatio; | |||
windowRatio = (float)fbWidth / winWidth; | |||
t1 = system::getTime(); | |||
if (APP->scene) { | |||
// DEBUG("%f %f %d %d", pixelRatio, windowRatio, fbWidth, winWidth); | |||
// Resize scene | |||
APP->scene->box.size = math::Vec(fbWidth, fbHeight).div(pixelRatio); | |||
// Step scene | |||
APP->scene->step(); | |||
t2 = system::getTime(); | |||
// Render scene | |||
bool visible = true; | |||
if (visible) { | |||
// Update and render | |||
nvgBeginFrame(vg, fbWidth, fbHeight, pixelRatio); | |||
nvgScale(vg, pixelRatio, pixelRatio); | |||
// Draw scene | |||
widget::Widget::DrawArgs args; | |||
args.vg = vg; | |||
args.clipBox = APP->scene->box.zeroPos(); | |||
APP->scene->draw(args); | |||
t3 = system::getTime(); | |||
// glViewport(0, -winHeight, fbWidth, fbHeight); | |||
// glClearColor(0.0, 0.0, 0.0, 1.0); | |||
// glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); | |||
nvgEndFrame(vg); | |||
t4 = system::getTime(); | |||
} | |||
} | |||
t5 = system::getTime(); | |||
// DEBUG("pre-step %6.1f step %6.1f draw %6.1f nvgEndFrame %6.1f glfwSwapBuffers %6.1f total %6.1f", | |||
// (t1 - frameTime) * 1e3f, | |||
// (t2 - t1) * 1e3f, | |||
// (t3 - t2) * 1e3f, | |||
// (t4 - t2) * 1e3f, | |||
// (t5 - t4) * 1e3f, | |||
// (t5 - frameTime) * 1e3f | |||
// ); | |||
internal->frame++; | |||
} | |||
void Window::screenshot(const std::string&) { | |||
} | |||
void Window::screenshotModules(const std::string&, float) { | |||
} | |||
void Window::close() { | |||
internal->ui->getWindow().close(); | |||
} | |||
void Window::cursorLock() { | |||
if (!settings::allowCursorLock) | |||
return; | |||
internal->ignoreNextMouseDelta = true; | |||
} | |||
void Window::cursorUnlock() { | |||
if (!settings::allowCursorLock) | |||
return; | |||
internal->ignoreNextMouseDelta = true; | |||
} | |||
bool Window::isCursorLocked() { | |||
return false; | |||
} | |||
int Window::getMods() { | |||
int mods = 0; | |||
/* | |||
if (glfwGetKey(win, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS || glfwGetKey(win, GLFW_KEY_RIGHT_SHIFT) == GLFW_PRESS) | |||
mods |= GLFW_MOD_SHIFT; | |||
if (glfwGetKey(win, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS || glfwGetKey(win, GLFW_KEY_RIGHT_CONTROL) == GLFW_PRESS) | |||
mods |= GLFW_MOD_CONTROL; | |||
if (glfwGetKey(win, GLFW_KEY_LEFT_ALT) == GLFW_PRESS || glfwGetKey(win, GLFW_KEY_RIGHT_ALT) == GLFW_PRESS) | |||
mods |= GLFW_MOD_ALT; | |||
if (glfwGetKey(win, GLFW_KEY_LEFT_SUPER) == GLFW_PRESS || glfwGetKey(win, GLFW_KEY_RIGHT_SUPER) == GLFW_PRESS) | |||
mods |= GLFW_MOD_SUPER; | |||
*/ | |||
return mods; | |||
} | |||
void Window::setFullScreen(bool) { | |||
} | |||
bool Window::isFullScreen() { | |||
return false; | |||
} | |||
double Window::getMonitorRefreshRate() { | |||
return internal->monitorRefreshRate; | |||
} | |||
double Window::getFrameTime() { | |||
return internal->frameTime; | |||
} | |||
double Window::getLastFrameDuration() { | |||
return internal->lastFrameDuration; | |||
} | |||
double Window::getFrameDurationRemaining() { | |||
double frameDurationDesired = internal->frameSwapInterval / internal->monitorRefreshRate; | |||
return frameDurationDesired - (system::getTime() - internal->frameTime); | |||
} | |||
std::shared_ptr<Font> Window::loadFont(const std::string& filename) { | |||
const auto& pair = internal->fontCache.find(filename); | |||
if (pair != internal->fontCache.end()) | |||
return pair->second; | |||
// Load font | |||
std::shared_ptr<Font> font; | |||
try { | |||
font = std::make_shared<Font>(); | |||
font->loadFile(filename, vg); | |||
} | |||
catch (Exception& e) { | |||
WARN("%s", e.what()); | |||
font = NULL; | |||
} | |||
internal->fontCache[filename] = font; | |||
return font; | |||
} | |||
std::shared_ptr<Image> Window::loadImage(const std::string& filename) { | |||
const auto& pair = internal->imageCache.find(filename); | |||
if (pair != internal->imageCache.end()) | |||
return pair->second; | |||
// Load image | |||
std::shared_ptr<Image> image; | |||
try { | |||
image = std::make_shared<Image>(); | |||
image->loadFile(filename, vg); | |||
} | |||
catch (Exception& e) { | |||
WARN("%s", e.what()); | |||
image = NULL; | |||
} | |||
internal->imageCache[filename] = image; | |||
return image; | |||
} | |||
bool& Window::fbDirtyOnSubpixelChange() { | |||
return internal->fbDirtyOnSubpixelChange; | |||
} | |||
void init() { | |||
} | |||
void destroy() { | |||
} | |||
} // namespace window | |||
} // namespace rack |
@@ -0,0 +1,19 @@ | |||
// This source file compiles those annoying implementation-in-header libraries | |||
#include <common.hpp> // for fopen_u8 | |||
#define GLEW_STATIC | |||
#define GLEW_NO_GLU | |||
#include <GL/glew.h> | |||
#include <nanovg.h> | |||
#define BLENDISH_IMPLEMENTATION | |||
#include <blendish.h> | |||
#define NANOSVG_IMPLEMENTATION | |||
#define NANOSVG_ALL_COLOR_KEYWORDS | |||
#include <nanosvg.h> | |||
#define STB_IMAGE_WRITE_IMPLEMENTATION | |||
#include <stb_image_write.h> |