Canvas学习笔记

89 阅读3分钟

前言:

虽然早就听说了Canvas,但是一直没有找到应用场景,所以也没有专门去学习,最近想了解下webgis,就学习了相关的知识,也就学习了Canvas了。二者的关系见链接

Canvas的文档很全面,所以这里仅仅总结了学习过程中遇到的一些关键问题,以及手写的一些案例(Canvas的API很简单,但是做成复杂的功能就需要js+实战经验了)

学习资料

  1. Canvas - Web API 接口参考 | MDN (mozilla.org)
  2. 公众号文章——Canvas 从入门到劝朋友放弃(图解版)
  3. Canvas视频教程

重难点

1px线的显示效果问题

我的理解:Canvas的线是有粗度的,所以不像数学里把两条线连起来就可以了。而像素是最小不可分割的点,所以当画一条从(100, 100)到(200, 100)的直线时,这两个点的连线即中轴线,然后 1px 的宽度在两边各占了一半,所以为了补齐整个像素点,会有半个像素被染色,整体的宽度也变成了 2px,然后总体颜色也变浅了。

var ctx = document.getElementById("myCanvas").getContext('2d')
    ctx.beginPath()
    ctx.moveTo(100, 100)
    ctx.lineTo(200, 100)
    ctx.strokeStyle = 'red'
    ctx.stroke()

image.png

而画(100, 100.5),(200, 100.5)时刚好在100到101之间,占了1px,也不会占到别的像素点。所以就是1px啦

var ctx = document.getElementById("myCanvas").getContext('2d')
    ctx.beginPath()
    ctx.moveTo(100, 100.5)
    ctx.lineTo(200, 100.5)
    ctx.strokeStyle = 'red'
    ctx.stroke()

image.png

beginPath与closePath

beginPath用于在绘制多条线段且分别设置线段样式时,开辟新路径。否则前后的线段的样式会互相影响。而且这种影响不统一,比如strokeRect画矩形,前后的样式不会影响;而rect画矩形前后样式就会影响

所以最佳实践就是**在画每一个图之前都要设置 beginPath() **

closePath不是关闭beginPath开启的路径。,而是用于闭合路径。当用线段构成图形时,最后闭合最好用closePath,而不要用lineTo去回到起点:

cxt.moveTo(5050)  
 cxt.lineTo(20050)  
 cxt.lineTo(200120)  
 cxt.lineTo(50120)  
 cxt.lineTo(5050// 需要闭合,又或者使用 closePath() 方法进行闭合,推荐使用 closePath()  
  
 cxt.stroke()

几组API分类整理

画线段

 // 起点
 cxt.moveTo(5050)  
 // 经过的点
 cxt.lineTo(20050)  
 cxt.lineTo(200120)  
 // 线的颜色
 ctx.strokeStyle = 'green'
 // 线帽
 ctx.lineCap = 'round'
 // 线宽
 ctx.lineWidth = 20
 
 cxt.stroke()

画矩形

// strokeRect() 描边矩形
cxt.strokeStyle = 'pink'
// 线宽
cxt.lineWidth = 20
// strokeRect(x, y, width, height) 方法  分别对应左上角坐标 宽、高
cxt.strokeRect(50, 50, 200, 100)
    // fillRect() 描边矩形
    // 填充颜色
    cxt.fillStyle = 'pink'
    // 线宽  这个属性就没用了
    cxt.lineWidth = 20
    // fillRect(x, y, width, height) 方法  分别对应左上角坐标 宽、高
    cxt.fillRect(50, 50, 200, 100)

画圆

案例

1.炫彩小球

动画的逻辑:清除原动画、渲染新动画…… 不停地清除、渲染,实现动画

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>炫彩小球</title>
</head>

<body>
  <canvas width="800" height="500" id="mycanvas" style="border:1px solid red"></canvas>
  <script>
    const canvas = document.getElementById('mycanvas')
    const ctx = canvas.getContext('2d')
    console.log(ctx)
    // 设置初始球类
    class Ball {
      static ballArr = []
      constructor(x, y, r) {
        this.x = x
        this.y = y
        this.r = r
        this.color = this.getRandom()
        // 设置行进方向
        this.dx = parseInt(Math.random() * 10) - 5
        this.dy = parseInt(Math.random() * 10) - 5
        // 将创建的小球存到数组中
        Ball.ballArr.push(this)
      }

      // 获取随机颜色
      getRandom () {
        const allType = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 'a', 'b', 'c', 'd', 'e', 'f']
        let color = '#'
        for (let i = 0; i < 6; i++) {
          let random = Math.floor(Math.random() * 16)
          color += allType[random]
        }
        return color
      }
      // 渲染小球
      render () {
        console.log('渲染')
        ctx.beginPath()
        ctx.arc(this.x, this.y, this.r, 0, 360 * Math.PI / 180, false)
        ctx.fillStyle = this.color
        ctx.closePath()
        ctx.fill()
      }
      // 让小球动起来
      update () {
        // 小球随机移动
        this.x += this.dx
        this.y += this.dy
        this.r -= 0.5
        if (this.r <= 0) {
          this.remove()
        }
      }
      remove () {
        for (let i = 0; i < Ball.ballArr.length; i++) {
          if (Ball.ballArr[i].r <= 0) {
            Ball.ballArr.splice(i, 1)
          }
        }
      }
    }

    // canvas设置鼠标监听
    canvas.addEventListener('mousemove', function (e) {
      Ball.ballArr.push(new Ball(e.offsetX, e.offsetY, 30))
    })
    // 定时器进行动画渲染和更新
    setInterval(() => {
      // 动画:清屏-更新-渲染
      ctx.clearRect(0, 0, canvas.width, canvas.height)
      for (let i = 0; i < Ball.ballArr.length; i++) {
        Ball.ballArr[i].render()
        Ball.ballArr[i].update()
      }

      // Ball.ballArr.splice(0, Ball.ballArr.length - 1)
    }, 50);
  </script>
