Canvas
定义
一块矩形画布,可以控制其中的每一像素
原理
通过控制每一个像素进行绘制,通过每一帧的绘制及更新达到动画的效果
准备
HTML
<canvas id="myCanvas" width="300" height="200"></canvas>
JS
<script>
var canvas = document.getElementById('myCanvas') //【DOM获取canvas节点】
var ctx = canvas.getContext('2d'); //【绘制2D图像】
</script>
理解
拿到一个`CanvasRenderingContext2D`对象,赋值给ctx
CanvasRenderingContext2D对象`有很多绘制方法`
例子
HTML先建立Canvas元素
<canvas id="myCanvas" width="200" height="100" style="border:1px solid #c3c3c3;">
Your browser does not support the canvas element. // 作为浏览器不支持Canvas时的提示
</canvas>
由于我们项目中主要用到的是绘制文字和绘制图像,所以这里主要讲的是这两个,其他内容的绘制参考HTML Canvas 参考手册
绘制文字
基本使用
- 设置字体的样式、粗细、字号、字体系列
ctx.font="30px Arial";
语法:ctx.font = "font-style font-variant font-weight font-size/line-height font-family"
- 通过设置
ctx.fillStyle = "color",调用ctx.fillText(str,startX,startY)方法进行绘制
ctx.fillStyle = "blue" / "#0000ff"
ctx.fillText("Hello World",10,50);
解决字体大小自适应问题(设置自适应单位)
解决思路:
- 调用获取设备宽度/高度的接口
- 根据宽度/高度去计算出一个自适应单位
- 以这个自适应单位设置字体大小
解决文本换行问题(分多次绘制)
Canvas 绘制文本不会自动换行,所以需要我们自定义方法去处理这个问题 代码如下:
function text(str) {
// 画布总宽度 px单位
let canvasWidth = wx.getSystemInfoSync().windowWidth * wx.getSystemInfoSync().pixelRatio * 0.9
// 字体大小 px单位
let fontSize = parseInt((wx.getSystemInfoSync().windowWidth/375) * 30)
// 每行所需字数 = 画布总宽度 / 单个字体大小
let rowFontNum = Math.floor(canvasWidth / fontSize)
// 字符串总长度
let strLength = str.length
// 所需行数 = 字符总长度 / 每行所需字数
let rows = Math.ceil(strLength / rowFontNum)
// 返回所需行数和每行所能放置的字数
return {rowFontNum,rows}
}
- 设置 for 循环的循环次数为行数;每次循环都调用
slice(start,end)方法去截取要绘制的字符串的一部分
let result = this.text(string)
for (let i = 0; i < result.rows; i++) {
ctx.fillText(string.slice(result.rowFontNum * i,result.rowFontNum * (i+1)),20,630 + i*70)
}
假设经过自定义函数 text 的计算后得到要绘制的文本 string 是 3 × 22 + 5,也就是说,总共有 4 行,每行最多放了 22 个字,其中有 3 行是放满的,最后一行只有 5 个字。所以会经历 4 次循环:
string.slice(0,22)
string.slice(22,44)
string.slice(44,66)
string.slice(66,88)
成功实现了换行,效果图:
解决文本溢出问题(字符串截取)
Canvas 绘制的文本要是超出规定的范围,就不会绘制了,这个时候我们最好就是用省略号来代替被隐藏的文本
解决思路:
- 计算绘制区域总共最多能画几个字(包括全部行)
- if ... else ... 判断是否超出,如果超出使用
splice(start)(第二个参数不写)从超出部分开始截取后面的全部字符 - 接上 ... 省略号(记得预留放置省略号的空间)
效果图如下:(左图文本不溢出,右图文本溢出)
绘制图像
基本使用
- 规定图像来源
var img = new Image()
img.src = "flower.png"
cxt.drawImage(img,0,0);
- 调用
ctx.drawImage()方法进行绘制
context.drawImage(img,sx,sy,swidth,sheight,x,y,width,height);
其中 img(图像src来源)、x、y 都是必填的:
性能优化
- 比如有多次选择边框的需求: 项目中有实现给头像添加边框的功能:当用户选择多种边框后,canvas 会进行绘制,因而才能展示给用户(预览功能),所以如果这样的话,每次都会重复地绘制处于下方的头像,这就造成不必要的重复绘制;
- 优化方式: 采用 分层画布 的思路,把绘制头像的 canvas 放在底部,然后在另外一个 canvas 绘制边框,然后叠在这个 canvas 之上,因此就不用重复绘制处于下方的头像了,性能也就提升上来了
- 比如制作像全民打飞机那样的游戏:
举个例子:我们通过手指触摸或键盘输入或鼠标操作控制飞机移动,可不必每次都
ctx.drawImage()重复地把飞机绘制出来
- 优化方式:
我们可以采用更佳的方式
ctx.putImageData()通过操作像素点来把已经绘制的部分拷贝过去,然后把原来那份清理掉,这样就实现了优化的效果。同时背景图片放在另外的 canvas 中去绘制,也是采用 分层绘制 的方式进行性能优化。
绘制内容模糊问题出现的原因是什么?(像素点占用:1个占2格)
出现模糊的问题实际上是显示器的问题,比如有的设备:屏幕的物理像素(640px)是屏幕宽度(320px)的两倍。物理像素 / 设备分辨率 = 设备像素比。在浏览器全局对象中就有了这样一个属性 —— devicePixelRatio 就是指的设备像素比。
- 物理像素:手机屏幕截图后图片的尺寸大小即可看出
- 屏幕像素:只跟手机屏幕的尺寸大小有关 物理像素比屏幕像素大得多是因为:显示得更加的高清。所以屏幕显示内容之前有 缩小 的过程!
- 所以我们用 canvas 进行绘制的时候
- 我们绘制的 1px 的点实际上是通过两个像素点来控制的
- 所以一个像素点按理说各占两个像素点的一半
- 由于不存在 0.5 个像素点,所以会把另一半给补上,所以造成 1px 实际上是 2 个像素点
- 视觉上就会变得模糊(先补充放大,后缩小显示,所以模糊)
绘制内容模糊问题怎么解决?(画布跟着屏幕内容同步缩小显示)
思路:
放大 canvas 的画布尺寸,但是 canvas 显示尺寸 不变
而 canvas 的设计的时候正好有对象的属性来分别管理画布尺寸和显示尺寸:
- canvas 的 width、height 属性用于管理 画布尺寸;
- canvas 的 style 属性中的 width、height 用于管理 显示尺寸;(或者用 CSS 来控制)
若已知设备的设备像素比
我们直接通过在 HTML、CSS 中将 画布尺寸 和 显示尺寸 写死
// devicePixelRatio = 2
// 在 2k 显示器上的 demo,3k、4k进行自行换算
<style>
canvas {
width: 200px;
height: 200px;
}
</style>
<canvas id="canvas" width="400" height="400"></canvas>
若未知设备的设备像素比
我们通过 JavaScript 同时控制 canvas 的 画布尺寸 和 显示尺寸:
devicePixelRatio = window.devicePixelRatio || 1,
backingStoreRatio = context.webkitBackingStorePixelRatio || 1,
ratio = devicePixelRatio / backingStoreRatio;
var w = $("#code").width();
var h = $("#code").height();
//要将 canvas 的宽高设置成容器宽高的 2 倍
var canvas = document.createElement("canvas");
canvas.width = w * ratio; // 画布尺寸
canvas.height = h * ratio;
canvas.style.width = w + "px"; // 显示尺寸
canvas.style.height = h + "px";
var context = canvas.getContext("2d");
//然后将画布缩放,将图像放大两倍画到画布上
context.scale(ratio,ratio);
高级操作 —— 像素操作
// 拷贝对应像素点的内容
<script>
ctx.fillStyle = "green";
ctx.fillRect(10, 10, 50, 50);
function copy() {
var imgData = ctx.getImageData(0, 0, 60, 60); //获取起点坐标(0,0)宽高为(60,60)区域的画布内容
ctx.putImageData(imgData, 60, 60); // 将获取到的画布内容拷贝到起点坐标为(60,60)的位置
}
</script>
高级操作 —— 动画绘制
注意理解本质:canvas绘是通过不断的更新帧从而达到动画的效果
绘制方法如下:
- 有 for 循环
- 在 for 循环里面重复地调用 ctx 进行绘制
- 循环里头用 i 来作参数
<script type="text/javascript">
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
var i = 0;
function move()
{
ctx.fillStyle = 'red';
ctx.fillRect(i,i,50,50);
i++;
if (i==350)
{
i=0;
ctx.clearRect(0,0,400,400);
}
}
setInterval("move()",10); // 借助 setInterval() 进行动画绘制
</script>
像动画、缩放、各种滤镜和像素转换等高级操作,但元素太多、操作复杂时,canvas性能下降,需要优化
优化方案
1. 分层画布
多个相互重叠的canvas根据变化程度分开渲染,越复杂的场景越适合
举个例子:一个画板,比如这样一个简单的画圆,可以看到有原来的绘画的痕迹,那就要 渲染没画圆之前的画布 -> 再画圆,鼠标移动时会清空整块画布去画圆,意味着曲线也会被清空,所以需要不断的重复执行这个渲染过程
这个时候就可以使用分层画布,画画在一个上层canvas-1上,原来绘画痕迹在下层canvas-2上,那么画圆的过程就是清空 -> 画圆,画好之后再放到下层canvas-2上,这样就不需要去渲染之前的画布,就能节约性能开销
<canvas id="canvas-1"></canvas>
<canvas id="canvas-2"></canvas>
2. 离屏渲染
举个例子,想用Canvas实现一个人挥手的动作,基于Canvas的原理是每一帧每一帧的绘制然后更新来达到这个效果,显然前前后后要绘制很多帧相似度极高的图片,所以我们采用离屏渲染的方式来进行优化
- 实现原理 屏幕内创建一块canvas-1画布画除了手以外的部分(不变的部分),在屏幕外再创建另一块canvas-2画布画挥手的部分(变化的部分),将canvas-2绘制的一帧帧画面通过drawImage()的方式拷贝到canvas-1上,相比于只在canvas-1上面画整个画面要快很多
- 注意事项 并不是说离屏渲染就一定可以提升性能,如果说整个画面每一帧都在变化,那这时非离屏渲染要比离屏渲染好,因为拷贝的过程(也就是drawImage时)也相应地会造成性能损耗
3. 背景图片如果不变的话可以直接用图片放到最底层
如果像大多数游戏那样,你有一张静态的背景图,用一个静态的<div>元素,结合background 特性或<img>标签,将它置于画布元素之后。这么做可以避免在每一帧在画布上绘制大图。
4. 尽量使用整数坐标而不是浮点数
- 原因:浏览器为了达到抗锯齿的效果会做额外的运算,会造成一定的性能损耗
- 解决:尽量使用整数坐标而非浮点数坐标【整数坐标计算更加简便】