dancewiththesky's
CickenFillet
<
View Source
Panorama 2 Beta
// ==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,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', }[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); })();
Tastefully Powered by ChickenFillet