* Update typescript, prettier, tslint -> eslint * Organize imports * Use glob for eslint fix react import
604 lines
17 KiB
JavaScript
604 lines
17 KiB
JavaScript
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
/* eslint-plugin-disable lit */
|
|
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
import { EventsMixin } from "../mixins/events-mixin";
|
|
|
|
/**
|
|
* Color-picker custom element
|
|
*
|
|
* @appliesMixin EventsMixin
|
|
*/
|
|
class HaColorPicker extends EventsMixin(PolymerElement) {
|
|
static get template() {
|
|
return html`
|
|
<style>
|
|
:host {
|
|
user-select: none;
|
|
-webkit-user-select: none;
|
|
}
|
|
|
|
#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"
|
|
></feOffset>
|
|
<feGaussianBlur
|
|
result="blurOut"
|
|
in="offOut"
|
|
stdDeviation="2"
|
|
></feGaussianBlur>
|
|
<feComponentTransfer in="blurOut" result="alphaOut">
|
|
<feFuncA type="linear" slope="0.3"></feFuncA>
|
|
</feComponentTransfer>
|
|
<feBlend
|
|
in="SourceGraphic"
|
|
in2="alphaOut"
|
|
mode="normal"
|
|
></feBlend>
|
|
</filter>
|
|
</defs>
|
|
</svg>
|
|
<canvas id="backgroundLayer"></canvas>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
static get properties() {
|
|
return {
|
|
hsColor: {
|
|
type: Object,
|
|
},
|
|
|
|
// use these properties to update the state via attributes
|
|
desiredHsColor: {
|
|
type: Object,
|
|
observer: "applyHsColor",
|
|
},
|
|
|
|
// 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 = continuous gradient
|
|
// other than 0 gives 'pie-pieces'
|
|
hueSegments: {
|
|
type: Number,
|
|
value: 0,
|
|
observer: "segmentationChange",
|
|
},
|
|
|
|
// the amount segments for the hue
|
|
// 0 = continuous gradient
|
|
// 1 = only fully saturated
|
|
// > 1 = segments from white to fully saturated
|
|
saturationSegments: {
|
|
type: Number,
|
|
value: 0,
|
|
observer: "segmentationChange",
|
|
},
|
|
|
|
// 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.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) {
|
|
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.canvas.classList.remove("mouse", "dragging");
|
|
this.removeEventListener("mousemove", this.onMouseSelect);
|
|
}
|
|
|
|
onMouseSelect(ev) {
|
|
requestAnimationFrame(() => this.processUserSelect(ev));
|
|
}
|
|
|
|
// Touch events
|
|
|
|
onTouchStart(ev) {
|
|
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.canvas.classList.remove("touch", "dragging");
|
|
this.removeEventListener("touchmove", this.onTouchSelect);
|
|
}
|
|
|
|
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 hs = this.getColor(canvasXY.x, canvasXY.y);
|
|
this.onColorSelect(hs);
|
|
}
|
|
|
|
// apply color to marker position and canvas
|
|
onColorSelect(hs) {
|
|
this.setMarkerOnColor(hs); // marker always follows mounse 'raw' hs value (= mouse position)
|
|
if (!this.ignoreSegments) {
|
|
// apply segments if needed
|
|
hs = this.applySegmentFilter(hs);
|
|
}
|
|
// always apply the new color to the interface / canvas
|
|
this.applyColorToCanvas(hs);
|
|
// 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(hs); // do it for the final time
|
|
}, this.throttle);
|
|
return;
|
|
}
|
|
this.fireColorSelected(hs); // do it
|
|
this.colorSelectIsThrottled = true;
|
|
setTimeout(() => {
|
|
this.colorSelectIsThrottled = false;
|
|
}, this.throttle);
|
|
}
|
|
|
|
// set color values and fire colorselected event
|
|
fireColorSelected(hs) {
|
|
this.hsColor = hs;
|
|
this.fire("colorselected", { hs: { h: hs.h, s: hs.s } });
|
|
}
|
|
|
|
/*
|
|
* Interface updating
|
|
*/
|
|
|
|
// set marker position to the given color
|
|
setMarkerOnColor(hs) {
|
|
var dist = hs.s * this.radius;
|
|
var theta = ((hs.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(hs) {
|
|
// we're not really converting hs to hsl here, but we keep it cheap
|
|
// setting the color on the interactionLayer, the svg elements can inherit
|
|
this.interactionLayer.style.color = `hsl(${hs.h}, 100%, ${
|
|
100 - hs.s * 50
|
|
}%)`;
|
|
}
|
|
|
|
applyHsColor(hs) {
|
|
// do nothing is we already have the same color
|
|
if (this.hsColor && this.hsColor.h === hs.h && this.hsColor.s === hs.s) {
|
|
return;
|
|
}
|
|
this.setMarkerOnColor(hs); // marker is always set on 'raw' hs position
|
|
if (!this.ignoreSegments) {
|
|
// apply segments if needed
|
|
hs = this.applySegmentFilter(hs);
|
|
}
|
|
this.hsColor = hs;
|
|
// always apply the new color to the interface / canvas
|
|
this.applyColorToCanvas(hs);
|
|
}
|
|
|
|
/*
|
|
* 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 };
|
|
}
|
|
|
|
applySegmentFilter(hs) {
|
|
// apply hue segment steps
|
|
if (this.hueSegments) {
|
|
const angleStep = 360 / this.hueSegments;
|
|
const halfAngleStep = angleStep / 2;
|
|
hs.h -= halfAngleStep; // take the 'centered segemnts' into account
|
|
if (hs.h < 0) {
|
|
hs.h += 360;
|
|
} // don't end up below 0
|
|
const rest = hs.h % angleStep;
|
|
hs.h -= rest - angleStep;
|
|
}
|
|
|
|
// apply saturation segment steps
|
|
if (this.saturationSegments) {
|
|
if (this.saturationSegments === 1) {
|
|
hs.s = 1;
|
|
} else {
|
|
var segmentSize = 1 / this.saturationSegments;
|
|
var saturationStep = 1 / (this.saturationSegments - 1);
|
|
var calculatedSat = Math.floor(hs.s / segmentSize) * saturationStep;
|
|
hs.s = Math.min(calculatedSat, 1);
|
|
}
|
|
}
|
|
return hs;
|
|
}
|
|
|
|
/*
|
|
* 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);
|
|
}
|
|
|
|
segmentationChange() {
|
|
if (this.backgroundLayer) {
|
|
this.drawColorWheel();
|
|
}
|
|
}
|
|
}
|
|
customElements.define("ha-color-picker", HaColorPicker);
|