HTML系列 -- Canvas

758 阅读6分钟

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 参考手册

绘制文字

image.png

基本使用

  1. 设置字体的样式、粗细、字号、字体系列
ctx.font="30px Arial";

语法:ctx.font = "font-style font-variant font-weight font-size/line-height font-family"

image.png

  1. 通过设置 ctx.fillStyle = "color",调用 ctx.fillText(str,startX,startY) 方法进行绘制
ctx.fillStyle = "blue" / "#0000ff"
ctx.fillText("Hello World",10,50);

image.png

解决字体大小自适应问题(设置自适应单位)

解决思路:

  1. 调用获取设备宽度/高度的接口
  2. 根据宽度/高度去计算出一个自适应单位
  3. 以这个自适应单位设置字体大小

解决文本换行问题(分多次绘制)

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}
}
  1. 设置 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 绘制的文本要是超出规定的范围,就不会绘制了,这个时候我们最好就是用省略号来代替被隐藏的文本

解决思路:

  1. 计算绘制区域总共最多能画几个字(包括全部行)
  2. if ... else ... 判断是否超出,如果超出使用 splice(start)(第二个参数不写)从超出部分开始截取后面的全部字符
  3. 接上 ... 省略号(记得预留放置省略号的空间)

效果图如下:(左图文本不溢出,右图文本溢出)

绘制图像

image.png

基本使用

  1. 规定图像来源
var img = new Image()
img.src = "flower.png"
cxt.drawImage(img,0,0);
  1. 调用 ctx.drawImage() 方法进行绘制
context.drawImage(img,sx,sy,swidth,sheight,x,y,width,height);

其中 img(图像src来源)、x、y 都是必填的:

image.png

性能优化

  1. 比如有多次选择边框的需求: 项目中有实现给头像添加边框的功能:当用户选择多种边框后,canvas 会进行绘制,因而才能展示给用户(预览功能),所以如果这样的话,每次都会重复地绘制处于下方的头像,这就造成不必要的重复绘制;
  • 优化方式: 采用 分层画布 的思路,把绘制头像的 canvas 放在底部,然后在另外一个 canvas 绘制边框,然后叠在这个 canvas 之上,因此就不用重复绘制处于下方的头像了,性能也就提升上来了
  1. 比如制作像全民打飞机那样的游戏: 举个例子:我们通过手指触摸或键盘输入或鼠标操作控制飞机移动,可不必每次都 ctx.drawImage() 重复地把飞机绘制出来
  • 优化方式: 我们可以采用更佳的方式 ctx.putImageData() 通过操作像素点来把已经绘制的部分拷贝过去,然后把原来那份清理掉,这样就实现了优化的效果。同时背景图片放在另外的 canvas 中去绘制,也是采用 分层绘制 的方式进行性能优化。

绘制内容模糊问题出现的原因是什么?(像素点占用:1个占2格)

出现模糊的问题实际上是显示器的问题,比如有的设备:屏幕的物理像素(640px)是屏幕宽度(320px)的两倍。物理像素 / 设备分辨率 = 设备像素比。在浏览器全局对象中就有了这样一个属性 —— devicePixelRatio 就是指的设备像素比。

  • 物理像素:手机屏幕截图后图片的尺寸大小即可看出
  • 屏幕像素:只跟手机屏幕的尺寸大小有关 物理像素比屏幕像素大得多是因为:显示得更加的高清。所以屏幕显示内容之前有 缩小 的过程!
  1. 所以我们用 canvas 进行绘制的时候
  2. 我们绘制的 1px 的点实际上是通过两个像素点来控制的
  3. 所以一个像素点按理说各占两个像素点的一半
  4. 由于不存在 0.5 个像素点,所以会把另一半给补上,所以造成 1px 实际上是 2 个像素点
  5. 视觉上就会变得模糊(先补充放大,后缩小显示,所以模糊)

绘制内容模糊问题怎么解决?(画布跟着屏幕内容同步缩小显示)

思路:

放大 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);

高级操作 —— 像素操作

image.png

// 拷贝对应像素点的内容
<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绘是通过不断的更新帧从而达到动画的效果

image.gif

绘制方法如下:

  1. 有 for 循环
  2. 在 for 循环里面重复地调用 ctx 进行绘制
  3. 循环里头用 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.gif

这个时候就可以使用分层画布,画画在一个上层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. 尽量使用整数坐标而不是浮点数

  • 原因:浏览器为了达到抗锯齿的效果会做额外的运算,会造成一定的性能损耗
  • 解决:尽量使用整数坐标而非浮点数坐标【整数坐标计算更加简便】