mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-23 17:26:42 +00:00
Update circular slider design (#17490)
This commit is contained in:
parent
2bbce135bb
commit
e9e31d51ec
@ -150,17 +150,13 @@ export class DemoHaCircularSlider extends LitElement {
|
|||||||
}
|
}
|
||||||
ha-control-circular-slider {
|
ha-control-circular-slider {
|
||||||
--control-circular-slider-color: #ff9800;
|
--control-circular-slider-color: #ff9800;
|
||||||
--control-circular-slider-background: #ff9800;
|
|
||||||
--control-circular-slider-background-opacity: 0.3;
|
|
||||||
}
|
}
|
||||||
ha-control-circular-slider[inverted] {
|
ha-control-circular-slider[inverted] {
|
||||||
--control-circular-slider-color: #2196f3;
|
--control-circular-slider-color: #2196f3;
|
||||||
--control-circular-slider-background: #2196f3;
|
|
||||||
}
|
}
|
||||||
ha-control-circular-slider[dual] {
|
ha-control-circular-slider[dual] {
|
||||||
--control-circular-slider-high-color: #2196f3;
|
--control-circular-slider-high-color: #2196f3;
|
||||||
--control-circular-slider-low-color: #ff9800;
|
--control-circular-slider-low-color: #ff9800;
|
||||||
--control-circular-slider-background: var(--disabled-color);
|
|
||||||
}
|
}
|
||||||
.field {
|
.field {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -18,10 +18,9 @@ import {
|
|||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { ifDefined } from "lit/directives/if-defined";
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
import { styleMap } from "lit/directives/style-map";
|
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { clamp } from "../common/number/clamp";
|
import { clamp } from "../common/number/clamp";
|
||||||
import { arc } from "../resources/svg-arc";
|
import { svgArc } from "../resources/svg-arc";
|
||||||
|
|
||||||
const MAX_ANGLE = 270;
|
const MAX_ANGLE = 270;
|
||||||
const ROTATE_ANGLE = 360 - MAX_ANGLE / 2 - 90;
|
const ROTATE_ANGLE = 360 - MAX_ANGLE / 2 - 90;
|
||||||
@ -388,44 +387,130 @@ export class HaControlCircularSlider extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _strokeDashArc(
|
private _strokeCircleDashArc(value: number): [string, string] {
|
||||||
percentage: number,
|
return this._strokeDashArc(value, value);
|
||||||
inverted?: boolean
|
}
|
||||||
): [string, string] {
|
|
||||||
const maxRatio = MAX_ANGLE / 360;
|
private _strokeDashArc(from: number, to: number): [string, string] {
|
||||||
const f = RADIUS * 2 * Math.PI;
|
const start = this._valueToPercentage(from);
|
||||||
if (inverted) {
|
const end = this._valueToPercentage(to);
|
||||||
const arcLength = (1 - percentage) * f * maxRatio;
|
|
||||||
const strokeDasharray = `${arcLength} ${f - arcLength}`;
|
const track = (RADIUS * 2 * Math.PI * MAX_ANGLE) / 360;
|
||||||
const strokeDashOffset = `${arcLength + f * (1 - maxRatio)}`;
|
const arc = Math.max((end - start) * track, 0);
|
||||||
return [strokeDasharray, strokeDashOffset];
|
const arcOffset = start * track - 0.5;
|
||||||
}
|
|
||||||
const arcLength = percentage * f * maxRatio;
|
const strokeDasharray = `${arc} ${track - arc}`;
|
||||||
const strokeDasharray = `${arcLength} ${f - arcLength}`;
|
const strokeDashOffset = `-${arcOffset}`;
|
||||||
const strokeDashOffset = "0";
|
|
||||||
return [strokeDasharray, strokeDashOffset];
|
return [strokeDasharray, strokeDashOffset];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected renderArc(
|
||||||
|
id: string,
|
||||||
|
value: number | undefined,
|
||||||
|
inverted: boolean | undefined
|
||||||
|
) {
|
||||||
|
const limit = inverted ? this.max : this.min;
|
||||||
|
|
||||||
|
const path = svgArc({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
start: 0,
|
||||||
|
end: MAX_ANGLE,
|
||||||
|
r: RADIUS,
|
||||||
|
});
|
||||||
|
|
||||||
|
const current = this.current ?? limit;
|
||||||
|
const target = value ?? limit;
|
||||||
|
|
||||||
|
const showActive = inverted ? target <= current : current <= target;
|
||||||
|
|
||||||
|
const activeArcDashArray = showActive
|
||||||
|
? inverted
|
||||||
|
? this._strokeDashArc(target, current)
|
||||||
|
: this._strokeDashArc(current, target)
|
||||||
|
: this._strokeCircleDashArc(target);
|
||||||
|
|
||||||
|
const arcDashArray = inverted
|
||||||
|
? this._strokeDashArc(target, limit)
|
||||||
|
: this._strokeDashArc(limit, target);
|
||||||
|
|
||||||
|
const targetCircleDashArray = this._strokeCircleDashArc(target);
|
||||||
|
|
||||||
|
const currentCircleDashArray =
|
||||||
|
this.current != null && showActive
|
||||||
|
? this._strokeCircleDashArc(this.current)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return svg`
|
||||||
|
<path
|
||||||
|
class="arc arc-clear"
|
||||||
|
d=${path}
|
||||||
|
stroke-dasharray=${arcDashArray[0]}
|
||||||
|
stroke-dashoffset=${arcDashArray[1]}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
class="arc arc-background ${classMap({ [id]: true })}"
|
||||||
|
d=${path}
|
||||||
|
stroke-dasharray=${arcDashArray[0]}
|
||||||
|
stroke-dashoffset=${arcDashArray[1]}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
.id=${id}
|
||||||
|
d=${path}
|
||||||
|
class="arc arc-active ${classMap({ [id]: true })}"
|
||||||
|
stroke-dasharray=${activeArcDashArray[0]}
|
||||||
|
stroke-dashoffset=${activeArcDashArray[1]}
|
||||||
|
role="slider"
|
||||||
|
tabindex="0"
|
||||||
|
aria-valuemin=${this.min}
|
||||||
|
aria-valuemax=${this.max}
|
||||||
|
aria-valuenow=${
|
||||||
|
this._localValue != null
|
||||||
|
? this._steppedValue(this._localValue)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
aria-disabled=${this.disabled}
|
||||||
|
aria-label=${ifDefined(this.lowLabel ?? this.label)}
|
||||||
|
@keydown=${this._handleKeyDown}
|
||||||
|
@keyup=${this._handleKeyUp}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
class="target"
|
||||||
|
d=${path}
|
||||||
|
stroke-dasharray=${targetCircleDashArray[0]}
|
||||||
|
stroke-dashoffset=${targetCircleDashArray[1]}
|
||||||
|
/>
|
||||||
|
${
|
||||||
|
currentCircleDashArray
|
||||||
|
? svg`
|
||||||
|
<path
|
||||||
|
class="current"
|
||||||
|
d=${path}
|
||||||
|
stroke-dasharray=${currentCircleDashArray[0]}
|
||||||
|
stroke-dashoffset=${currentCircleDashArray[1]}
|
||||||
|
/>
|
||||||
|
`
|
||||||
|
: nothing
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
const trackPath = arc({ x: 0, y: 0, start: 0, end: MAX_ANGLE, r: RADIUS });
|
const trackPath = svgArc({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
start: 0,
|
||||||
|
end: MAX_ANGLE,
|
||||||
|
r: RADIUS,
|
||||||
|
});
|
||||||
|
|
||||||
const lowValue = this.dual ? this._localLow : this._localValue;
|
const lowValue = this.dual ? this._localLow : this._localValue;
|
||||||
const highValue = this._localHigh;
|
const highValue = this._localHigh;
|
||||||
const lowPercentage = this._valueToPercentage(lowValue ?? this.min);
|
const current = this.current;
|
||||||
const highPercentage = this._valueToPercentage(highValue ?? this.max);
|
|
||||||
|
|
||||||
const [lowStrokeDasharray, lowStrokeDashOffset] = this._strokeDashArc(
|
const currentStroke = current
|
||||||
lowPercentage,
|
? this._strokeCircleDashArc(current)
|
||||||
this.inverted
|
: undefined;
|
||||||
);
|
|
||||||
|
|
||||||
const [highStrokeDasharray, highStrokeDashOffset] = this._strokeDashArc(
|
|
||||||
highPercentage,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentPercentage = this._valueToPercentage(this.current ?? 0);
|
|
||||||
const currentAngle = currentPercentage * MAX_ANGLE;
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<svg
|
<svg
|
||||||
@ -447,79 +532,25 @@ export class HaControlCircularSlider extends LitElement {
|
|||||||
</g>
|
</g>
|
||||||
<g id="display">
|
<g id="display">
|
||||||
<path class="background" d=${trackPath} />
|
<path class="background" d=${trackPath} />
|
||||||
${lowValue != null
|
${currentStroke
|
||||||
? svg`
|
? svg`
|
||||||
<circle
|
<path
|
||||||
.id=${this.dual ? "low" : "value"}
|
class="current"
|
||||||
class="track"
|
d=${trackPath}
|
||||||
cx="0"
|
stroke-dasharray=${currentStroke[0]}
|
||||||
cy="0"
|
stroke-dashoffset=${currentStroke[1]}
|
||||||
r=${RADIUS}
|
/>
|
||||||
stroke-dasharray=${lowStrokeDasharray}
|
`
|
||||||
stroke-dashoffset=${lowStrokeDashOffset}
|
: nothing}
|
||||||
role="slider"
|
${lowValue != null
|
||||||
tabindex="0"
|
? this.renderArc(
|
||||||
aria-valuemin=${this.min}
|
this.dual ? "low" : "value",
|
||||||
aria-valuemax=${this.max}
|
lowValue,
|
||||||
aria-valuenow=${
|
this.inverted
|
||||||
lowValue != null ? this._steppedValue(lowValue) : undefined
|
)
|
||||||
}
|
|
||||||
aria-disabled=${this.disabled}
|
|
||||||
aria-label=${ifDefined(this.lowLabel ?? this.label)}
|
|
||||||
@keydown=${this._handleKeyDown}
|
|
||||||
@keyup=${this._handleKeyUp}
|
|
||||||
/>
|
|
||||||
`
|
|
||||||
: nothing}
|
: nothing}
|
||||||
${this.dual && highValue != null
|
${this.dual && highValue != null
|
||||||
? svg`
|
? this.renderArc("high", highValue, true)
|
||||||
<circle
|
|
||||||
id="high"
|
|
||||||
class="track"
|
|
||||||
cx="0"
|
|
||||||
cy="0"
|
|
||||||
r=${RADIUS}
|
|
||||||
stroke-dasharray=${highStrokeDasharray}
|
|
||||||
stroke-dashoffset=${highStrokeDashOffset}
|
|
||||||
role="slider"
|
|
||||||
tabindex="0"
|
|
||||||
aria-valuemin=${this.min}
|
|
||||||
aria-valuemax=${this.max}
|
|
||||||
aria-valuenow=${
|
|
||||||
highValue != null
|
|
||||||
? this._steppedValue(highValue)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
aria-disabled=${this.disabled}
|
|
||||||
aria-label=${ifDefined(this.highLabel)}
|
|
||||||
@keydown=${this._handleKeyDown}
|
|
||||||
@keyup=${this._handleKeyUp}
|
|
||||||
/>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
${this.current != null
|
|
||||||
? svg`
|
|
||||||
<g
|
|
||||||
style=${styleMap({ "--current-angle": `${currentAngle}deg` })}
|
|
||||||
class="current"
|
|
||||||
>
|
|
||||||
<line
|
|
||||||
x1=${RADIUS - 12}
|
|
||||||
y1="0"
|
|
||||||
x2=${RADIUS - 15}
|
|
||||||
y2="0"
|
|
||||||
stroke-width="4"
|
|
||||||
/>
|
|
||||||
<line
|
|
||||||
x1=${RADIUS - 15}
|
|
||||||
y1="0"
|
|
||||||
x2=${RADIUS - 20}
|
|
||||||
y2="0"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-width="4"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
`
|
|
||||||
: nothing}
|
: nothing}
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
@ -531,7 +562,7 @@ export class HaControlCircularSlider extends LitElement {
|
|||||||
return css`
|
return css`
|
||||||
:host {
|
:host {
|
||||||
--control-circular-slider-color: var(--primary-color);
|
--control-circular-slider-color: var(--primary-color);
|
||||||
--control-circular-slider-background: #8b97a3;
|
--control-circular-slider-background: var(--disabled-color);
|
||||||
--control-circular-slider-background-opacity: 0.3;
|
--control-circular-slider-background-opacity: 0.3;
|
||||||
--control-circular-slider-low-color: var(
|
--control-circular-slider-low-color: var(
|
||||||
--control-circular-slider-color
|
--control-circular-slider-color
|
||||||
@ -573,8 +604,7 @@ export class HaControlCircularSlider extends LitElement {
|
|||||||
stroke-width: 24px;
|
stroke-width: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.track {
|
.arc {
|
||||||
outline: none;
|
|
||||||
fill: none;
|
fill: none;
|
||||||
stroke-linecap: round;
|
stroke-linecap: round;
|
||||||
stroke-width: 24px;
|
stroke-width: 24px;
|
||||||
@ -586,29 +616,64 @@ export class HaControlCircularSlider extends LitElement {
|
|||||||
opacity 180ms ease-in-out;
|
opacity 180ms ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.track:focus-visible {
|
.target {
|
||||||
stroke-width: 28px;
|
fill: none;
|
||||||
}
|
stroke-linecap: round;
|
||||||
|
stroke-width: 18px;
|
||||||
.pressed .track {
|
stroke: white;
|
||||||
transition: stroke-width 300ms ease-in-out;
|
transition:
|
||||||
|
stroke-width 300ms ease-in-out,
|
||||||
|
stroke-dasharray 300ms ease-in-out,
|
||||||
|
stroke-dashoffset 300ms ease-in-out,
|
||||||
|
stroke 180ms ease-in-out,
|
||||||
|
opacity 180ms ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.current {
|
.current {
|
||||||
stroke: var(--primary-text-color);
|
fill: none;
|
||||||
transform: rotate(var(--current-angle, 0));
|
stroke-linecap: round;
|
||||||
transition: transform 300ms ease-in-out;
|
stroke-width: 8px;
|
||||||
|
stroke: white;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition:
|
||||||
|
stroke-width 300ms ease-in-out,
|
||||||
|
stroke-dasharray 300ms ease-in-out,
|
||||||
|
stroke-dashoffset 300ms ease-in-out,
|
||||||
|
stroke 180ms ease-in-out,
|
||||||
|
opacity 180ms ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
#value {
|
.arc-clear {
|
||||||
|
stroke: var(--clear-background-color);
|
||||||
|
}
|
||||||
|
.arc-background {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.arc-active {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.arc-active:focus-visible {
|
||||||
|
stroke-width: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pressed .arc,
|
||||||
|
.pressed .target,
|
||||||
|
.pressed .current {
|
||||||
|
transition:
|
||||||
|
stroke-width 300ms ease-in-out,
|
||||||
|
stroke 180ms ease-in-out,
|
||||||
|
opacity 180ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
stroke: var(--control-circular-slider-color);
|
stroke: var(--control-circular-slider-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
#low {
|
.low {
|
||||||
stroke: var(--control-circular-slider-low-color);
|
stroke: var(--control-circular-slider-low-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
#high {
|
.high {
|
||||||
stroke: var(--control-circular-slider-high-color);
|
stroke: var(--control-circular-slider-high-color);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -45,7 +45,8 @@ documentContainer.innerHTML = `<custom-style>
|
|||||||
--card-background-color: #ffffff;
|
--card-background-color: #ffffff;
|
||||||
--primary-background-color: #fafafa;
|
--primary-background-color: #fafafa;
|
||||||
--secondary-background-color: #e5e5e5; /* behind the cards on state */
|
--secondary-background-color: #e5e5e5; /* behind the cards on state */
|
||||||
|
--clear-background-color: #ffffff;
|
||||||
|
|
||||||
/* for header */
|
/* for header */
|
||||||
--header-height: 56px;
|
--header-height: 56px;
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ export const darkStyles = {
|
|||||||
"primary-background-color": "#111111",
|
"primary-background-color": "#111111",
|
||||||
"card-background-color": "#1c1c1c",
|
"card-background-color": "#1c1c1c",
|
||||||
"secondary-background-color": "#282828",
|
"secondary-background-color": "#282828",
|
||||||
|
"clear-background-color": "#111111",
|
||||||
"primary-text-color": "#e1e1e1",
|
"primary-text-color": "#e1e1e1",
|
||||||
"secondary-text-color": "#9b9b9b",
|
"secondary-text-color": "#9b9b9b",
|
||||||
"disabled-text-color": "#6f6f6f",
|
"disabled-text-color": "#6f6f6f",
|
||||||
|
@ -25,7 +25,7 @@ type ArcOptions = {
|
|||||||
rotate?: number;
|
rotate?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const arc = (options: ArcOptions) => {
|
export const svgArc = (options: ArcOptions) => {
|
||||||
const { x, y, r, start, end, rotate = 0 } = options;
|
const { x, y, r, start, end, rotate = 0 } = options;
|
||||||
const cx = x;
|
const cx = x;
|
||||||
const cy = y;
|
const cy = y;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user