如何提高 Html5 Canvas 性能

How to Improve Html5 Canvas Performance

所以我一直在做这个项目,它的目标是在二维平面上随机生成地形,并在背景中放置雨水,我选择使用 html5 canvas 元素来实现这个目标。创建它后,我对结果很满意,但我遇到了性能问题,可以使用一些关于如何修复它的建议。到目前为止,我只尝试清除所需的 canvas 位,它位于我在地形下绘制的矩形上方以填充它,但因此我必须重新绘制圆圈。 rn(rain number) 已经降低了大约 2 倍,而且仍然滞后,有什么建议吗?

注意 - 代码片段中的代码不会因为它的体积小而滞后,但如果我要 运行 全屏显示实际降雨量 (800),它会滞后。我有 sh运行k 适合代码段的值。

var canvas = document.getElementById('gamecanvas');
var c = canvas.getContext('2d');

var ma = Math.random;
var mo = Math.round;

var wind = 5;

var rn = 100;
var rp = [];

var tp = [];
var tn;

function setup() {
    
    //fillstyle
    c.fillStyle = 'black';

    //canvas size
    canvas.height = window.innerHeight;
    canvas.width = window.innerWidth;

    //rain setup
    for (i = 0; i < rn; i++) {
        let x = mo(ma() * canvas.width);
        let y = mo(ma() * canvas.width);
        let w = mo(ma() * 1) + 1;
        let s = mo(ma() * 5) + 10;
        rp[i] = { x, y, w, s };
    }

    //terrain setup
    tn = (canvas.width) + 20;
    tp[0] = { x: -2, y: canvas.height - 50 };
    for (i = 1; i <= tn; i++) {
        let x = tp[i - 1].x + 2;
        let y = tp[i - 1].y + (ma() * 20) - 10;
        if (y > canvas.height - 50) {
            y = tp[i - 1].y -= 1;
        }
        if (y < canvas.height - 100) {
            y = tp[i - 1].y += 1;
        }
        tp[i] = { x, y };
        c.fillRect(x, y, 4, canvas.height - y);
    }
}

function gameloop() {

    //clearing canvas
    for (i = 0; i < tn; i++) {
        c.clearRect(tp[i].x - 2, 0, 2, tp[i].y);
    }

    for (i = 0; i < rn; i++) {

        //rain looping
        if (rp[i].y > canvas.height + 5) {
            rp[i].y = -5;
        }
        if (rp[i].x > canvas.width + 5) {
            rp[i].x = -5;
        }

        //rain movement
        rp[i].y += rp[i].s;
        rp[i].x += wind;

        //rain drawing
        c.fillRect(rp[i].x, rp[i].y, rp[i].w, 6);
    }

    for (i = 0; i < tn; i++) {

        //terrain drawing
        c.beginPath();
        c.arc(tp[i].x, tp[i].y, 6, 0, 7);
        c.fill();
    }
}

setup();
setInterval(gameloop, 1000 / 60);
body {
    background-color: white;
    overflow: hidden;
    margin: 0;
}
canvas {
    background-color: white;
}
<html>
<head>
    <link rel="stylesheet" href="index.css">
    <title>A Snowy Night</title>
</head>
<body id="body"> <canvas id="gamecanvas"></canvas>
    <script src="index.js"></script>
</body>
</html>

一般来说,拥有更多的绘画指令成本最高,这些绘画指令的复杂性只有在真的复杂时才会发挥作用。

你在这里用绘画指令向 GPU 发送垃圾邮件:

  • (canvas.width) + 20 调用 clearRect(). clearRect() 绘画指令,而不是一个便宜的。偶尔使用它,但实际上,您应该只使用它来清除整个上下文。
  • 每个雨滴 fillRect()。它们都是相同的颜色,它们可以合并到单个子路径中并在单个绘制调用中绘制。
  • 构成地形的每个圆填充一个。

因此,我们可以只用两次绘制调用来代替大量的绘制调用:

一个 clearRect,一个 fill() 一个包含 drops 和 地形。

然而,将地形和雨水分开肯定更实用,所以让我们通过将地形保留在其自己的 Path2D 对象中来进行三个绘制调用,这对 CPU 更友好:

var canvas = document.getElementById('gamecanvas');
var c = canvas.getContext('2d');

var ma = Math.random;
var mo = Math.round;

var wind = 5;

var rn = 100;
var rp = [];

// this will hold our Path2D object
// which will hold the full terrain drawing
// set a 'let' because we will set it again on resize
let terrain;
var tp = [];
var tn;

