You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用单个JavaScript文件为多个Canvas应用相同交互效果?

解决多Canvas粒子交互的问题(单JS文件实现)

嘿,作为刚摸Canvas的新手,你碰到的问题核心是没做好多个Canvas的实例隔离——原代码里把所有Canvas的上下文、粒子都共用了全局变量,肯定会出问题!咱完全可以用单个JS文件搞定,还能支持任意数量的Canvas,下面一步步给你修正:

先说说原代码的几个关键问题

  • 全局变量冲突:你用了全局的canvas、context和net数组,但每个Canvas需要自己独立的上下文和粒子集合,混在一起只会让所有Canvas显示一样的内容还乱套
  • Canvas选择错误:初始化时写的document.getElementById("canvas#dot_"+j)是错的,ID是dot_j,直接用dot_${j}就行,不需要加canvas#
  • 按钮类名拼写错:HTML里按钮的类是color_chane,但JS里写的color_change,导致点击事件根本绑定不上
  • Dot类依赖全局:Dot类里直接用全局的canvas/context,没有和具体的Canvas关联,粒子没法正确对应到各自的画布

修正后的完整实现

HTML部分(仅修正按钮类名拼写)

<body>
 <div class="container" style="display: flex;">
  <div class="buttons">
   <a href="#" class="color_change">Button 1</a>
   <canvas id="dot_1"></canvas>
  </div>
  <div class="buttons">
   <a href="#" class="color_change">Button 2</a>
   <canvas id="dot_2"></canvas>
  </div>
  <div class="buttons">
   <a href="#" class="color_change">Button 3</a>
   <canvas id="dot_3"></canvas>
  </div>
 </div>
</body>

JavaScript部分(单文件搞定,支持任意数量Canvas)

// 全局颜色配置与索引(所有Canvas同步切换)
let index = localStorage.getItem('index') ? parseInt(localStorage.getItem('index')) : 0;
const colorSets = {
  first: ["#4B2F73", "#8C0335", "#21EE03", "#011526", "#2960A4", "#0F0140", "#8C0444"],
  second: ["#3B2559", "#021E73", "#26C013", "#00070D", "#3E86C0", "#060126", "#591547"],
  third: ["#0A2B40", "#0442BF", "#29A619", "#024059", "#11C6D8", "#0D05F2", "#101C40"],
  fourth: ["#71C6D9", "#034C8C", "#737373", "#024959", "#BDA092", "#060273", "#011526"],
  fifth: ["#5FCDD9", "#023059", "#282828", "#3CA692", "#D83911", "#0ABF04", "#F20505"]
};

// 粒子类:现在接收对应的Canvas上下文作为参数,不再依赖全局
class Dot {
  constructor(context) {
    this.context = context;
    this.canvas = context.canvas;
    this.x = 0;
    this.y = 0;
    this.radius = 0;
    this.direction = 0;
    this.speed = 0;
  }

  getFillStyle() {
    const canvasWidth = this.canvas.width;
    if (this.x > 4 * canvasWidth / 5) return colorSets.fifth[index];
    if (this.x > 3 * canvasWidth / 5) return colorSets.fourth[index];
    if (this.x > 2 * canvasWidth / 5) return colorSets.third[index];
    if (this.x > canvasWidth / 5) return colorSets.second[index];
    return colorSets.first[index];
  }

  update() {
    const dir = Math.PI * this.direction / 180;
    this.y += Math.sin(dir) * this.speed;
    this.x += Math.cos(dir) * this.speed;
  }

  draw() {
    const ctx = this.context;
    ctx.beginPath();
    ctx.fillStyle = this.getFillStyle();
    ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI);
    ctx.fill();
  }

  setup() {
    this.x = Math.random() * this.canvas.width;
    this.y = Math.random() * this.canvas.height;
    this.speed = Math.random() * 1.5;
    this.direction = Math.random() * 360;
    this.radius = Math.random() * (this.canvas.width / this.canvas.height) * 0.15;
  }
}

// 单个Canvas的管理器:封装画布、上下文、粒子集合和动画逻辑
class CanvasManager {
  constructor(canvasElement) {
    this.canvas = canvasElement;
    this.context = canvas.getContext('2d');
    // 设置Canvas尺寸(可根据需求调整)
    this.canvas.width = 200;
    this.canvas.height = 200;
    this.nbDots = 20;
    this.weight = 60;
    this.net = [];
    this.initParticles();
    this.startAnimation();
  }

  initParticles() {
    for (let i = 0; i < this.nbDots; i++) {
      const dot = new Dot(this.context);
      dot.setup();
      this.net.push(dot);
    }
  }

  drawDotLines() {
    const copyNet = this.net.slice();
    while (copyNet.length > 0) {
      const dot = copyNet.pop();
      for (let n = 0; n < copyNet.length; n++) {
        const ndot = copyNet[n];
        const rx = Math.pow(dot.x - ndot.x, 2);
        const ry = Math.pow(dot.y - ndot.y, 2);
        const dist = Math.sqrt(rx + ry);
        if (dist < this.weight) {
          this.context.beginPath();
          this.context.strokeStyle = dot.getFillStyle();
          this.context.lineWidth = 2 * (this.weight - dist) / this.weight;
          this.context.moveTo(dot.x, dot.y);
          this.context.lineTo(ndot.x, ndot.y);
          this.context.stroke();
        }
      }
    }
  }

  update() {
    const ctx = this.context;
    ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    for (const dot of this.net) {
      dot.update();
      dot.draw();
      // 粒子出界重置
      if (dot.x < 0 || dot.x > this.canvas.width || dot.y < 0 || dot.y > this.canvas.height) {
        dot.setup();
      }
    }
    this.drawDotLines();
  }

  startAnimation() {
    // 用requestAnimationFrame代替setInterval,动画更流畅
    const animate = () => {
      this.update();
      requestAnimationFrame(animate);
    };
    animate();
  }
}

// 初始化所有Canvas
const canvasElements = document.querySelectorAll('canvas[id^="dot_"]');
const managers = [];
canvasElements.forEach(canvas => {
  managers.push(new CanvasManager(canvas));
});

// 颜色切换按钮事件(修复类名,同步所有Canvas的颜色)
document.querySelectorAll(".color_change").forEach(btn => {
  btn.addEventListener('click', (e) => {
    e.preventDefault(); // 阻止默认跳转
    index = index >= colorSets.first.length - 1 ? 0 : index + 1;
    localStorage.setItem('index', index);
    // 不需要手动调用update,动画循环会自动更新颜色
  });
});

关键改进点说明

  1. 实例隔离:用CanvasManager类封装每个Canvas的所有状态(上下文、粒子、动画),每个Canvas完全独立,不会互相干扰
  2. 粒子关联画布:Dot类现在接收对应的Canvas上下文作为参数,所有计算都基于当前画布的尺寸,不再依赖全局变量
  3. 更流畅的动画:用requestAnimationFrame代替setInterval,浏览器会自动优化动画帧率,避免卡顿
  4. 支持任意数量Canvas:只要你的Canvas ID以dot_开头,初始化代码会自动识别并创建管理器,加多少个都没问题
  5. 修复按钮事件:修正了类名拼写错误,用querySelectorAll绑定所有按钮的点击事件,阻止默认跳转行为

内容的提问来源于stack exchange,提问作者LightninWolf

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 08:44:45