mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-17 14:26:35 +00:00
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:
parent
7cfa694980
commit
27046b00c6
@ -5,148 +5,612 @@
|
|||||||
<dom-module id='ha-color-picker'>
|
<dom-module id='ha-color-picker'>
|
||||||
<template>
|
<template>
|
||||||
<style>
|
<style>
|
||||||
canvas {
|
:host {
|
||||||
cursor: crosshair;
|
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>
|
</template>
|
||||||
</dom-module>
|
</dom-module>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
/**
|
/**
|
||||||
* Color-picker custom element
|
* 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) {
|
class HaColorPicker extends window.hassMixins.EventsMixin(Polymer.Element) {
|
||||||
static get is() { return 'ha-color-picker'; }
|
static get is() { return 'ha-color-picker'; }
|
||||||
|
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
color: {
|
hsvColor: {
|
||||||
type: Object,
|
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: {
|
width: {
|
||||||
type: Number,
|
type: Number,
|
||||||
|
value: 500,
|
||||||
},
|
},
|
||||||
|
|
||||||
height: {
|
height: {
|
||||||
type: Number,
|
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() {
|
ready() {
|
||||||
super.ready();
|
super.ready();
|
||||||
this.addEventListener('mousedown', ev => this.onMouseDown(ev));
|
this.applyRgbColor = this.applyRgbColor.bind(this);
|
||||||
this.addEventListener('mouseup', ev => this.onMouseUp(ev));
|
this.setupLayers();
|
||||||
this.addEventListener('touchstart', ev => this.onTouchStart(ev));
|
this.drawColorWheel();
|
||||||
this.addEventListener('touchend', ev => this.onTouchEnd(ev));
|
this.drawMarker();
|
||||||
this.setColor = this.setColor.bind(this);
|
|
||||||
this.mouseMoveIsThrottled = true;
|
this.interactionLayer.addEventListener('mousedown', ev => this.onMouseDown(ev));
|
||||||
this.canvas = this.$.canvas;
|
this.interactionLayer.addEventListener('touchstart', ev => this.onTouchStart(ev));
|
||||||
this.context = this.canvas.getContext('2d');
|
|
||||||
this.drawGradient();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
onMouseDown(ev) {
|
||||||
this.onMouseMove(ev);
|
const cc = this.convertToCanvasCoordinates(ev.clientX, ev.clientY);
|
||||||
this.addEventListener('mousemove', this.onMouseMove);
|
// 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() {
|
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) {
|
onTouchStart(ev) {
|
||||||
this.onTouchMove(ev);
|
var touch = ev.changedTouches[0];
|
||||||
this.addEventListener('touchmove', this.onTouchMove);
|
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() {
|
onTouchEnd() {
|
||||||
this.removeEventListener('touchmove', this.onTouchMove);
|
this.canvas.classList.remove('touch', 'dragging');
|
||||||
|
this.removeEventListener('touchmove', this.onTouchSelect);
|
||||||
}
|
}
|
||||||
|
|
||||||
onTouchMove(ev) {
|
onTouchSelect(ev) {
|
||||||
if (!this.mouseMoveIsThrottled) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
this.mouseMoveIsThrottled = false;
|
this.fireColorSelected(hsv); // do it
|
||||||
this.processColorSelect(ev.touches[0]);
|
this.colorSelectIsThrottled = true;
|
||||||
setTimeout(() => { this.mouseMoveIsThrottled = true; }, 100);
|
setTimeout(() => {
|
||||||
|
this.colorSelectIsThrottled = false;
|
||||||
|
}, this.throttle);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseMove(ev) {
|
// set color values and fire colorselected event
|
||||||
if (!this.mouseMoveIsThrottled) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
this.mouseMoveIsThrottled = false;
|
var hsv = this.RGBtoHSV(rgb);
|
||||||
this.processColorSelect(ev);
|
this.applyHsvColor(hsv); // marker is always set on 'raw' hsv position
|
||||||
setTimeout(() => { this.mouseMoveIsThrottled = true; }, 100);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
processColorSelect(ev) {
|
applyHsvColor(hsv) {
|
||||||
var rect = this.canvas.getBoundingClientRect();
|
// do nothing is we already have the same color
|
||||||
|
if (this.hsvColor &&
|
||||||
// boundary check because people can move off-canvas.
|
this.hsvColor.h === hsv.h &&
|
||||||
if (ev.clientX < rect.left || ev.clientX >= rect.left + rect.width ||
|
this.hsvColor.s === hsv.s &&
|
||||||
ev.clientY < rect.top || ev.clientY >= rect.top + rect.height) {
|
this.hsvColor.v === hsv.v) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.setMarkerOnColor(hsv); // marker is always set on 'raw' hsv position
|
||||||
this.onColorSelect(ev.clientX - rect.left, ev.clientY - rect.top);
|
if (!this.ignoreSegments) { // apply segments if needed
|
||||||
}
|
hsv = this.applySegmentFilter(hsv);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
width = this.width || parseInt(style.width, 10);
|
this.hsvColor = hsv;
|
||||||
height = this.height || parseInt(style.height, 10);
|
this.rgbColor = this.HSVtoRGB(hsv);
|
||||||
|
// always apply the new color to the interface / canvas
|
||||||
colorGradient = this.context.createLinearGradient(0, 0, width, 0);
|
this.applyColorToCanvas(hsv);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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);
|
customElements.define(HaColorPicker.is, HaColorPicker);
|
||||||
</script>
|
</script>
|
||||||
|
@ -31,10 +31,10 @@
|
|||||||
|
|
||||||
ha-color-picker {
|
ha-color-picker {
|
||||||
display: block;
|
display: block;
|
||||||
width: 250px;
|
width: 100%;
|
||||||
|
|
||||||
max-height: 0px;
|
max-height: 0px;
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
transition: max-height .2s ease-in;
|
transition: max-height .2s ease-in;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,7 +46,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.has-rgb_color ha-color-picker {
|
.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 {
|
paper-item {
|
||||||
@ -89,7 +94,13 @@
|
|||||||
on-change='wvSliderChanged'></ha-labeled-slider>
|
on-change='wvSliderChanged'></ha-labeled-slider>
|
||||||
</div>
|
</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-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>
|
<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,
|
type: Number,
|
||||||
value: 0,
|
value: 0,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
colorPickerColor: {
|
||||||
|
type: Object,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,6 +159,7 @@
|
|||||||
this.brightnessSliderValue = newVal.attributes.brightness;
|
this.brightnessSliderValue = newVal.attributes.brightness;
|
||||||
this.ctSliderValue = newVal.attributes.color_temp;
|
this.ctSliderValue = newVal.attributes.color_temp;
|
||||||
this.wvSliderValue = newVal.attributes.white_value;
|
this.wvSliderValue = newVal.attributes.white_value;
|
||||||
|
this.colorPickerColor = this.rgbArrToObj(newVal.attributes.rgb_color);
|
||||||
|
|
||||||
if (newVal.attributes.effect_list) {
|
if (newVal.attributes.effect_list) {
|
||||||
this.effectIndex = newVal.attributes.effect_list.indexOf(newVal.attributes.effect);
|
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
|
* Called when a new color has been picked.
|
||||||
* color pick event but have a pause between requests.
|
* should be throttled with the 'throttle=' attribute of the color picker
|
||||||
*/
|
*/
|
||||||
colorPicked(ev) {
|
colorPicked(ev) {
|
||||||
if (this.skipColorPicked) {
|
|
||||||
this.colorChanged = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.color = ev.detail.rgb;
|
this.color = ev.detail.rgb;
|
||||||
|
|
||||||
this.serviceChangeColor(this.hass, this.stateObj.entity_id, this.color);
|
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user