~connorbell/ShaderChain

5acda68a6cf2041107a59a4bdb8c0a908dd51752 — Connor Bell 3 years ago f2df01b
Added FFMPEG manager which is wrapped by a threaded system call so a
dialogue can be shown during processing. Refactored preset structure to
be exported to web.
A src/FfmpegManager.cpp => src/FfmpegManager.cpp +170 -0
@@ 0,0 1,170 @@
#include "FfmpegManager.h"
#include "PathUtil.h"

void FfmpegManager::saveGif(string filename, PNGRenderer *pngRenderer) {
	
	if (isRunning) return;

	vector<string> commands;
	statusLabel.set("Saving Gif...");
	cancelButtonRef->setName("Cancel");
	cancelButtonRef->setEnabled(true);
	container->setEnabled(true);

	int totalFrames = pngRenderer->FPS * pngRenderer->animduration;
	string totalZerosString = to_string((int)floor(log10(((float)totalFrames))) + 1);
	string fileWithoutExtension = pngRenderer->presetDisplayName;
	string rendersDirectory = ofFilePath::getAbsolutePath(ofToDataPath("")) + PathUtil::getSlash() + "renders" + PathUtil::getSlash();
	string targetDirectory = rendersDirectory + fileWithoutExtension + PathUtil::getSlash();

	string mkdirCommand = "mkdir \"" + targetDirectory + "\"";
	commands.push_back(mkdirCommand);

#if defined(WIN32) || defined(_WIN32) || defined(__WIN32) && !defined(__CYGWIN__)
	string moveCommand = "move ";
#else
	string moveCommand = "mv ";
#endif

	string moveFilesCommand = moveCommand + rendersDirectory + fileWithoutExtension + "_*.png " + targetDirectory;
	commands.push_back(moveFilesCommand);
	
	string ffmpegCommand = this->ffmpegCommand + " -r " + to_string(pngRenderer->FPS) + " -v warning -start_number 0 -i \"" + targetDirectory + fileWithoutExtension + "_%0" + totalZerosString + "d.png\" -vf scale=500:-1:flags=lanczos,palettegen=stats_mode=diff:reserve_transparent=off:max_colors=" + to_string(pngRenderer->gifNumColors) + " -y " + "\"" + targetDirectory + PathUtil::getSlash() + "palette.png" + "\"";
	commands.push_back(ffmpegCommand);
	cout << ffmpegCommand << endl;
	string targetFilename = targetDirectory + fileWithoutExtension + ".gif";
	targetFilename = PathUtil::createUniqueFilePath(targetFilename);

	int resX = (float)pngRenderer->resolutionX * pngRenderer->gifResolutionScale;
	int resY = (float)pngRenderer->resolutionY * pngRenderer->gifResolutionScale;

	string compressionCommand = this->ffmpegCommand + " -v warning -thread_queue_size 512 -start_number 0 -i \"" + targetDirectory + fileWithoutExtension + "_%0" + totalZerosString + "d.png\" -i \"" + targetDirectory + "palette.png\" -r 30 -lavfi scale=" + to_string(resX) + ":" + to_string(resY) + ":flags=\"lanczos [x]; [x][1:v] paletteuse\" -y \"" + targetFilename + "\"";
	commands.push_back(compressionCommand);

	mostRecentSavedFile = targetFilename;
	isRunning = true;
	processCall.systemCall(commands, &completedEventGif);
}

void FfmpegManager::saveVideo(string filename, string soundFilePath, PNGRenderer *pngRenderer) {

	if (isRunning) return;
	cancelButtonRef->setName("Cancel");
	cancelButtonRef->setEnabled(true);
	vector<string> commands;
	statusLabel.setName("Saving mp4...");

	container->setEnabled(true);

	string f = filename;

	int totalFrames = pngRenderer->FPS * pngRenderer->animduration;
	string rendersDirectory = PathUtil::getSlash() + "renders" + PathUtil::getSlash();
	string outputFilename = ofFilePath::getAbsolutePath(ofToDataPath("")) + rendersDirectory + f;
	string mkdirCommand = "mkdir " + outputFilename;
	commands.push_back(mkdirCommand);

#if defined(WIN32) || defined(_WIN32) || defined(__WIN32) && !defined(__CYGWIN__)
	string moveCommand = "move ";
#else
	string moveCommand = "mv ";
#endif

	string moveFilesCommand = moveCommand + outputFilename + "_*.png " + outputFilename;
	commands.push_back(moveFilesCommand);

	outputFilename = outputFilename + PathUtil::getSlash() + f;

	cout << "Creating mp4 " << outputFilename << endl;
	string outputMp4Filename = outputFilename + ".mp4";
	outputMp4Filename = PathUtil::createUniqueFilePath(outputMp4Filename);
	string fpsString = to_string(pngRenderer->FPS);
	string totalZerosString = to_string((int)floor(log10(((float)totalFrames))) + 1);

	string ffmpegCommand = this->ffmpegCommand + " -r " + fpsString + " -f image2 -s 1080x1920 -i \"" + outputFilename + "_%0" + totalZerosString + "d.png\" -vcodec libx264 -crf 18 -pix_fmt yuv420p " + outputMp4Filename;
	commands.push_back(ffmpegCommand);

	cout << ffmpegCommand << endl;

	mostRecentSavedFile = outputMp4Filename;

	// Add soundfile to video if it exists
	if (soundFilePath.length() > 0) {
		string outputMp4AudioFilename = outputFilename + "_audio.mp4";
		outputMp4AudioFilename = PathUtil::createUniqueFilePath(outputMp4AudioFilename);
		string addSoundCommand = this->ffmpegCommand + " -i \"" + outputMp4Filename + "\" -i \"" + soundFilePath + "\" -vcodec copy -acodec aac -shortest " + outputMp4AudioFilename;
		commands.push_back(addSoundCommand);
		outputMp4Filename = outputMp4AudioFilename;
	}

	// Loop the video if required
	if (pngRenderer->numLoops > 1) {
		string inputFileText = "";
		for (unsigned int i = 0; i < pngRenderer->numLoops; i++) {
			inputFileText += "file '" + outputMp4Filename + "''\n";
		}

		ofstream file;
		file.open("list.txt");
		file << inputFileText;
		file.close();

		string outputLoopedFilename = outputFilename + "_looped.mp4";
		outputLoopedFilename = PathUtil::createUniqueFilePath(outputLoopedFilename);

		string loopffmpegCommand = this->ffmpegCommand + " -f concat -safe 0 -i list.txt -c copy " + outputLoopedFilename;
		commands.push_back(loopffmpegCommand);

		outputMp4Filename = outputLoopedFilename;
	}

	isRunning = true;
	processCall.systemCall(commands, &completedEventMp4);
}

// Moves files in current directory to a newly created directory with the target file name
void FfmpegManager::moveFilesIntoSubdir(string filenamePrefix) {
	string mkdirCommand = "mkdir " + filenamePrefix;
	system(mkdirCommand.c_str());

#if defined(WIN32) || defined(_WIN32) || defined(__WIN32) && !defined(__CYGWIN__)
	string moveCommand = "move ";
#else
	string moveCommand = "mv ";
#endif

	string moveFilesCommand = moveCommand + filenamePrefix + "_*.png " + filenamePrefix;
	cout << moveFilesCommand << endl;
	system(moveFilesCommand.c_str());
}

void FfmpegManager::cancelButtonPressed() {
	container->setEnabled(false);
	isRunning = false;
}

void FfmpegManager::onCompletedGif() {
	// Check for video writing success
	cancelButtonRef->setName("Done");
	cancelButtonRef->setEnabled(true);
	if (PathUtil::fileExists(mostRecentSavedFile)) {
		updateStatusText("Gif saved to " + mostRecentSavedFile);
	}
	else {
		updateStatusText("Error saving video to " + mostRecentSavedFile);
	}
	isRunning = false;
}

void FfmpegManager::onCompletedMp4() {
	// Check for video writing success
	cancelButtonRef->setName("Done");
	cancelButtonRef->setEnabled(true);
	if (PathUtil::fileExists(mostRecentSavedFile)) {
		updateStatusText("Video saved to " + mostRecentSavedFile);
	}
	else {
		updateStatusText("Error saving video to " + mostRecentSavedFile);
	}
	isRunning = false;
}
\ No newline at end of file

A src/FfmpegManager.h => src/FfmpegManager.h +60 -0
@@ 0,0 1,60 @@
#pragma once

#include <string>
#include "ofxJSON.h"
#include "ThreadedSystemCall.h"
#include "PNGRenderer.h"

class FfmpegManager {
public:

	// Constructor that loads config which contains alternate ffmpeg path on macos
	FfmpegManager(ofxGui *gui) {

		container = gui->addContainer();

		container->add(statusLabel);
		container->setEnabled(false);
		cancelButton.addListener(this, &FfmpegManager::cancelButtonPressed);
		cancelButtonRef = this->container->add(cancelButton.set("Cancel"), ofJson({ {"type", "fullsize"}, {"text-align", "center"} }));
		this->container->setPosition(ofPoint(ofGetWidth() / 2 - this->container->getWidth() / 2, ofGetHeight() / 2 - this->container->getHeight() / 2));
		ofAddListener(this->completedEventGif, this, &FfmpegManager::onCompletedGif);
		ofAddListener(this->completedEventMp4, this, &FfmpegManager::onCompletedMp4);

#if __APPLE__
		std::string file = ofToDataPath("config.json");
		ofxJSONElement result;

		bool parsingSuccessful = result.open(file);

		if (parsingSuccessful)
		{
			ffmpegCommand = result["ffmpeg"].asString();
		}
#endif
	}

