UNPKG

vue-color-picker-wheel

Version:

A Vue color picker component based on the Farbtastic jQuery Color Picker plug-in

738 lines (681 loc) 28.6 kB
function isIE () { var userAgent = window.navigator.userAgent; return /MSIE|Trident/.test(userAgent); } // var DEFAULT_WIDTH_HEIGHT = 300; var DEFAULT_START_COLOR = '#000000'; var script = { name: 'color-picker', props: { width: { required: false, type: Number, default: DEFAULT_WIDTH_HEIGHT }, height: { required: false, type: Number, default: DEFAULT_WIDTH_HEIGHT }, disabled: { required: false, type: Boolean, default: false }, startColor: { required: false, type: String, }, value: { required: false, type: String } }, mounted: function mounted() { /** * @deprecated since: 0.4.0, remove in: 1.0.0, https://github.com/stijlbreuk/vue-color-picker-wheel/issues/6 */ if (this.hasCamelCaseColorChangeListener) { console.warn("Using the colorChange event is deprecated since version 0.4.0. It will be deleted in version 1.0.0. 'v-model' or the kebab-case variant 'color-change' should be used."); } this.initWidget(); this.setColor(this.value || this.startColor || DEFAULT_START_COLOR); }, data: function data() { return { debug: false, dragging: false, circleDrag: false, color: '', rgb: '', hsl: '', radius: '', square: '', mid: '', markerSize: '', ctxMask: '', ctxOverlay: '', cnvMask: '', cnvOverlay: '', offset: { left: '', top: '' } }; }, watch: { value: function value(newVal, oldVal) { if (newVal !== oldVal) { this.setColor(newVal); } } }, computed: { /** * @deprecated since: 0.4.0, remove in: 1.0.0, https://github.com/stijlbreuk/vue-color-picker-wheel/issues/6 */ hasCamelCaseColorChangeListener: function hasCamelCaseColorChangeListener() { return this.$listeners && this.$listeners.colorChange; }, solidStyle: function solidStyle() { return { 'background-color': this.pack(this.HSLToRGB([this.hsl[0], 1, 0.5])), width: ((this.square * 2 - 1) + "px"), height: ((this.square * 2 - 1) + "px"), left: ((this.mid - this.square) + "px"), top: ((this.mid - this.square) + "px") }; }, wheelWidth: function wheelWidth() { return (this.width || 300) / 10; } }, methods: { setColor: function setColor(color, noEmit) { if ( noEmit === void 0 ) noEmit = false; var unpack = this.unpack(color); if (this.color !== color && unpack) { this.color = color; this.rgb = unpack; this.hsl = this.RGBToHSL(this.rgb); this.updateDisplay(noEmit); } return this; }, setHSL: function setHSL(hsl) { this.hsl = hsl; this.rgb = this.HSLToRGB(hsl); this.color = this.pack(this.rgb); this.updateDisplay(); return this; }, initWidget: function initWidget() { // Determine layout this.radius = (this.width - this.wheelWidth) / 2 - 1; this.square = Math.floor((this.radius - this.wheelWidth / 2) * 0.7) - 1; this.mid = Math.floor(this.width / 2); this.markerSize = this.wheelWidth * 0.3; // Set up drawing context. this.cnvMask = this.$refs['farbtastic-mask']; this.ctxMask = this.cnvMask.getContext('2d'); this.cnvOverlay = this.$refs['farbtastic-overlay']; this.ctxOverlay = this.cnvOverlay.getContext('2d'); this.devicePixelRatio = window.devicePixelRatio || 1; this.upscaleCanvas(this.cnvMask); this.upscaleCanvas(this.cnvOverlay); this.ctxMask.translate(this.mid, this.mid); this.ctxOverlay.translate(this.mid, this.mid); // Draw widget base layers. this.drawCircle(); this.drawMask(); }, upscaleCanvas: function upscaleCanvas(cnv) { var ctx = cnv.getContext('2d'); var backingStoreRatio = ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1; if (this.devicePixelRatio !== backingStoreRatio) { var ratio = this.devicePixelRatio / backingStoreRatio; var oldWidth = cnv.width; var oldHeight = cnv.height; cnv.width = oldWidth * ratio; cnv.height = oldHeight * ratio; cnv.style.width = oldWidth + "px"; cnv.style.height = oldHeight + "px"; ctx.scale(ratio, ratio); } }, drawCircle: function drawCircle() { var tm = +(new Date()); // Draw a hue circle with a bunch of gradient-stroked beziers. // Have to use beziers, as gradient-stroked arcs don't work. var n = 24; var r = this.radius; var w = this.wheelWidth; var nudge = (8 / r / n) * Math.PI; // Fudge factor for seams. var m = this.ctxMask; var angle1 = 0; var angle2; // let d1; var color1; var color2; m.save(); m.lineWidth = w / r; m.scale(r, r); // Each segment goes from angle1 to angle2. // eslint-disable-next-line for (var i = 0; i <= n; ++i) { var d2 = i / n; angle2 = d2 * Math.PI * 2; // Endpoints var x1 = Math.sin(angle1); var y1 = -Math.cos(angle1); var x2 = Math.sin(angle2); var y2 = -Math.cos(angle2); // Midpoint chosen so that the endpoints are tangent to the circle. var am = (angle1 + angle2) / 2; var tan = 1 / Math.cos((angle2 - angle1) / 2); var xm = Math.sin(am) * tan; var ym = -Math.cos(am) * tan; // New color color2 = this.pack(this.HSLToRGB([d2, 1, 0.5])); if (i > 0) { // Create gradient fill between the endpoints. var grad = m.createLinearGradient(x1, y1, x2, y2); grad.addColorStop(0, color1); grad.addColorStop(1, color2); m.strokeStyle = grad; // Draw quadratic curve segment. m.beginPath(); m.moveTo(x1, y1); m.quadraticCurveTo(xm, ym, x2, y2); m.stroke(); } // Prevent seams where curves join. angle1 = angle2 - nudge; color1 = color2; // d1 = d2; } m.restore(); if (this.debug) { var debugElement = document.createElement('div'); debugElement.textContent = "drawCircle " + ((+(new Date()) - tm)) + " ms"; document.body.appendChild(debugElement); } }, drawMask: function drawMask() { var this$1 = this; var tm = +(new Date()); // Iterate over sat/lum space and calculate appropriate mask pixel values. var size = this.square * 2; var sq = this.square; function calculateMask(sizex, sizey, outputPixel) { var isx = 1 / sizex; var isy = 1 / sizey; // eslint-disable-next-line for (var y = 0; y <= sizey; ++y) { var l = 1 - y * isy; // eslint-disable-next-line for (var x = 0; x <= sizex; ++x) { var s = 1 - x * isx; // From sat/lum to alpha and color (grayscale) var a = 1 - 2 * Math.min(l * s, (1 - l) * s); var c = a > 0 ? (2 * l - 1 + a) * (0.5 / a) : 0; outputPixel(x, y, c, a); } } } // Method #1: direct pixel access (new Canvas). if (this.ctxMask.getImageData) { // Create half-resolution buffer. var sz = Math.floor(size / 2); var buffer = document.createElement('canvas'); buffer.width = sz + 1; buffer.height = sz + 1; var ctx = buffer.getContext('2d'); var frame = ctx.getImageData(0, 0, sz + 1, sz + 1); var i = 0; calculateMask(sz, sz, function (x, y, c, a) { // eslint-disable-next-line frame.data[i++] = frame.data[i++] = frame.data[i++] = c * 255; // eslint-disable-next-line frame.data[i++] = a * 255; }); ctx.putImageData(frame, 0, 0); this.ctxMask.drawImage( buffer, 0, 0, sz + 1, sz + 1, -sq, -sq, sq * 2, sq * 2 ); } else if (!isIE()) { // Render directly at half-resolution var sz$1 = Math.floor(size / 2); calculateMask(sz$1, sz$1, function (x, y, _c, a) { var c = Math.round(_c * 255); this$1.ctxMask.fillStyle = "rgba(" + c + ", " + c + ", " + c + ", " + a + ")"; this$1.ctxMask.fillRect(x * 2 - sq - 1, y * 2 - sq - 1, 2, 2); }); } else { var cacheLast; var cache; var w = 6; // Each strip is 6 pixels wide. var sizex = Math.floor(size / w); // 6 vertical pieces of gradient per strip. calculateMask(sizex, 6, function (x, y, c, a) { if (x === 0) { cacheLast = cache; cache = []; } c = Math.round(c * 255); a = Math.round(a * 255); // We can only start outputting gradients // once we have two rows of pixels. if (y > 0) { var cLast = cacheLast[x][0]; var aLast = cacheLast[x][1]; var color1 = this$1.packDX(cLast, aLast); var color2 = this$1.packDX(c, a); var y1 = Math.round(this$1.mid + ((y - 1) * 0.333 - 1) * sq); var y2 = Math.round(this$1.mid + (y * 0.333 - 1) * sq); // Append div to canvasMask var div = document.createElement('div'); div.style.position = 'absolute'; div.style.filter = "progid:DXImageTransform.Microsoft.Gradient(StartColorStr=" + color1 + ", EndColorStr=" + color2 + ", GradientType=0)"; div.style.top = y1; div.style.height = y2 - y1; div.style.left = this$1.mid + (x * w - sq - 1); div.style.width = this$1.mid + (x * w - sq - 1); this$1.cnvMask.appendChild(div); } cache.push([c, a]); }); } if (this.debug) { var debugElement = document.createElement('div'); debugElement.textContent = "drawMask " + ((+(new Date()) - tm)) + " ms"; document.body.appendChild(debugElement); } }, drawMarkers: function drawMarkers() { // Determine marker dimensions var sz = this.width; var lw = Math.ceil(this.markerSize / 4); var r = this.markerSize - lw + 1; var angle = this.hsl[0] * 6.28; var x1 = Math.sin(angle) * this.radius; var y1 = -Math.cos(angle) * this.radius; var x2 = 2 * this.square * (0.5 - this.hsl[1]); var y2 = 2 * this.square * (0.5 - this.hsl[2]); var c1 = this.invert ? '#fff' : '#000'; var c2 = this.invert ? '#000' : '#fff'; var circles = [ { x: x1, y: y1, r: r, c: '#000', lw: lw + 1 }, { x: x1, y: y1, r: this.markerSize, c: '#fff', lw: lw }, { x: x2, y: y2, r: r, c: c2, lw: lw + 1 }, { x: x2, y: y2, r: this.markerSize, c: c1, lw: lw } ]; // Update the overlay canvas. this.ctxOverlay.clearRect(-this.mid, -this.mid, sz, sz); for (var i = 0; i < circles.length; i += 1) { var c = circles[i]; this.ctxOverlay.lineWidth = c.lw; this.ctxOverlay.strokeStyle = c.c; this.ctxOverlay.beginPath(); this.ctxOverlay.arc(c.x, c.y, c.r, 0, Math.PI * 2, true); this.ctxOverlay.stroke(); } }, updateDisplay: function updateDisplay(noEmit) { // Determine whether labels/markers should invert. this.invert = this.rgb[0] * 0.3 + this.rgb[1] * 0.59 + this.rgb[2] * 0.11 <= 0.6; // Draw markers this.drawMarkers(); if (!noEmit) { // Emit color this.$emit('input', this.color); /** * @deprecated since: 0.4.0, remove in: 1.0.0, https://github.com/stijlbreuk/vue-color-picker-wheel/issues/6 */ this.$emit('colorChange', this.color); this.$emit('color-change', this.color); } }, widgetCoords: function widgetCoords(event) { return { x: event.clientX - this.offset.left - this.mid, y: event.clientY - this.offset.top - this.mid }; }, mousedown: function mousedown(event) { if (this.disabled) { return false; } // Capture mouse if (!this.dragging) { document.addEventListener('mousemove', this.mousemove); document.addEventListener('mouseup', this.mouseup); this.dragging = true; } // Update the stored offset for the widget. this.offset = { left: this.$refs['color-wheel'].getBoundingClientRect().left, top: this.$refs['color-wheel'].getBoundingClientRect().top }; // Check which area is being dragged var pos = this.widgetCoords(event); this.circleDrag = Math.max(Math.abs(pos.x), Math.abs(pos.y)) > this.square + 2; // Process this.mousemove(event); return false; }, mousemove: function mousemove(event) { // Get coordinates relative to color picker center var pos = this.widgetCoords(event); // Set new HSL parameters if (this.circleDrag) { var hue = Math.atan2(pos.x, -pos.y) / 6.28; this.setHSL([(hue + 1) % 1, this.hsl[1], this.hsl[2]]); } else { var sat = Math.max(0, Math.min(1, -(pos.x / this.square / 2) + 0.5)); var lum = Math.max(0, Math.min(1, -(pos.y / this.square / 2) + 0.5)); this.setHSL([this.hsl[0], sat, lum]); } return false; }, mouseup: function mouseup() { // Uncapture mouse document.removeEventListener('mousemove', this.mousemove); document.removeEventListener('mouseup', this.mouseup); this.dragging = false; }, /* Constious color utility functions */ dec2hex: function dec2hex(x) { return (x < 16 ? '0' : '') + x.toString(16); }, packDX: function packDX(c, a) { return ("#" + (this.dec2hex(a) + this.dec2hex(c) + this.dec2hex(c) + this.dec2hex(c))); }, pack: function pack(rgb) { var r = Math.round(rgb[0] * 255); var g = Math.round(rgb[1] * 255); var b = Math.round(rgb[2] * 255); return ("#" + (this.dec2hex(r) + this.dec2hex(g) + this.dec2hex(b))); }, unpack: function unpack(color) { if (color.length === 7) { return [1, 3, 5].map( function (i) { return parseInt(color.substring(i, i + 2), 16) / 255; } ); } else if (color.length === 4) { return [1, 2, 3].map(function (i) { return parseInt(color.substring(i, i + 1), 16) / 15; }); } return false; }, HSLToRGB: function HSLToRGB(hsl) { var h = hsl[0]; var s = hsl[1]; var l = hsl[2]; var m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s; var m1 = l * 2 - m2; return [ this.hueToRGB(m1, m2, h + 0.33333), this.hueToRGB(m1, m2, h), this.hueToRGB(m1, m2, h - 0.33333) ]; }, hueToRGB: function hueToRGB(m1, m2, h) { h = (h + 1) % 1; if (h * 6 < 1) { return m1 + (m2 - m1) * h * 6; } if (h * 2 < 1) { return m2; } if (h * 3 < 2) { return m1 + (m2 - m1) * (0.66666 - h) * 6; } return m1; }, RGBToHSL: function RGBToHSL(rgb) { var r = rgb[0]; var g = rgb[1]; var b = rgb[2]; var min = Math.min(r, g, b); var max = Math.max(r, g, b); var delta = max - min; var h = 0; var s = 0; var l = (min + max) / 2; if (l > 0 && l < 1) { s = delta / (l < 0.5 ? 2 * l : 2 - 2 * l); } if (delta > 0) { if (max === r && max !== g) { h += (g - b) / delta; } if (max === g && max !== b) { h += 2 + (b - r) / delta; } if (max === b && max !== r) { h += 4 + (r - g) / delta; } h /= 6; } return [h, s, l]; }, /** * Helper for returning coordinates relative to the center with touch event */ widgetCoordsTouch: function widgetCoordsTouch(event) { return { x: event.targetTouches[0].clientX - this.offset.left - this.mid, y: event.targetTouches[0].clientY - this.offset.top - this.mid }; }, /** * Handle the touchstart events */ touchHandleStart: function touchHandleStart(event) { // Ignore the event if another is already being handled if (this.touchHandled) { return; } // Set the flag to prevent others from inheriting the touch event this.touchHandled = true; // Track movement to determine if interaction was a click this._touchMoved = false; // Update the stored offset for the widget. this.offset = { left: this.$refs['color-wheel'].getBoundingClientRect().left, top: this.$refs['color-wheel'].getBoundingClientRect().top }; // Check which area is being dragged var pos = this.widgetCoordsTouch(event); this.circleDrag = Math.max(Math.abs(pos.x), Math.abs(pos.y)) > this.square + 2; }, /** * Handle the touchstart events */ touchHandleMove: function touchHandleMove(event) { // Ignore event if not handled if (!this.touchHandled) { return; } event.preventDefault(); // Interaction was not a click this._touchMoved = true; // Get coordinates relative to color picker center var pos = this.widgetCoordsTouch(event); // Set new HSL parameters if (this.circleDrag) { var hue = Math.atan2(pos.x, -pos.y) / 6.28; this.setHSL([(hue + 1) % 1, this.hsl[1], this.hsl[2]]); } else { var sat = Math.max(0, Math.min(1, -(pos.x / this.square / 2) + 0.5)); var lum = Math.max(0, Math.min(1, -(pos.y / this.square / 2) + 0.5)); this.setHSL([this.hsl[0], sat, lum]); } }, /** * Handle the touchstart events */ touchHandleEnd: function touchHandleEnd() { // Ignore event if not handled if (!this.touchHandled) { return; } // Unset the flag to allow other widgets to inherit the touch event this.touchHandled = false; } } }; /* script */ var __vue_script__ = script; /* template */ var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{ref:"color-wheel",staticClass:"cpw_container",class:{s_disabled: _vm.disabled},style:({width: (_vm.width + "px"), height: (_vm.height + "px"), position: 'relative'}),attrs:{"id":"color-wheel"}},[_c('div',{ref:"farbtastic-solid",staticClass:"farbtastic-solid",staticStyle:{"position":"absolute"},style:(_vm.solidStyle)}),_vm._v(" "),_c('canvas',{ref:"farbtastic-mask",staticClass:"farbtastic-mask",style:({width: _vm.width, height: _vm.height}),attrs:{"width":_vm.width,"height":_vm.height}}),_vm._v(" "),_c('canvas',{ref:"farbtastic-overlay",staticClass:"farbtastic-overlay",style:({width: _vm.width, height: _vm.height}),attrs:{"width":_vm.width,"height":_vm.height},on:{"mousedown":_vm.mousedown,"touchstart":_vm.touchHandleStart,"touchmove":_vm.touchHandleMove,"touchend":_vm.touchHandleEnd}})])}; var __vue_staticRenderFns__ = []; /* style */ var __vue_inject_styles__ = function (inject) { if (!inject) { return } inject("data-v-58e7c5c8_0", { source: "\n.s_disabled[data-v-58e7c5c8]{opacity:.5\n}\n.cpw_container[data-v-58e7c5c8]{-webkit-touch-callout:none;-webkit-text-size-adjust:none;-moz-text-size-adjust:none;-ms-text-size-adjust:none;text-size-adjust:none;tap-highlight-color:transparent;tap-highlight-color:transparent;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none\n}\n.cpw_container .farbtastic-mask[data-v-58e7c5c8]{position:absolute;left:0\n}\n.cpw_container .farbtastic-overlay[data-v-58e7c5c8]{position:absolute;left:0\n}", map: undefined, media: undefined }); }; /* scoped */ var __vue_scope_id__ = "data-v-58e7c5c8"; /* module identifier */ var __vue_module_identifier__ = undefined; /* functional template */ var __vue_is_functional_template__ = false; /* component normalizer */ function __vue_normalize__( template, style, script$$1, scope, functional, moduleIdentifier, createInjector, createInjectorSSR ) { var component = (typeof script$$1 === 'function' ? script$$1.options : script$$1) || {}; // For security concerns, we use only base name in production mode. component.__file = "color-picker.vue"; if (!component.render) { component.render = template.render; component.staticRenderFns = template.staticRenderFns; component._compiled = true; if (functional) { component.functional = true; } } component._scopeId = scope; { var hook; if (style) { hook = function(context) { style.call(this, createInjector(context)); }; } if (hook !== undefined) { if (component.functional) { // register for functional component in vue file var originalRender = component.render; component.render = function renderWithStyleInjection(h, context) { hook.call(context); return originalRender(h, context) }; } else { // inject component registration as beforeCreate hook var existing = component.beforeCreate; component.beforeCreate = existing ? [].concat(existing, hook) : [hook]; } } } return component } /* style inject */ function __vue_create_injector__() { var head = document.head || document.getElementsByTagName('head')[0]; var styles = __vue_create_injector__.styles || (__vue_create_injector__.styles = {}); var isOldIE = typeof navigator !== 'undefined' && /msie [6-9]\\b/.test(navigator.userAgent.toLowerCase()); return function addStyle(id, css) { if (document.querySelector('style[data-vue-ssr-id~="' + id + '"]')) { return } // SSR styles are present. var group = isOldIE ? css.media || 'default' : id; var style = styles[group] || (styles[group] = { ids: [], parts: [], element: undefined }); if (!style.ids.includes(id)) { var code = css.source; var index = style.ids.length; style.ids.push(id); if (css.map) { // https://developer.chrome.com/devtools/docs/javascript-debugging // this makes source maps inside style tags work properly in Chrome code += '\n/*# sourceURL=' + css.map.sources[0] + ' */'; // http://stackoverflow.com/a/26603875 code += '\n/*# sourceMappingURL=data:application/json;base64,' + btoa(unescape(encodeURIComponent(JSON.stringify(css.map)))) + ' */'; } if (isOldIE) { style.element = style.element || document.querySelector('style[data-group=' + group + ']'); } if (!style.element) { var el = style.element = document.createElement('style'); el.type = 'text/css'; if (css.media) { el.setAttribute('media', css.media); } if (isOldIE) { el.setAttribute('data-group', group); el.setAttribute('data-next-index', '0'); } head.appendChild(el); } if (isOldIE) { index = parseInt(style.element.getAttribute('data-next-index')); style.element.setAttribute('data-next-index', index + 1); } if (style.element.styleSheet) { style.parts.push(code); style.element.styleSheet.cssText = style.parts .filter(Boolean) .join('\n'); } else { var textNode = document.createTextNode(code); var nodes = style.element.childNodes; if (nodes[index]) { style.element.removeChild(nodes[index]); } if (nodes.length) { style.element.insertBefore(textNode, nodes[index]); } else { style.element.appendChild(textNode); } } } } } /* style inject SSR */ var component = __vue_normalize__( { render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ }, __vue_inject_styles__, __vue_script__, __vue_scope_id__, __vue_is_functional_template__, __vue_module_identifier__, __vue_create_injector__, undefined ); // Import vue component // Declare install function executed by Vue.use() function install(Vue) { if (install.installed) { return; } install.installed = true; Vue.component('ColorPicker', component); } // Create module definition for Vue.use() var plugin = { install: install, }; // Auto-install when vue is found (eg. in browser via <script> tag) var GlobalVue = null; if (typeof window !== 'undefined') { GlobalVue = window.Vue; } else if (typeof global !== 'undefined') { GlobalVue = global.Vue; } if (GlobalVue) { GlobalVue.use(plugin); } export default component; export { install };