一份八千字超级详细的Canvas从0到1入门指南
Canvas简介
提供了一个通过JavaScript 和 HTML的<canvas>元素来绘制图形的方式。它可以用于动画、游戏画面、数据可视化、图片编辑以及实时视频处理等方面。在实际应用中,我们很少有直接操作canvas的需求,大部分情况都是直接操作canvas的库,比如说echarts,一些图片裁剪插件等。题主最近在看canvas的相关书籍准备从头学习一下canvas,这篇文章主要是一些canvas的总结和记录一些常用api方法。
注:我这边所有的代码实例都是基于ts+webpack环境实现的,有不懂的朋友可以去ts和[webpack】(webpack.docschina.org/)官网查看
- 兼容性:IE9+,可以通过这个插件来兼容IE7、IE8
1. Canvas绘制几何图形
1.1 创建canvas元素步骤
- 创建canvas元素 默认宽为300,高为150
- 获取上下文对象context
- 开始绘制 坐标轴原点在屏幕左上角
const canvas = document.querySelector<HTMLCanvasElement>("#canvas") || new HTMLCanvasElement();
const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
注意这里设置canvas宽高时候,不要通过style去设置,因为这样设置之后我们获取canvas.width还是默认的宽高,我们要在canvas上面直接设置宽高
<canvas id="canvas" width="500" height="300" />
1.2 绘制直线图形
1.2.1 直线
- ctx.moveTo(x, y): 移动画笔到(x,y)位置
- ctx.lineTo(x1, y1): 让画笔画一条线从(x,y)到(x1,y1)
- ctx.stroke():开始画
const canvas = document.querySelector<HTMLCanvasElement>("#canvas") || new HTMLCanvasElement();
const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
ctx.moveTo(0, 0)
ctx.lineTo(300, 300)
ctx.stroke()
画多条直线
ctx.moveTo(200, 100)
ctx.lineTo(400, 100)
ctx.lineTo(400, 50)
ctx.moveTo(200, 300)
ctx.lineTo(400, 300)
ctx.stroke()
每次绘制如果没有moveTo,则从上一次的lineTo位置继续画,如果有则将画笔移动至moveTo位置再画像lineTo
1.2.2 矩形
- ctx.strokeRect(x, y, width, height):从(x,y)的位置开始画一个width长,height高的空心矩形
- 可以通过ctx.strokeStyle = "";修改边的颜色
ctx.strokeStyle = "red";
ctx.strokeRect(100, 100, 200, 100);
strokeStyle和strokeRect的顺序不能反
- ctx.fillRect(x, y, width, height):从(x,y)的位置开始画一个width长,height高的实心矩形
- 可以通过ctx.fillStyle = "";修改填充的颜色
ctx.fillStyle = "purple"
ctx.fillRect(100, 100, 200, 100);
fillStyle和fillRect的顺序不能反
- rect(x, y, width, height):从(x,y)的位置开始画一个width长,height高的矩形,不同于上面两个的是,调用这个方法绘制的矩形不会立即显示出来,需要调用fill()或者stroke()方法才会显示出来,可以设置描边颜色和填充颜色
ctx.strokeStyle = "blue";
ctx.fillStyle = "pink"
ctx.rect(100, 100, 200, 100)
ctx.fill();
ctx.stroke();
- ctx.clearRect(x, y, width, height):清除从(x,y)的位置width长,height高的矩形
- 这里清除只是清除的填充(fill)颜色,描边(stroke)颜色不会清除
ctx.strokeStyle = "blue";
ctx.fillStyle = "pink"
ctx.rect(100, 100, 200, 100)
ctx.fill();
ctx.stroke();
ctx.clearRect(120, 120, 160, 60)
1.2.3 多边形
-
多边形都是通过lineTo和moveTo来实现的
-
三角形
ctx.moveTo(200, 100)
ctx.lineTo(200, 300)
ctx.lineTo(400, 200)
ctx.lineTo(200, 100)
ctx.stroke()
大家可以发挥想象,试着画一画六边形,五边形等
1.3 曲线图形
- ctx.arc(x, y, radius, startAngle ,endAngle, anticlockwise):圆心位置为(x, y),半径为radius,开始角度startAngle,结束角度endAngle,画一个圆弧
- anticlockwise:true逆时针画,false顺时针画
- 角度:度数 x Math.PI/180
1.3.1 圆
- 空心圆
ctx.arc(250, 250, 100, 0 ,360 * Math.PI/180);
ctx.stroke();
- 实心圆
ctx.arc(250, 250, 100, 0 ,360 * Math.PI/180);
ctx.fill()
- 同心圆 大家可以试一试同心圆怎么画,相信大家一定是这样画的:
ctx.arc(250, 250, 100, 0 ,360 * Math.PI/180);
ctx.stroke();
ctx.arc(250, 250, 50, 0 ,360 * Math.PI/180);
ctx.stroke();
画出来大家会发现两个圆连在一起了:
- ctx.beginPath():开始一个新路径
- ctx.closePath():结束一个路径
- 这两个方法一般是成对出现 修改代码:
// 开始一个路径
ctx.beginPath()
ctx.arc(250, 250, 100, 0 ,360 * Math.PI/180);
ctx.stroke();
// 抬笔动作
ctx.closePath()
// 再开始一个新的路径
ctx.beginPath()
ctx.arc(250, 250, 50, 0 ,360 * Math.PI/180);
ctx.stroke();
ctx.closePath()
注意:stroke()和fill()这两个方法只生效与当前路径,关闭之后就无效了!
1.3.1 圆弧
- 绘制弧线时候,不需要关闭ctx.closePath()路径
ctx.beginPath()
ctx.arc(250, 250, 100, 0 ,60 * Math.PI/180);
ctx.stroke();
关闭路径
还有一种画圆弧方式:
- ctx.arcTo(cx,cy,x2,y2,radius):(cx,cy)为控制点,(x2,y2)为圆弧的结束点,没有开始点的原因是因为用这种方式画圆弧,通长都是以lineTo()或者moveTo()为开始
例子:
ctx.moveTo(50, 100);
ctx.lineTo(200, 100); // 画水平线,(200,100)相当于圆弧开始点
ctx.arcTo(250,100,250,200,50) // 控制点(250, 100), 圆弧结束点(250, 200), 半径50
ctx.lineTo(250, 250);
ctx.stroke();
这种方式比较难理解,大家可以自己画图慢慢理解一下。
1.4 贝塞尔曲线
贝塞尔曲线(Bézier curve),又称贝兹曲线或贝济埃曲线,是应用于二维图形应用程序的数学曲线。一般的矢量图形软件通过它来精确画出曲线,贝兹曲线由线段与节点组成,节点是可拖动的支点,线段像可伸缩的皮筋,我们在绘图工具上看到的钢笔工具就是来做这种矢量的。以上是百度百科的解释,其实贝塞尔曲线就是由一个点或者多个点去控制一条曲线。
1.4.1 二次贝塞尔曲线
- ctx.quadraticCurveTo(cx, cy, x2, y2):与arcTo类似,贝塞尔曲线也是由三个坐标构成,(x1,y1)为起始点由moveTo或者lineTo提供,(cx,cy)为控制点,(x2,y2)为结束点。
注意:二次贝塞尔曲线只有一个控制点
ctx.moveTo(100, 200)
ctx.quadraticCurveTo(150, 200, 200, 300)
ctx.stroke()
画个聊天气泡
ctx.moveTo(75, 25);
ctx.quadraticCurveTo(25, 25, 25, 62);
ctx.quadraticCurveTo(25, 100, 50, 100);
ctx.quadraticCurveTo(50, 120, 30, 125);
ctx.quadraticCurveTo(60, 120, 65, 100);
ctx.quadraticCurveTo(125, 100, 125, 62);
ctx.quadraticCurveTo(125, 25, 75, 25);
ctx.stroke();
1.4.2 三次贝塞尔曲线
- ctx.bezierCurveTo(cx1 , cy1, cx2, cy2, x, y):三次贝塞尔曲线是由两个控制点和结束坐标(x,y)构成,同样的开始坐标也是由lineTo和moveTo提供。
ctx.moveTo(200, 100);
ctx.bezierCurveTo(150, 50, 250, 200, 200, 200);
ctx.stroke();
1.4.3 四次贝塞尔曲线
图示:
2. Canvas线条样式
2.1 lineWidth 线条粗细
- ctx.lineWidth
ctx.beginPath()
ctx.lineWidth = 2
ctx.moveTo(100, 100)
ctx.lineTo(300, 100);
ctx.stroke();
ctx.closePath()
ctx.beginPath()
ctx.lineWidth = 10
ctx.moveTo(300, 100);
ctx.lineTo(300, 200);
ctx.stroke();
ctx.closePath()
2.2 lineCap 线条开始和结束处样式
- ctx.lineCap
- 是线条的开始和结束处,不是链接处
- ctx.lineCap = 'butt' 默认值无线帽
- ctx.lineCap = 'round' 圆形
- ctx.lineCap = 'square' 方形
注意:这里设置除了默认值以外的样式会使线段长一点,因为会在线段的末端增加一段线帽,多出来一部分的长度是线段宽度的一半
2.3 lineJoin 线条连接处样式
-
ctx.lineJoin
-
是线条的链接处,不是首尾末端
-
ctx.lineJoin = 'miter' 默认值,尖角
- ctx.lineJoin = 'bevel' 斜角
- ctx.lineJoin = 'round' 圆角
总结:
2.4 setLineDash 设置线条虚实样式
- ctx.setLineDash([实线宽度, 距离])
ctx.beginPath()
ctx.setLineDash([2, 2]);
ctx.moveTo(100, 100)
ctx.lineTo(300, 100);
ctx.stroke();
ctx.closePath()
3. 文本操作
3.1 文本操作方法
- 绘制填充文本 ctx.fillText(文本, x, y, maxWidth) 超过maxWidth会被压缩到maxWidth长度
ctx.fillText("哈哈", 100, 100)
- 绘制描边文本 ctx.strokeText(文本, x, y, maxWidth)
ctx.strokeText("哈哈", 100, 100)
- 获取文本长度 measureText(文本) 是获取文本的UI长度,单位px,并不是文本的字数长度
const l = ctx.measureText("哈哈").width;
console.log(l); // 20
3.2 文本属性
- ctx.font = "font-style font-weight font-size/line-height font-family"
ctx.font = "bold 100px Kai"
ctx.strokeText("哈哈", 100, 100)
- 文本对齐方式 cxt.textAlign
ctx.font = "bold 30px 宋体"
ctx.moveTo(100, 100)
ctx.lineTo(100, 500)
ctx.stroke()
ctx.textAlign = 'start'
ctx.strokeText("哈哈", 100, 100)
ctx.textAlign = 'end'
ctx.strokeText("哈哈", 100, 150)
ctx.textAlign = 'center'
ctx.strokeText("哈哈", 100, 200)
ctx.textAlign = 'left'
ctx.strokeText("哈哈", 100, 250)
ctx.textAlign = 'right'
ctx.strokeText("哈哈", 100, 300)
- 文本对齐方式 textBaseline
文本操作属性用的最多的还是font属性,对齐方式用的很少
4. 图形操作
4.1 图片基础操作
canvas通过 drawImage() 方法提供了图片导入,对图片进行一系列的裁剪,平铺等处理。
4.1.1 图片绘制
- ctx.drawImage(image, dx, dy):image是图片元素,(dx,dy)是绘制图片左上角的坐标。
const img = new Image();
img.src = '/src/img/aaa.png'
img.onload = () => {
ctx.drawImage(img, 100, 100);
}
注意:如果是通过new创建的图片对象,需要在图片加载完成onload之后在进行操作
- ctx.drawImage(image, dx, dy, dw, dh):dw和dh是绘制图片的宽高。
const img = new Image();
img.src = '/src/img/aaa.png'
img.onload = () => {
ctx.drawImage(img, 100, 100, 50, 50);
}
- drawImage(image , sx, sy, sw, sh, dx, dy, dw, dh):(sx, sy)是从源图片的那个坐标点开始裁剪,裁剪的宽度sw和高度sh。 注意:这里的sx, sy, sw, sh是针对于源图片,dx, dy, dw, dh是画完的图片即canvas上的图片
const img = new Image();
img.src = '/src/img/aaa.png'
img.onload = () => {
ctx.drawImage(img, 50, 50, 50, 50, 0, 0, 100, 100)
}
这种方法可以实现雪碧图效果,只加载一次图片,通过裁剪不同位置从而实现显示不同的图片元素。
4.1.2 平铺效果
- ctx.createPattern(element, type)
- type类型有:
- repeat:x,y轴全平铺
- repeat-x:x轴平铺
- repeat-y:y轴平铺
- no-repeat:不平铺
平铺一般都是配合rect和arc圆来使用,element既可以平铺图片,也可以平铺视频甚至canvas元素。
- 平铺图片
const img = new Image();
img.src = '/src/img/aaa.png'
img.onload = () => {
ctx.rect(0, 0, 300, 300)
const pattern = ctx.createPattern(img, 'repeat') as CanvasPattern;
ctx.fillStyle = pattern;
ctx.fill();
}
- 平铺canvas
// 创建一个新的canvas
const canvas2 = document.createElement('canvas')
canvas2.width = 50;
canvas2.height = 50;
const ctx2 = canvas2.getContext('2d') as CanvasRenderingContext2D;
ctx2.fillStyle = 'blue'
ctx2.arc(25, 25, 25, 0, 360 * Math.PI/180);
ctx2.fill()
ctx.rect(0, 0, 300, 300)
const pattern = ctx.createPattern(canvas2, 'repeat-x') as CanvasPattern;
ctx.fillStyle = pattern;
ctx.fill();
e.g.图片填充文字
// 图片填充文字
const pattern = ctx.createPattern(img, 'repeat') as CanvasPattern
ctx.font = 'bold 100px Kai'
ctx.fillStyle = pattern;
ctx.fillText('CANVAS', 80, 100);
4.1.3 裁剪功能
裁剪功能需要三步:
- 创建一个裁剪模板,即把一张图片裁剪成什么形状
- 调用裁剪方法:cxt.clip()
- 把图片裁剪成第一步的形状
// 1.创建裁减模板
ctx.arc(50, 50, 50, 0, 360 * Math.PI/180);
// 2.裁剪
ctx.clip()
// 3.绘制裁剪图片
ctx.drawImage(img, 0, 0);
4.2 变形操作
4.2.1 平移
- ctx.translate(x, y):将图形平移x,y距离 注意:平移之后,平移之前的图形不会清除
ctx.beginPath()
ctx.fillRect(0, 0, 100, 100)
setTimeout(() => {
ctx.translate(200, 200);
ctx.fillRect(0, 0, 100, 100)
}, 1000)
ctx.closePath()
4.2.2 缩放
- ctx.scale(x, y):将图形沿x,y轴缩放 注意:缩放之后,图形的xy坐标也会被缩放, 线条的宽度也会缩放
ctx.fillRect(200, 200, 100, 100)
ctx.scale(1.5, 1.5)
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)'
ctx.fillRect(200, 200, 100, 100)
ctx.closePath()
我们可以通过translate将x,y左移缩放的距离
ctx.fillRect(200, 200, 100, 100)
ctx.translate(-100, -100)
ctx.scale(1.5, 1.5)
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)'
ctx.fillRect(200, 200, 100, 100)
ctx.closePath()
4.2.3 旋转
- ctx.rotate(deg):将图形旋转deg度 注意:旋转的中心点是坐标原点
ctx.fillRect(200, 200, 50, 50)
ctx.rotate(10 * Math.PI/180)
ctx.fillRect(200, 200, 50, 50)
- 可以通过translate修改旋转中心点
ctx.fillRect(200, 200, 50, 50)
ctx.translate(225, -95)
ctx.rotate(45 * Math.PI/180)
ctx.fillRect(200, 200, 50, 50)
4.2.4 一步到位写法
- ctx.transform(a,b,c,d,e,f)
- a 水平缩放
- b 水平倾斜
- c 垂直倾斜
- d 垂直缩放
- e 水平移动
- f 垂直移动
ctx.translate(-100, -100) 就相当于 ctx.transform(1,0,0,1,-100,-100)
4.3 像素操作
-
getImageData(x, y, w, h) 获取图片像素数据
-
putImageData() 把像素数据重新绘制到canvas上面 注意getImageData()之前必须先将图片画在canvas上面
-
getImageData(x, y, w, h):获取canvas从(x,y)开始,宽w,高h的像素数据
const img = new Image()
img.src = '/src/img/aaa.png'
img.onload = () => {
ctx.drawImage(img, 0, 0);
console.log(ctx.getImageData(0, 0, 500, 500));
}
其中data里面的数据就是像素数据,data里面的数据就分别对应r,g,b,a四个值,每四个循环一次:[R1,G1,B1,A1,R2,G2,B2,A2,...],并且RGBA分别都是数组结构
- putImageData(pixiList, x, y):将像素数据绘制在(x,y)坐标
const img = new Image()
img.src = '/src/img/aaa.png'
img.onload = () => {
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, 500, 500)
ctx.putImageData(imageData, 100, 100)
}
我们可以将获取到的数据处理之后再次输出到canvas上面
注意:如果你不想你的浏览器卡死,最好不要for循环打印data里面的数据!!!😒
4.3.1 颜色翻转(正负片效果)
- 颜色翻转是将R、G、B三个值取反,即:255-R,255-G,255-B
for (let i = 0; i < imageData.data.length; i+=4) {
// 遍历每个像素
imageData.data[i] = 255 - imageData.data[i]
imageData.data[i+1] = 255 - imageData.data[i+1]
imageData.data[i+2] = 255 - imageData.data[i+2]
}
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.putImageData(imageData, 0, 0)
4.3.2 黑白效果
- 是取RGB三个颜色的平均值,然后给所有像素点都设置这个平均值
for (let i = 0; i < imageData.data.length; i+=4) {
// 遍历每个像素
const avg = (imageData.data[i] + imageData.data[i+1] + imageData.data[i+2] ) / 3
imageData.data[i] = avg
imageData.data[i+1] = avg
imageData.data[i+2] = avg
}
使用加权平均值效果会更好
const avg = imageData.data[i] * 0.3 + imageData.data[i] * 0.4 + imageData.data[i] * 0.3
分别对RGB取加权平均值实现复古效果
imageData.data[i] = imageData.data[i] * 0.39 + imageData.data[i + 1] * 0.76 + imageData.data[i+2] * 0.18
imageData.data[i+1] = imageData.data[i] * 0.35 + imageData.data[i + 1] * 0.68 + imageData.data[i+2] * 0.16
imageData.data[i+2] = imageData.data[i] * 0.27 + imageData.data[i + 1] * 0.53 + imageData.data[i+2] * 0.13
4.3.3 调整亮暗
- 给每个RGB值加一个正数就是变亮,加个负数就是变暗
for (let i = 0; i < imageData.data.length; i+=4) {
const l = 80 // -80
imageData.data[i] += l
imageData.data[i+1] += l
imageData.data[i+2] += l
}
4.3.4 红色、绿色、蓝色蒙版
- R取RGB的平均值,GB都是0,其它的蒙版同理
for (let i = 0; i < imageData.data.length; i+=4) {
const avg = (imageData.data[i] + imageData.data[i+1] + imageData.data[i+2] ) / 3
imageData.data[i] = avg
imageData.data[i+1] = 0
imageData.data[i+2] = 0
}
4.3.5 修改透明度
- 将A值乘以一个小数就可以修改透明度
for (let i = 0; i < imageData.data.length; i+=4) {
imageData.data[i+3] = imageData.data[i+3] * 0.5
}
大家可以想想怎么将一个图片转换成像素风,这里给大家一个思路,可以先将图片缩小,然后在放大,具体阈值,大家可以自己尝试。
4.3.6 创建像素区域
- createImageData(w, h) 创建一个宽w,高h的像素操作区域
- createImageData(imageData) 创建一个和imageData一样宽高的像素操作区域
对一个区域进行像素操作
const imageData2 = ctx.createImageData(300, 300)
for (let i = 0; i < 300 * 300 * 4; i+=4) {
imageData2.data[i] = 100
imageData2.data[i+1] = 100
imageData2.data[i+2] = 188
imageData2.data[i+3] = 255
}
5. 渐变与阴影
5.1 线性渐变
- ctx.createLinearGradient(x1, y1, x2, y2):创建一个从(x1,y1)开始,到(x2,y2)结束的渐变对象
- addColorStop(value, color); 给上面创建的渐变对象添加颜色,可以添加多个,value为渐变位置偏移量(0-1),color为渐变颜色
ctx.rect(0, 0, canvas.width, canvas.height)
const gnt = ctx.createLinearGradient(0, 0, canvas.width, 0);
gnt.addColorStop(0.1, 'red');
gnt.addColorStop(0.2, 'green');
gnt.addColorStop(0.3, 'blue');
gnt.addColorStop(0.4, 'purple');
ctx.fillStyle = gnt
ctx.fill()
- 文字渐变
ctx.font = 'bold 50px Kai'
const gnt = ctx.createLinearGradient(0, 0, canvas.width, 0);
gnt.addColorStop(0, 'purple');
gnt.addColorStop(1, '#fff');
ctx.fillStyle = gnt
ctx.fillText("Canvas", 100, 100);
5.2 径向渐变
- createRadialGradient(x1,y1,r1,x2,y2,r2):(x1,y1),r1是渐变开始的圆心坐标和半径,(x2,y2),r2是结束渐变的圆心坐标和半径
const gnt = ctx.createRadialGradient(250, 250, 100, 300, 300, 50);
gnt.addColorStop(0, 'red')
gnt.addColorStop(0.5, 'blue')
gnt.addColorStop(1, 'yellow')
ctx.fillStyle = gnt;
ctx.arc(250, 250, 100, 0, 2*Math.PI);
ctx.fill()
5.3 阴影
- shadowOffsetX:阴影右偏移量
- shadowOffsetY:阴影向下偏移量
- shadowColor:阴影颜色
- shadowBlur:阴影模糊程度,值越大越模糊
ctx.shadowOffsetX = 10;
ctx.shadowOffsetY = 10;
ctx.shadowColor = 'red'
ctx.shadowBlur = 5
ctx.fillRect(100, 100, 100, 100)
- 给文字设置阴影
ctx.font = 'bold 50px Kai'
ctx.shadowOffsetX = 10;
ctx.shadowOffsetY = 10;
ctx.shadowColor = 'red'
ctx.shadowBlur = 5
ctx.fillText("Canvas", 100, 100);
- 给图片设置阴影
ctx.shadowOffsetX = 10;
ctx.shadowOffsetY = 10;
ctx.shadowColor = 'red'
ctx.shadowBlur = 5
const img = new Image();
img.src = '/src/img/aaa.png'
img.onload = () => {
ctx.drawImage(img, 100, 100, 100, 100)
}
注意:设置阴影的代码要写在绘制代码之前
6 canvas路径
在Canvas中除了矩形,其它所有的图形都是基于路径绘制的
6.1 开始路径和关闭路径
- beginPath()和closePath()
6.1.1 开始路径
先看下面的例子:
ctx.moveTo(100, 100)
ctx.lineTo(300, 100)
ctx.strokeStyle = "red"
ctx.stroke()
ctx.moveTo(100, 200)
ctx.lineTo(300, 200)
ctx.strokeStyle = "blue"
ctx.stroke()
代码中我们画了两条直线,上面的是红色,下面的是蓝色,但是我们发现实际画出来的,上面是紫色,下面的是蓝色,这是因为第二次画的蓝色线,他设置的蓝色样式会影响第一次设置的红色样式,是直接覆盖上面,所以红色+蓝色,第一条直线会变成紫色。
Canvas是根据状态绘图的,每次绘制会检测所有的状态比如说:strokeStyle,fillStyle等,如果我们只是设置了一次,那么整个程序会一直用这个状态,如果我们每次都使用beginPath()一个新的路径,那么他们就会用各自的状态。
将上面的代码改成这样:
ctx.moveTo(100, 100)
ctx.lineTo(300, 100)
ctx.strokeStyle = "red"
ctx.stroke()
ctx.beginPath()
ctx.moveTo(100, 200)
ctx.lineTo(300, 200)
ctx.strokeStyle = "blue"
ctx.stroke()
加了beginPath,这样两个线条都显示正确的颜色了。
注意:判断是否属于同一路径的标准是是否使用了beginPath()方法,而不是视觉上是否有首尾连线
使用以下方法只是绘制图形,并不会开始新路径:moveTo()、lineTo()、strokeRect()、fillRect()、rect()、arc()、arcTo()、quadricCurveTo()和bezierCurveTo()
6.1.2 关闭路径
这里先理解几个概念:
- 关闭路径:是指将一个路径的起点和终点链接起来,使其成为一个封闭的图形
- 结束路径:是指开始一个新的路径,所以我们要想结束一个路径,开始新路径只有beginPath()
看这个例子:
ctx.arc(250, 250, 100, 0, Math.PI)
ctx.strokeStyle = 'red'
ctx.stroke();
当前是只有一个路径,但是我们没有关闭路径,所以这个圆弧没有密封。
添加closePath()
ctx.arc(250, 250, 50, 0, Math.PI)
ctx.strokeStyle = 'red'
ctx.closePath()
ctx.stroke();
这时候我们会发现,圆弧已经封闭了
注意:关闭路径要写在stroke之前
我们再新增一个蓝色的圆弧:
ctx.arc(250, 250, 50, 0, Math.PI)
ctx.strokeStyle = 'red'
ctx.closePath()
ctx.stroke();
ctx.arc(150, 250, 50, 0, Math.PI)
ctx.strokeStyle = 'blue'
ctx.closePath()
ctx.stroke();
我们会发现,第二个圆弧和第一个连起来了,并且,第一个红色的圆弧颜色也变了,这是因为clothPath是关闭路径,他只会将图形起点和终点连起来,并不会开始新的路径,现在相当于还是一个路径,状态共享,所以颜色叠加了,同时第二个圆弧的起点也是第一个圆弧的终点,所以他们连一起了,要想分开并显示正确的颜色,我们只需添加一下beginPath:
ctx.arc(250, 250, 50, 0, Math.PI)
ctx.strokeStyle = 'red'
ctx.closePath()
ctx.stroke();
ctx.beginPath()
ctx.arc(150, 250, 50, 0, Math.PI)
ctx.strokeStyle = 'blue'
ctx.closePath()
ctx.stroke();
大家一定要记住不要搞混关闭路径和结束路径!!!
6.2 判断某个点是否在当前路径
- isPointInPath(x, y)
ctx.arc(250, 250, 50, 0, Math.PI)
ctx.closePath()
ctx.stroke()
console.log(ctx.isPointInPath(250, 250)); // true
注意:isPointInPath方法不支持fillRect和strokeRect方法,如果想使用矩形,可以用rect()方法代替
7. canvas状态
canvas是基于状态去绘图,如果我们不使用beginPath开始新路径,那么全局都是使用这一个状态,我们可以通过canvas提供的两个方法去储存和恢复状态。
7.1 clip() 方法
在使用clip()方法之前,要绘制一个基本图形,然后调用clip(),这个基本图形就会变成剪切区域,后面绘制的图形超过这个区域就不会显示出来
注意:fillRect和strokeRect方法不能作为剪切区域,可以使用rect代替!!!
// 1. 绘制基本图形
ctx.rect(100, 100, 100, 100)
// 2. 裁剪矩形作为剪切区域
ctx.clip()
// 3. 画个超出矩形范围的图片(填充颜色也行)
const img = new Image()
img.src = '/src/img/man.jpg'
img.onload = () => {
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
}
7.2 save()和restore()
大家思考一下,上面我们裁减了图片,如果后面我们有不需要裁剪的图要画,这时候也是会被裁减的,怎样把后面的图片显示出来?我们只需要将裁剪之前的状态保存下来,然后开始一个新的路径去做裁剪操作,后面我们不需要裁剪了,我们只需要恢复到裁剪之前的状态就可以继续干后面的事情了。
调用save方法可以将当前状态保存下来,使用restore方法恢复保存的状态
// 1.将没有裁剪的环境保存下来
ctx.save();
// 2.开始裁剪操作
ctx.beginPath();
ctx.arc(250, 250, 100, 0, Math.PI * 2);
ctx.stroke();
ctx.clip();
const img = new Image();
img.src = "/src/img/man.jpg";
img.onload = () => {
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
fn()
};
function fn() {
// 3.裁剪完成,恢复之前的状态,去加不需要裁剪的图
ctx.restore();
const img2 = new Image();
img2.src = "/src/img/aaa.png";
img2.onload = () => {
ctx.drawImage(img2, 100, 250, 50, 50);
};
}
流程大概是这样:
之前创建两个不同颜色的线段是通过beginPath实现的,大家也可以试试用save和restore来实现。
8. canvas一些其它方法
8.1 toDataURL()
将canvas画布保存成一张图片,返回base64,可以通过 window.location = ctx.toDataURL("image/png")方法实现浏览器下载。
8.2 globalAlpha 属性
修改全局透明度,范围是0-1,要写在最前面,对全局生效。
8.3 globalCompositeOperation属性
正常在canvas绘制图形,一般都是后面覆盖前面的,可以通过这个属性来设置覆盖方式,有下面几种属性,大家可以自己尝试一下效果:
9. 事件操作
9.1 封装鼠标方法
window.tools = {};
window.tools.getMouse = function (element) {
var mouse = { x: 0, y: 0 };
element.addEventListener("mousemove", function (e) {
var x, y;
var e = e || window.event;
if (e.pageX || e.pageY) {
x = e.pageX;
y = e.pageY;
} else {
x =
e.clientX +
document.body.scrollLeft +
document.documentElement.scrollLeft;
y =
e.clientY +
document.body.scrollTop +
document.documentElement.scrollTop;
}
x -= element.offsetLeft;
y -= element.offsetTop;
mouse.x = x;
mouse.y = y;
});
return mouse;
};
使用:
// @ts-ignore
const mouse = window.tools.getMouse(canvas)
canvas.addEventListener('mousemove', function(){
console.log(mouse.x, mouse.y);
})
9.2 键盘事件
window.addEventListener('keydown', function(e){
console.log(e);
})
10. 物理数学和动画
10.1 动画 requestAnimationFrame
我们制作动画最好不要使用setInterval,setInterval本身性能不是很好,所以我们用requestAnimationFrame来代替,但是其存在兼容性,所以我们写一个工具方法,后面去引入使用。
window.requestAnimationFrame =
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.msRequestAnimationFrame ||
window.oRequestAnimationFrame ||
function (callback) {
return window.setTimeout(callback, 1000 / 60);
};
使用方法:
let x = 0;
(function move(){
window.requestAnimationFrame(move)
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.beginPath()
ctx.arc(x, 250, 50, 0, 2*Math.PI)
ctx.stroke()
x+=2
})()
10.2 三角函数
10.2.1 三角函数定义
首先先了解一下我们常用的六种三角函数:
- sin(θ):Math.sin(θ*Math.PI/180)
- cos(θ):Math.cos(θ*Math.PI/180)
- tan(θ):Math.tan(θ*Math.PI/180)
- arcsin(x/R):Math.asin(x/R)*(180/Math.PI)
- arccos(y/R):Math.acos(x/y)*(180/Math.PI)
- arctan(x/y):Math.atan(x/y)*(180/Math.PI)
具体对应关系如下图:
注意在JS中所有的角度都是弧度制,即:Math.PI = 180度,大家可以通过这种方式来转换 度数*Math.PI/180
- 试着写一个跟随鼠标移动的箭头
class Arrow {
x = 0;
y = 0;
ctx: CanvasRenderingContext2D;
constructor(x: number, y: number, ctx: CanvasRenderingContext2D) {
this.x = x;
this.y = y;
this.ctx = ctx;
this.drawArrow(0);
}
drawArrow(deg: number) {
this.ctx.save()
this.ctx.translate(this.x, this.y)
this.ctx.rotate(deg)
this.ctx.moveTo(0, 0);
this.ctx.lineTo(-50, -10);
this.ctx.lineTo(20, -10);
this.ctx.lineTo(20, -30);
this.ctx.lineTo(50, 0);
this.ctx.lineTo(20, 30);
this.ctx.lineTo(20, 10);
this.ctx.lineTo(-50, 10);
this.ctx.lineTo(-50, -10);
this.ctx.fillStyle = 'black'
this.ctx.fill();
this.ctx.restore()
this.ctx.beginPath();
this.ctx.arc(this.x, this.y, 2, 0, 2 * Math.PI);
this.ctx.fillStyle = 'red'
this.ctx.fill();
}
linePoint(x: number, y: number) {
this.ctx.beginPath();
this.ctx.setLineDash([2, 2]);
this.ctx.moveTo(this.x, this.y);
this.ctx.lineTo(x, y);
this.ctx.stroke();
}
getDeg(x: number, y: number) {
const deg = Math.atan2(y - this.y, x - this.x) * (180 / Math.PI);
this.rotate(deg);
}
rotate(deg: number) {
this.drawArrow(deg * Math.PI / 180)
}
doAnimate(x: number, y: number) {
this.ctx.clearRect(0, 0, canvas.width, canvas.height);
this.linePoint(x, y);
this.getDeg(x, y);
}
}
let arrow = new Arrow(100, 100, ctx);
// @ts-ignore
const mouse = window.tools.getMouse(canvas);
canvas.addEventListener("mousemove", function () {
arrow.doAnimate(mouse.x, mouse.y);
});
如果有旋转的逻辑,最好绘制图形的时候,以屏幕左上角为原点绘制,使用translate()方法移动至需要的位置,这样旋转的时候就不会错位了
10.2.2 椭圆
首先要知道椭圆的方程:(x/a)² + (y/b)² = 1,即椭圆上任意一点(x,y)都满足这个方程
由上图可知椭圆每一点坐标为:
- x = centerX + Math.cos(angle)*radiusX
- y = centerY + Math.sin(angle)*radiusY
class Ellips {
centerX = 0
centerY = 0
radiusX = 10
radiusY = 5
canvas: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;
constructor(canvas: HTMLCanvasElement, cx:number, cy:number, rx:number, ry:number) {
this.centerX = cx;
this.centerY = cy;
this.radiusX = rx;
this.radiusY = ry;
this.canvas = canvas;
this.ctx = this.canvas.getContext('2d') as CanvasRenderingContext2D
this.draw()
}
draw() {
this.ctx.beginPath()
this.ctx.moveTo(this.centerX + this.radiusX, this.centerY)
for (let deg = 1; deg < 360; deg++) {
const x = this.centerX + Math.cos(deg * Math.PI/180)*this.radiusX
const y = this.centerY + Math.sin(deg * Math.PI/180)*this.radiusY
this.ctx.lineTo(x, y)
}
this.ctx.closePath()
this.ctx.stroke()
}
}
new Ellips(canvas, 250, 250, 100, 50)
10.2.3 波形运动
- 如果赋予canvas对象x坐标为正弦函数,则其会左右摇摆运动
const speed = 0.1 // 速度
const swing = 5 // 摆动幅度
function XMove(deg:number) {
window.requestAnimationFrame(() => {
ctx.beginPath()
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.translate(Math.sin(deg) * swing,0)
ctx.arc(100, 250, 30, 0, 2 * Math.PI)
ctx.stroke()
deg+=speed;
XMove(deg)
})
}
XMove(0)
- 如果赋予canvas对象y坐标为正弦函数,x坐标正常增加常量,则会正弦函数轨迹运动
const speed = 0.1
const swing = 5
function XMove(deg:number) {
window.requestAnimationFrame(() => {
ctx.beginPath()
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.translate(1, Math.sin(deg) * swing)
ctx.arc(30, 250, 30, 0, 2 * Math.PI)
ctx.fill()
deg+=speed;
XMove(deg)
})
}
XMove(0)
10.3 加速运动
- 匀加速运动
let v = 0;
let a = 0.1;
(function XMove() {
window.requestAnimationFrame(() => {
ctx.beginPath()
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.translate(v, 0)
ctx.arc(40, 250, 30, 0, 2 * Math.PI)
ctx.stroke()
v+=a;
XMove()
})
})()
大家可以考虑一下匀加速,再回头运动。
重力其实就是y轴方向的加速运动,同理摩擦力就是x轴向运动方向相反给的加速,可以给a乘一个小数。
canvas 基础内容就大概差不多这么多,学会上面API做点小特效还是没什么问题的,大家可以自己试着研究一下碰撞检测,有能力的童鞋可以尝试写个简单的物理引擎。
参考资料: