Files
haiguji-website-export/js/particles.js
T
Claude f347a99dd9 首页英雄区改为深海岩层暗色背景,新增右下角跟随鼠标粒子效果与Hyper-seq风格排版
- 替换 hero 渐变背景为深海军蓝岩层质感(多层径向渐变 + SVG噪点纹理)
- 新增 js/particles.js:右下角粒子网络,随鼠标移动产生吸引连线效果
- 首页主标语改为「突破极限 / 生命从此被计算」,引入 Orbitron + Noto Sans SC 粗体字体
- 新增极低成本/高效/灵活/高通量徽标行,呼应参考宣传册排版
2026-07-14 06:23:00 +00:00

173 lines
5.7 KiB
JavaScript

/**
* 右下角粒子网络效果
* 在英雄区右下角绘制一片跟随鼠标移动的粒子网络,呼应科技/基因数据感。
*/
(function () {
const canvas = document.getElementById('hero-particle-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const DPR = window.devicePixelRatio || 1;
let width = 0;
let height = 0;
let particles = [];
let animationId = null;
const mouse = { x: null, y: null, active: false };
const config = {
density: 9000, // 每 9000 平方像素一个粒子
maxParticles: 90,
linkDistance: 120,
mouseLinkDistance: 170,
mouseForce: 0.9,
dotColor: '124, 211, 252',
lineColor: '56, 189, 248'
};
function resize() {
const rect = canvas.getBoundingClientRect();
width = rect.width;
height = rect.height;
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() {
const count = Math.min(config.maxParticles, Math.floor((width * height) / config.density));
particles = new Array(Math.max(count, 20)).fill(null).map(() => ({
x: Math.random() * width,
y: Math.random() * height,
vx: (Math.random() - 0.5) * 0.35,
vy: (Math.random() - 0.5) * 0.35,
r: Math.random() * 1.4 + 0.8
}));
}
function step() {
particles.forEach(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) {
const dx = mouse.x - p.x;
const dy = mouse.y - p.y;
const dist = Math.hypot(dx, dy);
if (dist < config.mouseLinkDistance && dist > 0.001) {
const 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 (let i = 0; i < particles.length; i++) {
for (let j = i + 1; j < particles.length; j++) {
const dx = particles[i].x - particles[j].x;
const dy = particles[i].y - particles[j].y;
const dist = Math.hypot(dx, dy);
if (dist < config.linkDistance) {
ctx.strokeStyle = `rgba(${config.lineColor}, ${(1 - dist / config.linkDistance) * 0.5})`;
ctx.lineWidth = 0.6;
ctx.beginPath();
ctx.moveTo(particles[i].x, particles[i].y);
ctx.lineTo(particles[j].x, particles[j].y);
ctx.stroke();
}
}
// 粒子与鼠标的连线
if (mouse.active) {
const dx = particles[i].x - mouse.x;
const dy = particles[i].y - mouse.y;
const dist = Math.hypot(dx, dy);
if (dist < config.mouseLinkDistance) {
ctx.strokeStyle = `rgba(${config.lineColor}, ${(1 - dist / config.mouseLinkDistance) * 0.7})`;
ctx.lineWidth = 0.8;
ctx.beginPath();
ctx.moveTo(particles[i].x, particles[i].y);
ctx.lineTo(mouse.x, mouse.y);
ctx.stroke();
}
}
}
// 粒子本体
particles.forEach(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 = 5;
ctx.fill();
ctx.shadowBlur = 0;
});
// 鼠标光点
if (mouse.active) {
ctx.beginPath();
ctx.arc(mouse.x, mouse.y, 2.2, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
ctx.shadowColor = 'rgba(255, 255, 255, 0.9)';
ctx.shadowBlur = 10;
ctx.fill();
ctx.shadowBlur = 0;
}
}
function loop() {
step();
render();
animationId = requestAnimationFrame(loop);
}
function updateMouseFromEvent(clientX, clientY) {
const rect = canvas.getBoundingClientRect();
const x = clientX - rect.left;
const y = clientY - rect.top;
// 允许鼠标在画布周边一定范围内也能产生吸引效果
const margin = 160;
if (x > -margin && x < width + margin && y > -margin && y < height + margin) {
mouse.x = Math.max(0, Math.min(width, x));
mouse.y = Math.max(0, Math.min(height, y));
mouse.active = true;
} else {
mouse.active = false;
}
}
document.addEventListener('mousemove', e => updateMouseFromEvent(e.clientX, e.clientY));
document.addEventListener('mouseleave', () => { mouse.active = false; });
canvas.addEventListener('touchmove', e => {
if (e.touches && e.touches[0]) {
updateMouseFromEvent(e.touches[0].clientX, e.touches[0].clientY);
}
}, { passive: true });
window.addEventListener('resize', resize);
const prefersReducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
resize();
if (!prefersReducedMotion) {
loop();
} else {
render();
}
})();