/** * AKSRT Enhancements Part 2 — 高级交互组件 * * 包含:3D 悬停倾斜、滚动触发动画、封面轮播、打赏按钮 * * @package AKSRT */ (function () { 'use strict'; /* ============================================================ * 1. 3D 悬停倾斜效果 (vanilla-tilt 轻量版) * ============================================================ */ function initTilt() { document.querySelectorAll('[data-aksrt-tilt]').forEach(function (el) { if (el.dataset.tiltInit) return; el.dataset.tiltInit = '1'; var maxTilt = parseInt(el.dataset.aksrtTiltMax || '15', 10); var perspective = parseInt(el.dataset.aksrtTiltPerspective || '1000', 10); var scale = parseFloat(el.dataset.aksrtTiltScale || '1.03'); var speed = parseInt(el.dataset.aksrtTiltSpeed || '400', 10); var glare = el.dataset.aksrtTiltGlare === '1'; el.style.transformStyle = 'preserve-3d'; el.style.transition = 'transform ' + speed + 'ms ease'; var glareEl = null; if (glare) { glareEl = document.createElement('div'); glareEl.style.cssText = 'position:absolute;inset:0;border-radius:inherit;pointer-events:none;opacity:0;transition:opacity ' + speed + 'ms;'; el.style.position = el.style.position || 'relative'; el.style.overflow = 'hidden'; el.appendChild(glareEl); } el.addEventListener('mousemove', function (e) { var rect = el.getBoundingClientRect(); var x = (e.clientX - rect.left) / rect.width; var y = (e.clientY - rect.top) / rect.height; var tiltX = (0.5 - y) * maxTilt; var tiltY = (x - 0.5) * maxTilt; el.style.transform = 'perspective(' + perspective + 'px) rotateX(' + tiltX + 'deg) rotateY(' + tiltY + 'deg) scale3d(' + scale + ',' + scale + ',' + scale + ')'; if (glareEl) { glareEl.style.opacity = '1'; glareEl.style.background = 'linear-gradient(135deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.2) 50%, rgba(255,255,255,0) 100%)'; glareEl.style.transform = 'translate(' + ((x - 0.5) * 100) + '%, ' + ((y - 0.5) * 100) + '%)'; } }); el.addEventListener('mouseleave', function () { el.style.transform = 'perspective(' + perspective + 'px) rotateX(0deg) rotateY(0deg) scale3d(1,1,1)'; if (glareEl) glareEl.style.opacity = '0'; }); }); } /* ============================================================ * 2. 滚动触发动画 (IntersectionObserver) * ============================================================ */ function initScrollAnimations() { if (!('IntersectionObserver' in window)) { // 不支持时直接显示所有元素 document.querySelectorAll('[data-aksrt-animate]').forEach(function (el) { el.style.opacity = '1'; el.style.transform = 'none'; }); return; } var observer = new IntersectionObserver(function (entries) { entries.forEach(function (entry) { if (!entry.isIntersecting) return; var el = entry.target; var animation = el.dataset.aksrtAnimate || 'fadeInUp'; var delay = parseInt(el.dataset.aksrtAnimateDelay || '0', 10); var duration = parseInt(el.dataset.aksrtAnimateDuration || '600', 10); setTimeout(function () { el.style.transition = 'opacity ' + duration + 'ms ease, transform ' + duration + 'ms ease'; el.style.opacity = '1'; el.style.transform = 'translateY(0) translateX(0) scale(1)'; el.classList.add('aksrt-animated'); }, delay); observer.unobserve(el); }); }, { threshold: 0.1, rootMargin: '0px 0px -50px 0px', }); document.querySelectorAll('[data-aksrt-animate]').forEach(function (el) { if (el.dataset.aksrtAnimateInit) return; el.dataset.aksrtAnimateInit = '1'; var animation = el.dataset.aksrtAnimate || 'fadeInUp'; // 设置初始状态 el.style.opacity = '0'; switch (animation) { case 'fadeIn': el.style.transform = 'none'; break; case 'fadeInUp': el.style.transform = 'translateY(30px)'; break; case 'fadeInDown': el.style.transform = 'translateY(-30px)'; break; case 'fadeInLeft': el.style.transform = 'translateX(-30px)'; break; case 'fadeInRight': el.style.transform = 'translateX(30px)'; break; case 'zoomIn': el.style.transform = 'scale(0.9)'; break; default: el.style.transform = 'translateY(30px)'; } observer.observe(el); }); } /* ============================================================ * 5. 粘性元素 (Sticky) * ============================================================ */ function initSticky() { document.querySelectorAll('[data-aksrt-sticky]').forEach(function (el) { if (el.dataset.aksrtStickyInit) return; el.dataset.aksrtStickyInit = '1'; var top = parseInt(el.dataset.aksrtStickyTop || '80', 10); var offset = parseInt(el.dataset.aksrtStickyOffset || '0', 10); function update() { var rect = el.getBoundingClientRect(); var parentRect = el.parentElement.getBoundingClientRect(); if (parentRect.top < top && parentRect.bottom > el.offsetHeight + top) { el.style.position = 'sticky'; el.style.top = top + 'px'; } } window.addEventListener('scroll', update, { passive: true }); }); } /* ============================================================ * 6. 视差滚动 * ============================================================ */ var parallaxScrollBound = false; function initParallax() { if (!document.querySelector('[data-aksrt-parallax]')) return; if (parallaxScrollBound) return; parallaxScrollBound = true; var ticking = false; function update() { document.querySelectorAll('[data-aksrt-parallax]').forEach(function (el) { var speed = parseFloat(el.dataset.aksrtParallax || '0.3'); var rect = el.getBoundingClientRect(); var center = rect.top + rect.height / 2; var windowCenter = window.innerHeight / 2; var offset = (center - windowCenter) * speed; el.style.transform = 'translateY(' + offset + 'px)'; }); ticking = false; } window.addEventListener('scroll', function () { if (!ticking) { requestAnimationFrame(update); ticking = true; } }, { passive: true }); } /* ============================================================ * 7. 涟漪点击效果 — document 委托只绑定一次 * ============================================================ */ var rippleBound = false; function initRipple() { if (rippleBound) return; rippleBound = true; document.addEventListener('click', function (e) { var el = e.target.closest('.aksrt-ripple'); if (!el) return; var rect = el.getBoundingClientRect(); var x = e.clientX - rect.left; var y = e.clientY - rect.top; var size = Math.max(rect.width, rect.height) * 2; var wave = document.createElement('span'); wave.className = 'aksrt-ripple__wave'; wave.style.width = wave.style.height = size + 'px'; wave.style.left = (x - size / 2) + 'px'; wave.style.top = (y - size / 2) + 'px'; el.appendChild(wave); wave.addEventListener('animationend', function () { wave.remove(); }); }); } /* ============================================================ * 8. 表单浮动标签自动初始化 * ============================================================ */ function initFloatLabels() { document.querySelectorAll('.aksrt-float-label__input').forEach(function (input) { if (input.dataset.aksrtFloatInit) return; input.dataset.aksrtFloatInit = '1'; // 检查初始值 if (input.value) { input.setAttribute('data-has-value', '1'); } input.addEventListener('input', function () { if (this.value) { this.setAttribute('data-has-value', '1'); } else { this.removeAttribute('data-has-value'); } }); }); } /* ============================================================ * 8. 签到日历可视化 * ============================================================ */ function initSigninCalendar() { var calendar = document.querySelector('[data-aksrt-signin-calendar]'); if (!calendar) return; var data = []; try { data = JSON.parse(calendar.dataset.aksrtSigninCalendar || '[]'); } catch (e) {} // 生成最近 30 天的格子 var today = new Date(); var html = '