Skip to content

HTML5 引入了 CanvasWebGL,使得在浏览器中绘制 2D3D 图形成为可能,提供了更丰富的视觉效果和交互性。

5-1.canvas

1.介绍

canvas可用于绘制 2D 图像。它在文档流中是行内块元素。可以通过 css 设置 widthheight

HTMLCanvasElement 本身也有 widthheight 属性。

我们通常会直接设置,不设置的话,<canvas>默认width300px,height150px

譬如下面这个没有设置宽和高的 <canvas>

这是一个背景色为skyblue的canvas

2.绘图面积与元素面积

我们知道设置 canvas 大小的方式有两种:

  1. 直接设置 <canvas> 标签的 widthheight,即 element-properties
  2. 通过 css 来设置 <canvas>widthheight,即 css-properties

但是这两种方式影响 <canvas> 的方式是不同的。它们分别影响的是绘图面积元素面积。(至少我是这么理解的😂)。

下面通过在 600 ✖️ 300 的画布上绘制一个 300 ✖️ 150 的矩形,分成3种情况来分析:

2-1.只设置element-properties

html
<canvas id="canvas-2-1" width="600" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
  var canvas = document.querySelector('#canvas-2-1')
  var ctx = canvas.getContext('2d')
  ctx.rect(0, 0, 300, 150)
  ctx.fillStyle = 'skyblue'
  ctx.fill()
</script>

绘制出来的矩形大小确实是 300 ✖️ 150。这种形式的绘图也是我们平常使用的最多的。

2-2.只设置css-properties

html
<canvas id="canvas-2-2" style="width: 600px; height: 300px; border: 1px solid #ccc;"></canvas>
<script>
  var canvas = document.querySelector('#canvas-2-2')
  var ctx = canvas.getContext('2d')
  ctx.rect(0, 0, 300, 150)
  ctx.fillStyle = 'skyblue'
  ctx.fill()
</script>

当使用 css 来设置 widthheight 后,会发现 300 ✖️ 150 的矩形被绘制成了 600 ✖️ 300

这是因为虽然没有设置 element-properties,但是这时 canvas 默认的绘图面积就是 300 * 150

它会先以这种设置来绘制,然后将 canvas 填充到元素面积为 600 ✖️ 300 的元素中。

2-3.同时设置element-propertiescss-properties

html
<canvas id="canvas-2-3" width="600" height="300" style="width: 300px; height: 150px; border: 1px solid #ccc;"></canvas>
<script>
  var canvas = document.querySelector('#canvas-2-3')
  var ctx = canvas.getContext('2d')
  ctx.rect(0, 0, 300, 150)
  ctx.fillStyle = 'skyblue'
  ctx.fill()
</script>

将绘图面积设置成 1200 ✖️ 600,元素面积设置成 600 ✖️ 300。同样绘制一个 300 ✖️ 150 的矩形,实际上会绘制 150 ✖️ 75。具体结果如下:

ios 上,利用 canvas 绘制图片时,由于其高倍屏,可能会出现图片模糊的问题,这时就可以采用这种解决方式,来绘制2倍图甚至多倍图。

2-4.总结

其实绘图面积与元素面积的比例不止会影响图形的大小,也会影响图形的坐标位置

假设绘图面积为 draw,元素面积为 ele,要绘制的图形的宽度为 width, 实际绘制的图形的宽度为 w。(高度、x 坐标、y 坐标同理)那么比例关系如下:

w = width * (ele / draw)

因此为了保证绘制正确,通常还要结合 ctx.scale(scaleX, scaleY) 来设置画布缩放。

3.获取2D上下文

HTMLCanvasElement 具有一个 getContext 方法。我们可以通过它来获取目标 canvas2d 上下文,进而绘制图形。

html
<canvas id="canvas-3"></canvas>
<script>
  var canvas = document.querySelector('#canvas-3')
  var ctx = canvas.getContext('2d')
</script>

而且同一个 HTMLCanvasElement 调用多次 getContext 方法,返回的对象都是同一个。

canvas绘制图形,分为两种方式:描边(stroke)与填充(fill)。这里先将二者的相关 api 进行分类下:

