Skip to content

Commit 894950b

Browse files
authored
feat: add seasonal visual improvements (#1820)
### Description Add optional seasonal visuals, toggleable between 19th December and 20th of January. ### How Has This Been Tested: Verified that toggle does not display in incorrect time periods, made sure the visual changes look good, made sure the visuals are not obstructive. ### How Has This Change Been Documented: Not applicable.
1 parent f492c87 commit 894950b

File tree

12 files changed

+333
-7
lines changed

12 files changed

+333
-7
lines changed

packages/vscode-extension/package-lock.json

Lines changed: 22 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/vscode-extension/package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,12 @@
409409
"scope": "window",
410410
"default": true,
411411
"description": "Shows device frame in the IDE panel."
412+
},
413+
"RadonIDE.userInterface.christmasMode": {
414+
"type": "boolean",
415+
"scope": "window",
416+
"default": false,
417+
"description": "Enables festive mode in the IDE panel."
412418
}
413419
}
414420
},
@@ -1357,7 +1363,8 @@
13571363
"vscode-test": "^1.6.1",
13581364
"ws": "^8.18.1",
13591365
"xml2js": "^0.6.2",
1360-
"zod": "^3.25.32"
1366+
"zod": "^3.25.32",
1367+
"react-snowfall": "^2.4.0"
13611368
},
13621369
"optionalDependencies": {
13631370
"@rollup/rollup-linux-x64-gnu": "4.40.0"

packages/vscode-extension/src/common/State.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ export type RadonAISettings = {
9797
export type UserInterfaceSettings = {
9898
panelLocation: PanelLocation;
9999
showDeviceFrame: boolean;
100+
christmasMode?: boolean;
100101
};
101102

102103
export type DeviceControlSettings = {
@@ -627,6 +628,7 @@ export const initialState: State = {
627628
userInterface: {
628629
panelLocation: "tab",
629630
showDeviceFrame: true,
631+
christmasMode: false,
630632
},
631633
deviceSettings: {
632634
deviceRotation: DeviceRotation.Portrait,

packages/vscode-extension/src/utilities/workspaceConfiguration.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const WorkspaceConfigurationKeyMap = {
2323
userInterface: {
2424
panelLocation: "userInterface.panelLocation",
2525
showDeviceFrame: "userInterface.showDeviceFrame",
26+
christmasMode: "userInterface.christmasMode",
2627
},
2728
deviceSettings: {
2829
deviceRotation: "deviceSettings.deviceRotation",
@@ -64,6 +65,8 @@ export function getCurrentWorkspaceConfiguration(config: WorkspaceConfiguration)
6465
"tab",
6566
showDeviceFrame:
6667
config.get<boolean>(WorkspaceConfigurationKeyMap.userInterface.showDeviceFrame) ?? true,
68+
christmasMode:
69+
config.get<boolean>(WorkspaceConfigurationKeyMap.userInterface.christmasMode) ?? false,
6770
},
6871
deviceControl: {
6972
startDeviceOnLaunch:

packages/vscode-extension/src/webview/components/PreviewLoader.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import classNames from "classnames";
22
import { useEffect, useState } from "react";
3+
import { use$ } from "@legendapp/state/react";
34

45
import "./PreviewLoader.css";
56

@@ -10,7 +11,6 @@ import { useProject } from "../providers/ProjectProvider";
1011
import Button from "./shared/Button";
1112
import { Output } from "../../common/OutputChannel";
1213
import { useStore } from "../providers/storeProvider";
13-
import { use$ } from "@legendapp/state/react";
1414
import {
1515
DevicePlatform,
1616
DeviceRotation,
@@ -19,6 +19,8 @@ import {
1919
} from "../../common/State";
2020
import { useSelectedDeviceSessionState } from "../hooks/selectedSession";
2121

22+
import AsciiChristmasTree from "./christmas/AsciiChristmasTree";
23+
2224
const startupStageWeightSum = StartupStageWeight.map((item) => item.weight).reduce(
2325
(acc, cur) => acc + cur,
2426
0
@@ -111,8 +113,11 @@ function PreviewLoader({ onRequestShowPreview }: { onRequestShowPreview: () => v
111113
}
112114
}
113115

116+
const christmasMode = use$(store$.workspaceConfiguration.userInterface.christmasMode);
117+
114118
return (
115119
<div className={`preview-loader-wrapper ${isLandscape ? "landscape" : "portrait"}`}>
120+
{christmasMode && <AsciiChristmasTree />}
116121
<div className="preview-loader-load-info">
117122
<button className="preview-loader-container" onClick={handleLoaderClick}>
118123
<div className="preview-loader-button-group">

packages/vscode-extension/src/webview/components/SettingsDropdown.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { ActivateLicenseView } from "../views/ActivateLicenseView";
1616
import { LicenseStatus } from "../../common/License";
1717
import ExportLogsView from "../views/ExportLogsView";
1818

19+
import { ChristmasModeToggle } from "./christmas/ChristmasModeToggle";
20+
1921
interface SettingsDropdownProps {
2022
children: React.ReactNode;
2123
isDeviceRunning: boolean;
@@ -166,6 +168,7 @@ function SettingsDropdown({ project, isDeviceRunning, children, disabled }: Sett
166168
</DropdownMenu.Item>
167169
{telemetryEnabled && <SendFeedbackItem />}
168170
{shouldShowActivateLicenseItem && <ActivateLicenseItem />}
171+
<ChristmasModeToggle />
169172
<div className="dropdown-menu-item device-settings-version-text">
170173
Radon IDE version: {extensionVersion}
171174
</div>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
.ascii-christmas-tree {
2+
font-family: monospace;
3+
font-size: 10px;
4+
line-height: 1;
5+
margin: 0;
6+
padding: 0;
7+
white-space: pre;
8+
user-select: none;
9+
text-align: center;
10+
}
11+
12+
.tree-row {
13+
display: block;
14+
white-space: pre;
15+
}
16+
17+
.preview-loader-tree-container {
18+
filter: contrast(0.5);
19+
opacity: 0.5;
20+
position: absolute;
21+
bottom: 0;
22+
margin-bottom: 10px;
23+
pointer-events: none;
24+
}
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import { useState, useEffect, useMemo } from "react";
2+
import "./AsciiChristmasTree.css";
3+
4+
interface Pixel {
5+
value: string;
6+
color?: string;
7+
}
8+
9+
const COLORS = {
10+
white: "#ffffff",
11+
green: "#338a3d",
12+
yellow: "#dfc321",
13+
red: "#c22525",
14+
magenta: "#1f52b8",
15+
gray: "#808080",
16+
};
17+
18+
function AsciiChristmasTree({ width = 46, height = 19 }: { width?: number; height?: number }) {
19+
const [baubleColors, setBaubleColors] = useState<string[]>([
20+
COLORS.yellow,
21+
COLORS.red,
22+
COLORS.magenta,
23+
]);
24+
25+
// Rotate bauble colors every 500ms for blinking effect
26+
useEffect(() => {
27+
const interval = setInterval(() => {
28+
setBaubleColors((prev) => {
29+
const rotated = [...prev];
30+
rotated.push(rotated.shift()!);
31+
return rotated;
32+
});
33+
}, 1000);
34+
35+
return () => clearInterval(interval);
36+
}, []);
37+
38+
const treeImage = useMemo(() => {
39+
return generateChristmasScene(width, height, baubleColors);
40+
}, [width, height, baubleColors]);
41+
42+
return (
43+
<div className="preview-loader-tree-container">
44+
<pre className="ascii-christmas-tree" aria-label="ASCII Christmas Tree">
45+
{treeImage.map((row, i) => (
46+
<div key={i} className="tree-row">
47+
{row.map((pixel, j) => (
48+
<span
49+
key={j}
50+
style={{
51+
color: pixel.color || "inherit",
52+
}}>
53+
{pixel.value || " "}
54+
</span>
55+
))}
56+
</div>
57+
))}
58+
</pre>
59+
</div>
60+
);
61+
}
62+
63+
function generateChristmasScene(width: number, _height: number, baubleColors: string[]): Pixel[][] {
64+
// Generate single centered tree with fixed size
65+
const treeWidth = 21;
66+
const tree = generateTree(treeWidth, baubleColors, 0);
67+
68+
// Calculate the actual height needed for the tree
69+
const actualHeight = tree.length;
70+
71+
// Create empty background with calculated height
72+
const image: Pixel[][] = Array(actualHeight)
73+
.fill(null)
74+
.map(() =>
75+
Array(width)
76+
.fill(null)
77+
.map(() => ({ value: " " }))
78+
);
79+
80+
const x = Math.floor((width - treeWidth) / 2);
81+
pasteTree(image, tree, x, 0);
82+
83+
return image;
84+
}
85+
86+
function generateTree(w: number, baubleColors: string[], offset: number): Pixel[][] {
87+
// Ensure odd width
88+
if (w % 2 === 0) {
89+
w -= 1;
90+
}
91+
92+
const tree: Pixel[][] = [];
93+
94+
// Star on top
95+
tree.push(centerPixels(w, pixels(" * ", COLORS.yellow)));
96+
97+
// Tree crown
98+
tree.push(centerPixels(w, pixels(" /_\\ ", COLORS.green)));
99+
tree.push(centerPixels(w, pixels(" /_\\_\\ ", COLORS.green)));
100+
101+
// Tree body
102+
for (let i = 3; i < (w - 2) / 2; i++) {
103+
const left = ` ${"_\\".repeat(i)} `;
104+
const right = ` ${"/_".repeat(i)}\\ `;
105+
tree.push(centerPixels(w, pixels(left, COLORS.green)));
106+
tree.push(centerPixels(w, pixels(right, COLORS.green)));
107+
}
108+
109+
// Add baubles - need to modify tree directly, not slices
110+
for (let rowIdx = 2; rowIdx < tree.length; rowIdx++) {
111+
const row = tree[rowIdx];
112+
const third = Math.floor(w / 3);
113+
114+
const colors = [...baubleColors];
115+
// Rotate colors based on row for variation
116+
for (let r = 0; r < (offset + rowIdx) % 3; r++) {
117+
colors.push(colors.shift()!);
118+
}
119+
120+
// Place baubles in three sections of the row
121+
const sectionRanges = [
122+
{ start: 0, end: third },
123+
{ start: third, end: third * 2 },
124+
{ start: third * 2, end: w },
125+
];
126+
127+
sectionRanges.forEach((range, sectionIdx) => {
128+
// Find all underscore positions in this section
129+
const underscorePositions: number[] = [];
130+
for (let j = range.start; j < range.end && j < row.length; j++) {
131+
if (row[j].value === "_") {
132+
underscorePositions.push(j);
133+
}
134+
}
135+
136+
// Place a bauble at a random underscore position
137+
if (underscorePositions.length > 0) {
138+
const randomIdx = Math.floor(Math.random() * underscorePositions.length);
139+
const pos = underscorePositions[randomIdx];
140+
row[pos] = { value: "*", color: colors[sectionIdx] };
141+
}
142+
});
143+
}
144+
145+
// Add pot
146+
if (w >= 15) {
147+
tree.push(centerPixels(w, pixels(" [_____] ", COLORS.gray)));
148+
tree.push(centerPixels(w, pixels(" \\___/ ", COLORS.gray)));
149+
} else {
150+
const n = w >= 10 ? 3 : 1;
151+
tree.push(centerPixels(w, pixels(` \\${"_".repeat(n)}/ `, COLORS.gray)));
152+
}
153+
154+
return tree;
155+
}
156+
157+
function pixels(str: string, color: string): Pixel[] {
158+
return str.split("").map((c) => ({ value: c, color }));
159+
}
160+
161+
function centerPixels(width: number, pixelArray: Pixel[]): Pixel[] {
162+
const row: Pixel[] = Array(width)
163+
.fill(null)
164+
.map(() => ({ value: " " }));
165+
const start = Math.floor((width - pixelArray.length) / 2);
166+
167+
for (let i = 0; i < pixelArray.length; i++) {
168+
row[start + i] = pixelArray[i];
169+
}
170+
171+
return row;
172+
}
173+
174+
function pasteTree(bg: Pixel[][], fg: Pixel[][], x: number, y: number): void {
175+
const maxY = bg.length - y;
176+
177+
for (let i = 0; i < fg.length; i++) {
178+
const row = fg[fg.length - 1 - i];
179+
for (let j = 0; j < row.length; j++) {
180+
if (row[j].value !== " ") {
181+
const bgRow = maxY - i - 1;
182+
if (bgRow >= 0 && bgRow < bg.length && x + j < bg[0].length) {
183+
bg[bgRow][x + j] = row[j];
184+
}
185+
}
186+
}
187+
}
188+
}
189+
190+
export default AsciiChristmasTree;

0 commit comments

Comments
 (0)