// ==UserScript==
// @name					Panorama 2 Beta
// @namespace				http://dancewiththesky.deviantart.com
// @description				An image viewer with panning, zooming and color adjustment for deviantART. 
// @include					http://www.deviantart.com/deviation/*
// @include					http://www.deviantart.com/view/*
// @include					http://*.deviantart.com/art/*
// @x-pkg-guid				{843910fe-46fc-4f15-a319-aca2bd71b55d}
// @x-pkg-version			1.9.1
// ==/UserScript==

(function(){

var experience = {
	utils : {
		/**
		 *  Thanks to 
		 *  http://adomas.org/javascript-mouse-wheel/
		 *  http://www.ogonek.net/mousewheel/demo.html
		 */	
		getWheelDelta: function(/*MouseEvent*/ e){
			var delta = 0;
			
		    if (e.wheelDelta) { // IE/Opera. 
		        delta = e.wheelDelta/120;
		        //In Opera 9, delta differs in sign as compared to IE.
		        if (window.opera)
		            delta = -delta;
		    } else if (e.detail) { // Mozilla case. 
		            // In Mozilla, sign of delta is different than in IE.
		            // Also, delta is multiple of 3.
		            delta = -e.detail/3;
		    }

		    delta = Math.round(delta); //Safari Round		
		    
        	// Basically, delta is now positive if wheel was scrolled up,
        	// and negative, if wheel was scrolled down.		
			return delta;
		},
		
		
		merge: function(obj1, obj2){
			for(var p1 in obj1){
				for(var p2 in obj2){
					if(!obj1[p2]){
						obj1[p2] = obj2[p2];
					}
				}
			}
			
			return obj1;
		}
		
	}

};

if(undefined == Function.prototype.__bind__){ 
	/**
	 * @param {Object} context the 'this' value to be used.
	 * @return {Function} a function that applies the original
	 * function with 'context' as the thisArg.
	 * Adapted from http://dhtmlkitchen.com/?category=/JavaScript/&date=2008/09/11/&entry=Function-prototype-bind
	 */
	Function.prototype.__bind__ = function(context){
		var fn = this;

		return function() {
			if(arguments.length !== 0) {
				return fn.apply(context, arguments);
			} else {
				return fn.call(context); // faster in Firefox.
			}
		}; 
	};
}

experience.slider = {

	/* A light-weight quick'n'dirty horizontal slider*/
	
	Slider : function(params){
	
		// MEMBER ASSIGNMENT ////////////////////////////////
		
		/* a hack for maintaining an object-oreinted look'n'feel
			even inside event handlers (fixating context)  */
		for(var m in experience.slider){
			if ('Slider' != m){
				this[m] = experience.slider[m].__bind__(this);
			}
		}
		
		// INITIALIZE ////////////////////////////////////////////////		
	
		this.params = experience.utils.merge(params, {
			'Label': '',
			'Value': 0,
			'SliderWidth': 200,
			'SliderHeight': 20,
			'KnobWidth': 13,
		});
		
		if(this.params['onchange']){
			this.onchange = this.params['onchange'].__bind__(this);
		} else {
			this.onchange = function(){};
		}
		
		if(this.params['onstop']){
			this.onstop = this.params['onstop'].__bind__(this);
		} else {
			this.onstop = function(){};
		}				
	
		this.container = document.createElement('div');
		this.label = document.createTextNode(this.params.Label);
		this.container.appendChild(this.label);
		this.knob = document.createElement('div');
		this.container.appendChild(this.knob);
		
		this.container.className = 'experienceSlider';
		this.container.setAttribute('style', 'width: ' + this.params.SliderWidth + 'px; '
			+ 'height: ' + this.params.SliderHeight + 'px');
		this.knob.setAttribute('style', 'width: ' + this.params.KnobWidth + 'px; '
			+ 'height: ' + this.params.SliderHeight + 'px');
		
		this.knob.addEventListener('mousedown', this._handleMouseDown, false);
		document.addEventListener('mousemove', this._handleMouseMove, false);		
		document.addEventListener('mouseup', this._handleMouseUp, false);
				
		this.setValue(this.params['Value']);
	},
	
	getDOMNode: function(){
		return this.container;
	},
	
	setLabel: function(str){
		this.label.nodeValue = str;
	},
	
	getLabel: function(){
		return this.label.nodeValue;
	},
	
	setValue: function(/*float*/ value){
		if (value < 0){
			value = 0;
		} else if (value > 1){
			value = 1;
		} 
		
		var allTheWay = this.params.SliderWidth - this.params.KnobWidth;
		this.knob.style.left = (allTheWay * value) + 'px';	
	},
	
	getValue: function(){
		var currentlyAt = parseFloat(this.knob.style.left);;
		var allTheWay = this.params.SliderWidth - this.params.KnobWidth;
		return (currentlyAt / allTheWay);
	},
	
	_handleMouseDown : function(e){
		if(0 == e.button){ // "left"-click?
            this.isBeingDragged = true;
            this.lastKnobLeft = parseFloat(getComputedStyle(this.knob, null).left);
            this.lastMouseX = e.screenX;
            e.stopPropagation();
            e.preventDefault();
		}		
	},
	
	_handleMouseMove : function(e){
        if (
        	this.isBeingDragged 
        	&& parseFloat(getComputedStyle(this.knob, false).left) >= 0
        	&& (parseFloat(getComputedStyle(this.knob, false).left) + this.params.KnobWidth 
        			<= this.params.SliderWidth)
        ){        	
			var newLeft =  this.lastKnobLeft + (e.screenX - this.lastMouseX);
			
			if (newLeft < 0){
				newLeft = 0;
			} else if (newLeft + this.params.KnobWidth > this.params.SliderWidth){
				 newLeft = this.params.SliderWidth - this.params.KnobWidth;
			}
			
			if(parseFloat(this.knob.style.left) != newLeft){ 
				this.knob.style.left = newLeft + "px";
				this.onchange(this.getValue());	
			}
			
			e.stopPropagation();
			e.preventDefault();
        }
	}, 
	
	_handleMouseUp : function(e){	
		if(this.isBeingDragged){
			this.isBeingDragged = false;	
			this.onstop(this.getValue());
		}	
	},
}

experience.panorama = {
	
	Viewer : function(params){
	
		// MEMBER ASSIGNMENT ////////////////////////////////
		
		/* a hack for maintaining an object-oreinted look'n'feel
			even inside event handlers (fixating context)  */
		for(var m in experience.panorama){
			if ('Viewer' != m){
				this[m] = experience.panorama[m].__bind__(this);
			}
		}
		
		// INITIALIZE ////////////////////////////////////////////////	
		
		this.params = experience.utils.merge(params, {
			'ZoomFactor': 0.05,
			'InnerUpperOffset': 40,
			'MaxZoom': 10 
		});

		
		/* We'll maintain a log of each pixel-manipulating action 
		  in this array along with a copy of the canvas before that action */
		this.history = [];
	
		this.container = document.createElement('div');
		this.container.className = 'panoramaContainer';
		document.body.appendChild(this.container);	
		
		this.canvas = document.createElement('canvas');
		this.canvas.width = params.ImageWidth;
		this.canvas.height = params.ImageHeight; 
		
		if(undefined != params.onready){
			this.onready = params.onready.__bind__(this);
		}
		
		// attach event handlers (condense into a loop?)
		this.container.addEventListener('mousedown', this._handleMouseDown, false);
		this.container.addEventListener('mousemove', this._handleMouseMove, false);		
		this.container.addEventListener('mouseup', this._handleMouseUp, false);
		this.container.addEventListener('mousewheel', this._handleMouseWheel, false);
		this.container.addEventListener('DOMMouseScroll', this._handleMouseWheel, false); // for mozilla			
		
		this.image = new Image();
		this.image.style.position = 'relative';
		this.image.width = this.params.ImageWidth;
		this.image.height = this.params.ImageHeight;
		this.image.alt = 'Loading ...';
		this._positionImageComfortably();		
		this.image.addEventListener('load', this._initCanvas, false);
		this.image.src = params.ImageURL;
		this.container.appendChild(this.image);
		
		var closeIcon = new Image();
		closeIcon.src = this._getResource('discard.png');
		closeIcon.title = closeIcon.alt = 'Close me, unenlightened soul.'
		closeIcon.setAttribute('style', 'position: absolute; right: 10px; top: 10px; cursor: pointer');
		closeIcon.addEventListener('click', (function(){
			this.setVisible(false);
		}).__bind__(this), false);
		this.container.appendChild(closeIcon);
		
		this._buildUIControls();		
	},
	
	// PUBLIC METHODS ////////////////////////////////////////////////////
	
	setVisible: function(isVisible){
		this.container.style.visibility =  isVisible? 'visible' : 'hidden';
	},			
	
	// PRIVATE METHODS ///////////////////////////////////////////////////

	_initCanvas: function(){		
		this.canvas.getContext('2d').drawImage(this.image, 0, 0);
		if(undefined != this.onready){
			this.onready();
		}
	},
	
	
	_buildUIControls : function(){
		var viewer = this;
		var topMargin = 100;
		var leftMargin = 10;
		var iconSize = 25; // 22 + 3
		
		this._buildIcon('Zoom In', topMargin, leftMargin, function(){ viewer._zoom(1); });
		this._buildIcon('Zoom Out', topMargin += iconSize, leftMargin , function(){ viewer._zoom(-1); });
		this._buildIcon('Original Size', topMargin += iconSize, leftMargin, function(){ viewer._zoom(0); });
		
		this._buildIcon('Undo', topMargin += (iconSize * 1.5), leftMargin, this._undo);
		this._buildIcon('Flip Horizontally', topMargin += iconSize, leftMargin, function(){ viewer._process('fliph'); });
		this._buildIcon('Flip Vertically', topMargin += iconSize, leftMargin, function(){ viewer._process('flipv'); });
		
		this._buildIcon('Color Balance', topMargin += iconSize, leftMargin, function(e){ 
			viewer._showDialog('colorbalance', {'Red': 0.5, 'Green': 0.5, 'Blue': 0.5}, e) 
		});
		
		this._buildIcon('Brightness-Contrast', topMargin += iconSize, leftMargin + 2, function(e){ 
			viewer._showDialog('bc', {'Brightness': 0.5, 'Contrast': 0.5}, e) 
		});		

		this._buildIcon('Hue-Saturation-Lightness', topMargin += iconSize, leftMargin, function(e){ 
			viewer._showDialog('hsl', {'Hue': 0.5, 'Saturation': 0.5, 'Lightness': 0.5}, e) 
		});			
		
		
		this.zoomSlider = new experience.slider.Slider({
			'Label': '100%',
			'onchange': function(){
				this.setLabel(Math.round(this.getValue() * viewer.params.MaxZoom * 100) + "%");	
				viewer._resizeToRatio(this.getValue() * viewer.params.MaxZoom);
			}
		});
		
		this.zoomSlider.getDOMNode().className += ' panoramaZoomSlider';
		this.container.appendChild(this.zoomSlider.getDOMNode());
		this.zoomSlider.setValue(1/ this.params.MaxZoom); 
	},
	
	_buildIcon : function(title, top, left, actionHandler){
		var icon =  document.createElement('img');
		icon.src = this._getResource(title.toLowerCase().replace(' ', '-') + '.png');
		icon.title = icon.alt = title;
		icon.addEventListener('click', actionHandler, false);
		icon.setAttribute('style', 
			'position: absolute; top: ' + top + 'px; left: ' + left + 'px; cursor: pointer');
		this.container.appendChild(icon);
	},
	
	
	_showDialog: function(action, sliders, e){
		if(!this.image.complete){
			alert('Can\'t do this until the the image is fully loaded.');
			return;
		}
	
		var viewer = this;
		if(!this.dialogs){
			this.dialogs = {};
		}		
		
		if((this.lastOpenDialog && action == this.lastOpenDialog)){
			this._discard();
			this._closeDialog(action);
			return;
		} else {
			if(!this._checkUnappliedChanges()){
				return;
			}
		}
		
		if (this.dialogs[action]){
			// Reset sliders
			for(var s in sliders){
				this.dialogs[action][s].setValue(sliders[s]);
			}
		
			this.dialogs[action].style.display = 'block';
		} else {
			/* Create the dialog and cache it. A dialog is just a bunch of sliders; and
			   an Apply and Discard icons */
			this.dialogs[action] = document.createElement('div');
			this.dialogs[action].className = 'panoramaDialog';
			this.dialogs[action].setAttribute('style', 'position: absolute; left: ' 
				+ (parseFloat(e.target.style.left) + 30) + 'px; top: ' + (e.target.style.top) + ';');
			for(var s in sliders){
				this.dialogs[action][s] = new experience.slider.Slider({
					'Label': s,
					'Value': sliders[s],
					'onstop': function(){ viewer._preview(action, sliders); }
				});
				this.dialogs[action].appendChild(this.dialogs[action][s].getDOMNode());
			}

			var applyIcon = new Image();
			applyIcon.src = this._getResource('apply.png');
			applyIcon.title = applyIcon.alt = 'Apply';
			applyIcon.setAttribute('style', 'float: right; margin: 5px 0 0 5px; cursor: pointer'); 	
			applyIcon.addEventListener('click', function(e){
				viewer._apply();
				viewer._closeDialog(action);
			}, false);		
			this.dialogs[action].appendChild(applyIcon);
			
			var discardIcon = new Image();
			discardIcon.src = this._getResource('discard.png');
			discardIcon.title = discardIcon.alt = 'Discard';
			discardIcon.setAttribute('style', 'float: right; margin-top: 5px; cursor: pointer');
			discardIcon.addEventListener('click', function(e){
				viewer._discard();
				viewer._closeDialog(action);
			}, false)			 
			this.dialogs[action].appendChild(discardIcon);
			
			this.container.appendChild(this.dialogs[action]);
		}
		
		this.lastOpenDialog = action;
	},
	
	_closeDialog : function(action){
		this.dialogs[action].style.display = 'none';
		this.lastOpenDialog = null;
	},

	_cloneCanvas : function(canvas){
		if(!canvas){
			canvas = this.canvas;
		}
	
		var canvasCopy = unsafeWindow.document.createElement("canvas"); // FIXME
		canvasCopy.width = canvas.width;
		canvasCopy.height = canvas.height;
		canvasCopy.getContext("2d").drawImage(canvas, 0, 0, canvas.width, canvas.height);
		return canvasCopy;
	},
	
	_clearCanvas : function(){
		this.canvas.getContext('2d').clearRect(0, 0, this.canvas.width, this.canvas.height);	
	},
	
	_syncImage : function(){
		// FIXME fails in Chrome because of same-origin policies?
		this.image.src = this.canvas.toDataURL('image/png');
	},
	
	_positionImageComfortably : function(){ // IMHO!
        var containerWidth  = parseFloat(getComputedStyle(this.container, null).width);
        var containerHeight = parseFloat(getComputedStyle(this.container, null).height);

        // Center if it doesn't fill
        
        if (this.image.width > containerWidth){
            this.image.style.left = this.params.InnerUpperOffset + "px";
        } else {
            this.image.style.left =  ((containerWidth / 2) -  (this.image.width / 2)) + "px";
        }
           
        if (this.image.height > containerHeight){
            this.image.style.top = this.params.InnerUpperOffset + "px";
        } else {
            this.image.style.top =  ((containerHeight / 2) -  (this.image.height / 2)) + "px";
        }		
	},

	// Panning Support

	_handleMouseDown : function(e){
		if(
			0 == e.button  && // "left"-click?
			(e.target == this.container || e.target == this.image)
		){

            this.isBeingDragged = true;
            this.lastImageLeft = parseFloat(this.image.style.left);
            this.lastImageTop  = parseFloat(this.image.style.top);
            this.lastMouseX = e.screenX;
            this.lastMouseY = e.screenY;
            e.stopPropagation();
            e.preventDefault();
            this.container.style.cursor = '-moz-grabbing';
		}
		
	},
	
	_handleMouseMove : function(e){
        if (
        	this.isBeingDragged && 
        	(e.target == this.container || e.target == this.image)
        ){
			this.image.style.left =  this.lastImageLeft + (e.screenX - this.lastMouseX) + "px"; 
            this.image.style.top  =  this.lastImageTop  + (e.screenY - this.lastMouseY) + "px"; 
			e.stopPropagation();
			e.preventDefault();
        }
	}, 
	
	_handleMouseUp : function(e){
        this.isBeingDragged = false;
        this.container.style.cursor = '-moz-grab';	
	},	
	
	
	// Mouse wheel zooming support
    _handleMouseWheel: function(e){
        var delta = experience.utils.getWheelDelta(e);
        if (delta){
			this._zoom(delta, e);
			//var currentRatio = this.image.width / this.params.ImageWidth;
			//var sign = (delta >= 0? 1 : -1);
			//this._resizeToRatio(currentRatio + (sign * this.params.ZoomFactor), e);
        }
        
		e.stopPropagation();
    },	
    
    _takeHistorySnapshot : function(action, params){
		this.history[this.history.length] = {
			'action': action,
			'params': params,
			'snapshot': this._cloneCanvas()
		};    	
    },    
	
	
	_undo : function(){
		if(this.lastOpenDialog){
			this._discard();
			this._closeDialog(this.lastOpenDialog);
			return;
		}
	
		if (this.history.length >= 1){
			var lastEntry = this.history.pop();
			this.canvas = lastEntry.snapshot;
			this._syncImage();
		}
	},
	
	/*
	 * mode: 1 to zoom in, -1 to zoom out, 0 to restore original size 
	 * (it also is used to change math sign in relevant calculations)
	 */
	_zoom:  function(mode, mouseEvent){
        var zoomFactor = this.params.ZoomFactor; // from global parameters
        var newWidth, newHeight, newLeft, newTop;
        var currentWidth = this.image.width;
        var currentHeight = this.image.height;
        
        if (0 == mode){ // restore original size
  			this.image.width  = this.params.ImageWidth;
			this.image.height = this.params.ImageHeight;          
            this._positionImageComfortably();
            this.zoomSlider.setValue(1 / this.params.MaxZoom); // TODO extract this stuff to constannts
            this.zoomSlider.setLabel('100%');  
            
        } else {
        
        	if(1 == mode){ // zoom in
		        newWidth = (currentWidth + (currentWidth * zoomFactor));
		        newHeight = (currentHeight + (currentHeight * zoomFactor)); 
        	} else if (-1 == mode){ // zoom out
		        newWidth = (currentWidth - (currentWidth * zoomFactor));
		        newHeight =  (currentHeight - (currentHeight * zoomFactor));        	
        	} else {
        		throw 'Unsupported zoom mode';
        	}
        	
        	var ratio = newWidth/this.params.ImageWidth; 
        	
        	if(ratio < 0){
        		return;
        	}
        	
        	this.zoomSlider.setLabel((Math.round((ratio) * 100) + '%'));
        	this.zoomSlider.setValue(ratio / this.params.MaxZoom);	


			// Redjusting position ...
			
			if(mouseEvent){ // zooming was triggerd by mouse wheel? 
				// try to keep the same mouse position relative to the image 
				var currentTop = parseFloat(this.image.style.top);
				var currentLeft = parseFloat(this.image.style.left)
			
				var sign = mode;
				/* // if the mouse is not on the image, make the image gravitate towards the mouse
				if(this.image != mouseEvent.target){
					sign = (mode == -1? mode : - mode);
				}*/
			
				var newTop  = currentTop  - (sign * (mouseEvent.clientY - currentTop) * zoomFactor); 
				var newLeft = currentLeft - (sign * (mouseEvent.clientX - currentLeft) * zoomFactor); 
			} else { // using icons?
		        // distribute size change 
		        newLeft = (parseFloat(this.image.style.left) - (mode * (currentWidth * zoomFactor) / 2));
		        newTop = (parseFloat(this.image.style.top)   - (mode * (currentHeight * zoomFactor) / 2)); 			
			}
			
			this.image.style.left   = newLeft   + "px";
        	this.image.style.top    = newTop    + "px";	
			this.image.width  = newWidth;
			this.image.height = newHeight;
        }
	},
	
	// TODO conslidate zoom and resize? (tried but too many rounding errors)
	_resizeToRatio: function(ratio){
        var currentWidth = this.image.width;
        var currentHeight = this.image.height;
		var newWidth = this.params.ImageWidth * ratio;
		var newHeight = this.params.ImageHeight * ratio;
		this.image.width  = newWidth;
		this.image.height = newHeight; 		
	
	    // distribute size change
	    newLeft = (parseFloat(this.image.style.left) - ((newWidth - currentWidth) / 2));
    	newTop  = (parseFloat(this.image.style.top)  - ((newHeight - currentHeight) / 2));
        this.image.style.left   = newLeft   + "px";
        this.image.style.top    = newTop    + "px";
	},
	
	
		
	// ACTIONS ////////////////////////////////////////////////////////////
	
	_preview: function(action, sliders){
		if(!this.beforePreviewCanvas){
			this.beforePreviewCanvas = this._cloneCanvas();
		} else {
			this.canvas = this._cloneCanvas(this.beforePreviewCanvas);
		}
	
	
		// Collect all slider values
		var params = {};
		for(var s in sliders){
			params[s]= this.dialogs[action][s].getValue();
		}
		
		this.lastPreviewAction = action;
		this.lastPreviewParams = params;
		
		this['_do_' + action](params);
		this._syncImage();
	},
	
	_apply: function(){
		var newCanvas = this._cloneCanvas();
		this.canvas = this.beforePreviewCanvas;
		this._takeHistorySnapshot(this.lastPreviewAction, this.lastPreviewParams);
		this.canvas = newCanvas;
		this.beforePreviewCanvas = null;
	},
	
	_discard: function(){
		if(this.beforePreviewCanvas){
			this.canvas = this._cloneCanvas(this.beforePreviewCanvas);
			this.beforePreviewCanvas = null;
			this._syncImage();
		}
	},
		
	_checkUnappliedChanges : function(){
		if(this.lastOpenDialog){
			if(this.beforePreviewCanvas){
				// FIXME won't work on IE7?
				if(window.confirm('Some changes are still not applied. Discard them?')){
					this._discard();
					this._closeDialog(this.lastOpenDialog);
				} else {
					return false;
				}
			} else {
				this._closeDialog(this.lastOpenDialog);				
			}
		}
		
		return true;	
	},
	
	_process : function(action, params){				
		if(this.image.complete){
			if(!this._checkUnappliedChanges()){
				return;
			}		
		
			if(this['_do_' + action]){
				if(!params){
					params = {};
				}
				
				this._takeHistorySnapshot(action, params);
				this['_do_' + action](params);
				this._syncImage();	
			} else {
				throw 'Unsupported action';
			}
		} else {
			alert('Can\'t do this until the the image is fully loaded.');
		}
	},


	/*
	 * The following methods were adapted from http://www.pixastic.com/lib/
	 */
	 
	_do_flipv: function(){
		var canvasCopy = this._cloneCanvas(this.canvas);
		var ctx = this.canvas.getContext('2d');	
		
		ctx.save();
		ctx.scale(1,-1);
		ctx.drawImage(this.canvas, 0, -this.canvas.height, this.canvas.width, this.canvas.height)
		ctx.restore();
	},
	
	_do_fliph: function(){
		var canvasCopy = this._cloneCanvas(this.canvas);
		var ctx = this.canvas.getContext('2d');	
	
		ctx.save();
		ctx.scale(-1,1);
		ctx.drawImage(canvasCopy, -this.canvas.width, 0, this.canvas.width, this.canvas.height)
		ctx.restore();					
	},
	
	_do_desaturate: function(){
		var dataObject = this.canvas.getContext('2d').getImageData(0, 0, this.canvas.width, this.canvas.height);
		var p = this.canvas.width * this.canvas.height;
		var pix = p*4, pix1, pix2;	
		
		var data = dataObject.data;
		while (p--){
			data[pix-=4] = data[pix1=pix+1] = data[pix2=pix+2] = (data[pix]+data[pix1]+data[pix2])/3;
		}
		
		this.canvas.getContext("2d").putImageData(dataObject, 0, 0);
	},
	
	_do_colorbalance: function(params){
		var dataObject = this.canvas.getContext('2d').getImageData(0, 0, this.canvas.width, this.canvas.height);
		var data = dataObject.data;	
		
		//  normalize all to -1->1		var red = Math.round(((params.Red * 2) - 1) * 255);
		var green = Math.round(((params.Green * 2) - 1) * 255);
		var blue = Math.round(((params.Blue * 2) - 1) * 255);
		
		var p = this.canvas.width * this.canvas.height;
		var pix = p*4, pix1, pix2;
		var r, g, b;
		
		while (p--) {
			pix -= 4;

			if (red) {
				if ((r = data[pix] + red) < 0 ) 
					data[pix] = 0;
				else if (r > 255 ) 
					data[pix] = 255;
				else
					data[pix] = r;
			}

			if (green) {
				if ((g = data[pix1=pix+1] + green) < 0 ) 
					data[pix1] = 0;
				else if (g > 255 ) 
					data[pix1] = 255;
				else
					data[pix1] = g;
			}

			if (blue) {
				if ((b = data[pix2=pix+2] + blue) < 0 ) 
					data[pix2] = 0;
				else if (b > 255 ) 
					data[pix2] = 255;
				else
					data[pix2] = b;
			}
		}
			
		this.canvas.getContext("2d").putImageData(dataObject, 0, 0);
	},
	
	_do_sepia: function(){
		var dataObject = this.canvas.getContext('2d').getImageData(0, 0, this.canvas.width, this.canvas.height);
		var data = dataObject.data;
		var w = this.canvas.width;
		var h = this.canvas.height;
		var w4 = w*4;
		var y = h;
		
		do {
			var offsetY = (y-1)*w4;
			var x = w;
			do {
				var offset = offsetY + (x-1)*4;
				var or = data[offset];
				var og = data[offset+1];
				var ob = data[offset+2];

				var r = (or * 0.393 + og * 0.769 + ob * 0.189);
				var g = (or * 0.349 + og * 0.686 + ob * 0.168);
				var b = (or * 0.272 + og * 0.534 + ob * 0.131);

				if (r < 0) r = 0; if (r > 255) r = 255;
				if (g < 0) g = 0; if (g > 255) g = 255;
				if (b < 0) b = 0; if (b > 255) b = 255;

				data[offset] = r;
				data[offset+1] = g;
				data[offset+2] = b;
			} while (--x);

		} while (--y);
		
		this.canvas.getContext("2d").putImageData(dataObject, 0, 0);	
	},
	
	_do_bc : function(params){		var dataObject = this.canvas.getContext('2d').getImageData(0, 0, this.canvas.width, this.canvas.height);
		var data = dataObject.data;		

		// normalize to -150 -> 150 
		var brightness = (params.Brightness * 300) - 150;
		// normalize constrast from 0 -> 0.5 to -1 -> 0  and 0.5 -> 1 to 0 -> 3 
		var contrast = (params.Contrast <= 0.5)? (params.Contrast * 2) - 1: (params.Contrast * 6) - 3;
		var legacy = false; // this is a little.. arbitrary

		if (legacy) {
			brightness = Math.min(150, Math.max(-150, brightness));
		} else {
			var brightMul = 1 + Math.min(150, Math.max(-150, brightness)) / 150;
		}
		
		contrast = Math.max(0, contrast + 1);

		var w = this.canvas.width;
		var h = this.canvas.height;
		var p = w * h;
		var pix = p * 4, pix1, pix2;

		var mul, add;
		if (contrast != 1) {
			if (legacy) {
				mul = contrast;
				add = (brightness - 128) * contrast + 128;
			} else {
				mul = brightMul * contrast;
				add = - contrast * 128 + 128;
			}
		} else {  // this if-then is not necessary anymore, is it?
			if (legacy) {
				mul = 1;
				add = brightness;
			} else {
				mul = brightMul;
				add = 0;
			}
		}
		
		var r, g, b;
		while (p--) {
			if ((r = data[pix-=4] * mul + add) > 255 )
				data[pix] = 255;
			else if (r < 0)
				data[pix] = 0;
			else
				data[pix] = r;

			if ((g = data[pix1=pix+1] * mul + add) > 255 ) 
				data[pix1] = 255;
			else if (g < 0)
				data[pix1] = 0;
			else
				data[pix1] = g;

			if ((b = data[pix2=pix+2] * mul + add) > 255 ) 
				data[pix2] = 255;
			else if (b < 0)
				data[pix2] = 0;
			else
				data[pix2] = b;
		}
		
		this.canvas.getContext("2d").putImageData(dataObject, 0, 0);
	},
	
	_do_hsl : function(params){
		var dataObject = this.canvas.getContext('2d').getImageData(0, 0, this.canvas.width, this.canvas.height);
		var data = dataObject.data;	

		// normalize to -180 -> 180
		var hue = (params.Hue * 360) - 180;
		// normalize to -1 -> 1 (well docs say -100 -> 100 but it gets divided on 100)
		var saturation = ((params.Saturation * 2) - 1);
		var lightness =  ((params.Lightness *  2) - 1) ;

		// this seems to give the same result as Photoshop
		if (saturation < 0) {
			var satMul = 1 + saturation;
		} else {
			var satMul = 1 + saturation * 2;
		}

		hue = (hue % 360) / 360;
		var hue6 = hue * 6;
		var rgbDiv = 1 / 255;
		var light255 = lightness * 255;
		var lightp1 = 1 + lightness;
		var lightm1 = 1 - lightness;
		var w = this.canvas.width;
		var h = this.canvas.height;
		var w4 = w * 4;
		var y = h;

		do {
			var offsetY = (y-1)*w4;
			var x = w;
			do {
				var offset = offsetY + (x*4-4);

				var r = data[offset];
				var g = data[offset+1];
				var b = data[offset+2];

				if (hue != 0 || saturation != 0) {
					// ok, here comes rgb to hsl + adjust + hsl to rgb, all in one jumbled mess. 
					// It's not so pretty, but it's been optimized to get somewhat decent performance.
					// The transforms were originally adapted from the ones found in Graphics Gems, but have been heavily modified.
					var vs = r;
					if (g > vs) vs = g;
					if (b > vs) vs = b;
					var ms = r;
					if (g < ms) ms = g;
					if (b < ms) ms = b;
					var vm = (vs-ms);
					var l = (ms+vs)/255 * 0.5;
					if (l > 0) {
						if (vm > 0) {
							if (l <= 0.5) {
								var s = vm / (vs+ms) * satMul;
								if (s > 1) s = 1;
								var v = (l * (1+s));
							} else {
								var s = vm / (510-vs-ms) * satMul;
								if (s > 1) s = 1;
								var v = (l+s - l*s);
							}
							if (r == vs) {
								if (g == ms)
									var h = 5 + ((vs-b)/vm) + hue6;
								else
									var h = 1 - ((vs-g)/vm) + hue6;
							} else if (g == vs) {
								if (b == ms)
									var h = 1 + ((vs-r)/vm) + hue6;
								else
									var h = 3 - ((vs-b)/vm) + hue6;
							} else {
								if (r == ms)
									var h = 3 + ((vs-g)/vm) + hue6;
								else
									var h = 5 - ((vs-r)/vm) + hue6;
							}
							if (h < 0) h+=6;
							if (h >= 6) h-=6;
							var m = (l+l-v);
							var sextant = h>>0;
							switch (sextant) {
								case 0: r = v*255; g = (m+((v-m)*(h-sextant)))*255; b = m*255; break;
								case 1: r = (v-((v-m)*(h-sextant)))*255; g = v*255; b = m*255; break;
								case 2: r = m*255; g = v*255; b = (m+((v-m)*(h-sextant)))*255; break;
								case 3: r = m*255; g = (v-((v-m)*(h-sextant)))*255; b = v*255; break;
								case 4: r = (m+((v-m)*(h-sextant)))*255; g = m*255; b = v*255; break;
								case 5: r = v*255; g = m*255; b = (v-((v-m)*(h-sextant)))*255; break;
							}
						}
					}
				}

				if (lightness < 0) {
					r *= lightp1;
					g *= lightp1;
					b *= lightp1;
				} else if (lightness > 0) {
					r = r * lightm1 + light255;
					g = g * lightm1 + light255;
					b = b * lightm1 + light255;
				}

				if (r < 0) r = 0;
				if (g < 0) g = 0;
				if (b < 0) b = 0;
				if (r > 255) r = 255;
				if (g > 255) g = 255;
				if (b > 255) b = 255;

				data[offset] = r;
				data[offset+1] = g;
				data[offset+2] = b;

			} while (--x);
		} while (--y);

		this.canvas.getContext("2d").putImageData(dataObject, 0, 0);	
	},
	
	_getResource : function(uri){
		// FIXME do a generic implementation
	}
}

	/// CSS ///////////////////////////////////////////////
	
	// python> open('panorama.css', 'rb').read().replace("\n", " ").replace("\r", " ")
	GM_addStyle(" .experienceSlider { \ttext-align: right;   \tborder-bottom: 1px dashed #fcb54d; \tcolor: white;  \ttext-shadow: black 1px 1px; \tfont-size: small; \tmargin-top: 4px; \t \t/* don't change these */ \tpadding: 0; }  .experienceSlider div { /* the knob */ \tbackground-color: #fcb54d;  \tcursor: pointer;  \topacity: 0.7; \t\t\t \t/* don't change these */\t \tposition: relative;  \tleft: 0;  \ttop; 0;  \tmargin: 0;  \tpadding: 0;  \tfloat: left; }  .panoramaContainer {            background-color: black;        /* Don't edit any of the following          unless you know what you're doing .. */      margin: 0;      padding: 0;      border-width: 0;      top: 0;      left: 0;      bottom: 0;      right: 0;      width: 100%;      height: 100%;      overflow: hidden;     background-color: black;     cursor: -moz-grab;  visibility: hidden; z-index: 9999;      position: fixed;  }  .panoramaZoomSlider { \tposition: absolute; \tbottom: 10px; \tright: 10px; \twidth: 150px;\t }  .panoramaDialog { \twidth: 200px; \tborder-right: 1px solid orange;  \tpadding: 5px; } ");


	// USERSPCRIPT-SPECIFIC /////////////////////////////////
	
  	/* This is a workaround for bypassing same-origin restrictions in canvas.
  	   When the image is loaded. It's requested again using GM_xmlhttpRequest()
  	   to obtain it's binary date, then the data is base64-encoded and assigned to 
  	   the SRC attribute. This causes the image onloaded handler to fire again but this time
  	   with a data: URL that won't cause the canvas to throw security errors because of
  	   same-origin restrictions. 
  	   
  	   Canvas is also created from unsafeWindow because getImageData returns null if it's not. */ 	
	
  	experience.panorama._initCanvas = (function(){
  		if('data' == this.image.src.substr(0,4)){			
	 		this.canvas = unsafeWindow.document.createElement('canvas');
			this.canvas.width = this.params.ImageWidth;
			this.canvas.height = this.params.ImageHeight;
			this.canvas.getContext('2d').drawImage(this.image, 0, 0);
			
			if(undefined != this.onready){
				this.onready();
			}    		
  		} else {
	  		//this.image.complete = false;

		 	GM_xmlhttpRequest({
				'method': 'GET',
				'overrideMimeType': 'text/plain; charset=x-user-defined', // why?
				'url': this.params.ImageURL,
				'onload': (function(response){	
					var type = response.responseHeaders.match(/Content-Type: (.*)/i)[1];
					var base64EncodedData = btoa(response.responseText.replace(/[\u0100-\uffff]/g, function(c) {
							return String.fromCharCode(c.charCodeAt(0) & 0xff);
	  				}));
				
					this.image.src = 'data:' + type + ';base64,' + base64EncodedData;
				}).__bind__(this) // onload 
			});
			
		} // if data:
  	});	
	
	experience.panorama._getResource = function(uri){
		return {
			'zoom-in.png':
			 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAASxSURBVDiNhZXtT1NXHMe/59x7e9sLvaUPtiAOLRaID8EqPlNUcBjjkpklW/YPuL2ZS5wZmpm9WTJZWGOyZMuSjVebuvi4qcmWhcYxEeNA59NAIICIUxigBUppS3t7f3tRwEK77CTfnJOT3M/9nt/3d+5lRITFo8Ffv4PAa4mL1YyS5QBATHjAdK2ZQQ8crTvWkvHQosHSwQ3+eisxsdGSZ9vnLvManQ4bc1lzAQAj42GMPg/SQM+92ORE8BdG2jtH646N/y+4wV/vAxcvr/L61Io1HrHjWQRjU0mEZ5JgAHJlAQ6zgLWFCv7s7NO67rWGoGv7j9Yda/1PcIO/3gou9u157S0bF4240TuFfKsCWeIAACIgSYSZhI7nk1FUlpihazE0/Xw+CF3zZHPOU/UTG1d5fSoXjWgbiGBlgQpVkTA0GsKVa9240tKNp/+EIIscrjwFrX1hcNGIVV6fSkxszOZYMOUYdlisjk+qfdvkQGcIK/NVGEQOgygg0NaP4wcqUbOhCKcDD1G63IEkEWRRQPfwNGrWFfLBJ0/cv1399bqvsmpwgWMCr3WXeY0dTyNY5sgBZwAYoBNhOhoHEQACItE4kjqlRASTLKHjaQTuMq+RwGsXOxaJi9VOh409HEnCpsro/TuIOz3DiMYSAKVeoBOgE3Cu6S/IsoTS5UvgtJsxEtKwtsDGiIvVGWBGyXKXNRdtgxMwKzpudw3h+IFKzDWhrgMEwon3dkInAhHwUWMrLKqCyekEXKsdmOv1jPBSyRMiMxqIgPQrQ2kzYwxEQFzTMTwRhZbUs+WWcgwmPBibmK60mET0DoeRZ1Vx6KtrSCQ0cMZw4uAuEAGHvmwGEcEgiVi21AbFIKLIYcTY5DTAhAdZSqE1vwiOb3/FvoQNTyZgdqkoyreAMeD3tl7oesozA7B7W9l8OXQiFNklvAiOESOtObMUpAf6u+/ESlxGuFQJikGAIgvIkUXkmAwQOIPAGXIUw/y+IgtwqhI8TiP6u+7GQHpgMZgRET4/4b+wfvOO/e4VbvHmowg4Y+AMeDYaQmf/CABgrceFZU7LvOMtxSY8GhjQ7t5quXzkcN2bWcEN/norF6S+199422YwmNAzGsdMAuAsFVh6wLLIUOqSEJ+J4aeLpxOdHR0HQ6HQmUs/XgllgAHgs4ZPqyRJvrRx6y51dalbDEZ0ROIM0YQOBsAoceTIgNXE0NU7oP1x4yotXZoviYIUb2oKxMPhqX3nz168ngFmjPFDH7xvLygs/MZuX7K33LvJ6LDlsVzFBAAIR6J4EZyg+/faY0NDz1putbdf37Z968cVFRVGUTDg1KmT0+FweO+5Mxda58EsdV4GQAAgfXjk8E6z2bzbpChVIFoDADpR98T4ePvjx4NtZ344e99mt+W+WlvT5FnpMZktudi8aesCeDqYz4LFWUmL5gxVbNyw3l284osST4mi5pnn4VNTU3uyORayQIQ0zRmQAEibt2xcV7S86Os5uLd8A74/+V2POJs2zaafROr26rNrLQ32sj1SawmAsb3t9k0uCO8C+La42K2MjI4AQMGCfx572Vssi4CFn5E51zIAg69q+xZXvsvPOXcmElrdvwehEBG8PNGfAAAAAElFTkSuQmCC',
			 
		'zoom-out.png':
			'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAARvSURBVDiNhZXfTxxVFMe/98fsDMPuwP4oC6VSF5Y2/SHdlv6gsFSp0jQ1UWPif2B9M6mmiDa+mCiG8OKDMVGetI1prSaaqDEQStrSKLRWSqhAoFJqW0Jplx+7LAs7M8eH3cVlWeNkTs69ydxPPnPOnTuMiJB7tXe0HSHwZuKyiZFVAwDExBCzzV4Gu7u15cyVDYtyLpYNbu9ocxOTnUXFnhOB7SGtxOdhfrcTADAzF8OjxxGaHBtMLMxHfmZknmxtOTP3v+D2jrYwuPxhRyhs1O4KyuEHccxGLcRWLDAATlXA5xLYXa7j99sT5shg3yJs8+XWljN9/wlu72hzg8uJYy++5uFSw7XxKErdOlSFAwCIAIsIK0kbjxeW0VDtgm0m0PXTxQhsM5jPnKfqJzt3hMIGlxr6J+OoKjNg6Ao0RUBVBBTJITmHKjn8xTr6JmLgUsOOUNggJjvzGYuCQseRIrfvg6bwYbX79iKqSg04JIdDCkjBQACICDYBtk2wiKBKgdHpJRzdU86n7t0LXOr55Wq4oXFqnTGBNwe2h7Th+3Fs8RWCMwAMsIlgWgTLTkf2mAgFqoLh+3EEtoc0Am/ONZbEZVOJz8P+nLHgMVSM/x3BzbFpLCeSAAGElC1RylxVFWzbugklXhdmFk3sLvMw4rJpA5iRVeN3O9E/NQ+XbuPGyEN89HoD1jZhGk6Uegsi4L3OPhQZOhaWkvDv9CGz1zc0D+k6xlfMlFnWA5SVGWMgAlZNG9PzyzAtO1/fUsZgYmh2fqmhqEBifDqGYreBU59eRjJpgrNM8zK2BIcisWWzB7pDosKnYXZhCWBiKE8pzN4nkbn6p7yb2PRCEi6/gYrSIjAGcMaA1J0yzyqHTYQKr4InkVliZPZuLAXZ3XdGbyaq/Rr8hgLdIaCrAoWqzMoyZy5QYigIlmi4M/JHAmR354JFd1fPVM+lnr1CM7Y9U+nnM1ELDingkBxqOmdCERxSMCiCo66qEGMTk+bdv0Z/bD39bkfe5pGdPDl4/cqiZSYQDhbC60pZ6qpI53/nXqeC+qAOK7mCm/29NDx0q+uVV18ycsFrh9DH7R82Kor6/f6654yd2wIyErcRX2VYTtpgADSFo1AF3AUMI+OT5m/Xemjz5lJFCmW1q6t7NRaLnrh44burG8CMMX7qrTe9ZeXln3u9m47XhA5oPk8xc+oFAIBYfBlPIvN0a3Ag8fDhgyvXBwauHq6ve7+2tlaTwoFz584uxWKx49+c/7ZvDcwYS3/IEACU0++8/azL5Xq+QNcbQbQLAGyi0fm5uYG7d6f6z3994ZbH63G+0Hy0K1gVLHAVOXHwQN06eDaYp8EyHUpO3hC1+/ftDVQ+/Ul1sFo3il1r8Gg0eiyfscgDEVmREVAAKAcP7d9TsbXisww8VLMPX539ckwCABFRig0r/S3Y6bGZBWPZTU+DtYH+G79yId4A8EVlZUCfeTQDAGXr/nlp88zC3ADWHyMZaxWAI9xYf8hf6u/gnJckk2bLP8ZP9XmWx8YrAAAAAElFTkSuQmCC',
			 
		'original-size.png':
			'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAASgSURBVDiNhZVtbFNVHMafc+657e1d260vtoPppFsnweFWGIy3Dhg4QmYihiif+Ipf0KDEQUL8YmJm5oIxmpgomqgQwwQjJGqkCywZI7ihMMYIEF4GhJeMQTfWrtt67z1/P7Qb21rjSZ6ce+/J/5fnPv9z7mVEhLmjpbV5LYE3EBf1jKwqACCm9DFpdjDI9r1N+zpziuYMNhPc0trsISYOFBZ5G0MLI1rA72VBjxMAMDicxKPHcRq41jvxdCT+ByNzx96mfcP/C25pbY6Ci+OLIlF3TWVY9N9PYShhITlpgQFw2hX4XQoWl+j45/IN80pv1yikuWVv076u/wS3tDZ7wMWNTa+95eVCw5nrCRR7dNhVDgAgAiwiTBoSj5+OY02FC9KcQOz3I3FIM5zPOc/kJw4sikTdXGjoHkihfJ4bbl2FpipoO3EJbbFLOBLrh11wBIt0dN1IggsNiyJRNzFxIJ9jxVFgW1vo8X9UH11lb788ivJiN2yCwyYUfP9bLz57Zz021S7AiZ7bqCwPwCKCXSi4+nAMG6pL+J27d0OnTv55Orqm7s4sxwTeEFoY0frvpfC8vwCcAWDAt8fOY//OdSACQAARwZJZEcFhV9F/L4XQwohG4A05URAX9QG/lw0lLQiFw7QI3x2/gNad6yAlICkDkgQYloRhEUxTQkpgcNREwO9lxEV9DpiRVRX0OJGYsGCYEpOmhW0Nr2DXFx3Y9WUHpMy4JSKkTQnDlEhbEpOGhUejaQQ9Tkzt9ZzmZTpPSE2a06+7rWFx5vnUOgDGGExLYjiZxsORcZiWzNc3AIAAU/qGRsbWFDoErj9MQlU4NBuHpiowTAnDkiAC0qbE4Mg4DJMgiaDbBEr9GoaejgFM6csThdnxJD5ML/hs0O0KNBsHZwyGJWFJgpSZGBgAu1Cg2xUU2AV0u4JSn4on8WFiZHbkRkGy/ebV8xMVQQ1Btwrd9qxY4WyWZkIDbhXhgIabVy5MgGT7XDAjIny6v/Xoktq1W0ILQuLsrRQ4Y+AM4Izh11OXAQBbN1aCKLNLiIAVZQ7cGhgwL5zrPL5nd9ObeZtH0tjRe65z1DInEA0XwOcS0LPOtjdWY3tj9fS9z6lidViHZUzifHcH9fddjL2x9XV3XscA8EnLx3Wqaj+2bOV698svhUQ8JZFKM4wbEgyApnIU2AGPg+HK9QHzrzMnaf78YlUoajoWa08nk4nGI22/nM4BM8b4e++/65tXUvK1z/fc5qrIcs3vLWJO3QEASKbG8SQ+Qhd7eyYePLjfea6n5/Sq1Ss/rKmp0YRiw6FDB8eSyeTmnw8f7ZoGM8ayBxkKAPWDPbvXuVyujQ5drwNRJQBIoqsjw8M9t2/f6T78U9tFr8/rfLVhQyxcHna4Cp2oXb5yFnwmmGfBIit1zpyjmmVLl4TKFnxeEa7Q3UWuaXgikdiUz7GSB6LM0JQBFYBau2JZdemLpV9NwSNVS/HjwR+uCQAgIsqwYSFzemX22pwBYzObngVrPd1/n+WK8jaAb8rKQvrgo0EAmDfrn5d1PlU4V8CzTwdmuLYDsEXrVq8IFgdbOecBwzCb/gWLjR2Ihu0tHwAAAABJRU5ErkJggg==',
			
			
		'reset.png' : 
			'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAABIAAAASABGyWs+AAAACXZwQWcAAAAYAAAAGAB4TKWmAAAE1klEQVRIx+VTa0wUVxg9987MzuzsguDyLgqosUhQiwhWahurNU0aY9KHxgZsalqR2l99pIlJ0/5pmjSamFRbAmJbLWCjTdpqbG1iDIqP6CJIUQRCfSAPAXFZ9j0z997+YDGwskl/9U+/5GTuJN895zvn3gv8b2rz5mPSv+l7tupo7up3js2d+ifxGkt3HF6sKOonIFhnWSyTCygSJX5KyQjj4iwX/DgXgfPXaneaU3tWvduYzqm4ZDGsaztUfi+OgCBlu36qA8jW55+ZZ1uSlyY7HSooJTAME4GQiXsPvKytezAwPBaQCMFXfqrsc0oGhUHdjPPFgvCca7Vv9c0qULbr6Pe5mUlbtr2yTI+YDOP+MBJ0FbqqQKIAoQQQAAiBPxhBU+vd8JUbA4bgGFxTlLOgo3fY9I4H86/UVfQDgDydvLjyyCqHpryxoXSB/s1xNx/1BCmlAGdCCAIrw+UMrSrMdhQ9nSEJAYRMgXUlC7WVBdla7/1HCS+tzCHXu4csg0t8ipNOF9BkpSrRqTrqfmsVEwGT2DUlzDg/dqW2nIYo0geH/RtONff8/MV3zcGrXUPCoUoYGAvhUcBCSUEWUWQKxjhkLTy7AAhe7hvyEldSgmdRbkabYVgegO8BgI7qck/LofKrl6rf3Goa7Lk/L/a4D/7SEky2U8xP1dHUMQJFomCck0g8B4JD1e3qwPzstJo5ulYPQnuYsHliz8l9sPy6YVq7JwKGlGBXMDAWAgegKRSMCaJw+bHAjDNgnAdTnPavE+zaScNQH4oIOZwwL20iVqC4sn6pTMmJqtdXqqnJOjJTHFhbmAZVkWBxQWwsjoA9MyvvBM7xE9s3csSp4sofM4kgpxkT+oHj7ojgAkIAApNfzoXNYIHH+2dc05LKhlECOGcjFgI+QvmGRzZXl9M3bI83gCKgug+/PQIQ8YQDAEn1n2+UbTIFQEAIEDYsfFrbHLk77P3was229mhfZNJNjQIk62WFRWFXgpHc/vf9sv6Rh7UASZk1IghBNJuMsx3DIBAoy09F45lbkb4R3wF3TUV97LQyd6RLKq9W7b5fvWGeOu7377TJMpveE3NNJ6c2LY7sFAcIgPbeUUEJWVqy/ei8J/KQkMgYX+31BSs67/R/7AuEcwnF7RlDzDAAQQiA5XlJ6B8LYsBDsff9F7XGM13rT13s7Srd2bCfM9ECQfqowmTOyV67qiR13R5Ya1Mk4Zqjm57xUF1cgUkTBMGQgYKn5iBiMbTe8eCFovlSaUGW3nJr6KPBh77w4KiPjI0HHZKNIN2lIz8vDSlJDtLw+/WwbhkNcQUoIeTugwns/rZpnBLQjWsW2V4sztE8AQtMAEX5mXIhy3ASMpmtyQUsi2PcH0HDH21+ZpofNP2wPTwzxWnrrOLXPrt8oz/Qd/Pcpu7T+/fdDyQOXuj0LPT6wg5Flmiiw0acdgVCAIxz+IIGWjr7+cnzXRHf2IM9rUeqGqKcAgCLfQda8Y56j3+wc0v3qS9vArBHoWWt2LQ8fcn6rYqevAKUqA5VMYMRSxIC4BHfhZGe5uq+y43tAEJRBKOYIUCWVVS/+lf9e+cAaADUaVCikCSHS5mbXTg36BnyBEZ6PACM6LuIRMl90bWIFYiNTomBHGOfATCjMABYU7H8p/UP2+k2OJhcuQMAAAAldEVYdGNyZWF0ZS1kYXRlADIwMDktMDYtMjBUMTI6MzM6MDkrMDA6MDB/pLsmAAAAJXRFWHRtb2RpZnktZGF0ZQAyMDA5LTA2LTIwVDEyOjMzOjA5KzAwOjAwIBXNEgAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAAASUVORK5CYII=',
			
			
		'flip-horizontally.png':
			'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAABmJLR0QA/wD/AP+gvaeTAAAAB3RJTUUH1wgRFic7aF95jwAAAlRJREFUOI3tlc1LVFEYxn/vnREmHJRaFAWlOUK5KHEQgihnFi7EWgRBm2b+g3TVshbRulb2F0yBBEGgyazCQQsluDJCuEnGGb+whSBNknHveVvM1e7cuVdy34GH+3HO+zsPzznnXvjfvCZRHXaeDMosQPp18zg7h3rV2XSBUli9dRx0oKCRjq69/ArKrJ0n80/gJuj+diQ43n6a/lfrkfAmsB+q259QjXasqsjPKv0TtVD4EbgZOoepFQHFSiSxc6hfViIJKKZWRPbX6J+otsClBbpVwlSLoC5y7gZydhAk1up4ZxHdWQSJYXWNoO2XKT/qOlpQaYJufsSsfQB1IyMIbRLD6r6Dtqcoj3WDkLWOoPUapjIN7gEY52RyDzCVaSQu9D3/DMqshZBdyguSvITVNdJwa36fTOo24nAMK09vgpBtzbhWxKy+a2R8IYOcvwVWSMabJXSr1IghdR/tuEJ5rOdvxqG7ojqD+fYWa/AJy48HML/qTVArkeT6i2XMl2dYvQ/QzquUx1LRJ9HOk7FzqKqqqUyp2VtV7/jG/bJzqNlbVVOZUne3MSZyHwOSLjBfdxheygvSfdc/LhFQI47OPsrjKeoOw+kC8/i+PYfgGNAGtA1NsrBRZ3QpL0hHjx98ylMCwP3xnfJ4Lxt1RocmWTis91iId9OiN6Pc7jvDe4D0ay763KidYx1gZZd7D2eYA9ygxJdd2ATB9wQAThgUcOKAAsZzFJTr63cOHXvPx0njXjG+In+h463D4UTBMerVm8DV9f8ZxIP4JQFo0ECYewX4AxNNbwgY23+PAAAAAElFTkSuQmCC',
		
		
		'flip-vertically.png':
			'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAABmJLR0QA/wD/AP+gvaeTAAAAB3RJTUUH1wgRFic7aF95jwAAAnVJREFUOI2t1M9LFGEcx/H3M5uobaaiZmYKQmsHUXCjJDq45E0PehBEnP0X9FYd+geCRDr4F2xevAldRCo9lOjCLNvSQbGLm+UmS4FsITnPt8OM7s7u7LpRD8xh5vl+XvN9nvmhqGJYUYYR1lFEwjE2qskY1aKDMQFh3YoyXA18IWqZiIiI/MyIiIhlIv+EF6I6Exc7+UJ0Jl41rsqhZ8uXzBbyZQNEgzJQN4ZR7UMkoopKe14Ce9DDTeTgjYOeJwxU50PU9fsVcVUW/foO/fk1iO3TTgDj5giq40FZ3AubyGBMkNzB+TXJbCPf4vnAtbuo9nv582Aniagi/NJrXSpuJhHNzxu1QQYWPiKH7/NQW5gPc7fRJ7nSlZSDi+9qmTlBGaB/F6zRQJ/kSmorwv5DvDBycaQqWIpg+V8wAvrUe/63sGV6U0Zt0Ldjozbo7H/BuPCt6H+2ReBKa95Jr3lgSa8x8NwCw4nauR+kHt0p6dj7d1NEUo+HEFsgm0JvPkHSqw7sHpJeRW89hWwKscVBFZFKcCAcY3v/mLHk7C2ksQ8jNO18zgUwojFC00hjH8nZW+wfMxaOsQ0E/GAF1AA1EyvEE0dMJudCSFM/Rq/pPC99CgJGr4k09ZOcC5E4YnJihfhZFp9/TwC4DDQD7UD3QoQZy0Ts7K7ovWU5fTUmem9Z7OyuWCayEGEG6Hbrm918oBhWQB1wFWgDOoGe+Qimg++I/r4jdnZHLBOZj2ACPW5dm5ur8+v4rOt6t6gV6AC6FkeYskzk16e3YpnI4ghTQJc73+rW1/t1Wzxq3MIGd4ktS6OMWyayNMo40OJeb3DravyQP0M4N+ktkZDlAAAAAElFTkSuQmCC',
			
		'undo.png':
			'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAABHNCSVQICAgIfAhkiAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANlSURBVDiNpZRraBxlFIafM7fMpNvcTAK6aVMhjbWiNlaEZNsi+WEoSH8IVSiarRSKiIWighgKSn9URBsURFCKsMRSSlERvCCtjRE30qDSBk2Naau1iYo0l20uuzuz833+yG7Y1GRd8MDLfHwcHt5555wRrTXlVjIh7cB3sbgO/qvXKB9qPAL0A2Y5/WWBB/vs572qDcdFbKdcIyXByYQY3x5336lq2Hr4nq4TrpgVqlywVQLqmZb7cX3zzu2339/jGqZbLnN1cDIhjablno3etX9j9M5uR3IXwLgPy66SUMKfz52IXA6DzBmtw7djcZ1aiSE3T0UyIXcYpjuwsf1w/S3rdpj454EsmM1oo5mcn8JP/8X0eH92YqQvp3VwVKng5Vh8OWgZOJmQrWKYA5sfPFoZqW0StXAayBW3g6xBjBrE2YTSVfz6/WsLUxPffKbC3J7iMbw5ilEhHL7+26m2Cm+3m019ifIvrZqj4bSw/u5nKmHh4cnxoTeBp0tF4RgGH9bcem/nhi37PH/mGGF2GDuyEyuyC9QUKriGCsbJpQcRWYNT9xzDp1/MhEF6SyyuR2GFcYvFta8Uu6b/vHBybOittF29D8vrwI508VP/EXUx+e78xNiPc/Nz1cpteAkxa8nNfURDtM4Wg4OrOi6uwT551Vu77kBr+0HPcmr44dMDoQoX2oBWMeipu61lc9OmTje4cYq5mWnGL3O+/XHdtqLj4up4Qr+QvnHt0MjXr2T8zN+F67FYXH+gFdum/rg0Ovn7F2g1je2A1jQt5V8KDNDRrXv9hev7L351JK2Uv9Qfi+u0CMO5zFUAshkwhF/KBuchfYE/uxudyxbukgmpRNNh5udqPoWvFP0lwSJiiIglIraIOCJSsW0vZ6ZSPNTbh9X5gHhi8EmkhmhVHaTnYWaSjNa8scQo/ngiYrH4WzSKngUVSp89Rm9NLY+tb8ULsnB1lHnf56nte/X7yxzLYlksLowNOEAF4AJeXpVAZVcHjY5Fd1ML3uwMXBkhPTtHz44nOSkissxx/sIqkrnC2QAUEA68x+eWSavWpK6M82j3IYZY3P1AFyLQWpM/G3mnHhABqoE6oB5oLFJDtJHG15+lDVibfxO7YLKgfy1I3n0h40LOUtSiC87zUnqFLSu5ef+n/gFfRWllKiHS4AAAAABJRU5ErkJggg==',
			
		'color-balance.png':
			'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA3XAAAN1wFCKJt4AAAAB3RJTUUH1wEJERga4RpIaQAABPZJREFUOMu11XdslGUcwPHv+74331u0vTtG6WaVUUgLDSIrcigWB1osEoUIAU0MUUiMJJrKqGI0MYT1h0AQCcOoyCxhyGhZAUQ4RhmldDA66XF37XXdvY9/2DZFCv/5JL/kyZPn+fx+efIM+J+a1N1gQW6UHciSZHkWMFwIEY0QZkmSHyHxl9C048DpCXm+Ux1rVs6UBVC2aIeW1C1ckBs1WZZ1vzqSxxpjBr1sNkcnoRhtKAYr4SYfodrbNNbcaPOXnW0N1RZfE5o29+87/uvDMmZz9eIWFu3QpKfggtyobIPNvSUla7mqM9nxFR8Vj0vPNrYEakxCtIZ6JGXaLK7+kt7aE2vvDAL3L4rf1yyS/os+BRcudR7vN/WbiZIsqC/ez+OSKw3h5uAC4CAwxRQVv673qOkWf/kJZMXI4b0H6UAzBkQfFlpkyoQ8nwCQu1QrC00bY4tNJ3CvEIt7CHpLzBOJ9Wq0ZHEPRdbpn0DTZ22iUhowrqJO+w4wdlOx60LqjB9HinADLYEKjPYUary7GgMPvMaopDGtceM+UetL9rBz3fJOtO/wF7HGJDIgcyYP8heGRFPt2xPyfId0XWERCX9bsj93W5/M9wz2xLGyyZGItVeaRTH1oNl3Wxe4f7QT7WyKDqOoRW4sYkj2D+r17XM2A727OxX9JUX3OZClxiRHpc5Yb666vJaW1iYObdtOavosHlVeoabSS68RE3HYVLZeG4y3Oortedn48t9vkgLF6dLzDnnhUteNYbO3DgpWnWbnmq8ZljGb6odeaiq99MnwYFX1rDw9mMzR4xmaNoSWoJ+68isis2rhZ/Kz0ILcKAlIMDrinkJjM6dgs5txOW1kjwyx/+QtLpcHqAlJxCUNlGSdYYb8nIJTVVd/beO2P7FNWk+RIZ2aSi9xL7yG3a7idtpxOCxMTgujSK0c2HuEwycukTHQidDCw58HTzwTedeSk5OD2+0GIH7stH9Rlw2Hw4JZNWEw6Niz4AHfT3+IEq4n2hIGELpnqV7XsnU5OTmcP3+esrIy+hpOYbWouKJVbFYD+Ju5vvgAzZUBYmemMyw7jc0f+jEpTSDJrUp36OrVq0VXNF49i81mxu20YrebMZmM3Fy8j8GvzyXR2Yerm/LxX35I/My3aGvw8+jm4VNPbEVycvKylJQUsWrVqk40wXqBZkMbP9fns/bhHzwiiKzoCNyqwVxegfHcOUxmA+5XhqM6R1JbdLA50tqyXemKSpL0lcfjwefzsXv3buZ/8CaGSDn7Go+TNf5jVJfKiZICxjnTEJqg9JcjlAb9BAMhUj/9AmGqpuLYGp8Q2jy5AwXmezwe7t69i9/vx6zXNDkSwBWbRlBuoE4u4m64EF9rAIDY7BGEY230WzGdqaUH6DEqjps7FzdpkfCcCXm+kJSSkrIEWNp5rYVo0zTtXqozcGNeVrxn9OzVxnr5AbuKt1IdLMWhs7Kw/ztIshGTIxW9JQF/+Rlx58CKlqKSqiXzNwQ2A487bl40ENMe1vYXSvlymmXySyNiPorPnC6qE1MNcY5+NERCJBicCK0NX0mhVnFyY7iqLuDNv9i44aeCpvPAY6CuA3YACqB2gXWAPGmIvucbGSbPgL7qq3qFaAFCCCICtPt1bZc2HQv+dvp2WxkQbI9qICg95y9UAH17EkN739CeUGmf0wa0ACGgsb0vAP4Bkgocl0H4oW0AAAAASUVORK5CYII=',
			
		'apply.png':
		'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAABJ0gAASdIBqEWK+AAAAAd0SU1FB9gHGBYuEWwoYZ4AAAOMSURBVDjLtZRfbFNVHMe/957ee3tvuW33x27rkMKckzEDGw8FN0kGI8AggDEmJHuAGBOf/JM9yMSnveiCJr5ITNSoiU/+jb4sJOpk6bK5qQvbGNThNqBdHaztut7be9t7e//4MkhXikgTT/LNycn5nc/vl+85vwP8T4Mq92DXGyIcThqGZsEUZAz3b9yny4E+0wvYxD4Ak1plOMcUnRW69/e5NsSQcsBtRyBYtjPU13O+ur2lq3YpETmhavLpQJCM3BjL3ym74qwu9u95qsMjupzQ7Chee/4t8blnT21nndxHZVvReU7czjn4Vw4FTzpvxscRlxaQ1SVcWZhW85bxdVng7lcBlqY/P7r3JKfqK0irt1HlasRcZNaej12LGaz0QVngvOjq8bnrdjYHWuil1Rk4aAG8owrfhb7UDMs6M9wP45HB3f2VbhqOC8fajwt30nPQ81n4xCaEpn7WND3zzdCA/GthvKNw0XG2Gv4bCbJYAXPy441gUzffa23czQs8h0hiFm6+BmuyjPHwmG7adm9xIfcq7jzLb+aJvpxq3KRWVrhfL2yeg28KbYSwp4PNQeffqauwbcDD+3FxfFA1bbN3aEBOluy8g30VHhBzsnNn59atddvIxYlBJb6W+MNE/tQ+VomP6Z7p/a0Hdvh9lXRSvokqMYBYPGUNT/1y5ce3020A7JIVE2J93/rErvpAnZ9I2SiO7j3s2t3U2k7A/BnKun6odFc1bKn10wn5FgjNwbJZjMyM6HnDOFMKes9jy7ZbBCfHmqaBnJaBkgujsX4zU+N9zDs8NXqs4+k99KochZHPw+uux8TsbznL0D+9dF6ZftBlEwBoaGcGY8nlI2lF4gM1DYxlGUirt8EwNJoDTZRpqcjkknCyIiRFw+T1y5Jissejo6r+r+DFUT3xeJf2iSLrgcXlSFNtRQ0rcC4ouRSUXApaXgFAgWe8CM1MKFldezn07trlR/o2u85teoFQ5LMdgSeFLT4fUTUJpqWD5zxYWkka4ej8+E/vSPse9u7va5Chgcy30LHr2q258ER4RgUYMIRHTtMQjs4bVs5+6b/846TE2rEwpimrMf0L7zareim50iIKAnM9Gs3KGe3DS+/LgwDYdTnWz1DrsoutoNYD7lPwRf6Q28dcgIW/fv9K6klHkAVgPkQ2VWQLKZoLdTfWBmAVySyaH+gVVZCMKtBdsF2QAKWa5B/AJIpkSgpA9QAAAABJRU5ErkJggg==',
		
		'discard.png':
		'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAL7SURBVDiNtZTNbttGFIU/akhaod3alUCLplv0J21SoKsAQhIEcijkAbzOC3TfXbvoousWfYVs67UeILAiIYjiAAG6KtB/16IoqXAbR6FkipzpwjQryRTiTQa4IDBz7kfinLnUlFK8iVV4I1RAX9zY1zS9r+t7AJU4vl9XKs5rfJ1Om7ViX9P0QNcbG47jAfwbBE0njncXmy6jy8Dn4ne2t71P792zAH56+DD8p9uda7qsLvO4r+t7G47jXa/XrWm3y7Tb5Xq9bm24rhfoemNf0/TsS133os5xvHNr5sAACpBKIZUiHo+JfJ9r9bq1nsIDXW+su653rV63It8nHo8z/eLdmrdCiMa663qf3L1rxUGAnEwoFIvojsNv7fYrgI9qtdXFs58fPQpf+H7TSZLMiovhCdF423W9j3d25uCG4wAwXYD+0mqFJwvQC+A5+NaWd7VWs6aDAXI8RjPNM7uiiMKVKxibm/zabocnvd4FaC54wZadD+/cWYt6PWQUnYVimphbW/z++PHohe+38qCQMyB5Yaq0ANSSsBbXUivWKhXvg9u3relwiJxM5q0oFjFsmz+ePAlH/f7rrdjXNN0XovFWpeK9f+tWBi0Ui5i2DUA0s2fYNn92OuHLfr/pLgsvg25ueu/dvHkGPT2lsLKCYdscPXs2Ani3Wl1bPPvr6dPw5WAwB88GpCfE3mq57G1Xq9bpYEA8mYBpIsplDjud8CQIWidB0DrsdEJRLoNpEk8mnA4GbFer1mq57PWEyJ88ACklUkowDESpxNHBQTgaDptukuy6SbI7Gg6bRwcHoSiVwDD+1y8L79yKVdv23Bs3LAD/+fPwVQqd/QldRpcbnlUqeQCj4+P2d0ly/0eQgJbK1GcgvhLih7VSqQYQHh8vD0/TNBMwr4L1tRAPJBS+TJIv/gbB2X0XKVwCSQnkt0J8XwD5TZJ8fgghEAGRUiqeBQvABFbSpwkYM3UOBkiA6UxFwCStqVJK5o50+iItBWlpyJkVMyUBVA7kPzxLG7YwV5HcAAAAAElFTkSuQmCC',
		
		
		'brightness-contrast.png':
		'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsQAAALEAGtI711AAAAB3RJTUUH1gIPDQoQxZB9GQAABP9JREFUOMuVlWtsVEUUx/9n7t3ttt3t0m6xSRNakEK7y0MgQaGCtIClwW8CrWhijM8IKkUjISHGR1ItRRRCgNQYQ6BooBYoGAVaCgp8UIomrd2+MGk/tLy03d12u3t378zxw94lG0Ii3pvcmcyd+Z3/zDlzjob/8WyoXlcyZ67P7u/uGf+vufSgwbr62iIA1QAWAcgBEGPm4Xlz54+fajkZKyoqqt2+bUfAWs8PBa6rr93jcDi2VFasDeXn54/a7fZRIvonZsQCQhP6gYP70212/Xw0Gr2yfduOaykcAqCSxvQUYB6AE5Vrnin1lngRDI5RJDJJsZgBIqJIJCKcTifpNl1//ZU30NvX6wt/GM4YGBjQpVSfAChtOtZMAGwA4iJF7Inqqo2l0wsLEAiOwu/3o6HhK9TUbCUGw5QmkRDCneUON3zdsLSgsJBysj1lUqq2b74+VAqg3eLEAJCe3H7lmrWl2dlTYMZNNB45ikuXLhEAMIPATJmZmQQGVVVVuwJjQc/OnZ+tHBwa8uzfdxAvv/oSAJxKPVK9rr62yOFwbPGW+BAMjuJIAgpGAggASikCIIxYVNOkJtxul/i0tk5omo7qjRuQkZ6+2+vzHmg61pzksg7gxcqKtQiGxuD39ySgjKSzicEkFYOIBUAEyWLSNIURM0SWawoKCgphmvG4zWbLAJAMQxYAVufn54OVws+/XAYzA2BYXwIDzApKsWClhFRKmMoUUSMixicCqNlSAwBzMzIzMrt6/rgXZTqAErvdjmg0gv6+voQ5MMAgRqLDrMCJOBJgIqWUkFKKicgEsjLd0DRt2uXLV4Yvtl80N1SvGwPwmg7AnXASIxQKWYottQCBQZKVIEAwiFhJoRQLpUzBCoAQeO/d9x/ThAYishNR3ua332zQAQSZOVuxgtPlRCgYSipOOBBMrJTVh2CwkEoKZiWINMh4HHv2fvHnyM0RX8yIKQB/A9ilA+g1DGOpUhKziorQ0XE99TiQ3A2DBZiJwYIVC8WsZTjsmAiHQYJur1pZXrn4icUj87wLGQAEgLbhkWEQaSh9cinYehNnzMRgSKVISimkUkJKJRRL0nUbOZ0utLa2QtO07rhpTiahSfDh1rZzcGdlwTfHh+XLlyUVAgAppUhKk6RUmpRSAyAcjnQtJ9sjDCOGjt+v4e6du1rLqTMGAM1iktbW2j5aVr4i253lXuLJzUWJtxiBYBBDQ0OSmSMAjIo1qw1d05GW5tDtdodjMjzpPHz4kOv0mdP46IOPueVMy+NxIzY80H+jxwJDA4C21vZzM2YWVjw6Y+Y0l8uF4pLZKJxeoMy4GQ2Hw9GylSuiff397MnO0RuPNqZ1dXUG4nFzQJmqu6n5+OzvGo/hxMnmmT3+3qPWzaJ72e3WrdtV3zcfb3p6dcWS4lnFWLhgEXw+L8XjkkwZZ1aSXK4sikajYsH8hb/d+GtgvKuzayAYCv66vurZzZqmlVhqBQBxD7z3y323AJTLWrn76tUrm1aWr0LeI3nQbTqkVOLOzbvsSEsXk+FJunChLTY4ONh99qfz1wFIAN8CiFiZLQ4gnprok9b0t97ZNGvq1KnP6Zq2CEQeZhULBILG8xtfkLs+r9e6u/xbOzu7xq2kblpQw4KaACQ9oKKIlIqQHOOnypZ5cnNz10ej0e4ffzjbkbLGtCoHW60EEtf2YUoW3/cvWeuEBbt/Dv4FMcN4kEWVoFoAAAAASUVORK5CYII=',
		
		
		'hue-saturation-lightness.png':
			'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAWjSURBVDiNfZRbbFTXFYa/c84+Z+5jj4exg28xDhhDqFMwN0NqUgxNqTDBTVpFIPKQqlVbgUr7EqNUakWrxGmKWlVFpFIVoqYSJUpQ0qtCa+JA7KQgHBwTmoAzGOP7bewZz+Vc5uw+2FihQt3S/7Kl9e1f/9prKVJK7nWaWkMPAls8ht4ISoPjOGW6EP2ulO9btn0e6GxvS924ZzGg/C+4qTUU1IX4bVGo+Jvb1zVrKyvWGstK6lgSrGImM8zNsR7+M9Btnet+Kz+ZHP2d7Tit7W0p8/+Cm1pDj+hCnG7e8mRhy9bvGYnMTVK5EZK5YdLWBEFPCWFvGWFvKdHAcl6/cNz8c9erI7bjPN7eluq+J7ipNbQjEip684ffOBooiZTzydjfmTPHAdA1PwW+MtLmJFk7AUDAE2PN0r2MTQ/zy9eOpBOpyYfb21JX7gI3tYbCQoi+Zw+8GPN5FeJT55FSUlO8k5rirxD0FAMKADl7ltszl+gZPE1eWlRHt5HLCZ774+G4Zdur2ttSFoAKoAtx4tGNzeFIqIj41Hk8IsT2mlbWVRwg6Cnh2JlDtByt4pmXW/DqBayI7WDX6uco8i/js8kOisIhtq9tLhWa9vM7jtWm1tD6kD/y2M76Fk98sgMpXTZVfQfXCS7m1RvvIpmZ4tqtfy/epTJZtlb/AEMEuDHRzq7NT3hD/sjBptZQ1R3HjRtXb9KT5m2ydoIHlnyZ0YkE+19Yw9O/2kDvza67uh0fucrhlx5l/y/W0NHzF+orDpCzZ5nN9bO+dosLNACoHsPYVh6rMNLWFADVSxr528WTCFVwa+wTnjnZwvB0HICMNcehE01cHfgAVVH568WXqYxsRqge0uYEFcUVfqFpWwBUKeWGWGQpWSuBomgU+io5tOcYuzc9jdfwkXds8m5+3q6UWLaJR/Oy7Qt7+cm+P6AoKoX++8nY05REShWhaY0Aqu04JWF/CCufJmBE0VSDgkCUb331pzxS9zgu7t0fX1Goq97KweYXKYlUAhD2LsVy0gT9YfKuuwxA1YUYn06No2s+MtYUrnQ43/smTz5fQ8dHb+C6ebx6AACfESQv83T3vcu+Fx7kTOeJ+UbmxtA1H9PJMTRV7QdQFUW5PDw1gK75cWWemextrsQvkLOyKCiUx1bgMXwA5F2b2rJ6hCownRwXPz2LRJLI9mOIIIOT/dJx3fcAVNOyOgbHByxdmy/un+rkqR1HWF25kUN7jvHKjy4T9kUAEJrB8YPv8ON9r7Dm/ga+v7uNoZlunHwOQ/NzazSedhynE0AAF65c/8ja+lCjoWs+ro+fpSKynl9/9+3FXFVVRVU1FGV++hpW7aJh1S5MJ8U/rh1BaF5UfFz9rFcF3gfQ4u+ZQyfP/qzedswHVlbWiYw1zVjqY2Khlfj0eacrytZSVVzLYw3fpjRavTjaXTePM5sdIhqo5u0P/pUZmhj4/T+fT576/K4oFELre+pr+6KFBUFSuVEURWX1fXuoKd6JR4QW3TuuydDMZbpvv4rppAl7lzI+lZJ/OntqcKzPXHXldDYjpZSKlBJFUZTGw4HdBQWBU0/s2BsoicZI5UZx3Pk16zeiFPrKSVuTJHMjSOkiVA9hbymD4yOcOfdWZvjG3Nc/PJXtAkzAVphfWwLw1+/37YlWGr/5Ym2df9u6hw0XE8edl+vaqKqOUD0I1QPSwzuX3rV7r3+c7L+Ue/bGObMTmAFmgewdsA4EgcLCCq38ob2Bo+Ei3+baquVaafF9IhaJURAMM5OaZSIxyeDYiPNpf587NZTt+vC1uZfMOTkOTAOJBXjmDlgFjAV4CAhVf8lYHy7VNhQWG/W6X1mexynQpJjJpWQ8MWz2Tvfne4Z77GvAHJBcUArIANZixp9zLhYe8SzIAAxNx8jbuIALOIAN5BYytRZkA3kppfwvM8pvTiwfKGAAAAAASUVORK5CYII=',		
			 
		}[uri];
	};

	/// INITIALIZE ////////////////////////////////////////
  
	var viewer;
  	
	var panoramaIcon = document.createElement('img');
	panoramaIcon.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAALwSURBVDiNlZNPbJN1GMc/v/dPu3bdumYZDS6U1i4e1ilSyYgCl23AIJODGiVI4skTB0ICIZkeRGRkGQlXjQf/sChRDgpGOsM0MjABzGCwBYdj2M6t+0fGC33b0ffX98cBOhEOxu/pOTz55Ps8z/cRSinK6u7pCiqh7VVCbxWq9DyAEvo1oUr9QrlH9u/rtHhCogzo7ulqRxi9DYnmYCwaMcKhAAAzCzlu/ZWRYyMXLZTcuX9fZ+opQHdPV4u/MnhqQ2uH3zBNhjIF7KKLAPwenUR9BbgOA/0/5PO29frjEN1X6alGGGdb2l+rSQ3O8vXP49yavENNwEt+scjA5TS/XMmSd6B13Wrz5o3rW86f//WT9es23AfQlND2NCSaQ2gml/6Y49sPttL73mY8hsbw2Axfvd/OiQNbuXB9FjSThkRzUAltb9mBpoTRFotGjMG0jc+jA/DO4T5kSZGIh9lx8KFbQxNcTtvEohFDCb21DDCEKq0KhwJYY7dpjC/j7UMpmuJh4itqcUou+fslOj+/xIpnapm2imxqqqN8IQCtXDjSJbuwyPJlIe4VJFbe4U6uSK7gMPznNHfzRfSl7n+kIfShOcsmUusjPXmbI++uZeTmDIOjWYbHZxn/e56Th15lbt4iWudn3rJBGNeWAELJM5nMhExGKzE0AcD3H3UwmV3A59H57mDHw1l1weqVfjITE1Io2b+0A5R7dPTqhV3PxWN1G1+q560DP4IQNDWEEQK2f3gaBbQl63Glw8iV3xZnp6c/fipIgeqaU5s2b/N7vV6mLJe8Uw6SRrhKQzpFfkqdpKLClANnzy3adm7LN8dPnHs8yi2abh5PrnklFF0ZMaoqfQDcswukMxPy94sDBdeVnmQy6TV0D729x+xcLrdRPPFM1Zqm79FNs82VchWAbhhD0nHOTE1NfZrNZkfjz8YDVcEAL76Q5MtjX4z+C/BfenP7G+uB07FYNNDY2ERfX+ru/wI8grwMfCaEWA7sfgAAwUcdC8899wAAAABJRU5ErkJggg==';
	panoramaIcon.setAttribute('style', 'position: absolute; left: 40px');

	var panoramaLink = document.createElement('a');
	panoramaLink.href = "javascript:;";
	panoramaLink.innerHTML = "Panorama View";
	panoramaLink.addEventListener('click', function(e){ 
		// lazily initialize
		if(!viewer){
			if(unsafeWindow.deviantART.pageData["fullview"]["src"]){
			  	viewer = new experience.panorama.Viewer({
			  		'ImageURL' : unsafeWindow.deviantART.pageData["fullview"]["src"],
			  		'ImageWidth': unsafeWindow.deviantART.pageData["fullview"]["width"],
			  		'ImageHeight': unsafeWindow.deviantART.pageData["fullview"]["height"]
			  	});  			
			} else {
				return;
			}
		} 
		
		viewer.setVisible(true);
	}, false);

	var devLinks = document.getElementById('deviation-links')
	devLinks.insertBefore(panoramaLink, devLinks.getElementsByTagName('strong')[0]);
	devLinks.insertBefore(panoramaIcon, panoramaLink);

})();
