1. 什么是 Canvas ?
Canvas 最初由 Apple 于 2004 年 引入,用于 Mac OS X Webkit 组件,为仪表盘小组件和 Safari 浏览器等应用程序提供支持。后来,它被 Gecko 内核的浏览器(尤其是 Mozilla Firefox),Opera 和 Chrome 实现,并被网页超文本应用技术工作小组提议为下一代的网络技术的标准元素(HTML5新增元素)。
Canvas 提供了非常多的 JavaScript绘图 API (比如:绘制路径、举行、圆、文本和图像等方法),与 <canvas>
元素可以绘制各种 2D 图形。
Canvas API 主要聚焦于 2D 图形。当然也可以使用 <canvas>
元素对象的 WebGL API 来绘制 2D 和 3D 图形。
Canvas 可用于动画、游戏画面、数据可视化、图片编辑以及实现视频处理等方面。
浏览器兼容性
Canvas 优点:
- Canvas 提供的功能更原始,适合像素处理,动态渲染和数据量大的绘制,如:图片编辑、热力图、炫光尾迹特效等。
- Canvas 非常适合图像密集的游戏开发,适合频繁重绘许多的对象。
- Canvas能够以 .png 或 .jpg 格式保存结果图片,适合对图像进行像素级的处理。
Canvas 缺点:
- 在移动端可能因为 Canvas 数量多,而导致内存占用超出了手机的承受能力,导致浏览器崩溃。
- Canvas 绘图只能通过 JavaScript 脚本操作 all in js。
- Canvas 是由一个个像素点构成的图形,放大会使图形变得颗粒状和像素化,导致模糊。
2. Canvas 绘制图形
Canvas 支持两种方式来绘制矩形:矩形方法 和 路径方法。
2.1 矩形方法
- 路径是通过不同颜色和宽度的线段或曲线相连形成的不同形状的点的集合。
- 除了矩形,其他的图形都是通过一条或者多条路径组合而成的。
- 通常我们会通过众多的路径来绘制复杂的图形。
- fillRect(x, y, width, height): 绘制一个填充的矩形
- strokeRect(x, y, width, height): 绘制一个矩形的边框
- clearRect(x, y, width, height): 清除指定矩形区域,让清除部分完全透明。
Canvas 绘制一个矩形:
<canvas id="tutorial" width="300" height="300px">
你的浏览器不兼容Canvas,请升级您的浏览器!
</canvas>
<script>
window.onload = function() {
let canvasEl = document.getElementById('tutorial')
if(!canvasEl.getContext){
return
}
let ctx = canvasEl.getContext('2d') // 2d | webgl
ctx.fillRect(0,0, 100, 50) // 单位也是不用写 px
}
</script>
2.2 路径方法
使用路径绘制图形的步骤:
- 首先需要创建路径起始点(beginPath)。
- 然后使用画图命令去画出路径( arc绘制圆弧 、lineTo画直线 )。
- 之后把路径闭合( closePath , 不是必须)。
- 一旦路径生成,就能通过 描边(stroke) 或 填充路径区域(fill) 来渲染图形。
以下是绘制路径时,所要用到的函数
- beginPath():新建一条路径,生成之后,图形绘制命令被指向到新的路径上绘图,不会关联到旧的路径。
- closePath():闭合路径之后图形绘制命令又重新指向到 beginPath之前的上下文中。
- stroke():通过线条来绘制图形轮廓/描边 (针对当前路径图形)。
- fill():通过填充路径的内容区域生成实心的图形 (针对当前路径图形)。
<canvas id="tutorial" width="300" height="300px">
你的浏览器不兼容Canvas,请升级您的浏览器!
</canvas>
<script>
window.onload = function () {
let canvasEl = document.getElementById("tutorial");
if (!canvasEl.getContext) {
return;
}
let ctx = canvasEl.getContext("2d"); // 2d | webgl
// 1.创建一个路径
ctx.beginPath();
// 2.绘图指令
// ctx.moveTo(0, 0)
// ctx.rect(100, 100, 100, 50);
ctx.moveTo(100, 100);
ctx.lineTo(200, 100);
ctx.lineTo(200, 150);
ctx.lineTo(100, 150);
// 3.闭合路径
ctx.closePath();
// 4.填充和描边
ctx.stroke();
};
</script>
lineTo 和 arc 两个函数结合既能绘制直线也能绘制圆弧,因此路径方法还可以绘制许多图形,比如三角形、菱形、梯形、椭圆形、圆形等等。。。
3. Canvas 样式和颜色
3.1 色彩 Colors
如果我们想要给图形上色,有两个重要的属性可以做到:
- fillStyle = color: 设置图形的填充颜色,需在 fill() 函数前调用。
- strokeStyle = color: 设置图形轮廓的颜色,需在 stroke() 函数前调用。
:::warning{title="注意"}
一旦设置了 strokeStyle 或者 fillStyle 的值,那么这个新值就会成为新绘制的图形的默认值。
如果你要给图形上不同的颜色,你需要重新设置 fillStyle 或 strokeStyle 的
:::
3.2 透明度 Transparent
除了可以绘制实色图形,我们还可以用 canvas 来绘制半透明的图形。
- 方式一:strokeStyle 和 fillStyle属性结合RGBA:
// 指定透明颜色,用于描边和填充样式
ctx.strokeStyle = "rgba(255,0,0,0.5)";
ctx.fillStyle = "rgba(255,0,0,0.5)";
- 方式二:globalAlpha 属性
// 针对于Canvas中所有的图形生效
ctx.globalAlpha = 0.3
// 2.修改画笔的颜色
// ctx.fillStyle = 'rgba(255, 0, 0, 0.3)'
ctx.fillRect(0,0, 100, 50) // 单位也是不用写 px
ctx.fillStyle = 'blue'
ctx.fillRect(200, 0, 100, 50)
ctx.fillStyle = 'green' // 关键字, 十六进制, rbg , rgba
ctx.beginPath()
ctx.rect(0, 100, 100, 50)
ctx.fill()
:::warning{title="注意"}
globalAlpha = 0 ~ 1
✓ 这个属性影响到 canvas 里所有图形的透明度
✓ 有效的值范围是 0.0(完全透明)到 1.0(完全不透明),默认是 1.0。
:::
3.3 线型 Line styles
调用lineTo()函数绘制的线条,是可以通过一系列属性来设置线的样式。
- lineWidth = value: 设置线条宽度。
- lineCap = type: 设置线条末端样式。
- lineJoin = type: 设定线条与线条间接合处的样式。
- ......
lineWidth
- 设置线条宽度的属性值必须为正数。默认值是 1.0px,不需单位。( 零、负数、Infinity和NaN值将被忽略)
- 线宽是指给定路径的中心到两边的粗细。换句话说就是在路径的两边各绘制线宽的一半。
- 如果你想要绘制一条从 (3,1) 到 (3,5),宽度是 1.0 的线条,你会得到像第二幅图一样的结果。
✓ 路径的两边个各延伸半个像素填充并渲染出1像素的线条(深蓝色部分)
✓ 两边剩下的半个像素又会以实际画笔颜色一半色调来填充(浅蓝部分)
✓ 实际画出线条的区域为(浅蓝和深蓝的部分),填充色大于1像素了,这就是为何宽度为 1.0 的线经常并不准确的原因。- 要解决这个问题,必须对路径精确的控制。如,1px的线条会在路径两边各延伸半像素,那么像第三幅图那样绘制从 (3.5 ,1) 到 (3.5, 5) 的线条,其边缘正好落在像素边界,填充出来就是准确的宽为 1.0 的线条。
lineCap: 属性的值决定了线段端点显示的样子。它可以为下面的三种的其中之一:
- butt 截断,默认是 butt。
- round 圆形
- square 正方形
lineJoin: 属性的值决定了图形中线段连接处所显示的样子。它可以是这三种之一:
- round 圆形
- bevel 斜角
- miter 斜槽规,默认是 miter。
3.4 绘制文本
canvas 提供了两种方法来渲染文本:
- fillText(text, x, y [, maxWidth])
✓ 在 (x,y) 位置,填充指定的文本
✓ 绘制的最大宽度(可选)。 - strokeText(text, x, y [, maxWidth])
✓ 在 (x,y) 位置,绘制文本边框
✓ 绘制的最大宽度(可选)。
文本的样式(需在绘制文本前调用)
- font = value: 当前绘制文本的样式。这个字符串使用和 CSS font 属性相同的语法。默认的字体是:
10px sans-serif
。 - textAlign = value:文本对齐选项。可选的值包括:
start
,end
,left
,right
orcenter
. 默认值是start
- textBaseline = value:基线对齐选项。可选的值包括:
top
, hanging,middle
, alphabetic, ideographic,bottom
。
✓ 默认值是 alphabetic。
<canvas id="tutorial" width="300" height="300px">
你的浏览器不兼容Canvas,请升级您的浏览器!
</canvas>
<script>
window.onload = function () {
let canvasEl = document.getElementById("tutorial");
if (!canvasEl.getContext) {
return;
}
let ctx = canvasEl.getContext("2d"); // 2d | webgl
ctx.font = "60px sen-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.strokeStyle = "red";
ctx.fillStyle = "red";
// 将字体绘制在 100, 100 这个坐标点
ctx.fillText("Ay", 100, 100);
// ctx.strokeText("Ay", 100, 100);
};
</script>
3.5 绘制图片
绘制图片,可以使用 drawImage 方法将它渲染到 canvas 里。drawImage 方法有三种形态:
drawImage(image, x, y)
- 其中 image 是 image 或者 canvas 对象,x 和 y 是其在目标 canvas 里的起始坐标。
drawImage(image, x, y, width, height)
- 这个方法多了 2 个参数:width 和 height,这两个参数用来控制 当向 canvas 画入时应该缩放的大小
drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
- 第一个参数和其它的是相同的,都是一个图像或者另一个 canvas 的引用。其它 8 个参数最好是参照右边的图解,前 4 个是定义图像源的切片位置和大小,后 4 个则是定义切片的目标显示位置和大小。
:::info{title="图片的来源"}
- HTMLImageElement:这些图片是由Image()函数构造出来的,或者任何的
<img>
元素。 - HTMLVideoElement:用一个 HTML 的
<video>
元素作为你的图片源,可以从视频中抓取当前帧作为一个图像。 - HTMLCanvasElement:可以使用另一个
<canvas>
元素作为你的图片源。 - 等等
:::
4. Canvas 状态和形变
4.1 Canvas 绘画状态-保存和恢复
Canvas 绘画状态是当前绘画时所产生的样式和变形的一个快照,Canvas 在绘画时,会产生相应的绘画状态,其实我们是可以将某些绘画的状态存储在栈中来为以后复用,Canvas 绘画状态的可以调用 save 和 restore 方法是用来保存和恢复,这两个方法都没有参数,并且它们是成对存在的。
保存和恢复(Canvas)绘画状态
- save():保存画布 (canvas) 的所有绘画状态
- restore():恢复画布 (canvas) 的所有绘画状态
Canvas绘画状态包括:
- 当前应用的变形(即移动,旋转和缩放)
- 以及这些属性:
strokeStyle
,fillStyle
,globalAlpha
,lineWidth
,lineCap
,lineJoin
, miterLimit, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor,font
,textAlign
,textBaseline
...... - 当前的裁切路径(clipping path)
4.2 移动 - translate
translate方法,它用来移动 canvas 和它的原点到一个不同的位置。
translate(x, y)
- x 是左右偏移量,y 是上下偏移量(无需单位)。
移动 canvas 原点的好处
- 如不使用 translate方法,那么所有矩形默认都将被绘制在相同的(0,0)坐标原点。
- translate方法可让我们任意放置图形,而不需要手工一个个调整坐标值。
移动矩形案例
- 第一步:先保存一下canvas当前的状态
- 第二步:在绘制图形前translate移动画布
- 第三步:开始绘制图形,并填充颜色
<script>
///1.形变( 没有保存状态)
ctx.translate(100, 100);
ctx.fillRect(0, 0, 100, 50); // 单位也是不用写 px
ctx.translate(100, 100);
ctx.strokeRect(0, 0, 100, 50);
</script>
<script>
// 2.形变(保存形变之前的状态)
ctx.save();
ctx.translate(100, 100);
ctx.fillRect(0, 0, 100, 50); // 单位也是不用写 px
ctx.restore(); // 恢复了形变之前的状态( 0,0)
ctx.save(); // (保存形变之前的状态)
ctx.translate(100, 100);
ctx.fillStyle = "red";
ctx.fillRect(0, 0, 50, 30);
ctx.restore();
</script>
4.3 移动 - rotate
rotate方法,它用于以原点为中心旋转 canvas,即沿着 z轴 旋转。
rotate(angle)
- 只接受一个参数:旋转的角度 (angle),它是顺时针方向,以弧度为单位的值。
- 角度与弧度的 JS 表达式:弧度=( Math.PI / 180 ) * 角度 ,即 1角度 = Math.PI/180 个弧度。
- 比如:旋转90°:Math.PI / 2; 旋转180°:Math.PI ; 旋转360°:Math.PI * 2; 旋转-90°:-Math.PI / 2;
- 旋转的中心点始终是 canvas 的原坐标点,如果要改变它,我们需要用到
translate
方法。
<SCRIPT>
// 保存形变之前的状态
ctx.save()
// 1.形变
ctx.translate(100, 100)
// 360 -> Math.PI * 2
// 180 -> Math.PI
// 1 -> Math.PI / 180
// 45 -> Math.PI / 180 * 45
ctx.rotate(Math.PI / 180 * 45)
ctx.fillRect(0, 0, 50, 50)
// ctx.translate(100, 0)
// ctx.fillRect(0, 0, 50, 50)
// 绘图结束(恢复形变之前的状态)
ctx.restore()
ctx.save()
ctx.translate(100, 0)
ctx.fillRect(0, 0, 50, 50)
ctx.restore()
// ....下面在继续写代码的话,坐标轴就是参照的是原点了
<SCRIPT>
4.4 移动 - scale
scale(x, y) 方法可以缩放画布。可用它来增减图形在 canvas 中的像素数目,对图形进行缩小或者放大。x 为水平缩放因子,y 为垂直缩放因子,也支持负数。
<script>
// 保存形变之前的状态
ctx.save()
// 1.形变
ctx.translate(100, 100) // 平移坐标系统
ctx.scale(2, 2) // 对坐标轴进行了放大(2倍)
ctx.translate(10, 0) // 10px -> 20px
ctx.fillRect(0, 0, 50, 50)
// 绘图结束(恢复形变之前的状态)
ctx.restore()
// ....下面在继续写代码的话,坐标轴就是参照的是原点了
</script>
5. Canvas 动画和案例
5.1 Canvas 动画
Canvas绘图都是通过JavaScript 去操控的,如要实现一些交互性动画是相当容易的。我们可以使用 setInterval
、 setTimeout
和 requestAnimationFrame
三种方法来定期执行指定函数进行重绘。
Canvas 画出一帧动画的基本步骤(如要画出流畅动画,1s 需绘60帧):
- 第一步:用 clearRect 方法清空 canvas ,除非接下来要画的内容会完全充满 canvas(例如背景图),否则你需要清空所有。
- 第二步:保存 canvas 状态,如果加了 canvas 状态的设置(样式,变形之类的),又想在每画一帧之时都是原始状态的话,
你需要先保存一下,后面再恢复原始状态。
- 第三步:绘制动画图形(animated shapes) ,即绘制动画中的一帧。
- 第四步:恢复 canvas 状态,如果已经保存了 canvas 的状态,可以先恢复它,然后重绘下一帧。
5.2 案例:太阳系动画效果
代码
<script>
window.onload = function () {
let canvasEl = document.getElementById("tutorial");
if (!canvasEl.getContext) {
return;
}
let ctx = canvasEl.getContext("2d"); // 2d | webgl
let sun = new Image();
sun.src = "../../images/canvas_sun.png";
// sun.onload = function() {
// // draw
// }
let earth = new Image();
earth.src = "../../images/canvas_earth.png";
let moon = new Image();
moon.src = "../../images/canvas_moon.png";
requestAnimationFrame(draw);
/**
1秒钟会回调 61次
*/
function draw() {
console.log("draw");
ctx.clearRect(0, 0, 300, 300);
ctx.save();
// 1.绘制背景
drawBg();
// 2.地球
drawEarth();
ctx.restore();
requestAnimationFrame(draw);
}
function drawBg() {
ctx.save();
ctx.drawImage(sun, 0, 0); // 背景图
ctx.translate(150, 150); // 移动坐标
ctx.strokeStyle = "rgba(0, 153, 255, 0.4)";
ctx.beginPath(); // 绘制轨道
ctx.arc(0, 0, 105, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
}
function drawEarth() {
let time = new Date();
let second = time.getSeconds();
let milliseconds = time.getMilliseconds();
ctx.save(); // earth start
ctx.translate(150, 150); // 中心点坐标系
// 地球的旋转
// Math.PI * 2 一整个圆的弧度
// Math.PI * 2 / 60 分成 60 份
// Math.PI * 2 / 60 1s
// Math.PI * 2 / 60 / 1000 1mm
// 1s 1mm
// Math.PI * 2 / 60 * second + Math.PI * 2 / 60 / 1000 * milliseconds
ctx.rotate(
((Math.PI * 2) / 10) * second +
((Math.PI * 2) / 10 / 1000) * milliseconds
);
ctx.translate(105, 0); // 圆上的坐标系
ctx.drawImage(earth, -12, -12);
// 3.绘制月球
drawMoon(second, milliseconds);
// 4.绘制地球的蒙版
drawEarthMask();
ctx.restore(); // earth end
}
function drawMoon(second, milliseconds) {
ctx.save(); // moon start
// 月球的旋转
// Math.PI * 2 一圈 360
// Math.PI * 2 / 10 1s(10s一圈)
// Math.PI * 2 / 10 * 2 2s(10s一圈)
// Math.PI * 2 / 10 / 1000 1mm 的弧度
// 2s + 10mm = 弧度
// Math.PI * 2 / 10 * second + Math.PI * 2 / 10 / 1000 * milliseconds
ctx.rotate(
((Math.PI * 2) / 2) * second +
((Math.PI * 2) / 2 / 1000) * milliseconds
);
ctx.translate(0, 28);
ctx.drawImage(moon, -3.5, -3.5);
ctx.restore(); // moon end
}
function drawEarthMask() {
// 这里的坐标系是哪个? 圆上的坐标系
ctx.save();
ctx.fillStyle = "rgba(0, 0, 0, 0.4)";
ctx.fillRect(0, -12, 40, 24);
ctx.restore();
}
};
</script>