Skip to content

Commit 05f23d7

Browse files
committed
refactor: tidy up and comments
1 parent 8d8ddf1 commit 05f23d7

1 file changed

Lines changed: 108 additions & 50 deletions

File tree

website/src/components/HdiLogo.astro

Lines changed: 108 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,26 @@
44
role="img"
55
aria-label="hdi">
66
_____ _____ _____
7-
||h |||d |||i ||
7+
||<span class="hero-logo-letter" data-key="h">h</span> |||<span class="hero-logo-letter" data-key="d">d</span> |||<span class="hero-logo-letter" data-key="i">i</span> ||
88
||___|||___|||___||
99
|/___\|/___\|/___\|</pre>
1010

1111
<script>
12-
// ── Logo key press animation ──────────────────────────────────────────
13-
// Each key has 4 rows of exactly 6 chars.
14-
// "pressed" shifts the key down one row: top becomes spaces, shadow gone.
12+
// Animates the ASCII art logo so h, d, i keys visually depress when pressed
13+
// (via keyboard or mouse click). Letters also highlight on hover.
14+
//
15+
// Each key is a 6-char-wide column rendered across 4 rows:
16+
// row 0 — top cap: ' _____'
17+
// row 1 — letter: '||h |'
18+
// row 2 — bottom cap: '||___|'
19+
// row 3 — shadow: '|/___\'
20+
//
21+
// The "pressed" state shifts the column down one row, replacing the top cap
22+
// with spaces and dropping the shadow — giving the illusion of a key sinking.
23+
//
24+
// The static <pre> above is the no-JS fallback; render() rewrites it with
25+
// interactive <span> elements once JS loads.
26+
1527
// prettier-ignore
1628
const KEY_COLS: Record<string, { normal: string[]; pressed: string[] }> = {
1729
// row0 row1 row2 row3
@@ -20,91 +32,137 @@
2032
pressed: [' ', ' _____', '||h |', '||___|' ],
2133
},
2234
d: {
35+
// Leading '|' on pressed row1 closes h's right wall when h is up
2336
normal: [' _____', '||d |', '||___|', '|/___\\'],
24-
pressed: [' ', ' _____', '||d |', '||___|' ],
37+
pressed: [' ', '|_____', '||d |', '||___|' ],
2538
},
2639
i: {
40+
// Same as d — leading '|' closes d's right wall when d is up
2741
normal: [' _____', '||i |', '||___|', '|/___\\'],
28-
pressed: [' ', ' _____', '||i |', '||___|' ],
42+
pressed: [' ', '|_____', '||i |', '||___|' ],
2943
},
3044
};
31-
// Trailing char appended after the 3 key columns, one per row:
32-
// Row 1 suffix disappears when "i" (last key) is pressed
33-
const LOGO_ROW_SUFFIX = [" ", "|", "|", "|"];
34-
const LOGO_ROW_SUFFIX_I_PRESSED = [" ", " ", "|", "|"];
3545

36-
const _pressedKeys = new Set<string>();
37-
const _logoPre = document.getElementById("hero-logo-pre")!;
46+
// Trailing character appended after the 3 key columns, one entry per row.
47+
// Row 1's trailing '|' closes i's right wall — replaced with ' ' when i is pressed.
48+
const ROW_SUFFIX = [" ", "|", "|", "|"];
49+
50+
// Maps each key to its left neighbour, used to detect adjacent pressed keys.
51+
const KEY_LEFT: Record<string, string> = { d: "h", i: "d" };
52+
53+
const pressedKeys = new Set<string>();
54+
let hoveredKey: string | null = null;
55+
const logoPre = document.getElementById("hero-logo-pre")!;
3856

