canvas 入门 canvas优化

1,455 阅读6分钟

一、canvas应用场景:

1.游戏:很多页面H5游戏都是用canvas绘制出来的。

2.图表

二、如何使用canvas

1.首先在html中创建一个canvas元素,告诉浏览器我要开始绘图了。

 <canvas id="myCanvas" width="500" height="500"></canvas>

2.获得canvas元素

var canvas =document.getElementById('myCanvas')

3.获得canvas的上下文

 var ctx = canvas.getContext('2d');

因为是在平面绘图,所以是2d而不是3d。

  • 这里需要区分两个对象:元素对象和上下文

  • 元素对象是canvas元素,相当于我们的画布

  • 上下文对象时通过getContext(‘2d’)获取的

  • context 是一个状态机。你可以改变 context 的若干状态,而几乎所有的渲染操作,最终的效果与 context 本身的状态有关系。比如,调用 strokeRect 绘制的矩形边框,边框宽度取决于 context 的状态 lineWidth,而后者是之前设置的。

4、canvas模糊

有时候我们的canvas绘制出来的图片放到html 会失真, 这个时候把画布调大, 然后放到html 中就不会出现模糊了。

由此我们可以看出来,canvas其实是位图,它会出现失真的现象。

三、绘制图案

绘制形状

1.线段 moveTo——lineTo

  • 其中moveTo就是将画笔移动到某一个点

  • lineTo就是从刚才移动到的点画到另外一个点。

  • 不过canvas绘制还需要多两步:
    beginPath() // 新建一条路径,生成之后,图形绘制命令被指向到路径上生成路径
    clothPath() // 闭合路径之后图形绘制命令又重新指向到上下文中

重点:

1.fill和stroke方法都是作用在当前的所有子路径 fill 通过填充路径的内容区域生成实心的图形 stroke 通过线条来绘制图形轮廓。

当你调用fill()函数时,所有没有闭合的形状都会自动闭合,所以你不需要调用closePath()函数。但是调用stroke()时不会自动闭合

2.完成一条路径之后要开启另一条路径必须要beginPath()

  ctx.lineWidth = 10;
  ctx.fillStyle = "yellow";
  ctx.beginPath();
  ctx.moveTo(50,50);
  ctx.lineTo(100,100);
  ctx.closePath();
  ctx.fill(); // 通过填充路径的内容区域生成实心的图形

  ctx.strokeStyle = 'red';
  ctx.stroke();//通过线条来绘制图形轮廓。

2.矩形

  矩形也有三种方法:

  1.rect(x,y,width,height);

  2.fillRect(x,y,width,height);

  3.strokeRect(x,y,width,height)
  
  4.clearRect(x, y, width, height)
  -   清除指定矩形区域,让清除部分完全透明。

  由于第一种没有给路径上色,所以我们一般使用后两种,第二个是绘制并填充矩形,第三个是绘制并描边

3.弧形

  arc(x, y, r, 起始弧度,结束弧度,弧形的方向 ) 其中0是顺时针 1是逆时针

  arc方法将当前点和弧形和起点用一条直线连接

  arcTo(x1, y1, x2, y2, r)

6.渐变

 createLinearGradient(x1, y1, x2, y2);线性渐变
 

7. 二次贝塞尔曲线及三次贝塞尔曲线

  • quadraticCurveTo(cp1x, cp1y, x, y)

  • 绘制二次贝塞尔曲线,cp1x,cp1y为一个控制点,x,y为结束点。

  • bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)

  • 绘制三次贝塞尔曲线,cp1x,cp1y为控制点一,cp2x,cp2y为控制点二,x,y为结束点。

image.png

