一、canvas简介

  • canvas(“画布”)本身是HTML5提供的一种新标签, <canvas>标签本身只是图形容器,需要通过脚本 (通常是JavaScript)可以进行图形的绘制(canvas有多种绘制路径、矩形、圆形、字符以及添加图像的方法)。
  • canvas目前主要可以应用于游戏、数据可视化、图片的操作等领域。

二、canvas基础用法

1.canvas标签

canvas标签可以说是我们绘制的容器,内容的绘制我们需要依靠canvas的context对象来实现,标签有两个自有的属性widthheight,用来规定画布的宽高,<canvas> 标签支持 HTML 的全局属性,另外我们可以在标签中嵌入后备替代文本,可以在不支持canvas元素的设备上让用户了解当前内容,类似img的alt属性在图像为正常显示时,代替图像展示。

<canvas id="myCanvas" width="800" height="800">
  您的浏览器不支持canvas,请更换浏览器。
</canvas>
2.canvas的坐标系

在进行绘制操作前我们还需要了解一下canvas的坐标
image.png

3.基本api

context:canvas的上下文、绘制环境或者我们可以理解为运行环境,canvas所有的api操作都是基于context,我们需要通过js来操作context来实现我们所需。

从下面的代码中我们可以学习如何进行文字、线段、矩形、圆形等基本图形的绘制

    const canvasDom = document.getElementById('myCanvas') // 获取画布元素
    const ctx = canvasDom.getContext('2d') // 获取canvas对象 2D绘图的上下文
    ctx.font = '38px Arial' // 文本相关设置
    ctx.fillStyle = 'orange' // 填充色
    ctx.fillText('一些基础图形', 320, 300) // 绘制填充的文本
    ctx.fillRect(50, 198, 50, 104) // 绘制矩形

    ctx.beginPath() // 绘制路径开始
    ctx.lineWidth = 3 // 线条宽度
    ctx.strokeStyle = 'red' // 填充色
    ctx.moveTo(100, 200) // 绘制起点(x, y)
    ctx.lineTo(300, 300) // 绘制直线(x,y)
    ctx.lineTo(100, 300)
    ctx.closePath() // 绘制路径闭合 闭合路径会自动把结束的点和开始的线连在一起
    ctx.stroke() // 绘制线段

    ctx.beginPath()
    ctx.lineWidth = 16
    ctx.strokeStyle = 'blue'
    ctx.fillStyle = 'green'
    // 绘制圆形 圆心坐标 半径长度 初始角度 结束角度 顺时针、逆时针绘制
    // (x,y,r,sAngle,eAngle,counterclockwise)
    ctx.arc(220, 194, 50, 0, 1 * Math.PI, false)
    ctx.stroke()
    ctx.fill() // 是将闭合的路径的内容进行填充
    ctx.beginPath()
    ctx.strokeStyle = 'green'
    ctx.fillStyle = 'blue'
    ctx.arc(220, 194, 50, 0, 1 * Math.PI, true)
    ctx.stroke()
    ctx.fill()
    ctx.beginPath()
    ctx.fillStyle = 'red'
    ctx.lineWidth = 2
    ctx.rect(110, 240, 50, 50) // 绘制矩形 (x, y, width, height)
    ctx.stroke()
    ctx.fill()

运行效果如图:
image.png
本例完整代码

最后我们可以使用clearRect进行清除操作,参数和绘制矩形一样 坐标以及清除的长宽(x, y, width, hegiht)。
清除局部的操作可以在类似涂鸦白板的场景中的实现橡皮擦功能。
而当我们把初始坐标(0,0)和当前画布宽高传入就可以对整个画面做清空操作。

    ctx.clearRect(110, 240, 20, 20) // 清除局部
    ctx.clearRect(0, 0, 800, 800) // 初始点坐标 加容器宽高 清除全部

image.png

下面案例会详细讲解图片的绘制drawImage 一个非常强大api

三、canvas基础案例

0.准备工作

drawImage是一个功能强大的api它支持向画布上渲染图片而且输入源支持图像、画布以及视频,通过它我们也可以做很多有趣的事。
以下案例大多都需要使用drawImage方法

