Analog clock: CSS animation and seconds motion options (#26943)
* Increase analog clock interval and improve accuracy * Restore * Use CSS to render hands instead of JS interval with resync to adjust offsets * Option * Remove * Option * Fixpull/27151/head
parent
3d173ad03e
commit
88ac56ac0b
|
@ -4,7 +4,6 @@ import { customElement, property, state } from "lit/decorators";
|
|||
import { classMap } from "lit/directives/class-map";
|
||||
import type { ClockCardConfig } from "../types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { INTERVAL } from "../hui-clock-card";
|
||||
import { resolveTimeZone } from "../../../../common/datetime/resolve-time-zone";
|
||||
|
||||
function romanize12HourClock(num: number) {
|
||||
|
@ -35,13 +34,11 @@ export class HuiClockCardAnalog extends LitElement {
|
|||
|
||||
@state() private _dateTimeFormat?: Intl.DateTimeFormat;
|
||||
|
||||
@state() private _hourDeg?: number;
|
||||
@state() private _hourOffsetSec?: number;
|
||||
|
||||
@state() private _minuteDeg?: number;
|
||||
@state() private _minuteOffsetSec?: number;
|
||||
|
||||
@state() private _secondDeg?: number;
|
||||
|
||||
private _tickInterval?: undefined | number;
|
||||
@state() private _secondOffsetSec?: number;
|
||||
|
||||
private _initDate() {
|
||||
if (!this.config || !this.hass) {
|
||||
|
@ -63,7 +60,7 @@ export class HuiClockCardAnalog extends LitElement {
|
|||
resolveTimeZone(locale.time_zone, this.hass.config?.time_zone),
|
||||
});
|
||||
|
||||
this._tick();
|
||||
this._computeOffsets();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
|
@ -77,27 +74,25 @@ export class HuiClockCardAnalog extends LitElement {
|
|||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._startTick();
|
||||
document.addEventListener("visibilitychange", this._handleVisibilityChange);
|
||||
this._computeOffsets();
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._stopTick();
|
||||
document.removeEventListener(
|
||||
"visibilitychange",
|
||||
this._handleVisibilityChange
|
||||
);
|
||||
}
|
||||
|
||||
private _startTick() {
|
||||
this._tickInterval = window.setInterval(() => this._tick(), INTERVAL);
|
||||
this._tick();
|
||||
}
|
||||
|
||||
private _stopTick() {
|
||||
if (this._tickInterval) {
|
||||
clearInterval(this._tickInterval);
|
||||
this._tickInterval = undefined;
|
||||
private _handleVisibilityChange = () => {
|
||||
if (!document.hidden) {
|
||||
this._computeOffsets();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private _tick() {
|
||||
private _computeOffsets() {
|
||||
if (!this._dateTimeFormat) return;
|
||||
|
||||
const parts = this._dateTimeFormat.formatToParts();
|
||||
|
@ -108,10 +103,14 @@ export class HuiClockCardAnalog extends LitElement {
|
|||
const hour = hourStr ? parseInt(hourStr, 10) : 0;
|
||||
const minute = minuteStr ? parseInt(minuteStr, 10) : 0;
|
||||
const second = secondStr ? parseInt(secondStr, 10) : 0;
|
||||
const ms = new Date().getMilliseconds();
|
||||
const secondsWithMs = second + ms / 1000;
|
||||
|
||||
this._hourDeg = hour * 30 + minute * 0.5; // 30deg per hour + 0.5deg per minute
|
||||
this._minuteDeg = minute * 6 + second * 0.1; // 6deg per minute + 0.1deg per second
|
||||
this._secondDeg = this.config?.show_seconds ? second * 6 : undefined; // 6deg per second
|
||||
const hour12 = hour % 12;
|
||||
|
||||
this._secondOffsetSec = secondsWithMs;
|
||||
this._minuteOffsetSec = minute * 60 + secondsWithMs;
|
||||
this._hourOffsetSec = hour12 * 3600 + minute * 60 + secondsWithMs;
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -212,16 +211,24 @@ export class HuiClockCardAnalog extends LitElement {
|
|||
<div class="center-dot"></div>
|
||||
<div
|
||||
class="hand hour"
|
||||
style=${`--hand-rotation: ${this._hourDeg ?? 0}deg;`}
|
||||
style=${`animation-delay: -${this._hourOffsetSec ?? 0}s;`}
|
||||
></div>
|
||||
<div
|
||||
class="hand minute"
|
||||
style=${`--hand-rotation: ${this._minuteDeg ?? 0}deg;`}
|
||||
style=${`animation-delay: -${this._minuteOffsetSec ?? 0}s;`}
|
||||
></div>
|
||||
${this.config.show_seconds
|
||||
? html`<div
|
||||
class="hand second"
|
||||
style=${`--hand-rotation: ${this._secondDeg ?? 0}deg;`}
|
||||
class=${classMap({
|
||||
hand: true,
|
||||
second: true,
|
||||
step: this.config.seconds_motion === "tick",
|
||||
})}
|
||||
style=${`animation-delay: -${
|
||||
(this.config.seconds_motion === "tick"
|
||||
? Math.floor(this._secondOffsetSec ?? 0)
|
||||
: (this._secondOffsetSec ?? 0)) as number
|
||||
}s;`}
|
||||
></div>`
|
||||
: nothing}
|
||||
</div>
|
||||
|
@ -350,10 +357,13 @@ export class HuiClockCardAnalog extends LitElement {
|
|||
left: 50%;
|
||||
bottom: 50%;
|
||||
transform-origin: 50% 100%;
|
||||
transform: translate(-50%, 0) rotate(var(--hand-rotation, 0deg));
|
||||
transform: translate(-50%, 0) rotate(0deg);
|
||||
background: var(--primary-text-color);
|
||||
border-radius: 2px;
|
||||
will-change: transform;
|
||||
animation-name: ha-clock-rotate;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.hand.hour {
|
||||
|
@ -362,6 +372,7 @@ export class HuiClockCardAnalog extends LitElement {
|
|||
background: var(--primary-text-color);
|
||||
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.2);
|
||||
z-index: 1;
|
||||
animation-duration: 43200s; /* 12 hours */
|
||||
}
|
||||
|
||||
.hand.minute {
|
||||
|
@ -371,6 +382,7 @@ export class HuiClockCardAnalog extends LitElement {
|
|||
box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.2);
|
||||
opacity: 0.9;
|
||||
z-index: 3;
|
||||
animation-duration: 3600s; /* 60 minutes */
|
||||
}
|
||||
|
||||
.hand.second {
|
||||
|
@ -380,6 +392,20 @@ export class HuiClockCardAnalog extends LitElement {
|
|||
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.2);
|
||||
opacity: 0.8;
|
||||
z-index: 2;
|
||||
animation-duration: 60s; /* 60 seconds */
|
||||
}
|
||||
|
||||
.hand.second.step {
|
||||
animation-timing-function: steps(60, end);
|
||||
}
|
||||
|
||||
@keyframes ha-clock-rotate {
|
||||
from {
|
||||
transform: translate(-50%, 0) rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: translate(-50%, 0) rotate(360deg);
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
|
|
@ -3,10 +3,11 @@ import type { PropertyValues } from "lit";
|
|||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { ClockCardConfig } from "../types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { INTERVAL } from "../hui-clock-card";
|
||||
import { useAmPm } from "../../../../common/datetime/use_am_pm";
|
||||
import { resolveTimeZone } from "../../../../common/datetime/resolve-time-zone";
|
||||
|
||||
const INTERVAL = 1000;
|
||||
|
||||
@customElement("hui-clock-card-digital")
|
||||
export class HuiClockCardDigital extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
|
|
@ -11,8 +11,6 @@ import type {
|
|||
} from "../types";
|
||||
import type { ClockCardConfig } from "./types";
|
||||
|
||||
export const INTERVAL = 1000;
|
||||
|
||||
@customElement("hui-clock-card")
|
||||
export class HuiClockCard extends LitElement implements LovelaceCard {
|
||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||
|
|
|
@ -375,6 +375,7 @@ export interface ClockCardConfig extends LovelaceCardConfig {
|
|||
clock_style?: "digital" | "analog";
|
||||
clock_size?: "small" | "medium" | "large";
|
||||
show_seconds?: boolean | undefined;
|
||||
seconds_motion?: "continuous" | "tick";
|
||||
time_format?: TimeFormat;
|
||||
time_zone?: string;
|
||||
no_background?: boolean;
|
||||
|
|
|
@ -52,6 +52,12 @@ const cardConfigStruct = assign(
|
|||
literal("hour")
|
||||
)
|
||||
),
|
||||
seconds_motion: optional(
|
||||
defaulted(
|
||||
union([literal("continuous"), literal("tick")]),
|
||||
literal("continuous")
|
||||
)
|
||||
),
|
||||
face_style: optional(
|
||||
defaulted(
|
||||
union([
|
||||
|
@ -78,7 +84,8 @@ export class HuiClockCardEditor
|
|||
(
|
||||
localize: LocalizeFunc,
|
||||
clockStyle: ClockCardConfig["clock_style"],
|
||||
ticks: ClockCardConfig["ticks"]
|
||||
ticks: ClockCardConfig["ticks"],
|
||||
showSeconds: boolean | undefined
|
||||
) =>
|
||||
[
|
||||
{ name: "title", selector: { text: {} } },
|
||||
|
@ -170,6 +177,33 @@ export class HuiClockCardEditor
|
|||
},
|
||||
},
|
||||
},
|
||||
...(showSeconds
|
||||
? ([
|
||||
{
|
||||
name: "seconds_motion",
|
||||
description: {
|
||||
suffix: localize(
|
||||
`ui.panel.lovelace.editor.card.clock.seconds_motion.description`
|
||||
),
|
||||
},
|
||||
default: "continuous",
|
||||
selector: {
|
||||
select: {
|
||||
mode: "dropdown",
|
||||
options: ["continuous", "tick"].map((value) => ({
|
||||
value,
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.clock.seconds_motion.${value}.label`
|
||||
),
|
||||
description: localize(
|
||||
`ui.panel.lovelace.editor.card.clock.seconds_motion.${value}.description`
|
||||
),
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[])
|
||||
: []),
|
||||
...(ticks !== "none"
|
||||
? ([
|
||||
{
|
||||
|
@ -257,7 +291,8 @@ export class HuiClockCardEditor
|
|||
.schema=${this._schema(
|
||||
this.hass.localize,
|
||||
this._data(this._config).clock_style,
|
||||
this._data(this._config).ticks
|
||||
this._data(this._config).ticks,
|
||||
this._data(this._config).show_seconds
|
||||
)}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
.computeHelper=${this._computeHelperCallback}
|
||||
|
@ -278,10 +313,17 @@ export class HuiClockCardEditor
|
|||
ev.detail.value.border = ev.detail.value.border ?? false;
|
||||
ev.detail.value.ticks = ev.detail.value.ticks ?? "hour";
|
||||
ev.detail.value.face_style = ev.detail.value.face_style ?? "markers";
|
||||
if (ev.detail.value.show_seconds) {
|
||||
ev.detail.value.seconds_motion =
|
||||
ev.detail.value.seconds_motion ?? "continuous";
|
||||
} else {
|
||||
delete ev.detail.value.seconds_motion;
|
||||
}
|
||||
} else {
|
||||
delete ev.detail.value.border;
|
||||
delete ev.detail.value.ticks;
|
||||
delete ev.detail.value.face_style;
|
||||
delete ev.detail.value.seconds_motion;
|
||||
}
|
||||
|
||||
if (ev.detail.value.ticks !== "none") {
|
||||
|
@ -333,6 +375,10 @@ export class HuiClockCardEditor
|
|||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.ticks.label`
|
||||
);
|
||||
case "seconds_motion":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.seconds_motion.label`
|
||||
);
|
||||
case "face_style":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.face_style.label`
|
||||
|
@ -354,6 +400,10 @@ export class HuiClockCardEditor
|
|||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.ticks.description`
|
||||
);
|
||||
case "seconds_motion":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.seconds_motion.description`
|
||||
);
|
||||
case "face_style":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.face_style.description`
|
||||
|
|
|
@ -7830,6 +7830,18 @@
|
|||
"description": "60 ticks (Every minute)"
|
||||
}
|
||||
},
|
||||
"seconds_motion": {
|
||||
"label": "Seconds motion",
|
||||
"description": "How the seconds hand moves on the clock",
|
||||
"continuous": {
|
||||
"label": "Continuous",
|
||||
"description": "The seconds hand moves continuously"
|
||||
},
|
||||
"tick": {
|
||||
"label": "Tick",
|
||||
"description": "The seconds hand moves in 1 second steps"
|
||||
}
|
||||
},
|
||||
"face_style": {
|
||||
"label": "Clock face style",
|
||||
"description": "Which kind of indices to use for the clock face",
|
||||
|
|
Loading…
Reference in New Issue