mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-15 21:36:36 +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'>
|
||||
<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>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user