Skip to content

Commit 974ac31

Browse files
authored
Winter is coming (#28036)
* Winter is coming * Set max to 30 on mobile * Change storage key * Use core * Add browser level switch for winter mode * Simplify logic
1 parent 47e98d5 commit 974ac31

File tree

2 files changed

+180
-0
lines changed

2 files changed

+180
-0
lines changed

src/components/ha-snowflakes.ts

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import { css, html, LitElement, nothing } from "lit";
2+
import { customElement, property, state } from "lit/decorators";
3+
import type { HomeAssistant } from "../types";
4+
import { subscribeLabFeatures } from "../data/labs";
5+
import { SubscribeMixin } from "../mixins/subscribe-mixin";
6+
7+
interface Snowflake {
8+
id: number;
9+
left: number;
10+
size: number;
11+
duration: number;
12+
delay: number;
13+
blur: number;
14+
}
15+
16+
@customElement("ha-snowflakes")
17+
export class HaSnowflakes extends SubscribeMixin(LitElement) {
18+
@property({ attribute: false }) public hass?: HomeAssistant;
19+
20+
@property({ type: Boolean }) public narrow = false;
21+
22+
@state() private _enabled = false;
23+
24+
@state() private _snowflakes: Snowflake[] = [];
25+
26+
private _maxSnowflakes = 50;
27+
28+
public hassSubscribe() {
29+
return [
30+
subscribeLabFeatures(this.hass!.connection, (features) => {
31+
this._enabled =
32+
features.find(
33+
(f) =>
34+
f.domain === "frontend" && f.preview_feature === "winter_mode"
35+
)?.enabled ?? false;
36+
}),
37+
];
38+
}
39+
40+
private _generateSnowflakes() {
41+
if (!this._enabled) {
42+
this._snowflakes = [];
43+
return;
44+
}
45+
46+
const snowflakes: Snowflake[] = [];
47+
for (let i = 0; i < this._maxSnowflakes; i++) {
48+
snowflakes.push({
49+
id: i,
50+
left: Math.random() * 100, // Random position from 0-100%
51+
size: Math.random() * 12 + 8, // Random size between 8-20px
52+
duration: Math.random() * 8 + 8, // Random duration between 8-16s
53+
delay: Math.random() * 8, // Random delay between 0-8s
54+
blur: Math.random() * 1, // Random blur between 0-1px
55+
});
56+
}
57+
this._snowflakes = snowflakes;
58+
}
59+
60+
protected willUpdate(changedProps: Map<string, unknown>) {
61+
super.willUpdate(changedProps);
62+
if (changedProps.has("_enabled")) {
63+
this._generateSnowflakes();
64+
}
65+
}
66+
67+
protected render() {
68+
if (!this._enabled) {
69+
return nothing;
70+
}
71+
72+
const isDark = this.hass?.themes.darkMode ?? false;
73+
74+
return html`
75+
<div class="snowflakes ${isDark ? "dark" : "light"}" aria-hidden="true">
76+
${this._snowflakes.map(
77+
(flake) => html`
78+
<div
79+
class="snowflake ${this.narrow && flake.id >= 30
80+
? "hide-narrow"
81+
: ""}"
82+
style="
83+
left: ${flake.left}%;
84+
font-size: ${flake.size}px;
85+
animation-duration: ${flake.duration}s;
86+
animation-delay: ${flake.delay}s;
87+
filter: blur(${flake.blur}px);
88+
"
89+
>
90+
91+
</div>
92+
`
93+
)}
94+
</div>
95+
`;
96+
}
97+
98+
static readonly styles = css`
99+
:host {
100+
display: block;
101+
position: fixed;
102+
top: 0;
103+
left: 0;
104+
width: 100%;
105+
height: 100%;
106+
pointer-events: none;
107+
z-index: 9999;
108+
overflow: hidden;
109+
}
110+
111+
.snowflakes {
112+
position: absolute;
113+
top: -10%;
114+
left: 0;
115+
width: 100%;
116+
height: 110%;
117+
pointer-events: none;
118+
}
119+
120+
.snowflake {
121+
position: absolute;
122+
top: -10%;
123+
opacity: 0.7;
124+
user-select: none;
125+
pointer-events: none;
126+
animation: fall linear infinite;
127+
}
128+
129+
.light .snowflake {
130+
color: #00bcd4;
131+
text-shadow:
132+
0 0 5px #00bcd4,
133+
0 0 10px #00e5ff;
134+
}
135+
136+
.dark .snowflake {
137+
color: #fff;
138+
text-shadow:
139+
0 0 5px rgba(255, 255, 255, 0.8),
140+
0 0 10px rgba(255, 255, 255, 0.5);
141+
}
142+
143+
.snowflake.hide-narrow {
144+
display: none;
145+
}
146+
147+
@keyframes fall {
148+
0% {
149+
transform: translateY(-10vh) translateX(0);
150+
}
151+
25% {
152+
transform: translateY(30vh) translateX(10px);
153+
}
154+
50% {
155+
transform: translateY(60vh) translateX(-10px);
156+
}
157+
75% {
158+
transform: translateY(85vh) translateX(10px);
159+
}
160+
100% {
161+
transform: translateY(120vh) translateX(0);
162+
}
163+
}
164+
165+
@media (prefers-reduced-motion: reduce) {
166+
.snowflake {
167+
animation: none;
168+
display: none;
169+
}
170+
}
171+
`;
172+
}
173+
174+
declare global {
175+
interface HTMLElementTagNameMap {
176+
"ha-snowflakes": HaSnowflakes;
177+
}
178+
}

src/layouts/home-assistant-main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { listenMediaQuery } from "../common/dom/media_query";
77
import { toggleAttribute } from "../common/dom/toggle_attribute";
88
import { computeRTLDirection } from "../common/util/compute_rtl";
99
import "../components/ha-drawer";
10+
import "../components/ha-snowflakes";
1011
import { showNotificationDrawer } from "../dialogs/notifications/show-notification-drawer";
1112
import type { HomeAssistant, Route } from "../types";
1213
import "./partial-panel-resolver";
@@ -50,6 +51,7 @@ export class HomeAssistantMain extends LitElement {
5051
this.hass.panels && this.hass.userData && this.hass.systemData;
5152

5253
return html`
54+
<ha-snowflakes .hass=${this.hass} .narrow=${this.narrow}></ha-snowflakes>
5355
<ha-drawer
5456
.type=${sidebarNarrow ? "modal" : ""}
5557
.open=${sidebarNarrow ? this._drawerOpen : false}

0 commit comments

Comments
 (0)