/* 东燊科技 Donsen — 未来科技感动效层(fx.js) 依赖:fx.css。与 main.js 完全解耦,任一方失效不影响基础功能。 模块:① 深空星野 ② 鼠标聚光 ③ 自定义光标 ④ 点击爆炸 ⑤ 卡片 3D 倾斜 ⑥ 数字滚动 */ (function () { 'use strict'; var html = document.documentElement; var params = new URLSearchParams(location.search); var staticMode = params.get('static') === '1'; var reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; // 触屏设备(含无头移动端模拟)不启用自定义光标/聚光/倾斜;爆炸效果保留(pointerdown 兼容触摸) var isTouch = (navigator.maxTouchPoints || 0) > 0 || 'ontouchstart' in window; var finePointer = window.matchMedia && window.matchMedia('(pointer:fine)').matches && !isTouch; var dpr = Math.min(window.devicePixelRatio || 1, 2); /* ============ ① 深空星野 ============ */ function starfield() { var cv = document.getElementById('starfield'); if (!cv) return null; var ctx = cv.getContext('2d'); var W = 0, H = 0, stars = [], shooters = [], raf = 0, mx = 0, my = 0, tmx = 0, tmy = 0; function resize() { W = window.innerWidth; H = window.innerHeight; cv.width = W * dpr; cv.height = H * dpr; cv.style.width = W + 'px'; cv.style.height = H + 'px'; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); } function seed(done) { var n = Math.round((W * H) / 14000); stars = []; var i = 0; // 分片生成(每帧 40 颗):避免首屏启动时一次循环占满主线程构成长任务 function chunk() { var end = Math.min(i + 40, n); for (; i < end; i++) { var depth = 0.25 + Math.random() * 0.75; stars.push({ x: Math.random() * W, y: Math.random() * H, r: depth * 1.5 + 0.3, a: 0.25 + Math.random() * 0.65, tw: 0.6 + Math.random() * 1.8, ph: Math.random() * Math.PI * 2, vy: depth * 0.05, hue: Math.random() < 0.12 ? 190 : (Math.random() < 0.1 ? 32 : 215), depth: depth }); } if (i < n) { requestAnimationFrame(chunk); return; } if (done) done(); } chunk(); } function seedAndStart() { stop(); seed(function () { if (staticMode || reduceMotion) { frame(0); stop(); } else start(); }); } function spawnShooter() { if (shooters.length > 1 || Math.random() > 0.35) return; shooters.push({ x: Math.random() * W * 0.7, y: Math.random() * H * 0.4, vx: 7 + Math.random() * 5, vy: 2.4 + Math.random() * 1.6, life: 1 }); } function frame(ms) { var t = ms / 1000; tmx += (mx - tmx) * 0.04; tmy += (my - tmy) * 0.04; ctx.clearRect(0, 0, W, H); for (var i = 0; i < stars.length; i++) { var s = stars[i]; var tw = 0.72 + 0.28 * Math.sin(t * s.tw + s.ph); var px = s.x + tmx * 26 * s.depth; var py = s.y + tmy * 18 * s.depth; if (!reduceMotion) { s.y += s.vy; if (s.y > H + 4) { s.y = -4; s.x = Math.random() * W; } } ctx.beginPath(); ctx.arc(px, py, s.r * tw, 0, Math.PI * 2); ctx.fillStyle = 'hsla(' + s.hue + ',90%,' + (72 + s.depth * 18) + '%,' + (s.a * tw).toFixed(3) + ')'; ctx.fill(); if (s.r > 1.35) { // 大星加光晕 ctx.beginPath(); ctx.arc(px, py, s.r * 3.2, 0, Math.PI * 2); ctx.fillStyle = 'hsla(' + s.hue + ',95%,75%,' + (s.a * 0.10).toFixed(3) + ')'; ctx.fill(); } } // 流星 if (!reduceMotion && Math.random() < 0.0022) spawnShooter(); for (var j = shooters.length - 1; j >= 0; j--) { var sh = shooters[j]; sh.x += sh.vx; sh.y += sh.vy; sh.life -= 0.012; var g = ctx.createLinearGradient(sh.x, sh.y, sh.x - sh.vx * 9, sh.y - sh.vy * 9); g.addColorStop(0, 'rgba(255,255,255,' + (0.85 * sh.life).toFixed(3) + ')'); g.addColorStop(1, 'rgba(255,255,255,0)'); ctx.strokeStyle = g; ctx.lineWidth = 1.6; ctx.beginPath(); ctx.moveTo(sh.x, sh.y); ctx.lineTo(sh.x - sh.vx * 9, sh.y - sh.vy * 9); ctx.stroke(); if (sh.life <= 0 || sh.x > W + 60) shooters.splice(j, 1); } if (!document.hidden) raf = requestAnimationFrame(frame); } function start() { if (!raf && !document.hidden) raf = requestAnimationFrame(frame); } function stop() { if (raf) { cancelAnimationFrame(raf); raf = 0; } } document.addEventListener('visibilitychange', function () { document.hidden ? stop() : start(); }); window.addEventListener('resize', function () { resize(); seedAndStart(); }); window.addEventListener('mousemove', function (e) { mx = (e.clientX / W - 0.5) * 2; my = (e.clientY / H - 0.5) * 2; }); resize(); seedAndStart(); return { stop: stop, start: start }; } /* ============ ② 鼠标聚光 + ③ 自定义光标 ============ */ function pointerFx() { if (staticMode || reduceMotion || !finePointer) return null; html.classList.add('fx-on'); var spot = document.getElementById('spotlight'); var dot = document.querySelector('.cursor-dot'); var ring = document.querySelector('.cursor-ring'); var tx = innerWidth / 2, ty = innerHeight / 2, sx = tx, sy = ty, rx = tx, ry = ty, raf = 0; function loop() { sx += (tx - sx) * 0.12; sy += (ty - sy) * 0.12; rx += (tx - rx) * 0.24; ry += (ty - ry) * 0.24; if (spot) spot.style.transform = 'translate3d(' + sx + 'px,' + sy + 'px,0)'; if (dot) dot.style.transform = 'translate3d(' + tx + 'px,' + ty + 'px,0)'; if (ring) ring.style.transform = 'translate3d(' + rx + 'px,' + ry + 'px,0)'; // 三轴全部收敛到指针(<0.5px)即休眠,下次 mousemove 重启——避免指针静止后 rAF 永不停转 if (Math.abs(tx - sx) < 0.5 && Math.abs(ty - sy) < 0.5 && Math.abs(tx - rx) < 0.5 && Math.abs(ty - ry) < 0.5) { raf = 0; return; } raf = requestAnimationFrame(loop); } window.addEventListener('mousemove', function (e) { tx = e.clientX; ty = e.clientY; if (!raf) raf = requestAnimationFrame(loop); }, { passive: true }); if (!raf) raf = requestAnimationFrame(loop); // 悬停可交互元素:光环放大 var hot = 'a,button,[data-tilt],.scene-chips span,.benefit-item'; document.addEventListener('mouseover', function (e) { if (e.target.closest && e.target.closest(hot) && ring) ring.classList.add('is-hot'); }); document.addEventListener('mouseout', function (e) { if (e.target.closest && e.target.closest(hot) && ring) ring.classList.remove('is-hot'); }); return true; } /* ============ ④ 点击爆炸 ============ */ function explosion() { var layer = document.getElementById('fx-layer'); if (!layer) return null; var ctx = layer.getContext('2d'); var parts = [], rings = [], raf = 0; var COLORS = ['#5B9BFF', '#2E7CF6', '#5BE9FF', '#FFFFFF', '#9D8CFF']; function resize() { layer.width = innerWidth * dpr; layer.height = innerHeight * dpr; layer.style.width = innerWidth + 'px'; layer.style.height = innerHeight + 'px'; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); } window.addEventListener('resize', resize); resize(); function burst(x, y, warm) { if (staticMode || reduceMotion) return; var n = 34 + Math.round(Math.random() * 10); for (var i = 0; i < n; i++) { var ang = Math.random() * Math.PI * 2; var sp = 1.6 + Math.random() * 7.2; parts.push({ x: x, y: y, vx: Math.cos(ang) * sp, vy: Math.sin(ang) * sp - 1.4, r: 1.1 + Math.random() * 2.6, life: 1, decay: 0.016 + Math.random() * 0.02, col: warm && Math.random() < 0.4 ? '#FFB37A' : COLORS[(Math.random() * COLORS.length) | 0] }); } rings.push({ x: x, y: y, r: 6, life: 1, warm: !!warm }); var el = document.createElement('div'); el.className = 'shockwave' + (warm ? ' warm' : ''); el.style.left = x + 'px'; el.style.top = y + 'px'; document.body.appendChild(el); setTimeout(function () { el.remove(); }, 700); if (!raf) raf = requestAnimationFrame(frame); } function frame() { ctx.clearRect(0, 0, innerWidth, innerHeight); var i, p; for (i = parts.length - 1; i >= 0; i--) { p = parts[i]; p.x += p.vx; p.y += p.vy; p.vy += 0.14; p.vx *= 0.985; p.vy *= 0.985; p.life -= p.decay; if (p.life <= 0) { parts.splice(i, 1); continue; } ctx.globalAlpha = Math.max(0, p.life); ctx.fillStyle = p.col; ctx.beginPath(); ctx.arc(p.x, p.y, p.r * p.life + 0.4, 0, Math.PI * 2); ctx.fill(); } for (i = rings.length - 1; i >= 0; i--) { var r = rings[i]; r.r += 7.5; r.life -= 0.045; if (r.life <= 0) { rings.splice(i, 1); continue; } ctx.globalAlpha = Math.max(0, r.life * 0.8); ctx.strokeStyle = r.warm ? '#FFB37A' : '#5BE9FF'; ctx.lineWidth = 2 * r.life; ctx.beginPath(); ctx.arc(r.x, r.y, r.r, 0, Math.PI * 2); ctx.stroke(); } ctx.globalAlpha = 1; raf = (parts.length || rings.length) ? requestAnimationFrame(frame) : 0; } window.addEventListener('pointerdown', function (e) { burst(e.clientX, e.clientY, Math.random() < 0.16); }); return { burst: burst }; } /* ============ ⑤ 卡片 3D 倾斜 + 指针辉光 ============ */ function tilt() { if (staticMode || reduceMotion || !finePointer) return null; var els = document.querySelectorAll('[data-tilt]'); // 矩形缓存:mouseenter 时读一次,mousemove 直接用;滚动/缩放/离开时失效 // (15 张卡 × 每秒数十次 mousemove,每次都 getBoundingClientRect 会强制布局) function invalidate() { for (var i = 0; i < els.length; i++) els[i]._rect = null; } els.forEach(function (el) { el.addEventListener('mouseenter', function () { el._rect = el.getBoundingClientRect(); }); el.addEventListener('mousemove', function (e) { var r = el._rect; if (!r) r = el._rect = el.getBoundingClientRect(); var px = (e.clientX - r.left) / r.width, py = (e.clientY - r.top) / r.height; el.style.setProperty('--mx', (px * 100).toFixed(1) + '%'); el.style.setProperty('--my', (py * 100).toFixed(1) + '%'); var rx = (0.5 - py) * 5, ry = (px - 0.5) * 5; el.style.transform = 'perspective(900px) rotateX(' + rx.toFixed(2) + 'deg) rotateY(' + ry.toFixed(2) + 'deg) translateY(-4px)'; }); el.addEventListener('mouseleave', function () { el._rect = null; el.style.transform = 'perspective(900px) rotateX(0deg) rotateY(0deg)'; }); }); window.addEventListener('scroll', invalidate, { passive: true }); window.addEventListener('resize', invalidate); return els.length; } /* ============ ⑥ 数字滚动 ============ */ function countUp() { var els = Array.prototype.slice.call(document.querySelectorAll('[data-count]')); if (!els.length) return 0; function setFinal(el) { el.textContent = el.getAttribute('data-count') + (el.getAttribute('data-suffix') || ''); } if (staticMode || reduceMotion || !('IntersectionObserver' in window)) { els.forEach(setFinal); return els.length; } var io = new IntersectionObserver(function (entries) { entries.forEach(function (en) { if (!en.isIntersecting) return; var el = en.target, end = parseFloat(el.getAttribute('data-count')) || 0; var suffix = el.getAttribute('data-suffix') || ''; var t0 = performance.now(), dur = 1100; (function step(now) { var k = Math.min(1, (now - t0) / dur); var eased = 1 - Math.pow(1 - k, 3); el.textContent = Math.round(end * eased) + suffix; if (k < 1) requestAnimationFrame(step); else el.textContent = end + suffix; })(t0); io.unobserve(el); }); }, { threshold: 0.4 }); els.forEach(function (el) { io.observe(el); }); return els.length; } /* ============ 启动 ============ */ function boot() { starfield(); pointerFx(); explosion(); tilt(); countUp(); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot); else boot(); })();