/** * 全站深色岩层背景上的粒子网络效果 * 固定在视口内(position: fixed),任意页面、任意滚动位置都可见,跟随鼠标移动。 */ (function () { var canvas = document.querySelector('.site-particles'); if (!canvas) return; var ctx = canvas.getContext('2d'); var DPR = window.devicePixelRatio || 1; var prefersReducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; var width = 0; var height = 0; var particles = []; var mouse = { x: null, y: null, active: false }; var config = { density: 9000, maxParticles: 130, linkDistance: 150, mouseLinkDistance: 220, mouseForce: 1.1, dotRadiusMin: 1.6, dotRadiusMax: 3.4, dotColor: '124, 211, 252', lineColor: '56, 189, 248' }; function resize() { width = window.innerWidth; height = window.innerHeight; canvas.width = Math.max(1, Math.round(width * DPR)); canvas.height = Math.max(1, Math.round(height * DPR)); ctx.setTransform(DPR, 0, 0, DPR, 0, 0); createParticles(); } function createParticles() { var count = Math.min(config.maxParticles, Math.floor((width * height) / config.density)); particles = new Array(Math.max(count, 30)).fill(null).map(function () { return { x: Math.random() * width, y: Math.random() * height, vx: (Math.random() - 0.5) * 0.4, vy: (Math.random() - 0.5) * 0.4, r: Math.random() * (config.dotRadiusMax - config.dotRadiusMin) + config.dotRadiusMin }; }); } document.addEventListener('mousemove', function (e) { mouse.x = e.clientX; mouse.y = e.clientY; mouse.active = true; }); document.addEventListener('mouseleave', function () { mouse.active = false; }); function step() { particles.forEach(function (p) { p.x += p.vx; p.y += p.vy; if (p.x <= 0 || p.x >= width) p.vx *= -1; if (p.y <= 0 || p.y >= height) p.vy *= -1; p.x = Math.max(0, Math.min(width, p.x)); p.y = Math.max(0, Math.min(height, p.y)); if (mouse.active) { var dx = mouse.x - p.x; var dy = mouse.y - p.y; var dist = Math.hypot(dx, dy); if (dist < config.mouseLinkDistance && dist > 0.001) { var pull = (1 - dist / config.mouseLinkDistance) * config.mouseForce; p.x -= (dx / dist) * pull; p.y -= (dy / dist) * pull; } } }); } function render() { ctx.clearRect(0, 0, width, height); for (var i = 0; i < particles.length; i++) { for (var j = i + 1; j < particles.length; j++) { var dx = particles[i].x - particles[j].x; var dy = particles[i].y - particles[j].y; var dist = Math.hypot(dx, dy); if (dist < config.linkDistance) { ctx.strokeStyle = 'rgba(' + config.lineColor + ', ' + ((1 - dist / config.linkDistance) * 0.45) + ')'; ctx.lineWidth = 0.7; ctx.beginPath(); ctx.moveTo(particles[i].x, particles[i].y); ctx.lineTo(particles[j].x, particles[j].y); ctx.stroke(); } } if (mouse.active) { var mdx = particles[i].x - mouse.x; var mdy = particles[i].y - mouse.y; var mdist = Math.hypot(mdx, mdy); if (mdist < config.mouseLinkDistance) { ctx.strokeStyle = 'rgba(' + config.lineColor + ', ' + ((1 - mdist / config.mouseLinkDistance) * 0.65) + ')'; ctx.lineWidth = 0.9; ctx.beginPath(); ctx.moveTo(particles[i].x, particles[i].y); ctx.lineTo(mouse.x, mouse.y); ctx.stroke(); } } } particles.forEach(function (p) { ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); ctx.fillStyle = 'rgba(' + config.dotColor + ', 0.85)'; ctx.shadowColor = 'rgba(56, 189, 248, 0.8)'; ctx.shadowBlur = 7; ctx.fill(); ctx.shadowBlur = 0; }); if (mouse.active) { ctx.beginPath(); ctx.arc(mouse.x, mouse.y, 2.6, 0, Math.PI * 2); ctx.fillStyle = 'rgba(255, 255, 255, 0.85)'; ctx.shadowColor = 'rgba(255, 255, 255, 0.85)'; ctx.shadowBlur = 12; ctx.fill(); ctx.shadowBlur = 0; } } function loop() { step(); render(); requestAnimationFrame(loop); } window.addEventListener('resize', resize); resize(); if (!prefersReducedMotion) { loop(); } else { render(); } })();