type描边填充
strokefill
矩形strokeRectfillRect
文本strokeTextfillText
设置颜色strokeStylefillStyle

4.绘制线条line

4-1.绘制线条并描边

html
<canvas id="canvas-4-1" style="border: 1px solid #ccc;"></canvas>
<script>
  var canvas = document.querySelector('#canvas-4')
  var ctx = canvas.getContext('2d')
  ctx.moveTo(100, 50)
  ctx.lineTo(200, 50)
  // 不要忘记描边 否则图形显示不出来
  ctx.stroke()
</script>

4-2.线条模糊

上面线条貌似有点模糊。由于 canvas 绘制时,它的 1px 会各占左右 0.5px。产生虚影,看上去就跟模糊了一样。

解决办法是将绘制坐标减去 0.5 即可。本例代码是这样:

html
<canvas id="canvas-4-2" style="border: 1px solid #ccc;"></canvas>
<script>
  var canvas = document.querySelector('#canvas-4-1')
  var ctx = canvas.getContext('2d')
  ctx.moveTo(100, 49.5)
  ctx.lineTo(200, 49.5)
  ctx.stroke()
</script>

看起来好多了~

4-3.beginPath

我想要绘制两条线,一条颜色是红色,另一条是蓝色。代码如下:

html
<canvas id="canvas-4-3-1" style="border: 1px solid #ccc;"></canvas>
<script>
  var canvas = document.querySelector('#canvas-4-3-1')
  var ctx = canvas.getContext('2d')
  ctx.moveTo(100, 49.5)
  ctx.lineTo(200, 49.5)
  // 设置样式需要在stroke或者fill方法调用之前
  ctx.strokeStyle = 'red'
  ctx.stroke()
  ctx.moveTo(100, 99.5)
  ctx.lineTo(200, 99.5)
  ctx.strokeStyle = 'blue'
  ctx.stroke()
</script>

然后发现绘制了两条蓝色的线条:

之前说过,同一个 canvas 只会有一个 ctx。所以这个 ctx 是共用的,这样就导致绘制线条时,互相之间会有影响。

最好解决办法是使用 beginPath

我们推荐只要是非连续路径绘制,就使用beginPath

改进后的代码:

html
<canvas id="canvas-4-3-2" style="border: 1px solid #ccc;"></canvas>
<script>
  var canvas = document.querySelector('#canvas-4-3-2')
  var ctx = canvas.getContext('2d')
  // 第一个线条的beginPath
  ctx.beginPath()
  ctx.moveTo(100, 49.5)
  ctx.lineTo(200, 49.5)
  // 注意,如果这里没有设置strokeStyle的话,线条也会被影响成蓝色。所以strokeStyle一般考虑与save restore方法联用,以保证画布其他部分不受影响。
  ctx.strokeStyle = 'red'
  ctx.stroke()
  // 第二个线条的beginPath
  ctx.beginPath()
  ctx.moveTo(100, 99.5)
  ctx.lineTo(200, 99.5)
  ctx.strokeStyle = 'blue'
  ctx.stroke()
</script>

4-4.线条设置属性

  • 宽度 lineWidth

    默认值是1.0(直接绘制图形,会模糊的原因,浏览器将 1px 边缘柔化)。如果是负数、0、NaN 或者 Infinity 都会被忽略。

  • 端点 lineCap

    butt: 默认值。round: 线的两端会出现半圆。square:线的两端出现方块。

  • 转角 lineJoin

    英文单词 join 意为 连接。这里也就是指线条的转角位置。

    miter: 默认值,尖头。通常与 miterLimit 联用,对尖头的尖锐程度进行限制。

    round: 圆头。bevel: 平头。

  • 虚线偏移距离 lineDashOffset

    该属性可以设置虚线起始绘制的偏移距离,为浮点型。默认值为 0.0。通常与设置虚线 setLineDash 联用。与其相对的方法是 getLineDash

4-5.closePath

首先要明确的一点是,closePathbeginPath 并不是相对的。

以绘制三角形为例来了解下 closePath

