• 作者:老汪软件技巧
  • 发表时间:2024-10-13 10:03
  • 浏览量:

最近做AGV和无人叉车,涉及到2D地图绘制,所以系统性地总结这块。

Canvas

DP是物理像素;DIP是逻辑像素,也就是多少寸;DRP=DP/DIP DRP一般大于等于1

canvas 2d使用的是CPU处理

… canvas介绍

优化 HTML5 Canvas 的性能

具体实现方法包括:

动静分离:将画面内容根据变动频率拆分成动态和静态部分。静态部分在不需要更新时,可以减少渲染次数,动态部分则实时渲染1。分块处理:将大画布拆分成多个小块,每个小块独立渲染。这样可以减少单个画布的负载,提高渲染效率2。图片分割:对于图片内容,可以将大图拆分成多个小图,分别加载和渲染。这种方法适用于图片密集的应用场景,如拼图游戏等。图层分离(Layering)/分层 Canvas/拆分canvas:

为了减少重绘面积,可以将不同类型的内容绘制到不同的 Canvas 元素上。例如,将背景、游戏角色和 UI 元素分别绘制到不同的画布上。

"backgroundCanvas" width="800" height="600">
"spriteCanvas" width="800" height="600">
"uiCanvas" width="800" height="600">

分层 Canvas 的出发点是:

区域重绘/脏区重绘(Dirty Rectangles) :

改变某一部分区域,比如一部分动画:

方法1:拆分canvas

方法2:脏矩形渲染 (clip方法可以绘制矩阵,也可以绘制圆吧?)【得到脏矩形,得到脏矩形中相交和里面的元素,清除再重绘】

脏矩形渲染简单来说,就是计算被改变的目标图形两帧所产生的包围盒(脏矩形),将该区域清空,然后将和脏矩形发生相交的所有图形在这个区域内重绘。

对于前面移动红球的场景,具体逻辑为:

计算红球在当前帧和下一帧所形成的包围盒,这个包围盒就是脏矩形。遍历绿球的物理信息,计算它们的包围盒,取出和脏矩形发生相交的蓝球。将脏矩形区域清空。将脏矩形设置为裁剪区域,这样保证只能绘制在脏矩形中。(clip())按顺序绘制蓝球,最后绘制红球。按顺序是为了保证层级正确。

相比全部绘制,局部绘制能有效减少需要绘制的图形数量,减少对 GPU 绘制指令的调用,从而提高渲染性能。

这里还有个优化点,就是减少遍历的图形数量,可以使用 四叉树碰撞检测 来做优化。

只重绘实际发生变化的区域,而不是整个画布。这可以极大地提高渲染效率。

1、找出这一帧变化的矩形区域进行清除;

2、利用canvas的api实现脏区重绘。

实现脏区重绘非常简单,只需要在全部绘制前加那么一段clip,这个一般用于实现遮罩。简单使用clip虽然性能优化不是太明显,但还是有20%的提升的。

for (var i = 0; i < dirtyRectList.length; i++) { 
    var rect = dirtyRectList[i]; 
    ctx.clearRect(rect.x, rect.y, rect.width, rect.height);   //先清除
} 
ctx.beginPath();
for (var i = 0; i < dirtyRectList.length; i++) {
    var rect = dirtyRectList[i];
    ctx.rect(rect.x, rect.y, rect.width, rect.height);  //再重绘
}
ctx.clip();  //加上这句

再举例说明

function draw() {
    context.clearRect(dirtyX, dirtyY, dirtyWidth, dirtyHeight);   //先清除
    context.drawImage(someObject, dirtyX, dirtyY);   //再重绘
    // 其他绘制操作
}

/developer/a… 具体可以查看这个

… 有实例

codesandbox.io/p/sandbox/1… 实例代码

分块/拆分处理

// 假设我们有一个大数组需要在canvas中处理
let data = new Array(10000).fill(null).map(() => Math.random());
 
// 每块的大小
let chunkSize = 100;
 
// 分块处理数组
function processDataInChunks(array, size, processFunction) {
    for (let i = 0; i < array.length; i += size) {
        let chunk = array.slice(i, i + size);
        processFunction(chunk, i);
    }
}
 
