工作室介绍,开工(
This commit is contained in:
@@ -15,15 +15,15 @@
|
||||
let rafId = null;
|
||||
|
||||
const waveDefs = [
|
||||
{ yRatio: 0.3, amplitude: 22, wavelength: 940, speed: 0.5, alpha: 0.98 },
|
||||
{ yRatio: 0.45, amplitude: 16, wavelength: 1020, speed: 0.58, alpha: 0.92 },
|
||||
{ yRatio: 0.65, amplitude: 14, wavelength: 1160, speed: 0.6, alpha: 0.9 },
|
||||
{ yRatio: 0.3, amplitude: 24, wavelength: 940, speed: 0.5, alpha: 0.98 },
|
||||
{ yRatio: 0.45, amplitude: 14, wavelength: 1020, speed: 0.58, alpha: 0.92 },
|
||||
{ yRatio: 0.65, amplitude: 10, wavelength: 1160, speed: 0.6, alpha: 0.9 },
|
||||
];
|
||||
|
||||
const palettes = {
|
||||
light: {
|
||||
gradient: ['#e8f3ff', '#cfe6fb', '#558ec1', '#2476af', '#2b6fb3'],
|
||||
waves: ['rgba(170, 215, 245, 0.91)', 'rgba(120, 185, 230, 0.93)', 'rgba(80, 155, 210, 0.95)'],
|
||||
gradient: ['#d6e8fa', '#a7c8e8', '#6f9fc8', '#4f84b6', '#376e9f'],
|
||||
waves: ['rgba(124, 167, 205, 0.96)', 'rgba(90, 143, 190, 0.97)', 'rgba(62, 118, 169, 0.98)'],
|
||||
},
|
||||
dark: {
|
||||
gradient: ['#0d1117', '#111d2b', '#14334d', '#17507a', '#1b6aa6'],
|
||||
|
||||
+167
-16
@@ -3,38 +3,189 @@
|
||||
if (!hero) return;
|
||||
|
||||
const body = document.body;
|
||||
const desktopQuery = window.matchMedia('(min-width: 900px)');
|
||||
let hasScrolled = false;
|
||||
const header = document.querySelector('.site-header');
|
||||
const titleSvg = hero.querySelector('.hero-title-svg');
|
||||
const scrollIndicator = hero.querySelector('.hero-scroll-indicator');
|
||||
const scrollArrow = scrollIndicator ? scrollIndicator.querySelector('.scroll-arrow') : null;
|
||||
const scrollText = scrollIndicator ? scrollIndicator.querySelector('.scroll-text') : null;
|
||||
|
||||
const applyState = () => {
|
||||
const desktopQuery = window.matchMedia('(min-width: 900px)');
|
||||
const reducedMotionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||
const anime = window.anime;
|
||||
const hasAnime = typeof anime === 'function';
|
||||
|
||||
let arrowPulse = null;
|
||||
let didIntro = false;
|
||||
let ticking = false;
|
||||
|
||||
const stopArrowPulse = () => {
|
||||
if (!arrowPulse) return;
|
||||
arrowPulse.pause();
|
||||
arrowPulse = null;
|
||||
};
|
||||
|
||||
const startArrowPulse = () => {
|
||||
if (!hasAnime || reducedMotionQuery.matches || !scrollArrow) return;
|
||||
stopArrowPulse();
|
||||
scrollArrow.style.animation = 'none';
|
||||
anime.remove(scrollArrow);
|
||||
anime.set(scrollArrow, {
|
||||
translateY: 0,
|
||||
opacity: 0.5,
|
||||
rotate: 45,
|
||||
});
|
||||
arrowPulse = anime({
|
||||
targets: scrollArrow,
|
||||
translateY: [0, 6, 0],
|
||||
opacity: [0.5, 1, 0.5],
|
||||
rotate: 45,
|
||||
duration: 1600,
|
||||
easing: 'easeInOutSine',
|
||||
loop: true,
|
||||
});
|
||||
};
|
||||
|
||||
const clearInlineState = () => {
|
||||
if (header) {
|
||||
header.style.transform = '';
|
||||
}
|
||||
if (scrollIndicator) {
|
||||
scrollIndicator.style.opacity = '';
|
||||
scrollIndicator.style.transform = '';
|
||||
}
|
||||
};
|
||||
|
||||
const animateScrollState = (isAtTop) => {
|
||||
if (!hasAnime || reducedMotionQuery.matches) return;
|
||||
|
||||
if (header) {
|
||||
anime.remove(header);
|
||||
anime({
|
||||
targets: header,
|
||||
translateY: isAtTop ? '-100%' : '0%',
|
||||
duration: isAtTop ? 500 : 380,
|
||||
easing: isAtTop ? 'easeInOutCubic' : 'easeOutCubic',
|
||||
});
|
||||
}
|
||||
|
||||
if (scrollIndicator) {
|
||||
anime.remove(scrollIndicator);
|
||||
anime({
|
||||
targets: scrollIndicator,
|
||||
opacity: isAtTop ? 1 : 0,
|
||||
duration: isAtTop ? 380 : 260,
|
||||
easing: isAtTop ? 'easeOutCubic' : 'easeOutQuad',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const applyState = (animated) => {
|
||||
if (!desktopQuery.matches) {
|
||||
body.classList.remove('home-hero-initial', 'home-hero-scrolled');
|
||||
stopArrowPulse();
|
||||
clearInlineState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasScrolled || window.scrollY > 6) {
|
||||
body.classList.remove('home-hero-initial');
|
||||
body.classList.add('home-hero-scrolled');
|
||||
} else {
|
||||
body.classList.add('home-hero-initial');
|
||||
body.classList.remove('home-hero-scrolled');
|
||||
const isAtTop = window.scrollY <= 6;
|
||||
body.classList.toggle('home-hero-initial', isAtTop);
|
||||
body.classList.toggle('home-hero-scrolled', !isAtTop);
|
||||
|
||||
if (animated) {
|
||||
animateScrollState(isAtTop);
|
||||
} else if (hasAnime) {
|
||||
if (header) {
|
||||
anime.remove(header);
|
||||
}
|
||||
if (scrollIndicator) {
|
||||
anime.remove(scrollIndicator);
|
||||
}
|
||||
}
|
||||
|
||||
if (isAtTop) {
|
||||
startArrowPulse();
|
||||
} else {
|
||||
stopArrowPulse();
|
||||
}
|
||||
};
|
||||
|
||||
const playIntro = () => {
|
||||
if (didIntro || !hasAnime || reducedMotionQuery.matches || !desktopQuery.matches || window.scrollY > 6) {
|
||||
return;
|
||||
}
|
||||
|
||||
didIntro = true;
|
||||
if (titleSvg) {
|
||||
anime.set(titleSvg, { opacity: 0, translateY: 14, scale: 1.01 });
|
||||
}
|
||||
if (scrollArrow) {
|
||||
anime.set(scrollArrow, { opacity: 0, translateY: 8, rotate: 45 });
|
||||
}
|
||||
if (scrollText) {
|
||||
anime.set(scrollText, { opacity: 0, translateY: 8 });
|
||||
}
|
||||
|
||||
const timeline = anime.timeline({
|
||||
easing: 'easeOutCubic',
|
||||
});
|
||||
|
||||
if (titleSvg) {
|
||||
timeline.add({
|
||||
targets: titleSvg,
|
||||
opacity: 1,
|
||||
translateY: -8,
|
||||
scale: 1.06,
|
||||
duration: 860,
|
||||
easing: 'easeOutExpo',
|
||||
});
|
||||
}
|
||||
|
||||
if (scrollArrow || scrollText) {
|
||||
timeline.add({
|
||||
targets: [scrollArrow, scrollText].filter(Boolean),
|
||||
opacity: (el) => (el === scrollArrow ? [0, 1] : [0, 1]),
|
||||
translateY: 0,
|
||||
rotate: (el) => (el === scrollArrow ? 45 : 0),
|
||||
duration: 420,
|
||||
delay: anime.stagger(80),
|
||||
}, '-=300');
|
||||
}
|
||||
|
||||
timeline.finished.then(() => {
|
||||
startArrowPulse();
|
||||
});
|
||||
};
|
||||
|
||||
const onScroll = () => {
|
||||
hasScrolled = window.scrollY > 6;
|
||||
applyState();
|
||||
if (ticking) return;
|
||||
ticking = true;
|
||||
window.requestAnimationFrame(() => {
|
||||
applyState(true);
|
||||
ticking = false;
|
||||
});
|
||||
};
|
||||
|
||||
const onResize = () => {
|
||||
if (!desktopQuery.matches) {
|
||||
hasScrolled = false;
|
||||
}
|
||||
applyState();
|
||||
applyState(false);
|
||||
};
|
||||
|
||||
applyState();
|
||||
const onReducedMotionChange = () => {
|
||||
if (reducedMotionQuery.matches) {
|
||||
stopArrowPulse();
|
||||
if (scrollArrow) {
|
||||
scrollArrow.style.animation = 'none';
|
||||
}
|
||||
} else if (window.scrollY <= 6 && desktopQuery.matches) {
|
||||
startArrowPulse();
|
||||
}
|
||||
applyState(false);
|
||||
};
|
||||
|
||||
applyState(false);
|
||||
playIntro();
|
||||
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
window.addEventListener('resize', onResize);
|
||||
desktopQuery.addEventListener('change', onResize);
|
||||
reducedMotionQuery.addEventListener('change', onReducedMotionChange);
|
||||
})();
|
||||
|
||||
+106
-5
@@ -1,5 +1,30 @@
|
||||
const themeToggle = document.querySelector('.theme-toggle');
|
||||
const themeToggles = document.querySelectorAll('.theme-toggle');
|
||||
const html = document.documentElement;
|
||||
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||
const supportsViewTransition = typeof document.startViewTransition === 'function';
|
||||
const viewTransitionStyleId = 'itstudio-view-transition-style';
|
||||
let isThemeTransitioning = false;
|
||||
|
||||
function ensureViewTransitionStyles() {
|
||||
if (!supportsViewTransition || document.getElementById(viewTransitionStyleId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.id = viewTransitionStyleId;
|
||||
style.textContent = `
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
animation: none;
|
||||
mix-blend-mode: normal;
|
||||
}
|
||||
|
||||
::view-transition-group(root) {
|
||||
animation: none;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
function updateLogoColor(theme) {
|
||||
const logoText = document.querySelector('#logo-text-cn');
|
||||
@@ -21,19 +46,95 @@ function getPreferredTheme() {
|
||||
function setTheme(theme) {
|
||||
html.setAttribute('data-theme', theme);
|
||||
localStorage.setItem('theme', theme);
|
||||
themeToggles.forEach((toggle) => {
|
||||
toggle.setAttribute('aria-pressed', theme === 'dark' ? 'true' : 'false');
|
||||
});
|
||||
updateLogoColor(theme);
|
||||
}
|
||||
|
||||
const initialTheme = getPreferredTheme();
|
||||
setTheme(initialTheme);
|
||||
ensureViewTransitionStyles();
|
||||
|
||||
if (themeToggle) {
|
||||
themeToggle.addEventListener('click', () => {
|
||||
function getToggleCenter(toggle) {
|
||||
if (!toggle) {
|
||||
return {
|
||||
x: window.innerWidth / 2,
|
||||
y: window.innerHeight / 2,
|
||||
};
|
||||
}
|
||||
|
||||
const rect = toggle.getBoundingClientRect();
|
||||
return {
|
||||
x: rect.left + rect.width / 2,
|
||||
y: rect.top + rect.height / 2,
|
||||
};
|
||||
}
|
||||
|
||||
function getTransitionOrigin(event, toggle) {
|
||||
const hasPointerPoint = Number.isFinite(event.clientX) && Number.isFinite(event.clientY) && (event.clientX !== 0 || event.clientY !== 0);
|
||||
if (hasPointerPoint) {
|
||||
return {
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
};
|
||||
}
|
||||
|
||||
return getToggleCenter(toggle);
|
||||
}
|
||||
|
||||
function getRevealRadius(x, y) {
|
||||
const maxX = Math.max(x, window.innerWidth - x);
|
||||
const maxY = Math.max(y, window.innerHeight - y);
|
||||
return Math.hypot(maxX, maxY);
|
||||
}
|
||||
|
||||
function switchThemeWithScatter(theme, origin) {
|
||||
if (isThemeTransitioning || prefersReducedMotion.matches || !supportsViewTransition) {
|
||||
setTheme(theme);
|
||||
return;
|
||||
}
|
||||
|
||||
isThemeTransitioning = true;
|
||||
const x = origin.x;
|
||||
const y = origin.y;
|
||||
const endRadius = getRevealRadius(x, y);
|
||||
const clipFrom = `circle(0px at ${x}px ${y}px)`;
|
||||
const clipTo = `circle(${endRadius}px at ${x}px ${y}px)`;
|
||||
|
||||
const transition = document.startViewTransition(() => {
|
||||
setTheme(theme);
|
||||
});
|
||||
|
||||
transition.ready
|
||||
.then(() => {
|
||||
document.documentElement.animate(
|
||||
{
|
||||
clipPath: [clipFrom, clipTo],
|
||||
},
|
||||
{
|
||||
duration: 650,
|
||||
easing: 'cubic-bezier(0.22, 1, 0.36, 1)',
|
||||
pseudoElement: '::view-transition-new(root)',
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
transition.finished.finally(() => {
|
||||
isThemeTransitioning = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
themeToggles.forEach((themeToggle) => {
|
||||
themeToggle.addEventListener('click', (event) => {
|
||||
const currentTheme = html.getAttribute('data-theme');
|
||||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||
setTheme(newTheme);
|
||||
const origin = getTransitionOrigin(event, themeToggle);
|
||||
switchThemeWithScatter(newTheme, origin);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
||||
if (!localStorage.getItem('theme')) {
|
||||
|
||||
Vendored
+8
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user