Skip to content

Commit dbdf6f8

Browse files
authored
feat(ui): add home section animation hook (#335)
1 parent 253b600 commit dbdf6f8

File tree

1 file changed

+365
-0
lines changed

1 file changed

+365
-0
lines changed
Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
1+
<style>
2+
.scroll-progress {
3+
position: fixed;
4+
top: 0;
5+
left: 0;
6+
z-index: 2000;
7+
width: 100%;
8+
height: 3px;
9+
background: transparent;
10+
pointer-events: none;
11+
}
12+
13+
.scroll-progress__bar {
14+
width: 100%;
15+
height: 100%;
16+
transform-origin: 0 50%;
17+
transform: scaleX(0);
18+
background: linear-gradient(90deg, #0f766e 0%, #06b6d4 50%, #60a5fa 100%);
19+
box-shadow: 0 0 12px rgba(6, 182, 212, 0.38);
20+
}
21+
22+
.home-section {
23+
position: relative;
24+
}
25+
26+
.js-reveal-item {
27+
opacity: 0;
28+
transform: translateY(18px);
29+
transition: opacity 520ms ease, transform 520ms ease;
30+
will-change: opacity, transform;
31+
}
32+
33+
.js-reveal-item.is-visible {
34+
opacity: 1;
35+
transform: translateY(0);
36+
}
37+
38+
.js-hero-item {
39+
opacity: 0;
40+
transform: translateY(14px);
41+
transition: opacity 560ms ease, transform 560ms ease;
42+
will-change: opacity, transform;
43+
}
44+
45+
.js-hero-item.is-ready {
46+
opacity: 1;
47+
transform: translateY(0);
48+
}
49+
50+
.js-hero-layer {
51+
opacity: 0;
52+
transform: translateY(26px) scale(0.97);
53+
transition: opacity 720ms ease, transform 720ms cubic-bezier(0.2, 0.8, 0.2, 1);
54+
will-change: opacity, transform;
55+
}
56+
57+
.js-hero-layer.is-ready {
58+
opacity: 1;
59+
transform: translateY(0) scale(1);
60+
}
61+
62+
.js-hero-heading {
63+
opacity: 0;
64+
transform: translateY(20px);
65+
letter-spacing: 0.04em;
66+
transition: opacity 640ms ease, transform 640ms cubic-bezier(0.2, 0.8, 0.2, 1), letter-spacing 640ms ease;
67+
will-change: opacity, transform;
68+
}
69+
70+
.js-hero-heading.is-ready {
71+
opacity: 1;
72+
transform: translateY(0);
73+
letter-spacing: 0;
74+
}
75+
76+
.js-hero-body {
77+
opacity: 0;
78+
transform: translateY(16px);
79+
filter: blur(3px);
80+
transition: opacity 620ms ease, transform 620ms cubic-bezier(0.2, 0.8, 0.2, 1), filter 620ms ease;
81+
will-change: opacity, transform, filter;
82+
}
83+
84+
.js-hero-body.is-ready {
85+
opacity: 1;
86+
transform: translateY(0);
87+
filter: blur(0);
88+
}
89+
90+
.js-magnetic-btn {
91+
transition: transform 110ms ease, box-shadow 110ms ease;
92+
will-change: transform;
93+
}
94+
95+
@media (prefers-reduced-motion: reduce) {
96+
.scroll-progress__bar,
97+
.js-reveal-item,
98+
.js-hero-item,
99+
.js-hero-layer,
100+
.js-hero-heading,
101+
.js-hero-body,
102+
.js-magnetic-btn {
103+
transition: none !important;
104+
animation: none !important;
105+
}
106+
}
107+
</style>
108+
109+
<script>
110+
document.addEventListener('DOMContentLoaded', function () {
111+
var reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
112+
113+
initScrollProgress();
114+
initHeroEntrance();
115+
initScrollReveal(reducedMotion);
116+
initMagneticButtons(reducedMotion);
117+
118+
function getAboutSection() {
119+
return document.getElementById('about') || document.querySelector('.wg-about-biography');
120+
}
121+
122+
function initScrollProgress() {
123+
if (document.querySelector('.scroll-progress')) return;
124+
125+
var progress = document.createElement('div');
126+
progress.className = 'scroll-progress';
127+
128+
var bar = document.createElement('div');
129+
bar.className = 'scroll-progress__bar';
130+
progress.appendChild(bar);
131+
document.body.appendChild(progress);
132+
133+
var ticking = false;
134+
135+
function update() {
136+
var doc = document.documentElement;
137+
var max = doc.scrollHeight - window.innerHeight;
138+
var ratio = max > 0 ? window.scrollY / max : 0;
139+
var clamped = Math.min(Math.max(ratio, 0), 1);
140+
bar.style.transform = 'scaleX(' + clamped + ')';
141+
ticking = false;
142+
}
143+
144+
function requestTick() {
145+
if (ticking) return;
146+
ticking = true;
147+
window.requestAnimationFrame(update);
148+
}
149+
150+
update();
151+
window.addEventListener('scroll', requestTick, { passive: true });
152+
window.addEventListener('resize', requestTick);
153+
}
154+
155+
function initHeroEntrance() {
156+
var about = getAboutSection();
157+
if (!about) return;
158+
if (reducedMotion) return;
159+
if (typeof Element === 'undefined' || !Element.prototype.animate) return;
160+
161+
var profile = about.querySelector('#profile');
162+
var avatar = about.querySelector('#profile .avatar');
163+
var title = about.querySelector('#profile .portrait-title');
164+
var heading = about.querySelector('h1');
165+
var article = about.querySelector('.article-style');
166+
var interestsHeading = about.querySelector('.col-md-5 .section-subheading');
167+
var educationHeading = about.querySelector('.col-md-7 .section-subheading');
168+
var interests = about.querySelector('.ul-interests');
169+
var education = about.querySelector('.ul-edu');
170+
var iconItems = about.querySelectorAll('#profile .network-icon li');
171+
var interestItems = about.querySelectorAll('.ul-interests li');
172+
var educationItems = about.querySelectorAll('.ul-edu li');
173+
174+
function run(el, keyframes, options) {
175+
if (!el) return;
176+
el.animate(keyframes, options);
177+
}
178+
179+
run(profile, [
180+
{ opacity: 0, transform: 'translateY(26px) scale(0.97)' },
181+
{ opacity: 1, transform: 'translateY(0) scale(1)' }
182+
], {
183+
duration: 760,
184+
delay: 120,
185+
easing: 'cubic-bezier(0.2, 0.8, 0.2, 1)',
186+
fill: 'both'
187+
});
188+
189+
run(avatar, [
190+
{ opacity: 0, transform: 'translateY(22px) scale(0.96)' },
191+
{ opacity: 1, transform: 'translateY(0) scale(1)' }
192+
], {
193+
duration: 720,
194+
delay: 210,
195+
easing: 'cubic-bezier(0.2, 0.8, 0.2, 1)',
196+
fill: 'both'
197+
});
198+
199+
run(title, [
200+
{ opacity: 0, transform: 'translateY(20px)' },
201+
{ opacity: 1, transform: 'translateY(0)' }
202+
], {
203+
duration: 680,
204+
delay: 300,
205+
easing: 'cubic-bezier(0.2, 0.8, 0.2, 1)',
206+
fill: 'both'
207+
});
208+
209+
run(heading, [
210+
{ opacity: 0, transform: 'translateY(20px)', letterSpacing: '0.04em' },
211+
{ opacity: 1, transform: 'translateY(0)', letterSpacing: '0' }
212+
], {
213+
duration: 700,
214+
delay: 390,
215+
easing: 'cubic-bezier(0.2, 0.8, 0.2, 1)',
216+
fill: 'both'
217+
});
218+
219+
[article, interests, education].forEach(function (el, idx) {
220+
run(el, [
221+
{ opacity: 0, transform: 'translateY(16px)' },
222+
{ opacity: 1, transform: 'translateY(0)' }
223+
], {
224+
duration: 700,
225+
delay: 500 + idx * 110,
226+
easing: 'cubic-bezier(0.2, 0.8, 0.2, 1)',
227+
fill: 'both'
228+
});
229+
});
230+
231+
[interestsHeading, educationHeading].forEach(function (el, idx) {
232+
run(el, [
233+
{ opacity: 0, transform: 'translateY(14px)' },
234+
{ opacity: 1, transform: 'translateY(0)' }
235+
], {
236+
duration: 580,
237+
delay: 570 + idx * 90,
238+
easing: 'cubic-bezier(0.2, 0.8, 0.2, 1)',
239+
fill: 'both'
240+
});
241+
});
242+
243+
Array.prototype.forEach.call(iconItems, function (el, idx) {
244+
run(el, [
245+
{ opacity: 0, transform: 'translateY(14px)' },
246+
{ opacity: 1, transform: 'translateY(0)' }
247+
], {
248+
duration: 500,
249+
delay: 350 + idx * 55,
250+
easing: 'cubic-bezier(0.2, 0.8, 0.2, 1)',
251+
fill: 'both'
252+
});
253+
});
254+
255+
Array.prototype.forEach.call(interestItems, function (el, idx) {
256+
run(el, [
257+
{ opacity: 0, transform: 'translateY(14px)' },
258+
{ opacity: 1, transform: 'translateY(0)' }
259+
], {
260+
duration: 520,
261+
delay: 650 + idx * 70,
262+
easing: 'cubic-bezier(0.2, 0.8, 0.2, 1)',
263+
fill: 'both'
264+
});
265+
});
266+
267+
Array.prototype.forEach.call(educationItems, function (el, idx) {
268+
run(el, [
269+
{ opacity: 0, transform: 'translateY(14px)' },
270+
{ opacity: 1, transform: 'translateY(0)' }
271+
], {
272+
duration: 520,
273+
delay: 730 + idx * 80,
274+
easing: 'cubic-bezier(0.2, 0.8, 0.2, 1)',
275+
fill: 'both'
276+
});
277+
});
278+
}
279+
280+
function initScrollReveal(isReduced) {
281+
if (isReduced || !('IntersectionObserver' in window)) return;
282+
283+
var sections = document.querySelectorAll('.page-body .home-section, main .home-section');
284+
if (!sections.length) return;
285+
286+
var itemObserver = new IntersectionObserver(function (entries, observer) {
287+
entries.forEach(function (entry) {
288+
if (!entry.isIntersecting) return;
289+
entry.target.classList.add('is-visible');
290+
entry.target.style.transitionDelay = '0ms';
291+
observer.unobserve(entry.target);
292+
});
293+
}, { threshold: 0.1, rootMargin: '0px 0px -6% 0px' });
294+
295+
var itemSelector = [
296+
'.card',
297+
'.card-simple.view-card',
298+
'.media.stream-item',
299+
'.project-card',
300+
'.pub-list-item.view-citation',
301+
'#recent-news li',
302+
'#awards-and-grants li',
303+
'#profile',
304+
'.ul-edu li',
305+
'.ul-interests li',
306+
'.experience .card',
307+
'.section-heading',
308+
'.tag-cloud a',
309+
'.taxonomy-terms-cloud a'
310+
].join(',');
311+
312+
sections.forEach(function (section) {
313+
if (section.id === 'about') return;
314+
315+
var items = section.querySelectorAll(itemSelector);
316+
Array.prototype.forEach.call(items, function (item, idx) {
317+
if (item.classList.contains('js-hero-item')) return;
318+
if (item.classList.contains('js-hero-layer')) return;
319+
if (item.classList.contains('js-hero-body')) return;
320+
if (item.classList.contains('js-hero-heading')) return;
321+
322+
item.classList.add('js-reveal-item');
323+
item.style.transitionDelay = Math.min(idx * 65, 420) + 'ms';
324+
itemObserver.observe(item);
325+
});
326+
});
327+
}
328+
329+
function initMagneticButtons(isReduced) {
330+
if (isReduced) return;
331+
332+
var buttons = document.querySelectorAll(
333+
'.btn.btn-primary, .btn.btn-outline-primary, a.nav-link[href*="cv/releases/latest/download/cv.pdf"]'
334+
);
335+
336+
Array.prototype.forEach.call(buttons, function (button) {
337+
if (button.classList.contains('js-magnetic-btn')) return;
338+
button.classList.add('js-magnetic-btn');
339+
340+
button.addEventListener('mouseenter', function () {
341+
button.style.transitionDuration = '0ms';
342+
});
343+
344+
button.addEventListener('mousemove', function (event) {
345+
var rect = button.getBoundingClientRect();
346+
var relX = (event.clientX - rect.left) / rect.width - 0.5;
347+
var relY = (event.clientY - rect.top) / rect.height - 0.5;
348+
var moveX = relX * 6;
349+
var moveY = relY * 6;
350+
button.style.transform = 'translate(' + moveX.toFixed(2) + 'px, ' + moveY.toFixed(2) + 'px)';
351+
});
352+
353+
button.addEventListener('mouseleave', function () {
354+
button.style.transitionDuration = '';
355+
button.style.transform = '';
356+
});
357+
358+
button.addEventListener('blur', function () {
359+
button.style.transitionDuration = '';
360+
button.style.transform = '';
361+
});
362+
});
363+
}
364+
});
365+
</script>

0 commit comments

Comments
 (0)