mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-21 16:26:43 +00:00
New color picker for HS and color temp (#16549)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
parent
a5b5e61ed4
commit
e9d9d89d79
3
gallery/src/pages/components/ha-hs-color-picker.markdown
Normal file
3
gallery/src/pages/components/ha-hs-color-picker.markdown
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
title: HS Color Picker
|
||||||
|
---
|
120
gallery/src/pages/components/ha-hs-color-picker.ts
Normal file
120
gallery/src/pages/components/ha-hs-color-picker.ts
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import "../../../../src/components/ha-hs-color-picker";
|
||||||
|
|
||||||
|
import { css, html, LitElement, TemplateResult } from "lit";
|
||||||
|
import { customElement, state } from "lit/decorators";
|
||||||
|
|
||||||
|
import "../../../../src/components/ha-card";
|
||||||
|
import "../../../../src/components/ha-slider";
|
||||||
|
import { hsv2rgb } from "../../../../src/common/color/convert-color";
|
||||||
|
|
||||||
|
@customElement("demo-components-ha-hs-color-picker")
|
||||||
|
export class DemoHaHsColorPicker extends LitElement {
|
||||||
|
@state()
|
||||||
|
brightness = 255;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
value: [number, number] = [0, 0];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
liveValue?: [number, number];
|
||||||
|
|
||||||
|
private _brightnessChanged(ev) {
|
||||||
|
this.brightness = Number(ev.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _hsColorCursor(ev) {
|
||||||
|
this.liveValue = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _hsColorChanged(ev) {
|
||||||
|
this.value = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _hueChanged(ev) {
|
||||||
|
this.value = [ev.target.value, this.value[1]];
|
||||||
|
}
|
||||||
|
|
||||||
|
private _saturationChanged(ev) {
|
||||||
|
this.value = [this.value[0], ev.target.value];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
const h = (this.liveValue ?? this.value)[0];
|
||||||
|
const s = (this.liveValue ?? this.value)[1];
|
||||||
|
|
||||||
|
const rgb = hsv2rgb([h, s, this.brightness]);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-card>
|
||||||
|
<div class="card-content">
|
||||||
|
<p class="value">${h}° - ${Math.round(s * 100)}%</p>
|
||||||
|
<p class="value">${rgb.map((v) => Math.round(v)).join(", ")}</p>
|
||||||
|
<ha-hs-color-picker
|
||||||
|
colorBrightness=${this.brightness}
|
||||||
|
.value=${this.value}
|
||||||
|
@value-changed=${this._hsColorChanged}
|
||||||
|
@cursor-moved=${this._hsColorCursor}
|
||||||
|
></ha-hs-color-picker>
|
||||||
|
<p>Hue : ${this.value[0]}</p>
|
||||||
|
<ha-slider
|
||||||
|
step="1"
|
||||||
|
pin
|
||||||
|
min="0"
|
||||||
|
max="360"
|
||||||
|
.value=${this.value[0]}
|
||||||
|
@change=${this._hueChanged}
|
||||||
|
>
|
||||||
|
</ha-slider>
|
||||||
|
<p>Saturation : ${this.value[1]}</p>
|
||||||
|
<ha-slider
|
||||||
|
step="0.01"
|
||||||
|
pin
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
.value=${this.value[1]}
|
||||||
|
@change=${this._saturationChanged}
|
||||||
|
>
|
||||||
|
</ha-slider>
|
||||||
|
<p>Color Brighness : ${this.brightness}</p>
|
||||||
|
<ha-slider
|
||||||
|
step="1"
|
||||||
|
pin
|
||||||
|
min="0"
|
||||||
|
max="255"
|
||||||
|
.value=${this.brightness}
|
||||||
|
@change=${this._brightnessChanged}
|
||||||
|
>
|
||||||
|
</ha-slider>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
ha-card {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 24px auto;
|
||||||
|
}
|
||||||
|
.card-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
ha-hs-color-picker {
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
.value {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"demo-components-ha-hs-color-picker": DemoHaHsColorPicker;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
title: Temp Color Picker
|
||||||
|
---
|
117
gallery/src/pages/components/ha-temp-color-picker.ts
Normal file
117
gallery/src/pages/components/ha-temp-color-picker.ts
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import "../../../../src/components/ha-temp-color-picker";
|
||||||
|
|
||||||
|
import { css, html, LitElement, TemplateResult } from "lit";
|
||||||
|
import { customElement, state } from "lit/decorators";
|
||||||
|
|
||||||
|
import "../../../../src/components/ha-card";
|
||||||
|
import "../../../../src/components/ha-slider";
|
||||||
|
|
||||||
|
@customElement("demo-components-ha-temp-color-picker")
|
||||||
|
export class DemoHaTempColorPicker extends LitElement {
|
||||||
|
@state()
|
||||||
|
min = 3000;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
max = 7000;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
value = 4000;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
liveValue?: number;
|
||||||
|
|
||||||
|
private _minChanged(ev) {
|
||||||
|
this.min = Number(ev.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _maxChanged(ev) {
|
||||||
|
this.max = Number(ev.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _valueChanged(ev) {
|
||||||
|
this.value = Number(ev.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _tempColorCursor(ev) {
|
||||||
|
this.liveValue = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _tempColorChanged(ev) {
|
||||||
|
this.value = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<ha-card>
|
||||||
|
<div class="card-content">
|
||||||
|
<p class="value">${this.liveValue ?? this.value} K</p>
|
||||||
|
<ha-temp-color-picker
|
||||||
|
.min=${this.min}
|
||||||
|
.max=${this.max}
|
||||||
|
.value=${this.value}
|
||||||
|
@value-changed=${this._tempColorChanged}
|
||||||
|
@cursor-moved=${this._tempColorCursor}
|
||||||
|
></ha-temp-color-picker>
|
||||||
|
<p>Min temp : ${this.min} K</p>
|
||||||
|
<ha-slider
|
||||||
|
step="1"
|
||||||
|
pin
|
||||||
|
min="2000"
|
||||||
|
max="10000"
|
||||||
|
.value=${this.min}
|
||||||
|
@change=${this._minChanged}
|
||||||
|
>
|
||||||
|
</ha-slider>
|
||||||
|
<p>Max temp : ${this.max} K</p>
|
||||||
|
<ha-slider
|
||||||
|
step="1"
|
||||||
|
pin
|
||||||
|
min="2000"
|
||||||
|
max="10000"
|
||||||
|
.value=${this.max}
|
||||||
|
@change=${this._maxChanged}
|
||||||
|
>
|
||||||
|
</ha-slider>
|
||||||
|
<p>Value : ${this.value} K</p>
|
||||||
|
<ha-slider
|
||||||
|
step="1"
|
||||||
|
pin
|
||||||
|
min=${this.min}
|
||||||
|
max=${this.max}
|
||||||
|
.value=${this.value}
|
||||||
|
@change=${this._valueChanged}
|
||||||
|
>
|
||||||
|
</ha-slider>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
ha-card {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 24px auto;
|
||||||
|
}
|
||||||
|
.card-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
ha-temp-color-picker {
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
.value {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"demo-components-ha-temp-color-picker": DemoHaTempColorPicker;
|
||||||
|
}
|
||||||
|
}
|
@ -1,644 +0,0 @@
|
|||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
||||||
/* eslint-plugin-disable lit */
|
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
import { hs2rgb, rgb2hs } from "../common/color/convert-color";
|
|
||||||
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",
|
|
||||||
},
|
|
||||||
|
|
||||||
// use these properties to update the state via attributes
|
|
||||||
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 = 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();
|
|
||||||
|
|
||||||
if (this.desiredHsColor) {
|
|
||||||
this.applyHsColor(this.desiredHsColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.desiredRgbColor) {
|
|
||||||
this.applyRgbColor(this.desiredRgbColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
const svgPoint = this.interactionLayer.createSVGPoint();
|
|
||||||
svgPoint.x = clientX;
|
|
||||||
svgPoint.y = clientY;
|
|
||||||
const 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) {
|
|
||||||
const 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) {
|
|
||||||
const canvasXY = this.convertToCanvasCoordinates(ev.clientX, ev.clientY);
|
|
||||||
const hs = this.getColor(canvasXY.x, canvasXY.y);
|
|
||||||
let rgb;
|
|
||||||
if (!this.isInWheel(canvasXY.x, canvasXY.y)) {
|
|
||||||
const [r, g, b] = hs2rgb([hs.h, hs.s]);
|
|
||||||
rgb = { r, g, b };
|
|
||||||
} else {
|
|
||||||
rgb = this.getRgbColor(canvasXY.x, canvasXY.y);
|
|
||||||
}
|
|
||||||
this.onColorSelect(hs, rgb);
|
|
||||||
}
|
|
||||||
|
|
||||||
// apply color to marker position and canvas
|
|
||||||
onColorSelect(hs, rgb) {
|
|
||||||
this.setMarkerOnColor(hs); // marker always follows mouse '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, rgb); // do it for the final time
|
|
||||||
}, this.throttle);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.fireColorSelected(hs, rgb); // do it
|
|
||||||
this.colorSelectIsThrottled = true;
|
|
||||||
setTimeout(() => {
|
|
||||||
this.colorSelectIsThrottled = false;
|
|
||||||
}, this.throttle);
|
|
||||||
}
|
|
||||||
|
|
||||||
// set color values and fire colorselected event
|
|
||||||
fireColorSelected(hs, rgb) {
|
|
||||||
this.hsColor = hs;
|
|
||||||
this.fire("colorselected", { hs, rgb });
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Interface updating
|
|
||||||
*/
|
|
||||||
|
|
||||||
// set marker position to the given color
|
|
||||||
setMarkerOnColor(hs) {
|
|
||||||
if (!this.marker || !this.tooltip) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const dist = hs.s * this.radius;
|
|
||||||
const theta = ((hs.h - 180) / 180) * Math.PI;
|
|
||||||
const markerdX = -dist * Math.cos(theta);
|
|
||||||
const markerdY = -dist * Math.sin(theta);
|
|
||||||
const translateString = `translate(${markerdX},${markerdY})`;
|
|
||||||
this.marker.setAttribute("transform", translateString);
|
|
||||||
this.tooltip.setAttribute("transform", translateString);
|
|
||||||
}
|
|
||||||
|
|
||||||
// apply given color to interface elements
|
|
||||||
applyColorToCanvas(hs) {
|
|
||||||
if (!this.interactionLayer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
applyRgbColor(rgb) {
|
|
||||||
const [h, s] = rgb2hs(rgb);
|
|
||||||
this.applyHsColor({ h, s });
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* input processing helpers
|
|
||||||
*/
|
|
||||||
|
|
||||||
// get angle (degrees)
|
|
||||||
getAngle(dX, dY) {
|
|
||||||
const theta = Math.atan2(-dY, -dX); // radians from the left edge, clockwise = positive
|
|
||||||
const 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) {
|
|
||||||
const hue = this.getAngle(x, y); // degrees, clockwise from right
|
|
||||||
const relativeDistance = this.getDistance(x, y); // edge of radius = 1
|
|
||||||
const sat = Math.min(relativeDistance, 1); // Distance from center
|
|
||||||
return { h: hue, s: sat };
|
|
||||||
}
|
|
||||||
|
|
||||||
getRgbColor(x, y) {
|
|
||||||
// get current pixel
|
|
||||||
const imageData = this.backgroundLayer
|
|
||||||
.getContext("2d")
|
|
||||||
.getImageData(x + 250, y + 250, 1, 1);
|
|
||||||
const pixel = imageData.data;
|
|
||||||
return { r: pixel[0], g: pixel[1], b: pixel[2] };
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
const segmentSize = 1 / this.saturationSegments;
|
|
||||||
const saturationStep = 1 / (this.saturationSegments - 1);
|
|
||||||
const 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 CSS 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 (let angle = 0; angle <= 360; angle += angleStep) {
|
|
||||||
const startAngle = (angle - halfAngleStep) * (Math.PI / 180);
|
|
||||||
const endAngle = (angle + halfAngleStep + 1) * (Math.PI / 180);
|
|
||||||
context.beginPath();
|
|
||||||
context.moveTo(cX, cY);
|
|
||||||
context.arc(
|
|
||||||
cX,
|
|
||||||
cY,
|
|
||||||
wheelRadius,
|
|
||||||
startAngle,
|
|
||||||
endAngle,
|
|
||||||
counterClockwise
|
|
||||||
);
|
|
||||||
context.closePath();
|
|
||||||
// gradient
|
|
||||||
const 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 (let stop = 1; stop < saturationSegments; stop += 1) {
|
|
||||||
const 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);
|
|
329
src/components/ha-hs-color-picker.ts
Normal file
329
src/components/ha-hs-color-picker.ts
Normal file
@ -0,0 +1,329 @@
|
|||||||
|
import { DIRECTION_ALL, Manager, Pan, Tap } from "@egjs/hammerjs";
|
||||||
|
import { css, html, LitElement, PropertyValues, svg } from "lit";
|
||||||
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
|
import { classMap } from "lit/directives/class-map";
|
||||||
|
import { styleMap } from "lit/directives/style-map";
|
||||||
|
import { hsv2rgb, rgb2hex } from "../common/color/convert-color";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
|
||||||
|
function xy2polar(x: number, y: number) {
|
||||||
|
const r = Math.sqrt(x * x + y * y);
|
||||||
|
const phi = Math.atan2(y, x);
|
||||||
|
return [r, phi];
|
||||||
|
}
|
||||||
|
|
||||||
|
function polar2xy(r: number, phi: number) {
|
||||||
|
const x = Math.cos(phi) * r;
|
||||||
|
const y = Math.sin(phi) * r;
|
||||||
|
return [x, y];
|
||||||
|
}
|
||||||
|
|
||||||
|
function rad2deg(rad: number) {
|
||||||
|
return (rad / (2 * Math.PI)) * 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deg2rad(deg: number) {
|
||||||
|
return (deg / 360) * 2 * Math.PI;
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawColorWheel(ctx: CanvasRenderingContext2D, colorBrightness = 255) {
|
||||||
|
const radius = ctx.canvas.width / 2;
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||||
|
ctx.beginPath();
|
||||||
|
|
||||||
|
const cX = ctx.canvas.width / 2;
|
||||||
|
const cY = ctx.canvas.width / 2;
|
||||||
|
for (let angle = 0; angle < 360; angle += 1) {
|
||||||
|
const startAngle = deg2rad(angle - 0.5);
|
||||||
|
const endAngle = deg2rad(angle + 1.5);
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(cX, cY);
|
||||||
|
ctx.arc(cX, cY, radius, startAngle, endAngle);
|
||||||
|
ctx.closePath();
|
||||||
|
|
||||||
|
const gradient = ctx.createRadialGradient(cX, cY, 0, cX, cY, radius);
|
||||||
|
const start = rgb2hex(hsv2rgb([angle, 0, colorBrightness]));
|
||||||
|
const end = rgb2hex(hsv2rgb([angle, 1, colorBrightness]));
|
||||||
|
gradient.addColorStop(0, start);
|
||||||
|
gradient.addColorStop(1, end);
|
||||||
|
ctx.fillStyle = gradient;
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement("ha-hs-color-picker")
|
||||||
|
class HaHsColorPicker extends LitElement {
|
||||||
|
@property({ type: Boolean, reflect: true })
|
||||||
|
public disabled = false;
|
||||||
|
|
||||||
|
@property({ type: Number, attribute: false })
|
||||||
|
public renderSize?: number;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public value?: [number, number];
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public colorBrightness?: number;
|
||||||
|
|
||||||
|
@query("#canvas") private _canvas!: HTMLCanvasElement;
|
||||||
|
|
||||||
|
private _mc?: HammerManager;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private _pressed?: string;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private _cursorPosition?: [number, number];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private _localValue?: [number, number];
|
||||||
|
|
||||||
|
protected firstUpdated(changedProps: PropertyValues): void {
|
||||||
|
super.firstUpdated(changedProps);
|
||||||
|
this._setupListeners();
|
||||||
|
this._generateColorWheel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _generateColorWheel() {
|
||||||
|
const ctx = this._canvas.getContext("2d")!;
|
||||||
|
drawColorWheel(ctx, this.colorBrightness);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback(): void {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._setupListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback(): void {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this._destroyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProps: PropertyValues): void {
|
||||||
|
super.updated(changedProps);
|
||||||
|
if (changedProps.has("colorBrightness")) {
|
||||||
|
this._generateColorWheel();
|
||||||
|
}
|
||||||
|
if (changedProps.has("value")) {
|
||||||
|
if (
|
||||||
|
this.value !== undefined &&
|
||||||
|
(this._localValue?.[0] !== this.value[0] ||
|
||||||
|
this._localValue?.[1] !== this.value[1])
|
||||||
|
) {
|
||||||
|
this._resetPosition();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_setupListeners() {
|
||||||
|
if (this._canvas && !this._mc) {
|
||||||
|
this._mc = new Manager(this._canvas);
|
||||||
|
this._mc.add(
|
||||||
|
new Pan({
|
||||||
|
direction: DIRECTION_ALL,
|
||||||
|
enable: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this._mc.add(new Tap({ event: "singletap" }));
|
||||||
|
|
||||||
|
let savedPosition;
|
||||||
|
this._mc.on("panstart", (e) => {
|
||||||
|
if (this.disabled) return;
|
||||||
|
this._pressed = e.pointerType;
|
||||||
|
savedPosition = this._cursorPosition;
|
||||||
|
});
|
||||||
|
this._mc.on("pancancel", () => {
|
||||||
|
if (this.disabled) return;
|
||||||
|
this._pressed = undefined;
|
||||||
|
this._cursorPosition = savedPosition;
|
||||||
|
});
|
||||||
|
this._mc.on("panmove", (e) => {
|
||||||
|
if (this.disabled) return;
|
||||||
|
this._cursorPosition = this._getPositionFromEvent(e);
|
||||||
|
this._localValue = this._getValueFromCoord(...this._cursorPosition);
|
||||||
|
fireEvent(this, "cursor-moved", { value: this._localValue });
|
||||||
|
});
|
||||||
|
this._mc.on("panend", (e) => {
|
||||||
|
if (this.disabled) return;
|
||||||
|
this._pressed = undefined;
|
||||||
|
this._cursorPosition = this._getPositionFromEvent(e);
|
||||||
|
this._localValue = this._getValueFromCoord(...this._cursorPosition);
|
||||||
|
fireEvent(this, "cursor-moved", { value: undefined });
|
||||||
|
fireEvent(this, "value-changed", { value: this._localValue });
|
||||||
|
});
|
||||||
|
|
||||||
|
this._mc.on("singletap", (e) => {
|
||||||
|
if (this.disabled) return;
|
||||||
|
this._cursorPosition = this._getPositionFromEvent(e);
|
||||||
|
this._localValue = this._getValueFromCoord(...this._cursorPosition);
|
||||||
|
fireEvent(this, "value-changed", { value: this._localValue });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _resetPosition() {
|
||||||
|
if (this.value === undefined) return;
|
||||||
|
this._cursorPosition = this._getCoordsFromValue(this.value);
|
||||||
|
this._localValue = this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getCoordsFromValue = (value: [number, number]): [number, number] => {
|
||||||
|
const phi = deg2rad(value[0]);
|
||||||
|
|
||||||
|
const r = Math.min(value[1], 1);
|
||||||
|
|
||||||
|
const [x, y] = polar2xy(r, phi);
|
||||||
|
|
||||||
|
return [x, y];
|
||||||
|
};
|
||||||
|
|
||||||
|
private _getValueFromCoord = (x: number, y: number): [number, number] => {
|
||||||
|
const [r, phi] = xy2polar(x, y);
|
||||||
|
|
||||||
|
const deg = Math.round(rad2deg(phi)) % 360;
|
||||||
|
|
||||||
|
const hue = (deg + 360) % 360;
|
||||||
|
const saturation = Math.round(Math.min(r, 1) * 100) / 100;
|
||||||
|
|
||||||
|
return [hue, saturation];
|
||||||
|
};
|
||||||
|
|
||||||
|
private _getPositionFromEvent = (e: HammerInput): [number, number] => {
|
||||||
|
const x = e.center.x;
|
||||||
|
const y = e.center.y;
|
||||||
|
const boundingRect = e.target.getBoundingClientRect();
|
||||||
|
const offsetX = boundingRect.left;
|
||||||
|
const offsetY = boundingRect.top;
|
||||||
|
const maxX = e.target.clientWidth;
|
||||||
|
const maxY = e.target.clientHeight;
|
||||||
|
|
||||||
|
const _x = (2 * (x - offsetX)) / maxX - 1;
|
||||||
|
const _y = (2 * (y - offsetY)) / maxY - 1;
|
||||||
|
|
||||||
|
const [r, phi] = xy2polar(_x, _y);
|
||||||
|
const [__x, __y] = polar2xy(Math.min(1, r), phi);
|
||||||
|
return [__x, __y];
|
||||||
|
};
|
||||||
|
|
||||||
|
_destroyListeners() {
|
||||||
|
if (this._mc) {
|
||||||
|
this._mc.destroy();
|
||||||
|
this._mc = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const size = this.renderSize || 400;
|
||||||
|
const canvasSize = size * window.devicePixelRatio;
|
||||||
|
|
||||||
|
const rgb =
|
||||||
|
this._localValue !== undefined
|
||||||
|
? hsv2rgb([
|
||||||
|
this._localValue[0],
|
||||||
|
this._localValue[1],
|
||||||
|
this.colorBrightness ?? 255,
|
||||||
|
])
|
||||||
|
: ([255, 255, 255] as [number, number, number]);
|
||||||
|
|
||||||
|
const [x, y] = this._cursorPosition ?? [0, 0];
|
||||||
|
|
||||||
|
const cx = ((x + 1) * size) / 2;
|
||||||
|
const cy = ((y + 1) * size) / 2;
|
||||||
|
|
||||||
|
const markerPosition = `${cx}px, ${cy}px`;
|
||||||
|
const markerScale = this._pressed ? "1.5" : "1";
|
||||||
|
const markerOffset =
|
||||||
|
this._pressed === "touch" ? `0px, -${size / 8}px` : "0px, 0px";
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="container ${classMap({ pressed: Boolean(this._pressed) })}">
|
||||||
|
<canvas id="canvas" .width=${canvasSize} .height=${canvasSize}></canvas>
|
||||||
|
<svg id="interaction" viewBox="0 0 ${size} ${size}" overflow="visible">
|
||||||
|
<defs>${this.renderSVGFilter()}</defs>
|
||||||
|
<g
|
||||||
|
style=${styleMap({
|
||||||
|
fill: rgb2hex(rgb),
|
||||||
|
transform: `translate(${markerPosition})`,
|
||||||
|
})}
|
||||||
|
class="cursor"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
cx="0"
|
||||||
|
cy="0"
|
||||||
|
r="16"
|
||||||
|
style=${styleMap({
|
||||||
|
fill: rgb2hex(rgb),
|
||||||
|
transform: `translate(${markerOffset}) scale(${markerScale})`,
|
||||||
|
visibility: this._cursorPosition ? undefined : "hidden",
|
||||||
|
})}
|
||||||
|
></circle>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSVGFilter() {
|
||||||
|
return svg`
|
||||||
|
<filter
|
||||||
|
id="marker-shadow"
|
||||||
|
x="-50%"
|
||||||
|
y="-50%"
|
||||||
|
width="200%"
|
||||||
|
height="200%"
|
||||||
|
filterUnits="objectBoundingBox"
|
||||||
|
>
|
||||||
|
<feDropShadow dx="0" dy="1" stdDeviation="2" flood-opacity="0.3" flood-color="rgba(0, 0, 0, 1)"/>
|
||||||
|
<feDropShadow dx="0" dy="1" stdDeviation="3" flood-opacity="0.15" flood-color="rgba(0, 0, 0, 1)"/>
|
||||||
|
</filter>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
svg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
circle {
|
||||||
|
fill: black;
|
||||||
|
stroke: white;
|
||||||
|
stroke-width: 2;
|
||||||
|
filter: url(#marker-shadow);
|
||||||
|
}
|
||||||
|
.container:not(.pressed) circle {
|
||||||
|
transition: transform 100ms ease-in-out, fill 100ms ease-in-out;
|
||||||
|
}
|
||||||
|
.container:not(.pressed) .cursor {
|
||||||
|
transition: transform 200ms ease-in-out;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-hs-color-picker": HaHsColorPicker;
|
||||||
|
}
|
||||||
|
}
|
354
src/components/ha-temp-color-picker.ts
Normal file
354
src/components/ha-temp-color-picker.ts
Normal file
@ -0,0 +1,354 @@
|
|||||||
|
import { DIRECTION_ALL, Manager, Pan, Tap } from "@egjs/hammerjs";
|
||||||
|
import { css, html, LitElement, PropertyValues, svg } from "lit";
|
||||||
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
|
import { classMap } from "lit/directives/class-map";
|
||||||
|
import { styleMap } from "lit/directives/style-map";
|
||||||
|
import { rgb2hex } from "../common/color/convert-color";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HASSDomEvents {
|
||||||
|
"cursor-moved": { value?: any };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function xy2polar(x: number, y: number) {
|
||||||
|
const r = Math.sqrt(x * x + y * y);
|
||||||
|
const phi = Math.atan2(y, x);
|
||||||
|
return [r, phi];
|
||||||
|
}
|
||||||
|
|
||||||
|
function polar2xy(r: number, phi: number) {
|
||||||
|
const x = Math.cos(phi) * r;
|
||||||
|
const y = Math.sin(phi) * r;
|
||||||
|
return [x, y];
|
||||||
|
}
|
||||||
|
|
||||||
|
function temperature2rgb(temperature: number): [number, number, number] {
|
||||||
|
const value = temperature / 100;
|
||||||
|
return [getRed(value), getGreen(value), getBlue(value)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRed(temperature: number): number {
|
||||||
|
if (temperature <= 66) {
|
||||||
|
return 255;
|
||||||
|
}
|
||||||
|
const tmp_red = 329.698727446 * (temperature - 60) ** -0.1332047592;
|
||||||
|
return clamp(tmp_red);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGreen(temperature: number): number {
|
||||||
|
let green: number;
|
||||||
|
if (temperature <= 66) {
|
||||||
|
green = 99.4708025861 * Math.log(temperature) - 161.1195681661;
|
||||||
|
} else {
|
||||||
|
green = 288.1221695283 * (temperature - 60) ** -0.0755148492;
|
||||||
|
}
|
||||||
|
return clamp(green);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBlue(temperature: number): number {
|
||||||
|
if (temperature >= 66) {
|
||||||
|
return 255;
|
||||||
|
}
|
||||||
|
if (temperature <= 19) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const blue = 138.5177312231 * Math.log(temperature - 10) - 305.0447927307;
|
||||||
|
return clamp(blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clamp(value: number): number {
|
||||||
|
return Math.max(0, Math.min(255, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawColorWheel(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
minTemp: number,
|
||||||
|
maxTemp: number
|
||||||
|
) {
|
||||||
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||||
|
const radius = ctx.canvas.width / 2;
|
||||||
|
|
||||||
|
const min = Math.max(minTemp, 2000);
|
||||||
|
const max = Math.min(maxTemp, 40000);
|
||||||
|
|
||||||
|
for (let y = -radius; y < radius; y += 1) {
|
||||||
|
const x = radius * Math.sqrt(1 - (y / radius) ** 2);
|
||||||
|
|
||||||
|
const fraction = (y / radius + 1) / 2;
|
||||||
|
|
||||||
|
const temperature = min + fraction * (max - min);
|
||||||
|
|
||||||
|
const color = rgb2hex(temperature2rgb(temperature));
|
||||||
|
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.fillRect(radius - x, radius + y - 0.5, 2 * x, 2);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement("ha-temp-color-picker")
|
||||||
|
class HaTempColorPicker extends LitElement {
|
||||||
|
@property({ type: Boolean, reflect: true })
|
||||||
|
public disabled = false;
|
||||||
|
|
||||||
|
@property({ type: Number, attribute: false })
|
||||||
|
public renderSize?: number;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public value?: number;
|
||||||
|
|
||||||
|
@property() min = 2000;
|
||||||
|
|
||||||
|
@property() max = 10000;
|
||||||
|
|
||||||
|
@query("#canvas") private _canvas!: HTMLCanvasElement;
|
||||||
|
|
||||||
|
private _mc?: HammerManager;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private _pressed?: string;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private _cursorPosition?: [number, number];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private _localValue?: number;
|
||||||
|
|
||||||
|
protected firstUpdated(changedProps: PropertyValues): void {
|
||||||
|
super.firstUpdated(changedProps);
|
||||||
|
this._setupListeners();
|
||||||
|
this._generateColorWheel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _generateColorWheel() {
|
||||||
|
const ctx = this._canvas.getContext("2d")!;
|
||||||
|
drawColorWheel(ctx, this.min, this.max);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback(): void {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._setupListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback(): void {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this._destroyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProps: PropertyValues): void {
|
||||||
|
super.updated(changedProps);
|
||||||
|
if (changedProps.has("min") || changedProps.has("max")) {
|
||||||
|
this._generateColorWheel();
|
||||||
|
this._resetPosition();
|
||||||
|
}
|
||||||
|
if (changedProps.has("value")) {
|
||||||
|
if (this.value !== undefined && this._localValue !== this.value) {
|
||||||
|
this._resetPosition();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_setupListeners() {
|
||||||
|
if (this._canvas && !this._mc) {
|
||||||
|
this._mc = new Manager(this._canvas);
|
||||||
|
this._mc.add(
|
||||||
|
new Pan({
|
||||||
|
direction: DIRECTION_ALL,
|
||||||
|
enable: true,
|
||||||
|
threshold: 0,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this._mc.add(new Tap({ event: "singletap" }));
|
||||||
|
|
||||||
|
let savedPosition;
|
||||||
|
this._mc.on("panstart", (e) => {
|
||||||
|
if (this.disabled) return;
|
||||||
|
this._pressed = e.pointerType;
|
||||||
|
savedPosition = this._cursorPosition;
|
||||||
|
});
|
||||||
|
this._mc.on("pancancel", () => {
|
||||||
|
if (this.disabled) return;
|
||||||
|
this._pressed = undefined;
|
||||||
|
this._cursorPosition = savedPosition;
|
||||||
|
});
|
||||||
|
this._mc.on("panmove", (e) => {
|
||||||
|
if (this.disabled) return;
|
||||||
|
this._cursorPosition = this._getPositionFromEvent(e);
|
||||||
|
this._localValue = this._getValueFromCoord(...this._cursorPosition);
|
||||||
|
fireEvent(this, "cursor-moved", { value: this._localValue });
|
||||||
|
});
|
||||||
|
this._mc.on("panend", (e) => {
|
||||||
|
if (this.disabled) return;
|
||||||
|
this._pressed = undefined;
|
||||||
|
this._cursorPosition = this._getPositionFromEvent(e);
|
||||||
|
this._localValue = this._getValueFromCoord(...this._cursorPosition);
|
||||||
|
fireEvent(this, "cursor-moved", { value: undefined });
|
||||||
|
fireEvent(this, "value-changed", { value: this._localValue });
|
||||||
|
});
|
||||||
|
|
||||||
|
this._mc.on("singletap", (e) => {
|
||||||
|
if (this.disabled) return;
|
||||||
|
this._cursorPosition = this._getPositionFromEvent(e);
|
||||||
|
this._localValue = this._getValueFromCoord(...this._cursorPosition);
|
||||||
|
fireEvent(this, "value-changed", { value: this._localValue });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _resetPosition() {
|
||||||
|
if (this.value === undefined) return;
|
||||||
|
const [, y] = this._getCoordsFromValue(this.value);
|
||||||
|
const currentX = this._cursorPosition?.[0] ?? 0;
|
||||||
|
const x =
|
||||||
|
Math.sign(currentX) * Math.min(Math.sqrt(1 - y ** 2), Math.abs(currentX));
|
||||||
|
this._cursorPosition = [x, y];
|
||||||
|
this._localValue = this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getCoordsFromValue = (temperature: number): [number, number] => {
|
||||||
|
const fraction = (temperature - this.min) / (this.max - this.min);
|
||||||
|
const y = 2 * fraction - 1;
|
||||||
|
return [0, y];
|
||||||
|
};
|
||||||
|
|
||||||
|
private _getValueFromCoord = (_x: number, y: number): number => {
|
||||||
|
const fraction = (y + 1) / 2;
|
||||||
|
const temperature = this.min + fraction * (this.max - this.min);
|
||||||
|
return Math.round(temperature);
|
||||||
|
};
|
||||||
|
|
||||||
|
private _getPositionFromEvent = (e: HammerInput): [number, number] => {
|
||||||
|
const x = e.center.x;
|
||||||
|
const y = e.center.y;
|
||||||
|
const boundingRect = e.target.getBoundingClientRect();
|
||||||
|
const offsetX = boundingRect.left;
|
||||||
|
const offsetY = boundingRect.top;
|
||||||
|
const maxX = e.target.clientWidth;
|
||||||
|
const maxY = e.target.clientHeight;
|
||||||
|
|
||||||
|
const _x = (2 * (x - offsetX)) / maxX - 1;
|
||||||
|
const _y = (2 * (y - offsetY)) / maxY - 1;
|
||||||
|
|
||||||
|
const [r, phi] = xy2polar(_x, _y);
|
||||||
|
const [__x, __y] = polar2xy(Math.min(1, r), phi);
|
||||||
|
return [__x, __y];
|
||||||
|
};
|
||||||
|
|
||||||
|
_destroyListeners() {
|
||||||
|
if (this._mc) {
|
||||||
|
this._mc.destroy();
|
||||||
|
this._mc = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const size = this.renderSize || 400;
|
||||||
|
const canvasSize = size * window.devicePixelRatio;
|
||||||
|
|
||||||
|
const rgb =
|
||||||
|
this._localValue !== undefined
|
||||||
|
? temperature2rgb(this._localValue)
|
||||||
|
: ([255, 255, 255] as [number, number, number]);
|
||||||
|
|
||||||
|
const [x, y] = this._cursorPosition ?? [0, 0];
|
||||||
|
|
||||||
|
const cx = ((x + 1) * size) / 2;
|
||||||
|
const cy = ((y + 1) * size) / 2;
|
||||||
|
|
||||||
|
const markerPosition = `${cx}px, ${cy}px`;
|
||||||
|
const markerScale = this._pressed ? "1.5" : "1";
|
||||||
|
const markerOffset =
|
||||||
|
this._pressed === "touch" ? `0px, -${size / 8}px` : "0px, 0px";
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="container ${classMap({ pressed: Boolean(this._pressed) })}">
|
||||||
|
<canvas id="canvas" .width=${canvasSize} .height=${canvasSize}></canvas>
|
||||||
|
<svg id="interaction" viewBox="0 0 ${size} ${size}" overflow="visible">
|
||||||
|
<defs>${this.renderSVGFilter()}</defs>
|
||||||
|
<g
|
||||||
|
style=${styleMap({
|
||||||
|
fill: rgb2hex(rgb),
|
||||||
|
transform: `translate(${markerPosition})`,
|
||||||
|
})}
|
||||||
|
class="cursor"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
cx="0"
|
||||||
|
cy="0"
|
||||||
|
r="16"
|
||||||
|
style=${styleMap({
|
||||||
|
fill: rgb2hex(rgb),
|
||||||
|
transform: `translate(${markerOffset}) scale(${markerScale})`,
|
||||||
|
visibility: this._cursorPosition ? undefined : "hidden",
|
||||||
|
})}
|
||||||
|
></circle>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSVGFilter() {
|
||||||
|
return svg`
|
||||||
|
<filter
|
||||||
|
id="marker-shadow"
|
||||||
|
x="-50%"
|
||||||
|
y="-50%"
|
||||||
|
width="200%"
|
||||||
|
height="200%"
|
||||||
|
filterUnits="objectBoundingBox"
|
||||||
|
>
|
||||||
|
<feDropShadow dx="0" dy="1" stdDeviation="2" flood-opacity="0.3" flood-color="rgba(0, 0, 0, 1)"/>
|
||||||
|
<feDropShadow dx="0" dy="1" stdDeviation="3" flood-opacity="0.15" flood-color="rgba(0, 0, 0, 1)"/>
|
||||||
|
</filter>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
svg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
circle {
|
||||||
|
fill: black;
|
||||||
|
stroke: white;
|
||||||
|
stroke-width: 2;
|
||||||
|
filter: url(#marker-shadow);
|
||||||
|
}
|
||||||
|
.container:not(.pressed) circle {
|
||||||
|
transition: transform 100ms ease-in-out, fill 100ms ease-in-out;
|
||||||
|
}
|
||||||
|
.container:not(.pressed) .cursor {
|
||||||
|
transition: transform 200ms ease-in-out;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-temp-color-picker": HaTempColorPicker;
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,6 @@
|
|||||||
import "@material/mwc-button";
|
import "@material/mwc-button";
|
||||||
import "@material/mwc-tab-bar/mwc-tab-bar";
|
import "@material/mwc-tab-bar/mwc-tab-bar";
|
||||||
import "@material/mwc-tab/mwc-tab";
|
import "@material/mwc-tab/mwc-tab";
|
||||||
import { mdiPalette } from "@mdi/js";
|
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
@ -11,12 +10,13 @@ import {
|
|||||||
PropertyValues,
|
PropertyValues,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { hs2rgb, rgb2hs } from "../../../../common/color/convert-color";
|
||||||
import { throttle } from "../../../../common/util/throttle";
|
import { throttle } from "../../../../common/util/throttle";
|
||||||
import "../../../../components/ha-button-toggle-group";
|
import "../../../../components/ha-button-toggle-group";
|
||||||
import "../../../../components/ha-color-picker";
|
import "../../../../components/ha-hs-color-picker";
|
||||||
import "../../../../components/ha-control-slider";
|
|
||||||
import "../../../../components/ha-icon-button-prev";
|
import "../../../../components/ha-icon-button-prev";
|
||||||
import "../../../../components/ha-labeled-slider";
|
import "../../../../components/ha-labeled-slider";
|
||||||
|
import "../../../../components/ha-temp-color-picker";
|
||||||
import {
|
import {
|
||||||
getLightCurrentModeRgbColor,
|
getLightCurrentModeRgbColor,
|
||||||
LightColorMode,
|
LightColorMode,
|
||||||
@ -35,8 +35,6 @@ class MoreInfoViewLightColorPicker extends LitElement {
|
|||||||
|
|
||||||
@property() public params?: LightColorPickerViewParams;
|
@property() public params?: LightColorPickerViewParams;
|
||||||
|
|
||||||
@state() private _ctSliderValue?: number;
|
|
||||||
|
|
||||||
@state() private _cwSliderValue?: number;
|
@state() private _cwSliderValue?: number;
|
||||||
|
|
||||||
@state() private _wwSliderValue?: number;
|
@state() private _wwSliderValue?: number;
|
||||||
@ -47,11 +45,9 @@ class MoreInfoViewLightColorPicker extends LitElement {
|
|||||||
|
|
||||||
@state() private _brightnessAdjusted?: number;
|
@state() private _brightnessAdjusted?: number;
|
||||||
|
|
||||||
@state() private _hueSegments = 24;
|
@state() private _hsPickerValue?: [number, number];
|
||||||
|
|
||||||
@state() private _saturationSegments = 8;
|
@state() private _ctPickerValue?: number;
|
||||||
|
|
||||||
@state() private _colorPickerColor?: [number, number, number];
|
|
||||||
|
|
||||||
@state() private _mode?: Mode;
|
@state() private _mode?: Mode;
|
||||||
|
|
||||||
@ -94,47 +90,34 @@ class MoreInfoViewLightColorPicker extends LitElement {
|
|||||||
)}
|
)}
|
||||||
</mwc-tab-bar>
|
</mwc-tab-bar>
|
||||||
`
|
`
|
||||||
: ""}
|
: nothing}
|
||||||
<div class="content">
|
<div class="content">
|
||||||
${this._mode === LightColorMode.COLOR_TEMP
|
${this._mode === LightColorMode.COLOR_TEMP
|
||||||
? html`
|
? html`
|
||||||
<p class="color-temp-value">
|
<p class="color-temp-value">
|
||||||
${this._ctSliderValue ? `${this._ctSliderValue} K` : nothing}
|
${this._ctPickerValue ? `${this._ctPickerValue} K` : nothing}
|
||||||
</p>
|
</p>
|
||||||
<ha-control-slider
|
<ha-temp-color-picker
|
||||||
vertical
|
@value-changed=${this._ctColorChanged}
|
||||||
class="color-temp"
|
@cursor-moved=${this._ctColorCursorMoved}
|
||||||
label=${this.hass.localize("ui.card.light.color_temperature")}
|
|
||||||
min="1"
|
|
||||||
max="100"
|
|
||||||
mode="cursor"
|
|
||||||
.value=${this._ctSliderValue}
|
|
||||||
@value-changed=${this._ctSliderChanged}
|
|
||||||
@slider-moved=${this._ctSliderMoved}
|
|
||||||
.min=${this.stateObj.attributes.min_color_temp_kelvin!}
|
.min=${this.stateObj.attributes.min_color_temp_kelvin!}
|
||||||
.max=${this.stateObj.attributes.max_color_temp_kelvin!}
|
.max=${this.stateObj.attributes.max_color_temp_kelvin!}
|
||||||
|
.value=${this._ctPickerValue}
|
||||||
>
|
>
|
||||||
</ha-control-slider>
|
</ha-temp-color-picker>
|
||||||
`
|
`
|
||||||
: ""}
|
: nothing}
|
||||||
${this._mode === "color"
|
${this._mode === "color"
|
||||||
? html`
|
? html`
|
||||||
<div class="segmentation-container">
|
<ha-hs-color-picker
|
||||||
<ha-color-picker
|
@value-changed=${this._hsColorChanged}
|
||||||
class="color"
|
@cursor-moved=${this._hsColorCursorMoved}
|
||||||
@colorselected=${this._colorPicked}
|
.value=${this._hsPickerValue}
|
||||||
.desiredRgbColor=${this._colorPickerColor}
|
.colorBrightness=${this._colorBrightnessSliderValue != null
|
||||||
throttle="500"
|
? (this._colorBrightnessSliderValue * 255) / 100
|
||||||
.hueSegments=${this._hueSegments}
|
: undefined}
|
||||||
.saturationSegments=${this._saturationSegments}
|
>
|
||||||
>
|
</ha-hs-color-picker>
|
||||||
</ha-color-picker>
|
|
||||||
<ha-icon-button
|
|
||||||
.path=${mdiPalette}
|
|
||||||
@click=${this._segmentClick}
|
|
||||||
class="segmentation-button"
|
|
||||||
></ha-icon-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${supportsRgbw || supportsRgbww
|
${supportsRgbw || supportsRgbww
|
||||||
? html`<ha-labeled-slider
|
? html`<ha-labeled-slider
|
||||||
@ -188,9 +171,9 @@ class MoreInfoViewLightColorPicker extends LitElement {
|
|||||||
pin
|
pin
|
||||||
></ha-labeled-slider>
|
></ha-labeled-slider>
|
||||||
`
|
`
|
||||||
: ""}
|
: nothing}
|
||||||
`
|
`
|
||||||
: ""}
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -212,7 +195,7 @@ class MoreInfoViewLightColorPicker extends LitElement {
|
|||||||
this._brightnessAdjusted = maxVal;
|
this._brightnessAdjusted = maxVal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._ctSliderValue =
|
this._ctPickerValue =
|
||||||
stateObj.attributes.color_mode === LightColorMode.COLOR_TEMP
|
stateObj.attributes.color_mode === LightColorMode.COLOR_TEMP
|
||||||
? stateObj.attributes.color_temp_kelvin
|
? stateObj.attributes.color_temp_kelvin
|
||||||
: undefined;
|
: undefined;
|
||||||
@ -239,14 +222,12 @@ class MoreInfoViewLightColorPicker extends LitElement {
|
|||||||
? Math.round((Math.max(...currentRgbColor.slice(0, 3)) * 100) / 255)
|
? Math.round((Math.max(...currentRgbColor.slice(0, 3)) * 100) / 255)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
this._colorPickerColor = currentRgbColor?.slice(0, 3) as [
|
this._hsPickerValue = currentRgbColor
|
||||||
number,
|
? rgb2hs(currentRgbColor.slice(0, 3) as [number, number, number])
|
||||||
number,
|
: undefined;
|
||||||
number
|
|
||||||
];
|
|
||||||
} else {
|
} else {
|
||||||
this._colorPickerColor = [0, 0, 0];
|
this._hsPickerValue = [0, 0];
|
||||||
this._ctSliderValue = undefined;
|
this._ctPickerValue = undefined;
|
||||||
this._wvSliderValue = undefined;
|
this._wvSliderValue = undefined;
|
||||||
this._cwSliderValue = undefined;
|
this._cwSliderValue = undefined;
|
||||||
this._wwSliderValue = undefined;
|
this._wwSliderValue = undefined;
|
||||||
@ -295,14 +276,79 @@ class MoreInfoViewLightColorPicker extends LitElement {
|
|||||||
this._mode = newMode;
|
this._mode = newMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _ctSliderMoved(ev: CustomEvent) {
|
private _hsColorCursorMoved(ev: CustomEvent) {
|
||||||
|
if (!ev.detail.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._hsPickerValue = ev.detail.value;
|
||||||
|
|
||||||
|
this._throttleUpdateColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _throttleUpdateColor = throttle(() => this._updateColor(), 500);
|
||||||
|
|
||||||
|
private _updateColor() {
|
||||||
|
const hs_color = this._hsPickerValue!;
|
||||||
|
const rgb_color = hs2rgb(hs_color);
|
||||||
|
|
||||||
|
if (
|
||||||
|
lightSupportsColorMode(this.stateObj!, LightColorMode.RGBWW) ||
|
||||||
|
lightSupportsColorMode(this.stateObj!, LightColorMode.RGBW)
|
||||||
|
) {
|
||||||
|
this._setRgbWColor(
|
||||||
|
this._colorBrightnessSliderValue
|
||||||
|
? this._adjustColorBrightness(
|
||||||
|
rgb_color,
|
||||||
|
(this._colorBrightnessSliderValue * 255) / 100
|
||||||
|
)
|
||||||
|
: rgb_color
|
||||||
|
);
|
||||||
|
} else if (lightSupportsColorMode(this.stateObj!, LightColorMode.RGB)) {
|
||||||
|
if (this._brightnessAdjusted) {
|
||||||
|
const brightnessAdjust = (this._brightnessAdjusted / 255) * 100;
|
||||||
|
const brightnessPercentage = Math.round(
|
||||||
|
((this.stateObj!.attributes.brightness || 0) * brightnessAdjust) / 255
|
||||||
|
);
|
||||||
|
this.hass.callService("light", "turn_on", {
|
||||||
|
entity_id: this.stateObj!.entity_id,
|
||||||
|
brightness_pct: brightnessPercentage,
|
||||||
|
rgb_color: this._adjustColorBrightness(
|
||||||
|
rgb_color,
|
||||||
|
this._brightnessAdjusted,
|
||||||
|
true
|
||||||
|
),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.hass.callService("light", "turn_on", {
|
||||||
|
entity_id: this.stateObj!.entity_id,
|
||||||
|
rgb_color,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.hass.callService("light", "turn_on", {
|
||||||
|
entity_id: this.stateObj!.entity_id,
|
||||||
|
hs_color: [hs_color[0], hs_color[1] * 100],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _hsColorChanged(ev: CustomEvent) {
|
||||||
|
if (!ev.detail.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._hsPickerValue = ev.detail.value;
|
||||||
|
|
||||||
|
this._updateColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _ctColorCursorMoved(ev: CustomEvent) {
|
||||||
const ct = ev.detail.value;
|
const ct = ev.detail.value;
|
||||||
|
|
||||||
if (isNaN(ct) || this._ctSliderValue === ct) {
|
if (isNaN(ct) || this._ctPickerValue === ct) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._ctSliderValue = ct;
|
this._ctPickerValue = ct;
|
||||||
|
|
||||||
this._throttleUpdateColorTemp();
|
this._throttleUpdateColorTemp();
|
||||||
}
|
}
|
||||||
@ -310,18 +356,18 @@ class MoreInfoViewLightColorPicker extends LitElement {
|
|||||||
private _throttleUpdateColorTemp = throttle(() => {
|
private _throttleUpdateColorTemp = throttle(() => {
|
||||||
this.hass.callService("light", "turn_on", {
|
this.hass.callService("light", "turn_on", {
|
||||||
entity_id: this.stateObj!.entity_id,
|
entity_id: this.stateObj!.entity_id,
|
||||||
color_temp_kelvin: this._ctSliderValue,
|
color_temp_kelvin: this._ctPickerValue,
|
||||||
});
|
});
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
private _ctSliderChanged(ev: CustomEvent) {
|
private _ctColorChanged(ev: CustomEvent) {
|
||||||
const ct = ev.detail.value;
|
const ct = ev.detail.value;
|
||||||
|
|
||||||
if (isNaN(ct) || this._ctSliderValue === ct) {
|
if (isNaN(ct) || this._ctPickerValue === ct) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._ctSliderValue = ct;
|
this._ctPickerValue = ct;
|
||||||
|
|
||||||
this.hass.callService("light", "turn_on", {
|
this.hass.callService("light", "turn_on", {
|
||||||
entity_id: this.stateObj!.entity_id,
|
entity_id: this.stateObj!.entity_id,
|
||||||
@ -399,16 +445,6 @@ class MoreInfoViewLightColorPicker extends LitElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _segmentClick() {
|
|
||||||
if (this._hueSegments === 24 && this._saturationSegments === 8) {
|
|
||||||
this._hueSegments = 0;
|
|
||||||
this._saturationSegments = 0;
|
|
||||||
} else {
|
|
||||||
this._hueSegments = 24;
|
|
||||||
this._saturationSegments = 8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _adjustColorBrightness(
|
private _adjustColorBrightness(
|
||||||
rgbColor: [number, number, number],
|
rgbColor: [number, number, number],
|
||||||
value?: number,
|
value?: number,
|
||||||
@ -448,68 +484,6 @@ class MoreInfoViewLightColorPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when a new color has been picked.
|
|
||||||
* should be throttled with the 'throttle=' attribute of the color picker
|
|
||||||
*/
|
|
||||||
private _colorPicked(
|
|
||||||
ev: CustomEvent<{
|
|
||||||
hs: { h: number; s: number };
|
|
||||||
rgb: { r: number; g: number; b: number };
|
|
||||||
}>
|
|
||||||
) {
|
|
||||||
this._colorPickerColor = [
|
|
||||||
ev.detail.rgb.r,
|
|
||||||
ev.detail.rgb.g,
|
|
||||||
ev.detail.rgb.b,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (
|
|
||||||
lightSupportsColorMode(this.stateObj!, LightColorMode.RGBWW) ||
|
|
||||||
lightSupportsColorMode(this.stateObj!, LightColorMode.RGBW)
|
|
||||||
) {
|
|
||||||
this._setRgbWColor(
|
|
||||||
this._colorBrightnessSliderValue
|
|
||||||
? this._adjustColorBrightness(
|
|
||||||
[ev.detail.rgb.r, ev.detail.rgb.g, ev.detail.rgb.b],
|
|
||||||
(this._colorBrightnessSliderValue * 255) / 100
|
|
||||||
)
|
|
||||||
: [ev.detail.rgb.r, ev.detail.rgb.g, ev.detail.rgb.b]
|
|
||||||
);
|
|
||||||
} else if (lightSupportsColorMode(this.stateObj!, LightColorMode.RGB)) {
|
|
||||||
const rgb_color: [number, number, number] = [
|
|
||||||
ev.detail.rgb.r,
|
|
||||||
ev.detail.rgb.g,
|
|
||||||
ev.detail.rgb.b,
|
|
||||||
];
|
|
||||||
if (this._brightnessAdjusted) {
|
|
||||||
const brightnessAdjust = (this._brightnessAdjusted / 255) * 100;
|
|
||||||
const brightnessPercentage = Math.round(
|
|
||||||
((this.stateObj!.attributes.brightness || 0) * brightnessAdjust) / 255
|
|
||||||
);
|
|
||||||
this.hass.callService("light", "turn_on", {
|
|
||||||
entity_id: this.stateObj!.entity_id,
|
|
||||||
brightness_pct: brightnessPercentage,
|
|
||||||
rgb_color: this._adjustColorBrightness(
|
|
||||||
rgb_color,
|
|
||||||
this._brightnessAdjusted,
|
|
||||||
true
|
|
||||||
),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.hass.callService("light", "turn_on", {
|
|
||||||
entity_id: this.stateObj!.entity_id,
|
|
||||||
rgb_color,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.hass.callService("light", "turn_on", {
|
|
||||||
entity_id: this.stateObj!.entity_id,
|
|
||||||
hs_color: [ev.detail.hs.h, ev.detail.hs.s * 100],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
css`
|
css`
|
||||||
@ -526,35 +500,16 @@ class MoreInfoViewLightColorPicker extends LitElement {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.segmentation-container {
|
ha-hs-color-picker {
|
||||||
position: relative;
|
max-width: 320px;
|
||||||
max-height: 500px;
|
min-width: 200px;
|
||||||
display: flex;
|
margin: 44px 0 44px 0;
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.segmentation-button {
|
ha-temp-color-picker {
|
||||||
position: absolute;
|
max-width: 320px;
|
||||||
top: 5%;
|
min-width: 200px;
|
||||||
left: 0;
|
margin: 20px 0 44px 0;
|
||||||
color: var(--secondary-text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
ha-color-picker {
|
|
||||||
--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;
|
|
||||||
}
|
|
||||||
|
|
||||||
ha-control-slider {
|
|
||||||
height: 45vh;
|
|
||||||
max-height: 320px;
|
|
||||||
min-height: 200px;
|
|
||||||
margin: 20px 0;
|
|
||||||
--control-slider-thickness: 100px;
|
|
||||||
--control-slider-border-radius: 24px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-labeled-slider {
|
ha-labeled-slider {
|
||||||
@ -572,17 +527,6 @@ class MoreInfoViewLightColorPicker extends LitElement {
|
|||||||
direction: ltr;
|
direction: ltr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-temp {
|
|
||||||
--control-slider-background: -webkit-linear-gradient(
|
|
||||||
top,
|
|
||||||
rgb(166, 209, 255) 0%,
|
|
||||||
white 50%,
|
|
||||||
rgb(255, 160, 0) 100%
|
|
||||||
);
|
|
||||||
--control-slider-background-opacity: 1;
|
|
||||||
margin-bottom: 44px;
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
border-color: var(--divider-color);
|
border-color: var(--divider-color);
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user