html
<canvas id="canvas-4-5" style="border: 1px solid #ccc;"></canvas>
<script>
  var canvas = document.querySelector('#canvas-4-5')
  var ctx = canvas.getContext('2d')
  ctx.beginPath()
  ctx.moveTo(10, 10)
  ctx.lineTo(100, 10)
  // 在同一个path中,可以使用lineTo多次连续绘制线条
  ctx.lineTo(100, 60)
  ctx.stroke()
  
  ctx.beginPath()
  ctx.moveTo(120, 10)
  ctx.lineTo(210, 10)
  ctx.lineTo(210, 60)
  ctx.closePath()
  // 在closePath后,需要再调用stroke方法
  ctx.stroke()
</script>

利用 beginPath 来区分路径 否则路径之间会互相影响。

closePath 会将最近一次的 moveTo 与最后一次的 lineTo 坐标连接起来。

5.绘制矩形rect

矩形只是线条形式的一种变形。正如 strokeRect 只是 stroke 方法的一种变形

绘制矩形的方法有两种:strokeRect(x, y, width, height)fillRect(x, y, width, height)。不再赘述。

值得记住的是 clearRect(x, y, width, height),它是清除画布的神技。

6.绘制圆弧arc

arc 绘制的是圆弧路径。在绘制完后需要调用stroke,同样适用于beginPath方法。

html
<canvas id="canvas-6" style="border: 1px solid #ccc;"></canvas>
<script>
  var canvas = document.querySelector('#canvas-6')
  var ctx = canvas.getContext('2d')
  // ctx.arc(x, y, r, startAngle, endAngle, anticlockwise)  anticlockwis默认是false 顺时针
  ctx.arc(150, 75, 30, 0, Math.PI * 2)
  ctx.stroke()
</script>

7.绘制文本text

  • strokeText 描边文本
  • fillText 填充文本
  • font 设置文本字体。默认值为 10px sans-serif
  • textAlign 文本水平对齐方式。五个值:left right center start end
  • textBaseline 文本垂直对齐方式。六个值:alphabetic(默认) top middle bottom hanging ideographic
  • measureText 可以用来获取文本相关信息。譬如宽度等。
html
<canvas id="canvas-7" style="border: 1px solid #ccc;"></canvas>
<script>
  var canvas = document.querySelector('#canvas-7')
  var ctx = canvas.getContext('2d')
  ctx.beginPath()
  ctx.moveTo(0, 75)
  ctx.lineTo(300, 75)
  ctx.stroke()
  ctx.beginPath()
  ctx.moveTo(150, 0)
  ctx.lineTo(150, 150)
  ctx.stroke()
  // 在(150, 75)处绘制文字
  ctx.font = 'bold 30px SimSun, Songti SC'
  ctx.save()
  ctx.strokeStyle = 'red'
  ctx.textAlign = 'right'
  ctx.textBaseline = 'bottom'
  ctx.strokeText('hello world', 150, 75)
  // save以栈的形式将绘图状态予以保存 restore将save的状态依次取出 
  ctx.restore()
  ctx.fillStyle = 'blue'
  ctx.textAlign = 'left'
  ctx.textBaseline = 'top'
  ctx.fillText('你好', 150, 75)
</script>

8.绘制图片image

  • 绘制图片

    html
      <canvas id="canvas-8-1" style="border: 1px solid #ccc;"></canvas>
      <script>
        var canvas = document.querySelector('#canvas-8-1')
        var ctx = canvas.getContext('2d')
        // 在这里 获取图片元素有两种方式
        // ① 在页面上增加一个隐藏的img标签,引入src。然后通过DOM获取到该img元素。但是这种方式显然不是很好,因为无缘无故增加了新的DOM节点。
        // ② 利用Image构造函数 创造一个图片实例出来。推荐这种方式。
        var image = new Image()
        image.src = './images/sasuke.jpeg'
        // 图片加载是异步的 所以必须利用onload监听 加载完成后再drawImage
        image.onload = function () {
          ctx.drawImage(image, 0, 0, 120, 120)
        }
        //!!! 该例因为HTML没有commonJS或者Es module, 而且vuepress打包不了以上面的形式的引入的图片,除非经过file-loader或者url-loader。所以图片加载不了。这里就先不展示效果了。
      </script>

    绘制图片的时候,也可以只绘制图片的一部分。因为用的情况不多,暂不赘述。

  • canvas 转化为图片

    这部分和绘制图片的主题有点关系。所以把它也整理在此处。

    首先要说明的是,将 canvas 转化为图片的能力是属于 HTMLCanvasElement 的。而不是总在使用的2d上下文 ctx

    具体方法有两种:toDataURLtoBlob。分别会将 canvas 转为 dataURLblob 两种形式的资源。