// 处理每个小块的函数
function drawChunk(chunk, index) {
    // 假设我们在这里绘制canvas
    // 获取canvas上下文
    let canvas = document.getElementById('myCanvas');
    let ctx = canvas.getContext('2d');
 
    // 绘制每个数据点
    for (let i = 0; i < chunk.length; i++) {
        let x = index + i;
        let y = chunk[i] * 100; // 假设我们将数据缩放到100
        ctx.fillRect(x, y, 1, 1);
    }
}
 
// 开始分块处理
processDataInChunks(data, chunkSize, drawChunk);

/qq_37377764… 实例

离屏 Canvas(Offscreen Canvas)

复杂的图形可以先绘制到离屏的 Canvas,然后再将其内容绘制到主画布上。这样可以减少复杂图形的重新计算和绘制。

var offscreenCanvas = document.createElement('canvas');
var offscreenContext = offscreenCanvas.getContext('2d');
// 在离屏 canvas 上绘制
offscreenContext.drawImage(someComplexObject, 0, 0);
// 复制到主 canvas
context.drawImage(offscreenCanvas, 0, 0);

const canvas  = document.getElementById('canvas');
const offscreen = canvas.transferControlToOffscreen();
我写了下面这个小demo 验证下到底是不是可靠的
 
  const canvas  = document.getElementById('canvas');
  // 离屏canvas 
  const offscreen1 = new OffscreenCanvas(200, 200);
  const offscreen2 = canvas.transferControlToOffscreen();
  console.error(offscreen1,offscreen2, '222') 

离屏canvas也不能用的太泛滥,如果用太多离屏canvas也会有性能问题

离屏绘制,使用缓存

绘制同样的一块区域,如果数据源是一张大图上的一部分,性能就会比较差,因为每一次绘制还包含了裁剪工作。可以先把待绘制的区域裁剪好,保存起来,这样每次绘制时就能轻松很多。

drawImage 方法的第一个参数不仅可以接收 Image 对象,也可以接收另一个 Canvas 对象。而且,使用 Canvas 对象绘制的开销与使用 Image 对象的开销几乎完全一致。我们只需要实现将对象绘制在一个未插入页面的 Canvas 中,然后每一帧使用这个 Canvas 来绘制。

使用 Web Worker

使用 Web Worker,在另一个线程里进行计算。

将任务拆分为多个较小的任务,插在多帧中进行。

Web Worker 是好东西,性能很好,兼容性也不错。用另一个线程来运行 Worker 中的 JavaScript 代码,完全不会阻碍主线程的运行。动画(尤其是游戏)中难免会有一些时间复杂度比较高的算法,用 Web Worker 来运行再合适不过了。

图片和资源的预加载

在应用启动时预先加载所有需要用到的图片和资源,避免在绘制过程中出现延迟。

var image = new Image();
image.onload = function() {
    context.drawImage(image, 0, 0);
};
image.src = 'path/to/image.png';

减少状态切换

尽量减少对 Canvas 绘制状态(如颜色、线宽)的修改,因为这些状态切换也会带来性能开销。

梳理的拼音__梳理的英文

context.fillStyle = '#000'; // 只设置一次,而不是重复设置

6. 使用适当的数据结构:

使用合适的数据结构来管理和更新需要绘制的对象,例如四叉树或空间分区算法,以减少需要重绘对象的数量。

硬件加速

使用 CSS transform 和硬件加速技术,可以提高某些场景下的渲染性能。比如:

canvas {
    transform: translateZ(0); // 触发硬件加速
}

尽量使用 Path2D

如果你的应用程序涉及很多路径操作,使用 Path2D 对象而不是每次都重建路径,可以提高性能。

var path = new Path2D();
path.moveTo(10, 10);
path.lineTo(100, 100);
context.stroke(path);

循环刷新 canvas :requestAnimationFrame 相对于 setinterval 处理动画更优

定时器循环刷新 canvas ,比如动画,游戏,优势在于:

canvas scale() 天然支持放大缩小,不用逻辑处理

尽量少调用 canvasAPI ,尽可能集中绘制

提升 canvas 的性能最主要的还是得注意代码的结构,减少不必要的API调用,在每一帧中减少复杂的运算或者把复杂运算由每一帧算一次改成数帧算一次。

