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
JavaScript
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 };