上述效果的核心代码如下:

js
  // 获取到目标元素
  var target = document.querySelector('#img-dataURL-Blob')
  // btn-toDataURL
  var dataURLBtn = document.querySelector('#btn-toDataURL')
  dataURLBtn.onclick = function () {
    var dataURL = dataURLCanvas.toDataURL()
    target.src = dataURL
  }
  // btn-toBlob
  var blobBtn = document.querySelector('#btn-toBlob')
  blobBtn.onclick = function () {
    blobCanvas.toBlob(function (blob) {
      // 将blob转化为blobURL
      var blobURL = URL.createObjectURL(blob)
      target.src = blobURL
    }, 'image/png')
  }

canvas 跨域问题:

如果使用 drawImage 方法在 canvas 上绘制了跨域的图片,这时直接将 canvas 使用 toDataURL 或者 toBlob 转化时,chrome 浏览器控制台会认为该 canvas 是被污染的画布,报如下错误:

解决办法需要满足两个条件:

①跨域图片设置了 Access-Control-Allow-Origin,允许当前域。

②定义 img 标签或者 Image 实例的 crossOrigin 属性为 true

例子如下:

html
<canvas id="canvas-8-cross-origin" style="border: 1px solid #ccc;"></canvas>
<button id="btn">click</button>
<script>
  var canvas = document.querySelector('#canvas-8-cross-origin')
  var ctx = canvas.getContext('2d')
  var image = new Image()
  // 引入了豆瓣的一张图片,该图片已经设置`Access-Control-Allow-Origin`为*。但随时可能失效。
  image.src = 'https://img9.doubanio.com/view/dale-online/dale_ad/public/65236355cf022f6.jpg'
  // 设置crossOrigin为true
  image.crossOrigin = true
  image.onload = function () {
    ctx.drawImage(image, 0, 0, 280, 140)
  }
  var btn = document.querySelector('#btn')
  // 点击按钮,控制台可以打印出dataURL和blob
  btn.onclick = function () {
    var dataURL = canvas.toDataURL()
    console.log(dataURL)
    canvas.toBlob(function (blob) {
      console.log(blob)
    })
  }
</script>

9.绘制视频video

有两个额外的 api,它们是 getImageDataputImageData

它们可以操作画布上某一区域上的像素点,不单单只是有像素变色这种操作,它还能实现对画布的裁剪以及转换等高级操作。

html
<video id="video-9" src="./video/402620458.mp4" controls style="width: 300px;height: 150px;"></video>
<canvas id="canvas-9-1"></canvas>
<script>
  var video = document.querySelector('#video-9')
  var myCanvas = document.querySelector('#canvas-9-1')
  var myCtx = myCanvas.getContext('2d')
  video.onplay = function () {
    window.requestAnimationFrame(draw)
  }
  function draw () {
    myCtx.drawImage(video, 15, 0, 270, 150)
    // getImageData与putImageData 结合使用可以操作视频的每一帧的像素
    var frame = myCtx.getImageData(15, 0, 270, 150)
    // vuepress总是报分号错误 下面代码暂时不加了
    // var data = frame.data
    // for (let i = 0; i < data.length; i += 4) {
  	// 	var avg = (data[i] + data[i + 1] + data[i + 2]) / 3
		// 	data[i] = avg
		// 	data[i + 1] = avg
		// 	data[i + 2] = avg
		// }
    myCtx.putImageData(frame, 15, 0)
    window.requestAnimationFrame(draw)
  }
</script>

在这里推荐一篇有趣的博文:用 canvas 的 getImageData 做点有趣的事

另外,我测试发现,getImageDataputImageData 方法都不会受到 canvastransform 方法的影响

它俩针对的始终是 canvas 画布本身的实在坐标。具体细节见下例:

