前端基础系列之Html-canvas手撕粒子线条

68 阅读2分钟

内容: canvas详细使用和解题思路

效果: 如下图

lizixiantiao.jpg

一、使用canvas画线
<canvas id="canvas" ref="canvasRef"></canvas>
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
onMounted(() => {
  init()
})
function init() {
  const canvas = canvasRef.value // 获取canvas
  const ctx = canvas.getContext('2d') // 建立一个二维渲染上下文
  [查看api](https://www.canvasapi.cn/HTMLCanvasElement/getContext)
  canvas.width = window.innerWidth; // canvas宽度为窗口的文档显示区的宽度
  canvas.height = window.innerHeight; // canvas高度为窗口的文档显示区的高度
  
  ctx.lineWidth = 0.3 // 设置线宽,默认为1
  ctx.strokeStyle = 'red'; // 设置描边颜色
  ctx.beginPath(); // 开始绘制
  ctx.moveTo(100, 100); // 起始点移动到100,100
  ctx.lineTo(mouse.value.x, mouse.value.y); // 从100,100到鼠标点击位置
  ctx.stroke() // 绘制
  ctx.closePath() // 结束
}
// 好了,线画完了,基本的canvas使用就这么简单
<script>
二、实现随机在画布上放置点

先理一下思路,既然是随机放置点,点的颜色要不同,点的位置要随机,点的数量要多,我们定义一个点类,里面有颜色,位置等属性和画点的方法,在init()函数中循环调用,就可以随机画点了

class Point {
  private _x: number;
  private _y: number;
  private _ctx: any;
  private _random: number;
  private _color: string;
  private _scale: number;
  public canvas: any;
  constructor(x: number, y: number, ctx: any, canvas: any) {
    this._x = x; // 循环的时候传如画布宽度内的随机数
    this._y = y; // 循环的时候传如画布高度内的随机数
    this._ctx = ctx; // 使用ctx
    this.canvas = canvas; // 使用canvas
    this._random = Math.random() * 100; // 生成随机数给四种情况赋值
    this._color = color(); // 点的颜色
    this._scale = randomNumber(1, 2) // 点的大小
    this.draw() // 绘制
  }
  draw() {
    const ctx = this._ctx
    try {
      ctx.fillStyle = this._color // 设置填充颜色
      ctx.beginPath() // 开始绘制
      ctx.arc(this._x, this._y, this._scale, 0, 2 * Math.PI) // 绘制
      ctx.closePath() // 结束
      ctx.fill() // 填充
    }
    catch (err) {
    }
  }
}
/**
 * @description: 生成min-max之间随机数
 */
function randomNumber(min: number, max: number): number {
  return Math.floor(Math.random() * (max - min + 1) + min);
}
/**
 * @description: 生成随机颜色
 */
function color(): string {
  let sum = ""
  for (let i = 0; i < 6; i++) {
    sum += Math.floor(Math.random() * 16).toString(16)
  }
  return '#' + sum
}
function init() {
  ...
  // 在init中画200个点,之前的线可以删掉了
  for (let i = 0; i < 200; i++) {
    const [x, y] = [randomNumber(0, window.innerWidth), randomNumber(0, window.innerHeight)]
    new Point(x, y, ctx)
  }
}
// 到这里,两百个随机点就放上了
三、让点动起来

使用requestAnimationFrame(),(让网页动画效果能够有一个统一的刷新机制,请百度or谷歌,目前鱼儿是百度搜索引擎工程师),并定义点的render方法,在实例建立后不断的让实例的x和y属性变化,就动起来了

class Point {
  ...
  render() {
    const random = this._random // [0,100)随机数
    switch (true) {
      case random >= 0 && random < 25:
        this._x = this._x + 0.4
        this._y = this._y + 0.4
        break; // 右下
      case random >= 25 && random < 50:
        this._x = this._x - 0.4
        this._y = this._y + 0.4
        break; // 左下
      case random >= 50 && random < 75:
        this._x = this._x - 0.4
        this._y = this._y - 0.4
        break; // 左上
      case random >= 75 && random < 100:
        this._x = this._x + 0.4
        this._y = this._y - 0.4
        break; // 右上
    }
    if (this._x < 0 || this._x > this.canvas.width) { // 如果小于0或者大于画布宽度,则随机重新放置
      this._x = randomNumber(0, window.innerWidth)
    }
    if (this._y < 0 || this._y > this.canvas.height) { // 如果小于0或者大于画布高度,则随机重新放置
      this._y = randomNumber(0, window.innerHeight)
    }
    this.draw()
  }
}
function init() {
  ...
  const pointBox = [] as any // 收集点实例,以便使用实例的属性
  for (let i = 0; i < 200; i++) {
    const [x, y] = [randomNumber(0, window.innerWidth), randomNumber(0, window.innerHeight)]
    const point = new Point(x, y, ctx, canvas) // 渲染的时候循环调用这些点自身的方法
    pointBox.push(point)
  }
  function render(): void {
    ctx.clearRect(0, 0, canvas.width, canvas.height); // 不断清除画布内容再绘制,形成动画效果
    for (let i = 0; i < pointBox.length; i++) {
       pointBox[i].render() // 调用点的render()方法,改变x,y属性
    }
    requestAnimationFrame(render) // 重绘移动点
  }
  render()
}
// 你的点动起来了吗?这里再塞一个个监听事件
window.addEventListener('resize', function () { // 窗口变化也要重新绘制
  init()
});
四、开始画线

现在一共有200个点,遍历每个点,从每个点出发,找到它周围一定距离的所有点,画线连接即可

// 先声明线类
class Line {
  private _ctx;
  private _ordinal: any; // 起始点
  private _target: any; // 目标点
  private _color: string; // 线的颜色,为传入的起始点的颜色
  constructor(ctx: any, _ordinal: object, target: object = { x: 100, y: 100 }, color: string) {
    this._ctx = ctx;
    this._ordinal = _ordinal;
    this._target = target;
    this._color = color;
    this.draw();
  }
  draw() {
    const ordinal = this._ordinal
    const target = this._target
    const ctx = this._ctx
    ctx.lineWidth = 0.3 // 设置线宽,默认为1
    ctx.strokeStyle = this._color; // 设置描边颜色
    ctx.beginPath(); // 开始绘制
    ctx.moveTo(ordinal.x, ordinal.y); // 起始点
    ctx.lineTo(target.x, target.y); // 目标点
    ctx.stroke() // 绘制
    ctx.closePath() // 结束
  }
}
const mouse = ref({ // 记录鼠标位置
  x: 0,
  y: 0
})
function init() {
   ...
   function render(): void {
    ctx.clearRect(0, 0, canvas.width, canvas.height); // 不断清除画布内容再绘制,形成动画效果
    for (let i = 0; i < pointBox.length; i++) {  // 每个点都要寻找自己附近的点作为target去链接
      for (let j = 0; j < pointBox.length; j++) { // 再次遍历。寻找周围点
        if ((pointBox[j]._x < (pointBox[i]._x + 100) && pointBox[j]._x > (pointBox[i]._x - 100)) 
        && (pointBox[j]._y < (pointBox[i]._y + 100)) && pointBox[j]._y > (pointBox[i]._y - 100)) { 
        // 满足上下左右不超过pointBox[i]的100,就是它附近的点,这里已经可以绘制线了
          if ((pointBox[j]._x < (mouse.value.x + 100) && pointBox[j]._x > (mouse.value.x - 100)) 
          && (pointBox[j]._y < (mouse.value.y + 100)) && pointBox[j]._y > (mouse.value.y - 100)) { 
          // 加一个鼠标那在哪里,哪里就有线的判断,满足鼠标区域内才绘制点
            new Line(ctx, { x: pointBox[i]._x, y: pointBox[i]._y }, { x: pointBox[j]._x, y: pointBox[j]._y }, pointBox[i]._color)
          }
        }
      }
      pointBox[i].render() // 重新调用这些点,发现点直接移动出了画布,需要加判断条件
    }
    requestAnimationFrame(render)
  }
  render()
}
window.addEventListener('mousemove', function (e) {
  // 鼠标位置,用于满足鼠标区域内才绘制点判断
  mouse.value.x = e.pageX;
  mouse.value.y = e.pageY;
});

好了,粒子线条实现了.其实可以拓展出很多很多,比如下雨、撒花场景,或者增加其它鼠标特效,关键是看我们想做什么了