'use strict'; (function(jQuery, angular) { // h, s, v [0,1] function hsvToRgb(h,s,v) { if( s === 0 ){ return {r: v, g: v, b: v}; } var i = Math.floor(h*6); var f = 6*h - i; // fractional part var p = v * (1 - s); var q = v * (1 - s*f); var t = v * (1 - s*(1-f)); switch(i%6){ case 0: return {r:v,g:t,b:p}; case 1: return {r:q,g:v,b:p}; case 2: return {r:p,g:v,b:t}; case 3: return {r:p,g:q,b:v}; case 4: return {r:t,g:p,b:v}; default: return {r:v,g:p,b:q}; } } function rgbToHsv(r,g,b) { var max = Math.max(r, g, b), min = Math.min(r, g, b); var h, s, v = max; var d = max - min; s = max === 0 ? 0 : d / max; if(max == min) { h = 0; // achromatic } else { switch(max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } h /= 6; } return { h: h, s: s, v: v }; } function expandColor(val, partial) { if(!val) val = {}; if(val.a === undefined) val.a = 1.0; if(partial && partial.a !== undefined) val.a = partial.a; if(partial && (partial.h!==undefined || partial.s!==undefined || partial.v!==undefined)) { if(partial.h !== undefined) val.h = partial.h; if(partial.s !== undefined) val.s = partial.s; if(partial.v !== undefined) val.v = partial.v; var rgb = hsvToRgb(val.h, val.s, val.v); val.r = Math.max(rgb.r, 0); val.g = Math.max(rgb.g, 0); val.b = Math.max(rgb.b, 0); val.hex = ('00'+Math.round(val.r*255).toString(16)).slice(-2) +('00'+Math.round(val.g*255).toString(16)).slice(-2) +('00'+Math.round(val.b*255).toString(16)).slice(-2) } else if(partial && (partial.r!==undefined || partial.g!==undefined || partial.b!==undefined)) { if(partial.r !== undefined) val.r = partial.r; if(partial.g !== undefined) val.g = partial.g; if(partial.b !== undefined) val.b = partial.b; var hsv = rgbToHsv(val.r, val.g, val.b); val.h = hsv.h; val.s = hsv.s; val.v = hsv.v; val.hex = ('00'+Math.round(val.r*255).toString(16)).slice(-2) +('00'+Math.round(val.g*255).toString(16)).slice(-2) +('00'+Math.round(val.b*255).toString(16)).slice(-2) } else if( /^[0-9A-Fa-f]{6}$/.test(partial) ) { val.hex = partial; partial = parseInt(partial, 16); val.r = ((partial & 0xff0000) >> 16) / 255; val.g = ((partial & 0x00ff00) >> 8) / 255; val.b = (partial & 0x0000ff) / 255; var hsv = rgbToHsv(val.r, val.g, val.b); val.h = hsv.h; val.s = hsv.s; val.v = hsv.v; } var rgba = [Math.round(val.r*255), Math.round(val.g*255), Math.round(val.b*255), val.a]; val.css = 'rgba('+rgba.join(',')+')'; val.background = 'linear-gradient('+val.css+','+val.css+'), url('+transparencyBgUrl+')'; return val; } var template = '
\n \n
\n
\n
\n
\n
\n
\n #\n \n \n \n \n
\n
\n
\n'; var vertShaderSrc = 'precision lowp float;\nattribute vec3 vertPosition;\nvarying vec2 windowPosition;\nuniform vec2 windowDimensions;\n\nvoid main(void)\n{\n mat3 xform = mat3(0.5*windowDimensions.x, 0.0, 0.0, 0.0, 0.5*windowDimensions.y, 0.0, windowDimensions.x/2.0, windowDimensions.y/2.0, 1.0);\n windowPosition = (xform * vec3(vertPosition.xy, 1.0)).xy;\n gl_Position = vec4(vertPosition,1);\n}\n\n'; var fragShaderSrc = 'precision lowp float;\n#define M_PI 3.141592653589\n\nvarying vec2 windowPosition;\nuniform vec4 selectedColor;\nuniform float swatchWidth;\nuniform float marginWidth;\nuniform vec2 windowDimensions;\nuniform bool radial;\nuniform bool useAlpha;\nuniform sampler2D tex;\n\nvec3 hsv2rgb(vec3 c){\n vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);\n vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);\n return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);\n}\n\nvec4 getSelectionColor(vec4 baseColor){\n return vec4( vec3(1.0)-baseColor.rgb, baseColor.a );\n}\n\nvoid main(void){\n\n float taLeft = useAlpha ? swatchWidth + marginWidth : 0.0;\n float taWidth = windowDimensions.x - swatchWidth - marginWidth - taLeft;\n vec2 center = vec2(taWidth/2.0+taLeft, windowDimensions.y/2.0);\n float aspect = taWidth / windowDimensions.y;\n vec4 color;\n vec2 selectionPosition;\n\n if( windowPosition.x >= taLeft && windowPosition.x <= taLeft+taWidth )\n {\n if(radial)\n {\n vec2 radialVec = (windowPosition - center)*vec2(2.0/taWidth, 2.0/windowDimensions.y);\n radialVec = mat2(max(1.0,aspect), 0.0, 0.0, max(1.0,1.0/aspect)) * radialVec;\n if(length(radialVec) > 1.0) discard;\n float hue = atan(radialVec.y,radialVec.x)/(2.0*M_PI) + 0.5;\n color = vec4(hue, length(radialVec), selectedColor.z, 1.0);\n\n float angle = (selectedColor.x-0.5)*2.0*M_PI;\n selectionPosition = min(center.x-taLeft, center.y) * selectedColor.y * vec2(cos(angle), sin(angle)) + center;\n }\n else {\n color = vec4((windowPosition.x-taLeft)/taWidth, windowPosition.y/windowDimensions.y, selectedColor.z, 1.0);\n selectionPosition = vec2(selectedColor.x*taWidth+taLeft, selectedColor.y*windowDimensions.y);\n }\n\n vec2 difference = selectionPosition - windowPosition;\n float radius = length(difference);\n\n if( radius > 4.5 && radius < 6.0 )\n gl_FragColor = getSelectionColor(vec4(hsv2rgb(color.xyz), 1.0 ));\n else\n gl_FragColor = vec4( hsv2rgb(color.xyz), 1.0);\n }\n\n else if(useAlpha && windowPosition.x < swatchWidth)\n {\n vec4 color = vec4(mix( texture2D(tex, windowPosition/16.0).xyz, hsv2rgb(selectedColor.xyz), windowPosition.y/windowDimensions.y), 1.0);\n\n if( windowDimensions.y * abs(windowPosition.y/windowDimensions.y-selectedColor.w) < 1.0 )\n gl_FragColor = getSelectionColor(color);\n else\n gl_FragColor = color;\n }\n\n else if(windowPosition.x > windowDimensions.x-swatchWidth)\n {\n vec4 color = vec4( selectedColor.x, selectedColor.y, windowPosition.y/windowDimensions.y, 1.0);\n\n if( windowDimensions.y * abs(windowPosition.y/windowDimensions.y-selectedColor.z) < 1.0 )\n gl_FragColor = getSelectionColor(vec4(hsv2rgb(color.xyz), 1.0 ));\n else\n gl_FragColor = vec4( hsv2rgb(color.xyz), 1.0);\n }\n else\n discard;\n}\n\n'; var transparencyBgUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAK0lEQVQoz2Pcu3cvAzbg5OSEVZyJgUQwqoEYwPj//3+sEvv27RsNJfppAAD+GAhT8tRPqwAAAABJRU5ErkJggg=='; var paletteInitialized = false; var PalettePopup = { "colorCallback": null, "colorSelectCallback": null, "elem": document.createElement('div'), "initialize": function() { var self = this; if(document.getElementById('htmlPalettePopup')) return; // insert template into dom this.elem = document.createElement('div'); this.elem.id = 'htmlPalettePopup'; this.elem.innerHTML = template; this.elem.onclick = function(e){ e.stopPropagation(); } this.elem.style.display = 'none'; document.body.appendChild(this.elem); document.addEventListener('click', function(evt){ self.hide(); }); this.canvas = this.elem.children[0].children[0]; // get gl context this.gl = this.canvas.getContext('webgl'); if(!this.gl) this.gl = this.canvas.getContext('experimental-webgl'); var gl = this.gl; if(!gl){ console.error('Your web browser does not support WebGL, so cannot make use of the HtmlPalette color picker. Sorry :('); return; } /******************************** * Initialize the webgl canvas *********************************/ // set up vert shader var vert = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(vert, vertShaderSrc); gl.compileShader(vert); if( !gl.getShaderParameter(vert, gl.COMPILE_STATUS) ){ console.error('Vert shader error:', gl.getShaderInfoLog(vert)); return; } // set up frag shader var frag = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(frag, fragShaderSrc); gl.compileShader(frag); if( !gl.getShaderParameter(frag, gl.COMPILE_STATUS) ){ console.error('Frag shader error:', gl.getShaderInfoLog(frag)); return; } // link shaders var program = this._program = gl.createProgram(); gl.attachShader(program, vert); gl.attachShader(program, frag); gl.linkProgram(program); if( !gl.getProgramParameter(program, gl.LINK_STATUS) ){ console.error('Unable to link program'); return; } else { gl.useProgram(program); this._program = program; } var vertPositionAttrib = gl.getAttribLocation(program, 'vertPosition'); gl.enableVertexAttribArray(vertPositionAttrib); var buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ -1.0, -1.0, 0.0, -1.0, 1.0, 0.0, 1.0, -1.0, 0.0, 1.0, 1.0, 0.0 ]), gl.STATIC_DRAW); gl.vertexAttribPointer(vertPositionAttrib, 3, gl.FLOAT, false, 0, 0); // bind non-configurable uniforms gl.uniform1f(gl.getUniformLocation(program, 'swatchWidth'), 20); gl.uniform1f(gl.getUniformLocation(program, 'marginWidth'), 10); // bind transparency texture var gltex = gl.createTexture(); var img = new Image(); img.onload = function() { gl.bindTexture(gl.TEXTURE_2D, gltex); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); }; img.src = transparencyBgUrl; gl.clearColor(0.0, 0.0, 0.0, 0.0); /****************************************** * Bind event listeners ******************************************/ var twoaxis = this.elem.querySelector('.twoaxis'), oneaxis = this.elem.querySelector('.oneaxis'), alpha = this.elem.querySelector('.alpha'); twoaxis.ondragstart = function(evt){ if(evt.dataTransfer.setDragImage) evt.dataTransfer.setDragImage(document.createElement('div'),0,0); if(evt.dataTransfer.dropEffect) evt.dataTransfer.dropEffect = 'move'; } twoaxis.ondrag = twoaxis.onmousedown = function(evt){ var paletteWidth = self.canvas.width - (self.lastOpts && self.lastOpts.useAlpha ? 60 : 30); var h = self.canvas.height; if( self.lastOpts.radial ){ var center = [paletteWidth/2, h/2]; var clickDiff = [evt.offsetX-1 - center[0], -(evt.offsetY-1) + center[1]]; var hue = Math.atan2(clickDiff[1], clickDiff[0])/(2*Math.PI) + 0.5; var sat = Math.sqrt(clickDiff[0]*clickDiff[0] + clickDiff[1]*clickDiff[1]) / Math.min(center[0], center[1]); if( evt.screenX !== 0 || evt.screenY !== 0 ) self.color({h: hue, s: Math.min(sat, 1.0)}); } else if( evt.screenX !== 0 || evt.screenY !== 0 ) self.color({h: Math.min(1, Math.max(0, (evt.offsetX-1)/paletteWidth)), s: Math.min(1, Math.max(0, (h-evt.offsetY+1)/(h-1)))}); } oneaxis.ondragstart = function(evt){ evt.dataTransfer.setDragImage(document.createElement('div'),0,0); } oneaxis.ondrag = oneaxis.onmousedown = function(evt){ var h = self.canvas.height; if( evt.screenX !== 0 || evt.screenY !== 0 ) self.color({v: Math.max(0, Math.min(1, (h-evt.offsetY-1)/(h-1)))}); } alpha.ondragstart = function(evt){ evt.dataTransfer.setDragImage(document.createElement('div'),0,0); } alpha.ondrag = alpha.onmousedown = function(evt){ var h = self.canvas.height; if( evt.screenX !== 0 || evt.screenY !== 0 ) self.color({a: Math.max(0, Math.min(1, (h-evt.offsetY-1)/(h-1)))}); } twoaxis.ondragend = twoaxis.onmouseup = oneaxis.ondragend = oneaxis.onmouseup = alpha.ondragend = alpha.onmouseup = function(evt){ if(self.colorSelectCallback) self.colorSelectCallback(self.selection); }; var initialValue, initialMouse; function bindRGBElement(e, channel) { e.ondragstart = function(evt){ initialValue = Math.round(self.selection[channel]*255); initialMouse = evt.offsetY; evt.dataTransfer.effectAllowed = 'none'; evt.dataTransfer.setDragImage(document.createElement('div'),0,0); } e.ondrag = function(evt){ //evt.preventDefault(); var newVal = initialValue + initialMouse-evt.offsetY; if(evt.screenX !== 0 || evt.screenY !== 0){ var color = {}; color[channel] = Math.max(0, Math.min(1, newVal/255)); self.color(color); } } e.ondragend = function(evt){ if(self.colorSelectCallback) self.colorSelectCallback(self.selection); }; } bindRGBElement(this.elem.querySelector('.r'), 'r'); bindRGBElement(this.elem.querySelector('.g'), 'g'); bindRGBElement(this.elem.querySelector('.b'), 'b'); bindRGBElement(this.elem.querySelector('.a'), 'a'); }, // bind configurable uniforms "rebind": function(useAlpha, radial) { var style = window.getComputedStyle(this.elem.querySelector('.palette')); var w = parseInt(style.width), h = parseInt(style.height)-4; this.gl.uniform2f(this.gl.getUniformLocation(this._program, 'windowDimensions'), w,h); this.gl.uniform1i(this.gl.getUniformLocation(this._program, 'useAlpha'), !!useAlpha); this.gl.uniform1i(this.gl.getUniformLocation(this._program, 'radial'), !!radial); }, "placePopup": function(evt, popupEdge) { evt.stopPropagation(); var popupStyle = window.getComputedStyle(this.elem); var popupWidth = parseInt(popupStyle.width) || 0; var popupHeight = parseInt(popupStyle.height) || 0; var box = evt.target.getBoundingClientRect(); var origin = { x: (box.left+box.right)/2, y: (box.top+box.bottom)/2 }; var offset = 20; // fall back on sw on missing or invalid option if( !/^[ns]?[we]?$/.test(popupEdge) || !popupEdge ) popupEdge = 'se'; // set position vertically if(/^n/.test(popupEdge)) this.elem.style.top = origin.y - popupHeight - offset + 'px'; else if(/^s/.test(popupEdge)) this.elem.style.top = origin.y + offset + 'px'; else this.elem.style.top = origin.y - popupHeight/2 + 'px'; // set position horizontally if(/e$/.test(popupEdge)) this.elem.style.left = origin.x + offset + 'px'; else if(/w$/.test(popupEdge)) this.elem.style.left = origin.x - popupWidth - offset + 'px'; else this.elem.style.left = origin.x - popupWidth/2 + 'px'; this.elem.style.display = ''; }, // manually position the various ui elements "sizePopup": function(useAlpha, classes) { this.elem.setAttribute('class', classes || ''); var popupStyle = window.getComputedStyle(this.elem); this._popupWidth = parseInt(popupStyle.width) || 0; this._popupHeight = parseInt(popupStyle.height) || 0; // size the canvas var style = window.getComputedStyle(this.elem.querySelector('.palette')); var w = parseInt(style.width), h = parseInt(style.height)-4; this.canvas.width = w; this.canvas.height = h; this.gl.viewport(0, 0, w, h); // size the overlay var paletteWidth = useAlpha ? w-60 : w-30; var twoaxis = this.elem.querySelector('.twoaxis'); var oneaxis = this.elem.querySelector('.oneaxis') var alpha = this.elem.querySelector('.alpha') twoaxis.style.width = paletteWidth + 'px'; twoaxis.style.height = h + 'px'; oneaxis.style.height = h + 'px'; alpha.style.height = h + 'px'; if(!useAlpha){ alpha.style.display = 'none'; this.elem.querySelector('.rgbInput .a').style.display = 'none'; twoaxis.style.left = '0px'; } else { alpha.style.display = ''; this.elem.querySelector('.rgbInput .a').style.display = ''; twoaxis.style.left = ''; } }, "color": function(val) { this.selection = expandColor( this.selection, val ); if(val) { this.redraw(); if(this.colorCallback) this.colorCallback(this.selection); } }, "redraw": function() { var selectionUniform = this.gl.getUniformLocation(this._program, 'selectedColor'); this.gl.uniform4f(selectionUniform, this.selection.h, this.selection.s, this.selection.v, this.selection.a); this.gl.clear(this.gl.COLOR_BUFFER_BIT); this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4); this.elem.querySelector('.rgbInput .r').innerHTML = this.selection.hex.slice(0,2); this.elem.querySelector('.rgbInput .g').innerHTML = this.selection.hex.slice(2,4); this.elem.querySelector('.rgbInput .b').innerHTML = this.selection.hex.slice(4,6); this.elem.querySelector('.rgbInput .a').innerHTML = ('00'+Math.round(this.selection.a*255).toString(16)).slice(-2); this.elem.querySelector('.colorswatch').style['background'] = this.selection.background; }, "show": function(opts, evt) { if(this.gl){ this.lastOpts = opts; opts = opts || {}; this.placePopup(evt, opts.popupEdge); this.sizePopup(opts.useAlpha, opts.css); this.rebind(opts.useAlpha, opts.radial); this.selection = opts.color; this.redraw(); } }, "hide": function(){ this.elem.style.display = 'none'; this.colorCallback = null; this.colorSelectCallback = null; this.triggerElem = null; } }; function Trigger(triggerElem, opts) { this.triggerElem = triggerElem; this.colorCallback = opts.colorCallback || null; this.colorSelectCallback = opts.colorSelectCallback || null; this.popupEdge = opts.popupEdge || 'se'; this.css = opts.css || ''; this.radial = opts.radial || false; this.useAlpha = opts.useAlpha || false; this.updateTriggerBg = opts.updateTriggerBg !== undefined ? opts.updateTriggerBg : true; this.disabled = opts.disabled || false; this.selection = {}; this.color(opts.initialColor || 'aaaaaa'); PalettePopup.initialize(); var self = this; triggerElem.addEventListener('click', this._clickHandler = function(event) { if(PalettePopup.triggerElem !== self.triggerElem && !self.disabled) { PalettePopup.triggerElem = self.triggerElem; PalettePopup.colorCallback = self.color.bind(self); PalettePopup.colorSelectCallback = function(color){ if(self.colorSelectCallback) self.colorSelectCallback(JSON.parse(JSON.stringify(color))); }; PalettePopup.show({ radial: self.radial, useAlpha: self.useAlpha, popupEdge: self.popupEdge, css: self.css, color: self.selection }, event); } }); } Trigger.prototype.color = function(val, isAngular) { if(val === undefined) return this.selection; else { /*var flag = false; for(var i in val){ if(this.selection[i] !== val[i]) flag = true; } if(!flag) return;*/ if( this.selection !== val ) this.selection = expandColor(this.selection, val); if(this.updateTriggerBg){ this.triggerElem.style['background'] = this.selection.background; } if(this.colorCallback){ this.colorCallback(JSON.parse(JSON.stringify(this.selection)), isAngular); } } } Trigger.prototype.destroy = function() { this.triggerElem.removeEventListener('click', this._clickHandler); if(PalettePopup.triggerElem === this.triggerElem){ PalettePopup.hide(); } } window.HtmlPalette = Trigger; window.HtmlPalette.PalettePopup = PalettePopup; if(jQuery) { jQuery.fn.extend({ 'HtmlPalette': function(cmd) { var args = Array.prototype.slice.call(arguments, 1); var palette = this.data('HtmlPalette'); // when in doubt, return the palette object if(!cmd) return palette; // initialize else if(cmd instanceof Object) { if(palette) palette.destroy(); palette = this.data('HtmlPalette', new HtmlPalette(this[0], cmd)); if(palette.colorCallback) palette.colorCallback = palette.colorCallback.bind(this); } // error on command without initialization else if(!palette) { throw new Error('HtmlPalette is uninitialized on this element'); } // evaluate command else switch(cmd) { case 'color': if(args.length) palette.color(args[0]); else return palette.color(); break; case 'colorCallback': if(args.length) palette.colorCallback = args[0].bind(this); else return palette.colorCallback; break; case 'colorSelectCallback': if(args.length) palette.colorSelectCallback = args[0].bind(this); else return palette.colorSelectCallback; break; case 'popupEdge': if(args.length) palette.popupEdge = args[0]; else return palette.popupEdge; break; case 'radial': if(args.length){ palette.radial = args[0]; } else return palette.radial; break; case 'updateTriggerBg': if(args.length) palette.updateTriggerBg = args[0]; else return palette.updateTriggerBg; break; case 'disabled': if(args.length) palette.disabled = args[0]; else return palette.disabled; break; case 'destroy': palette.destroy(); this.data('htmlPalette', null); break; } } }); } if(angular) { var app = angular.module('html-palette', []); app.directive('htmlPalette', ['$timeout', function($timeout) { return { restrict: 'AE', scope: { color: '=', radial: '=?', alpha: '=?', disabled: '=?', onColorSelect: '&?' }, link: function($scope, elem, attrs) { var throttleTimeout = null; attrs.initialColor = $scope.color; var palette = new Trigger(elem[0], attrs); $scope.$watch('radial', function(newval){ palette.radial = !!newval; }); $scope.$watch('alpha', function(newval){ palette.alpha = !!newval; }); $scope.$watch('disabled', function(newval){ palette.disabled = !!newval; }); elem.bind('$destroy', function(){ palette.destroy(); }); palette.colorSelectCallback = function(color){ $scope.onColorSelect && $scope.onColorSelect({color: color}); }; palette.colorCallback = function(color, isAngular) { if($scope.color) { if(attrs.colorProfile === 'hex') $scope.color.hex = color.hex; else if(/^[rgbhsva]+$/.test(attrs.colorProfile)){ for(var i=0; i