Skip to content

Commit 56068c0

Browse files
committed
Merge branch 'main' of github.com:grega/hdi
2 parents 6000124 + 1473199 commit 56068c0

4 files changed

Lines changed: 526 additions & 149 deletions

File tree

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
<pre
2+
class="hero-logo"
3+
id="hero-logo-pre"
4+
role="img"
5+
aria-label="hdi">
6+
_____ _____ _____
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> ||
8+
||___|||___|||___||
9+
|/___\|/___\|/___\|</pre>
10+
11+
<script>
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+
27+
// prettier-ignore
28+
const KEY_COLS: Record<string, { normal: string[]; pressed: string[] }> = {
29+
// row0 row1 row2 row3
30+
h: {
31+
normal: [' _____', '||h |', '||___|', '|/___\\'],
32+
pressed: [' ', ' _____', '||h |', '||___|' ],
33+
},
34+
d: {
35+
// Leading '|' on pressed row1 closes h's right wall when h is up
36+
normal: [' _____', '||d |', '||___|', '|/___\\'],
37+
pressed: [' ', '|_____', '||d |', '||___|' ],
38+
},
39+
i: {
40+
// Same as d — leading '|' closes d's right wall when d is up
41+
normal: [' _____', '||i |', '||___|', '|/___\\'],
42+
pressed: [' ', '|_____', '||i |', '||___|' ],
43+
},
44+
};
45+
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")!;
56+
57+
function render() {
58+
const keys = ["h", "d", "i"] as const;
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 (
63+
keys
64+
.map((k) => {
65+
let seg = (
66+
pressedKeys.has(k) ? KEY_COLS[k].pressed : KEY_COLS[k].normal
67+
)[row];
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
78+
return /[a-z]/.test(seg[2])
79+
? `${seg.slice(0, 2)}<span class="hero-logo-letter" data-key="${k}">${seg[2]}</span>${seg.slice(3)}`
80+
: seg;
81+
})
82+
.join("") + suffix
83+
);
84+
});
85+
logoPre.innerHTML = rows.join("\n");
86+
applyClasses();
87+
}
88+
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+
});
95+
}
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;
103+
}
104+
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+
}
123+
});
124+
document.addEventListener("mouseup", () => {
125+
if (pressedKeys.size === 0) return;
126+
pressedKeys.clear();
127+
render();
128+
});
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;
134+
document.addEventListener("keydown", (e) => {
135+
if (KEYS.has(e.key) && !isTextField(e.target)) {
136+
pressedKeys.add(e.key);
137+
render();
138+
}
139+
});
140+
document.addEventListener("keyup", (e) => {
141+
if (KEYS.has(e.key)) {
142+
pressedKeys.delete(e.key);
143+
render();
144+
}
145+
});
146+
147+
render();
148+
</script>
149+
150+
<style>
151+
.hero-logo {
152+
font-size: 1.5rem;
153+
line-height: initial;
154+
cursor: default;
155+
user-select: none;
156+
@media (scripting: enabled) {
157+
cursor: pointer;
158+
}
159+
160+
.hero-logo-letter {
161+
color: var(--green);
162+
transition: all 150ms linear;
163+
&.hovered {
164+
color: var(--mauve);
165+
}
166+
}
167+
}
168+
</style>

0 commit comments

Comments
 (0)