html
<canvas id="canvas-9-3" width="600" height="300" style="border: 1px solid #ccc;"></canvas>
<canvas id="canvas-9-4" width="600" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
  var canvas1 = document.querySelector('#canvas-9-3')
  var canvas2 = document.querySelector('#canvas-9-4')
  var ctx1 = canvas1.getContext('2d')
  var ctx2 = canvas2.getContext('2d')
  // 绘制一个矩形 然后获取其像素 再将其绘制到另一个区域
  ctx1.fillStyle = 'red'
  ctx1.fillRect(20, 20, 80, 40)
  var imageData1 = ctx1.getImageData(20, 20, 80, 40)
  ctx1.putImageData(imageData1, 500, 240)

  ctx2.fillStyle = 'green'
  ctx2.translate(20, 20)
  ctx2.fillRect(0, 0, 120, 80)
  ctx2.fillStyle = 'blue'
  ctx2.fillRect(20, 20, 80, 40)
  var imageData2 = ctx2.getImageData(20, 20, 80, 40)
  ctx2.putImageData(imageData2, 500, 240)
  // 测试发现 getImageData与putImageData方法都不会受到canvas的transform的影响。它俩针对的就是canvas画布本身实实在在的坐标。
</script>

10.颜色渐变

颜色渐变分为两种:

  • 线性渐变 createLinearGradient
    html
      <canvas id="canvas-linear" style="border: 1px solid #ccc;"></canvas>
      <script>
        var canvas = document.querySelector('#canvas-linear')
        var ctx = canvas.getContext('2d')
        // 创建线性渐变
        var linearGradient = ctx.createLinearGradient(20, 20, 260, 110)
        linearGradient.addColorStop(0, '#d53369')
        linearGradient.addColorStop(1, '#cbad6d')
        ctx.fillStyle = linearGradient
        ctx.fillRect(20, 20, 260, 110)
      </script>

  • 径向渐变 createRadialGradient

    html
      <canvas id="canvas-radial" style="border: 1px solid #ccc;"></canvas>
      <script>
        var canvas = document.querySelector('#canvas-radial')
        var ctx = canvas.getContext('2d')
        // 创建线性渐变
        var radialGradient = ctx.createRadialGradient(150, 75, 0, 150, 75, 75)
        radialGradient.addColorStop(0, '#d53369')
        radialGradient.addColorStop(1, '#cbad6d')
        ctx.fillStyle = radialGradient
        ctx.fillRect(0, 0, 300, 150)
      </script>

11.saverestore

save() 方法会以栈的形式来保存画布的绘制状态。具体状态包括:

  • 矩阵变换。translate() rotate() scale() transform() 等。
  • 剪裁区域。clip()
  • 虚线设置。setLineDash()
  • 以及一些属性。strokeStyle fillStyle globalAlpha lineWidth lineCap lineJoin miterLimit lineDashOffeset shadowOffsetX shadowOffsetY shadowBlur shadowColor globalCompositionOperation font textAlign textBaseLine

restore() 方法会从栈中依次取出保存的状态,如果没有栈中没有存储的状态,执行该方法后,不会产生任何变化。

这二者的具体使用时机是在我们改变绘制状态时。可以先调用 save() 保存一下之前的状态,然后书写我们的绘制状态,在结束后,再调用 restore() 方法来将画布重置到之前保存的状态。

下例的核心代码,都是绘制一个平移加旋转的矩形,对比看下有无 saverestore 的区别:

html
<canvas id="canvas-11-1" style="border: 1px solid #ccc;"></canvas>
<canvas id="canvas-11-2" style="border: 1px solid #ccc;"></canvas>
<script>
  var canvas1 = document.querySelector('#canvas-11-1')
  var canvas2 = document.querySelector('#canvas-11-2')
  var ctx1 = canvas1.getContext('2d')
  var ctx2 = canvas2.getContext('2d')
  ctx1.rotate(-Math.PI / 2)
  ctx1.translate(-50, 0)
  ctx1.fillRect(0, 0, 40, 80)

  ctx2.save()
  // 毫无疑问 第二个图形的矩形不会发生旋转
  ctx2.rotate(-Math.PI / 2)
  ctx2.restore()
  ctx2.translate(0, 50)
  ctx2.fillRect(0, 0, 40, 80)
