數據可視化之路 - canvas 粒子背景效果

以前的文章提到過 canvas 動畫的難點和通常規則。canvas 採用了一種叫作 當即渲染模式 ,調用繪製方法後 canvas 會直接將圖形繪製到畫布上,而後就再也不管它,canvas 也不會記住你繪製的任何圖形。這和 dom 模型不一樣,咱們始終能夠在內存中獲取到 dom 元素的引用。基於這種特色,咱們在動畫每一幀以前都會清除畫布,從新繪製一遍。繪製步驟以下:javascript

  1. 初始化圖形的狀態,如座標、大小、速度,運動方向等
  2. 清除畫布,根據圖形狀態調用繪製函數畫圖
  3. 更新圖形狀態,返回步驟 2

本文咱們作一個粒子動畫效果看看:java

需求分析

從圖中能夠看出,粒子運動效果依據如下規則:git

  1. 畫布上有固定若干個粒子和線組成,一開始生成n個隨機座標的粒子
  2. 粒子朝隨機方向固定速度運動,當觸碰到畫布邊緣時回彈
  3. 鼠標移動到畫布上時,會在鼠標位置生成一個粒子
  4. 當兩個粒子直線距離小於必定距離時連線,不然不連線或取消連線

實現分析(不帶代碼)

  1. 爲了記錄粒子的座標、速度、方向、連線等狀態,並封裝繪圖方法,咱們須要一個粒子類,爲了繪製n個粒子,咱們須要一個數組在存放粒子對象。首先將粒子類初始化,賦予隨機的座標並給定初始速度和初始方向。
  2. 往粒子數組內追加一個鼠標粒子對象,座標爲當前鼠標位置,監聽鼠標滑動並更新鼠標粒子座標。
  3. 每一幀咱們會按速度和方向更新粒子對象的座標並重繪畫布,當監測粒子座標超出畫布時,給定粒子類一個相反的方向。
  4. 當檢測到兩個粒子距離小於必定距離時,連線。

代碼解讀

首先是粒子類,它接受一個渲染上下文做爲參數,並封裝了粒子的狀態和繪圖方法。github

class Particle {
  ctx: CanvasRenderingContext2D;
  x: number;
  y: number;
  vx: number;
  vy: number;
  constructor(ctx: CanvasRenderingContext2D) {
    this.ctx = ctx;
    this.x = random(0, CANVAS_WIDTH);
    this.y = random(0, CANVAS_HEIGHT);
    this.vx = random(-VELOCITY, VELOCITY);
    this.vy = random(-VELOCITY, VELOCITY);
  }
  draw() {
    if (this.x > CANVAS_WIDTH || this.x < 0) {
      this.vx = -this.vx;
    }
    if (this.y > CANVAS_HEIGHT || this.y < 0) {
      this.vy = -this.vy;
    }
    this.x += this.vx;
    this.y += this.vy;
    this.ctx.beginPath();
    this.ctx.arc(this.x, this.y, PARTICLE_RADIUS, 0, Math.PI * 2);
    // this.ctx.closePath();
    this.ctx.fill();
  }
}
複製代碼

其次是鼠標粒子,它接受渲染上下文和dom容器做爲參數,和粒子類相同,而且監聽容器的鼠標位置以更新其座標。canvas

class MouseParticle {
  ctx: CanvasRenderingContext2D;
  x = -1;
  y = -1;
  // 單例的
  static instance: MouseParticle;
  constructor(ctx: CanvasRenderingContext2D, container: HTMLCanvasElement) {
    this.ctx = ctx;
    if (MouseParticle.instance) return MouseParticle.instance;
    container.addEventListener("mouseenter", e => {
      this.x = e.clientX;
      this.y = e.clientY;
    });
    container.addEventListener("mousemove", e => {
      this.x = e.clientX;
      this.y = e.clientY;
    });
    container.addEventListener("mouseleave", e => {
      // 移到canvas外
      this.x = -1;
      this.y = -1;
    });
    MouseParticle.instance = this;
    return this;
  }
  draw() {
    this.ctx.beginPath();
    this.ctx.arc(this.x, this.y, PARTICLE_RADIUS, 0, Math.PI * 2);
    this.ctx.fill();
  }
}
複製代碼

接着咱們初始化粒子和鼠標粒子對象,並保存進粒子數組。數組

const particles: (Particle | MouseParticle)[] = [];
for (let index = 0; index < PARTICLE_COUNT; index++) {
  const particle = new Particle(ctx);
  particles.push(particle);
}
const mouseParticle = new MouseParticle(ctx, canvas);
複製代碼

接下來是咱們的主繪圖函數,它會在每一幀調用,根據粒子的狀態重繪畫布。dom

function draw(ctx: CanvasRenderingContext2D) {
  requestAnimationFrame(() => draw(ctx));
  // 清除畫布並重繪粒子
  ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  particles.forEach(particle => {
    particle.draw();
  });
  // 重繪線
  ctx.save();
  particles.forEach(source => {
    particles.forEach(target => {
      const xDistance = Math.abs(source.x - target.x);
      const yDistance = Math.abs(source.y - target.y);
      const dis = Math.round(
        Math.sqrt(Math.pow(xDistance, 2) + Math.pow(yDistance, 2))
      );
      if (dis < PARTICLE_DISTANCE) {
        ctx.globalAlpha = dis / PARTICLE_DISTANCE;
        ctx.beginPath();
        ctx.moveTo(source.x, source.y);
        ctx.lineTo(target.x, target.y);
        ctx.closePath();
        ctx.stroke();
      }
    });
  });
}
複製代碼

在主繪圖函數裏咱們遍歷粒子數組,當兩個粒子距離小於必定距離時連線。函數

完整的代碼在這裏post

這樣咱們就完成一個粒子效果啦~~~動畫


本人正在編寫數據可視化之路系列文章,輸出一些可視化方面的教程和經驗,你能夠經過如下途徑閱讀它們。

相關文章
相關標籤/搜索