drawImage(img,sx,sy,swidth,sheight,x,y,width,height)
参数说明是否必须
img规定要使用的图像、画布或视频
sx开始剪切的 x 坐标位置
sy开始剪切的 y 坐标位置
swidth被剪切图像的宽度
sheight被剪切图像的高度
x在画布上放置图像的 x 坐标位置
y在画布上放置图像的 y 坐标位置
width要使用的图像的宽度
height要使用的图像的高度

api参数很简单下面我们直接结合实例看一下

1.本地图片预览 支持放大镜旋转

本案例可以学习drawImage 9个参数的使用

核心代码:

    const ACCEPT = ['image/jpg', 'image/png', 'image/jpeg'] // 允许的图片格式
    const uploadDom = document.getElementById('upload') // 获取upload元素
    const canvasDom = document.getElementById('imgCanvas') // 获取画布元素
    const ctx = canvasDom.getContext('2d') // 获取canvas对象 2D绘图的上下文
    const magnifierDom = document.getElementById('magnifierCanvas') // 获取画布元素
    const magnifierCtx = magnifierDom.getContext('2d') // 获取canvas对象 2D绘图的上下文
    const imgBoxSize = 400 // 容器尺寸
    const xpos = imgBoxSize / 2
    const ypos = imgBoxSize / 2
    let currentRotate = 0 // 当前旋转角度
    let base64 = null // 缓存所选图片
    let magnifierX = 120
    let magnifierY = 120
    let curTime = null
    function draw (base64Img) {
      ctx.clearRect(0, 0, 400, 400)
      const img = new Image()
      img.src = base64Img || base64
      // img.crossOrigin = '*' // 开启CORS功能
      // 图片加载
      img.onload = function () {
        // 旋转图片
        if (currentRotate > 0) {
          ctx.save() // 保存当前的绘图状态
          // canvas 中的所有几何变换针对的不是绘制的图形,而是针对画布本身
          ctx.translate(xpos, ypos) // 调整画布初始点位置
          ctx.rotate(currentRotate * Math.PI / 180) // 旋转当前的绘图
          ctx.translate(-xpos, -ypos) // 复原
        }
        // 绘制
        ctx.drawImage(img, 0, 0, imgBoxSize, imgBoxSize)
        drawMagnifier()
        if (currentRotate > 0) {
          ctx.restore() // 恢复之前保存的绘图状态
        }
        if (currentRotate > 270) {
          currentRotate = 0
        }
      }
    }
    // 绘制放大区域
    function drawMagnifier () {
      magnifierCtx.clearRect(0, 0, 400, 400)
      const img = new Image()
      img.src = base64
      // 图片加载
      img.onload = function () {
        const scaleX = img.naturalWidth / imgBoxSize
        const scaleY = img.naturalHeight / imgBoxSize
        // 旋转后 绘制坐标计算 后续补充
        if (currentRotate > 0) {
          magnifierCtx.save() // 保存当前的绘图状态
          magnifierCtx.translate(xpos, ypos) // 调整画布初始点位置
          magnifierCtx.rotate(currentRotate * Math.PI / 180) // 旋转当前的绘图
          magnifierCtx.translate(-xpos, -ypos) // 复原
        }
        // 绘制放大
        magnifierCtx.drawImage(img, (magnifierX-25) * scaleX, (magnifierY-25) * scaleY, 50 * scaleX, 50 * scaleY, 0, 0, imgBoxSize, imgBoxSize)
        if (currentRotate > 0) {
          magnifierCtx.restore() // 恢复之前保存的绘图状态
        }
      }
    }
    // 更新旋转角度
    function updateRotate () {
      if (!base64) return
      currentRotate += 90
      draw()
    }

放大效果:
image.png
旋转效果:
image.png

本例完整代码