</script>

12.转换transform

canvas 中的转换与 css 中的转换有些区别。

css 的转换影响元素,而 canvas的转换通过影响到坐标系,间接影响元素

总之一点,canvas上使用转换后,尤其是rotate,要改变视角来看待整个坐标系。

canvas的坐标系原点默认是在左上角。从左至右是x轴,从上至下是y轴。转换只会影响坐标系,不会改变画布本身的大小和位置。

TIP

另外,例如像 fillRectdrawImage 这些 api 中的一些参数我通常认作 widthheight,但这是不准确的一种认知。

应该将其当做在对应的x轴和y轴上面的长度。

12-1.translate

将y轴向下平移 30px

html
<canvas id="canvas-12-1-1" style="border-top: 1px solid #ccc; border-left: 1px solid #ccc;"></canvas>
<canvas id="canvas-12-1-2" style="border-top: 1px solid #ccc; border-left: 1px solid #ccc;"></canvas>
<script>
  var canvas1 = document.querySelector('#canvas-12-1-1')
  var canvas2 = document.querySelector('#canvas-12-1-2')
  var ctx1 = canvas1.getContext('2d')
  var ctx2 = canvas2.getContext('2d')
  ctx1.fillRect(0, 0, 200, 100)
  ctx2.translate(0, 50)
  ctx2.fillRect(0, 0, 200, 100)
</script>

12-2.rotate

rotatecss 中的 rotate 不同。

cssrotate 针对的是角度 deg,而 canvasrotate 针对的数学角度的 Math.PI。下例将坐标系逆时针旋转π / 2

html
<canvas id="canvas-12-2-1" style="border-top: 1px solid #ccc; border-left: 1px solid #ccc;"></canvas>
<canvas id="canvas-12-2-2" style="border-bottom: 1px solid #ccc; border-left: 1px solid #ccc;"></canvas>
<img id="img-12-2" src="./images/avatar.jpeg" alt="avatar" style="display: none;">
<script>
  // 由于引入图片 图片加载需要时间 所以等DOM加载完 再去drawImage 否则会失效
  window.addEventListener('load', function () {
    var canvas1 = document.querySelector('#canvas-12-2-1')
    var canvas2 = document.querySelector('#canvas-12-2-2')
    var ctx1 = canvas1.getContext('2d')
    var ctx2 = canvas2.getContext('2d')
    var img = document.querySelector('#img-12-2')
    ctx1.drawImage(img, 0, 0, 200, 100)
    // 将坐标原点改为左下角
    ctx2.translate(0, 150)
    ctx2.rotate(-Math.PI / 2)
    // 正如之前说的,这里的100指的是在对应x轴上的长度 而不是横轴长度。
    ctx2.drawImage(img, 0, 0, 100, 200)
  })
</script>

12-3.scale

scale 会成比例的改变元素在 x轴y轴 的值,无论是坐标还是长度。如下面的例子,不仅是圆心的坐标改变,圆半径也成比例的改变了。

html
<canvas id="canvas-12-3-1" style="border-top: 1px solid #ccc; border-left: 1px solid #ccc;"></canvas>
<canvas id="canvas-12-3-2" style="border-top: 1px solid #ccc; border-left: 1px solid #ccc;"></canvas>
<script>
  var canvas1 = document.querySelector('#canvas-12-3-1')
  var canvas2 = document.querySelector('#canvas-12-3-2')
  var ctx1 = canvas1.getContext('2d')
  var ctx2 = canvas2.getContext('2d')
  // arc只是路径 必须stroke
  ctx1.arc(50, 35, 30, 0, Math.PI * 2)
  ctx1.stroke()
  ctx2.scale(2, 2)
  ctx2.arc(50, 35, 30, 0, Math.PI * 2)
  ctx2.stroke()
</script>

12-4.transform

这部分主要是 transformsetTransform 的区别,关于 matrix 矩阵变换的内容暂时没有时间去验证。推荐张鑫旭的矩阵博客

二者的区别总结成一句话:transform 的转换是累计的,而 setTransform 会清除以前的转换效果后再进行转换