	void saveVideo(string filename, string soundFilePath, PNGRenderer *pngRenderer);
	void saveGif(string filename, PNGRenderer *pngRenderer);

private:
	ofParameter<void> cancelButton;
	ofParameter<string> statusLabel;
	string ffmpegCommand = "ffmpeg";
	ofxGuiButton *cancelButtonRef;
	ofxGuiContainer *container = nullptr;
	ThreadedSystemCall processCall;
	ofEvent<void> completedEventGif;
	ofEvent<void> completedEventMp4;
	string mostRecentSavedFile;
	bool isRunning = false;

	void cancelButtonPressed();
	void onCompletedGif();
	void onCompletedMp4();
	void moveFilesIntoSubdir(string filenamePrefix);

	void updateStatusText(string text) {
		statusLabel.set(text, "");
	}
};
\ No newline at end of file

M src/PNGRenderer.cpp => src/PNGRenderer.cpp +81 -30
@@ 6,8 6,6 @@

PNGRenderer::PNGRenderer(float animduration, int fps, glm::vec2 resolution) {
    this->animduration = animduration;
    this->FPS = fps;
    this->presetFilePath = "";
    this->presetDisplayName = "";
    this->currentFrame = 0;
    this->totalFrames = animduration * fps;


@@ 17,10 15,64 @@ PNGRenderer::PNGRenderer(float animduration, int fps, glm::vec2 resolution) {
    this->displayScaleParam = 1.0;
    this->renderedFrames = 1;
    this->frameskip = 1;
    this->FPS = 30;
    this->numLoops = 1;
    this->numBlendFrames = 1;
    this->preview = false;
	this->animduration.addListener(this, &PNGRenderer::animDurationUpdated);
	numBlendFrames.addListener(this, &PNGRenderer::numBlendFramesUpdated);
	resolutionX.addListener(this, &PNGRenderer::resolutionXUpdated);
	resolutionY.addListener(this, &PNGRenderer::resolutionYUpdated);
	frameskip.addListener(this, &PNGRenderer::frameskipUpdated);
	displayScaleParam.addListener(this, &PNGRenderer::displayScaleParamUpdated);
	exportToWebButton.addListener(this, &PNGRenderer::exportToWebButtonPressed);
	updateAllHtmlButton.addListener(this, &PNGRenderer::updateAllHtmlButtonPressed);
	FPS.addListener(this, &PNGRenderer::fpsUpdated);
}

PNGRenderer::~PNGRenderer() {
	/*
	this->animduration.removeListener(this, &PNGRenderer::animDurationUpdated);
	resolutionX.removeListener(this, &PNGRenderer::resolutionXUpdated);
	resolutionY.removeListener(this, &PNGRenderer::resolutionYUpdated);
	displayScaleParam.removeListener(this, &PNGRenderer::displayScaleParamUpdated);
	frameskip.removeListener(this, &PNGRenderer::frameskipUpdated);
	numBlendFrames.removeListener(this, &PNGRenderer::numBlendFramesUpdated);
	*/
}

void PNGRenderer::animDurationUpdated(float &val) {
	if (presetRef != nullptr)
		presetRef->animduration = val;
}

void PNGRenderer::resolutionXUpdated(float &val) {
	if (presetRef != nullptr)
		presetRef->resolutionX = val;
}

void PNGRenderer::resolutionYUpdated(float &val) {
	if (presetRef != nullptr)
		presetRef->resolutionY = val;
}

void PNGRenderer::frameskipUpdated(int &val) {
	if (presetRef != nullptr)
		presetRef->frameskip = val;
}

void PNGRenderer::displayScaleParamUpdated(float &val) {
	if (presetRef != nullptr)
		presetRef->displayScaleParam = val;
}

void PNGRenderer::numBlendFramesUpdated(int &val) {
	if (presetRef != nullptr)
		presetRef->numBlendFrames = val;
}

void PNGRenderer::fpsUpdated(int &val) {
	if (presetRef != nullptr)
		presetRef->fps = val;
}

void PNGRenderer::AddToGui(ofxGuiContainer *panel, ofxGuiContainer *statusLabelPanel, FFTManager *fft) {


@@ 63,6 115,8 @@ void PNGRenderer::AddToGui(ofxGuiContainer *panel, ofxGuiContainer *statusLabelP
    renderingMenu->add(preview.set("Preview", preview));
    renderingMenu->add(saveScreenshotButton.set("Save Screenshot"), buttonStyling);
    saveFramesButton = renderingMenu->add(saveButton.set("Save Frames"), buttonStyling);
	renderingMenu->add(exportToWebButton.set("Export to Web"), buttonStyling);
	renderingMenu->add(updateAllHtmlButton.set("Update All HTML"), buttonStyling);

    panel->add(displayScaleParam.set("Display scale", displayScaleParam, 0.1, 5.0));



@@ 102,7 156,6 @@ void PNGRenderer::WritePNG(ofFbo *buffer) {
  s += std::to_string(this->currentFrame);
  buffer->readToPixels(outputPixels);
  string destFilePath = this->renderDirectory + this->presetDisplayName.get() + "_" + s + ".png";
  cout << destFilePath << endl;

  ofSaveImage(outputPixels, destFilePath, OF_IMAGE_QUALITY_BEST);
}


@@ 130,32 183,15 @@ void PNGRenderer::Start() {
    saveFramesButton->setName("Cancel");
}

void PNGRenderer::updatePath(string s) {
    if (s == "") return;

#if defined(WIN32) || defined(_WIN32) || defined(__WIN32) && !defined(__CYGWIN__)

	replace(s.begin(), s.end(), '/', '\\');
	static const std::string slash = "\\";
#else
	static const std::string slash = "/";
#endif

    string file = s.substr(s.find_last_of(slash) + 1);

    // add json extension if needed
    int indexOfPeriod = file.find_last_of(".");
    if (indexOfPeriod == std::string::npos) {
        file += ".json";
        s += ".json";
        indexOfPeriod = file.find_last_of(".");
    }

    string fileWithoutExtension = file.substr(0, indexOfPeriod);

    presetDisplayName.set(fileWithoutExtension);
    presetDisplayNameLabel.set("Preset: " + presetDisplayName.get());
    presetFilePath = s;
void PNGRenderer::updateUI(preset *currentPreset) {
	presetRef = currentPreset;
    presetDisplayName.set(currentPreset->presetDisplayName);
    presetDisplayNameLabel.set("Preset: " + currentPreset->presetDisplayName);
	resolutionX = currentPreset->resolutionX;
	resolutionY = currentPreset->resolutionY;
	FPS = currentPreset->fps;
	animduration = currentPreset->animduration;
	numBlendFrames = currentPreset->numBlendFrames;
}

void PNGRenderer::UpdateResolution(int w, int h) {


@@ 166,3 202,18 @@ void PNGRenderer::UpdateResolution(int w, int h) {
bool PNGRenderer::isMouseOverAnyMenu() {
    return fileMenu->ofxGuiElement::isMouseOver() || renderingMenu->ofxGuiElement::isMouseOver() || gifGroup->ofxGuiElement::isMouseOver() || vidGroup->ofxGuiElement::isMouseOver();
}

void PNGRenderer::exportToWebButtonPressed() {

	string destFilePath = "web" + PathUtil::getSlash() + "thumbnails" + PathUtil::getSlash() + presetRef->presetDisplayName + ".png";
	screenshot(&(presetRef->passes[presetRef->getLastEnabledPassIndex()]->swapBuffer), destFilePath);

	webExport exporter;
	exporter.exportPreset(presetRef);
}


void PNGRenderer::updateAllHtmlButtonPressed() {
	webExport exporter;
	exporter.updateAllHTML();
}
\ No newline at end of file

M src/PNGRenderer.h => src/PNGRenderer.h +19 -1
@@ 4,10 4,15 @@
#include "ofMain.h"
#include "ofxGuiExtended2.h"
#include "FFTManager.h"
#include "preset.h"
#include "webExport.h"

class PNGRenderer {

public:

	~PNGRenderer();

    string renderDirectory = "renders/";
    ofParameter<string> spaceBufferLabel;



@@ 37,6 42,8 @@ public:
    ofParameter<int> FPS;
    ofParameter<void> encodeMp4Button;
    ofParameter<void> encodeGifButton;
	ofParameter<void> exportToWebButton;
	ofParameter<void> updateAllHtmlButton;

    ofParameterGroup renderParameterGroup;
    ofParameterGroup pngSavingGroup;


@@ 56,6 63,8 @@ public:
    void AddToGui(ofxGuiContainer *panel, ofxGuiContainer *statusLabelPanel, FFTManager *fft);
    void UpdateResolution(int w, int h);
    void updatePath(string s);
	void updateUI(preset *currentPreset);

    ofParameter<bool> preview;
    ofParameter<void> saveButton;
    ofParameter<void> saveScreenshotButton;


@@ 74,5 83,14 @@ private:
    ofxGuiMenu* renderingMenu;
    ofxGuiMenu* gifGroup;
    ofxGuiMenu* vidGroup;

	preset *presetRef = nullptr;
	void animDurationUpdated(float &val);
	void resolutionXUpdated(float &val);
	void resolutionYUpdated(float &val);
	void displayScaleParamUpdated(float &val);
	void numBlendFramesUpdated(int &val);
	void frameskipUpdated(int &val);
	void fpsUpdated(int &val);
	void exportToWebButtonPressed();
	void updateAllHtmlButtonPressed();
};

M src/Parameters/TextureParameter.cpp => src/Parameters/TextureParameter.cpp +1 -1
@@ 150,6 150,7 @@ void TextureParameter::updateTextureFromFile(string &s) {
    else if (extension == "mp4" || extension == "mov" || extension == "avi" || extension == "mkv") {
        updateToNewType(VideoFile);
        this->videoFile.load(s);
		cout << "playing video " << s << endl;
        this->videoFile.setUseTexture(true);
        this->videoFile.play();
        filePath = s;


@@ 200,7 201,6 @@ void TextureParameter::startOfflineRender() {
        this->videoFile.setPaused(true);

        this->videoFile.setFrame(0);
        sleep(1);
    }
}


A src/PathUtil.cpp => src/PathUtil.cpp +58 -0
@@ 0,0 1,58 @@
#include "PathUtil.h"
#include "ofMain.h"

namespace PathUtil {
	std::string createUniqueFilePath(std::string path) {
		bool found = false;
		auto pathWithoutExtension = path.substr(0, path.find_last_of("."));
		auto extension = path.substr(path.find_last_of(".") + 1);
		int tries = 0;

		while (!found) {
			ofFile file;

			std::string p = "";

			if (tries == 0) {
				p = pathWithoutExtension + "." + extension;
			}
			else {
				p = pathWithoutExtension + to_string(tries) + "." + extension;
			}

			file.open(p, ofFile::ReadWrite, false);

			if (!file.exists()) {
				found = true;
				path = p;
			}
			file.close();
			tries++;
		}
		return path;
	}

	std::string getSlash() {
#if defined(WIN32) || defined(_WIN32) || defined(__WIN32) && !defined(__CYGWIN__)
		return "\\";
#else
		return "/";
#endif
	}

	bool fileExists(string path) {
		ofFile file;
		file.open(path, ofFile::ReadOnly, false);
		bool res = file.exists();
		file.close();
		return res;
	}

	int copyFile(std::string from, std::string to) {
		std::ifstream src(from, std::ios::binary);
		std::ofstream dst(to, std::ios::binary);

		dst << src.rdbuf();
		return 1; // all good!
	}
};
\ No newline at end of file

A src/PathUtil.h => src/PathUtil.h +12 -0
@@ 0,0 1,12 @@
#pragma once

#include <string>

namespace PathUtil {

	// Repeatedly check for existance of a path with an accumulating suffix, return the first one which doesn't have an existing file.
	std::string createUniqueFilePath(std::string path);
	std::string getSlash();
	bool fileExists(std::string path);
	int copyFile(std::string from, std::string to);
};
\ No newline at end of file

M src/ShaderChain.cpp => src/ShaderChain.cpp +100 -334
@@ 1,12 1,10 @@
#include "ShaderChain.h"
#include "ofxSortableList.h"
#include "RenderStruct.h"
#include "PathUtil.h"

void ShaderChain::Setup(glm::vec2 res) {
    ofDisableArbTex();
#if __APPLE__
    loadFfmpegPath();
#endif
    this->passesGui = new PassesGui();
    ofAddListener(passesGui->passButtons->elementRemoved, this, &ShaderChain::removed);
    ofAddListener(passesGui->passButtons->elementMoved, this, &ShaderChain::moved);


@@ 18,6 16,8 @@ void ShaderChain::Setup(glm::vec2 res) {
    this->guiGlobal = gui.addContainer();
    this->guiGlobal->setPosition(ofPoint(0, 10));
    this->statusContainer = gui.addContainer();

	this->ffmpegManager = new FfmpegManager(&gui);
    ofColor transparentColor;
    transparentColor.a = 0.0;
    this->statusContainer->setBackgroundColor(transparentColor);


@@ 44,7 44,7 @@ void ShaderChain::Setup(glm::vec2 res) {
	this->time = 0.0;
    this->parameterPanel = gui.addContainer();
    this->cumulativeShader.load("shaders/internal/vertex.vert","shaders/internal/cumulativeAdd.frag");
    this->renderStruct.passes = &this->passes;
    this->renderStruct.passes = &currentPreset.passes;
    this->renderStruct.time = 0.0;
    this->renderStruct.fft = &this->fft;
    this->renderStruct.vidGrabber = &this->vidGrabber;


@@ 63,27 63,16 @@ void ShaderChain::Setup(glm::vec2 res) {

ShaderChain::~ShaderChain() {
    delete this->pngRenderer;
    for (unsigned int i = 0; i < this->passes.size(); i++) {
        ofRemoveListener(passes[i]->shaderUpdatedEvent, this, &ShaderChain::SetupGui);
        delete this->passes[i];
	delete this->ffmpegManager;
    for (unsigned int i = 0; i < currentPreset.passes.size(); i++) {
        ofRemoveListener(currentPreset.passes[i]->shaderUpdatedEvent, this, &ShaderChain::SetupGui);
        delete currentPreset.passes[i];
    }
    ofRemoveListener(passesGui->passButtons->elementRemoved, this, &ShaderChain::removed);
    ofRemoveListener(passesGui->passButtons->elementMoved, this, &ShaderChain::moved);
    delete this->passesGui;
}

void ShaderChain::loadFfmpegPath() {
    std::string file = ofToDataPath("config.json");
    ofxJSONElement result;

    bool parsingSuccessful = result.open(file);

    if (parsingSuccessful)
    {
        ffmpegCommand = result["ffmpeg"].asString();
    }
}

void ShaderChain::openDefaultPreset() {
    ofFile file;



@@ 112,45 101,41 @@ void ShaderChain::UpdateResolutionIfChanged(bool force) {

    bool needsUpdate = force;

    if (this->passes.size() > 0) {
        if (this->passes[0]->targetResolution.x != this->pngRenderer->resolutionX ||
            this->passes[0]->targetResolution.y != this->pngRenderer->resolutionY) {
    if (currentPreset.passes.size() > 0) {
        if (currentPreset.passes[0]->targetResolution.x != pngRenderer->resolutionX ||
			currentPreset.passes[0]->targetResolution.y != pngRenderer->resolutionY) {
            needsUpdate = true;
        }
    }

    if (needsUpdate) {
        ofSetFrameRate(pngRenderer->FPS);
        ofSetFrameRate(currentPreset.fps);
        ofFloatColor black;

        cumulativeBuffer.allocate(this->pngRenderer->resolutionX, this->pngRenderer->resolutionY, GL_RGBA32F);
        cumulativeDrawBuffer.allocate(this->pngRenderer->resolutionX, this->pngRenderer->resolutionY, GL_RGBA32F);
        cumulativeBufferSwap.allocate(this->pngRenderer->resolutionX, this->pngRenderer->resolutionY, GL_RGBA32F);
        cumulativeBuffer.allocate(currentPreset.resolutionX, currentPreset.resolutionY, GL_RGBA32F);
        cumulativeDrawBuffer.allocate(currentPreset.resolutionX, currentPreset.resolutionY, GL_RGBA32F);
        cumulativeBufferSwap.allocate(currentPreset.resolutionX, currentPreset.resolutionY, GL_RGBA32F);
        cumulativeBufferSwap.clearColorBuffer(black);
        cumulativeBuffer.clearColorBuffer(black);

        this->cumulativeRenderPlane.set(pngRenderer->resolutionX, pngRenderer->resolutionY, 2, 2);
    	this->cumulativeRenderPlane.setPosition({pngRenderer->resolutionX/2, pngRenderer->resolutionY/2, 0.0f});
        this->cumulativeRenderPlane.set(currentPreset.resolutionX, currentPreset.resolutionY, 2, 2);
    	this->cumulativeRenderPlane.setPosition({ currentPreset.resolutionX/2, currentPreset.resolutionY/2, 0.0f});

        for (unsigned int i = 0; i < this->passes.size(); i++) {
            this->passes[i]->UpdateResolution(this->pngRenderer->resolutionX, this->pngRenderer->resolutionY);
        }
		currentPreset.updateResolution(currentPreset.resolutionX, currentPreset.resolutionY);
    }
}

void ShaderChain::BeginSaveFrames() {
    this->isRunning = true;

    for (int i = 0; i < this->passes.size(); i++) {
        passes[i]->startOfflineRender();
    }
	currentPreset.startRender();

    if (fft.currentState == InputStateSoundFile) {
        this->time = 0.0;
        ofFloatColor *black = new ofFloatColor(0.0, 0.0, 0.0, 0.0);
        for (unsigned int i = 0; i < this->passes.size(); i++) {
            if (passes[i]->lastBuffer.isAllocated()) {
                passes[i]->lastBuffer.clearColorBuffer(*black);
        for (unsigned int i = 0; i < currentPreset.passes.size(); i++) {
            if (currentPreset.passes[i]->lastBuffer.isAllocated()) {
				currentPreset.passes[i]->lastBuffer.clearColorBuffer(*black);
            }
        }
        delete black;


@@ 161,14 146,14 @@ void ShaderChain::BeginSaveFrames() {
void ShaderChain::update() {

    float mouseX = ofMap((float)ofGetMouseX(),
                        ofGetWidth()/2.0-pngRenderer->resolutionX*0.5*pngRenderer->displayScaleParam,
                        ofGetWidth()/2.0+pngRenderer->resolutionX*0.5*pngRenderer->displayScaleParam,
                        ofGetWidth()/2.0- currentPreset.resolutionX*0.5*currentPreset.displayScaleParam,
                        ofGetWidth()/2.0+ currentPreset.resolutionX*0.5*currentPreset.displayScaleParam,
                        0.0,
                        1.0);

    float mouseY = ofMap((float)ofGetMouseY(),
                        ofGetHeight()/2.0-pngRenderer->resolutionY*0.5*pngRenderer->displayScaleParam,
                        ofGetHeight()/2.0+pngRenderer->resolutionY*0.5*pngRenderer->displayScaleParam,
                        ofGetHeight()/2.0- currentPreset.resolutionY*0.5*currentPreset.displayScaleParam,
                        ofGetHeight()/2.0+ currentPreset.resolutionY*0.5*currentPreset.displayScaleParam,
                        0.0,
                        1.0);



@@ 185,21 170,18 @@ void ShaderChain::update() {
    renderStruct.isMouseDown = isMouseDown;
    renderStruct.mousePosition = glm::vec2(mouseX, mouseY);
    renderStruct.isOfflineRendering = pngRenderer->isCapturing;

    for (int i = 0; i < this->passes.size(); i++) {
        this->passes[i]->update(&renderStruct);
    }
	currentPreset.updatePreRender(&renderStruct);
}

void ShaderChain::draw() {
    bool capturingThisFrame = pngRenderer->isCapturing;
    renderStruct.frame = pngRenderer->currentFrame;
    renderStruct.numBlendFrames = pngRenderer->numBlendFrames;
    renderStruct.numBlendFrames = currentPreset.numBlendFrames;
    renderStruct.isScrubbing = scrubber.isScrubbing;
    UpdateResolutionIfChanged(false);

    ofClear(25);
    if (this->passes.size() > 0) {
    if (currentPreset.passes.size() > 0) {
        ofFloatColor black(0,0,0,1);

        ofColor red(255, 0, 0);


@@ 207,7 189,7 @@ void ShaderChain::draw() {
        if (capturingThisFrame && this->isRunning) {
            pngRenderer->Tick();
        }
        float deltaTime = 1. / (pngRenderer->FPS * pngRenderer->numBlendFrames);
        float deltaTime = 1. / (currentPreset.fps * currentPreset.numBlendFrames);

        if (this->isRunning) {
            this->cumulativeBuffer.begin();


@@ 218,9 200,9 @@ void ShaderChain::draw() {
            ofClear(0,0,0,255);
            this->cumulativeBufferSwap.end();

            float blendFactor = (1./pngRenderer->numBlendFrames);
            float blendFactor = (1./ currentPreset.numBlendFrames);

            for (unsigned int i = 0; i < pngRenderer->numBlendFrames; i++) {
            for (unsigned int i = 0; i < currentPreset.numBlendFrames; i++) {

                if (capturingThisFrame) {
                    this->time = this->time + deltaTime;


@@ 229,24 211,23 @@ void ShaderChain::draw() {
                }
                else {
                    if (!scrubber.isScrubbing) {
                         this->time = pngRenderer->preview ? fmod(this->time + deltaTime, pngRenderer->animduration) : this->time + deltaTime;
                         this->time = pngRenderer->preview ? fmod(this->time + deltaTime, currentPreset.animduration) : this->time + deltaTime;
                    }
                    fft.Update();
                }

                this->renderStruct.time = this->time;

                if (frame % pngRenderer->frameskip == 0) {
                if (frame % currentPreset.frameskip == 0) {
                    RenderPasses();
                }

                this->cumulativeBuffer.begin();
                this->cumulativeShader.begin();
                ofClear(0, 0, 0, 255);
                int idx = getLastEnabledPassIndex();
                int idx = currentPreset.getLastEnabledPassIndex();
                this->cumulativeShader.setUniform1f("factor", blendFactor);
                this->cumulativeShader.setUniformTexture("_CumulativeTexture", this->cumulativeBufferSwap.getTexture(), 1);
                this->cumulativeShader.setUniformTexture("_IncomingTexture", this->passes[idx]->buffer.getTexture(), 2);
                this->cumulativeShader.setUniformTexture("_IncomingTexture", currentPreset.passes[idx]->buffer.getTexture(), 2);
                this->cumulativeDrawBuffer.draw(0,0);
                this->cumulativeShader.end();
                this->cumulativeBuffer.end();


@@ 257,23 238,23 @@ void ShaderChain::draw() {
            }
        }

        int idx = this->passes.size()-1;
        float x = ofGetWidth()/2.-this->pngRenderer->resolutionX*0.5*this->pngRenderer->displayScaleParam;
        float y = ofGetHeight()/2.-this->pngRenderer->resolutionY*0.5*this->pngRenderer->displayScaleParam;
        float w = this->pngRenderer->resolutionX*this->pngRenderer->displayScaleParam;
        float h = this->pngRenderer->resolutionY*this->pngRenderer->displayScaleParam;
        int idx = currentPreset.passes.size()-1;
        float x = ofGetWidth()/2.- currentPreset.resolutionX*0.5*currentPreset.displayScaleParam;
        float y = ofGetHeight()/2.- currentPreset.resolutionY*0.5*currentPreset.displayScaleParam;
        float w = currentPreset.resolutionX*currentPreset.displayScaleParam;
        float h = currentPreset.resolutionY*currentPreset.displayScaleParam;

        this->cumulativeBufferSwap.draw(x, y, w, h);

        scrubber.enabled = pngRenderer->preview;

        if (pngRenderer->preview) {
            scrubber.drawWithTime(this->time, pngRenderer->animduration);
            scrubber.drawWithTime(this->time, currentPreset.animduration);
        }

        for (int i = 0; i < this->passes.size(); i++) {
            if (this->passes[i]->hasError) {
                ofDrawBitmapStringHighlight(this->passes[i]->shader.compilerError, 10, ofGetHeight()-100, black, red);
        for (int i = 0; i < currentPreset.passes.size(); i++) {
            if (currentPreset.passes[i]->hasError) {
                ofDrawBitmapStringHighlight(currentPreset.passes[i]->shader.compilerError, 10, ofGetHeight()-150, black, red);
                break;
            }
        }


@@ 284,8 265,8 @@ void ShaderChain::draw() {

        // On finished
        if (!this->pngRenderer->isCapturing) {
            for (int i = 0; i < this->passes.size(); i++) {
                passes[i]->stopOfflineRender();
            for (int i = 0; i < currentPreset.passes.size(); i++) {
				currentPreset.passes[i]->stopOfflineRender();
            }
            this->isRunning = false;
        }


@@ 295,35 276,17 @@ void ShaderChain::draw() {
    creditsGui.draw();
}

int ShaderChain::getFirstEnabledPassIndex() {
    int idx = 0;
    for (int i = 0; i < this->passes.size(); i++) {
        if (this->passes[i]->enabled) {
            return i;
        }
    }
    return 0;
}

int ShaderChain::getLastEnabledPassIndex() {
    for (int i = this->passes.size()-1; i >= 0; i--) {
        if (this->passes[i]->enabled) {
            return i;
        }
    }
    return 0;
}

void ShaderChain::RenderPasses() {
    int lastRenderedPassIndex = -1;
    for (int i = getFirstEnabledPassIndex(); i <= getLastEnabledPassIndex(); i++) {

    for (int i = currentPreset.getFirstEnabledPassIndex(); i <= currentPreset.getLastEnabledPassIndex(); i++) {

        if (lastRenderedPassIndex != -1) {
            this->passes[i]->Render(&(passes[lastRenderedPassIndex]->swapBuffer), &renderStruct);
			currentPreset.passes[i]->Render(&(currentPreset.passes[lastRenderedPassIndex]->swapBuffer), &renderStruct);
        }
        if (this->passes[i]->enabled) {
        if (currentPreset.passes[i]->enabled) {
            lastRenderedPassIndex = i;
            this->passes[i]->Render(nullptr, &renderStruct);
			currentPreset.passes[i]->Render(nullptr, &renderStruct);
        }
    }
}


@@ 359,17 322,16 @@ void ShaderChain::KeyPressed(int key) {
}

void ShaderChain::SetupGui() {
    cout << "setup gui" << endl;

    parameterPanel->clear();

    
	parameterPanel->clear();
    textureInputSelectionView.passNames.clear();
    for (unsigned int i = 0; i < passes.size(); i++) {
        textureInputSelectionView.passNames.push_back(passes[i]->displayName);

    for (unsigned int i = 0; i < currentPreset.passes.size(); i++) {
        textureInputSelectionView.passNames.push_back(currentPreset.passes[i]->displayName);
    }

    for (int i = 0; i < this->passes.size(); i++) {
        this->passes[i]->AddToGui(parameterPanel, &textureInputSelectionView);
    for (int i = 0; i < currentPreset.passes.size(); i++) {
		currentPreset.passes[i]->AddToGui(parameterPanel, &textureInputSelectionView);
    }

    this->parameterPanel->setPosition(ofPoint(ofGetWidth()-this->parameterPanel->getWidth(), 10));


@@ 381,9 343,9 @@ void ShaderChain::newMidiMessage(ofxMidiMessage& msg) {
            if (midiMapper.isShowing) {
                midiMapper.midiSet(msg.control);
            } else {
                for (int i = 0; i < this->passes.size(); i++) {
                    for (int j = 0; j < this->passes[i]->params.size(); j++) {
                        this->passes[i]->params[j]->UpdateMidi(msg.control, msg.value);
                for (int i = 0; i < currentPreset.passes.size(); i++) {
                    for (int j = 0; j < currentPreset.passes[i]->params.size(); j++) {
						currentPreset.passes[i]->params[j]->UpdateMidi(msg.control, msg.value);
                    }
                }
            }


@@ 392,43 354,18 @@ void ShaderChain::newMidiMessage(ofxMidiMessage& msg) {
}

void ShaderChain::ReadFromJson(std::string filepath) {
    bool parsingSuccessful = result.open(filepath);

    for (int i = 0; i < this->passes.size(); i++) {
        delete this->passes[i];
    }
    this->passes.clear();
    bool parsingSuccessful = currentPreset.load(filepath);

    if (parsingSuccessful) {
        this->time = 0;
        this->pngRenderer->updatePath(filepath);

        this->pngRenderer->resolutionX = result["res"]["x"].asFloat();
        this->pngRenderer->resolutionY = result["res"]["y"].asFloat();

        if (result.isMember("duration")) {
            this->pngRenderer->animduration.set(this->result["duration"].asFloat());
        }
        if (result.isMember("fps")) {
            this->pngRenderer->FPS.set(this->result["fps"].asFloat());
        }

        if (result.isMember("blend")) {
            this->pngRenderer->numBlendFrames.set(this->result["blend"].asInt());
        }

        if (result.isMember("scale")) {
            this->pngRenderer->displayScaleParam.set(this->result["scale"].asFloat());
        }
        this->pngRenderer->updateUI(&currentPreset);

        for (int i = 0; i < result["data"].size(); i++) {
            ShaderPass *pass = new ShaderPass();
            pass->LoadFromJson(result["data"][i], this->pngRenderer->resolutionX, this->pngRenderer->resolutionY);
            this->passes.push_back(pass);
            ofAddListener(passes[passes.size()-1]->shaderUpdatedEvent, this, &ShaderChain::SetupGui);
		// Add listeners to the shader passes to update the gui.
        for (int i = 0; i < this->currentPreset.passes.size(); i++) {
            ofAddListener(this->currentPreset.passes[i]->shaderUpdatedEvent, this, &ShaderChain::SetupGui);
        }

        this->passesGui->Setup(&this->passes);
        this->passesGui->setup(&currentPreset);
        SetupGui();
		UpdateResolutionIfChanged(true);
    }


@@ 453,10 390,10 @@ void ShaderChain::LoadPassFromFile(string filepath) {
    auto relativeFileNameWithoutExtension = relativeFileName.substr(0,relativeFileName.find("frag")-1);
    ShaderPass *pass = new ShaderPass(relativeFileNameWithoutExtension, glm::vec2(this->pngRenderer->resolutionX,this->pngRenderer->resolutionY) );
    pass->LoadJsonParametersFromLoadedShader();
    this->passes.push_back(pass);
    ofAddListener(passes[passes.size()-1]->shaderUpdatedEvent, this, &ShaderChain::SetupGui);
	currentPreset.addPass(pass);
	ofAddListener(currentPreset.passes[currentPreset.passes.size()-1]->shaderUpdatedEvent, this, &ShaderChain::SetupGui);
    SetupGui();
    this->passesGui->Setup(&this->passes);
    passesGui->setup(&currentPreset);
    UpdateResolutionIfChanged(true);
}



@@ 469,10 406,10 @@ void ShaderChain::processFileInput(string filePath) {
    } else if (extension == "mp3") {
        fft.loadSoundFile(filePath);
    } else if (extension == "png" || extension == "jpeg" || extension == "jpg" || extension == "bmp" || extension == "mp4" || extension == "mov" || extension == "mkv") {
        for (int i = 0; i < this->passes.size(); i++) {
            for (int j = 0; j < this->passes[i]->params.size(); j++) {
                if (this->passes[i]->params[j]->isMouseHoveredOver()) {
                    this->passes[i]->params[j]->handleInputFile(filePath);
        for (int i = 0; i < currentPreset.passes.size(); i++) {
            for (int j = 0; j < currentPreset.passes[i]->params.size(); j++) {
                if (currentPreset.passes[i]->params[j]->isMouseHoveredOver()) {
					currentPreset.passes[i]->params[j]->handleInputFile(filePath);
                }
            }
        }


@@ 480,42 417,23 @@ void ShaderChain::processFileInput(string filePath) {
}

void ShaderChain::WriteToJson() {
    this->result.clear();
    this->result["res"]["x"] = (float)this->pngRenderer->resolutionX;
    this->result["res"]["y"] = (float)this->pngRenderer->resolutionY;

    this->result["duration"] = (float)this->pngRenderer->animduration;
    this->result["fps"] = (float)this->pngRenderer->FPS;
    this->result["blend"] = (float)this->pngRenderer->numBlendFrames;
    this->result["scale"] = (float)this->pngRenderer->displayScaleParam;

    for (int i = 0; i < this->passes.size(); i++) {
        this->result["data"][i]["shaderName"] = this->passes[i]->filePath;
        this->result["data"][i]["numPassesPerFrame"] = this->passes[i]->numPassesPerFrame;
        this->result["data"][i]["scale"] = this->passes[i]->scale;
        for (int j = 0; j < this->passes[i]->params.size(); j++) {
            this->result["data"][i]["parameters"][j]["name"] = this->passes[i]->params[j]->uniform;
            this->passes[i]->params[j]->UpdateJson((this->result["data"][i]["parameters"][j]));
        }
    }

    if (!this->result.save(pngRenderer->presetFilePath, true)) {
        updateStatusText("Error saving " + this->pngRenderer->presetDisplayName.get());
    if (currentPreset.save()) {
		updateStatusText("Saved " + this->pngRenderer->presetDisplayName.get());
    } else {
        updateStatusText("Saved " + this->pngRenderer->presetDisplayName.get());
		updateStatusText("Error saving " + this->pngRenderer->presetDisplayName.get());
    }
}

void ShaderChain::removed(RemovedElementData& data) {
    auto item = (passes.begin() + data.index);
    auto item = (currentPreset.passes.begin() + data.index);
    ofRemoveListener((*item)->shaderUpdatedEvent, this, &ShaderChain::SetupGui);
    passes.erase(item);
	currentPreset.removePass(data.index);
    freeUnusedResources();
    SetupGui();
}

void ShaderChain::moved(MovingElementData &data) {
    iter_swap(passes.begin() + data.old_index, passes.begin() + data.new_index);
	currentPreset.swapPass(data.old_index, data.new_index);
    SetupGui();
}



@@ 537,7 455,8 @@ void ShaderChain::savePresetAsPressed() {
        ofFileDialogResult result = ofSystemSaveDialog(pngRenderer->presetDisplayName.get() + ".json", "Save preset");
        if (result.bSuccess) {
            string path = result.getPath();
            pngRenderer->updatePath(path);
            currentPreset.updatePath(path);
			pngRenderer->updateUI(&currentPreset);
            WriteToJson();
            updateStatusText("Saved preset");
        }


@@ 546,77 465,7 @@ void ShaderChain::savePresetAsPressed() {
}

void ShaderChain::saveVideo(string outputFilename) {
    string f = outputFilename;

    int totalFrames = pngRenderer->FPS * pngRenderer->animduration;
    string rendersDirectory = ShaderChain::getSlash() + "renders" + ShaderChain::getSlash();

    outputFilename = ofFilePath::getAbsolutePath( ofToDataPath("") ) + rendersDirectory + outputFilename;

    string mkdirCommand = "mkdir " + outputFilename;
    system(mkdirCommand.c_str());

#if defined(WIN32) || defined(_WIN32) || defined(__WIN32) && !defined(__CYGWIN__)
	string moveCommand = "move ";
#else
	string moveCommand = "mv ";
#endif

    string moveFilesCommand = moveCommand + outputFilename + "_*.png " + outputFilename;
	cout << moveFilesCommand << endl;
	system(moveFilesCommand.c_str());
    outputFilename = outputFilename + ShaderChain::getSlash() + f;

    cout << "Creating mp4 " << outputFilename << endl;
    string outputMp4Filename = outputFilename + ".mp4";
    outputMp4Filename = createUniqueFilePath(outputMp4Filename);
    string fpsString = to_string(pngRenderer->FPS);
    string totalZerosString = to_string((int)floor(log10 (((float)totalFrames)))+1);

    string ffmpegCommand = this->ffmpegCommand + " -r " + fpsString + " -f image2 -s 1080x1920 -i \"" + outputFilename + "_%0" + totalZerosString + "d.png\" -vcodec libx264 -crf 18 -pix_fmt yuv420p " + outputMp4Filename;

	system(ffmpegCommand.c_str());

    cout << ffmpegCommand << endl;

    if (fft.currentState == InputStateSoundFile) {
        string outputMp4AudioFilename = outputFilename + "_audio.mp4";
        outputMp4AudioFilename = createUniqueFilePath(outputMp4AudioFilename);
        string addSoundCommand = this->ffmpegCommand + " -i \"" + outputMp4Filename + "\" -i \"" + fft.soundFilePath + "\" -vcodec copy -acodec aac -shortest " + outputMp4AudioFilename;
        system(addSoundCommand.c_str());
        outputMp4Filename = outputMp4AudioFilename;
    }

    if (pngRenderer->numLoops > 1) {
        string inputFileText = "";
        for (unsigned int i = 0; i < pngRenderer->numLoops; i++) {
            inputFileText += "file '" + outputMp4Filename + "''\n";
        }

        ofstream file;
        file.open("list.txt");
        file << inputFileText;
        file.close();

        string outputLoopedFilename = outputFilename + "_looped.mp4";
        outputLoopedFilename = createUniqueFilePath(outputLoopedFilename);

        ffmpegCommand = this->ffmpegCommand + " -f concat -safe 0 -i list.txt -c copy " + outputLoopedFilename;
        system(ffmpegCommand.c_str());

        //remove("list.txt");
        outputMp4Filename = outputLoopedFilename;
    }


    ofFile file;
    file.open(outputMp4Filename, ofFile::ReadOnly, false);
    if (file.exists()) {
        updateStatusText("Video saved to " + outputMp4Filename  );
    } else {
        updateStatusText("Error saving video");
    }
    file.close();
	ffmpegManager->saveVideo(outputFilename, fft.soundFilePath, pngRenderer);
}

void ShaderChain::updateStatusText(string s) {


@@ 637,47 486,7 @@ void ShaderChain::encodeMp4Pressed() {
}

void ShaderChain::encodeGifPressed() {

    int totalFrames = pngRenderer->FPS * pngRenderer->animduration;
    string totalZerosString = to_string((int)floor(log10 (((float)totalFrames)))+1);

    string fileWithoutExtension = pngRenderer->presetDisplayName;

    string rendersDirectory = ofFilePath::getAbsolutePath( ofToDataPath("") ) + ShaderChain::getSlash() + "renders" + ShaderChain::getSlash();

    string targetDirectory = rendersDirectory + fileWithoutExtension + ShaderChain::getSlash();
    system(("mkdir \"" + targetDirectory + "\"").c_str());

#if defined(WIN32) || defined(_WIN32) || defined(__WIN32) && !defined(__CYGWIN__)
	string moveCommand = "move ";
#else
	string moveCommand = "mv ";
#endif

    string moveFilesCommand = moveCommand + rendersDirectory + fileWithoutExtension + "_*.png " + targetDirectory;
    system(moveFilesCommand.c_str());

	string ffmpegCommand = this->ffmpegCommand + " -r " + to_string(pngRenderer->FPS) + " -v warning -start_number 0 -i \"" + targetDirectory + fileWithoutExtension + "_%0" + totalZerosString + "d.png\" -vf scale=500:-1:flags=lanczos,palettegen=stats_mode=diff:reserve_transparent=off:max_colors=" + to_string(pngRenderer->gifNumColors) + " -y " + "\"" + targetDirectory + ShaderChain::getSlash() + "palette.png" + "\"";

	system(ffmpegCommand.c_str());

	string targetFilename = targetDirectory + fileWithoutExtension + ".gif";
	targetFilename = createUniqueFilePath(targetFilename);

    int resX = (float)pngRenderer->resolutionX * pngRenderer->gifResolutionScale;
    int resY = (float)pngRenderer->resolutionY * pngRenderer->gifResolutionScale;

	ffmpegCommand = this->ffmpegCommand + " -v warning -thread_queue_size 512 -start_number 0 -i \"" + targetDirectory + fileWithoutExtension + "_%0" + totalZerosString + "d.png\" -i \"" + targetDirectory + "palette.png\" -r 30 -lavfi scale="+to_string(resX)+":"+to_string(resY)+":flags=\"lanczos [x]; [x][1:v] paletteuse\" -y \"" + targetFilename + "\"";
	system(ffmpegCommand.c_str());

    ofFile file;
    file.open(targetFilename, ofFile::ReadOnly, false);
    if (file.exists()) {
        updateStatusText("Gif saved to " + targetFilename);
    } else {
        updateStatusText("Error saving gif");
    }
    file.close();
	ffmpegManager->saveGif(pngRenderer->presetDisplayName, pngRenderer);
}

void ShaderChain::toggleWebcam(bool &val) {


@@ 703,17 512,7 @@ void ShaderChain::stopWebcam() {
}

void ShaderChain::freeUnusedResources() {
    bool needsWebam = false;

    for (int i = 0; i < this->passes.size(); i++) {
        for (int j = 0; j < this->passes[i]->params.size(); j++) {
            if (passes[i]->params[j]->getTextureSourceType() == Webcam) {
                needsWebam = true;
            }
        }
    }

    if (!needsWebam) {
    if (!currentPreset.requiresWebcam()) {
        stopWebcam();
    }
}


@@ 723,42 522,10 @@ void ShaderChain::pauseResourcesForCurrentPlaybackState() {
    fft.setPaused(!this->isRunning);

    if (!pngRenderer->isCapturing) {
        for (int i = 0; i < this->passes.size(); i++) {
            for (int j = 0; j < this->passes[i]->params.size(); j++) {
                passes[i]->params[j]->playbackDidToggleState(!this->isRunning);
            }
        }
		currentPreset.updateIsRunning(!this->isRunning);
    }
}

string ShaderChain::createUniqueFilePath(string path) {
    bool found = false;
    auto pathWithoutExtension = path.substr(0, path.find_last_of("."));
    auto extension = path.substr(path.find_last_of(".") + 1);
    int tries = 0;

    while (!found) {
        ofFile file;

        string p = "";

        if (tries == 0) {
            p = pathWithoutExtension + "." + extension;
        } else {
            p = pathWithoutExtension + to_string(tries) + "." + extension;
        }

        file.open(p, ofFile::ReadWrite, false);

        if (!file.exists()) {
            found = true;
            path = p;
        }
        file.close();
        tries++;
    }
    return path;
}

bool ShaderChain::mouseScrolled(ofMouseEventArgs & args) {
	if (parameterPanel->isMouseOver()) {


@@ 788,23 555,22 @@ void ShaderChain::newPresetButtonPressed() {
        string result = ofSystemTextBoxDialog("New Preset name", "");
        result = ofToDataPath("presets/" + result);

        for (int i = 0; i < this->passes.size(); i++) {
            ofRemoveListener(this->passes[i]->shaderUpdatedEvent, this, &ShaderChain::SetupGui);
            delete this->passes[i];
        for (int i = 0; i < currentPreset.passes.size(); i++) {
            ofRemoveListener(currentPreset.passes[i]->shaderUpdatedEvent, this, &ShaderChain::SetupGui);
            delete currentPreset.passes[i];
        }
        this->passes.clear();
        this->passesGui->Setup(&this->passes);
		currentPreset.removeAllPasses();
        passesGui->setup(&currentPreset);
        SetupGui();
        pngRenderer->updatePath(result);
        currentPreset.updatePath(result);
		pngRenderer->updateUI(&currentPreset);

        isShowingFileDialogue = false;
    }
}

void ShaderChain::updateShaderJsonPressed() {
    for (int i = 0; i < this->passes.size(); i++) {
        passes[i]->updateShaderJson();
    }
	currentPreset.saveShaderJson();
    updateStatusText("Updated shader json");
}



@@ 815,10 581,10 @@ void ShaderChain::windowResized(int w, int h) {
}

void ShaderChain::shaderPassRightClicked(RightClickedElementData &data) {
    if (data.index < this->passes.size()) {
    if (data.index < currentPreset.passes.size()) {
        ofPoint p = data.position;
        p.y += passesGui->panel->getPosition().y;
        passGui.showWithShaderPass(&gui, p, passes[data.index]);
        passGui.showWithShaderPass(&gui, p, currentPreset.passes[data.index]);
    }
}



@@ 827,10 593,10 @@ void ShaderChain::scrubberProgressed(float &val) {
}

void ShaderChain::saveScreenshot() {
    if (this->passes.size() > 0) {
    if (currentPreset.passes.size() > 0) {
        string destFilePath = pngRenderer->renderDirectory + "screenshot_" + pngRenderer->presetDisplayName.get() + ".png";
        destFilePath = createUniqueFilePath(destFilePath);
        pngRenderer->screenshot(&(this->passes[getLastEnabledPassIndex()]->swapBuffer), destFilePath);
        destFilePath = PathUtil::createUniqueFilePath(destFilePath);
        pngRenderer->screenshot(&(currentPreset.passes[currentPreset.getLastEnabledPassIndex()]->swapBuffer), destFilePath);
        updateStatusText("Saved screenshot to " + destFilePath);
    }
}

M src/ShaderChain.h => src/ShaderChain.h +13 -17
@@ 13,18 13,23 @@
#include "shaderPassGui.h"
#include "previewProgressScrubber.h"
#include "credits.h"
#include "FfmpegManager.h"

class ShaderChain: public ofxMidiListener {
public:

    string defaultPresetPath = "presets/default.json";
    vector<ShaderPass*> passes;
    float time;
    ofParameter<bool> isRunning;
	preset currentPreset;

	ofParameter<bool> isRunning;
    ofxMidiIn midiIn;
    ofxJSONElement result;
    bool isMouseDown;
    ofFbo cumulativeBuffer;
    
	float time;
	bool isMouseDown;

	// FBOS	
	ofFbo cumulativeBuffer;
    ofFbo cumulativeBufferSwap;
    ofFbo cumulativeDrawBuffer;



@@ 46,16 51,10 @@ public:
    void SetupMidi();
    void dragEvent(ofDragInfo info);
    void windowResized(int w, int h);
	static string getSlash() {
#if defined(WIN32) || defined(_WIN32) || defined(__WIN32) && !defined(__CYGWIN__)
		return "\\";
#else
		return "/";
#endif
	}

private:
    PNGRenderer *pngRenderer;
	FfmpegManager *ffmpegManager;
    ofxGui gui;
    ofxGuiContainer *guiGlobal;
    ofxGuiContainer *statusContainer;


@@ 68,8 67,6 @@ private:
    ofVideoGrabber vidGrabber;
    previewProgressScrubber scrubber;

    string ffmpegCommand = "ffmpeg";

    float mouseMoveSpeed = 10.0;

    bool showGui;


@@ 102,12 99,10 @@ private:
    void stopWebcam();
    void freeUnusedResources();
    void pauseResourcesForCurrentPlaybackState();
    string createUniqueFilePath(string path);
    bool mouseScrolled(ofMouseEventArgs & args);
    void loadFfmpegPath();
    void midiButtonPressed();
    MidiMapper midiMapper;

	
    void newPresetButtonPressed();
    void updateShaderJsonPressed();
    void shaderPassRightClicked(RightClickedElementData &data);


@@ 115,4 110,5 @@ private:
    void scrubberProgressed(float &val);
    void saveScreenshot();
    void showCredits();
	void webExportedPressed();
};

M src/ShaderPass.cpp => src/ShaderPass.cpp +4 -2
@@ 129,7 129,6 @@ void ShaderPass::Render(ofFbo *previousBuffer, RenderStruct *renderStruct) {
    for (int i = 0; i < numPassesPerFrame; i++) {
        this->buffer.begin();
        this->shader.begin();

        UpdateTime(renderStruct->time);

        if (previousBuffer != nullptr) {


@@ 411,7 410,10 @@ void ShaderPass::LoadFromJson(Json::Value &json, float width, float height) {
    std::string shaderName = json["shaderName"].asString();
    if (json.isMember("numPassesPerFrame")) {
        this->numPassesPerFrame = json["numPassesPerFrame"].asInt();
    }
	}
	else {
		numPassesPerFrame = 1;
	}
    if (json.isMember("scale")) {
        this->scale = json["scale"].asFloat();
    }

A src/ThreadedSystemCall.cpp => src/ThreadedSystemCall.cpp +11 -0
@@ 0,0 1,11 @@
#include "ThreadedSystemCall.h"

ThreadedSystemCall::ThreadedSystemCall() {
	waitForThread(false);
}

void ThreadedSystemCall::systemCall(std::vector<std::string> commands, ofEvent<void> *completionEvent) {
	this->commands = commands;
	this->completionEvent = completionEvent;
	startThread();
}
\ No newline at end of file

A src/ThreadedSystemCall.h => src/ThreadedSystemCall.h +32 -0
@@ 0,0 1,32 @@
#pragma once

#include <string>
#include <vector>

#include "ofMain.h"

class ThreadedSystemCall : public ofThread {

public:
	ThreadedSystemCall();
	void systemCall(std::vector<std::string> commands, ofEvent<void> *completionEvent);

	void threadedFunction() {
		if (isThreadRunning()) {
			for (string command : commands) {
				system(command.c_str());
			}
			ofNotifyEvent(*completionEvent);
			stopThread();
		}
	}

	~ThreadedSystemCall() {
		stopThread();
		waitForThread(false);
	}

private:
	std::vector<std::string> commands;
	ofEvent<void> *completionEvent;
};
\ No newline at end of file

M src/gui/PassesGui.cpp => src/gui/PassesGui.cpp +3 -4
@@ 10,12 10,11 @@ PassesGui::~PassesGui() {

}

void PassesGui::Setup(std::vector<ShaderPass*> *passes) {
void PassesGui::setup(preset *currentPreset) {
    passButtons->clear();
    this->passes = passes;
    for (int i = 0; i < passes->size(); i++) {
    for (int i = 0; i < currentPreset->passes.size(); i++) {
        ofParameter<string> text;
        text.set(passes->at(i)->displayName);
        text.set(currentPreset->passes.at(i)->displayName);
        passButtons->add(text);
    }
}

M src/gui/PassesGui.h => src/gui/PassesGui.h +2 -2
@@ 4,6 4,7 @@
#include "ShaderPass.h"
#include "ofxGuiExtended2.h"
#include "ofxSortableList.h"
#include "preset.h"

class PassesGui {
public:


@@ 14,10 15,9 @@ public:
    ofxSortableList *passButtons;
    ofxGuiButton addPassButton;

    void Setup(std::vector<ShaderPass*> *passes);
    void setup(preset *currentPreset);
    ofxGuiContainer *panel;

private:
    ofxGui gui;
    std::vector<ShaderPass*> *passes;
};

M src/gui/credits.h => src/gui/credits.h +1 -1
@@ 6,7 6,7 @@
class credits {
public:
    credits();
    bool enabled;
    bool enabled = false;

    void draw();
private:

M src/ofApp.cpp => src/ofApp.cpp +1 -1
@@ 13,7 13,7 @@ ofApp::~ofApp() {
void ofApp::setup(){
    glm::vec2 res = glm::vec2(480, 270);
    ofSetWindowShape(1920, 1080);
    ofSetWindowPosition(0, 0);
    ofSetWindowPosition(0, 60);
    ofSetWindowTitle("ShaderChain");
    this->shaderChain.Setup(res);
}

A src/preset.cpp => src/preset.cpp +172 -0
@@ 0,0 1,172 @@
#include "preset.h"

bool preset::load(string path) {
	cout << "Loading " << path << endl;
	bool parsingSuccessful = json.open(path);

	for (int i = 0; i < this->passes.size(); i++) {
		delete this->passes[i];
	}
	this->passes.clear();
	this->frameskip = 1;
	if (parsingSuccessful) {
		this->updatePath(path);

		this->resolutionX = json["res"]["x"].asFloat();
		this->resolutionY = json["res"]["y"].asFloat();

		if (json.isMember("duration")) {
			this->animduration = json["duration"].asFloat();
		}
		if (json.isMember("fps")) {
			this->fps = json["fps"].asFloat();
		}

		if (json.isMember("blend")) {
			this->numBlendFrames = json["blend"].asInt();
		}

		if (json.isMember("scale")) {
			this->displayScaleParam = json["scale"].asFloat();
		}

		for (int i = 0; i < json["data"].size(); i++) {
			ShaderPass *pass = new ShaderPass();
			pass->LoadFromJson(json["data"][i], this->resolutionX, this->resolutionY);
			this->passes.push_back(pass);
// ->>>>>>			ofAddListener(passes[passes.size() - 1]->shaderUpdatedEvent, this, &ShaderChain::SetupGui);
		}
	}

	return parsingSuccessful;
}

bool preset::save() {
	json.clear();
	json["res"]["x"] = (float)resolutionX;
	json["res"]["y"] = (float)resolutionY;

	json["duration"] = (float)animduration;
	json["fps"] = (float)fps;
	json["blend"] = (float)numBlendFrames;
	json["scale"] = (float)displayScaleParam;

	for (int i = 0; i < passes.size(); i++) {
		json["data"][i]["shaderName"] = passes[i]->filePath;
		json["data"][i]["numPassesPerFrame"] = passes[i]->numPassesPerFrame;
		json["data"][i]["scale"] = passes[i]->scale;
		for (int j = 0; j < passes[i]->params.size(); j++) {
			json["data"][i]["parameters"][j]["name"] = passes[i]->params[j]->uniform;
			passes[i]->params[j]->UpdateJson((json["data"][i]["parameters"][j]));
		}
	}

	return json.save(presetFilePath, true);
}

void preset::addPass(ShaderPass *pass) {
	passes.push_back(pass);
}

void preset::removePass(int index) {
	auto item = (passes.begin() + index);
	passes.erase(item);
}

void preset::removeAllPasses() {
	passes.clear();
}

void preset::swapPass(int from, int to) {
	iter_swap(passes.begin() + from, passes.begin() + to);
}

bool preset::requiresWebcam() {
	for (int i = 0; i < this->passes.size(); i++) {
		for (int j = 0; j < this->passes[i]->params.size(); j++) {
			if (passes[i]->params[j]->getTextureSourceType() == Webcam) {
				return true;
			}
		}
	}
	return false;
}

void preset::updateIsRunning(bool isRunning) {
	for (int i = 0; i < this->passes.size(); i++) {
		for (int j = 0; j < this->passes[i]->params.size(); j++) {
			passes[i]->params[j]->playbackDidToggleState(isRunning);
		}
	}
}

void preset::saveShaderJson() {
	for (int i = 0; i < passes.size(); i++) {
		passes[i]->updateShaderJson();
	}
}


void preset::updateResolution(int width, int height) {
	for (unsigned int i = 0; i < this->passes.size(); i++) {
		passes[i]->UpdateResolution(width, height);
	}
}

void preset::updatePreRender(RenderStruct *renderStruct) {
	for (int i = 0; i < passes.size(); i++) {
		passes[i]->update(renderStruct);
	}
}

int preset::getFirstEnabledPassIndex() {
	int idx = 0;
	for (int i = 0; i < this->passes.size(); i++) {
		if (this->passes[i]->enabled) {
			return i;
		}
	}
	return 0;
}

int preset::getLastEnabledPassIndex() {
	for (int i = this->passes.size() - 1; i >= 0; i--) {
		if (this->passes[i]->enabled) {
			return i;
		}
	}
	return 0;
}

void preset::startRender() {
	for (int i = 0; i < passes.size(); i++) {
		passes[i]->startOfflineRender();
	}
}

void preset::updatePath(string s) {
		if (s == "") return;

#if defined(WIN32) || defined(_WIN32) || defined(__WIN32) && !defined(__CYGWIN__)

		replace(s.begin(), s.end(), '/', '\\');
		static const std::string slash = "\\";
#else
		static const std::string slash = "/";
#endif

		string file = s.substr(s.find_last_of(slash) + 1);

		// add json extension if needed
		int indexOfPeriod = file.find_last_of(".");
		if (indexOfPeriod == std::string::npos) {
			file += ".json";
			s += ".json";
			indexOfPeriod = file.find_last_of(".");
		}

		string fileWithoutExtension = file.substr(0, indexOfPeriod);

		presetDisplayName = fileWithoutExtension;
		presetFilePath = s;
}
\ No newline at end of file

A src/preset.h => src/preset.h +49 -0
@@ 0,0 1,49 @@
#pragma once

#include <string>
#include <vector>
#include "ShaderPass.h"
#include "RenderStruct.h"
class preset {

public:
	string name;
	string presetFilePath;

	// Without the full path or .json file extension 
	string presetDisplayName;

	vector<ShaderPass*> passes;

	int resolutionX;
	int resolutionY;
	float displayScaleParam;
	int frameskip;
	int numLoops;
	int numBlendFrames;
	float animduration;
	int fps;
	
	ofxJSONElement json;
	bool load(string name);
	bool save();
	void addPass(ShaderPass *pass);
	void removePass(int index);
	void removeAllPasses();
	void swapPass(int from, int to);
	void updateIsRunning(bool isRunning);
	bool requiresWebcam();

	// Seralize the json in the header of the shader
	void saveShaderJson();

	void updateResolution(int width, int height);
	void updatePreRender(RenderStruct *renderStruct);
	void updatePath(string s);

	void startRender();

	int getFirstEnabledPassIndex();
	int getLastEnabledPassIndex();

};
\ No newline at end of file

A src/webExport.cpp => src/webExport.cpp +247 -0
@@ 0,0 1,247 @@
#include "webExport.h"

void webExport::exportPreset(preset *currentPreset) {

	string webTemplatePath = "data" + PathUtil::getSlash() + "web" + PathUtil::getSlash() + "template.html";
	string exportPath = "data" + PathUtil::getSlash() + "web" + PathUtil::getSlash();
	
	string exportFolderPath = exportPath + PathUtil::getSlash() + currentPreset->presetDisplayName;
	string exportFilePath = exportFolderPath + PathUtil::getSlash() + "index.html";

	string mkdirCommand = "mkdir " + exportFolderPath;
	system(mkdirCommand.c_str());

	// Patch template to reference json
	string wordToReplace = "test.json";
	string wordToReplaceWith = currentPreset->presetDisplayName + ".json";

	string passesString = "";

	for (int i = 0; i < currentPreset->passes.size(); i++) {
		passesString += "<a href=\"" + currentPreset->passes[i]->filePath + ".frag\">" + currentPreset->passes[i]->displayName + "</a>";

		if (i != currentPreset->passes.size() - 1) {
			passesString += " -> ";
		}
	}

	std::map<string, string> words;
	words.insert(std::make_pair(wordToReplace, wordToReplaceWith));
	words.insert(std::make_pair("###PASSES###", passesString));

	copyFileReplacingText(webTemplatePath, exportFilePath, words);

	copyResources(exportPath, exportFolderPath, currentPreset);
	exportMainIndex(currentPreset, exportPath);
}

void webExport::createPatchedTemplate(string templatePath, string output) {

}

void webExport::copyResources(string exportPath, string exportFolderPath, preset *currentPreset) {
	string exportPresetPath = exportFolderPath + PathUtil::getSlash() + currentPreset->presetDisplayName + ".json";
	string shaderFolderPath = exportFolderPath + PathUtil::getSlash() + "shaders";

	// Make Shaders folder
	string mkdirCommand = "mkdir " + shaderFolderPath;
	system(mkdirCommand.c_str());

	// Copy shaders into shaders/ dir
	for (int i = 0; i < currentPreset->passes.size(); i++) {
	
		string sourcePath = "data" + PathUtil::getSlash() + currentPreset->passes[i]->filePath + ".frag";
		string destPath = exportFolderPath + PathUtil::getSlash() + currentPreset->passes[i]->filePath + ".frag";

		cout << "copy " << sourcePath << " to " << destPath << endl;

		std::map<string, string> words;
		words.insert(std::make_pair("out vec4 outputColor;", ""));
		words.insert(std::make_pair("outputColor", "gl_FragColor"));
		words.insert(std::make_pair("uv", "vUv"));
		words.insert(std::make_pair("uv", "vUv"));
		words.insert(std::make_pair("in vec2 uv;", "varying vec2 uv;"));
		words.insert(std::make_pair("in vec2 texCoord;", "varying vec2 texCoord;"));
		words.insert(std::make_pair("texture(", "texture2D("));

		words.insert(std::make_pair("#pragma include", ""));

		words.insert(std::make_pair("#version 150", ""));

		copyFileReplacingText(sourcePath, destPath, words);
	}

	// Export preset json
	PathUtil::copyFile(currentPreset->presetFilePath, exportPresetPath);
}

void webExport::copyFileReplacingText(string inputFilePath, string outputFilePath, std::map<std::string, std::string> words) {
	ifstream in(inputFilePath);
	ofstream out(outputFilePath);
	if (!in) {
		cerr << "Could not open " << inputFilePath << "\n";
		return;
	}

	if (!out) {
		cerr << "Could not open " << outputFilePath << "\n";
		return;
	}

	string line;
	while (getline(in, line))
	{
		std::map<string, string>::iterator it = words.begin();
		while (it != words.end()) {
			size_t len = it->first.length();
			size_t pos = line.find(it->first);

			if (pos != string::npos) {

				// Replace text with include file
				if (it->first == "#pragma include") {

					string replaceFilePath = "data" + PathUtil::getSlash() + line.substr(pos + it->first.length() + 1);
					replaceFilePath.erase(std::remove(replaceFilePath.begin(), replaceFilePath.end(), '\"'), replaceFilePath.end());

					ifstream includeFile(replaceFilePath);
					string includeLine;
					string includeFileString = "";

					if (includeFile) {
						while (getline(includeFile, includeLine))
						{
							includeFileString += includeLine + '\n';
						}
					}
					else {
						cerr << "Could not open include file " << replaceFilePath << "\n";
					}

					line.replace(pos, line.length(), includeFileString);
				}
				else {
					line = replaceAll(line, it->first, it->second);
				}
				it++;
			}
			else {
				it++;
			}
		}
		
		out << line << '\n';
	}
}

void webExport::exportMainIndex(preset *currentPreset, string presetExportPath) {
	ofxJSONElement json;
	string manifestPath = "web/manifest.json";
	string templatePath = "data/web/template_index.html";
	string outputPath = "data/web/index.html";
	bool parsingSuccessful = json.open(manifestPath);

	if (parsingSuccessful) {
		string patchString = "";
		ofxJSONElement latestJson;
		latestJson["link"] = currentPreset->presetDisplayName;
		
		// Check to see if preset already exported
		int presetIndex = json["data"].size();
		for (int i = 0; i < json["data"].size(); i++) {
			if (json["data"][i]["link"].asString() == currentPreset->presetDisplayName) {
				presetIndex = i;
				break;
			}
		}

		json["data"][presetIndex] = latestJson;

		json.save(manifestPath, true);

		for (int i = 0; i < json["data"].size(); i++) {
			string linkString = json["data"][i]["link"].asString();
			string imageString = "thumbnails/" + linkString + ".png";
			patchString += "\n<div class=\"button\">\n\t<div id=\"buttonText\">\n\t\t<a href=\"" + linkString + "\"><img src=\"" + imageString + "\"/></a>\n\t</div>\n</div>\n";
		}

		std::map<string, string> words;
		words.insert(std::make_pair("###LINKS###", patchString));
		words.insert(std::make_pair("###TITLE###", json["title"].asString()));
		words.insert(std::make_pair("###DESCRIPTION###", json["description"].asString()));

		copyFileReplacingText(templatePath, outputPath, words);
	}
	else {
		cout << "Failed to load " << manifestPath << endl;
	}
}

void webExport::updateAllHTML() {
	string manifestPath = "web/manifest.json";
	string webTemplatePath = "data" + PathUtil::getSlash() + "web" + PathUtil::getSlash() + "template.html";
	string exportPath = "data" + PathUtil::getSlash() + "web" + PathUtil::getSlash();

	ofxJSONElement json;
	cout << "Updating all html..." << endl;
	// Iterate thru the manifest and recreate the HTML from template.
	bool parsingSuccessful = json.open(manifestPath);
	if (parsingSuccessful) {
		cout << "Parsed Manifest..." << endl;
		for (int i = 0; i < json["data"].size(); i++) {

			string dirName = json["data"][i]["link"].asString();
			cout << "Trying dir: " << dirName << endl;
			ofDirectory dir("web/" + dirName);
			if (dir.exists()) {
				cout << "Dir exists: " << dirName << endl;

				// Open the preset (should be the same name as the dir)
				string presetPath = "web/" + dirName + PathUtil::getSlash() + dirName + ".json";

				if (PathUtil::fileExists(presetPath)) {
					cout << "Preset exists: " << presetPath << endl;

					ofxJSONElement presetJson;
					parsingSuccessful = presetJson.open(presetPath);
					
					string passesString = "";

					for (int j = 0; j < presetJson["data"].size(); j++) {
						string shaderLink = presetJson["data"][j]["shaderName"].asString();
						std::size_t shaderNameIdx = shaderLink.find_last_of("\\");
						string shaderName = shaderLink.substr(shaderNameIdx + 1);

						passesString += "<a href=\"" + shaderLink + ".frag\">" + shaderName + "</a>";

						if (j != presetJson["data"].size() - 1) {
							passesString += " -> ";
						}
					}
					cout << "Passes string: " << passesString << endl;

					std::map<string, string> words;
					words.insert(std::make_pair("test.json", dirName + ".json"));
					words.insert(std::make_pair("###PASSES###", passesString));

					if (parsingSuccessful) {
						cout << "Parsed: " << presetPath << endl;

						string exportFilePath = "data" + PathUtil::getSlash() + "web" + PathUtil::getSlash() + dirName + PathUtil::getSlash() + "index.html";
						copyFileReplacingText(webTemplatePath, exportFilePath, words);
					}
				}
			}
		}
	}
}

// from https://stackoverflow.com/questions/2896600/how-to-replace-all-occurrences-of-a-character-in-string
std::string webExport::replaceAll(std::string str, const std::string& from, const std::string& to) {
	size_t start_pos = 0;
	while ((start_pos = str.find(from, start_pos)) != std::string::npos) {
		str.replace(start_pos, from.length(), to);
		start_pos += to.length(); // Handles case where 'to' is a substring of 'from'
	}
	return str;
}
\ No newline at end of file

A src/webExport.h => src/webExport.h +19 -0
@@ 0,0 1,19 @@
#pragma once

#include <string>

#include "preset.h"
#include "PathUtil.h"

class webExport {
public:
	void exportPreset(preset *currentPreset);
	void updateAllHTML();

private:
	void createPatchedTemplate(string templatePath, string output);
	void copyResources(string exportPath, string exportFolderPath, preset *currentPreset);
	void copyFileReplacingText(string inputFilePath, string outputFilePath, std::map<std::string, std::string> words);
	std::string replaceAll(std::string str, const std::string& from, const std::string& to);
	void exportMainIndex(preset *currentPreset, string presetExportPath);
};
\ No newline at end of file