循环的时候,只赋值一次,不边循坏边赋值。

避免使用高耗能的 API避免「阻塞」

偶尔的且较小的阻塞是可以接收的,频繁或较大的阻塞是不可以接受的。也就是说,我们需要解决两种阻塞:

频繁(通常较小)的阻塞。其原因主要是过高的渲染性能开销,在每一帧中做的事情太多。

较大(虽然偶尔发生)的阻塞。其原因主要是运行复杂算法、大规模的 DOM 操作等等。

对前者,我们应当仔细地优化代码,比如分层canvas。

而对于后者,主要有以下两种优化的策略。

使用 Web Worker,在另一个线程里进行计算。

将任务拆分为多个较小的任务,插在多帧中进行。

Web Worker 是好东西,性能很好,兼容性也不错。用另一个线程来运行 Worker 中的 JavaScript 代码,完全不会阻碍主线程的运行。动画(尤其是游戏)中难免会有一些时间复杂度比较高的算法,用 Web Worker 来运行再合适不过了。

不要频繁设置绘图上下文的 font 属性

API:putImageData和getImageData

getImageData 方法:

含义: 从 Canvas 元素上获取特定矩形区域的像素数据。

语法: context.getImageData(sx, sy, sw, sh)

返回值: 一个 ImageData 对象,其包含指定区域内所有像素的数据(包括 red, green, blue 和 alpha 四个通道,每个通道 8 位表示)。

示例:

var canvas = document.getElementById('myCanvas');
var context = canvas.getContext('2d');
// 获取位置 (10, 10) 开始, 宽50,高 50 的像素数据
var imageData = context.getImageData(10, 10, 50, 50);

2. putImageData 方法: putImageData( imgData , x , y , dirtyX , dirtyY , dirtyWidth , dirtyHeight );

1.  **含义**: 将图像数据放回到 `Canvas` 元素上指定位置。
2.  **语法**: `context.putImageData(imageData, dx, dy)`
    *   `imageData`: 一个包含图像数据的 `ImageData` 对象。
    *   `dx`: 目标位置的左上角 x 坐标。
    *   `dy`: 目标位置的左上角 y 坐标。
3.  **可选参数**:
    *   `dirtyX`, `dirtyY`, `dirtyWidth`, `dirtyHeight`: 用于指定只更新 `ImageData` 对象的特定区域,而不是整个对象。

示例:

var canvas = document.getElementById('myCanvas');
var context = canvas.getContext('2d');
// 像之前一样获取一块 50x50 的区域数据
var imageData = context.getImageData(10, 10, 50, 50);
// 将该数据放回其他位置,如 (100, 100)
context.putImageData(imageData, 100, 100);

/developer/i… 拆分

/post/708348…

API: globalCompositeOperation和globalAlpha

求交集并集等

API:save()和restore()

要重绘实际发生变化的区域而不是整个画布,你需要使用save()和restore()方法来管理Canvas的状态。save()可以保存当前的绘图状态,而restore()可以恢复之前保存的状态。

// 获取Canvas元素并创建上下文
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
 
// 绘制一个蓝色的矩形
ctx.fillStyle = 'blue';
ctx.fillRect(50, 50, 100, 100);
 
// 保存当前绘图状态
ctx.save();
 
// 现在我们要改变绘图样式为红色,但不影响之前的蓝色矩形
ctx.fillStyle = 'red';
ctx.fillRect(150, 50, 100, 100);
 
// 重绘蓝色矩形的一部分,不影响红色矩形
// 先把ctx恢复到最初的状态,即蓝色绘图状态
ctx.restore();
// 重绘蓝色矩形的一部分
ctx.fillRect(70, 70, 30, 30);

save()保存当前上下文的状态。

restore()

返回之前保存的路径状态和属性。

Canvas模拟图层(Layers)效果使用多个元素模拟图层

创建多个元素。每个画布元素可以视为一个独立的图层,它们可以通过CSS的定位属性进行重叠和定位。

在单个元素上模拟图层

在单个元素上使用绘制顺序来模拟图层效果。通过控制绘制的顺序,后绘制的元素会覆盖先绘制的元素,从而实现图层效果。

/cnds123/art…