39-
function _renderLogo() {
57+
function render() {
4058
const keys = ["h", "d", "i"] as const;
41-
const rows = [0, 1, 2, 3].map(
42-
(row) =>
59+
const rows = [0, 1, 2, 3].map((row) => {
60+
// When i is pressed its trailing wall disappears (key shifted down)
61+
const suffix = row === 1 && pressedKeys.has("i") ? " " : ROW_SUFFIX[row];
62+
return (
4363
keys
4464
.map((k) => {
45-
const seg = (
46-
_pressedKeys.has(k) ? KEY_COLS[k].pressed : KEY_COLS[k].normal
65+
let seg = (
66+
pressedKeys.has(k) ? KEY_COLS[k].pressed : KEY_COLS[k].normal
4767
)[row];
48-
// Position 2 in each 6-char segment is the letter — wrap it in a clickable span
68+
// On row 1, a pressed key's leading '|' closes the left neighbour's key wall.
69+
// If the left neighbour is also pressed that wall is gone — drop the '|'.
70+
if (
71+
row === 1 &&
72+
pressedKeys.has(k) &&
73+
pressedKeys.has(KEY_LEFT[k])
74+
) {
75+
seg = " " + seg.slice(1);
76+
}
77+
// Position 2 of each segment is always the letter — wrap it in a span
4978
return /[a-z]/.test(seg[2])
5079
? `${seg.slice(0, 2)}<span class="hero-logo-letter" data-key="${k}">${seg[2]}</span>${seg.slice(3)}`
5180
: seg;
5281
})
53-
.join("") +
54-
(_pressedKeys.has("i") ? LOGO_ROW_SUFFIX_I_PRESSED : LOGO_ROW_SUFFIX)[
55-
row
56-
],
57-
);
58-
_logoPre.innerHTML = rows.join("\n");
82+
.join("") + suffix
83+
);
84+
});
85+
logoPre.innerHTML = rows.join("\n");
86+
applyClasses();
5987
}
6088

61-
function _pressLogoKey(key: string) {
62-
if (_pressedKeys.has(key)) return;
63-
_pressedKeys.add(key);
64-
_renderLogo();
89+
// Apply hover class without touching the DOM structure,
90+
// so CSS transitions on color have existing elements to animate between.
91+
function applyClasses() {
92+
logoPre.querySelectorAll<HTMLElement>(".hero-logo-letter").forEach((el) => {
93+
el.classList.toggle("hovered", el.dataset.key === hoveredKey);
94+
});
6595
}
66-
function _releaseLogoKey(key: string) {
67-
if (!_pressedKeys.has(key)) return;
68-
_pressedKeys.delete(key);
69-
_renderLogo();
96+
97+
// Maps a mouse event to a key by dividing the pre into 19 character columns.
98+
// h: cols 0–5, d: cols 6–11, i: cols 12–17, col 18 is the trailing '|' (ignored).
99+
function keyFromMouse(e: MouseEvent): string | null {
100+
const rect = logoPre.getBoundingClientRect();
101+
const col = Math.floor((e.clientX - rect.left) / (rect.width / 19));
102+
return col < 6 ? "h" : col < 12 ? "d" : col < 18 ? "i" : null;
70103
}
71104

72-
_logoPre.addEventListener("mousedown", (e) => {
73-
// Measure one character's width using the pre's own font
74-
const charWidth = _logoPre.getBoundingClientRect().width / 19; // logo is 19 chars wide
75-
const rect = _logoPre.getBoundingClientRect();
76-
const col = Math.floor((e.clientX - rect.left) / charWidth);
77-
// h: cols 0-5, d: cols 6-11, i: cols 12-17 (col 18 is trailing '|', ignore)
78-
const key = col < 6 ? "h" : col < 12 ? "d" : col < 18 ? "i" : null;
79-
if (key) _pressLogoKey(key);
105+
// Mouse
106+
logoPre.addEventListener("mousemove", (e) => {
107+
const key = keyFromMouse(e);
108+
if (key === hoveredKey) return;
109+
hoveredKey = key;
110+
applyClasses(); // class-only update — preserves spans so transition fires
111+
});
112+
logoPre.addEventListener("mouseleave", () => {
113+
if (hoveredKey === null) return;
114+
hoveredKey = null;
115+
applyClasses();
116+
});
117+
logoPre.addEventListener("mousedown", (e) => {
118+
const key = keyFromMouse(e);
119+
if (key) {
120+
pressedKeys.add(key);
121+
render();
122+
}
80123
});
81124
document.addEventListener("mouseup", () => {
82-
[..._pressedKeys].forEach((k) => _releaseLogoKey(k));
125+
if (pressedKeys.size === 0) return;
126+
pressedKeys.clear();
127+
render();
83128
});
129+
130+
// Keyboard — skip when focus is in a text input to avoid interfering with typing
131+
const KEYS = new Set(["h", "d", "i"]);
132+
const isTextField = (t: EventTarget | null) =>
133+
t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement;
84134
document.addEventListener("keydown", (e) => {
85-
if (
86-
["h", "d", "i"].includes(e.key) &&
87-
!(e.target instanceof HTMLInputElement) &&
88-
!(e.target instanceof HTMLTextAreaElement)
89-
)
90-
_pressLogoKey(e.key);
135+
if (KEYS.has(e.key) && !isTextField(e.target)) {
136+
pressedKeys.add(e.key);
137+
render();
138+
}
91139
});
92140
document.addEventListener("keyup", (e) => {
93-
if (["h", "d", "i"].includes(e.key)) _releaseLogoKey(e.key);
141+
if (KEYS.has(e.key)) {
142+
pressedKeys.delete(e.key);
143+
render();
144+
}
94145
});
95146

96-
_renderLogo();
147+
render();
97148
</script>
98149

99150
<style>
100151
.hero-logo {
101152
font-size: 1.5rem;
102153
line-height: initial;
103-
cursor: pointer;
154+
cursor: default;
104155
user-select: none;
156+
@media (scripting: enabled) {
157+
cursor: pointer;
158+
}
105159

106160
.hero-logo-letter {
107161
color: var(--green);
162+
transition: all 150ms linear;
163+
&.hovered {
164+
color: var(--mauve);
165+
}
108166
}
109167
}
110168
</style>

0 commit comments

Comments
 (0)