diff --git a/assets/css/about-hero.css b/assets/css/about-hero.css index 2554966..793b697 100644 --- a/assets/css/about-hero.css +++ b/assets/css/about-hero.css @@ -6,6 +6,80 @@ position: relative; } +body.intro-about { + -ms-overflow-style: none; + scrollbar-width: none; +} + +body.intro-about::-webkit-scrollbar { + width: 0; + height: 0; + display: none; +} + +html:has(body.intro-about) { + -ms-overflow-style: none; + scrollbar-width: none; +} + +html:has(body.intro-about)::-webkit-scrollbar { + width: 0; + height: 0; + display: none; +} + +body.intro-about .site-header, +body.intro-about .site-footer { + display: none !important; +} + +body.intro-about .hero-section { + min-height: 100vh; +} + +body.intro-about .about-theme-toggle { + position: fixed; + top: 18px; + right: 18px; + z-index: 40; + width: 72px; + height: 72px; + background: transparent !important; + border: none !important; + border-radius: 0; + color: color-mix(in srgb, var(--text-primary) 82%, var(--color-primary) 18%); + box-shadow: none; + backdrop-filter: none; + -webkit-backdrop-filter: none; +} + +body.intro-about .about-theme-toggle::before { + display: none; + content: none; +} + +body.intro-about .about-theme-toggle svg { + width: 60px; + height: 60px; +} + +body.intro-about .about-theme-toggle:hover, +body.intro-about .about-theme-toggle:focus-visible { + background: transparent !important; + border: none !important; + color: color-mix(in srgb, var(--text-primary) 72%, var(--color-primary) 28%); + box-shadow: none; +} + +body.intro-about .about-theme-toggle:hover { + transform: translateY(-1px); +} + +body.intro-about .about-theme-toggle:hover::before, +body.intro-about .about-theme-toggle:focus-visible::before { + display: none; +} + .hero-section { padding: 40px 0; background-color: var(--bg-body); @@ -131,7 +205,10 @@ html:not([data-theme="dark"]) .hero-waves { .hero-title { display: flex; + flex-direction: column; + align-items: center; justify-content: center; + gap: clamp(2px, 0.6vw, 10px); margin: 0 0 1.2rem; overflow: visible; --hero-title-gradient: linear-gradient(135deg, var(--text-primary) 0%, var(--color-primary) 100%); @@ -442,6 +519,11 @@ html:not([data-theme="dark"]) .hero-title-svg::before { } @media (max-width: 768px) { + body.intro-about .about-theme-toggle { + top: 12px; + right: 12px; + } + .hero-title { margin-bottom: 1rem; } diff --git a/assets/js/hero-waves.js b/assets/js/hero-waves.js index 83e37af..a2f53d4 100644 --- a/assets/js/hero-waves.js +++ b/assets/js/hero-waves.js @@ -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'], diff --git a/assets/js/home-hero.js b/assets/js/home-hero.js index 71ed5d2..aa26089 100644 --- a/assets/js/home-hero.js +++ b/assets/js/home-hero.js @@ -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); })(); diff --git a/assets/js/theme-toggle.js b/assets/js/theme-toggle.js index 136e11a..2400328 100644 --- a/assets/js/theme-toggle.js +++ b/assets/js/theme-toggle.js @@ -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')) { diff --git a/assets/js/vendor/anime.min.js b/assets/js/vendor/anime.min.js new file mode 100644 index 0000000..ed9da8a --- /dev/null +++ b/assets/js/vendor/anime.min.js @@ -0,0 +1,8 @@ +/* + * anime.js v3.2.2 + * (c) 2023 Julian Garnier + * Released under the MIT license + * animejs.com + */ + +!function(n,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):n.anime=e()}(this,function(){"use strict";var i={update:null,begin:null,loopBegin:null,changeBegin:null,change:null,changeComplete:null,loopComplete:null,complete:null,loop:1,direction:"normal",autoplay:!0,timelineOffset:0},M={duration:1e3,delay:0,endDelay:0,easing:"easeOutElastic(1, .5)",round:0},j=["translateX","translateY","translateZ","rotate","rotateX","rotateY","rotateZ","scale","scaleX","scaleY","scaleZ","skew","skewX","skewY","perspective","matrix","matrix3d"],l={CSS:{},springs:{}};function C(n,e,t){return Math.min(Math.max(n,e),t)}function u(n,e){return-1
+

@@ -19,217 +40,6 @@ get_header();

- -
- -
- -
-
-
-
-

-
- 'announcement', - 'post_status' => 'publish', - 'posts_per_page' => 5, - 'orderby' => 'date', - 'order' => 'DESC', - 'ignore_sticky_posts' => true, - 'no_found_rows' => true - )); - - if ($announcements->have_posts()) : - while ($announcements->have_posts()) : $announcements->the_post(); - ?> -
-
-

-
- -
- -
-
- -

- -
-
- -
-

-
- 'post', - 'post_status' => 'publish', - 'posts_per_page' => 5, - 'orderby' => 'date', - 'order' => 'DESC', - 'ignore_sticky_posts' => true, - 'no_found_rows' => true - )); - - if ($blogs->have_posts()) : - while ($blogs->have_posts()) : $blogs->the_post(); - ?> -
-
-

-
- -
- -
-
- -

- -
-
-
-
-