二次贝塞尔曲线有一个开始点(蓝色)、一个结束点(蓝色)以及一个控制点(红色),而三次贝塞尔曲线有两个控制点。

    // 二次贝塞尔曲线 对话气泡
    ctx.beginPath();
    ctx.moveTo(75, 25);
    ctx.quadraticCurveTo(25, 25, 25, 62.5);
    ctx.quadraticCurveTo(25, 100, 50, 100);
    ctx.quadraticCurveTo(50, 120, 30, 125);
    ctx.quadraticCurveTo(60, 120, 65, 100);
    ctx.quadraticCurveTo(125, 100, 125, 62.5);
    ctx.quadraticCurveTo(125, 25, 75, 25);
    ctx.stroke();
    
    // 三次贝塞尔曲线 绘制心形
    ctx.beginPath();
    ctx.moveTo(75, 40);
    ctx.bezierCurveTo(75, 37, 70, 25, 50, 25);
    ctx.bezierCurveTo(20, 25, 20, 62.5, 20, 62.5);
    ctx.bezierCurveTo(20, 80, 40, 102, 75, 120);
    ctx.bezierCurveTo(110, 102, 130, 80, 130, 62.5);
    ctx.bezierCurveTo(130, 62.5, 130, 25, 100, 25);
    ctx.bezierCurveTo(85, 25, 75, 37, 75, 40);
    ctx.fill();

image.png image.png

8、 clip 裁剪路径

  • 绘制图形 有stroke 和 fill 方法,这里介绍第三个方法clip

    将当前正在构建的路径转换为当前的裁剪路径。

我们使用 clip()方法来创建一个新的裁切路径。

默认情况下,canvas 有一个与它自身一样大的裁切路径(也就是没有裁切效果)

function draw() {
  var ctx = document.getElementById('canvas').getContext('2d');
  ctx.fillRect(0,0,150,150);
  ctx.translate(75,75);

  // Create a circular clipping path
  ctx.beginPath();
  ctx.arc(0,0,60,0,Math.PI*2,true);
  ctx.clip();

  // draw background
  var lingrad = ctx.createLinearGradient(0,-75,0,75);
  lingrad.addColorStop(0, '#232256');
  lingrad.addColorStop(1, '#143778');

  ctx.fillStyle = lingrad;
  ctx.fillRect(-75,-75,150,150);

  // draw stars
  for (var j=1;j<50;j++){
    ctx.save();
    ctx.fillStyle = '#fff';
    ctx.translate(75-Math.floor(Math.random()*150),
                  75-Math.floor(Math.random()*150));
    drawStar(ctx,Math.floor(Math.random()*4)+2);
    ctx.restore();
  }

}
function drawStar(ctx,r){
  ctx.save();
  ctx.beginPath()
  ctx.moveTo(r,0);
  for (var i=0;i<9;i++){
    ctx.rotate(Math.PI/5);
    if(i%2 == 0) {
      ctx.lineTo((r/0.525731)*0.200811,0);
    } else {
      ctx.lineTo(r,0);
    }
  }
  ctx.closePath();
  ctx.fill();
  ctx.restore();
}

裁切路径创建之后所有出现在它里面的东西才会画出来。在画线性渐变时我们就会注意到这点。然后会绘制出50 颗随机位置分布(经过缩放)的星星,当然也只有在裁切路径里面的星星才会绘制出来。

image.png

四、性能优化

1·、在离屏 canvas上预渲染 相似的图形或者重复的对象

发现自己在每个动画帧上重复了一些相同的绘制操作,请考虑将其分流到屏幕外的画布上。 然后,您可以根据需要频繁地将屏幕外图像渲染到主画布上,而不必首先重复生成该图像的步骤.

myEntity.offscreenCanvas = document.createElement("canvas");
myEntity.offscreenCanvas.width = myEntity.width;
myEntity.offscreenCanvas.height = myEntuty.heihgt;
myEntity.offscreenContext = myEntity.offscreenCanvas.getContext("2d");
...

myEntity.render(myEntity.offscreenContext);

2、避免浮点数坐标点 发生子像素渲染, 用整数替代

ctx.drawImage(myImage, 0.3, 0.5);
浏览器为了达到抗锯齿的效果会做额外的运算。为了避免这种情况,请保证在你调用drawImage,用Math.floor()对坐标取整

3、在离屏canvas中缓存图片的不同尺寸时候 不要在用drawImage时缩放图像

4、使用多层画布去画一个复杂的场景

某些对象需要经常移动或更改 假设您有一个游戏,其UI位于顶部,中间是游戏性动作,底部是静态背景。 在这种情况下,您可以将游戏分成三个<canvas>层。 UI将仅在用户输入时发生变化,游戏层随每个新框架发生变化,并且背景通常保持不变。

<div id="stage">
   <canvas id="ui-layer" width="" height=""/>
   <canvas id="game-layer" width="" height=""/>
   <canvas id="backgground-layer" width="" height=""/>