html
<canvas id="canvas-12-4-1" style="border: 1px solid #ccc;"></canvas>
<canvas id="canvas-12-4-2" style="border: 1px solid #ccc;"></canvas>
<script>
  var transformCanvas = document.querySelector('#canvas-12-4-1')
  var setTransformCanvas = document.querySelector('#canvas-12-4-2')
  var ctx = transformCanvas.getContext('2d')
  var setCtx = setTransformCanvas.getContext('2d')
  // transform
  ctx.transform(2, 0, 0, 2, 0, 0) // x轴 y轴放大两倍
  ctx.transform(1, 0, 0, 1, 20, 20) // x轴 y轴各平移20px 这里的1分别对应x轴和y轴的缩放
  ctx.fillRect(0, 0, 60, 30)
  // setTransform
  setCtx.transform(2, 0, 0, 2, 0, 0)
  setCtx.setTransform(1, 0, 0, 1, 20, 20)
  setCtx.fillRect(0, 0, 60, 30)
</script>

13.贝塞尔曲线

贝塞尔曲线可分为二阶贝塞尔曲线、三阶贝塞尔曲线乃至n阶贝塞尔曲线。其对应的控制点数量为阶数减一。 也就是说二阶贝塞尔曲线有一个控制点,三阶贝塞曲线有两个控制点。要注意的一点是,canvas 中关于贝塞尔曲线的方法 quadraticCurveTo() 以及 bezierCurveTo()它们绘制的都是路径 path,如果要看见实线,依然需要调用 stroke() 方法。

13-1.二阶贝塞尔曲线

canvas 中的二阶贝塞尔曲线的对应方法为 quadraticCurveTo(cx, cy, ex, ey)。其中 cx 代表控制点x坐标,cy 代表控制点y坐标,ex 代表结束点x坐标,ey 代表结束点y坐标。

下例:起始点坐标为 (0, 0),控制点坐标随便写了个 (220, 40),结束点坐标是 (300, 150)

html
<canvas id="canvas-13-1" style="border: 1px solid #ccc;"></canvas>
<script>
  var canvas = document.querySelector('#canvas-13-1')
  var ctx = canvas.getContext('2d')
  ctx.beginPath()
  ctx.strokeStyle = 'blue'
  ctx.moveTo(0, 0)
  ctx.quadraticCurveTo(220, 40, 300, 150)
  ctx.stroke()
</script>

但是问题来了,我们应该怎么得到控制点坐标,从而绘制曲线呢?所以我们需要封装一个方法,从而可以不再介意控制点坐标,以一种更加方便的方式来生成曲线。

js
/**
 * @description: 封装二阶贝塞尔曲线
 * @param {object} ctx
 * @param {object} 起点坐标
 * @param {object} 结束点坐标
 * @param {number} 偏移角度
 * @return: 
 */
function quadraticCurve (ctx, start, end, degree) {
  const { x: startX, y: startY } = start
  const { x: endX, y: endY } = end
  if (!ctx || !startX || !startY || !endX || !endY || !degree) {
    throw new Error('丢失参数')
  }
  // 计算中间控制点
  var center = {
    x: ( startX + endX ) / 2 - ( startY - endY ) * degree,
    y: ( startY + endY ) / 2 - ( endX - startX) * degree
  }
  ctx.beginPath()
  ctx.moveTo(startX, startY)
  ctx.quadraticCurveTo(center.x, center.y, endX, endY)
  ctx.stroke()
}

封装二阶贝塞尔曲线动画