2.本地压缩图片下载&&常规文件格式体积限制
<body>
  <input type="file" id="upload" />
  <script>
    const ACCEPT = ['image/jpg', 'image/png', 'image/jpeg'] // 允许的图片格式
    const maxSize = 1024 * 1024 // 文件最大
    const uploadDom = document.getElementById('upload') // 获取upload元素
    const max = 1024 // 压缩图片尺寸边界
    let fileType = '' // 保存文件类型 以及 名称 重新生成时进行回填
    let fileName = ''
    // base64读取 执行压缩回调
    function converImgToBase64 (file, cb) {
      let reader = new FileReader()
      reader.addEventListener('load', function (e) {
        const base64Img = e.target.result
        cb && cb(base64Img, downloadImg)
        reader = null
      })
      reader.readAsDataURL(file)

    }
    // 压缩
    function compress (base64Img, cb) {
      let imgW, imgH;
      const _img = new Image()
      _img.src = base64Img
      _img.addEventListener('load', function (e) {
        let ratio; // 图片长宽比
        let needCompress = false // 是否需要压缩判断
        imgW = _img.naturalWidth // 图像的原始宽度
        imgH = _img.naturalHeight // 图像的原始高度
        // 图片宽高任意超过我们设置的最大边界 进行压缩
        if (max < _img.naturalHeight || max < _img.naturalWidth) {
          needCompress = true
          if (_img.naturalHeight < _img.naturalWidth) {
            ratio = _img.naturalWidth / max
          } else {
            ratio = _img.naturalHeight / max
          }
          imgW = imgW / ratio
          imgH = imgH / ratio
        }
        // 创建canvas元素 渲染处理尺寸后的图片
        const _canvas = document.createElement('canvas')
        _canvas.setAttribute('id', 'compressImg')
        _canvas.width = imgW
        _canvas.height = imgH
        // 隐藏元素
        _canvas.style.visibility = 'hidden'
        document.body.appendChild(_canvas)
        const ctx = _canvas.getContext('2d')
        ctx.clearRect(0, 0, imgW, imgH)
        ctx.drawImage(_img, 0, 0, imgW, imgH)
        if (!fileName || !fileType) {
          return _canvas.remove()
        }
        // 方法返回一个包含图片展示的 data URI 
        // (type, encoderOptions) 图片格式 图片质量0-1之间
        const compressImg = _canvas.toDataURL(fileType, 0.8)
        cb && cb(compressImg)
        _canvas.remove() // 移除元素
      })
    }
    // 创建a标签触发点击事件进行下载
    function downloadImg (file) {
      const _a = document.createElement('a')
      _a.href = file
      _a.setAttribute('download', fileName) // 使用读取文件保存的文件名
      _a.click()
    }
    uploadDom.addEventListener('change', function (e) {
      const [file] = e.target.files
      if (!file) {
        return
      }
      const { type: _fileType, size: _fileSize, name: _fileName } = file
      // 文件类型检查
      if (!ACCEPT.includes(_fileType)) {
        uploadDom.value = ''
        return console.error('不支持当前文件格式')
      }
      fileType = _fileType
      fileName = _fileName
      // 文件体积检查
      if (_fileSize > maxSize) {
        uploadDom.value = ''
        return console.error('当前文件体积过大')
      }
      // base64转化
      converImgToBase64(file, compress)
    })
  </script>
</body>

压缩前后文件对比:
image.png
本例完整代码

3.解决canvas转图片不清晰的问题

在开发中我遇到过绘制的图片看起来十分的模糊的问题,实际上是画布尺寸与画布范围内实际像素不一致造成。我当时直接是直接用容器宽高*2处理的

const canvas = document.getElementById('canvas')
  canvas.style.backgroundColor = '#FFF'
  canvas.style.width = _width + 'px'
  canvas.style.height = _height + 'px'
  canvas.width = _width * 2
  canvas.height = _height * 2

更优的方案应该是根据设备的dpr去做

const canvas = document.getElementById('canvas')
const dpr = (scale = window.devicePixelRatio || 1)
const rect = canvas.getBoundingClientRect()
  canvas.width = rect.width * dpr
  canvas.height = rect.height * dpr
  canvas.style.width = rect.width + 'px'
  canvas.style.height = rect.height + 'px'

处理前
image.png
处理后
image.png

