一、canvas元素
基本概念
Canvas
是由 HTML
代码配合高度和宽度属性而定义出的可绘制区域。JavaScript
代码可以访问该区域,类似于其他通用的二维 API
,通过一套完整的绘图函数来动态生成图形。注意canvas只是一个存放图形的容器。
基本使用
一、 只有宽高两个属性,也可以使用 css
属性来设置宽高,但是如宽高属性和初始比例不一致,会出现扭曲。所以,建议永远不要使用 css
属性来设置 <canvas>
的宽高。
二、在创建 canvas 画布之后,其坐标系如下
其中橘色是画布区域,x, y 坐标轴的原点在画布的左上方,正方向如图:
三、 获取渲染上下文(目前只研究2d)
<body>
<!-- 添加画布 -->
<canvas id="tutorial"></canvas>
</body>
<script>
// 获取画布元素
const canvas = document.getElementById('tutorial');
//获得 2d 上下文对象
const ctx = canvas.getContext('2d');
</script>
为了显示兼容性,代码模板一般为:
<body>
<canvas id="tutorial" width="300" height="300">
这里是我的替代内容,在浏览器不支持canvas时渲染(Internet Explorer 8 及之前的版本)
</canvas>
</body>
<script type="text/javascript">
function draw(){
var canvas = document.getElementById('tutorial');
const ctx = canvas.getContext("2d");
if(!ctx) return;
//开始代码
}
draw();
</script>
二、canvas颜色、样式
-
fillStyle属性
-
作用:
用于设置或返回用于填充绘画的颜色、渐变或模式。
默认值:#000
-
属性值:
值 描述 color 指示绘图填充色的 CSS 颜色值。默认值是 #000000。 gradient 用于填充绘图的渐变对象(线性或 放射性)。 pattern 用于填充绘图的 pattern 对象。
-
-
fillStyle 与strokeStyle
如果想要给图形上色,有两个重要的属性可以做到。
fillStyle = color
设置图形的填充颜色strokeStyle = color
设置图形轮廓的颜色
注意:
- color 可以是表示 css 颜色值的字符串(rgba、颜色名、十六进制字符串等)、渐变对象或者图案对象。
- 默认情况下,线条和填充颜色都是黑色。
- 一旦设置了 strokeStyle 或者 fillStyle 的值,那么这个新值就会成为新绘制的图形的默认值。如果要给每个图形上不同的颜色,需要重新设置 fillStyle 或 strokeStyle 的值。
三、线段
绘制线段
-
基本步骤
下面是需要用到的方法:
-
beginPath()
新建一条路径
-
moveTo(x, y)
把画笔移动到指定的坐标
(x, y)
。相当于设置路径的起始点坐标。 -
lineTo(x, y)
绘制一条上次画笔停下的位置到该位置的线段
-
closePath()
闭合路径之后,图形绘制命令又重新指向到上下文中
-
stroke()
通过线条来绘制图形轮廓
-
fill()
通过填充路径的内容区域生成实心的图形
-
-
实例演示
<body> <canvas id=“tutorial”></canvas> </body> <script> function draw(){ const canvas = document.getElementById("tutorial"); const ctx = canvas.getContext("2d"); if(!ctx) return; else{ // 创建一条新路径 ctx.beginPath(); // 定位到起点 ctx.moveTo(50, 50); // 开始绘制线段 ctx.lineTo(100, 50); // 继续绘制线段 ctx.lineTo(100, 100); // 闭合路径 ctx.closePath(); // 渲染轮廓 ctx.stroke(); // 填充内部 ctx.fill(); } } // 调用 draw(); </script>
属性 | 描述 | 属性值 |
---|---|---|
lineCap | 设置或返回线条的结束端点样式 | 字符串,默认 “butt” |
lineJoin | 设置或返回两条线相交时,所创建的拐角类型 | 字符串,默认"miter" |
lineWidth | 设置或返回当前的线条宽度 | 正值,默认1.0 |
miterLimit | 设置或返回最大斜接长度 | 正值,无默认值 |
-
lineWidth
线宽在其轴线两端各占一半
-
lineCap
线条末端样式。
共有 3 个值:
-
butt
:线段末端以方形结束 -
round
:线段末端以圆形结束 -
square
:线段末端以方形结束,但是增加了一个宽度和线段相同,高度是线段厚度一半的矩形区域。
-
-
lineJoin
同一个 path 内,设定线条与线条间接合处的样式。
共有 3 个值
round
,bevel
和miter
:-
round
通过填充一个额外的,圆心在相连部分末端的扇形,绘制拐角的形状。 圆角的半径是线段的宽度。 -
bevel
在相连部分的末端填充一个额外的以三角形为底的区域, 每个部分都有各自独立的矩形拐角。 -
miter
(默认) 通过延伸相连部分的外边缘,使其相交于一点,形成一个额外的菱形区域。
-
-
miterLimit
miterLimit
属性设置或返回最大斜接长度。斜接长度指的是在两条线交汇处内角和外角之间的距离。
提示:只有当 lineJoin 属性为 “
miter
” 时,miterLimit 才有效。边角的角度越小,斜接长度就会越大。
为了避免斜接长度过长,我们可以使用 miterLimit 属性。
如果斜接长度超过 miterLimit 的值,边角会以 lineJoin 的 “
bevel
” 类型来显示(图解 3):
四、虚线
用 setLineDash
方法和 lineDashOffset
属性来制定虚线样式。
-
setLineDash
方法接受一个数组,来指定线段与间隙的交替; -
lineDashOffset
属性设置起始偏移量。
注意: getLineDash() 返回一个包含当前虚线样式,长度为非负偶数的数组。
function draw(){
var canvas = document.getElementById('tutorial');
if (!canvas.getContext) return;
var ctx = canvas.getContext("2d");
ctx.setLineDash([20, 5]); // [实线长度, 间隙长度]
ctx.lineDashOffset = 5;
ctx.strokeRect(50, 50, 210, 210);
}
draw();
五、弧线
绘制弧线
只是在绘制的过程中有所区别,在创建与渲染时与绘制线段前面一致。
两个方法:
(1)arc(x, y, r, startAngle, endAngle, anticlockwise): 以(x, y)
为圆心,以r
为半径,从 startAngle
弧度开始到endAngle
弧度结束。anticlosewise
是布尔值,true
表示逆时针,false
表示顺时针(默认是顺时针)。
注意:
I. 度数使用弧度制。
II. 0
弧度是指的 x
轴正方向。
radians=(Math.PI/180)*degrees //角度转换成弧度
(2)**arcTo(x1, y1, x2, y2, radius)**
:由三个点加半径控制
注意:
I. 第一个点是此次绘图的起点
II. 第二个点是(x1, y1)
III. 第三个点是(x2, y2)
IV. 其中,(x1, y1)
分别与起点和 (x2, y2)
的连线(射线)是以 radius 为半径的圆的切线,如下图所示:
// 首先绘制一个圆心(150, 150)且半径为150的圆
// 绘制圆弧一
ctx.beginPath();
ctx.moveTo(150, 0);
ctx.arcTo(300, 0, 300, 300, 150);
ctx.stroke();
// 绘制圆弧二
ctx.beginPath();
ctx.moveTo(300, 150);
ctx.arcTo(300, 300, 150, 300, 150);
ctx.stroke();
// 绘制圆弧三
ctx.beginPath();
ctx.moveTo(150, 300);
ctx.arcTo(0, 300, 0, 150, 150);
ctx.stroke();
// 绘制圆弧四
ctx.beginPath();
ctx.moveTo(0, 150);
ctx.arcTo(0, 0, 150, 0, 150);
ctx.stroke();
// 绘制切线圆弧
ctx.beginPath();
ctx.moveTo(150, 0);
ctx.arcTo(300, 0, 300, 150, 50);
ctx.lineTo(300, 150); // 默认是不会连到这里的
ctx.stroke();
六、矩形
<canvas>
只支持一种原生的图形绘制:矩形。所有其他图形都至少需要生成一种路径 (path
)。
canvas 提供了三种方法绘制矩形:
**rect(x, y, width, height)**
:定义一个矩形的路径,但不会立即绘制出矩形,需要 stroke 或 fill 一下。
**fillRect(x, y, width, height)**
:绘制一个填充的矩形。**strokeRect(x, y, width, height)**
:绘制一个矩形的边框。**clearRect(x, y, widh, height)**
:清除指定的矩形区域,然后这块区域会变的完全透明。
**说明:**这 3 个方法具有相同的参数。
- x, y:指的是矩形的左上角的坐标。(相对于canvas的坐标原点)
- width, height:指的是绘制的矩形的宽和高。
七、文本
方法 | 描述 |
---|---|
fillText() | 在画布上绘制 填充文本 |
strokeText() | 在画布上绘制 描边文本(无填充,只有文本的轮廓) |
measureText() | 返回包含指定文本宽度的对象 |
const ctx;
function draw(){
const canvas = document.getElementById('tutorial');
if (!canvas.getContext) return;
ctx = canvas.getContext("2d");
// 设置字体大小和类型
ctx.font = "100px sans-serif"
const txt = "天若有情"
// 绘制填充文本
ctx.fillText(txt, 10, 100);
// 绘制描边文本
ctx.strokeText(txt, 10, 200);
// 获取文本的宽度
ctx.measureText(txt).width;
}
draw();
属性 | 描述 |
---|---|
font | 设置或返回文本内容的当前字体属性 |
textAlign | 设置或返回文本内容的当前对齐方式 |
textBaseline | 设置或返回在绘制文本时使用的当前文本基线 |
-
font = value
当前我们用来绘制文本的样式。这个字符串使用和CSS font
属性相同的语法。 默认的字体是10px sans-serif
。 -
textAlign = value
文本对齐选项。 可选的值包括:start
,end
,left
,right
orcenter
。 默认值是start
。 -
textBaseline = value
基线对齐选项,默认值是alphabetic。
-
"top"
:文本的顶部与指定位置对齐。(英文不紧贴) -
"hanging"
:文本的悬挂部分(如某些字母的顶部装饰)与指定位置对齐。(英文紧贴) -
"middle"
:文本的中间与指定位置对齐。 -
"alphabetic"
:文本的字母基线与指定位置对齐(默认值)。(英文紧贴) -
"ideographic"
:文本的表意文字基线与指定位置对齐,通常用于东亚字符。 -
"bottom"
:文本的底部与指定位置对齐。(英文不紧贴)
-
-
direction = value
文本方向。可能的值包括:ltr
,rtl
,inherit
。默认值是inherit
。
八、图像
绘制步骤:
-
创建 img 实例
-
设置 img 源
-
绘制 img 图像
drawImage()函数大显神威
// 绘制image图像的三个步骤 const img = new Image(); img.src = "地址"; ctx.drawImage(img, 0, 0); // 后面的两个参数是坐标(x, y) // 方式一:考虑到图片可能从网络加载,防止出现异常,采用以下方式 const img = new Image(); img.src = "地址"; img.onload() = function(){ // 省略创建canvas的步骤 ctx.drawImage(img, 0, 0); } // 方式二:直接选中html的img元素,然后赋值给新创建的Image实例对象 const image = document.getElementById("an_image"); image.onload = function(){ const img = new Image(); img = image; ctx.drawImage(img, 0, 0) } //ps:drawImage(图像, x, y, width, height),通过后面宽高两个参数可以实现图像的缩放。
图像的切片
// 同样采用drawImage()函数,不过这次需要九个参数 const targetCanvas = document.getElementById('targetCanvas'); const sourceCanvas = document.getElementById('sourceCanvas'); // 确保这里也有获取sourceCanvas的代码 const sourceCtx = sourceCanvas.getContext('2d'); const targetCtx = targetCanvas.getContext('2d'); // 指定裁剪的源坐标和尺寸 const sx = 50; // 源图像裁剪起始 x 坐标 const sy = 50; // 源图像裁剪起始 y 坐标 const sWidth = 100; // 裁剪区域的宽度 const sHeight = 100; // 裁剪区域的高度 // 指定绘制到目标画布的尺寸 const dWidth = 200; // 目标画布上的图像宽度 const dHeight = 200; // 目标画布上的图像高度 // 进行切片 targetCtx.drawImage( sourceCanvas, sx, sy, sWidth, sHeight, 0, 0, dWidth, dHeight );
九、状态
Canvas
的状态就是当前画面应用的所有样式和变形的一个快照。
状态由保存和恢复两个方法,保存:save();恢复:restore()。两方法均无参数。
关于 save()和restore() :Canvas状态存储在栈中,每当save()方法被调用后,当前的状态就被推送到栈中保存。
每当restore()方法被调用后,就会从栈中取出栈顶的那个状态并将其画布恢复成该状态。
一个绘画状态包括:
- 当前应用的变形(即移动,旋转和缩放)
strokeStyle
,fillStyle
,globalAlpha
,lineWidth
,lineCap
,lineJoin
,miterLimit
,shadowOffsetX
,shadowOffsetY
,shadowBlur
,shadowColor
,globalCompositeOperation
的值- 当前的裁切路径(
clipping path
)
十、转换
-
translate(x, y)
作用:用来移动
canvas
的原点到指定的位置。 在做变形之前先保存状态是一个良好的习惯。大多数情况下,调用
restore
方法比手动恢复原先的状态要简单得多。 -
rotate(angle)
作用:用来旋转坐标轴。
这个方法只接受一个参数:旋转的角度(angle),它是顺时针方向的,以弧度为单位的值。旋转的中心是坐标原点。
-
scale(x, y)
作用:增减图形在
canvas
中的像素数目,对形状,位图进行缩小或者放大。
scale
方法接受两个参数。x,y
分别是横轴和纵轴的缩放因子,它们都必须是正值。值比 1.0 小表示缩 小,比 1.0 大则表示放大,值为 1.0 时什么效果都没有。 -
transform(a, b, c, d, e, f)
作用:实现图形的矩阵变换。
参数解释:
(1)水平缩放因子 a。值大于1表示放大,小于1表示缩小,负值表示水平翻转。
(2)水平错切因子 b。非零值会导致垂直方向上倾斜和拉伸。
(3)垂直错切因子 c。非零值会导致水平方向上倾斜和拉伸。
(4)垂直缩放因子 d。与水平缩放类似,值大于1表示放大,小于1表示缩小,负值表示垂直翻转。
(5)水平移动量 e。这个值表示在应用缩放和错切后,Canvas将沿x轴移动多少。
(6)垂直移动量 f。这个值表示在应用缩放和错切后,Canvas将沿y轴移动多少。
transform
方法不会清除当前路径,但是它会改变之后绘图命令的坐标系。如果你想要保存当前的绘图状态(包括变换矩阵),可以在调用transform
之前使用ctx.save()
,并在之后使用ctx.restore()
来恢复状态。
十一、合成
属性 | 描述 |
---|---|
globalAlpha | 设置或返回绘图的当前 alpha 或透明值 |
globalCompositeOperation | 设置或返回新图像如何绘制到已有的图像上 |
-
globalAlpha
属性属性值必须是介于
0.0
(完全透明) 与1.0
(不透明) 之间的数字。 -
globalCompositeOperation
属性值 描述 source-over 默认。在旧图像上显示新图像。 source-atop 在旧图像顶部显示新图像。新图像位于旧图像之外的部分是不可见的。 source-in 在旧图像中显示新图像。只有旧图像内的新图像部分会显示,旧图像是透明的。 source-out 在旧图像之外显示新图像。只会显示旧图像之外新图像部分,旧图像是透明的。 destination-over 在新图像上方显示旧图像。 destination-atop 在新图像顶部显示旧图像。新图像之外的旧图像部分不会被显示。 destination-in 在新图像中显示旧图像。只有新图像内的旧图像部分会被显示,新图像是透明的。 destination-out 在新图像外显示旧图像。只有新图像外的旧图像部分会被显示,新图像是透明的。 lighter 显示新图像 + 旧图像。 copy 显示新图像。忽略旧图像。 xor 使用异或操作对新图像与旧图像进行组合。
十二、裁剪路径
clip()
作用:从原始画布中剪切任意形状和尺寸。只显示裁剪路径内的区域,裁剪路径外的区域会被隐藏。
–问:关于如何确定裁剪区域呢?
–答:紧挨着的上一次的闭合的绘图区域,如何上次的绘图区域没有闭合,那么系统会自动设置为闭合状态。
注意:一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内(不能访问画布上的其他区域)。也可以在使用 clip() 方法前通过使用 save() 方法对当前画布区域进行保存,并在以后的任意时间对其进行恢复(通过 restore() 方法)
十三、动画
我们看到的都是<script>代码加载完成之后的图像,所以想要在js的循环中实现动画效果是不可能的。
所以一般就使用函数回调的形式实现动画效果。
一般用到以下三个方法:
-
setInterval()
执行多次,效果不如requestAnimationFrame()函数平滑
-
setTimeout()
执行一次
-
requestAnimationFrame()
这个方法在浏览器进行重绘时执行,并且回调后面参数中的那个函数。
动画演示一:
<!-- 绘制简易的星球运转图(仅包括太阳、地球和月亮)-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>星球公转演示</title>
</head>
<body>
<canvas id="solar_system" width="300px" height="300px"></canvas>
<canvas id="clock" width="300px" height="300px"></canvas>
</body>
<script>
// 声明变量
const sun = new Image();
const earth = new Image();
const moon = new Image();
// 初始化函数,
function init() {
// 获取画布
const canvas = document.querySelector("#solar");
ctx = canvas.getContext("2d");
// 判断浏览器是否支持(能否正确获取到)
if(ctx){
return;
}else{
// 初始化需要的图片
sun.src = "images/sun.png";
earth.src = "images/earth.png";
moon.src = "images/moon.png";
}
}
// 绘制函数
function draw() {
// 空所有的内容
ctx.clearRect(0, 0, 300, 300);
// 绘制太阳
ctx.drawImage(sun, 0, 0, 300, 300);
// 保存初始状态
ctx.save();
ctx.translate(150, 150);
// 制earth轨道
ctx.beginPath();
ctx.strokeStyle = "rgba(255,255,0,0.5)";
ctx.arc(0, 0, 100, 0, 2 * Math.PI)
ctx.stroke()
const time = new Date();
// 制地球
ctx.rotate(2 * Math.PI / 60 * time.getSeconds() + 2 * Math.PI / 60000 * time.getMilliseconds())
ctx.translate(100, 0);
ctx.drawImage(earth, -18, -18, 36, 36)
// 制月球轨道
ctx.beginPath();
ctx.strokeStyle = "rgba(255,255,255,0.3)";
ctx.arc(0, 0, 40, 0, 2 * Math.PI);
ctx.stroke();
// 制月球
ctx.rotate(2 * Math.PI / 6 * time.getSeconds() + 2 * Math.PI / 6000 * time.getMilliseconds());
ctx.translate(40, 0);
ctx.drawImage(moon, -6, -6, 12, 12);
ctx.restore();
requestAnimationFrame(draw);
}
// 调用
init();
// 可以使用Promise进行三张图片是否加载成功的判断,这里为了演示简单,就直接调用
draw();
</script>
</html>
动画演示二:
<!-- 绘制时钟演示 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>时钟</title>
</head>
<body>
<canvas id="clock" width="300px" height="300px"></canvas>
</body>
<script>
// 获取时钟画布
const clock = document.getElementById("clock");
const ctx_clock = clock.getContext("2d");
// 绘制时钟
// 变换原点
ctx_clock.translate(150, 150);
// 初始化表盘
function init_clock() {
// 保存当前状态
ctx_clock.save();
// 绘制表盘线条
for (let i = 1; i <= 60; i++) {
// 每个小时要有5个间隔
ctx_clock.rotate(Math.PI / 30);
// 整点处的线条与两个整点之间的线条应有区别
if (i % 5 == 0) {
ctx_clock.strokeStyle = "rgb(255,0,0)";
ctx_clock.lineWidth = 3;
} else {
ctx_clock.strokeStyle = "rgb(0,0,255)";
ctx_clock.lineWidth = 1.5;
}
ctx_clock.beginPath();
ctx_clock.moveTo(100, 0);
ctx_clock.lineTo(120, 0);
ctx_clock.stroke();
}
// 绘制表盘圆周;
ctx_clock.strokeStyle = "rgb(0,0,0)";
ctx_clock.lineWidth = 1;
ctx_clock.beginPath();
ctx_clock.arc(0, 0, 130, 0, 2 * Math.PI);
ctx_clock.stroke();
// 恢复状态
ctx_clock.restore();
}
// 绘制指针
function draw_hands() {
ctx_clock.save();
// 清除画布
ctx_clock.clearRect(-150, -150, 300, 300);
// 旋转x轴,使其正方向直线12
ctx_clock.rotate(3 * Math.PI / 2);
// 初始化表盘
init_clock();
// 获取当前时间
const time = new Date();
// 绘制时针
// 保存
ctx_clock.save();
const hours = time.getHours() % 12; // 转换为12小时制
ctx_clock.rotate(2 * Math.PI / 12 * hours + 2 * Math.PI / 12 / 60 * time.getMinutes());
ctx_clock.lineWidth = 3;
ctx_clock.beginPath();
ctx_clock.moveTo(-5, 0);
ctx_clock.lineTo(60, 0);
ctx_clock.stroke();
// 恢复
ctx_clock.restore();
// 绘制分针
// 保存表盘方向
ctx_clock.save();
ctx_clock.strokeStyle = "green";
ctx_clock.lineWidth = 2;
ctx_clock.rotate(2 * Math.PI / 60 * time.getMinutes() + 2 * Math.PI / 60 / 60 * time.getSeconds());
ctx_clock.beginPath();
ctx_clock.moveTo(-5, 0);
ctx_clock.lineTo(75, 0);
ctx_clock.stroke();
// 恢复
ctx_clock.restore();
// 绘制秒针
// 保存
ctx_clock.save();
ctx_clock.strokeStyle = "red";
ctx_clock.rotate(2 * Math.PI / 60 * time.getSeconds());
ctx_clock.beginPath();
ctx_clock.moveTo(-5, 0);
ctx_clock.lineTo(90, 0);
ctx_clock.stroke();
// 恢复
ctx_clock.restore();
// 恢复最初始
ctx_clock.restore();
requestAnimationFrame(draw_hands);
}
draw_hands();
</script>
</html>