HTML5
引入了 Canvas
和 WebGL
,使得在浏览器中绘制 2D
和 3D
图形成为可能,提供了更丰富的视觉效果和交互性。
5-1.canvas
1.介绍
canvas
可用于绘制 2D
图像。它在文档流中是行内块元素。可以通过 css
设置 width
和 height
。
HTMLCanvasElement
本身也有 width
和 height
属性。
我们通常会直接设置,不设置的话,<canvas>
默认width
为300px
,height
为150px
。
譬如下面这个没有设置宽和高的 <canvas>
:
这是一个背景色为skyblue的canvas
2.绘图面积与元素面积
我们知道设置 canvas
大小的方式有两种:
- 直接设置
<canvas>
标签的width
和height
,即element-properties
。 - 通过
css
来设置<canvas>
的width
和height
,即css-properties
。
但是这两种方式影响 <canvas>
的方式是不同的。它们分别影响的是绘图面积和元素面积。(至少我是这么理解的😂)。
下面通过在 600 ✖️ 300
的画布上绘制一个 300 ✖️ 150
的矩形,分成3种情况来分析:
2-1.只设置element-properties
<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
<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
来设置 width
和 height
后,会发现 300 ✖️ 150
的矩形被绘制成了 600 ✖️ 300
。
这是因为虽然没有设置 element-properties
,但是这时 canvas
默认的绘图面积就是 300 * 150
。
它会先以这种设置来绘制,然后将 canvas
填充到元素面积为 600 ✖️ 300
的元素中。
2-3.同时设置element-properties
和css-properties
<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
方法。我们可以通过它来获取目标 canvas
的 2d
上下文,进而绘制图形。
<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 | 描边 | 填充 |
---|---|---|
stroke | fill | |
矩形 | strokeRect | fillRect |
文本 | strokeText | fillText |
设置颜色 | strokeStyle | fillStyle |
4.绘制线条line
4-1.绘制线条并描边
<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
即可。本例代码是这样:
<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
我想要绘制两条线,一条颜色是红色,另一条是蓝色。代码如下:
<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
。
改进后的代码:
<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
首先要明确的一点是,closePath
与 beginPath
并不是相对的。
以绘制三角形为例来了解下 closePath
:
<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
方法。
<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
可以用来获取文本相关信息。譬如宽度等。
<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
。具体方法有两种:
toDataURL
与toBlob
。分别会将canvas
转为dataURL
与blob
两种形式的资源。
上述效果的核心代码如下:
// 获取到目标元素
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
。
例子如下:
<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
,它们是 getImageData
和putImageData
。
它们可以操作画布上某一区域上的像素点,不单单只是有像素变色这种操作,它还能实现对画布的裁剪以及转换等高级操作。
<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 做点有趣的事
另外,我测试发现,getImageData
与 putImageData
方法都不会受到 canvas
的 transform
方法的影响。
它俩针对的始终是 canvas
画布本身的实在坐标。具体细节见下例:
<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.save
和restore
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()
方法来将画布重置到之前保存的状态。
下例的核心代码,都是绘制一个平移加旋转的矩形,对比看下有无 save
及 restore
的区别:
<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
另外,例如像 fillRect
、drawImage
这些 api
中的一些参数我通常认作 width
和 height
,但这是不准确的一种认知。
应该将其当做在对应的x轴和y轴上面的长度。
12-1.translate
将y轴向下平移 30px
:
<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
rotate
与 css
中的 rotate
不同。
css
的 rotate
针对的是角度 deg
,而 canvas
的 rotate
针对的数学角度的 Math.PI
。下例将坐标系逆时针旋转π / 2
:
<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轴
的值,无论是坐标还是长度。如下面的例子,不仅是圆心的坐标改变,圆半径也成比例的改变了。
<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
这部分主要是 transform
与 setTransform
的区别,关于 matrix
矩阵变换的内容暂时没有时间去验证。推荐张鑫旭的矩阵博客。
二者的区别总结成一句话:transform
的转换是累计的,而 setTransform
会清除以前的转换效果后再进行转换
<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)
。
<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>
但是问题来了,我们应该怎么得到控制点坐标,从而绘制曲线呢?所以我们需要封装一个方法,从而可以不再介意控制点坐标,以一种更加方便的方式来生成曲线。
/**
* @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()
}
封装二阶贝塞尔曲线动画
<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
构建签字板。主要的功能点有:
PC
端签字- 移动端
- 签字完成后,可以保存成图片。(涉及到如何旋转签字图片)
这个签字板功能,没有按照面向对象的方式来写。代码是面向过程的。针对上面的功能点,总结下思路:
PC端签字依赖的事件 api
主要是 mousedown
mousemove
mouseup
。另外要额外注意的一点是这三者在移动端不会被触发。
移动端依赖的事件 api
主要是 touchstart
touchmove
touchend
。
在本例中,没有改变原始 canvas
元素的 x
轴和 y
轴,而且签字是横向签的。
所以利用 toDataURL
生成的图片默认会是竖向的。
解决办法是利用drawImage
方法将该图片绘制到一个转换过的虚拟canvas
上,再利用toDataURL
生成。
附录2.时钟
利用 canvas
绘制时钟。其实这里知识点还是挺多的。另外对数学的三角函数也要掌握。个人认为重点有两个:
- 根据当前时间计算出时针、分针、秒针的角度,再利用定时器不断的绘制。
- 利用三角函数计算出时针、分针、秒针的起点和终点,再利用
stroke
绘制。在本例代码中,我将其封装为了getCoord
函数。
数学是一门有趣且很重要的语言。计算机的基础是由数学理论来支撑的。
附录3.弹幕
利用 canvas
绘制弹幕。我写的例子比较简单,也存在许多可优化的地方。本例只是为了个人探究一下。思路的话就是:
利用位置绘制文字 改变位置 清空画布。一直循环到条件不满足。