4.最后附上一个自己很喜欢的小效果
<body>
  <canvas id="myCanvas">
    您的浏览器不支持canvas,请更换浏览器
  </canvas>
  <script>
    const canvasDom = document.getElementById('myCanvas')
    const ctx = canvasDom.getContext('2d')
    const getRandomFloat = (min, max) => (max - min) * Math.random() + min
    const getRandomInt = (min, max) => Math.floor(getRandomFloat(min, max + 1))
    class Container {
      constructor() {
        this.update()
      }
      update() {
        this.width = window.innerWidth
        this.height = window.innerHeight
        this.center = {
          x: this.width / 2,
          y: this.height / 2
        }
        canvasDom.width = this.width
        canvasDom.height = this.height
      }
    }
    const _container = new Container()
    const maxAmp = 250
    const minAmp = 150
    const numParticles = 120
    class Tracks {
      constructor() {
        const numTracks = 20
        this.tracks = []
        for (let i = 0; i < numTracks; ++i) {
          const ratio = i / numTracks
          this.tracks.push({
            amp: (maxAmp - minAmp) * ratio + minAmp
          })
        }
      }
    }
    const { tracks } = new Tracks()
    class Particle {
      constructor() {
        this.init()
      }
      init() {
        this.baseAmp = tracks[getRandomInt(0, tracks.length - 1)].amp
        this.rotSpeed = getRandomFloat(0.01, 0.015)
        this.ampSpeed = 1
        this.baseRad = 3
        this.more = 10
        this.num = 0
        this.amp = this.baseAmp
        this.rad = this.baseRad
        this.angle = getRandomFloat(0, Math.PI * 2)
        this.pos = {
          x: 0,
          y: 0
        }
      }
      updateRadius() {
        let ratio = this.amp / this.baseAmp * 2.5 - 1.5
        this.rad = ratio * this.baseRad
      }
      updatePosition() {
        this.angle += this.rotSpeed
        this.pos.x = _container.center.x + this.amp * Math.cos(this.angle) * 1.2
        this.pos.y =
          _container.center.y + this.amp * Math.pow(Math.sin(this.angle), 3) * 0.8
      }
      draw() {
        const { pos } = this
        const ageAttack = this.num / this.more
        const rad = this.rad * ageAttack
        const alpha = ageAttack
        ctx.beginPath()
        ctx.arc(pos.x, pos.y, rad, 0, Math.PI * 2)
        ctx.fillStyle = `rgba(${Math.floor(Math.random()*255)}, 255, 255, ${alpha}`
        ctx.fill()
      }
      update() {
        if (this.num < this.more) {
          this.num++
        }
        this.amp -= this.ampSpeed
        this.updateRadius()
        if (this.rad > 0) {
          this.updatePosition()
          this.draw()
        } else {
          this.init()
        }
      }
    }
    class Emitter {
      constructor() {
        this.particles = [...Array(numParticles).keys()].map(
          () => new Particle()
        )
      }
      update() {
        this.particles.forEach(particle => particle.update())
      }
    }
    const emitter = new Emitter()
    initCanvasColor = () => {
      ctx.fillStyle = `rgba(255, 255, 255, 1)`
      ctx.fillRect(0, 0, canvasDom.width, canvasDom.height)
      ctx.fillStyle = `rgba(0, 0, 0, 0.08)`
      ctx.fillRect(0, 0, canvasDom.width, canvasDom.height)
    }
    const render = () => {
      ctx.fillStyle = `rgba(0, 0, 0, 0.08)`
      ctx.fillRect(0, 0, canvasDom.width, canvasDom.height)
      emitter.update()
      requestAnimationFrame(render)
    }
    initCanvasColor()
    render()
  </script>
</body>

image.png

以上所有案例完整源码 github 地址

四、浏览器支持

  • canvas的基础支持基本覆盖当前主流浏览器
  • canvas在移动端的兼容情况非常不错
  • IE9以下浏览器可以考虑使用 Explorercanvas

image.png

*目前3D(WebGL)和2D在一些老版本浏览器兼容有所不同

image.png
image.png

更多相关支持可以查看 caniuse

五、第三方库推荐


清水
26 声望2 粉丝

清水既心、