</body>

</html>

2. 小球连线+碰壁折返

碰壁折返的算法核心:判断圆心与canvas边界的距离,改变dx、dy的值,即改变下一个圆的渲染位置,从而实现往反方向走

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>小球连线-碰壁折返</title>
  <style>
    #mycanvas {
      margin: 5px auto;
    }
  </style>
</head>

<body>
  <canvas id="mycanvas" style="border:1px solid red"></canvas>
  <script>
    const canvas = document.getElementById('mycanvas')
    let ctx = canvas.getContext('2d')
    // 设置画布尺寸——整个页面.注意这个-10,是为了避免小球有一半在外面
    canvas.width = document.documentElement.clientWidth - 10
    canvas.height = document.documentElement.clientHeight - 10

    // 创建小球类
    class Ball {
      static ballArr = []
      constructor() {
        this.x = parseInt(Math.random() * canvas.width)
        this.y = parseInt(Math.random() * canvas.height)
        this.r = 30
        // this.r = r
        this.color = this.getRandom()
        // 设置行进方向
        this.dx = parseInt(Math.random() * 20) - 10
        this.dy = parseInt(Math.random() * 20) - 10
        // 将创建的小球存到数组中
        Ball.ballArr.push(this)
        this.index = Ball.ballArr.length - 1
      }
      // 获取随机颜色
      getRandom () {
        const allType = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 'a', 'b', 'c', 'd', 'e', 'f']
        let color = '#'
        for (let i = 0; i < 6; i++) {
          let random = Math.floor(Math.random() * 16)
          color += allType[random]
        }
        return color
      }
      // 渲染小球
      render () {
        console.log('渲染')
        ctx.beginPath()
        // 透明度
        ctx.globalAlpha = 1
        // 画小球
        ctx.arc(this.x, this.y, this.r, 0, 360 * Math.PI / 180, false)
        ctx.fillStyle = this.color
        // ctx.closePath()
        ctx.fill()

        // 判断距离来画两点之间的线
        for (let i = this.index + 1; i < Ball.ballArr.length; i++) {
          let dis = this.judgeDis(this, Ball.ballArr[i])
          if (dis < 300) {
            ctx.strokeStyle = this.getRandom()
            ctx.beginPath()
            ctx.lineWidth = 10
            // 根据已经连线的球距离改变线的透明度
            ctx.globalAlpha = 1 - parseInt(dis / 300)
            ctx.moveTo(this.x, this.y)
            ctx.lineTo(Ball.ballArr[i].x, Ball.ballArr[i].y)
            ctx.closePath()
            ctx.stroke()
          }
        }
      }
      // 小球的更新
      update () {
        this.x += this.dx
        this.y += this.dy
        // 所谓反弹,也不过如此啦
        if (this.x <= this.r || this.x > canvas.width - this.r) {
          this.dx = -this.dx
        }
        if (this.y <= this.r || this.y > canvas.height - this.r) {
          this.dy = -this.dy
        }
      }
      // 判断距离
      judgeDis (A, B) {
        let distance = Math.sqrt(Math.pow(A.x - B.x, 2) + Math.pow(A.y - B.y, 2))
        return distance
      }

    }
    // 创建100个球
    for (let i = 0; i < 100; i++) {
      new Ball()
    }
    // 定时器动画
    for (let i = 0; i < Ball.ballArr.length; i++) {
      Ball.ballArr[i].render()
    }
    setInterval(() => {
      // 先清屏再画新的点
      ctx.clearRect(0, 0, canvas.width, canvas.height)
      for (let i = 0; i < Ball.ballArr.length; i++) {
        Ball.ballArr[i].render()
        Ball.ballArr[i].update()
      }
    }, 10)

  </script>
</body>

</html>