Skip to content

Commit 2b16d1f

Browse files
authored
Merge pull request #17 from 42devops/feat/pretext-hero-typography
feat: integrate Pretext dynamic typographic obstacle engine into hero banner
2 parents 40c79d8 + 8170586 commit 2b16d1f

3 files changed

Lines changed: 205 additions & 3 deletions

File tree

‎css/screen.css‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,29 @@ body {
189189

190190
/* Hero Intro Banner */
191191
.hero-banner {
192-
padding: 2rem 0 3rem 0;
192+
position: relative;
193+
text-align: center;
194+
padding: 5.5rem 0 5rem 0;
193195
margin-bottom: 2.5rem;
196+
overflow: hidden;
197+
border-radius: var(--radius-lg);
198+
}
199+
200+
.hero-pretext-canvas {
201+
position: absolute;
202+
top: 0;
203+
left: 0;
204+
width: 100%;
205+
height: 100%;
206+
pointer-events: none;
207+
z-index: 1;
208+
}
209+
210+
.hero-annotation-wrapper {
211+
position: relative;
212+
z-index: 2;
213+
display: inline-block;
214+
margin: 1.5rem 0;
194215
}
195216

196217
.hero-title {

‎index.html‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
title: "42DevOps | Infrastructure & Engineering Notes"
44
---
55

6-
<section class="hero-banner" style="text-align: center; padding: 6rem 0 5.5rem 0;">
7-
<div style="display: inline-block; margin: 2rem 0;">
6+
<section class="hero-banner">
7+
<canvas id="hero-pretext-canvas" class="hero-pretext-canvas"></canvas>
8+
<div class="hero-annotation-wrapper">
89
<span class="ann ann-s ann-amber ann-no-mark" data-note="42" style="--ann-font-size: 1.4rem; --ann-font-weight: 600; --ann-target-gap: 8px;">
910
<span class="ann ann-n ann-blue ann-no-mark" data-note="Don't Panic" style="--ann-font-size: 1.35rem; --ann-font-weight: 600; --ann-target-gap: 8px;">
1011
<span class="ann ann-e ann-purple ann-no-mark" data-note="Keep Walking" style="--ann-font-size: 1.25rem; --ann-font-weight: 600; --ann-target-gap: 8px;">
@@ -24,6 +25,7 @@
2425
</span>
2526
</div>
2627
</section>
28+
<script src="/js/hero-pretext.js?v={{ site.time | date: '%s' }}" defer></script>
2729

2830
<section id="articles" style="margin-bottom: 3rem;">
2931
<div class="section-header">

‎js/hero-pretext.js‎

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
/*****************************************************************************/
2+
/* Pretext Typographic Obstacle Engine for Hero Banner */
3+
/* Inspired by Cheng Lou's Pretext (https://chenglou.me/pretext/) */
4+
/*****************************************************************************/
5+
6+
(function() {
7+
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
8+
9+
const canvas = document.getElementById('hero-pretext-canvas');
10+
const container = document.querySelector('.hero-banner');
11+
const targetObj = document.querySelector('.hero-annotation-wrapper');
12+
if (!canvas || !container || !targetObj) return;
13+
14+
const ctx = canvas.getContext('2d');
15+
const phraseStream = [
16+
"42DevOps", "·", "Infrastructure", "Engineering", "·", "Kubernetes", "Clusters", "·",
17+
"Distributed", "Systems", "·", "High", "Performance", "Algorithms", "·", "Site", "Reliability", "·",
18+
"Automation", "·", "Cloud", "Native", "·", "Observability", "·", "Linux", "Kernel", "·",
19+
"System", "Architecture", "·", "SRE", "Notes", "·"
20+
];
21+
22+
let dpr = window.devicePixelRatio || 1;
23+
let width = 0;
24+
let height = 0;
25+
let wordWidths = [];
26+
27+
let mouse = { x: -1000, y: -1000, targetX: -1000, targetY: -1000 };
28+
const orbRadius = 65;
29+
30+
function getFont() {
31+
return `500 13px 'Charter', 'Source Serif 4', 'Noto Serif SC', Georgia, serif`;
32+
}
33+
34+
function measureWords() {
35+
ctx.font = getFont();
36+
wordWidths = phraseStream.map(w => ctx.measureText(w + " ").width);
37+
}
38+
39+
function resize() {
40+
const rect = container.getBoundingClientRect();
41+
width = rect.width;
42+
height = rect.height;
43+
dpr = window.devicePixelRatio || 1;
44+
45+
canvas.width = width * dpr;
46+
canvas.height = height * dpr;
47+
canvas.style.width = width + 'px';
48+
canvas.style.height = height + 'px';
49+
50+
ctx.scale(dpr, dpr);
51+
measureWords();
52+
}
53+
54+
function getThemeColors() {
55+
const style = getComputedStyle(document.documentElement);
56+
return {
57+
text: style.getPropertyValue('--text-light').trim() || '#6b6a64',
58+
brand: style.getPropertyValue('--brand-primary').trim() || '#1B365D'
59+
};
60+
}
61+
62+
container.addEventListener('pointermove', (e) => {
63+
const rect = container.getBoundingClientRect();
64+
mouse.targetX = e.clientX - rect.left;
65+
mouse.targetY = e.clientY - rect.top;
66+
});
67+
68+
container.addEventListener('pointerleave', () => {
69+
mouse.targetX = -1000;
70+
mouse.targetY = -1000;
71+
});
72+
73+
function draw() {
74+
ctx.clearRect(0, 0, width, height);
75+
76+
// Smooth lerp for mouse motion
77+
mouse.x += (mouse.targetX - mouse.x) * 0.12;
78+
mouse.y += (mouse.targetY - mouse.y) * 0.12;
79+
80+
const targetRect = targetObj.getBoundingClientRect();
81+
const containerRect = container.getBoundingClientRect();
82+
83+
// Bounds for the central compass annotation cluster
84+
const centerObstacle = {
85+
x: targetRect.left - containerRect.left + targetRect.width / 2,
86+
y: targetRect.top - containerRect.top + targetRect.height / 2,
87+
rx: targetRect.width / 2 + 30,
88+
ry: targetRect.height / 2 + 20
89+
};
90+
91+
const colors = getThemeColors();
92+
ctx.font = getFont();
93+
ctx.fillStyle = colors.text;
94+
ctx.globalAlpha = 0.28;
95+
96+
const lineHeight = 24;
97+
let wordIdx = 0;
98+
99+
for (let y = 22; y < height; y += lineHeight) {
100+
let x = 12;
101+
102+
while (x < width - 12) {
103+
let availableSpan = width - 12 - x;
104+
105+
// 1. Subtract central compass cluster obstacle
106+
if (Math.abs(y - centerObstacle.y) < centerObstacle.ry) {
107+
const obsLeft = centerObstacle.x - centerObstacle.rx;
108+
const obsRight = centerObstacle.x + centerObstacle.rx;
109+
110+
if (x < obsRight && x + availableSpan > obsLeft) {
111+
if (x < obsLeft) {
112+
availableSpan = obsLeft - x;
113+
} else {
114+
x = obsRight;
115+
availableSpan = width - 12 - x;
116+
}
117+
}
118+
}
119+
120+
// 2. Subtract mouse orb obstacle
121+
if (mouse.x > 0 && Math.abs(y - mouse.y) < orbRadius) {
122+
const dy = Math.abs(y - mouse.y);
123+
const dx = Math.sqrt(orbRadius * orbRadius - dy * dy);
124+
const orbLeft = mouse.x - dx;
125+
const orbRight = mouse.x + dx;
126+
127+
if (x < orbRight && x + availableSpan > orbLeft) {
128+
if (x < orbLeft) {
129+
availableSpan = orbLeft - x;
130+
} else {
131+
x = orbRight;
132+
availableSpan = width - 12 - x;
133+
}
134+
}
135+
}
136+
137+
let currentSpan = 0;
138+
let startWordIdx = wordIdx;
139+
140+
while (wordIdx < phraseStream.length && currentSpan + wordWidths[wordIdx] <= availableSpan) {
141+
currentSpan += wordWidths[wordIdx];
142+
wordIdx = (wordIdx + 1) % phraseStream.length;
143+
if (wordIdx === startWordIdx) break;
144+
}
145+
146+
if (currentSpan <= 0) {
147+
x += 20;
148+
continue;
149+
}
150+
151+
let renderX = x;
152+
let count = (wordIdx >= startWordIdx) ? (wordIdx - startWordIdx) : (phraseStream.length - startWordIdx + wordIdx);
153+
154+
for (let i = 0; i < count; i++) {
155+
const idx = (startWordIdx + i) % phraseStream.length;
156+
ctx.fillText(phraseStream[idx], renderX, y);
157+
renderX += wordWidths[idx];
158+
}
159+
160+
x += currentSpan + 8;
161+
}
162+
}
163+
164+
// Subtle magnetic aura under cursor
165+
if (mouse.x > 0) {
166+
ctx.globalAlpha = 0.05;
167+
ctx.fillStyle = colors.brand;
168+
ctx.beginPath();
169+
ctx.arc(mouse.x, mouse.y, orbRadius, 0, Math.PI * 2);
170+
ctx.fill();
171+
}
172+
173+
requestAnimationFrame(draw);
174+
}
175+
176+
window.addEventListener('resize', resize);
177+
resize();
178+
requestAnimationFrame(draw);
179+
})();

0 commit comments

Comments
 (0)