Replacing the color picker with a Hue-Saturation color wheel. (#695)

* first workable version of a colorwheel

* don't stretch it too big smaller screens

* better touch/drag behaviour
+ some properties centralized

* changed coordinate system, dragable marker

* little tweaks and cleanups

* touch drag color tooltip

* Segments, color via attribute, throttling, CSS-styling, small fixes

* inmproved segment behaviour

* styling with css vars/mixins

* structuring, commenting and cleanup

* properly prevent user select

* don't import debounce

* settled on some default color segmentation and wheel styling

* getting rid of the hidden #wheel element
just set css vars on the backgroundLayer and get those via
getCumputedStyle

* remove the #wheel css declaration too

* width is just a stupid word that looks too much like with

* move the color circle/tooltip a bit higher

* quote all attributes
This commit is contained in:
NovapaX 2017-11-30 20:20:16 +01:00 committed by Paulus Schoutsen
parent 7cfa694980
commit 27046b00c6
2 changed files with 577 additions and 109 deletions

View File

@ -5,148 +5,612 @@
<dom-module id='ha-color-picker'>
<template>
<style>
canvas {
cursor: crosshair;
:host {
user-select: none;
-webkit-user-select: none;
}
</style>
<canvas width='[[width]]' height='[[height]]' id='canvas'></canvas>
#canvas {
position: relative;
width: 100%;
max-width: 330px;
}
#canvas > * {
display: block;
}
#interactionLayer {
color: white;
position: absolute;
cursor: crosshair;
width: 100%;
height: 100%;
overflow: visible;
}
#backgroundLayer {
width: 100%;
overflow: visible;
--wheel-bordercolor: var(--ha-color-picker-wheel-bordercolor, white);
--wheel-borderwidth: var(--ha-color-picker-wheel-borderwidth, 3);
--wheel-shadow: var(--ha-color-picker-wheel-shadow, rgb(15, 15, 15) 10px 5px 5px 0px);
}
#marker {
fill: currentColor;
stroke: var(--ha-color-picker-marker-bordercolor, white);
stroke-width: var(--ha-color-picker-marker-borderwidth, 3);
filter: url(#marker-shadow)
}
.dragging #marker {
}
#colorTooltip {
display: none;
fill: currentColor;
stroke: var(--ha-color-picker-tooltip-bordercolor, white);
stroke-width: var(--ha-color-picker-tooltip-borderwidth, 3);
}
.touch.dragging #colorTooltip {
display: inherit;
}
</style>
<div id='canvas'>
<svg id='interactionLayer'>
<defs>
<filter id="marker-shadow" x="-50%" y="-50%" width="200%" height="200%" filterUnits="objectBoundingBox">
<feOffset result="offOut" in="SourceAlpha" dx="2" dy="2" />
<feGaussianBlur result="blurOut" in="offOut" stdDeviation="2" />
<feComponentTransfer in="blurOut" result="alphaOut">
<feFuncA type="linear" slope="0.3" />
</feComponentTransfer>
<feBlend in="SourceGraphic" in2="alphaOut" mode="normal" />
</filter>
</defs>
</svg>
<canvas id='backgroundLayer'></canvas>
</div>
</template>
</dom-module>
<script>
/**
* Color-picker custom element
* Originally created by bbrewer97202 (Ben Brewer). MIT Licensed.
* https://github.com/bbrewer97202/color-picker-element
*
* Adapted to work with Polymer.
*/
class HaColorPicker extends window.hassMixins.EventsMixin(Polymer.Element) {
static get is() { return 'ha-color-picker'; }
static get properties() {
return {
color: {
hsvColor: {
type: Object,
},
rgbColor: {
type: Object,
},
// use these properties to update the state via attributes
desiredHsvColor: {
type: Object,
observer: 'applyHsvColor'
},
desiredRgbColor: {
type: Object,
observer: 'applyRgbColor'
},
// width, height and radius apply to the coordinates of
// of the canvas.
// border width are relative to these numbers
// the onscreen displayed size should be controlled with css
// and should be the same or smaller
width: {
type: Number,
value: 500,
},
height: {
type: Number,
value: 500,
},
radius: {
type: Number,
value: 225,
},
// the amount segments for the hue
// 0 = continious gradient
// other than 0 gives 'pie-pieces'
hueSegments: {
type: Number,
value: 0,
},
// the amount segments for the hue
// 0 = continious gradient
// 1 = only fully saturated
// > 1 = segments from white to fully saturated
saturationSegments: {
type: Number,
value: 0,
},
// set to true to make the segments purely esthetical
// this allows selection off all collors, also
// interpolated between the segments
ignoreSegments: {
type: Boolean,
value: false
},
// throttle te amount of 'colorselected' events fired
// value is timeout in milliseconds
throttle: {
type: Number,
value: 500,
}
};
}
ready() {
super.ready();
this.addEventListener('mousedown', ev => this.onMouseDown(ev));
this.addEventListener('mouseup', ev => this.onMouseUp(ev));
this.addEventListener('touchstart', ev => this.onTouchStart(ev));
this.addEventListener('touchend', ev => this.onTouchEnd(ev));
this.setColor = this.setColor.bind(this);
this.mouseMoveIsThrottled = true;
this.canvas = this.$.canvas;
this.context = this.canvas.getContext('2d');
this.drawGradient();
this.applyRgbColor = this.applyRgbColor.bind(this);
this.setupLayers();
this.drawColorWheel();
this.drawMarker();
this.interactionLayer.addEventListener('mousedown', ev => this.onMouseDown(ev));
this.interactionLayer.addEventListener('touchstart', ev => this.onTouchStart(ev));
}
// converts browser coordinates to canvas canvas coordinates
// origin is wheel center
// returns {x: X, y: Y} object
convertToCanvasCoordinates(clientX, clientY) {
var svgPoint = this.interactionLayer.createSVGPoint();
svgPoint.x = clientX;
svgPoint.y = clientY;
var cc = svgPoint.matrixTransform(this.interactionLayer.getScreenCTM().inverse());
return { x: cc.x, y: cc.y };
}
// Mouse events
onMouseDown(ev) {
this.onMouseMove(ev);
this.addEventListener('mousemove', this.onMouseMove);
const cc = this.convertToCanvasCoordinates(ev.clientX, ev.clientY);
// return if we're not on the wheel
if (!this.isInWheel(cc.x, cc.y)) {
return;
}
// a mousedown in wheel is always a color select action
this.onMouseSelect(ev);
// allow dragging
this.canvas.classList.add('mouse', 'dragging');
this.addEventListener('mousemove', this.onMouseSelect);
this.addEventListener('mouseup', this.onMouseUp);
}
onMouseUp() {
this.removeEventListener('mousemove', this.onMouseMove);
this.canvas.classList.remove('mouse', 'dragging');
this.removeEventListener('mousemove', this.onMouseSelect);
}
onMouseSelect(ev) {
requestAnimationFrame(() => this.processUserSelect(ev));
}
// Touch events
onTouchStart(ev) {
this.onTouchMove(ev);
this.addEventListener('touchmove', this.onTouchMove);
var touch = ev.changedTouches[0];
const cc = this.convertToCanvasCoordinates(touch.clientX, touch.clientY);
// return if we're not on the wheel
if (!this.isInWheel(cc.x, cc.y)) {
return;
}
if (ev.target === this.marker) { // drag marker
ev.preventDefault();
this.canvas.classList.add('touch', 'dragging');
this.addEventListener('touchmove', this.onTouchSelect);
this.addEventListener('touchend', this.onTouchEnd);
return;
}
// don't fire color selection immediately,
// wait for touchend and invalidate when we scroll
this.tapBecameScroll = false;
this.addEventListener('touchend', this.onTap);
this.addEventListener('touchmove', () => { this.tapBecameScroll = true; }, { passive: true });
}
onTap(ev) {
if (this.tapBecameScroll) {
return;
}
ev.preventDefault();
this.onTouchSelect(ev);
}
onTouchEnd() {
this.removeEventListener('touchmove', this.onTouchMove);
this.canvas.classList.remove('touch', 'dragging');
this.removeEventListener('touchmove', this.onTouchSelect);
}
onTouchMove(ev) {
if (!this.mouseMoveIsThrottled) {
onTouchSelect(ev) {
requestAnimationFrame(() => this.processUserSelect(ev.changedTouches[0]));
}
/*
* General event/selection handling
*/
// Process user input to color
processUserSelect(ev) {
var canvasXY = this.convertToCanvasCoordinates(ev.clientX, ev.clientY);
var hsv = this.getColor(canvasXY.x, canvasXY.y);
this.onColorSelect(hsv);
}
// apply color to marker position and canvas
onColorSelect(hsv) {
this.setMarkerOnColor(hsv); // marker always follows mounse 'raw' hsv value (= mouse position)
if (!this.ignoreSegments) { // apply segments if needed
hsv = this.applySegmentFilter(hsv);
}
// always apply the new color to the interface / canvas
this.applyColorToCanvas(hsv);
// throttling is applied to updating the exposed colors (properties)
// and firing of events
if (this.colorSelectIsThrottled) {
// make sure we apply the last selected color
// eventually after throttle limit has passed
clearTimeout(this.ensureFinalSelect);
this.ensureFinalSelect = setTimeout(() => {
this.fireColorSelected(hsv); // do it for the final time
}, this.throttle);
return;
}
this.mouseMoveIsThrottled = false;
this.processColorSelect(ev.touches[0]);
setTimeout(() => { this.mouseMoveIsThrottled = true; }, 100);
this.fireColorSelected(hsv); // do it
this.colorSelectIsThrottled = true;
setTimeout(() => {
this.colorSelectIsThrottled = false;
}, this.throttle);
}
onMouseMove(ev) {
if (!this.mouseMoveIsThrottled) {
// set color values and fire colorselected event
fireColorSelected(hsv) {
this.hsvColor = hsv;
this.rgbColor = this.HSVtoRGB(this.hsvColor);
this.fire('colorselected', { rgb: this.rgbColor, hsv: this.hsvColor });
}
/*
* Interface updating
*/
// set marker position to the given color
setMarkerOnColor(hsv) {
var dist = hsv.s * this.radius;
var theta = ((hsv.h - 180) / 180) * Math.PI;
var markerdX = -dist * Math.cos(theta);
var markerdY = -dist * Math.sin(theta);
var translateString = `translate(${markerdX},${markerdY})`;
this.marker.setAttribute('transform', translateString);
this.tooltip.setAttribute('transform', translateString);
}
// apply given color to interface elements
applyColorToCanvas(hsv) {
// we're not really converting hsv to hsl here, but we keep it cheap
// setting the color on the interactionLayer, the svg elements can inherit
this.interactionLayer.style.color = `hsl(${hsv.h}, ${hsv.v * 100}%, ${100 - (hsv.s * 50)}%)`;
}
/*
* applyRgbColor and applyHsvColor are used for external updates
* (to prevent observer loops on this.hsvColor and this.rgbColor)
*/
applyRgbColor(rgb) {
// do nothing is we already have the same color
if (this.rgbColor &&
this.rgbColor.r === rgb.r &&
this.rgbColor.g === rgb.g &&
this.rgbColor.b === rgb.b) {
return;
}
this.mouseMoveIsThrottled = false;
this.processColorSelect(ev);
setTimeout(() => { this.mouseMoveIsThrottled = true; }, 100);
var hsv = this.RGBtoHSV(rgb);
this.applyHsvColor(hsv); // marker is always set on 'raw' hsv position
}
processColorSelect(ev) {
var rect = this.canvas.getBoundingClientRect();
// boundary check because people can move off-canvas.
if (ev.clientX < rect.left || ev.clientX >= rect.left + rect.width ||
ev.clientY < rect.top || ev.clientY >= rect.top + rect.height) {
applyHsvColor(hsv) {
// do nothing is we already have the same color
if (this.hsvColor &&
this.hsvColor.h === hsv.h &&
this.hsvColor.s === hsv.s &&
this.hsvColor.v === hsv.v) {
return;
}
this.onColorSelect(ev.clientX - rect.left, ev.clientY - rect.top);
}
onColorSelect(x, y) {
var data = this.context.getImageData(x, y, 1, 1).data;
this.setColor({ r: data[0], g: data[1], b: data[2] });
}
setColor(rgb) {
this.color = rgb;
this.fire('colorselected', { rgb: this.color });
}
drawGradient() {
var style;
var width;
var height;
var colorGradient;
var bwGradient;
if (!this.width || !this.height) {
style = getComputedStyle(this);
this.setMarkerOnColor(hsv); // marker is always set on 'raw' hsv position
if (!this.ignoreSegments) { // apply segments if needed
hsv = this.applySegmentFilter(hsv);
}
width = this.width || parseInt(style.width, 10);
height = this.height || parseInt(style.height, 10);
colorGradient = this.context.createLinearGradient(0, 0, width, 0);
colorGradient.addColorStop(0, 'rgb(255,0,0)');
colorGradient.addColorStop(0.16, 'rgb(255,0,255)');
colorGradient.addColorStop(0.32, 'rgb(0,0,255)');
colorGradient.addColorStop(0.48, 'rgb(0,255,255)');
colorGradient.addColorStop(0.64, 'rgb(0,255,0)');
colorGradient.addColorStop(0.80, 'rgb(255,255,0)');
colorGradient.addColorStop(1, 'rgb(255,0,0)');
this.context.fillStyle = colorGradient;
this.context.fillRect(0, 0, width, height);
bwGradient = this.context.createLinearGradient(0, 0, 0, height);
bwGradient.addColorStop(0, 'rgba(255,255,255,1)');
bwGradient.addColorStop(0.5, 'rgba(255,255,255,0)');
bwGradient.addColorStop(0.5, 'rgba(0,0,0,0)');
bwGradient.addColorStop(1, 'rgba(0,0,0,1)');
this.context.fillStyle = bwGradient;
this.context.fillRect(0, 0, width, height);
this.hsvColor = hsv;
this.rgbColor = this.HSVtoRGB(hsv);
// always apply the new color to the interface / canvas
this.applyColorToCanvas(hsv);
}
/*
* input processing helpers
*/
// get angle (degrees)
getAngle(dX, dY) {
var theta = Math.atan2(-dY, -dX); // radians from the left edge, clockwise = positive
var angle = ((theta / Math.PI) * 180) + 180; // degrees, clockwise from right
return angle;
}
// returns true when coordinates are in the colorwheel
isInWheel(x, y) {
return this.getDistance(x, y) <= 1;
}
// returns distance from wheel center, 0 = center, 1 = edge, >1 = outside
getDistance(dX, dY) {
return Math.sqrt((dX * dX) + (dY * dY)) / this.radius;
}
/*
* Getting colors
*/
getColor(x, y) {
var hue = this.getAngle(x, y); // degrees, clockwise from right
var relativeDistance = this.getDistance(x, y); // edge of radius = 1
var sat = Math.min(relativeDistance, 1); // Distance from center
return { h: hue, s: sat, v: 1 };
}
applySegmentFilter(hsv) {
// apply hue segment steps
if (this.hueSegments) {
const angleStep = 360 / this.hueSegments;
const halfAngleStep = angleStep / 2;
hsv.h -= halfAngleStep; // take the 'centered segemnts' into account
if (hsv.h < 0) { hsv.h += 360; } // don't end up below 0
const rest = hsv.h % angleStep;
hsv.h -= rest - angleStep;
}
// apply saturation segment steps
if (this.saturationSegments) {
if (this.saturationSegments === 1) {
hsv.s = 1;
} else {
var segmentSize = 1 / this.saturationSegments;
var saturationStep = 1 / (this.saturationSegments - 1);
var calculatedSat = Math.floor(hsv.s / segmentSize) * saturationStep;
hsv.s = Math.min(calculatedSat, 1);
}
}
return hsv;
}
/*
* Drawing related stuff
*/
setupLayers() {
this.canvas = this.$.canvas;
this.backgroundLayer = this.$.backgroundLayer;
this.interactionLayer = this.$.interactionLayer;
// coordinate origin position (center of the wheel)
this.originX = this.width / 2;
this.originY = this.originX;
// synchronise width/height coordinates
this.backgroundLayer.width = this.width;
this.backgroundLayer.height = this.height;
this.interactionLayer.setAttribute('viewBox', `${-this.originX} ${-this.originY} ${this.width} ${this.height}`);
}
drawColorWheel() {
/*
* Setting up all paremeters
*/
let shadowColor;
let shadowOffsetX;
let shadowOffsetY;
let shadowBlur;
const context = this.backgroundLayer.getContext('2d');
// postioning and sizing
const cX = this.originX;
const cY = this.originY;
const radius = this.radius;
const counterClockwise = false;
// styling of the wheel
const wheelStyle = window.getComputedStyle(this.backgroundLayer, null);
const borderWidth = parseInt(wheelStyle.getPropertyValue('--wheel-borderwidth'), 10);
const borderColor = wheelStyle.getPropertyValue('--wheel-bordercolor').trim();
const wheelShadow = wheelStyle.getPropertyValue('--wheel-shadow').trim();
// extract shadow properties from CCS variable
// the shadow should be defined as: "10px 5px 5px 0px COLOR"
if (wheelShadow !== 'none') {
const values = wheelShadow.split('px ');
shadowColor = values.pop();
shadowOffsetX = parseInt(values[0], 10);
shadowOffsetY = parseInt(values[1], 10);
shadowBlur = parseInt(values[2], 10) || 0;
}
const borderRadius = radius + (borderWidth / 2);
const wheelRadius = radius;
const shadowRadius = radius + borderWidth;
/*
* Drawing functions
*/
function drawCircle(hueSegments, saturationSegments) {
hueSegments = hueSegments || 360; // reset 0 segments to 360
const angleStep = 360 / hueSegments;
const halfAngleStep = angleStep / 2; // center segments on color
for (var angle = 0; angle <= 360; angle += angleStep) {
var startAngle = ((angle - halfAngleStep)) * (Math.PI / 180);
var endAngle = ((angle + halfAngleStep) + 1) * (Math.PI / 180);
context.beginPath();
context.moveTo(cX, cY);
context.arc(cX, cY, wheelRadius, startAngle, endAngle, counterClockwise);
context.closePath();
// gradient
var gradient = context.createRadialGradient(cX, cY, 0, cX, cY, wheelRadius);
let lightness = 100;
// first gradient stop
gradient.addColorStop(0, `hsl(${angle}, 100%, ${lightness}%)`);
// segment gradient stops
if (saturationSegments > 0) {
const ratioStep = 1 / saturationSegments;
let ratio = 0;
for (var stop = 1; stop < saturationSegments; stop += 1) {
var prevLighness = lightness;
ratio = stop * ratioStep;
lightness = 100 - (50 * ratio);
gradient.addColorStop(ratio, `hsl(${angle}, 100%, ${prevLighness}%)`);
gradient.addColorStop(ratio, `hsl(${angle}, 100%, ${lightness}%)`);
}
gradient.addColorStop(ratio, `hsl(${angle}, 100%, 50%)`);
}
// last gradient stop
gradient.addColorStop(1, `hsl(${angle}, 100%, 50%)`);
context.fillStyle = gradient;
context.fill();
}
}
function drawShadow() {
context.save();
context.beginPath();
context.arc(cX, cY, shadowRadius, 0, 2 * Math.PI, false);
context.shadowColor = shadowColor;
context.shadowOffsetX = shadowOffsetX;
context.shadowOffsetY = shadowOffsetY;
context.shadowBlur = shadowBlur;
context.fillStyle = 'white';
context.fill();
context.restore();
}
function drawBorder() {
context.beginPath();
context.arc(cX, cY, borderRadius, 0, 2 * Math.PI, false);
context.lineWidth = borderWidth;
context.strokeStyle = borderColor;
context.stroke();
}
/*
* Call the drawing functions
* draws the shadow, wheel and border
*/
if (wheelStyle.shadow !== 'none') { drawShadow(); }
drawCircle(this.hueSegments, this.saturationSegments);
if (borderWidth > 0) { drawBorder(); }
}
/*
* Draw the (draggable) marker and tooltip
* on the interactionLayer)
*/
drawMarker() {
const svgElement = this.interactionLayer;
const markerradius = this.radius * 0.08;
const tooltipradius = this.radius * 0.15;
const TooltipOffsetY = -(tooltipradius * 3);
const TooltipOffsetX = 0;
svgElement.marker = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
svgElement.marker.setAttribute('id', 'marker');
svgElement.marker.setAttribute('r', markerradius);
this.marker = svgElement.marker;
svgElement.appendChild(svgElement.marker);
svgElement.tooltip = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
svgElement.tooltip.setAttribute('id', 'colorTooltip');
svgElement.tooltip.setAttribute('r', tooltipradius);
svgElement.tooltip.setAttribute('cx', TooltipOffsetX);
svgElement.tooltip.setAttribute('cy', TooltipOffsetY);
this.tooltip = svgElement.tooltip;
svgElement.appendChild(svgElement.tooltip);
}
/**
* Color conversion helpers
*
* modified from:
* http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c
* these take/return h = hue (0-360), s = saturation (0-1), v = value (0-1)
*/
/* eslint-disable */
HSVtoRGB(hsv) {
var r, g, b, i, f, p, q, t;
var h = hsv.h, s = hsv.s, v = hsv.v;
h /= 360;
i = Math.floor(h * 6);
f = h * 6 - i;
p = v * (1 - s);
q = v * (1 - f * s);
t = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0: r = v, g = t, b = p; break;
case 1: r = q, g = v, b = p; break;
case 2: r = p, g = v, b = t; break;
case 3: r = p, g = q, b = v; break;
case 4: r = t, g = p, b = v; break;
case 5: r = v, g = p, b = q; break;
}
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255)
};
}
RGBtoHSV(rgb) {
var r = rgb.r / 255, g = rgb.g / 255, b = rgb.b / 255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, v = max;
var d = max - min;
s = max === 0 ? 0 : d / max;
if (max === min) {
h = 0; // achromatic
} else {
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h *= 60; // hue values 0-360
}
return {
h: h,
s: s,
v: v
};
}
/* eslint-enable */
}
customElements.define(HaColorPicker.is, HaColorPicker);
</script>

View File

@ -31,10 +31,10 @@
ha-color-picker {
display: block;
width: 250px;
width: 100%;
max-height: 0px;
overflow: hidden;
overflow: visible;
transition: max-height .2s ease-in;
}
@ -46,7 +46,12 @@
}
.has-rgb_color ha-color-picker {
max-height: 200px;
max-height: 500px;
--ha-color-picker-wheel-borderwidth: 5;
--ha-color-picker-wheel-bordercolor: white;
--ha-color-picker-wheel-shadow: none;
--ha-color-picker-marker-borderwidth: 2;
--ha-color-picker-marker-bordercolor: white;
}
paper-item {
@ -89,7 +94,13 @@
on-change='wvSliderChanged'></ha-labeled-slider>
</div>
<ha-color-picker on-colorselected='colorPicked' height='200' width='250'>
<ha-color-picker
on-colorselected='colorPicked'
desired-rgb-color='{{colorPickerColor}}'
throttle='500'
hue-segments='24'
saturation-segments='8'
>
</ha-color-picker>
<ha-attributes state-obj="[[stateObj]]" extra-filters="brightness,color_temp,white_value,effect_list,effect,rgb_color,xy_color,min_mireds,max_mireds"></ha-attributes>
@ -136,6 +147,10 @@
type: Number,
value: 0,
},
colorPickerColor: {
type: Object,
}
};
}
@ -144,6 +159,7 @@
this.brightnessSliderValue = newVal.attributes.brightness;
this.ctSliderValue = newVal.attributes.color_temp;
this.wvSliderValue = newVal.attributes.white_value;
this.colorPickerColor = this.rgbArrToObj(newVal.attributes.rgb_color);
if (newVal.attributes.effect_list) {
this.effectIndex = newVal.attributes.effect_list.indexOf(newVal.attributes.effect);
@ -237,29 +253,17 @@
});
}
rgbArrToObj(rgbArr) {
return { r: rgbArr[0], g: rgbArr[1], b: rgbArr[2] };
}
/**
* Called when a new color has been picked. We will not respond to every
* color pick event but have a pause between requests.
* Called when a new color has been picked.
* should be throttled with the 'throttle=' attribute of the color picker
*/
colorPicked(ev) {
if (this.skipColorPicked) {
this.colorChanged = true;
return;
}
this.color = ev.detail.rgb;
this.serviceChangeColor(this.hass, this.stateObj.entity_id, this.color);
this.colorChanged = false;
this.skipColorPicked = true;
this.colorDebounce = setTimeout(function () {
if (this.colorChanged) {
this.serviceChangeColor(this.hass, this.stateObj.entity_id, this.color);
}
this.skipColorPicked = false;
}.bind(this), 500);
}
}