原文来自我的个人博客
前言
先直接展示下最终效果,代码已上传至码上掘金
本章将会主要介绍关于 Canvas
的基础知识,看完之后应该就能理解最终的代码了。
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 可用于动画、游戏画面、数据可视化、图片编辑以及实现视频处理等方面。
1.1 浏览器兼容性
Canvas
的浏览器兼容性还是不错的,能兼容 e9
及其以上版本
1.2 Canvas 的优点:
Canvas
提供的功能更原始,适合像素处理,动态渲染和数据量大的绘制,如:图片编辑、热力图、炫光尾迹特效等。Canvas
非常适合图像密集的游戏开发,适合频繁重绘许多的对象。Canvas
能够以.png
或.jpg
格式保存结果图片,适合对图像进行像素级的处理。
1.3 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(10,10, 100, 50) // 单位也是不用写 px
}
</script>
效果:
代码解析:
忽略做兼容性的几行代码,上面的代码最终通过 ctx.fillRect(10,10,100,50)
在坐标为 (10,10)
的位置,绘制了一个长 100
宽 50
的实心矩形(默认为黑色)
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()
函数前调用。
一旦设置了 strokeStyle
或者 fillStyle
的值,那么这个新值就会成为新绘制的图形的默认值。
如果你要给图形上不同的颜色,你需要重新设置 fillStyle
或 strokeStyle
的
3.2 透明度 Transparent
除了可以绘制实色图形,我们还可以用 canvas 来绘制半透明的图形。
1. 方式一:strokeStyle
和 fillStyle
属性结合 RGBA
:
// 指定透明颜色,用于描边和填充样式
ctx.strokeStyle = "rgba(255,0,0,0.5)";
ctx.fillStyle = "rgba(255,0,0,0.5)";
2. 方式二: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()
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
个则是定义切片的目标显示位置和大小。(剪切)
- 第一个参数和其它的是相同的,都是一个图像或者另一个
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
方法可让我们任意放置图形,而不需要手工一个个调整坐标值。
- 如不使用
移动矩形案例一:形变( 没有保存状态)
<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. 实现太阳系动画代码
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 = "https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/640b4df0a5074e9a9bf4777fdf1fd74e~tplv-k3u1fbpfcp-watermark.image";
let earth = new Image();
earth.src = "https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f4ad71733e934b818a52bcfea56a683f~tplv-k3u1fbpfcp-watermark.image";
let moon = new Image();
moon.src = "https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/05bc3992bd5044448f029b7d68049b38~tplv-k3u1fbpfcp-watermark.image";
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();
}
};