function setup() {
    
    //fillstyle
    c.fillStyle = 'black';

    //canvas size
    canvas.height = window.innerHeight;
    canvas.width = window.innerWidth;

    //rain setup
    for (let i = 0; i < rn; i++) {
        let x = mo(ma() * canvas.width);
        let y = mo(ma() * canvas.width);
        let w = mo(ma() * 1) + 1;
        let s = mo(ma() * 5) + 10;
        rp[i] = { x, y, w, s };
    }

    //terrain setup
    tn = (canvas.width) + 20;
    tp[0] = { x: -2, y: canvas.height - 50 };

    terrain = new Path2D();
    for (let i = 1; i <= tn; i++) {
        let x = tp[i - 1].x + 2;
        let y = tp[i - 1].y + (ma() * 20) - 10;
        if (y > canvas.height - 50) {
            y = tp[i - 1].y -= 1;
        }
        if (y < canvas.height - 100) {
            y = tp[i - 1].y += 1;
        }
        tp[i] = { x, y };
        terrain.rect(x, y, 4, canvas.height - y);
        terrain.arc(x, y, 6, 0, Math.PI*2);
    }

}

function gameloop() {

    // clear the whole canvas
    c.clearRect(0, 0, canvas.width, canvas.height);

    // start a new sub-path for the rain
    c.beginPath();
    for (let i = 0; i < rn; i++) {

        //rain looping
        if (rp[i].y > canvas.height + 5) {
            rp[i].y = -5;
        }
        if (rp[i].x > canvas.width + 5) {
            rp[i].x = -5;
        }

        //rain movement
        rp[i].y += rp[i].s;
        rp[i].x += wind;

        //rain tracing
        c.rect(rp[i].x, rp[i].y, rp[i].w, 6);
    }
    // paint all the drops in a single op
    c.fill();
    // paint the whole terrain in a single op
    c.fill(terrain);

    // loop at screen refresh frequency
    requestAnimationFrame(gameloop);
}

setup();
requestAnimationFrame(gameloop);

onresize = () => setup();
body {
  background-color: white;
  overflow: hidden;
  margin: 0;
}
canvas {
  background-color: white;
}
<canvas id="gamecanvas"></canvas>

进一步可能的改进:

  • 不要让我们的地形路径成为一组矩形,只使用 lineTo 来描绘实际轮廓可能会有所帮助,在初始化时进行更多计算,但它只完成一次一会儿。

  • 如果地形变得更复杂,有更多细节,或者有各种颜色和阴影等,那么考虑只绘制一次,然后从 canvas 生成一个 ImageBitmap。然后在 gameLoop 你只需要 drawImage ImageBitmap(绘制位图非常快,但是存储它会消耗内存,所以记得 .close() ImageBitmap 当你不需要它时不再)。

叠加canvas

就像我在评论中建议的那样,使用第二个 canvas 点只需要绘制一次地形,因此它可以通过在每次新绘制时保存重绘来提高动画的性能框架。这可以通过 CSS 将一个放在另一个上(如图层)来完成。

#canvasBase {
  position: relative;
}

#canvasLayer1 {
  position: absolute;
  top: 0;
  left: 0;
}

#canvasLayer2 {
  position: absolute;
  top: 0;
  left: 0;
}

// etc...

另外我建议你使用 requestAnimationFrame over setinterval ().

requestAnimationFrame

但是,通过使用 requestAnimationFrame我们无法控制刷新率,它与客户端硬件相关联。所以我们需要处理它,为此,我们将使用 DOMHighResTimeStamp 作为参数传递给我们的回调方法。

我们的想法是让它以本机速度 运行 并通过仅在需要的时间更新逻辑(我们的计算)来管理 fps。例如,如果我们需要 fps = 60;,这意味着我们需要每隔 1000 / 60 = ~16,67 ms 更新我们的逻辑。因此,我们检查最后一帧时间的 deltaTime 是否等于或优于 ~16,67ms。如果没有足够的时间过去,我们调用一个新的框架,我们 return (重要的是,否则我们刚刚做的控制是无用的,因为无论结果如何,代码都会继续运行) .

let fps = 60;

/* Check if we need to update the logic */
/* if not request a new frame & return */

if(deltaLastUpdate <= 1000 / fps){ // 1000 / 60 = ~16,67ms
  requestAnimationFrame(animate);
  return;
}

正在清算canvas

因为你需要擦除所有过去的雨滴,所以最简单和最便宜的资源是一举清除整个上下文。

ctxRain.clearRect(0, 0, rainCanvas.width, rainCanvas.height);

2D 路径

由于您的绘图使用相同颜色的雨滴,您也可以将所有这些都分组到一个路径中:

rainPath = new Path2D();
...

所以你只需要一条指令来绘制它们(与 clearRect 相同的资源保存类型):

ctxRain.fill(rainPath);

结果

/* CANVAS "Terrain" */
const terrainCanvas = document.getElementById('gameTerrain');
const ctxTerrain = terrainCanvas.getContext('2d');
terrainCanvas.height = window.innerHeight;
terrainCanvas.width = window.innerWidth;

