- 替换 hero 渐变背景为深海军蓝岩层质感(多层径向渐变 + SVG噪点纹理) - 新增 js/particles.js:右下角粒子网络,随鼠标移动产生吸引连线效果 - 首页主标语改为「突破极限 / 生命从此被计算」,引入 Orbitron + Noto Sans SC 粗体字体 - 新增极低成本/高效/灵活/高通量徽标行,呼应参考宣传册排版
173 lines
5.7 KiB
JavaScript
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();
|
|
}
|
|
})();
|