/** * Wheel Color Picker for jQuery * * https://raffer.one/projects/jquery-wheelcolorpicker * * Author : Fajar Chandra * Date : 2019.06.12 * * Copyright © 2011-2019 Fajar Chandra. All rights reserved. * Released under MIT License. * http://www.opensource.org/licenses/mit-license.php */ (function ($) { /** * Function: wheelColorPicker * * The wheelColorPicker plugin wrapper. Firing all functions and * setting/getting all options in this plugin should be called via * this function. * * Before that, if wheelColorPicker instance is not yet initialized, * this will initialize ColorPicker widget. * * This function will look for the options parameter passed in, and * try to do something as specified in this order: * 1. If no argument is passed, then initialize the plugin or do nothing * 2. If object is passed, then call setOptions() * 3. If string is passed, then try to fire a method with that name * 4. If string is passed and no method matches the name, then try * to set/get an option with that name. If a setter/getter method * available (i.e. setSomething), it will set/get that option via that method. */ $.fn.wheelColorPicker = function() { var returnValue = this; // Allows method chaining // Separate first argument and the rest.. // First argument is used to determine function/option name // e.g. wheelColorPicker('setColor', { r: 0, g: 0, b: 1 }) if(arguments.length > 0) { var shift = [].shift; var firstArg = shift.apply(arguments); var firstArgUc = (typeof firstArg === "string") ? firstArg.charAt(0).toUpperCase() + firstArg.slice(1) : firstArg; } else { var firstArg = undefined; var firstArgUc = undefined; } var args = arguments; this.each(function() { // Grab ColorPicker object instance var instance = $(this).data('jQWCP.instance'); // Initialize if not yet created if(instance == undefined || instance == null) { // Get init options var options = {}; if(typeof firstArg === "object") { options = firstArg; } instance = new WCP.ColorPicker(this, options); $(this).data('jQWCP.instance', instance); } /// What to do? /// // No arguments provided, do nothing // wheelColorPicker() if(firstArg === undefined || typeof firstArg === "object") { } // Call a method // wheelColorPicker('show') else if(typeof instance[firstArg] === "function") { //console.log('method'); var ret = instance[firstArg].apply(instance, args); // If instance is not returned, no method chaining if(ret !== instance) { returnValue = ret; return false; } } // Try option setter // wheelColorPicker('color', '#ff00aa') else if(typeof instance['set'+firstArgUc] === "function" && args.length > 0) { //console.log('setter'); var ret = instance['set'+firstArgUc].apply(instance, args); // If instance is not returned, no method chaining if(ret !== instance) { returnValue = ret; return false; } } // Try option getter // wheelColorPicker('color') else if(typeof instance['get'+firstArgUc] === "function") { //console.log('getter'); var ret = instance['get'+firstArgUc].apply(instance, args); // If instance is not returned, no method chaining if(ret !== instance) { returnValue = ret; return false; } } // Set option value // wheelColorPicker('format', 'hex') else if(instance.options[firstArg] !== undefined && args.length > 0) { //console.log('set option'); instance.options[firstArg] = args[0]; } // Get option value // wheelColorPicker('format') else if(instance.options[firstArg] !== undefined) { //console.log('get option'); returnValue = instance.options[firstArg]; return false; } // Nothing matches, throw error else { $.error( 'Method/option named ' + firstArg + ' does not exist on jQuery.wheelColorPicker' ); } }); return returnValue; }; /******************************************************************/ ///////////////////////////////////////// // Shorthand for $.fn.wheelColorPicker // ///////////////////////////////////////// var WCP = $.fn.wheelColorPicker; ///////////////////////////////////////// /** * Object: defaults * * Contains default options for the wheelColorPicker plugin. * * Member properties: * * format - Color naming style. See colorToRgb for all * available formats. * live - Enable dynamic slider gradients. * preview - Enable live color preview on input field * userinput - (Deprecated) Enable picking color by typing directly * validate - When userinput is enabled, force the value to be * a valid format. If user input an invalid color, the value * will be reverted to last valid color. * autoResize - Automatically resize container width. * If set to false, you could manually adjust size with CSS. * autoFormat - Automatically convert input value to * specified format. For example, if format is "rgb", * "#ff0000" will automatically converted into "rgb(255,0,0)". * color - Initial value in any of supported color * value format or as an object. Setting this value will * override the current input value. * alpha - (Deprecated) Force the color picker to use alpha value * despite its selected color format. This option is * deprecated. Use sliders = "a" instead. * inverseLabel - (deprecated) Boolean use inverse color for * input label instead of black/white color. * preserveWheel - Boolean preserve color wheel shade when slider * position changes. If set to true, changing * color wheel from black will reset selectedColor.val * (shade) to 1. * interactive - Boolean enable interactive sliders where slider bar * gradients change dynamically as user drag a slider * handle. Set to false if this affect performance. * See also 'quality' option if you wish to keep * interactive slider but with reduced quality. * cssClass - Object CSS Classes to be added to the color picker. * layout - String [block|popup] Layout mode. * animDuration - Number Duration for transitions such as fade-in * and fade-out. * quality - Rendering details quality. The normal quality is 1. * Setting less than 0.1 may make the sliders ugly, * while setting the value too high might affect performance. * sliders - String combination of sliders. If null then the color * picker will show default values, which is "wvp" for * normal color or "wvap" for color with alpha value. * Possible combinations are "whsvrgbap". * Order of letters will affect slider positions. * sliderLabel - Boolean Show labels for each slider. * sliderValue - Boolean Show numeric value of each slider. * hideKeyboard - Boolean Keep input blurred to avoid on screen keyboard appearing. * If this is set to true, avoid assigning handler to blur event. * rounding - Round the alpha value to N decimal digits. Default is 2. * Set -1 to disable rounding. * mobile - Display mobile-friendly layout when opened in mobile device. * mobileWidth - Max screen width to use mobile layout instead of default one. * mobileAutoScroll - Automatically scroll the page if focused input element * gets obstructed by color picker dialog. * htmlOptions - Load options from HTML attributes. * To set options using HTML attributes, * prefix these options with 'data-wcp-' as attribute names. * snap - Snap color wheel and slider on 0, 0.5, and 1.0 * snapTolerance - Snap if slider position falls within defined * tolerance value. */ WCP.defaults = { format: 'hex', /* 1.x */ preview: false, /* 1.x */ live: true, /* 2.0 */ userinput: true, /* DEPRECATED 1.x */ validate: true, /* 1.x */ autoResize: true, /* 3.0 */ autoFormat: true, /* 3.0 */ //color: null, /* DEPRECATED 1.x */ /* OBSOLETE 3.0 */ /* Init-time only */ //alpha: null, /* DEPRECATED 1.x */ /* OBSOLETE 3.0 */ /* See methods.alpha */ preserveWheel: null, /* DEPRECATED 1.x */ /* Use live */ cssClass: '', /* 2.0 */ layout: 'popup', /* 2.0 */ /* Init-time only */ animDuration: 200, /* 2.0 */ quality: 1, /* 2.0 */ sliders: null, /* 2.0 */ //sliderLabel: true, /* 2.0 */ /* NOT IMPLEMENTED */ //sliderValue: false, /* 2.0 */ /* NOT IMPLEMENTED */ rounding: 2, /* 2.3 */ mobile: true, /* 3.0 */ mobileWidth: 480, /* 3.0 */ hideKeyboard: false, /* 2.4 */ htmlOptions: true, /* 2.3 */ snap: false, /* 2.5 */ snapTolerance: 0.05 /* 2.5 */ }; /******************************************************************/ ////////////////////////////// // STATIC OBJECTS AND FLAGS // ////////////////////////////// /* * Note: To determine input position (top and left), use the following: * WCP.ORIGIN.top + this.input.getBoundingClientRect().top * instead of using $(this.input).offset().top because on mobile browsers * (chrome) jQuery's offset() function returns wrong value. */ /// Top left of the page is not on (0,0), making window.scrollX/Y and offset() useless /// See WCP.ORIGIN WCP.BUG_RELATIVE_PAGE_ORIGIN = false; /// Coordinate of the top left page (mobile chrome workaround) WCP.ORIGIN = { left: 0, top: 0 }; /******************************************************************/ ////////////////////// // HELPER FUNCTIONS // ////////////////////// /** * Function: colorToStr * * Since 2.0 * * Convert color object to string in specified format * * Available formats: * - hex * - hexa * - css * - cssa * - rgb * - rgb% * - rgba * - rgba% * - hsv * - hsv% * - hsva * - hsva% * - hsb * - hsb% * - hsba * - hsba% */ WCP.colorToStr = function( color, format ) { var result = ""; switch( format ) { case 'css': result = "#"; case 'hex': var r = Math.round(color.r * 255).toString(16); if( r.length == 1) { r = "0" + r; } var g = Math.round(color.g * 255).toString(16); if( g.length == 1) { g = "0" + g; } var b = Math.round(color.b * 255).toString(16); if( b.length == 1) { b = "0" + b; } result += r + g + b; break; case 'cssa': result = "#"; case 'hexa': var r = Math.round(color.r * 255).toString(16); if( r.length == 1) { r = "0" + r; } var g = Math.round(color.g * 255).toString(16); if( g.length == 1) { g = "0" + g; } var b = Math.round(color.b * 255).toString(16); if( b.length == 1) { b = "0" + b; } var a = Math.round(color.a * 255).toString(16); if( a.length == 1) { a = "0" + a; } result += r + g + b + a; break; case 'rgb': result = "rgb(" + Math.round(color.r * 255) + "," + Math.round(color.g * 255) + "," + Math.round(color.b * 255) + ")"; break; case 'rgb%': result = "rgb(" + (color.r * 100) + "%," + (color.g * 100) + "%," + (color.b * 100) + "%)"; break; case 'rgba': result = "rgba(" + Math.round(color.r * 255) + "," + Math.round(color.g * 255) + "," + Math.round(color.b * 255) + "," + color.a + ")"; break; case 'rgba%': result = "rgba(" + (color.r * 100) + "%," + (color.g * 100) + "%," + (color.b * 100) + "%," + (color.a * 100) + "%)"; break; case 'hsv': result = "hsv(" + (color.h * 360) + "," + color.s + "," + color.v + ")"; break; case 'hsv%': result = "hsv(" + (color.h * 100) + "%," + (color.s * 100) + "%," + (color.v * 100) + "%)"; break; case 'hsva': result = "hsva(" + (color.h * 360) + "," + color.s + "," + color.v + "," + color.a + ")"; break; case 'hsva%': result = "hsva(" + (color.h * 100) + "%," + (color.s * 100) + "%," + (color.v * 100) + "%," + (color.a * 100) + "%)"; break; case 'hsb': result = "hsb(" + color.h + "," + color.s + "," + color.v + ")"; break; case 'hsb%': result = "hsb(" + (color.h * 100) + "%," + (color.s * 100) + "%," + (color.v * 100) + "%)"; break; case 'hsba': result = "hsba(" + color.h + "," + color.s + "," + color.v + "," + color.a + ")"; break; case 'hsba%': result = "hsba(" + (color.h * 100) + "%," + (color.s * 100) + "%," + (color.v * 100) + "%," + (color.a * 100) + "%)"; break; } return result; }; /** * Function: strToColor * * Since 2.0 * * Convert string to color object. * Please note that if RGB color is supplied, the returned value * will only contain RGB. * * If invalid string is supplied, FALSE will be returned. */ WCP.strToColor = function( val ) { var color = { a: 1 }; var tmp; var hasAlpha; // #fff // #ffff if( val.match(/^#[0-9a-f]{3}$/i) != null || val.match(/^#[0-9a-f]{4}$/i) ) { if( isNaN( color.r = parseInt(val.substr(1, 1), 16) * 17 / 255 ) ) { return false; } if( isNaN( color.g = parseInt(val.substr(2, 1), 16) * 17 / 255 ) ) { return false; } if( isNaN( color.b = parseInt(val.substr(3, 1), 16) * 17 / 255 ) ) { return false; } // Alpha if(val.length == 5) { if( isNaN( color.a = parseInt(val.substr(4, 1), 16) * 17 / 255 ) ) { return false; } } } // fff // ffff else if( val.match(/^[0-9a-f]{3}$/i) != null || val.match(/^[0-9a-f]{4}$/i) != null ) { if( isNaN( color.r = parseInt(val.substr(0, 1), 16) * 17 / 255 ) ) { return false; } if( isNaN( color.g = parseInt(val.substr(1, 1), 16) * 17 / 255 ) ) { return false; } if( isNaN( color.b = parseInt(val.substr(2, 1), 16) * 17 / 255 ) ) { return false; } // Alpha if(val.length == 4) { if( isNaN( color.a = parseInt(val.substr(3, 1), 16) * 17 / 255 ) ) { return false; } } } // #ffffff // #ffffffff else if( val.match(/^#[0-9a-f]{6}$/i) != null || val.match(/^#[0-9a-f]{8}$/i) != null ) { if( isNaN( color.r = parseInt(val.substr(1, 2), 16) / 255 ) ) { return false; } if( isNaN( color.g = parseInt(val.substr(3, 2), 16) / 255 ) ) { return false; } if( isNaN( color.b = parseInt(val.substr(5, 2), 16) / 255 ) ) { return false; } // Alpha if(val.length == 9) { if( isNaN( color.a = parseInt(val.substr(7, 2), 16) / 255 ) ) { return false; } } } // ffffff // ffffffff else if( val.match(/^[0-9a-f]{6}$/i) != null || val.match(/^[0-9a-f]{8}$/i) != null ) { if( isNaN( color.r = parseInt(val.substr(0, 2), 16) / 255 ) ) { return false; } if( isNaN( color.g = parseInt(val.substr(2, 2), 16) / 255 ) ) { return false; } if( isNaN( color.b = parseInt(val.substr(4, 2), 16) / 255 ) ) { return false; } // Alpha if(val.length == 8) { if( isNaN( color.a = parseInt(val.substr(6, 2), 16) / 255 ) ) { return false; } } } // rgb(100%,100%,100%) // rgba(100%,100%,100%,100%) // rgba(255,255,255,1) // rgba(100%,1, 0.5,.2) else if( val.match(/^rgba\s*\(\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*,\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*,\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*,\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*\)$/i) != null || val.match(/^rgb\s*\(\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*,\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*,\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*\)$/i) != null ) { if(val.match(/a/i) != null) { hasAlpha = true; } else { hasAlpha = false; } tmp = val.substring(val.indexOf('(')+1, val.indexOf(',')); if( tmp.charAt( tmp.length-1 ) == '%') { if( isNaN( color.r = parseFloat(tmp) / 100 ) ) { return false; } } else { if( isNaN( color.r = parseInt(tmp) / 255 ) ) { return false; } } tmp = val.substring(val.indexOf(',')+1, val.indexOf(',', val.indexOf(',')+1)); if( tmp.charAt( tmp.length-1 ) == '%') { if( isNaN( color.g = parseFloat(tmp) / 100 ) ) { return false; } } else { if( isNaN( color.g = parseInt(tmp) / 255 ) ) { return false; } } if(hasAlpha) { tmp = val.substring(val.indexOf(',', val.indexOf(',')+1)+1, val.lastIndexOf(',')); } else { tmp = val.substring(val.lastIndexOf(',')+1, val.lastIndexOf(')')); } if( tmp.charAt( tmp.length-1 ) == '%') { if( isNaN( color.b = parseFloat(tmp) / 100 ) ) { return false; } } else { if( isNaN( color.b = parseInt(tmp) / 255 ) ) { return false; } } if(hasAlpha) { tmp = val.substring(val.lastIndexOf(',')+1, val.lastIndexOf(')')); if( tmp.charAt( tmp.length-1 ) == '%') { if( isNaN( color.a = parseFloat(tmp) / 100 ) ) { return false; } } else { if( isNaN( color.a = parseFloat(tmp) ) ) { return false; } } } } // hsv(100%,100%,100%) // hsva(100%,100%,100%,100%) // hsv(360,1,1,1) // hsva(360,1, 0.5,.2) // hsb(100%,100%,100%) // hsba(100%,100%,100%,100%) // hsb(360,1,1,1) // hsba(360,1, 0.5,.2) else if( val.match(/^hsva\s*\(\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*,\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*,\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*,\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*\)$/i) != null || val.match(/^hsv\s*\(\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*,\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*,\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*\)$/i) != null || val.match(/^hsba\s*\(\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*,\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*,\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*,\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*\)$/i) != null || val.match(/^hsb\s*\(\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*,\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*,\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*\)$/i) != null ) { if(val.match(/a/i) != null) { hasAlpha = true; } else { hasAlpha = false; } tmp = val.substring(val.indexOf('(')+1, val.indexOf(',')); if( tmp.charAt( tmp.length-1 ) == '%') { if( isNaN( color.h = parseFloat(tmp) / 100 ) ) { return false; } } else { if( isNaN( color.h = parseFloat(tmp) / 360 ) ) { return false; } } tmp = val.substring(val.indexOf(',')+1, val.indexOf(',', val.indexOf(',')+1)); if( tmp.charAt( tmp.length-1 ) == '%') { if( isNaN( color.s = parseFloat(tmp) / 100 ) ) { return false; } } else { if( isNaN( color.s = parseFloat(tmp) ) ) { return false; } } if(hasAlpha) { tmp = val.substring(val.indexOf(',', val.indexOf(',')+1)+1, val.lastIndexOf(',')); } else { tmp = val.substring(val.lastIndexOf(',')+1, val.lastIndexOf(')')); } if( tmp.charAt( tmp.length-1 ) == '%') { if( isNaN( color.v = parseFloat(tmp) / 100 ) ) { return false; } } else { if( isNaN( color.v = parseFloat(tmp) ) ) { return false; } } if(hasAlpha) { tmp = val.substring(val.lastIndexOf(',')+1, val.lastIndexOf(')')); if( tmp.charAt( tmp.length-1 ) == '%') { if( isNaN( color.a = parseFloat(tmp) / 100 ) ) { return false; } } else { if( isNaN( color.a = parseFloat(tmp) ) ) { return false; } } } } else { return false; } return color; }; /** * Function: hsvToRgb * * Since 2.0 * * Perform HSV to RGB conversion */ WCP.hsvToRgb = function( h, s, v ) { // Calculate RGB from hue (1st phase) var cr = (h <= (1/6) || h >= (5/6)) ? 1 : (h < (1/3) ? 1 - ((h - (1/6)) * 6) : (h > (4/6) ? (h - (4/6)) * 6 : 0)); var cg = (h >= (1/6) && h <= (3/6)) ? 1 : (h < (1/6) ? h * 6 : (h < (4/6) ? 1 - ((h - (3/6)) * 6) : 0)); var cb = (h >= (3/6) && h <= (5/6)) ? 1 : (h > (2/6) && h < (3/6) ? (h - (2/6)) * 6 : (h > (5/6) ? 1 - ((h - (5/6)) * 6) : 0)); // console.log(cr + ' ' + cg + ' ' + cb); // Calculate RGB with saturation & value applied var r = (cr + (1-cr)*(1-s)) * v; var g = (cg + (1-cg)*(1-s)) * v; var b = (cb + (1-cb)*(1-s)) * v; // console.log(r + ' ' + g + ' ' + b + ' ' + v); return { r: r, g: g, b: b }; }; /** * Function: rgbToHsv * * Since 2.0 * * Perform RGB to HSV conversion */ WCP.rgbToHsv = function( r, g, b ) { var h; var s; var v; var maxColor = Math.max(r, g, b); var minColor = Math.min(r, g, b); var delta = maxColor - minColor; // Calculate saturation if(maxColor != 0) { s = delta / maxColor; } else { s = 0; } // Calculate hue // To simplify the formula, we use 0-6 range. if(delta == 0) { h = 0; } else if(r == maxColor) { h = (6 + (g - b) / delta) % 6; } else if(g == maxColor) { h = 2 + (b - r) / delta; } else if(b == maxColor) { h = 4 + (r - g) / delta; } else { h = 0; } // Then adjust the range to be 0-1 h = h/6; // Calculate value v = maxColor; // console.log(h + ' ' + s + ' ' + v); return { h: h, s: s, v: v }; }; /******************************************************************/ //////////////////////// // COLOR PICKER CLASS // //////////////////////// /** * Class: ColorPicker * * Since 3.0 */ WCP.ColorPicker = function ( elm, options ) { // Assign reference to input DOM element this.input = elm; // Setup selected color in the following priority: // 1. options.color // 2. input.value // 3. default this.color = { h: 0, s: 0, v: 1, r: 1, g: 1, b: 1, a: 1 }; this.setValue(this.input.value); // Set options this.options = $.extend(true, {}, WCP.defaults); this.setOptions(options); // Check sliders option, if not defined, set default sliders if(this.options.sliders == null) this.options.sliders = 'wvp' + (this.options.format.indexOf('a') >= 0 ? 'a' : ''); this.init(); }; //////////////////// // Static members // //////////////////// /** * Static Property: ColorPicker.widget * * Reference to global color picker widget (popup) */ WCP.ColorPicker.widget = null; /** * Property: ColorPicker.overlay * * Reference to overlay DOM element (overlay for global popup) */ WCP.ColorPicker.overlay = null; /** * Function: init * * Since 3.0 * 2.0 was methods.staticInit * * Initialize wheel color picker globally. */ WCP.ColorPicker.init = function() { // Only perform initialization once if(WCP.ColorPicker.init.hasInit == true) return; WCP.ColorPicker.init.hasInit = true; // Insert overlay element to handle popup closing // when hideKeyboard is true, hence input is always blurred var $overlay = $(''); $overlay.on('click', WCP.Handler.overlay_click); WCP.ColorPicker.overlay = $overlay.get(0); $('body').append($overlay); // Insert CSS for color wheel var wheelImage = WCP.ColorPicker.getWheelDataUrl(200); $('head').append( '' ); // Attach events $('html').on('mouseup.wheelColorPicker', WCP.Handler.html_mouseup); $('html').on('touchend.wheelColorPicker', WCP.Handler.html_mouseup); $('html').on('mousemove.wheelColorPicker', WCP.Handler.html_mousemove); $('html').on('touchmove.wheelColorPicker', WCP.Handler.html_mousemove); $(window).on('resize.wheelColorPicker', WCP.Handler.window_resize); }; /** * Function: createWidget * * Since 3.0 * 2.5 was private.initWidget * * Create color picker widget. */ WCP.ColorPicker.createWidget = function() { /// WIDGET /// // Notice: We won't use canvas to draw the color wheel since // it may takes time and cause performance issue. var $widget = $( "
" + "
" + "
" + "" + "
" + "
" + "" + "" + "
" + "
" + "" + "" + "
" + "
" + "" + "" + "
" + "
" + "" + "" + "
" + "
" + "" + "" + "
" + "
" + "" + "" + "
" + "
" + "" + "" + "
" + "
" + "" + "
" + "
" ); // Small UI fix to disable highlighting the widget // Also UI fix to disable touch context menu $widget.find('.jQWCP-wWheel, .jQWCP-slider-wrapper, .jQWCP-scursor, .jQWCP-slider') .attr('unselectable', 'on') .css('-moz-user-select', 'none') .css('-webkit-user-select', 'none') .css('user-select', 'none') .css('-webkit-touch-callout', 'none'); // Disable context menu on sliders // Workaround for touch browsers $widget.on('contextmenu.wheelColorPicker', function() { return false; }); // Bind widget events $widget.on('mousedown.wheelColorPicker', '.jQWCP-wWheel', WCP.Handler.wheel_mousedown); $widget.on('touchstart.wheelColorPicker', '.jQWCP-wWheel', WCP.Handler.wheel_mousedown); $widget.on('mousedown.wheelColorPicker', '.jQWCP-wWheelCursor', WCP.Handler.wheelCursor_mousedown); $widget.on('touchstart.wheelColorPicker', '.jQWCP-wWheelCursor', WCP.Handler.wheelCursor_mousedown); $widget.on('mousedown.wheelColorPicker', '.jQWCP-slider', WCP.Handler.slider_mousedown); $widget.on('touchstart.wheelColorPicker', '.jQWCP-slider', WCP.Handler.slider_mousedown); $widget.on('mousedown.wheelColorPicker', '.jQWCP-scursor', WCP.Handler.sliderCursor_mousedown); $widget.on('touchstart.wheelColorPicker', '.jQWCP-scursor', WCP.Handler.sliderCursor_mousedown); return $widget.get(0); }; /** * Function: getWheelDataUrl * * Create color wheel image and return as base64 encoded data url. */ WCP.ColorPicker.getWheelDataUrl = function( size ) { var r = size / 2; // radius var center = r; var canvas = document.createElement('canvas'); canvas.width = size; canvas.height = size; var context = canvas.getContext('2d'); // Fill the wheel with colors for(var y = 0; y < size; y++) { for(var x = 0; x < size; x++) { // Get the offset from central position var offset = Math.sqrt(Math.pow(x - center, 2) + Math.pow(y - center, 2)); // Skip pixels outside picture area (plus 2 pixels) if(offset > r + 2) { continue; } // Get the position in degree (hue) var deg = ( (x - center == 0 ? (y < center ? 90 : 270) : (Math.atan((center - y) / (x - center)) / Math.PI * 180) ) + (x < center ? 180 : 0) + 360 ) % 360; // Relative offset (sat) var sat = offset / r; // Value is always 1 var val = 1; // Calculate color var cr = (Math.abs(deg + 360) + 60) % 360 < 120 ? 1 : (deg > 240 ? (120 - Math.abs(deg - 360)) / 60 : (deg < 120 ? (120 - deg) / 60 : 0)); var cg = Math.abs(deg - 120) < 60 ? 1 : (Math.abs(deg - 120) < 120 ? (120 - Math.abs(deg - 120)) / 60 : 0); var cb = Math.abs(deg - 240) < 60 ? 1 : (Math.abs(deg - 240) < 120 ? (120 - Math.abs(deg - 240)) / 60 : 0); var pr = Math.round((cr + (1 - cr) * (1 - sat)) * 255); var pg = Math.round((cg + (1 - cg) * (1 - sat)) * 255); var pb = Math.round((cb + (1 - cb) * (1 - sat)) * 255); context.fillStyle = 'rgb(' + pr + ',' + pg + ',' + pb + ')'; context.fillRect(x, y, 1, 1); } } return canvas.toDataURL(); }; ///////////// // Members // ///////////// /** * Property: ColorPicker.options * * Plugin options for the color picker instance, extended from WCP.defaults. */ WCP.ColorPicker.prototype.options = null; /** * Property: ColorPicker.input * * Reference to input DOM element */ WCP.ColorPicker.prototype.input = null; /** * Property: ColorPicker.widget * * Reference to widget DOM element (global popup or private inline widget) */ WCP.ColorPicker.prototype.widget = null; /** * Property: ColorPicker.color * * Selected color object. */ WCP.ColorPicker.prototype.color = null; /** * Property: ColorPicker.lastValue * * Store last input value */ WCP.ColorPicker.prototype.lastValue = null; /** * Function: ColorPicker.setOptions * * Since 3.0 * * Set options to the color picker. If htmlOptions is set to true, * options set via html attributes are also reloaded. If both html * attribute and argument exists, option set via options argument * gets priority. */ WCP.ColorPicker.prototype.setOptions = function( options ) { // options should be a separate object (passed by value) // Make a copy of options options = $.extend(true, {}, options); // Load options from HTML attributes if(this.options.htmlOptions) { for(var key in WCP.defaults) { // Only if option key is valid and not set via function argument if(this.input.hasAttribute('data-wcp-'+key) && options[key] === undefined) { options[key] = this.input.getAttribute('data-wcp-'+key); // Change true/false string to boolean if(options[key] == 'true') { options[key] = true; } else if(options[key] == 'false') { options[key] = false; } } } } // Set options for(var key in options) { // Skip undefined option key if(this.options[key] === undefined) continue; var keyUc = key.charAt(0).toUpperCase() + key.slice(1); // If setter is available, try setting it via setter if(typeof this['set'+keyUc] === "function") { this['set'+keyUc](options[key]); } // Otherwise directly update options else { this.options[key] = options[key]; } } return this; // Allow chaining }; /** * Function: ColorPicker.init * * Initialize wheel color picker widget */ WCP.ColorPicker.prototype.init = function() { WCP.ColorPicker.init(); // Initialization must only occur once if(this.hasInit == true) return; this.hasInit = true; var instance = this; var $input = $(this.input); var $widget = null; /// LAYOUT & BINDINGS /// // Setup block mode layout if( this.options.layout == 'block' ) { // Create widget this.widget = WCP.ColorPicker.createWidget(); $widget = $(this.widget); // Store object instance reference $widget.data('jQWCP.instance', this); // Wrap widget around the input elm and put the input // elm inside widget $widget.insertAfter(this.input); // Retain display CSS property if($input.css('display') == "inline") { $widget.css('display', "inline-block"); } else { $widget.css('display', $input.css('display')); } $widget.append(this.input); $input.hide(); // Add tabindex attribute to make the widget focusable if($input.attr('tabindex') != undefined) { $widget.attr('tabindex', $input.attr('tabindex')); } else { $widget.attr('tabindex', 0); } // Further widget adjustments based on options this.refreshWidget(); // Draw shading this.redrawSliders(true); this.updateSliders(); // Bind widget element events $widget.on('focus.wheelColorPicker', WCP.Handler.widget_focus_block); $widget.on('blur.wheelColorPicker', WCP.Handler.widget_blur_block); } // Setup popup mode layout else { // Only need to create one widget, used globally if(WCP.ColorPicker.widget == null) { WCP.ColorPicker.widget = WCP.ColorPicker.createWidget(); $widget = $(WCP.ColorPicker.widget); // Assign widget to global $widget.hide(); $('body').append($widget); // Bind popup events $widget.on('mousedown.wheelColorPicker', WCP.Handler.widget_mousedown_popup); //$widget.on('mouseup.wheelColorPicker', WCP.Handler.widget_mouseup_popup); } this.widget = WCP.ColorPicker.widget; // Bind input element events $input.on('focus.wheelColorPicker', WCP.Handler.input_focus_popup); $input.on('blur.wheelColorPicker', WCP.Handler.input_blur_popup); } // Bind input events $input.on('keyup.wheelColorPicker', WCP.Handler.input_keyup); $input.on('change.wheelColorPicker', WCP.Handler.input_change); // Set color value // DEPRECATED by 3.0 if(typeof this.options.color == "object") { this.setColor(this.options.color); this.options.color = undefined; } else if(typeof this.options.color == "string") { this.setValue(this.options.color); this.options.color = undefined; } // Set readonly mode /* DEPRECATED */ if(this.options.userinput) { $input.removeAttr('readonly'); } else { $input.attr('readonly', true); } }; /** * Function: destroy * * Destroy the color picker and return it to normal element. */ WCP.ColorPicker.prototype.destroy = function() { var $widget = $(this.widget); var $input = $(this.input); // Reset layout // No need to delete global popup if(this.options.layout == 'block') { // Check if active control is the same widget as destroyed widget, remove the reference if it's true var $control = $( $('body').data('jQWCP.activeControl') ); // Refers to slider wrapper or wheel if ($control.length) { var controlWidget = $control.closest('.jQWCP-wWidget'); if ($widget.is(controlWidget)) { $('body').data('jQWCP.activeControl', null); } } $widget.before(this.input); $widget.remove(); $input.show(); } // Unbind events $input.off('focus.wheelColorPicker'); $input.off('blur.wheelColorPicker'); $input.off('keyup.wheelColorPicker'); $input.off('change.wheelColorPicker'); // Remove data $input.data('jQWCP.instance', null); // remove self delete this; }; /** * Function: refreshWidget * * Since 3.0 * 2.5 was private.adjustWidget * * Update widget to match current option values. */ WCP.ColorPicker.prototype.refreshWidget = function() { var $widget = $(this.widget); var options = this.options; var mobileLayout = false; // Set CSS classes $widget.attr('class', 'jQWCP-wWidget'); if(options.layout == 'block') { $widget.addClass('jQWCP-block'); } $widget.addClass(options.cssClass); //$widget.addClass(this.input.getAttribute('class')); // Check whether to use mobile layout if(window.innerWidth <= options.mobileWidth && options.layout != 'block' && options.mobile) { mobileLayout = true; $widget.addClass('jQWCP-mobile'); } // Rearrange sliders $widget.find('.jQWCP-wWheel, .jQWCP-slider-wrapper, .jQWCP-wPreview') .hide() .addClass('hidden'); for(var i in options.sliders) { var $slider = null; switch(this.options.sliders[i]) { case 'w': $slider = $widget.find('.jQWCP-wWheel'); break; case 'h': $slider = $widget.find('.jQWCP-wHue'); break; case 's': $slider = $widget.find('.jQWCP-wSat'); break; case 'v': $slider = $widget.find('.jQWCP-wVal'); break; case 'r': $slider = $widget.find('.jQWCP-wRed'); break; case 'g': $slider = $widget.find('.jQWCP-wGreen'); break; case 'b': $slider = $widget.find('.jQWCP-wBlue'); break; case 'a': $slider = $widget.find('.jQWCP-wAlpha'); break; case 'p': $slider = $widget.find('.jQWCP-wPreview'); break; } if($slider != null) { $slider.appendTo(this.widget); $slider.show().removeClass('hidden'); } } // If widget is hidden, show it first so we can calculate dimensions correctly //var widgetIsHidden = false; //if($widget.is(':hidden')) { //widgetIsHidden = true; //$widget.css({ opacity: '0' }).show(); //} // Adjust sliders height based on quality var sliderHeight = options.quality * 50; $widget.find('.jQWCP-slider').attr('height', sliderHeight); var $visElms = $widget.find('.jQWCP-wWheel, .jQWCP-slider-wrapper, .jQWCP-wPreview').not('.hidden'); // Adjust container and sliders width // Only if not on mobile layout (force fixed on mobile) if(options.autoResize && !mobileLayout) { // Auto resize var width = 0 // Set slider size first, then adjust container $visElms.css({ width: '', height: '' }); $visElms.each(function(index, item) { var $item = $(item); width += parseFloat($item.css('margin-left').replace('px', '')) + parseFloat($item.css('margin-right').replace('px', '')) + $item.outerWidth(); }); $widget.css({ width: width + 'px' }); } else { // Fixed size // Set container size first, then adjust sliders $widget.css({ width: '' }); var $visWheel = $widget.find('.jQWCP-wWheel').not('.hidden'); var $visSliders = $widget.find('.jQWCP-slider-wrapper, .jQWCP-wPreview').not('.hidden'); $visWheel.css({ height: $widget.height() + 'px', width: $widget.height() }); if($visWheel.length > 0) { var horzSpace = $widget.width() - $visWheel.outerWidth() - parseFloat($visWheel.css('margin-left').replace('px', '')) - parseFloat($visWheel.css('margin-right').replace('px', '')); } else { var horzSpace = $widget.width(); } if($visSliders.length > 0) { var sliderMargins = parseFloat($visSliders.css('margin-left').replace('px', '')) + parseFloat($visSliders.css('margin-right').replace('px', '')); $visSliders.css({ height: $widget.height() + 'px', width: (horzSpace - ($visSliders.length - 1) * sliderMargins) / $visSliders.length + 'px' }); } } // Reset visibility //if(widgetIsHidden) { //$widget.css({ opacity: '' }).hide(); //} return this; // Allows method chaining }; /** * Function: redrawSliders * * Introduced in 2.0 * * Redraw slider gradients. Hidden sliders are not redrawn as to * improve performance. If options.live is FALSE, sliders are not redrawn. * * Parameter: * force - Boolean force redraw all sliders. */ WCP.ColorPicker.prototype.redrawSliders = function( force ) { // Skip if widget not yet initialized if(this.widget == null) return this; var $widget = $(this.widget); // DEPRECATED 3.0 // In 2.0, parameters are ( sliders, force ) if(typeof arguments[0] === "string") { force = arguments[1]; } // No need to redraw sliders on global popup widget if not // attached to the input elm in current iteration if(this != $widget.data('jQWCP.instance')) return this; var options = this.options; var color = this.color; var w = 1; var h = options.quality * 50; var A = 1; var R = 0; var G = 0; var B = 0; var H = 0; var S = 0; var V = 1; // Dynamic colors if(options.live) { A = color.a; R = Math.round(color.r * 255); G = Math.round(color.g * 255); B = Math.round(color.b * 255); H = color.h; S = color.s; V = color.v; } /// PREVIEW /// // Preview box must always be redrawn, if not hidden var $previewBox = $widget.find('.jQWCP-wPreviewBox'); if(!$previewBox.hasClass('hidden')) { var previewBoxCtx = $previewBox.get(0).getContext('2d'); previewBoxCtx.fillStyle = "rgba(" + R + "," + G + "," + B + "," + A + ")"; previewBoxCtx.clearRect(0, 0, 1, 1); previewBoxCtx.fillRect(0, 0, 1, 1); } /// SLIDERS /// if(!this.options.live && !force) return this; /// ALPHA /// // The top color is (R, G, B, 1) // The bottom color is (R, G, B, 0) var $alphaSlider = $widget.find('.jQWCP-wAlphaSlider'); if(!$alphaSlider.hasClass('hidden') || force) { var alphaSliderCtx = $alphaSlider.get(0).getContext('2d'); var alphaGradient = alphaSliderCtx.createLinearGradient(0, 0, 0, h); alphaGradient.addColorStop(0, "rgba("+R+","+G+","+B+",1)"); alphaGradient.addColorStop(1, "rgba("+R+","+G+","+B+",0)"); alphaSliderCtx.fillStyle = alphaGradient; alphaSliderCtx.clearRect(0, 0, w, h); alphaSliderCtx.fillRect(0, 0, w, h); } /// RED /// // The top color is (255, G, B) // The bottom color is (0, G, B) var $redSlider = $widget.find('.jQWCP-wRedSlider'); if(!$redSlider.hasClass('hidden') || force) { var redSliderCtx = $redSlider.get(0).getContext('2d'); var redGradient = redSliderCtx.createLinearGradient(0, 0, 0, h); redGradient.addColorStop(0, "rgb(255,"+G+","+B+")"); redGradient.addColorStop(1, "rgb(0,"+G+","+B+")"); redSliderCtx.fillStyle = redGradient; redSliderCtx.fillRect(0, 0, w, h); } /// GREEN /// // The top color is (R, 255, B) // The bottom color is (R, 0, B) var $greenSlider = $widget.find('.jQWCP-wGreenSlider'); if(!$greenSlider.hasClass('hidden') || force) { var greenSliderCtx = $greenSlider.get(0).getContext('2d'); var greenGradient = greenSliderCtx.createLinearGradient(0, 0, 0, h); greenGradient.addColorStop(0, "rgb("+R+",255,"+B+")"); greenGradient.addColorStop(1, "rgb("+R+",0,"+B+")"); greenSliderCtx.fillStyle = greenGradient; greenSliderCtx.fillRect(0, 0, w, h); } /// BLUE /// // The top color is (R, G, 255) // The bottom color is (R, G, 0) var $blueSlider = $widget.find('.jQWCP-wBlueSlider'); if(!$blueSlider.hasClass('hidden') || force) { var blueSliderCtx = $blueSlider.get(0).getContext('2d'); var blueGradient = blueSliderCtx.createLinearGradient(0, 0, 0, h); blueGradient.addColorStop(0, "rgb("+R+","+G+",255)"); blueGradient.addColorStop(1, "rgb("+R+","+G+",0)"); blueSliderCtx.fillStyle = blueGradient; blueSliderCtx.fillRect(0, 0, w, h); } /// HUE /// // The hue slider is static. var $hueSlider = $widget.find('.jQWCP-wHueSlider'); if(!$hueSlider.hasClass('hidden') || force) { var hueSliderCtx = $hueSlider.get(0).getContext('2d'); var hueGradient = hueSliderCtx.createLinearGradient(0, 0, 0, h); hueGradient.addColorStop(0, "#f00"); hueGradient.addColorStop(0.166666667, "#ff0"); hueGradient.addColorStop(0.333333333, "#0f0"); hueGradient.addColorStop(0.5, "#0ff"); hueGradient.addColorStop(0.666666667, "#00f"); hueGradient.addColorStop(0.833333333, "#f0f"); hueGradient.addColorStop(1, "#f00"); hueSliderCtx.fillStyle = hueGradient; hueSliderCtx.fillRect(0, 0, w, h); } /// SAT /// // The top color is hsv(h, 1, v) // The bottom color is hsv(0, 0, v) var $satSlider = $widget.find('.jQWCP-wSatSlider'); if(!$satSlider.hasClass('hidden') || force) { var satTopRgb = $.fn.wheelColorPicker.hsvToRgb(H, 1, V); satTopRgb.r = Math.round(satTopRgb.r * 255); satTopRgb.g = Math.round(satTopRgb.g * 255); satTopRgb.b = Math.round(satTopRgb.b * 255); var satSliderCtx = $satSlider.get(0).getContext('2d'); var satGradient = satSliderCtx.createLinearGradient(0, 0, 0, h); satGradient.addColorStop(0, "rgb("+satTopRgb.r+","+satTopRgb.g+","+satTopRgb.b+")"); satGradient.addColorStop(1, "rgb("+Math.round(V*255)+","+Math.round(V*255)+","+Math.round(V*255)+")"); satSliderCtx.fillStyle = satGradient; satSliderCtx.fillRect(0, 0, w, h); } /// VAL /// // The top color is hsv(h, s, 1) // The bottom color is always black. var $valSlider = $widget.find('.jQWCP-wValSlider'); if(!$valSlider.hasClass('hidden') || force) { var valTopRgb = $.fn.wheelColorPicker.hsvToRgb(H, S, 1); valTopRgb.r = Math.round(valTopRgb.r * 255); valTopRgb.g = Math.round(valTopRgb.g * 255); valTopRgb.b = Math.round(valTopRgb.b * 255); var valSliderCtx = $valSlider.get(0).getContext('2d'); var valGradient = valSliderCtx.createLinearGradient(0, 0, 0, h); valGradient.addColorStop(0, "rgb("+valTopRgb.r+","+valTopRgb.g+","+valTopRgb.b+")"); valGradient.addColorStop(1, "#000"); valSliderCtx.fillStyle = valGradient; valSliderCtx.fillRect(0, 0, w, h); } return this; // Allows method chaining }; /** * Function: updateSliders * * Introduced in 2.0 * * Update slider cursor positions based on this.color value. * Only displayed sliders are updated. This function shall be called when widget is displayed * so positions could be determined properly. */ WCP.ColorPicker.prototype.updateSliders = function() { // Skip if not yet initialized if(this.widget == null) return this; var $widget = $(this.widget); var color = this.color; // No need to redraw sliders on global popup widget if not // attached to the input elm in current iteration if(this != $widget.data('jQWCP.instance')) return this; // Wheel var $wheel = $widget.find('.jQWCP-wWheel'); if(!$wheel.hasClass('hidden')) { var $wheelCursor = $widget.find('.jQWCP-wWheelCursor'); var $wheelOverlay = $widget.find('.jQWCP-wWheelOverlay'); var wheelX = Math.cos(2 * Math.PI * color.h) * color.s; var wheelY = Math.sin(2 * Math.PI * color.h) * color.s; var wheelOffsetX = $wheel.width() / 2; var wheelOffsetY = $wheel.height() / 2; $wheelCursor.css('left', (wheelOffsetX + (wheelX * $wheel.width() / 2)) + 'px'); $wheelCursor.css('top', (wheelOffsetY - (wheelY * $wheel.height() / 2)) + 'px'); // Keep shading to 1 if preserveWheel is true (DEPRECATED) or live is true if(this.options.preserveWheel == true || (this.options.preserveWheel == null && this.options.live == false)) { $wheelOverlay.css('opacity', 0); } else { $wheelOverlay.css('opacity', 1 - (color.v < 0.2 ? 0.2 : color.v)); } } // Hue var $hueSlider = $widget.find('.jQWCP-wHueSlider'); if(!$hueSlider.hasClass('hidden')) { var $hueCursor = $widget.find('.jQWCP-wHueCursor'); $hueCursor.css('top', (color.h * $hueSlider.height()) + 'px'); } // Saturation var $satSlider = $widget.find('.jQWCP-wSatSlider'); if(!$satSlider.hasClass('hidden')) { var $satCursor = $widget.find('.jQWCP-wSatCursor'); $satCursor.css('top', ((1 - color.s) * $satSlider.height()) + 'px'); } // Value var $valSlider = $widget.find('.jQWCP-wValSlider'); if(!$valSlider.hasClass('hidden')) { var $valCursor = $widget.find('.jQWCP-wValCursor'); $valCursor.css('top', ((1 - color.v) * $valSlider.height()) + 'px'); } // Red var $redSlider = $widget.find('.jQWCP-wRedSlider'); if(!$redSlider.hasClass('hidden')) { var $redCursor = $widget.find('.jQWCP-wRedCursor'); $redCursor.css('top', ((1 - color.r) * $redSlider.height()) + 'px'); } // Green var $greenSlider = $widget.find('.jQWCP-wGreenSlider'); if(!$greenSlider.hasClass('hidden')) { var $greenCursor = $widget.find('.jQWCP-wGreenCursor'); $greenCursor.css('top', ((1 - color.g) * $greenSlider.height()) + 'px'); } // Blue var $blueSlider = $widget.find('.jQWCP-wBlueSlider'); if(!$blueSlider.hasClass('hidden')) { var $blueCursor = $widget.find('.jQWCP-wBlueCursor'); $blueCursor.css('top', ((1 - color.b) * $blueSlider.height()) + 'px'); } // Alpha var $alphaSlider = $widget.find('.jQWCP-wAlphaSlider'); if(!$alphaSlider.hasClass('hidden')) { var $alphaCursor = $widget.find('.jQWCP-wAlphaCursor'); $alphaCursor.css('top', ((1 - color.a) * $alphaSlider.height()) + 'px'); } return this; // Allows method chaining }; /** * Function: updateSelection * * DEPRECATED by 2.0 * * Update color dialog selection to match current selectedColor value. */ WCP.ColorPicker.prototype.updateSelection = function() { this.redrawSliders(); this.updateSliders(); return this; }; /** * Function: updateInput * * Since 3.0 * * Update input value and background color (if preview is on) */ WCP.ColorPicker.prototype.updateInput = function() { // Skip if not yet initialized if(this.widget == null) return this; var $input = $(this.input); // #13 only update if value is different to avoid cursor repositioned to the end of text on some browsers if(this.input.value != this.getValue()) { $input.attr('value', this.getValue()); // Force attribute to be updated this.input.value = this.getValue(); } $input.trigger('colorchange'); if( this.options.preview ) { $input.css('background', WCP.colorToStr( this.color, 'rgba' )); if( this.color.v > .5 ) { $input.css('color', 'black'); } else { $input.css('color', 'white'); } } }; /** * Function: updateActiveControl * * Move the active control. */ WCP.ColorPicker.prototype.updateActiveControl = function( e ) { var $control = $( $('body').data('jQWCP.activeControl') ); // Refers to slider wrapper if($control.length == 0) return; var $input = $(this.input); var options = this.options; var color = this.color; // pageX and pageY wrapper for touches if(e.originalEvent.touches && e.originalEvent.touches.length > 0) { e.pageX = e.originalEvent.touches[0].pageX; e.pageY = e.originalEvent.touches[0].pageY; } //$('#log').html(e.pageX + '/' + e.pageY); /// WHEEL CONTROL /// if($control.hasClass('jQWCP-wWheel')) { var $cursor = $control.find('.jQWCP-wWheelCursor'); var $overlay = $control.find('.jQWCP-wWheelOverlay'); var relX = (e.pageX - $control.offset().left - ($control.width() / 2)) / ($control.width() / 2); var relY = - (e.pageY - $control.offset().top - ($control.height() / 2)) / ($control.height() / 2); // BUG_RELATIVE_PAGE_ORIGIN workaround if(WCP.BUG_RELATIVE_PAGE_ORIGIN) { var relX = (e.pageX - ($control.get(0).getBoundingClientRect().left - WCP.ORIGIN.left) - ($control.width() / 2)) / ($control.width() / 2); var relY = - (e.pageY - ($control.get(0).getBoundingClientRect().top - WCP.ORIGIN.top) - ($control.height() / 2)) / ($control.height() / 2); } //console.log(relX + ' ' + relY); // Sat value is calculated from the distance of the cursor from the central point var sat = Math.sqrt(Math.pow(relX, 2) + Math.pow(relY, 2)); // If distance is out of bound, reset to the upper bound if(sat > 1) { sat = 1; } // Snap to 0,0 if(options.snap && sat < options.snapTolerance) { sat = 0; } // Hue is calculated from the angle of the cursor. 0deg is set to the right, and increase counter-clockwise. var hue = (relX == 0 && relY == 0) ? 0 : Math.atan( relY / relX ) / ( 2 * Math.PI ); // If y is 0 and x is negative, fix the angle value to 0.5 (atan() gives 0) if (relX < 0 && relY == 0) { hue = 0.5; } // If hue is negative, then fix the angle value (meaning angle is in either Q2 or Q4) if( hue < 0 ) { hue += 0.5; } // If y is negative, then fix the angle value (meaning angle is in either Q3 or Q4) if( relY < 0 ) { hue += 0.5; } this.setHsv(hue, sat, color.v); } /// SLIDER CONTROL /// else if($control.hasClass('jQWCP-slider-wrapper')) { var $cursor = $control.find('.jQWCP-scursor'); var relY = (e.pageY - $control.offset().top) / $control.height(); // BUG_RELATIVE_PAGE_ORIGIN workaround if(WCP.BUG_RELATIVE_PAGE_ORIGIN) { var relY = (e.pageY - ($control.get(0).getBoundingClientRect().top - WCP.ORIGIN.top)) / $control.height(); } var value = relY < 0 ? 0 : relY > 1 ? 1 : relY; // Snap to 0.0, 0.5, and 1.0 //console.log(value); if(options.snap && value < options.snapTolerance) { value = 0; } else if(options.snap && value > 1-options.snapTolerance) { value = 1; } if(options.snap && value > 0.5-options.snapTolerance && value < 0.5+options.snapTolerance) { value = 0.5; } $cursor.css('top', (value * $control.height()) + 'px'); /// Update color value /// // Red if($control.hasClass('jQWCP-wRed')) { this.setRgb(1-value, color.g, color.b); } // Green if($control.hasClass('jQWCP-wGreen')) { this.setRgb(color.r, 1-value, color.b); } // Blue if($control.hasClass('jQWCP-wBlue')) { this.setRgb(color.r, color.g, 1-value); } // Hue if($control.hasClass('jQWCP-wHue')) { this.setHsv(value, color.s, color.v); } // Saturation if($control.hasClass('jQWCP-wSat')) { this.setHsv(color.h, 1-value, color.v); } // Value if($control.hasClass('jQWCP-wVal')) { this.setHsv(color.h, color.s, 1-value); } // Alpha if($control.hasClass('jQWCP-wAlpha')) { this.setAlpha(1-value); } } }; /** * Function: getColor * * Since 2.0 * * Return color components as an object. The object consists of: * { * r: red * g: green * b: blue * h: hue * s: saturation * v: value * a: alpha * } */ WCP.ColorPicker.prototype.getColor = function() { return this.color; }; /** * Function: getValue * * Get the color value as string. */ WCP.ColorPicker.prototype.getValue = function( format ) { var options = this.options; if( format == null ) { format = options.format; } // If settings.rounding is TRUE, round alpha value to N decimal digits if(options.rounding >= 0) { this.color.a = Math.round(this.color.a * Math.pow(10, options.rounding)) / Math.pow(10, options.rounding); } return WCP.colorToStr( this.color, format ); }; /** * Function: setValue * * Set the color value as string. * * Parameters: * value - String representation of color. * updateInput - Whether to update input text. Default is true. */ WCP.ColorPicker.prototype.setValue = function( value, updateInput ) { var color = WCP.strToColor(value); if(!color) return this; return this.setColor(color, updateInput); } /** * Function: setColor * * Introduced in 2.0 * * Set color by passing an object consisting of: * { r, g, b, a } or * { h, s, v, a } * * For consistency with options.color value, this function also * accepts string value. * * Parameters: * color - Color object or string representation of color. * updateInput - Whether to update input text. Default is true. */ WCP.ColorPicker.prototype.setColor = function( color, updateInput ) { if(typeof color === "string") { return this.setValue(color, updateInput); } else if(color.r != null) { return this.setRgba(color.r, color.g, color.b, color.a, updateInput); } else if(color.h != null) { return this.setHsva(color.h, color.s, color.v, color.a, updateInput); } else if(color.a != null) { return this.setAlpha(color.a, updateInput); } return this; }; /** * Function: setRgba * * Introduced in 2.0 * * Set color using RGBA combination. * * Parameters: * r - Red component [0..1] * g - Green component [0..1] * b - Blue component [0..1] * a - Alpha value [0..1] * updateInput - Whether to update input text. Default is true. */ WCP.ColorPicker.prototype.setRgba = function( r, g, b, a, updateInput ) { // Default value if(updateInput === undefined) updateInput = true; var color = this.color; color.r = r; color.g = g; color.b = b; if(a != null) { color.a = a; } var hsv = WCP.rgbToHsv(r, g, b); color.h = hsv.h; color.s = hsv.s; color.v = hsv.v; this.updateSliders(); this.redrawSliders(); if(updateInput) { this.updateInput(); } return this; }; /** * Function: setRgb * * Introduced in 2.0 * * Set color using RGB combination. * * Parameters: * r - Red component [0..1] * g - Green component [0..1] * b - Blue component [0..1] * updateInput - Whether to update input text. */ WCP.ColorPicker.prototype.setRgb = function( r, g, b, updateInput ) { return this.setRgba(r, g, b, null, updateInput); }; /** * Function: setHsva * * Introduced in 2.0 * * Set color using HSVA combination. * * Parameters: * h - Hue component [0..1] * s - Saturation component [0..1] * v - Value component [0..1] * a - Alpha value [0..1] * updateInput - Whether to update input text. Default is true. */ WCP.ColorPicker.prototype.setHsva = function( h, s, v, a, updateInput ) { // Default value if(updateInput === undefined) updateInput = true; var color = this.color; color.h = h; color.s = s; color.v = v; if(a != null) { color.a = a; } var rgb = WCP.hsvToRgb(h, s, v); color.r = rgb.r; color.g = rgb.g; color.b = rgb.b; this.updateSliders(); this.redrawSliders(); if(updateInput) { this.updateInput(); } return this; }; /** * Function: setHsv * * Introduced in 2.0 * * Set color using HSV combination. * * Parameters: * h - Hue component [0..1] * s - Saturation component [0..1] * v - Value component [0..1] * updateInput - Whether to update input text. */ WCP.ColorPicker.prototype.setHsv = function( h, s, v, updateInput ) { return this.setHsva(h, s, v, null, updateInput); }; /** * Function: setAlpha * * Introduced in 2.0 * * Set alpha value. * * Parameters: * value - Alpha value [0..1] * updateInput - Whether to update input text. Default is true. */ WCP.ColorPicker.prototype.setAlpha = function( value, updateInput ) { // Default value if(updateInput === undefined) updateInput = true; this.color.a = value; this.updateSliders(); this.redrawSliders(); if (updateInput) { this.updateInput(); } return this; }; /** * Function: show * * Show the color picker dialog. This function is only applicable to * popup mode color picker layout. */ WCP.ColorPicker.prototype.show = function() { var input = this.input; var $input = $(input); // Refers to input elm var $widget = $(this.widget); var options = this.options; // Don't do anything if not using popup layout if( options.layout != "popup" ) return; // Don't do anything if the popup is already shown and attached // to the correct input elm //if( this == $widget.data('jQWCP.instance') ) //return; // Attach instance to widget (because popup widget is global) $widget.data('jQWCP.instance', this); // Terminate ongoing transitions $widget.stop( true, true ); // Reposition the popup window $widget.css({ top: (input.getBoundingClientRect().top - WCP.ORIGIN.top + $input.outerHeight()) + 'px', left: (input.getBoundingClientRect().left - WCP.ORIGIN.left) + 'px' }); // Refresh widget with this instance's options this.refreshWidget(); // Redraw sliders this.redrawSliders(); // Store last textfield value (to determine whether to trigger onchange event later) this.lastValue = input.value; $widget.fadeIn( options.animDuration ); // Update slider positions after widget is displayed (so it could calculate properly) this.updateSliders(); // If hideKeyboard is true, force to hide soft keyboard if(options.hideKeyboard) { $input.blur(); $(WCP.ColorPicker.overlay).show(); } // On mobile layout, autoscroll page to keep input visible if($widget.hasClass('jQWCP-mobile')) { var scrollTop = $('html').scrollTop(); var inputTop = input.getBoundingClientRect().top - WCP.ORIGIN.top; // If input is way too top if(inputTop < scrollTop) { $('html').animate({ scrollTop: inputTop}); } // If input is way too bottom else if(inputTop + $input.outerHeight() > scrollTop + window.innerHeight - $widget.outerHeight()) { $('html').animate({ scrollTop: inputTop + $input.outerHeight() - window.innerHeight + $widget.outerHeight()}); } } }; /** * Function: hide * * Hide the color picker dialog. This function is only applicable to * popup mode color picker layout. */ WCP.ColorPicker.prototype.hide = function() { var $widget = $(this.widget); // Only hide if popup belongs to this instance if(this != $widget.data('jQWCP.instance')) return; $widget.fadeOut( this.options.animDuration ); $(WCP.ColorPicker.overlay).hide(); }; //////////////////// // Event Handlers // //////////////////// WCP.Handler = {}; /** * input.onFocus.popup */ WCP.Handler.input_focus_popup = function( e ) { var instance = $(this).data('jQWCP.instance'); instance.show(); // Workaround to prevent on screen keyboard from appearing if($(this).attr('readonly') == null) { $(this).attr('readonly', true); setTimeout(function() { $(instance.input).removeAttr('readonly'); }); // Firefox on Android if(navigator.userAgent.match(/Android .* Firefox/) != null) { setTimeout(function() { $(instance.input).attr('readonly', true); $(instance.input).one('blur', function() { $(instance.input).removeAttr('readonly'); }); }); } } }; /** * input.onBlur.popup * * onBlur event handler for popup layout. */ WCP.Handler.input_blur_popup = function( e ) { var instance = $(this).data('jQWCP.instance'); // If keyboard is hidden, input is always blurred so // no point in hiding if(instance.options.hideKeyboard) return; instance.hide(); // Trigger 'change' event only when it was modified by widget // because user typing on the textfield will automatically // trigger 'change' event on blur. if(instance.lastValue != this.value) { $(this).trigger('change'); } }; /** * input.onKeyUp * * Update the color picker while user type in color value. */ WCP.Handler.input_keyup = function( e ) { var instance = $(this).data('jQWCP.instance'); var color = WCP.strToColor(this.value); if(color) { instance.setColor(color, false); } }; /** * input.onChange * * Reformat the input value based on selected color and configurations. */ WCP.Handler.input_change = function( e ) { var instance = $(this).data('jQWCP.instance'); var color = WCP.strToColor(this.value); // If autoFormat option is enabled, try to reformat the value if it is a valid color if(instance.options.autoFormat && color) { instance.setColor(color, true); } // If validate option is enabled, revert the value if it is not a valid color else if(instance.options.validate && !color && this.value != '') { this.value = instance.getValue(); } }; /** * widget.onFocus.block * * Prepare runtime widget data */ WCP.Handler.widget_focus_block = function( e ) { var instance = $(this).data('jQWCP.instance'); var $input = $(instance.input); // Store last textfield value instance.lastValue = instance.input.value; // Trigger focus event $input.triggerHandler('focus'); }; /** * widget.onMouseDown.popup * * Prevent loss focus of the input causing the dialog to be hidden * because of input blur event. */ WCP.Handler.widget_mousedown_popup = function( e ) { e.preventDefault(); var instance = $(this).data('jQWCP.instance'); var $input = $(instance.input); // Temporarily unbind blur and focus event until mouse is released $input.off('focus.wheelColorPicker'); $input.off('blur.wheelColorPicker'); // Temporarily unbind all blur events until mouse is released // data('events') is deprecated since jquery 1.8 if($input.data('events') != undefined) { var blurEvents = $input.data('events').blur; } else { var blurEvents = undefined; } var suspendedEvents = { blur: [] }; //suspendedEvents.blur = blurEvents; //$input.off('blur'); if(blurEvents != undefined) { for(var i = 0; i < blurEvents.length; i++) { suspendedEvents.blur.push(blurEvents[i]); //suspendedEvents.blur['blur' + (blurEvents[i].namespace != '' ? blurEvents[i].namespace : '')] = blurEvents[i].handler; } } $input.data('jQWCP.suspendedEvents', suspendedEvents); //console.log(blurEvents); //console.log($input.data('jQWCP.suspendedEvents')); }; /** * widget.onMouseUp * * Re-bind events that was unbound by widget_mousedown_popup. */ /*WCP.Handler.widget_mouseup_popup = function( e ) { var instance = $(this).data('jQWCP.instance'); var $input = $(instance.input); //Input elm must always be focused, unless hideKeyboard is set to true if(!instance.options.hideKeyboard) { $input.trigger('focus.jQWCP_DONT_TRIGGER_EVENTS'); // This allow input to be focused without triggering events } //Rebind blur & focusevent $input.on('focus.wheelColorPicker', WCP.Handler.input_focus_popup); $input.on('blur.wheelColorPicker', WCP.Handler.input_blur_popup); };*/ /** * widget.onBlur * * Try to trigger onChange event if value has been changed. */ WCP.Handler.widget_blur_block = function( e ) { var instance = $(this).data('jQWCP.instance'); var $input = $(instance.input); // Trigger 'change' event only when it was modified by widget // because user typing on the textfield will automatically // trigger 'change' event on blur. if(instance.lastValue != instance.input.value) { $input.trigger('change'); } // Trigger blur event $input.triggerHandler('blur'); }; /** * wheelCursor.onMouseDown * * Begin clicking the wheel down. This will allow user to move * the crosshair although the mouse is outside the wheel. */ WCP.Handler.wheelCursor_mousedown = function( e ) { e.preventDefault(); var $this = $(this); // Refers to cursor var $widget = $this.closest('.jQWCP-wWidget'); var instance = $widget.data('jQWCP.instance'); var $input = $(instance.input); $('body').data('jQWCP.activeControl', $this.parent().get(0)); // Trigger sliderdown event $input.trigger('sliderdown'); }; /** * wheel.onMouseDown * * Begin clicking the wheel down. This will allow user to move * the crosshair although the mouse is outside the wheel. * * Basically this is the same as wheelCursor_mousedown handler */ WCP.Handler.wheel_mousedown = function( e ) { e.preventDefault(); var $this = $(this); // Refers to wheel var $widget = $this.closest('.jQWCP-wWidget'); var instance = $widget.data('jQWCP.instance'); var $input = $(instance.input); $('body').data('jQWCP.activeControl', $this.get(0)); // Trigger sliderdown event $input.trigger('sliderdown'); instance.updateActiveControl( e ); }; /** * slider.onMouseDown * * Begin clicking the slider down. This will allow user to move * the slider although the mouse is outside the slider. */ WCP.Handler.slider_mousedown = function( e ) { e.preventDefault(); var $this = $(this); // Refers to slider var $widget = $this.closest('.jQWCP-wWidget'); var instance = $widget.data('jQWCP.instance'); var $input = $(instance.input); $('body').data('jQWCP.activeControl', $this.parent().get(0)); // Trigger sliderdown event $input.trigger('sliderdown'); instance.updateActiveControl( e ); }; /** * sliderCursor.onMouseDown * * Begin clicking the slider down. This will allow user to move * the slider although the mouse is outside the slider. */ WCP.Handler.sliderCursor_mousedown = function( e ) { e.preventDefault(); var $this = $(this); // Refers to slider cursor var $widget = $this.closest('.jQWCP-wWidget'); var instance = $widget.data('jQWCP.instance'); var $input = $(instance.input); $('body').data('jQWCP.activeControl', $this.parent().get(0)); // Trigger sliderdown event $input.trigger('sliderdown'); }; /** * html.onMouseUp * * Clear active control reference. * Also do cleanups after widget.onMouseDown.popup * * Note: This event handler is also applied to touchend */ WCP.Handler.html_mouseup = function( e ) { var $control = $( $('body').data('jQWCP.activeControl') ); // Refers to slider wrapper or wheel // Do stuffs when there's active control if($control.length == 0) return; var $widget = $control.closest('.jQWCP-wWidget'); var instance = $widget.data('jQWCP.instance'); var $input = $(instance.input); // Rebind blur and focus event to input elm which was // temporarily released when popup dialog is shown if(instance.options.layout == 'popup') { // Focus first before binding event so it wont get fired // Input elm must always be focused, unless hideKeyboard is set to true if(!instance.options.hideKeyboard) { $input.trigger('focus.jQWCP_DONT_TRIGGER_EVENTS'); // This allow input to be focused without triggering events } // Rebind blur & focusevent $input.on('focus.wheelColorPicker', WCP.Handler.input_focus_popup); $input.on('blur.wheelColorPicker', WCP.Handler.input_blur_popup); // Rebind suspended events var suspendedEvents = $input.data('jQWCP.suspendedEvents'); if(suspendedEvents != undefined) { var blurEvents = suspendedEvents.blur; for(var i = 0; i < blurEvents.length; i++) { $input.on('blur' + (blurEvents[i].namespace == '' ? '' : '.' + blurEvents[i].namespace), blurEvents[i].handler); } } } // Update active control if($control.length != 0) { // Last time update active control before clearing // Only call this function if mouse position is known // On touch action, touch point is not available if (e.type != 'touchend') { instance.updateActiveControl( e ); } // Clear active control reference $('body').data('jQWCP.activeControl', null); // Trigger sliderup event $input.trigger('sliderup'); } }; /** * html.onMouseMove * * Move the active slider (when mouse click is down). * * Note: This event handler is also applied to touchmove */ WCP.Handler.html_mousemove = function( e ) { var $control = $( $('body').data('jQWCP.activeControl') ); // Refers to slider wrapper or wheel // Do stuffs when there's active control if($control.length == 0) return; // If active, prevent default e.preventDefault(); var $widget = $control.closest('.jQWCP-wWidget'); var instance = $widget.data('jQWCP.instance'); var $input = $(instance.input); instance.updateActiveControl( e ); // Trigger slidermove event $input.trigger('slidermove'); return false; }; /** * window.onResize * * Adjust block widgets */ WCP.Handler.window_resize = function( e ) { var $widgets = $('body .jQWCP-wWidget.jQWCP-block'); $widgets.each(function() { var instance = $(this).data('jQWCP.instance'); instance.refreshWidget(); instance.redrawSliders(); }); }; /** * overlay.onClick * * Hide colorpicker popup dialog if overlay is clicked. * This has the same effect as blurring input element if hideKeyboard = false. */ WCP.Handler.overlay_click = function( e ) { if(WCP.ColorPicker.widget == null) return; var $widget = $(WCP.ColorPicker.widget); var instance = $widget.data('jQWCP.instance'); // If no instance set, do nothing if(instance != null) { var $input = $(instance.input); // Trigger 'change' event only when it was modified by widget // because user typing on the textfield will automatically // trigger 'change' event on blur. if(instance.lastValue != instance.input.value) { $input.trigger('change'); } instance.hide(); } }; /******************************************************************/ //////////////////////////////////////////////////////// // Automatically initialize color picker on page load // // for elements with data-wheelcolorpicker attribute. // //////////////////////////////////////////////////////// $(document).ready(function() { $('[data-wheelcolorpicker]').wheelColorPicker({ htmlOptions: true }); }); /******************************************************************/ ////////////////////////////////// // Browser specific workarounds // ////////////////////////////////// (function() { // MOZILLA // // Force low resolution slider canvases to improve performance // Note: Do not rely on $.browser since it's obsolete by jQuery 2.x if($.browser != undefined && $.browser.mozilla) { $.fn.wheelColorPicker.defaults.quality = 0.2; } // MOBILE CHROME // // BUG_RELATIVE_PAGE_ORIGIN // Calibrate the coordinate of top left point of the page // On mobile chrome, the top left of the page is not always set at (0,0) // making window.scrollX/Y and $.offset() useless $(document).ready(function() { $('body').append( '
' ); var origin = document.getElementById('jQWCP-PageOrigin').getBoundingClientRect(); WCP.ORIGIN = origin; $(window).on('scroll.jQWCP_RelativePageOriginBugFix', function() { var origin = document.getElementById('jQWCP-PageOrigin').getBoundingClientRect(); WCP.ORIGIN = origin; if(origin.left != 0 || origin.top != 0) { WCP.BUG_RELATIVE_PAGE_ORIGIN = true; } }); }); })(); }) (jQuery);