/** * AKSRT Effects — count-up / countdown / marquee / 3D tilt / FPS detection * * 全部渐进增强,尊重 prefers-reduced-motion。 * * @package AKSRT */ (function () { 'use strict'; var reducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; /* ============================================================ * 1. 数字递增 (count-up) — IntersectionObserver 触发 * 用法:0 * ============================================================ */ function initCountUp() { if (reducedMotion) return; if (!('IntersectionObserver' in window)) return; var els = document.querySelectorAll('[data-aksrt-countup]'); if (!els.length) return; var observer = new IntersectionObserver(function (entries) { entries.forEach(function (entry) { if (!entry.isIntersecting) return; var el = entry.target; observer.unobserve(el); var target = parseFloat(el.dataset.target || '0'); var duration = parseInt(el.dataset.duration || '2000', 10); var decimals = (el.dataset.decimals || '0|').split('|'); var prefix = decimals[1] || ''; var dec = parseInt(decimals[0], 10) || 0; var start = 0; var startTime = null; function step(timestamp) { if (!startTime) startTime = timestamp; var progress = Math.min((timestamp - startTime) / duration, 1); // ease-out cubic var eased = 1 - Math.pow(1 - progress, 3); var current = start + (target - start) * eased; el.textContent = prefix + current.toFixed(dec).replace(/\B(?=(\d{3})+(?!\d))/g, ','); if (progress < 1) { requestAnimationFrame(step); } else { el.textContent = prefix + target.toFixed(dec).replace(/\B(?=(\d{3})+(?!\d))/g, ','); } } requestAnimationFrame(step); }); }, { threshold: 0.3 }); els.forEach(function (el) { observer.observe(el); }); } /* ============================================================ * 2. 倒计时组件 * 用法:--:--:-- * ============================================================ */ function initCountdown() { var els = document.querySelectorAll('[data-aksrt-countdown]'); if (!els.length) return; function pad(n) { return n < 10 ? '0' + n : '' + n; } function update() { var now = Date.now(); els.forEach(function (el) { var end = new Date(el.dataset.end).getTime(); var diff = end - now; if (diff <= 0) { el.textContent = el.dataset.expired || '已结束'; return; } var d = Math.floor(diff / 86400000); var h = Math.floor((diff % 86400000) / 3600000); var m = Math.floor((diff % 3600000) / 60000); var s = Math.floor((diff % 60000) / 1000); var format = el.dataset.format || 'dhms'; if (format === 'dhms') { el.textContent = (d > 0 ? d + '天 ' : '') + pad(h) + ':' + pad(m) + ':' + pad(s); } else if (format === 'hms') { el.textContent = pad(h + d * 24) + ':' + pad(m) + ':' + pad(s); } else { el.textContent = (d > 0 ? d + '天 ' : '') + pad(h) + '时' + pad(m) + '分' + pad(s) + '秒'; } }); } update(); setInterval(update, 1000); } /* ============================================================ * 3. 合作伙伴无缝滚动 (marquee) * 用法:
...内容...
* ============================================================ */ function initMarquee() { document.querySelectorAll('.aksrt-marquee').forEach(function (el) { if (el.dataset.marqueeBound) return; el.dataset.marqueeBound = '1'; var speed = parseFloat(el.dataset.speed || '1'); var inner = el.querySelector('.aksrt-marquee__inner') || el.children[0]; if (!inner) return; // 克隆内容实现无缝 var clone = inner.cloneNode(true); el.appendChild(clone); var pos = 0; var width = inner.scrollWidth; function animate() { pos -= speed; if (Math.abs(pos) >= width) pos = 0; inner.style.transform = 'translateX(' + pos + 'px)'; clone.style.transform = 'translateX(' + (pos + width) + 'px)'; if (!reducedMotion) requestAnimationFrame(animate); } if (!reducedMotion) { requestAnimationFrame(animate); } // 暂停/恢复 el.addEventListener('mouseenter', function () { el.dataset.paused = '1'; }); el.addEventListener('mouseleave', function () { delete el.dataset.paused; }); }); } /* ============================================================ * 4. 3D 悬停倾斜 (vanilla tilt) * 用法:
...
* ============================================================ */ function initTilt() { if (reducedMotion) return; document.querySelectorAll('[data-aksrt-tilt]').forEach(function (el) { if (el.dataset.tiltBound) return; el.dataset.tiltBound = '1'; var maxDeg = parseFloat(el.dataset.tiltMax || '10'); var perspective = parseFloat(el.dataset.tiltPerspective || '800'); var scale = parseFloat(el.dataset.tiltScale || '1.03'); var glare = el.dataset.tiltGlare === '1'; var glareEl = null; if (glare) { glareEl = document.createElement('div'); glareEl.style.cssText = 'position:absolute;inset:0;border-radius:inherit;pointer-events:none;background:linear-gradient(135deg,rgba(255,255,255,0) 0%,rgba(255,255,255,0.25) 100%);opacity:0;transition:opacity 0.2s;'; 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 rotateX = (0.5 - y) * maxDeg; var rotateY = (x - 0.5) * maxDeg; el.style.transform = 'perspective(' + perspective + 'px) rotateX(' + rotateX + 'deg) rotateY(' + rotateY + 'deg) scale3d(' + scale + ',' + scale + ',' + scale + ')'; if (glareEl) glareEl.style.opacity = '1'; }); el.addEventListener('mouseleave', function () { el.style.transform = 'perspective(' + perspective + 'px) rotateX(0deg) rotateY(0deg) scale3d(1,1,1)'; if (glareEl) glareEl.style.opacity = '0'; }); }); } /* ============================================================ * 5. FPS 检测 + 低端设备降级 * ============================================================ */ function initFpsDetection() { if (!('requestAnimationFrame' in window)) return; var frames = 0; var startTime = performance.now(); function countFrames() { frames++; var elapsed = performance.now() - startTime; if (elapsed >= 1000) { var fps = Math.round(frames * 1000 / elapsed); if (fps < 24) { document.body.classList.add('fps-accelerat'); } return; // 只检测一次 } requestAnimationFrame(countFrames); } requestAnimationFrame(countFrames); } /* ============================================================ * 初始化入口 * ============================================================ */ document.addEventListener('DOMContentLoaded', function () { initCountUp(); initCountdown(); initMarquee(); initTilt(); initFpsDetection(); }); })();