/*  CANVAS "Rain" */
const rainCanvas = document.getElementById('gameRain');
const ctxRain = rainCanvas.getContext('2d');
rainCanvas.height = window.innerHeight;
rainCanvas.width = window.innerWidth;

/* Game Constants */
const wind = 5;
const rainMaxParticules = 100;
const rain = [];
let rainPath;
const terrainMaxParticules = terrainCanvas.width + 20;
const terrain = [];
let terrainPath;

/* Maths help */
const ma = Math.random;
const mo = Math.round;

/* Clear */
function clearTerrain(){
    ctxTerrain.clearRect(0, 0, terrainCanvas.width, terrainCanvas.height);
}
function clearRain(){
    ctxRain.clearRect(0, 0, rainCanvas.width, rainCanvas.height);
}

/* Logic */
function initTerrain(){
    terrain[0] = { x: -2, y: terrainCanvas.height - 50 };
    for (let i = 1; i <= terrainMaxParticules; i++) {
        let x = terrain[i - 1].x + 2;
        let y = terrain[i - 1].y + (ma() * 20) - 10;
        if (y > terrainCanvas.height - 50) {
            y = terrain[i - 1].y -= 1;
        }
        if (y < terrainCanvas.height - 100) {
            y = terrain[i - 1].y += 1;
        }
        terrain[i] = { x, y };
    }
}

function initRain(){
    for (let i = 0; i < rainMaxParticules; i++) {
        let x = mo(ma() * rainCanvas.width);
        let y = mo(ma() * rainCanvas.width);
        let w = mo(ma() * 1) + 1;
        let s = mo(ma() * 5) + 10;
        rain[i] = { x, y, w, s };
    }
}

function init(){
  initTerrain();
  initRain();
}

function updateTerrain(){
    terrainPath = new Path2D();
    for(let i = 0; i < terrain.length; i++){
      terrainPath.arc(terrain[i].x, terrain[i].y, 6, Math.PI/2, 5*Math.PI/2);
    }
    terrainPath.lineTo(terrainCanvas.width, terrainCanvas.height);
    terrainPath.lineTo(0, terrainCanvas.height);
}

function updateRain(){
    rainPath = new Path2D();
    for (let i = 0; i < rain.length; i++) {
      // Rain looping
      if (rain[i].y > rainCanvas.height + 5) {
        rain[i].y = -5;
      }
      if (rain[i].x > rainCanvas.width + 5) {
        rain[i].x = -5;
      }
      // Rain movement
      rain[i].y += rain[i].s;
      rain[i].x += wind;
      
      // Path containing all the drops
      rainPath.rect(rain[i].x, rain[i].y, rain[i].w, 6);
    }
}

/* Drawing */
function drawTerrain(){
    ctxTerrain.fillStyle = 'black';
    ctxTerrain.fill(terrainPath);
}

function drawRain(){
    ctxRain.fillStyle = 'black';    
    ctxRain.fill(rainPath);
}

/* Animation Constant */
const fps = 60;
let lastTimestampUpdate;
let terrainDrawn = false;

/*  Game loop */
function animate(timestamp){

  /* Initialize rain & terrain particules */
  if(rain.length === 0 || terrain.length === 0){
    init();
  }

  /* Define "lastTimestampUpdate" from the first call */
  if (lastTimestampUpdate === undefined){
    lastTimestampUpdate = timestamp;
  }

  /* Check if we need to update the logic & the drawing, if not, request a new frame & return */
  if(timestamp - lastTimestampUpdate <= 1000 / fps){
    requestAnimationFrame(animate);
    return;
  }

  if(!terrainDrawn){
  /* Terrain --------------------- */
  /* Clear */
  clearTerrain();
  /* Logic */  
  updateTerrain();
  /* Draw */
  drawTerrain();
  /* ----------------------------- */
    terrainDrawn = true;
  }
  
  /* --- Rain -------------------- */
  /* Clear */
  clearRain();
  /* Logic  */ 
  updateRain();
  /* Draw */
  drawRain();
  /* ----------------------------- */
    
  /*  Request another frame */
  lastTimestampUpdate = timestamp;
  requestAnimationFrame(animate);

}

/*  Start the animation */
requestAnimationFrame(animate);
body {
    background-color: white;
    overflow: hidden;
    margin: 0;
}

#gameTerrain {
  position: relative;
}

#gameRain {
  position: absolute;
  top: 0;
  left: 0;
}
<body>  
  <canvas id="gameTerrain"></canvas>
  <canvas id="gameRain"></canvas>
</body>


一边

这不会影响性能,但是我鼓励您使用 const & let over var (What's the difference between using “let” and “var”?)。