-
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathHdiLogo.astro
More file actions
195 lines (179 loc) · 6.26 KB
/
HdiLogo.astro
File metadata and controls
195 lines (179 loc) · 6.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
<pre
class="hero-logo"
id="hero-logo-pre"
role="img"
aria-label="hdi">
_____ _____ _____
||<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> ||
||___|||___|||___||
|/___\|/___\|/___\|</pre>
<script>
// Animates the ASCII art logo so h, d, i keys visually depress when pressed
// (via keyboard or mouse click). Letters also highlight on hover.
//
// Each key is a 6-char-wide column rendered across 4 rows:
// row 0 — top cap: ' _____'
// row 1 — letter: '||h |'
// row 2 — bottom cap: '||___|'
// row 3 — shadow: '|/___\'
//
// The "pressed" state shifts the column down one row, replacing the top cap
// with spaces and dropping the shadow — giving the illusion of a key sinking.
//
// The static <pre> above is the no-JS fallback; render() rewrites it with
// interactive <span> elements once JS loads.
// prettier-ignore
const KEY_COLS: Record<string, { normal: string[]; pressed: string[] }> = {
// row0 row1 row2 row3
h: {
normal: [' _____', '||h |', '||___|', '|/___\\'],
pressed: [' ', ' _____', '||h |', '||___|' ],
},
d: {
// Leading '|' on pressed row1 closes h's right wall when h is up
normal: [' _____', '||d |', '||___|', '|/___\\'],
pressed: [' ', '|_____', '||d |', '||___|' ],
},
i: {
// Same as d — leading '|' closes d's right wall when d is up
normal: [' _____', '||i |', '||___|', '|/___\\'],
pressed: [' ', '|_____', '||i |', '||___|' ],
},
};
// Trailing character appended after the 3 key columns, one entry per row.
// Row 1's trailing '|' closes i's right wall — replaced with ' ' when i is pressed.
const ROW_SUFFIX = [" ", "|", "|", "|"];
// Maps each key to its left neighbour, used to detect adjacent pressed keys.
const KEY_LEFT: Record<string, string> = { d: "h", i: "d" };
const pressedKeys = new Set<string>();
let hoveredKey: string | null = null;
const logoPre = document.getElementById("hero-logo-pre")!;
function render() {
const keys = ["h", "d", "i"] as const;
const rows = [0, 1, 2, 3].map((row) => {
// When i is pressed its trailing wall disappears (key shifted down)
const suffix = row === 1 && pressedKeys.has("i") ? " " : ROW_SUFFIX[row];
return (
keys
.map((k) => {
let seg = (
pressedKeys.has(k) ? KEY_COLS[k].pressed : KEY_COLS[k].normal
)[row];
// On row 1, a pressed key's leading '|' closes the left neighbour's key wall.
// If the left neighbour is also pressed that wall is gone — drop the '|'.
if (
row === 1 &&
pressedKeys.has(k) &&
pressedKeys.has(KEY_LEFT[k])
) {
seg = " " + seg.slice(1);
}
// Position 2 of each segment is always the letter — wrap it in a span
return /[a-z]/.test(seg[2])
? `${seg.slice(0, 2)}<span class="hero-logo-letter" data-key="${k}">${seg[2]}</span>${seg.slice(3)}`
: seg;
})
.join("") + suffix
);
});
logoPre.innerHTML = rows.join("\n");
applyClasses();
}
// Apply hover class without touching the DOM structure,
// so CSS transitions on color have existing elements to animate between.
function applyClasses() {
logoPre.querySelectorAll<HTMLElement>(".hero-logo-letter").forEach((el) => {
const key = el.dataset.key!;
el.classList.toggle("hovered", key === hoveredKey);
el.classList.toggle("pressed", pressedKeys.has(key));
});
}
// Maps a clientX coordinate to a key by dividing the pre into 19 character columns.
// h: cols 0–5, d: cols 6–11, i: cols 12–17, col 18 is the trailing '|' (ignored).
function keyFromPoint(clientX: number): string | null {
const rect = logoPre.getBoundingClientRect();
const col = Math.floor((clientX - rect.left) / (rect.width / 19));
return col < 6 ? "h" : col < 12 ? "d" : col < 18 ? "i" : null;
}
function releaseAll() {
if (pressedKeys.size === 0) return;
pressedKeys.clear();
render();
}
function pressTouches(e: TouchEvent) {
e.preventDefault();
pressedKeys.clear();
for (const touch of e.touches) {
const key = keyFromPoint(touch.clientX);
if (key) pressedKeys.add(key);
}
render();
}
// Mouse
logoPre.addEventListener("mousemove", (e) => {
const key = keyFromPoint(e.clientX);
if (key !== hoveredKey) {
hoveredKey = key;
applyClasses();
}
if (e.buttons === 1) {
pressedKeys.clear();
if (key) pressedKeys.add(key);
render();
}
});
logoPre.addEventListener("mouseleave", () => {
if (hoveredKey === null) return;
hoveredKey = null;
applyClasses();
});
logoPre.addEventListener("mousedown", (e) => {
const key = keyFromPoint(e.clientX);
if (key) {
pressedKeys.add(key);
render();
}
});
document.addEventListener("mouseup", releaseAll);
// Touch
logoPre.addEventListener("touchstart", pressTouches, { passive: false });
logoPre.addEventListener("touchmove", pressTouches, { passive: false });
document.addEventListener("touchend", releaseAll);
document.addEventListener("touchcancel", releaseAll);
// Keyboard — skip when focus is in a text input to avoid interfering with typing
const KEYS = new Set(["h", "d", "i"]);
const isTextField = (t: EventTarget | null) =>
t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement;
document.addEventListener("keydown", (e) => {
if (KEYS.has(e.key) && !isTextField(e.target)) {
pressedKeys.add(e.key);
render();
}
});
document.addEventListener("keyup", (e) => {
if (KEYS.has(e.key)) {
pressedKeys.delete(e.key);
render();
}
});
render();
</script>
<style>
.hero-logo {
font-size: 1.5rem;
line-height: initial;
cursor: default;
user-select: none;
@media (scripting: enabled) {
cursor: pointer;
}
.hero-logo-letter {
color: var(--green);
transition: all 150ms linear;
&.hovered,
&.pressed {
color: var(--mauve);
}
}
}
</style>