Canvas
Canvas API 主要聚焦于 2D 图形。当然也可以使用<canvas>
元素对象的 WebGL API 来绘制 2D 和 3D 图形,可以用于动画、游戏画面、数据可视化、图片编辑以及实时视频处理。
- Canvas非常适合图像密集型的游戏开发,适合频繁重绘许多的对象。
- 能够以 .png 或 .jpg 格式保存结果图像,适合对图片进行像素级的处理。
- 在移动端可以能会因为Canvas数量多,而导致内存占用超出了手机的承受能力,导致浏览器崩溃。
- Canvas 是由一个个像素点构成的图形,放大会使图形变得颗粒状和像素化,导致模糊。
canvas
<canvas>
标签只有width和height属性,没有设置宽度和高度时,canvas 会初始化宽为300px 和高为 150px。
与<img>
元素不同,<canvas>
元素必须需要结束标签 </canvas>
。如结束标签不存在,则文档其余部分会被认为是替代内容,将不会显示出来。
<canvas id="myCanvas" width="200" height="200"></canvas>
测试 canvas.getContext()
方法的存在,可以检查浏览器是否支持Canvas
。
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (ctx) {
console.log('Canvas is supported!');
} else {
console.log('Canvas is not supported!');
}
绘制一个矩形
<canvas id="canvas" width="500" height="500"></canvas>
<script>
const canvasEl = document.getElementById('canvas');
const ctx = canvasEl.getContext('2d');
ctx.fillRect(0, 0, 100, 50);
</script>
坐标空间
<canvas>
元素默认被网格所覆盖,通常来说网格中的一个单元相当于 canvas 元素中的一像素。- 该网格的原点位于坐标 (0,0) 的左上角。所有元素都相对于该原点放置。
图形路径
Canvas 绘图的方法主要有两种:路径绘图和像素绘图。
- 路径绘图:路径绘图是通过一系列的路径命令来绘制图形。路径绘图可以用来绘制各种各样的图形,比如线条、矩形、圆形、多边形等等。Canvas 提供了一系列的路径绘图 API,比如
beginPath
、moveTo
、lineTo
、rect
、arc
等等。 - 像素绘图:像素绘图是通过直接操作像素来绘制图形。像素绘图可以用来绘制各种各样的图形,比如图片、文字、图标等等。Canvas 提供了一系列的像素绘图 API,比如
drawImage
、fillText
、getImageData
、putImageData
等等
- 需要注意的是,路径绘图和像素绘图是两种不同的绘图方式,它们各有优缺点,应根据具体情况来选择合适的绘图方式。在实际开发中,我们可以根据需求来选择合适的绘图方式,并进行适当的优化和调整。
Canvas 绘图的矩形方法:
- fillRect(x, y, width, height): 绘制一个填充的矩形
- strokeRect(x, y, width, height): 绘制一个矩形的边框
- clearRect(x, y, width, height): 清除指定矩形区域,让清除部分完全透明
Canvas 绘图的路径方法:
- 图形的基本元素是路径。路径是通过不同颜色和宽度的线段或曲线相连形成的不同形状的点的集合。
- 路径是可由很多子路径构成,这些子路径都是在一个列表中,列表中所有子路径(线、弧形等)将构成图形。
使用路径绘制图形的步骤
- 创建路径:使用
beginPath
方法来创建一个新的路径。每次调用beginPath
方法都会创建一个新的路径,之前的路径会被清除。 - 移动画笔:使用
moveTo
方法来移动画笔到指定的坐标点。调用moveTo
方法后,画笔会停留在指定的坐标点,等待下一次的路径命令。- moveTo(x, y)
- 将笔移动到指定的坐标 x 、 y 上
- moveTo(x, y)
- 然后使用画图命令去画出路径( arc 、lineTo )
- lineTo(x, y)
- 调用
lineTo
方法后,画笔会从当前位置绘制一条直线到指定的坐标点,并停留在指定的坐标点,等待下一次的路径命令。
- 调用
- ctx.rect(x, y, width, height);
- 使用
rect
方法来绘制一个矩形。调用rect
方法后,画笔会绘制一个矩形,并停留在矩形的右下角,等待下一次的路径命令。
- 使用
- arc(x, y, radius, startAngle, endAngle, anticlockwise);
- x、y:为绘制圆弧所在圆上的圆心坐标
- radius:为圆弧半径,弧度=( Math.PI / 180 ) * 角度
- startAngle、endAngle:该参数用弧度定义了开始以及结束的弧度。这些都是以 x 轴为基准。
- anticlockwise:为一个布尔值。为 true ,是逆时针方向,为false,是顺时针方向,默认为false。
- 调用
arc
方法后,画笔会绘制一个圆形,并停留在圆形的结束点,等待下一次的路径命令。
- lineTo(x, y)
- 闭合路径:使用
closePath
方法来闭合路径。调用closePath
方法后,画笔会从当前位置绘制一条直线到路径的起始点,并停留在起始点,等待下一次的路径命令。 - 绘制路径:使用
stroke
或fill
方法来绘制路径。调用stroke
方法可以绘制路径的线条,调用fill
方法可以填充路径的内部。
<canvas id="myCanvas" width="500" height="500"></canvas>
<script>
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(150, 50);
ctx.lineTo(150, 150);
ctx.lineTo(50, 150);
ctx.closePath();
// 绘制线条
ctx.lineWidth = 5;
ctx.strokeStyle = '#f00';
ctx.stroke();
ctx.beginPath();
ctx.moveTo(200, 200);
ctx.lineTo(300, 200);
ctx.lineTo(300, 300);
ctx.closePath();
// 填充区域
ctx.fillStyle = '#00f';
ctx.fill();
</script>
样式绘制
色彩 Colors
- 填充样式:使用
fillStyle
属性来设置填充的颜色或渐变对象 strokeStyle
是 Canvas 的 API 之一,用于设置描边的颜色或渐变对象。- 默认情况下,线条和填充颜色都是黑色
全局透明度: globalAlpha
属性
- 这个属性影响到 canvas 里所有图形的透明度,来设置全局透明度
- 取值范围为 0 到 1,表示透明度从完全透明到完全不透明的过渡
- 有效的值范围是 0.0(完全透明)到 1.0(完全不透明),默认是 1.0。
线条样式
- 使用
lineWidth
属性来设置线条的宽度- 线宽是指给定路径的中心到两边的粗细。换句话说就是在路径的两边各绘制线宽的一半
- 使用
lineCap
属性来设置线条的端点样式- butt 截断,默认是 butt。
- round 圆形
- square 正方形
- 使用
lineJoin
属性来设置线条的连接样式- round 圆形
- bevel 斜角
- miter 斜槽规,默认是 miter。
- 使用
miterLimit
属性来设置斜接连接的限制比例
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// 设置线条样式
ctx.lineWidth = 5;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.miterLimit = 10;
// 设置全局透明度
ctx.globalAlpha = 0.5;
// 绘制矩形
ctx.fillRect(50, 50, 100, 100);
绘制文本
Canvas 提供了多种 API 来绘制文本,包括 fillText
、strokeText
-
fillText
-
方法接受三个参数:要绘制的文本字符串、文本的 x 坐标和 y 坐标
-
const canvas = document.getElementById('myCanvas'); const ctx = canvas.getContext('2d'); // 设置字体样式 ctx.font = '30px Arial'; // 设置文本颜色 ctx.fillStyle = '#f00'; // 绘制文本 ctx.fillText('Hello, world!', 50, 50);
-
-
strokeText
- 方法接受三个参数:要绘制的文本字符串、文本的 x 坐标和 y 坐标。
文本的样式
-
Canvas 提供了多种 API 来设置文本的样式,包括
font
、textAlign
、textBaseline
-
const canvas = document.getElementById('myCanvas'); const ctx = canvas.getContext('2d'); // 设置字体样式 ctx.font = '30px Arial'; // 设置文本颜色 ctx.fillStyle = '#f00'; // 设置文本对齐方式 ctx.textAlign = 'center'; // 设置文本基线 ctx.textBaseline = 'middle'; // 绘制文本 ctx.fillText('Hello, world!', 200, 100);
绘制图片
Canvas 提供了 drawImage
方法来绘制图片。drawImage
方法接受三个参数:要绘制的图片对象、图片的 x 坐标和 y 坐标。
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// 创建图片对象
const img = new Image();
img.src = 'image.png';
// 等待图片加载完成后再绘制
img.onload = function() {
// 绘制图片
ctx.drawImage(img, 50, 50);
};
需要注意的是,绘制图片时需要等待图片加载完成后再进行绘制,否则可能会出现绘制不成功的情况。另外,绘制图片时需要指定图片的位置,可以使用 drawImage
方法的第二个和第三个参数来指定图片的位置。
图片的来源,canvas 的 API 可以使用下面这些类型中的一种作为图片的源
-
从本地文件获取图片:
-
const img = new Image(); img.src = 'image.png';
-
-
从网络获取图片:
-
const img = new Image(); img.src = 'https://example.com/image.png';
-
-
从视频截图获取图片:
-
const video = document.getElementById('myVideo'); const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); // 在视频播放时截取当前帧的图像 video.addEventListener('play', function() { setInterval(function() { // 将视频当前帧绘制到 Canvas 上 ctx.drawImage(video, 0, 0, canvas.width, canvas.height); // 获取 Canvas 上的图像数据 const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); // 创建图片对象 const img = new Image(); img.src = imageData; // 绘制图片 ctx.drawImage(img, 50, 50); }, 1000 / 30); });
-
绘画状态
在 Canvas 中,可以使用状态保存和恢复 API 来保存和恢复绘画状态。状态包括当前的变换矩阵、剪切区域、样式等等。
每一次 ctx.save
都会将当前的样式存储在一个栈中,恢复的时候先恢复最后加入的
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// 保存绘画状态
ctx.save();
// 设置绘画状态
ctx.fillStyle = '#f00';
ctx.translate(50, 50);
ctx.rotate(Math.PI / 4);
// 绘制矩形
ctx.fillRect(0, 0, 100, 100);
// 恢复绘画状态
ctx.restore();
// 绘制矩形
ctx.fillRect(150, 50, 100, 100);
在上面的示例中,我们首先在 HTML 中创建了一个 Canvas 元素,并设置了它的宽度和高度。然后,在 JavaScript 中,我们获取了 Canvas 元素的上下文对象 ctx
,并使用 Canvas 的 API 设置了绘画状态,包括填充样式、变换矩阵等等。接着,我们使用 save
方法保存了当前的绘画状态,然后绘制了一个矩形。最后,我们使用 restore
方法恢复了之前保存的绘画状态,并绘制了另一个矩形。
Transform
在 Canvas 中,可以使用变换矩阵来对绘制的图形进行变换,包括平移、旋转、缩放等等。Canvas 提供了多个 API 来设置变换矩阵,包括 translate
、rotate
、scale
等等。
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// 绘制矩形
ctx.fillStyle = '#f00';
ctx.fillRect(50, 50, 100, 100);
// 平移矩形
ctx.translate(100, 0);
// 旋转矩形
ctx.rotate(Math.PI / 4);
// 缩放矩形
ctx.scale(0.5, 0.5);
// 绘制矩形
ctx.fillStyle = '#0f0';
ctx.fillRect(50, 50, 100, 100);
注意事项:
- 在做变形之前先调用 save 方法保存状态是一个良好的习惯。
- 大多数情况下,调用 restore 方法比手动恢复原先的状态要简单得多。
- 如果在一个循环中做位移但没有保存和恢复canvas状态,很可能到最后会发现有些东西不见了,因为它很可能已超出canvas画布以外了。
- 形变需要在绘制图形前调用。
绘制动画
canvas可能最大的限制就是图像一旦绘制出来,它就是一直保持那样了,如需要执行动画,不得不对画布上所有图形进行一帧一帧的重绘。
画出一帧动画的基本步骤(如要画出流畅动画,1s 需绘60帧)
- 第一步:用 clearRect 方法清空 canvas ,除非接下来要画的内容会完全充满 canvas(例如背景图),否则你需要清空所有。
- 第二步:保存 canvas 状态,如果加了 canvas 状态的设置(样式,变形之类的),又想在每画一帧之时都是原始状态的话,你需要先保存一下,后面再恢复原始状态。
- 第三步:绘制动画图形(animated shapes) ,即绘制动画中的一帧。
- 第四步:恢复 canvas 状态,如果已经保存了 canvas 的状态,可以先恢复它,然后重绘下一帧。
如果想要更加平稳和更加精准的定时执行某个任务的话,可以使用requestAnimationFrame函数
时钟案例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
.clock {
width: 300px;
height: 300px;
margin: 10px;
border-radius: 50px;
background-color: black;
}
</style>
</head>
<body>
<div class="clock">
<canvas id="tutorial" width="300" height="300">
该浏览器不支持Canvas,请更新你的浏览器
</canvas>
</div>
<script>
window.onload = function () {
var canvas = document.getElementById("tutorial");
if (canvas.getContext) {
var ctx = canvas.getContext("2d");
}
requestAnimationFrame(draw);
function draw() {
ctx.clearRect(0, 0, 300, 300);
ctx.save();
ctx.translate(150, 150); // 移动坐标点(抽取)
// 拿到时间
var time = new Date();
var hours = time.getHours();
var min = time.getMinutes();
var second = time.getSeconds();
drawBg();
drawText();
drawHours(hours, min, second);
drawMinute(hours, min, second);
drawSecond(hours, min, second);
drawCircle();
drawHoursRule();
drawMinuteRule();
ctx.restore();
requestAnimationFrame(draw);
}
function drawBg() {
ctx.save();
// ctx.translate(150, 150); // 移动坐标点(抽取)
ctx.fillStyle = "white";
ctx.beginPath();
ctx.arc(0, 0, 130, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawText() {
ctx.save();
// ctx.translate(150, 150); //(抽取)
ctx.font = "30px fangsong";
ctx.textBaseline = "middle";
ctx.textAlign = "center";
// x = Math.cos( Math.PI * 2 / 12 * i ) * R
// y = Math.sin( Math.PI * 2 / 12 * i ) * R
var timeNumbers = [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 2];
for (var i = 0; i < 12; i++) {
var x = Math.cos(((Math.PI * 2) / 12) * i) * 100;
var y = Math.sin(((Math.PI * 2) / 12) * i) * 100;
ctx.fillText(timeNumbers[i], x, y);
}
ctx.restore();
}
function drawHours(hours, min, second) {
// 3.绘制时针
ctx.save();
ctx.lineWidth = 5;
ctx.lineCap = "round";
// ctx.translate(150, 150); //(抽取)
// 1小时 1分 1 秒 的弧度
// ((Math.PI * 2) / 12) * 1h => 分成 12 份
// ((Math.PI * 2) / 12 / 60) * 1m =>分成 60份
// ((Math.PI * 2) / 12 / 60 / 60) * 1s => 分成 60份
ctx.rotate(
((Math.PI * 2) / 12) * hours +
((Math.PI * 2) / 12 / 60) * min +
((Math.PI * 2) / 12 / 60 / 60) * second
);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, -50);
ctx.stroke();
ctx.restore();
}
function drawMinute(hours, min, second) {
// 4.绘制分针
ctx.save();
ctx.lineWidth = 3;
ctx.lineCap = "round";
// ctx.translate(150, 150); //(抽取)
// 1分 1 秒 的弧度
// ((Math.PI * 2) / 60) * 1m =>分成60份
// ((Math.PI * 2) / 60 / 60) * 1s => 分成 60份
ctx.rotate(
((Math.PI * 2) / 60) * min + ((Math.PI * 2) / 60 / 60) * second
);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, -70);
ctx.stroke();
ctx.restore();
}
function drawSecond(hours, min, second) {
// 5.绘制秒针
ctx.save();
ctx.lineWidth = 2;
ctx.lineCap = "round";
ctx.strokeStyle = "red";
// ctx.translate(150, 150); //(抽取)
// 1 秒 的弧度
// ((Math.PI * 2) / 60) * 1s => 分成 60份
ctx.rotate(((Math.PI * 2) / 60) * second);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, -90);
ctx.stroke();
ctx.restore();
}
function drawCircle() {
// 6.绘制圆环
ctx.save();
// ctx.translate(150, 150);
ctx.beginPath();
ctx.arc(0, 0, 8, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.fillStyle = "gray";
ctx.arc(0, 0, 5, 0, 2 * Math.PI);
ctx.fill();
ctx.restore();
}
function drawHoursRule() {
// 7.绘制时刻度
ctx.save();
ctx.lineWidth = 3;
// ctx.translate(150, 150);
// 时的刻度
for (var j = 0; j < 12; j++) {
ctx.rotate(((Math.PI * 2) / 12) * 1);
ctx.beginPath();
ctx.moveTo(0, -130);
ctx.lineTo(0, -122);
ctx.stroke();
}
ctx.restore();
}
function drawMinuteRule() {
// 8.绘制分刻度
ctx.save();
ctx.lineWidth = 1;
// ctx.translate(150, 150);
// 时的刻度
for (var j = 0; j < 60; j++) {
ctx.rotate(((Math.PI * 2) / 60) * 1);
ctx.beginPath();
ctx.moveTo(0, -130);
ctx.lineTo(0, -125);
ctx.stroke();
}
ctx.restore();
}
};
</script>
</body>
</html>