<div/>
<style>
  #stage {
    width: 480px;
    height: 320px;
    position: relative;
    border: 2px solid black
  }
  canvas { position: absolute; }
  #ui-layer { z-index: 3 }
  #game-layer { z-index: 2 }
  #background-layer { z-index: 1 }
</style>

5、可以避免在每一帧在画布上绘制大图 用CSS设置大的背景图 将它置于画布元素之后

6、 用CSS transforms特性缩放画布 CSS transforms使用GPU,速度更快

最好的情况是不直接缩放画布,或者具有较小的画布并按比例放大,而不是较大的画布并按比例缩小。

7、 如果你的游戏使用画布而且不需要透明 关闭透明度 帮助浏览器进行内部优化

var ctx = canvas.getContext('2d', { alpha: false });

8、有动画,请使用window.requestAnimationFrame() 而非window.setInterval()

浏览器可以优化并行的动画动作,更合理的重新排列动作序列,并把能够合并的动作放在一个渲染周期内完成,从而呈现出更流畅的动画效果。比如,通过requestAnimationFrame()JS动画能够和CSS动画/变换SVG SMIL动画同步发生。另外,如果在一个浏览器标签页里运行一个动画,当这个标签页不可见时,浏览器会暂停它,这会减少CPU,内存的压力,节省电池电量。

9. 避免大量计算造成阻塞, 可以使用 Worker 和拆分任务的方法避免复杂算法阻塞动画运行

10、 将画布的函数调用集合到一起, 不要频繁调度beginPath, closePath, stroke,fill,同时减少调用canvas的api。

for (var i = 0; i < points.length - 1; i++) {
  var p1 = points[i];
  var p2 = points[i+1];
  context.beginPath();
  context.moveTo(p1.x, p1.y);
  context.lineTo(p2.x, p2.y);
  context.stroke();
}
====>
context.beginPath();
for (var i = 0; i < points.length - 1; i++) {
  var p1 = points[i];
  var p2 = points[i+1];
  context.moveTo(p1.x, p1.y);
  context.lineTo(p2.x, p2.y);
}
context.stroke();

11、尽量少改变CANVAS状态机

  for (var i = 0; i < STRIPES; i++) {
          context.fillStyle = (i % 2 ? COLOR1 : COLOR2);
          context.fillRect(i * GAP, 0, GAP, 480);
      } 
      
   ====> 
  context.fillStyle = COLOR1;
      for (var i = 0; i < STRIPES / 2; i++) {
          context.fillRect((i * 2) * GAP, 0, GAP, 480);
      }
      context.fillStyle = COLOR2;
      for (var i = 0; i < STRIPES / 2; i++) {
          context.fillRect((i * 2 + 1) * GAP, 0, GAP, 480);
      }

12、局部重绘 渲染画布中的不同点,而非整个新状态

由于 Canvas 的绘制方式是画笔式的,在 Canvas 上绘图时每调用一次 API 就会在画布上进行绘制,一旦绘制就成为画布的一部分。绘制图形时并没有对象保存下来,一旦图形需要更新,需要清除整个画布重新绘制 Canvas 局部刷新的方案:

  1. 清除指定区域的颜色,并设置 clip(不了解可以看上面的clip 介绍)
-   clip() 确定绘制的的裁剪区域,区域之外的图形不能绘制,详情查看 CanvasRenderingContext2D.clip()
-   clearRect(*x**y**width**height*) 擦除指定矩形内的颜色,查看 CanvasRenderingContext2D.clearRect()
  1. 所有同这个区域相交的图形重新绘制

13、尽可能避免 shadowBlur特性 阴影渲染的性能开销通常比较高

14、尽可能避免text rendering

// 一个填充文本的示例
function draw() {
  var ctx = document.getElementById('canvas').getContext('2d');
  ctx.font = "48px serif";
  ctx.fillText("Hello world", 10, 50);
}
function draw() {
  var ctx = document.getElementById('canvas').getContext('2d');
  ctx.font = "48px serif";
  ctx.strokeText("Hello world", 10, 50);
}

15、请谨慎使用大型物理库

16、尝试不同的方法来清除画布(clearRect() vs. fillRect() vs. 调整canvas大小)

性能依次提升。