computerscare-vcv-modules

ComputerScare modules for VCV Rack
Log | Files | Refs

ComputerscareBlank.cpp (37527B)


      1 #include "Computerscare.hpp"
      2 #include "ComputerscareResizableHandle.hpp"
      3 #include "animatedGif.hpp"
      4 #include "CustomBlankFunctions.hpp"
      5 
      6 #include <osdialog.h>
      7 #include <iostream>
      8 #include <fstream>
      9 #include <sstream>
     10 #include <thread>
     11 #include <dirent.h>
     12 #include <algorithm>
     13 #include <random>
     14 #include <settings.hpp>
     15 #include <cctype>
     16 #include <algorithm>
     17 
     18 struct ComputerscareBlank : ComputerscareMenuParamModule {
     19 	bool loading = true;
     20 	bool loadedJSON = false;
     21 	bool jsonFlag = false;
     22 
     23 	bool ready = false;
     24 	std::string path;
     25 	std::string parentDirectory;
     26 
     27 	std::vector<std::string> paths;
     28 	std::vector<std::string> catalog;
     29 	int fileIndexInCatalog;
     30 	unsigned int numFilesInCatalog = 0;
     31 
     32 	float width = 120;
     33 	float height = 380;
     34 	int rotation = 0;
     35 	bool invertY = true;
     36 	float zoomX = 1.f;
     37 	float zoomY = 1.f;
     38 	float xOffset = 0.f;
     39 	float yOffset = 0.f;
     40 	int imageFitEnum = 0;
     41 	int currentFrame = 0;
     42 	int mappedFrame = 0;
     43 	int numFrames = 0;
     44 	int sampleCounter = 0;
     45 	float frameDelay = .5;
     46 	std::vector<float> frameDelays;
     47 	std::vector<int> frameMapForScan;
     48 	std::vector<int> shuffledFrames;
     49 	std::vector<float> gifDurationsForPingPong;
     50 	std::vector<int> framesForward;
     51 	std::vector<int> framesReverse;
     52 	std::vector<int> framesPingpong;
     53 
     54 	std::vector<std::vector<int>> frameScripts;
     55 
     56 	float totalGifDuration = 0.f;
     57 
     58 	int samplesDelay = 10000;
     59 	int speed = 100000;
     60 	int imageStatus = 0;
     61 	bool scrubbing = false;
     62 	int scrubFrame = 0;
     63 
     64 	int pingPongDirection = 1;
     65 
     66 	float speedFactor = 1.f;
     67 
     68 	float zeroOffset = 0.f;
     69 
     70 	bool tick = false;
     71 	float lastShuffle = 2.f;
     72 
     73 	float lastZoom = -100;
     74 	int zoomCheckInterval = 5000;
     75 	int zoomCheckCounter = 0;
     76 	bool pauseAnimation = true;
     77 
     78 	/*
     79 		uninitialized: 0
     80 		gif: 1
     81 		not gif: 2
     82 		error:3
     83 	*/
     84 
     85 	bool expanderConnected = false;
     86 
     87 	int clockMode = CLOCK_MODE_SYNC;
     88 	bool clockConnected = false;
     89 	bool resetConnected = false;
     90 	bool nextFileInputConnected = false;
     91 
     92 	std::vector<std::string> animationModeDescriptions;
     93 	std::vector<std::string> endBehaviorDescriptions;
     94 	std::vector<std::string> nextFileDescriptions;
     95 
     96 
     97 	dsp::SchmittTrigger clockTrigger;
     98 	dsp::SchmittTrigger resetTrigger;
     99 	dsp::SchmittTrigger resetButtonTrigger;
    100 
    101 	dsp::SchmittTrigger nextFileTrigger;
    102 	dsp::SchmittTrigger nextFileButtonTrigger;
    103 
    104 	dsp::Timer syncTimer;
    105 	dsp::Timer slideshowTimer;
    106 
    107 	dsp::PulseGenerator resetTriggerPulse;
    108 
    109 
    110 	ComputerscareSVGPanel* panelRef;
    111 
    112 	float leftMessages[2][11] = {};
    113 
    114 	enum ClockModes {
    115 		CLOCK_MODE_SYNC,
    116 		CLOCK_MODE_SCAN,
    117 		CLOCK_MODE_FRAME
    118 	};
    119 
    120 	enum AnimationModes {
    121 		ANIMATION_FORWARD,
    122 		ANIMATION_REVERSE,
    123 		ANIMATION_PINGPONG,
    124 		ANIMATION_SHUFFLE,
    125 		ANIMATION_RANDOM
    126 	};
    127 	enum ParamIds {
    128 		ANIMATION_SPEED,
    129 		ANIMATION_ENABLED,
    130 		CONSTANT_FRAME_DELAY,
    131 		ANIMATION_MODE,
    132 		END_BEHAVIOR,
    133 		SHUFFLE_SEED,
    134 		NEXT_FILE_BEHAVIOR,
    135 		SLIDESHOW_ACTIVE,
    136 		SLIDESHOW_TIME,
    137 		LIGHT_WIDGET_MODE,
    138 		NUM_PARAMS
    139 	};
    140 	enum InputIds {
    141 		NUM_INPUTS
    142 	};
    143 	enum OutputIds {
    144 		NUM_OUTPUTS
    145 	};
    146 	enum LightIds {
    147 		NUM_LIGHTS
    148 	};
    149 
    150 
    151 	ComputerscareBlank()  {
    152 		animationModeDescriptions.push_back("Forward");
    153 		animationModeDescriptions.push_back("Reverse");
    154 		animationModeDescriptions.push_back("Ping Pong");
    155 		animationModeDescriptions.push_back("Random Shuffled");
    156 		animationModeDescriptions.push_back("Full Random");
    157 
    158 		endBehaviorDescriptions.push_back("Repeat");
    159 		endBehaviorDescriptions.push_back("Stop");
    160 		endBehaviorDescriptions.push_back("Select Random");
    161 		endBehaviorDescriptions.push_back("Load Next");
    162 		endBehaviorDescriptions.push_back("Load Previous");
    163 
    164 		nextFileDescriptions.push_back("Load Next (Alphabetical) File in Directory");
    165 		nextFileDescriptions.push_back("Load Previous (Alphabetical) File in Directory");
    166 		nextFileDescriptions.push_back("Load Random File from Directory");
    167 
    168 		config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
    169 
    170 		configMenuParam(ANIMATION_SPEED, -1.f, 1.f, 0.f, "Animation Speed", 2, "x", 20.f);
    171 		configParam(ANIMATION_ENABLED, 0.f, 1.f, 1.f, "Animation Enabled");
    172 		configParam(CONSTANT_FRAME_DELAY, 0.f, 1.f, 0.f, "Constant Frame Delay");
    173 		configMenuParam(ANIMATION_MODE, 0.f, "Animation Mode", animationModeDescriptions);
    174 		configMenuParam(NEXT_FILE_BEHAVIOR, 0.f, "Next File Trigger / Button Behavior", nextFileDescriptions);
    175 		configMenuParam(SHUFFLE_SEED, 0.f, 1.f, 0.5f, "Shuffle Seed", 2);
    176 
    177 		configParam(SLIDESHOW_ACTIVE, 0.f, 1.f, 0.f, "Slideshow Active");
    178 		configMenuParam(SLIDESHOW_TIME, 0.f, 1.f, 0.200948f, "Slideshow Time", 2, " s",  400.f, 3.f);
    179 		configParam(LIGHT_WIDGET_MODE, 0.f, 1.f, 0.f, "Keep image fully opaque when dimming room lights");
    180 
    181 		paths.push_back("empty");
    182 
    183 		leftExpander.producerMessage = leftMessages[0];
    184 		leftExpander.consumerMessage = leftMessages[1];
    185 
    186 	}
    187 	void process(const ProcessArgs &args) override {
    188 		if (imageStatus == 1) {
    189 			sampleCounter++;
    190 			zoomCheckCounter++;
    191 			if (zoomCheckCounter > zoomCheckInterval) {
    192 				float zoom =  APP->scene->rackScroll->getZoom();
    193 				if (zoom != lastZoom) {
    194 					pauseAnimation = true;
    195 				}
    196 				else {
    197 					pauseAnimation = false;
    198 				}
    199 				lastZoom = zoom;
    200 				zoomCheckCounter = 0;
    201 			}
    202 		}
    203 
    204 		if (params[SLIDESHOW_ACTIVE].getValue()) {
    205 			//float dTime = exp(5 * params[SLIDESHOW_TIME].getValue());
    206 			float dTime =  3 * std::pow(400.f , params[SLIDESHOW_TIME].getValue());
    207 			if (slideshowTimer.process(args.sampleTime) > dTime) {
    208 				checkAndPerformEndAction(true);
    209 				slideshowTimer.reset();
    210 			}
    211 		}
    212 
    213 		samplesDelay = frameDelay * args.sampleRate;
    214 
    215 		bool shouldAdvanceAnimation = false;
    216 		bool clockTriggered = false;
    217 
    218 		if (ready && leftExpander.module && leftExpander.module->model == modelComputerscareBlankExpander) {
    219 			expanderConnected = true;
    220 			// me
    221 			float *messageToSendToExpander = (float*) leftExpander.module->rightExpander.producerMessage;
    222 
    223 			float *messageFromExpander = (float*) leftExpander.consumerMessage;
    224 
    225 			clockMode = messageFromExpander[0];
    226 			clockConnected = messageFromExpander[1];
    227 			resetConnected = messageFromExpander[3];
    228 			nextFileInputConnected = messageFromExpander[5];
    229 
    230 			zeroOffset = messageFromExpander[7];
    231 
    232 			scrubbing = messageFromExpander[8];
    233 
    234 			updateScrubFrame();
    235 
    236 			bool resetTriggered = false;
    237 			bool resetTimerHigh = false;
    238 
    239 			if (resetConnected) {
    240 				if (resetTrigger.process(messageFromExpander[4])) {
    241 					resetTriggered = true;
    242 
    243 					resetTriggerPulse.trigger(1e-3f);
    244 				}
    245 			}
    246 
    247 			resetTimerHigh = resetTriggerPulse.process(args.sampleTime);
    248 
    249 			if (clockConnected) {
    250 				clockTriggered = clockTrigger.process(messageFromExpander[2]);
    251 				if (clockMode == CLOCK_MODE_SYNC) {
    252 					//sync
    253 					float currentSyncTime = syncTimer.process(args.sampleTime);
    254 					if (clockTriggered) {
    255 						syncTimer.reset();
    256 						setSyncTime(currentSyncTime);
    257 						if (params[ANIMATION_ENABLED].getValue()) {
    258 							goToFrame(0);
    259 						}
    260 					}
    261 				}
    262 
    263 				else if (clockMode == CLOCK_MODE_SCAN) {
    264 					//scan
    265 					float scanPosition = messageFromExpander[2];
    266 					scanToPosition(scanPosition);
    267 				}
    268 				else if (clockMode == CLOCK_MODE_FRAME) {
    269 					//frame advance
    270 					//should be ignored if being reset
    271 					shouldAdvanceAnimation = clockTriggered && !resetTimerHigh;
    272 				}
    273 			}
    274 
    275 			if (nextFileInputConnected) {
    276 				if (nextFileTrigger.process(messageFromExpander[6])) {
    277 					checkAndPerformEndAction(true);
    278 				}
    279 			}
    280 
    281 			if (nextFileButtonTrigger.process(messageFromExpander[10])) {
    282 				checkAndPerformEndAction(true);
    283 			}
    284 
    285 			if (resetTriggered) {
    286 				goToFrame(0);
    287 			}
    288 			if (resetButtonTrigger.process(messageFromExpander[9])) {
    289 				goToFrame(0);
    290 			}
    291 
    292 			messageToSendToExpander[0] = float (currentFrame);
    293 			messageToSendToExpander[1] = float (numFrames);
    294 			messageToSendToExpander[2] = float (mappedFrame);
    295 			messageToSendToExpander[3] = float (scrubFrame);
    296 			messageToSendToExpander[4] = float (tick);
    297 
    298 			// Flip messages at the end of the timestep
    299 			leftExpander.module->rightExpander.messageFlipRequested = true;
    300 		}
    301 		else {
    302 			expanderConnected = false;
    303 		}
    304 
    305 		if (expanderConnected && clockConnected && (clockMode == CLOCK_MODE_FRAME)) {
    306 			//no-op for frame mode for some reason?
    307 		}
    308 		else {
    309 			if (sampleCounter > samplesDelay) {
    310 				sampleCounter = 0;
    311 				shouldAdvanceAnimation = true;
    312 			}
    313 		}
    314 		if (numFrames > 1) {
    315 			if (params[ANIMATION_ENABLED].getValue() && shouldAdvanceAnimation) {
    316 				tickAnimation();
    317 				//checkAndPerformEndAction();
    318 			}
    319 		}
    320 		else {
    321 			if ((clockTriggered && (clockConnected && clockMode == CLOCK_MODE_SYNC)) || numFrames > 1) {
    322 				if (currentFrame == 0) {
    323 					//checkAndPerformEndAction();
    324 				}
    325 			}
    326 		}
    327 	}
    328 
    329 	bool getLightWidgetMode() {
    330 		return params[LIGHT_WIDGET_MODE].getValue();
    331 	}
    332 	void updateScrubFrame() {
    333 		if (ready) {
    334 			scrubFrame = mapBlankFrameOffset(zeroOffset, numFrames);
    335 		}
    336 	}
    337 
    338 	void onReset() override {
    339 		zoomX = 1;
    340 		zoomY = 1;
    341 		xOffset = 0;
    342 		yOffset = 0;
    343 	}
    344 	void loadImageDialog(int index = 0) {
    345 		std::string dir = this->paths[index].empty() ?  asset::user("../") : asset::user(this->paths[index]);
    346 		char* pathC = osdialog_file(OSDIALOG_OPEN, dir.c_str(), NULL, NULL);
    347 		if (!pathC) {
    348 			return;
    349 		}
    350 
    351 		std::string path = pathC;
    352 		std::free(pathC);
    353 
    354 		setPath(path);
    355 		jsonFlag = false;
    356 	}
    357 	void setContainingDirectory(int index = 0) {
    358 		std::string dir = system::getDirectory(asset::user(paths[index]));
    359 		std::string currentImageFullpath;
    360 		parentDirectory = dir;
    361 
    362 		int imageIndex = 0;;
    363 
    364 		struct dirent* dirp = NULL;
    365 		DIR* rep = NULL;
    366 		rep = opendir(dir.c_str());
    367 		catalog.clear();
    368 
    369 		if (rep) {
    370 			while ((dirp = readdir(rep)) != NULL) {
    371 				std::string name = dirp->d_name;
    372 
    373 				std::size_t found = name.find(".gif", name.length() - 5);
    374 				if (found == std::string::npos) found = name.find(".GIF", name.length() - 5);
    375 				if (found == std::string::npos) found = name.find(".png", name.length() - 5);
    376 				if (found == std::string::npos) found = name.find(".PNG", name.length() - 5);
    377 				if (found == std::string::npos) found = name.find(".jpg", name.length() - 5);
    378 				if (found == std::string::npos) found = name.find(".JPG", name.length() - 5);
    379 				if (found == std::string::npos) found = name.find(".jpeg", name.length() - 5);
    380 				if (found == std::string::npos) found = name.find(".JPEG", name.length() - 5);
    381 				if (found == std::string::npos) found = name.find(".bmp", name.length() - 5);
    382 				if (found == std::string::npos) found = name.find(".BMP", name.length() - 5);
    383 				if (found != std::string::npos) {
    384 					currentImageFullpath = parentDirectory + "/" + name;
    385 					catalog.push_back(currentImageFullpath);
    386 					if (currentImageFullpath == paths[index]) {
    387 						fileIndexInCatalog = imageIndex;
    388 					}
    389 					imageIndex++;
    390 				}
    391 			}
    392 		}
    393 		numFilesInCatalog = catalog.size();
    394 	}
    395 
    396 	void loadRandomGif() {
    397 		fileIndexInCatalog = floor(random::uniform() * numFilesInCatalog);
    398 		loadNewFileByIndex();
    399 	}
    400 
    401 	void loadNewFileByIndex() {
    402 		if (numFilesInCatalog > 0) {
    403 			setPath(catalog[fileIndexInCatalog]);
    404 		}
    405 	}
    406 	void nextFileInCatalog() {
    407 		if (numFilesInCatalog > 0) {
    408 			fileIndexInCatalog++;
    409 			fileIndexInCatalog %= numFilesInCatalog;
    410 			loadNewFileByIndex();
    411 		}
    412 	}
    413 	void prevFileInCatalog() {
    414 		if (numFilesInCatalog > 0) {
    415 			fileIndexInCatalog--;
    416 			fileIndexInCatalog += numFilesInCatalog;
    417 			fileIndexInCatalog %= numFilesInCatalog;
    418 			loadNewFileByIndex();
    419 		}
    420 	}
    421 	void goToFileInCatelog(int index) {
    422 		fileIndexInCatalog = index;
    423 		fileIndexInCatalog %= numFilesInCatalog;
    424 		loadNewFileByIndex();
    425 	}
    426 
    427 	void setPath(std::string path, int index = 0) {
    428 		numFrames = 0;
    429 		paths[index] = path;
    430 		currentFrame = 0;
    431 	}
    432 	void setFrameCount(int frameCount) {
    433 		numFrames = frameCount;
    434 	}
    435 	void setImageStatus(int status) {
    436 		imageStatus = status;
    437 	}
    438 	void setSyncTime(float syncDuration) {
    439 		bool constantFrameDelay = params[CONSTANT_FRAME_DELAY].getValue() == 1;
    440 		if (params[ANIMATION_MODE].getValue() == ANIMATION_PINGPONG) {
    441 			if (numFrames > 1) {
    442 				float totalDurationForCurrentZeroOffset = gifDurationsForPingPong[mapBlankFrameOffset(zeroOffset, numFrames)];
    443 				if (constantFrameDelay) {
    444 					speedFactor = (2 * numFrames - 2) * defaultFrameDelayCentiseconds / syncDuration / 100;
    445 				}
    446 				else {
    447 					speedFactor = totalDurationForCurrentZeroOffset / syncDuration;
    448 				}
    449 			}
    450 		}
    451 		else { //all other modes
    452 			if (constantFrameDelay) {
    453 				speedFactor = numFrames * defaultFrameDelayCentiseconds / syncDuration / 100;
    454 			}
    455 			else {
    456 				speedFactor = totalGifDuration / syncDuration;
    457 			}
    458 		}
    459 	}
    460 	void setFrameDelay(float frameDelaySeconds) {
    461 		float speedKnob = std::pow(20.f, params[ANIMATION_SPEED].getValue());
    462 		float appliedSpeedDivisor = 1;
    463 		float base = frameDelaySeconds;
    464 
    465 		if (expanderConnected && clockConnected && (clockMode == CLOCK_MODE_SYNC)) {
    466 			appliedSpeedDivisor = speedFactor;
    467 		}
    468 		else {
    469 			appliedSpeedDivisor = speedKnob;
    470 		}
    471 
    472 
    473 		if (params[CONSTANT_FRAME_DELAY].getValue()) {
    474 			frameDelay = defaultFrameDelayCentiseconds / appliedSpeedDivisor / 100;
    475 		}
    476 		else {
    477 			frameDelay = base / appliedSpeedDivisor;
    478 		}
    479 
    480 	}
    481 	void setFrameDelays(std::vector<float> frameDelaysSeconds) {
    482 		frameDelays = frameDelaysSeconds;
    483 		setFrameMap();
    484 		setFrameShuffle();
    485 		setRegularFrameOrders();
    486 
    487 		frameScripts.resize(0);
    488 		frameScripts.push_back(framesForward);
    489 		frameScripts.push_back(framesReverse);
    490 		frameScripts.push_back(framesPingpong);
    491 		frameScripts.push_back(shuffledFrames);
    492 		frameScripts.push_back(framesForward);
    493 
    494 	}
    495 	void setReady(bool v) {
    496 		ready = v;
    497 	}
    498 	void setFrameMap() {
    499 		frameMapForScan.resize(0);
    500 
    501 		//  tokenStack.insert(tokenStack.end(), terminalStack.begin(), terminalStack.end());
    502 
    503 		for (unsigned int i = 0; i < frameDelays.size(); i++) {
    504 			int currentCentiseconds = (int) (frameDelays[i] * 100);
    505 			for (int j = 0; j < currentCentiseconds; j++) {
    506 				frameMapForScan.push_back(i);
    507 			}
    508 		}
    509 	}
    510 	void setFrameShuffle() {
    511 		shuffledFrames.resize(0);
    512 		for (int i = 0; i < numFrames; i++) {
    513 			shuffledFrames.push_back(i);
    514 		}
    515 		unsigned seed = (unsigned) (floor(params[SHUFFLE_SEED].getValue() * 999101));
    516 
    517 		std::shuffle(std::begin(shuffledFrames), std::end(shuffledFrames), std::default_random_engine(seed));
    518 
    519 	}
    520 	void setRegularFrameOrders() {
    521 		framesForward.resize(0);
    522 		framesReverse.resize(0);
    523 		framesPingpong.resize(0);
    524 		for (int i = 0; i < numFrames; i++) {
    525 			framesForward.push_back(i);
    526 			framesReverse.push_back(numFrames - i - 1);
    527 			framesPingpong.push_back(i);
    528 		}
    529 		framesPingpong.insert( framesPingpong.end(), framesReverse.begin() + 1, framesReverse.end() - 1 );
    530 
    531 
    532 	}
    533 	void setTotalGifDuration(float totalDuration) {
    534 		totalGifDuration = totalDuration;
    535 	}
    536 	void setTotalGifDurationIfInPingPongMode(std::vector<float> durs) {
    537 		/* the gif has a different total pingpong duration depending on where the reset frame is
    538 			eg a gif with frame durations:
    539 			1) 100
    540 			2) 5
    541 			3) 5
    542 			4) 5
    543 
    544 			if ponging around frame 1 has duration: 100(1)+ 5(2) + 5(3) + 5(4) + 5(3) + 5(2) = 125
    545 
    546 			but if ponging around frame 3: 5(3) + 5(4) + 100(1) + 5(2) + 100(1) + 5(4) = 240
    547 
    548 		*/
    549 		gifDurationsForPingPong = durs;
    550 
    551 	}
    552 	std::string getPath() {
    553 		//return numFrames > 0 ? paths[currentFrame] : "";
    554 		return paths[0];
    555 	}
    556 
    557 	/*
    558 	enum AnimationModes {
    559 		ANIMATION_FORWARD,
    560 		ANIMATION_REVERSE,
    561 		ANIMATION_PINGPONG,
    562 		ANIMATION_SHUFFLE,
    563 		ANIMATION_RANDOM
    564 	};
    565 	*/
    566 	void tickAnimation() {
    567 		if (numFrames > 1) {
    568 			int animationMode = params[ANIMATION_MODE].getValue();
    569 
    570 			bool onFinalFrame = currentFrame == frameScripts[animationMode].size() - 1;
    571 
    572 			if (animationMode == ANIMATION_RANDOM) {
    573 				onFinalFrame = false;
    574 			}
    575 
    576 			if (clockConnected && clockMode == CLOCK_MODE_SYNC && onFinalFrame) {
    577 				//hold up animation back to 1st frame when controlled.  Wait for clock signal
    578 			}
    579 			else {
    580 				/*if (animationMode == ANIMATION_FORWARD || animationMode == ANIMATION_SHUFFLE) {
    581 					nextFrame();
    582 				} else if (animationMode == ANIMATION_REVERSE)  {
    583 					prevFrame();
    584 				}
    585 				else if (animationMode == ANIMATION_PINGPONG) {
    586 					if (pingPongDirection == 1) {
    587 						nextFrame();
    588 					} else {
    589 						prevFrame();
    590 					}
    591 				}*/nextFrame();
    592 			}
    593 			if (animationMode == ANIMATION_RANDOM ) {
    594 				goToRandomFrame();
    595 			}
    596 			tick = !tick;
    597 			if (animationMode == ANIMATION_PINGPONG) {
    598 				if (pingPongDirection == 1) {
    599 					if (currentFrame == numFrames - 1) {
    600 						pingPongDirection = -1;
    601 					}
    602 				}
    603 				else {
    604 					if (currentFrame == 0) {
    605 						pingPongDirection = 1;
    606 					}
    607 				}
    608 			}
    609 
    610 			float shuf = params[SHUFFLE_SEED].getValue();
    611 			if (shuf != lastShuffle) {
    612 				setFrameShuffle();
    613 			}
    614 			lastShuffle = shuf;
    615 		}
    616 	}
    617 	void checkAndPerformEndAction(bool forceEndAction = false) {
    618 		if (currentFrame == 0 || forceEndAction) {
    619 			int eb = params[NEXT_FILE_BEHAVIOR].getValue();
    620 
    621 			if (eb == 0) {
    622 				nextFileInCatalog();
    623 			}
    624 			else if (eb == 1) {
    625 				prevFileInCatalog();
    626 			}
    627 			else if (eb == 2 ) {
    628 				loadRandomGif();
    629 			}
    630 		}
    631 	}
    632 	void setCurrentFrameDelayFromTable() {
    633 		if (ready) {
    634 			setFrameDelay(frameDelays[mappedFrame]);
    635 		}
    636 	}
    637 	void nextFrame() {
    638 		goToFrame(currentFrame + 1);
    639 
    640 	}
    641 	void prevFrame() {
    642 		goToFrame(currentFrame - 1);
    643 	}
    644 	void goToFrame(int frameNum) {
    645 		if (numFrames && ready && frameNum != currentFrame) {
    646 
    647 			int animationMode = params[ANIMATION_MODE].getValue();
    648 
    649 			sampleCounter = 0;
    650 
    651 			unsigned int numScriptFrames = frameScripts[animationMode].size();
    652 
    653 			currentFrame = frameNum + numScriptFrames * 10;
    654 			currentFrame %= numScriptFrames;
    655 			mappedFrame = currentFrame;
    656 
    657 			mappedFrame = frameScripts[animationMode][mappedFrame];
    658 			mappedFrame  += mapBlankFrameOffset(zeroOffset, numFrames) + 10 * numFrames;
    659 			mappedFrame %= numFrames;
    660 
    661 
    662 			setCurrentFrameDelayFromTable();
    663 		}
    664 	}
    665 	void goToRandomFrame() {
    666 		int randFrame = (int) std::floor(random::uniform() * numFrames);
    667 		goToFrame(randFrame);
    668 	}
    669 	void scanToPosition(float scanVoltage) {
    670 		/* 0v = frame 0
    671 			10.0v = frame n-1
    672 
    673 		*/
    674 		if (ready && numFrames > 1) {
    675 			int frameNum;
    676 			float vu = (scanVoltage) / 10.01f;
    677 			if (params[CONSTANT_FRAME_DELAY].getValue()) {
    678 				frameNum = floor((vu) * numFrames);
    679 			}
    680 			else {
    681 				//frameMapForScan
    682 				frameNum = frameMapForScan[floor(vu * frameMapForScan.size())];
    683 			}
    684 			goToFrame(frameNum);
    685 		}
    686 	}
    687 	void toggleAnimationEnabled() {
    688 		float current = params[ANIMATION_ENABLED].getValue();
    689 		if (current == 1.0) {
    690 			params[ANIMATION_ENABLED].setValue(0);
    691 		}
    692 		else {
    693 			params[ANIMATION_ENABLED].setValue(1);
    694 		}
    695 	}
    696 	json_t *dataToJson() override {
    697 		json_t *rootJ = json_object();
    698 		if (paths.size() > 0) {
    699 			json_object_set_new(rootJ, "path", json_string(paths[0].c_str()));
    700 		}
    701 
    702 		json_object_set_new(rootJ, "width", json_real(width));
    703 		json_object_set_new(rootJ, "imageFitEnum", json_integer(imageFitEnum));
    704 		json_object_set_new(rootJ, "invertY", json_boolean(invertY));
    705 		json_object_set_new(rootJ, "zoomX", json_real(zoomX));
    706 		json_object_set_new(rootJ, "zoomY", json_real(zoomY));
    707 		json_object_set_new(rootJ, "xOffset", json_real(xOffset));
    708 		json_object_set_new(rootJ, "yOffset", json_real(yOffset));
    709 		json_object_set_new(rootJ, "rotation", json_integer(rotation));
    710 		return rootJ;
    711 	}
    712 
    713 	void dataFromJson(json_t *rootJ) override {
    714 
    715 
    716 
    717 		json_t *pathJ = json_object_get(rootJ, "path");
    718 		if (pathJ) {
    719 			setPath(json_string_value(pathJ));
    720 		}
    721 
    722 		json_t *widthJ = json_object_get(rootJ, "width");
    723 		if (widthJ)
    724 			width = json_number_value(widthJ);
    725 		json_t *imageFitEnumJ = json_object_get(rootJ, "imageFitEnum");
    726 		if (imageFitEnumJ) { imageFitEnum = json_integer_value(imageFitEnumJ); }
    727 
    728 		json_t *invertYJ = json_object_get(rootJ, "invertY");
    729 		if (invertYJ) { invertY = json_is_true(invertYJ); }
    730 		json_t *zoomXJ = json_object_get(rootJ, "zoomX");
    731 		if (zoomXJ) {
    732 			zoomX = json_number_value(zoomXJ);
    733 		}
    734 		json_t *zoomYJ = json_object_get(rootJ, "zoomY");
    735 		if (zoomYJ) {
    736 			zoomY = json_number_value(zoomYJ);
    737 		}
    738 		json_t *xOffsetJ = json_object_get(rootJ, "xOffset");
    739 		if (xOffsetJ) {
    740 			xOffset = json_number_value(xOffsetJ);
    741 		}
    742 		json_t *yOffsetJ = json_object_get(rootJ, "yOffset");
    743 		if (yOffsetJ) {
    744 			yOffset = json_number_value(yOffsetJ);
    745 		}
    746 		json_t *rotationJ = json_object_get(rootJ, "rotation");
    747 		if (rotationJ) { rotation = json_integer_value(rotationJ); }
    748 		this->loading = false;
    749 	}
    750 
    751 };
    752 struct LoadImageItem : MenuItem {
    753 	ComputerscareBlank* blankModule;
    754 	void onAction(const event::Action& e) override {
    755 		blankModule->loadImageDialog();
    756 	}
    757 };
    758 struct ImageFitModeItem : MenuItem {
    759 	ComputerscareBlank *blank;
    760 	int imageFitEnum;
    761 	void onAction(const event::Action &e) override {
    762 		blank->imageFitEnum = imageFitEnum;
    763 	}
    764 	void step() override {
    765 		rightText = CHECKMARK(blank->imageFitEnum == imageFitEnum);
    766 		MenuItem::step();
    767 	}
    768 };
    769 struct InvertYMenuItem: MenuItem {
    770 	ComputerscareBlank *blank;
    771 	InvertYMenuItem() {
    772 
    773 	}
    774 	void onAction(const event::Action &e) override {
    775 		blank->invertY = !blank->invertY;
    776 	}
    777 	void step() override {
    778 		rightText = blank->invertY ? "✔" : "";
    779 		MenuItem::step();
    780 	}
    781 };
    782 struct ssmi : MenuItem
    783 {
    784 	int mySetVal = 1;
    785 	ParamQuantity *myParamQuantity;
    786 	ssmi(int i, ParamQuantity* pq)
    787 	{
    788 		mySetVal = i;
    789 		myParamQuantity = pq;
    790 	}
    791 
    792 	void onAction(const event::Action &e) override
    793 	{
    794 		myParamQuantity->setValue(mySetVal);
    795 		//pouter->setAll(mySetVal);
    796 	}
    797 	void step() override {
    798 		rightText = myParamQuantity->getValue() == mySetVal ? "✔" : "";
    799 		MenuItem::step();
    800 	}
    801 };
    802 struct ParamSelectMenu : MenuItem {
    803 	ParamQuantity* param;
    804 	std::vector<std::string> options;
    805 
    806 	Menu *createChildMenu() override {
    807 		Menu *menu = new Menu;
    808 		for (unsigned int i = 0; i < options.size(); i++) {
    809 			ssmi *menuItem = new ssmi(i, param);
    810 			menuItem->text = options[i];
    811 			//menuItem->pouter = pouter;
    812 			menu->addChild(menuItem);
    813 		}
    814 		return menu;
    815 	}
    816 	void step() override {
    817 		rightText = "(" + options[param->getValue()] + ") " + RIGHT_ARROW;
    818 		MenuItem::step();
    819 	}
    820 };
    821 struct KeyboardControlChildMenu : MenuItem {
    822 	ComputerscareBlank *blank;
    823 
    824 	Menu *createChildMenu() override {
    825 		Menu *menu = new Menu;
    826 		menu->addChild(construct<MenuLabel>(&MenuLabel::text, "A,S,D,F: Translate image position"));
    827 		menu->addChild(construct<MenuLabel>(&MenuLabel::text, "Z,X: Zoom in/out"));
    828 		menu->addChild(construct<MenuLabel>(&MenuLabel::text, "J,L: Previous / next frame"));
    829 		menu->addChild(construct<MenuLabel>(&MenuLabel::text, "K: Go to first frame"));
    830 
    831 		menu->addChild(construct<MenuLabel>(&MenuLabel::text, "I: Go to random frame"));
    832 
    833 		menu->addChild(construct<MenuLabel>(&MenuLabel::text, "[  (left square bracket): Load previous image from same directory"));
    834 		menu->addChild(construct<MenuLabel>(&MenuLabel::text, "]  (right square bracket): Load next image from same directory"));
    835 		menu->addChild(construct<MenuLabel>(&MenuLabel::text, "O: Load random image from same directory"));
    836 
    837 		menu->addChild(construct<MenuLabel>(&MenuLabel::text, "P: Toggle animation on/off"));
    838 
    839 
    840 		InvertYMenuItem *invertYMenuItem = new InvertYMenuItem();
    841 		invertYMenuItem->text = "Invert Y-Axis";
    842 		invertYMenuItem->blank = blank;
    843 		menu->addChild(invertYMenuItem);
    844 
    845 		return menu;
    846 	}
    847 
    848 };
    849 
    850 template <class TBase>
    851 struct tPNGDisplay : TBase {
    852 	ComputerscareBlank *blankModule;
    853 
    854 	int imgWidth, imgHeight;
    855 	float imgRatio, widgetRatio;
    856 	int lastEnum = -1;
    857 	std::string path = "empty";
    858 	int img = 0;
    859 	int currentFrame = -1;
    860 	bool missingOrBroken = false;
    861 	AnimatedGifBuddy gifBuddy;
    862 
    863 	bool lightWidgetMode = false;
    864 
    865 	tPNGDisplay() {
    866 	}
    867 
    868 
    869 	void resetZooms() {
    870 		if (blankModule->imageFitEnum == 0) {
    871 			blankModule->zoomX = blankModule->width / imgWidth;
    872 			blankModule->zoomY = blankModule->height / imgHeight;
    873 			blankModule->xOffset = 0;
    874 			blankModule->yOffset = 0;
    875 
    876 		} else if (blankModule->imageFitEnum == 1) { // fit width
    877 			blankModule->zoomX = blankModule->width / imgWidth;
    878 			blankModule->zoomY = blankModule->zoomX;
    879 			blankModule->xOffset = 0;
    880 			blankModule->yOffset = 0;
    881 
    882 		} else if (blankModule->imageFitEnum == 2) { //fit height
    883 			blankModule->zoomY = blankModule->height / imgHeight;
    884 			blankModule->zoomX = blankModule->zoomY;
    885 			blankModule->xOffset = 0;
    886 			blankModule->yOffset = 0;
    887 		}
    888 		else if (blankModule->imageFitEnum == 3) {
    889 
    890 		}
    891 	}
    892 
    893 	void setOffsets() {
    894 	}
    895 	void drawLayer(const BGPanel::DrawArgs& args, int layer) override {
    896 		if (layer == 1 && lightWidgetMode) {
    897 			drawImage(args);
    898 		}
    899 		Widget::drawLayer(args, layer);
    900 	}
    901 	void draw(const rack::Widget::DrawArgs &args) override {
    902 		if (!lightWidgetMode) {
    903 			drawImage(args);
    904 		}
    905 	}
    906 
    907 	void drawImage(const BGPanel::DrawArgs& args) {
    908 		if (blankModule && blankModule->loadedJSON) {
    909 			std::string modulePath = blankModule->getPath();
    910 			if (path != modulePath) {
    911 				gifBuddy = AnimatedGifBuddy(args.vg, modulePath.c_str());
    912 
    913 				if (gifBuddy.getImageStatus() == 3) {
    914 					std::string badGifPath = asset::plugin(pluginInstance, "res/broken-file.gif");
    915 					gifBuddy = AnimatedGifBuddy(args.vg, badGifPath.c_str());
    916 					missingOrBroken = true;
    917 				}
    918 				else {
    919 					missingOrBroken = false;
    920 				}
    921 				img = gifBuddy.getHandle();
    922 				int numImageFrames = gifBuddy.getFrameCount();
    923 
    924 				blankModule->setFrameCount(numImageFrames);
    925 
    926 				//if this check isnt performed, windows crashes with non-gifs due to
    927 				//the call to vector insert
    928 				if (numImageFrames > 1) {
    929 					blankModule->setFrameCount(gifBuddy.getFrameCount());
    930 					blankModule->setFrameDelays(gifBuddy.getAllFrameDelaysSeconds());
    931 					blankModule->setTotalGifDuration(gifBuddy.getTotalGifDuration());
    932 					blankModule->setTotalGifDurationIfInPingPongMode(gifBuddy.getPingPongGifDuration());
    933 					blankModule->setFrameDelay(gifBuddy.getSecondsDelay(0));
    934 				}
    935 
    936 				int imageStatus = gifBuddy.getImageStatus();
    937 				blankModule->setImageStatus(imageStatus);
    938 
    939 				//couldnt load file
    940 				blankModule->setContainingDirectory();
    941 				blankModule->setReady(true);
    942 
    943 
    944 				nvgImageSize(args.vg, img, &imgWidth, &imgHeight);
    945 				imgRatio = ((float)imgWidth / (float)imgHeight);
    946 
    947 				/*
    948 					1) user selects image from dialog: reset zooms
    949 					2) change came from slideshow: reset zooms
    950 
    951 					3) loaded from JSON dont reset zooms
    952 				*/
    953 
    954 				if (blankModule->jsonFlag && !missingOrBroken) {
    955 					//dont want to reset zooms if loading from json
    956 					//unsure of another way to distinguish (1) from (3)
    957 					//other than this janky flag
    958 					blankModule->jsonFlag = false;
    959 				} else {
    960 					resetZooms();
    961 				}
    962 
    963 				path = modulePath;
    964 
    965 			}
    966 
    967 			if (blankModule->imageFitEnum != lastEnum && lastEnum != -1) {
    968 				lastEnum = blankModule->imageFitEnum;
    969 				resetZooms();
    970 			}
    971 			lastEnum = blankModule->imageFitEnum;
    972 			if (!path.empty() && path != "empty") {
    973 				nvgBeginPath(args.vg);
    974 				NVGpaint imgPaint;
    975 				nvgScale(args.vg, blankModule->zoomX, blankModule->zoomY);
    976 				imgPaint = nvgImagePattern(args.vg, blankModule->xOffset, blankModule->yOffset, imgWidth, imgHeight, 0, img, 1.0f);
    977 				nvgRect(args.vg, blankModule->xOffset, blankModule->yOffset, imgWidth, imgHeight);
    978 				nvgFillPaint(args.vg, imgPaint);
    979 				nvgFill(args.vg);
    980 				nvgClosePath(args.vg);
    981 			}
    982 			if (!blankModule->pauseAnimation) {
    983 				gifBuddy.displayGifFrame(args.vg, currentFrame);
    984 			}
    985 		}
    986 	}
    987 	void step() override {
    988 		if (blankModule && blankModule->loadedJSON) {
    989 			if (blankModule->mappedFrame != currentFrame) {
    990 				currentFrame = blankModule->mappedFrame;
    991 			}
    992 			if (blankModule->scrubbing) {
    993 				currentFrame = blankModule->scrubFrame;
    994 			}
    995 		}
    996 		TBase::step();
    997 	}
    998 };
    999 
   1000 
   1001 typedef tPNGDisplay<TransparentWidget> PNGDisplayTransparentWidget;
   1002 
   1003 struct PNGDisplay : Widget {
   1004 	int counter = 0;
   1005 	bool lightWidgetMode = false;
   1006 	PNGDisplay(ComputerscareBlank *blankModule) {
   1007 		module = blankModule;
   1008 
   1009 		pngTransparent = new PNGDisplayTransparentWidget();
   1010 		pngTransparent->blankModule = blankModule;
   1011 
   1012 		addChild(pngTransparent);
   1013 		Widget();
   1014 	}
   1015 	void resetZooms() {
   1016 		pngTransparent->resetZooms();
   1017 	}
   1018 	void step() override {
   1019 		if (module) {
   1020 			bool moduleLightWidgetMode = module->getLightWidgetMode();
   1021 			if (moduleLightWidgetMode != lightWidgetMode) {
   1022 				lightWidgetMode = moduleLightWidgetMode;
   1023 				pngTransparent->lightWidgetMode = lightWidgetMode;
   1024 			}
   1025 		}
   1026 		Widget::step();
   1027 	}
   1028 	PNGDisplayTransparentWidget *pngTransparent;
   1029 	ComputerscareBlank *module;
   1030 };
   1031 
   1032 
   1033 struct GiantFrameDisplay : TransparentWidget {
   1034 	ComputerscareBlank *module;
   1035 	SmallLetterDisplay *description;
   1036 	SmallLetterDisplay *frameDisplay;
   1037 	GiantFrameDisplay() {
   1038 		box.size = Vec(200, 380);
   1039 
   1040 		description = new SmallLetterDisplay();
   1041 		description->value = "Frame Zero, for EOC output, reset input, and sync mode";
   1042 		description->fontSize = 24;
   1043 		description->breakRowWidth = 200.f;
   1044 		description->box.pos.y = box.size.y - 130;
   1045 
   1046 
   1047 		frameDisplay = new SmallLetterDisplay();
   1048 		frameDisplay->fontSize = 90;
   1049 		frameDisplay->box.size = Vec(300, 120);
   1050 		frameDisplay->textOffset = Vec(0, 50);
   1051 		frameDisplay->box.pos.y = box.size.y - 200;
   1052 		frameDisplay->breakRowWidth = 200.f;
   1053 		frameDisplay->baseColor = nvgRGBAf(0.8, 0.8, 0.8, 0.7);
   1054 
   1055 
   1056 
   1057 		addChild(frameDisplay);
   1058 		addChild(description);
   1059 
   1060 		TransparentWidget();
   1061 	}
   1062 	void step() {
   1063 		if (module) {
   1064 			visible = module->scrubbing;
   1065 			frameDisplay->value = string::f("%i / %i", module->scrubFrame + 1, module->numFrames);
   1066 		}
   1067 		else {
   1068 			visible = false;
   1069 		}
   1070 		TransparentWidget::step();
   1071 	}
   1072 	void draw(const DrawArgs &args) {
   1073 		if (module) {
   1074 			TransparentWidget::draw(args);
   1075 		}
   1076 	}
   1077 };
   1078 
   1079 struct ComputerscareBlankWidget : ModuleWidget {
   1080 	ComputerscareBlankWidget(ComputerscareBlank *blankModule) {
   1081 
   1082 		setModule(blankModule);
   1083 		if (blankModule) {
   1084 			this->blankModule = blankModule;
   1085 			box.size = Vec(blankModule->width, blankModule->height);
   1086 		} else {
   1087 			box.size = Vec(8 * 15, 380);
   1088 		}
   1089 		{
   1090 			BGPanel *bgPanel = new BGPanel(nvgRGB(0xE0, 0xE0, 0xD9));
   1091 			bgPanel->box.size = box.size;
   1092 			this->bgPanel = bgPanel;
   1093 			addChild(bgPanel);
   1094 			ComputerscareSVGPanel *panel = new ComputerscareSVGPanel();
   1095 			panel->box.size = box.size;
   1096 			panel->setBackground(APP->window->loadSvg(asset::plugin(pluginInstance, "res/ComputerscareCustomBlankPanel.svg")));
   1097 			this->panel = panel;
   1098 			addChild(panel);
   1099 
   1100 		}
   1101 
   1102 		if (blankModule) {
   1103 			{
   1104 				PNGDisplay *pngDisplay = new PNGDisplay(blankModule);
   1105 				pngDisplay->box.pos = Vec(0, 0);
   1106 
   1107 				pngDisplay->box.size = Vec( blankModule->width , blankModule->height );
   1108 
   1109 				this->pngDisplay = pngDisplay;
   1110 				addChild(pngDisplay);
   1111 			}
   1112 		}
   1113 		ComputerscareResizeHandle *leftHandle = new ComputerscareResizeHandle();
   1114 
   1115 		ComputerscareResizeHandle *rightHandle = new ComputerscareResizeHandle();
   1116 		rightHandle->right = true;
   1117 		this->rightHandle = rightHandle;
   1118 		addChild(leftHandle);
   1119 		addChild(rightHandle);
   1120 
   1121 		frameDisplay = new GiantFrameDisplay();
   1122 		frameDisplay->module = blankModule;
   1123 		addChild(frameDisplay);
   1124 
   1125 	}
   1126 
   1127 	void appendContextMenu(Menu* menu) override {
   1128 		ComputerscareBlank* blank = dynamic_cast<ComputerscareBlank*>(this->blankModule);
   1129 
   1130 
   1131 		modeMenu = new ParamSelectMenu();
   1132 		modeMenu->text = "Animation Mode";
   1133 		modeMenu->rightText = RIGHT_ARROW;
   1134 		modeMenu->param = blankModule->paramQuantities[ComputerscareBlank::ANIMATION_MODE];
   1135 		modeMenu->options = blankModule->animationModeDescriptions;
   1136 
   1137 		endMenu = new ParamSelectMenu();
   1138 		endMenu->text = "Slideshow / Next File Behavior";
   1139 		endMenu->rightText = RIGHT_ARROW;
   1140 		endMenu->param = blankModule->paramQuantities[ComputerscareBlank::NEXT_FILE_BEHAVIOR];
   1141 		endMenu->options = blankModule->nextFileDescriptions;
   1142 
   1143 		menu->addChild(new MenuEntry);
   1144 
   1145 
   1146 		menu->addChild(modeMenu);
   1147 		menu->addChild(endMenu);
   1148 
   1149 		KeyboardControlChildMenu *kbMenu = new KeyboardControlChildMenu();
   1150 		kbMenu->text = "Keyboard Controls";
   1151 		kbMenu->rightText = RIGHT_ARROW;
   1152 		kbMenu->blank = blank;
   1153 		menu->addChild(kbMenu);
   1154 
   1155 
   1156 		menu->addChild(construct<MenuLabel>(&MenuLabel::text, ""));
   1157 		LoadImageItem* loadImageItem = createMenuItem<LoadImageItem>("Load image (PNG, JPEG, BMP, GIF)");
   1158 		loadImageItem->blankModule = blank;
   1159 		menu->addChild(loadImageItem);
   1160 
   1161 		menu->addChild(construct<MenuLabel>(&MenuLabel::text, "Current Image Path:"));
   1162 		menu->addChild(construct<MenuLabel>(&MenuLabel::text, blank->getPath()));
   1163 
   1164 		menu->addChild(construct<MenuLabel>(&MenuLabel::text, ""));
   1165 
   1166 		menu->addChild(construct<MenuLabel>(&MenuLabel::text, "Image Scaling"));
   1167 		menu->addChild(construct<ImageFitModeItem>(&MenuItem::text, "Fit Both (stretch both directions)", &ImageFitModeItem::blank, blank, &ImageFitModeItem::imageFitEnum, 0));
   1168 		menu->addChild(construct<ImageFitModeItem>(&MenuItem::text, "Fit Width", &ImageFitModeItem::blank, blank, &ImageFitModeItem::imageFitEnum, 1));
   1169 		menu->addChild(construct<ImageFitModeItem>(&MenuItem::text, "Fit Height", &ImageFitModeItem::blank, blank, &ImageFitModeItem::imageFitEnum, 2));
   1170 		menu->addChild(construct<ImageFitModeItem>(&MenuItem::text, "Free", &ImageFitModeItem::blank, blank, &ImageFitModeItem::imageFitEnum, 3));
   1171 
   1172 		menu->addChild(construct<MenuLabel>(&MenuLabel::text, ""));
   1173 
   1174 		MenuToggle* animEnabled = new MenuToggle(blank->paramQuantities[ComputerscareBlank::ANIMATION_ENABLED]);
   1175 		menu->addChild(animEnabled);
   1176 
   1177 		MenuToggle* constantDelay = new MenuToggle(blank->paramQuantities[ComputerscareBlank::CONSTANT_FRAME_DELAY]);
   1178 		menu->addChild(constantDelay);
   1179 
   1180 		MenuToggle* slideshowEnabled = new MenuToggle(blank->paramQuantities[ComputerscareBlank::SLIDESHOW_ACTIVE]);
   1181 		menu->addChild(slideshowEnabled);
   1182 
   1183 		MenuToggle* lightWidgetMode = new MenuToggle(blank->paramQuantities[ComputerscareBlank::LIGHT_WIDGET_MODE]);
   1184 		menu->addChild(lightWidgetMode);
   1185 
   1186 		menu->addChild(construct<MenuLabel>(&MenuLabel::text, ""));
   1187 
   1188 		MenuParam* speedParam = new MenuParam(blank->paramQuantities[ComputerscareBlank::ANIMATION_SPEED], 2);
   1189 		menu->addChild(speedParam);
   1190 
   1191 		MenuParam* shuffleParam = new MenuParam(blank->paramQuantities[ComputerscareBlank::SHUFFLE_SEED], 2);
   1192 		menu->addChild(shuffleParam);
   1193 
   1194 		MenuParam* slideshowSpeedParam = new MenuParam(blank->paramQuantities[ComputerscareBlank::SLIDESHOW_TIME], 2);
   1195 		menu->addChild(slideshowSpeedParam);
   1196 
   1197 
   1198 	}
   1199 	void step() override {
   1200 		if (module) {
   1201 			if (blankModule && !blankModule->loadedJSON) {
   1202 				box.size.x = blankModule->width;
   1203 				panel->box.size.x = blankModule->width;
   1204 				bgPanel->box.size.x = blankModule->width;
   1205 				panel->box.pos.x = blankModule->width / 2 - 60.f;
   1206 				pngDisplay->box.size.x = blankModule->width;
   1207 
   1208 				rightHandle->box.pos.x = blankModule->width - rightHandle->box.size.x;
   1209 				blankModule->loadedJSON = true;
   1210 				blankModule->jsonFlag = true;
   1211 
   1212 			}
   1213 			else {
   1214 				if (box.size.x != blankModule->width) {
   1215 					blankModule->width = box.size.x;
   1216 					panel->box.pos.x = box.size.x / 2 - 60.f;
   1217 					pngDisplay->box.size.x = box.size.x;
   1218 
   1219 					bgPanel->box.size.x = box.size.x;
   1220 					rightHandle->box.pos.x = box.size.x - rightHandle->box.size.x;
   1221 					pngDisplay->resetZooms();
   1222 				}
   1223 				panel->visible = blankModule->path.empty();
   1224 			}
   1225 			ModuleWidget::step();
   1226 		}
   1227 	};
   1228 	void onHoverKey(const event::HoverKey& e) override {
   1229 		float dZoom = 0.05;
   1230 		float dPosition = 10.f;
   1231 		if (e.isConsumed())
   1232 			return;
   1233 
   1234 		// Scroll with arrow keys
   1235 		/*float arrowSpeed = 30.0;
   1236 		if ((e.mods & RACK_MOD_MASK) == (RACK_MOD_CTRL | GLFW_MOD_SHIFT))
   1237 			arrowSpeed /= 16.0;
   1238 		else if ((e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL)
   1239 			arrowSpeed *= 4.0;
   1240 		else if ((e.mods & RACK_MOD_MASK) == GLFW_MOD_SHIFT)
   1241 			arrowSpeed /= 4.0;*/
   1242 		//duplicate is ctrl-d, ignore keys if mods are pressed so duplication doesnt translate the image
   1243 		if (e.action == RACK_HELD && !e.mods ) {
   1244 			if (e.keyName == "a") {
   1245 				blankModule->xOffset += dPosition / blankModule->zoomX;
   1246 				e.consume(this);
   1247 			} else if (e.keyName == "s") {
   1248 				blankModule->yOffset -= (blankModule->invertY ? dPosition : -dPosition) / blankModule->zoomY;
   1249 				e.consume(this);
   1250 			} else if (e.keyName == "d") {
   1251 				blankModule->xOffset -= dPosition / blankModule->zoomX;
   1252 				e.consume(this);
   1253 			} else if (e.keyName == "w") {
   1254 				blankModule->yOffset += (blankModule->invertY ? dPosition : -dPosition) / blankModule->zoomY;
   1255 				e.consume(this);
   1256 			} else if (e.keyName == "z") {
   1257 				blankModule->zoomX *= (1 + dZoom);
   1258 				blankModule->zoomY *= (1 + dZoom);
   1259 				e.consume(this);
   1260 			} else if (e.keyName == "x") {
   1261 				blankModule->zoomX *= (1 - dZoom);
   1262 				blankModule->zoomY *= (1 - dZoom);
   1263 				e.consume(this);
   1264 			} else if (e.keyName == "q") {
   1265 				blankModule->rotation += 1;
   1266 				blankModule->rotation %= 4;
   1267 				e.consume(this);
   1268 			} else if (e.keyName == "e") {
   1269 				blankModule->rotation -= 1;
   1270 				blankModule->rotation += 4;
   1271 				blankModule->rotation %= 4;
   1272 				e.consume(this);
   1273 			} else if (e.keyName == "j") {
   1274 				blankModule->prevFrame();
   1275 				e.consume(this);
   1276 			} else if (e.keyName == "l") {
   1277 				blankModule->nextFrame();
   1278 				e.consume(this);
   1279 			}
   1280 
   1281 		}
   1282 		if (e.action == GLFW_RELEASE) {
   1283 			if (e.keyName == "k") {
   1284 				blankModule->goToFrame(0);
   1285 				e.consume(this);
   1286 			} else if (e.keyName == "i") {
   1287 				blankModule->goToRandomFrame();
   1288 				e.consume(this);
   1289 			} else if (e.keyName == "u") {
   1290 				blankModule->goToRandomFrame();
   1291 				e.consume(this);
   1292 			} else if (e.keyName == "p") {
   1293 				blankModule->toggleAnimationEnabled();
   1294 				e.consume(this);
   1295 			} else if (e.keyName == "o") {
   1296 				blankModule->loadRandomGif();
   1297 				e.consume(this);
   1298 			} else if (e.keyName == "[") {
   1299 				blankModule->prevFileInCatalog();
   1300 				e.consume(this);
   1301 			} else if (e.keyName == "]") {
   1302 				blankModule->nextFileInCatalog();
   1303 				e.consume(this);
   1304 			}
   1305 		}
   1306 		ModuleWidget::onHoverKey(e);
   1307 	}
   1308 	ComputerscareBlank *blankModule;
   1309 	PNGDisplay *pngDisplay;
   1310 	ComputerscareSVGPanel *panel;
   1311 	BGPanel *bgPanel;
   1312 	TransparentWidget *display;
   1313 	ComputerscareResizeHandle *leftHandle;
   1314 	ComputerscareResizeHandle *rightHandle;
   1315 	GiantFrameDisplay *frameDisplay;
   1316 	ParamSelectMenu *modeMenu;
   1317 	ParamSelectMenu *endMenu;
   1318 };
   1319 
   1320 
   1321 
   1322 Model *modelComputerscareBlank = createModel<ComputerscareBlank, ComputerscareBlankWidget>("computerscare-blank");