html
<canvas id="canvas-quadratic-animate" width="600" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
  var animateCanvas = document.querySelector('#canvas-quadratic-animate')
  var animateCtx = animateCanvas.getContext('2d')
  var percent = 0
  function animate() {
    animateCtx.clearRect( 0, 0, 800, 800 )
    animateCtx.beginPath()
    animateCtx.lineWidth = '4'
    drawCurve( 
      animateCtx,
      [ 100, 100 ],
      [ 200, 300 ],
      0.5,
      percent
    );
    animateCtx.stroke()
    // 可以暂停循环往复
    // if (percent >= 99) {
    //   return
    // }
    percent = ( percent + 1 ) % 100
    requestAnimationFrame( animate )
  }
  animate()
  /**
   * @description: 二阶贝塞尔曲线动画
   * @param {type} 
   * @return: 
   */
  function drawCurve (ctx, start, end, curveness, percent) {
    var cp = [
      ( start[ 0 ] + end[ 0 ] ) / 2 - ( start[ 1 ] - end[ 1 ] ) * curveness,
      ( start[ 1 ] + end[ 1 ] ) / 2 - ( end[ 0 ] - start[ 0 ] ) * curveness
    ];
    var t = percent / 100;
    var p0 = start;
    var p1 = cp;
    var p2 = end;
    var v01 = [ p1[ 0 ] - p0[ 0 ], p1[ 1 ] - p0[ 1 ] ];     // 向量<p0, p1>
    var v12 = [ p2[ 0 ] - p1[ 0 ], p2[ 1 ] - p1[ 1 ] ];     // 向量<p1, p2>
    var q0 = [ p0[ 0 ] + v01[ 0 ] * t, p0[ 1 ] + v01[ 1 ] * t ];
    var q1 = [ p1[ 0 ] + v12[ 0 ] * t, p1[ 1 ] + v12[ 1 ] * t ];
    var v = [ q1[ 0 ] - q0[ 0 ], q1[ 1 ] - q0[ 1 ] ];       // 向量<q0, q1>
    var b = [ q0[ 0 ] + v[ 0 ] * t, q0[ 1 ] + v[ 1 ] * t ];
    ctx.moveTo( p0[ 0 ], p0[ 1 ] );
    ctx.quadraticCurveTo( 
        q0[ 0 ], q0[ 1 ],
        b[ 0 ], b[ 1 ]
    )
  }
</script>

TIP

二阶贝塞尔曲线方程 B(t)=(1-t)²P₀ + 2t(1-t)P₁ + t²P₂

这一部分写的比较懒。二阶动画以及中间控制点的计算那里,没有去验证。目前先做记录,日后再深入研究。主要参考了用canvas绘制一个曲线动画——深入理解贝塞尔曲线

13-2.三阶贝塞尔曲线

canvas 中的二阶贝塞尔曲线的对应方法为 bezierCurveTo(cx1, cy1, cx2, cy2, ex, ey)。其中 cx1 代表第一个控制点x坐标,cy1 代表第一个控制点y坐标,cx2 代表第二个控制点x坐标,cy2 代表第二个控制点y坐标,ex 代表结束点x坐标,ey 代表结束点y坐标。

TIP

三阶贝塞尔曲线方程 B(t)=(1-t)³P₀ + 3t(1-t)²P₁ + 3t²(1-t)P₂ + t³P₃

附录1.签字板

利用 canvas 构建签字板。主要的功能点有:

  1. PC端签字
  2. 移动端
  3. 签字完成后,可以保存成图片。(涉及到如何旋转签字图片)

这个签字板功能,没有按照面向对象的方式来写。代码是面向过程的。针对上面的功能点,总结下思路:

PC端签字依赖的事件 api 主要是 mousedown mousemove mouseup。另外要额外注意的一点是这三者在移动端不会被触发。

移动端依赖的事件 api 主要是 touchstart touchmove touchend

在本例中,没有改变原始 canvas 元素的 x 轴和 y 轴,而且签字是横向签的。

所以利用 toDataURL 生成的图片默认会是竖向的。

解决办法是利用drawImage方法将该图片绘制到一个转换过的虚拟canvas上,再利用toDataURL生成

链接

附录2.时钟

利用 canvas 绘制时钟。其实这里知识点还是挺多的。另外对数学的三角函数也要掌握。个人认为重点有两个:

  1. 根据当前时间计算出时针、分针、秒针的角度,再利用定时器不断的绘制。
  2. 利用三角函数计算出时针、分针、秒针的起点和终点,再利用stroke绘制。在本例代码中,我将其封装为了getCoord函数。

数学是一门有趣且很重要的语言。计算机的基础是由数学理论来支撑的。

链接

附录3.弹幕

利用 canvas 绘制弹幕。我写的例子比较简单,也存在许多可优化的地方。本例只是为了个人探究一下。思路的话就是:

利用位置绘制文字 改变位置 清空画布。一直循环到条件不满足。

链接

5-2.WebGL