ChatGPT解决这个技术问题 Extra ChatGPT

在 HTML5 画布中设置单个像素的最佳方法是什么?

HTML5 Canvas 没有明确设置单个像素的方法。

可以使用非常短的线设置像素,但是抗锯齿和线帽可能会干扰。

另一种方法可能是创建一个小的 ImageData 对象并使用:

context.putImageData(data, x, y)

将其放置到位。

任何人都可以描述一种有效且可靠的方法吗?


P
Phrogz

有两个最佳竞争者:

创建一个1×1的图像数据,设置颜色,把ImageData放在位置:var id = myContext.createImageData(1,1); // 每页只执行一次 var d = id.data; // 每页只执行一次 d[0] = r; d[1] = g; d[2] = b; d[3] = 一个; myContext.putImageData(id, x, y);使用 fillRect() 绘制像素(应该没有锯齿问题): ctx.fillStyle = "rgba("+r+","+g+","+b+","+(a/255)+")" ; ctx.fillRect(x, y, 1, 1);

您可以在此处测试它们的速度:http://jsperf.com/setting-canvas-pixel/9 或此处 https://www.measurethat.net/Benchmarks/Show/1664/1

我建议对您关心的浏览器进行测试,以获得最大速度。截至 2017 年 7 月,fillRect() 在 Firefox v54 和 Chrome v59 (Win7x64) 上的速度提高了 5-6 倍。

其他更愚蠢的选择是:

在整个画布上使用 getImageData()/putImageData();这比其他选项慢约 100 倍。

使用数据 url 创建自定义图像并使用 drawImage() 显示它: var img = new Image; img.src = "数据:图像/png;base64," + myPNGEncoder(r,g,b,a); // 编写 PNGEncoder 留给读者作为练习

创建另一个填充了您想要的所有像素的 img 或画布,并使用 drawImage() 仅对您想要的像素进行 blit。这可能会非常快,但存在您需要预先计算所需像素的限制。

请注意,我的测试不会尝试保存和恢复画布上下文 fillStyle;这会降低 fillRect() 的性能。另请注意,我并不是从零开始,也不是为每个测试测试完全相同的像素集。


如果可以提交错误报告,我会再给你+10! :)
请注意,在我的带有 GPU 和图形驱动程序的机器上,fillRect() 最近比 Chromev24 上的 1x1 putimagedata 快了近 10 倍。所以...如果速度很关键并且您了解您的目标受众,请不要相信过时的答案(即使是我的)。而是:test!
请更新答案。在现代浏览器上,填充方法要快得多。
“编写 PNGEncoder 留给读者作为练习”让我大笑起来。
为什么我找到的所有出色的 Canvas 答案都恰好来自您? :)
U
Uwe Keim

尚未提及的一种方法是使用 getImageData,然后使用 putImageData。
此方法适用于您想一次性快速绘制很多内容的情况。
http://next.plnkr.co/edit/mfNyalsAR2MWkccr

var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var canvasWidth = canvas.width;
var canvasHeight = canvas.height;
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
var id = ctx.getImageData(0, 0, canvasWidth, canvasHeight);
var pixels = id.data;

var x = Math.floor(Math.random() * canvasWidth);
var y = Math.floor(Math.random() * canvasHeight);
var r = Math.floor(Math.random() * 256);
var g = Math.floor(Math.random() * 256);
var b = Math.floor(Math.random() * 256);
var off = (y * id.width + x) * 4;
pixels[off] = r;
pixels[off + 1] = g;
pixels[off + 2] = b;
pixels[off + 3] = 255;

ctx.putImageData(id, 0, 0);

@Alnitak 因为无法读懂你的想法而给我一个否定,这很低..其他人可能会来到这里希望能够绘制许多像素。我做了然后记住了更有效的方法,所以分享了它。
当戳大量像素时,对于计算每个像素或类似的图形演示,这是一种明智的方法。它比对每个像素使用 fillRect 快十倍。
是的,例外的答案说这种方法比其他方法慢 100 倍,这总是让我感到烦恼。如果您的绘图少于 1000,这可能是正确的,但从那里开始,此方法开始获胜,然后屠杀其他方法。这是一个测试用例.... measurethat.net/Benchmarks/Show/8386/0/…
t
thedayturns

我没有考虑过 fillRect(),但答案促使我将其与 putImage() 进行基准比较。

在(旧)MacBook Pro 上使用 Chrome 9.0.597.84 在随机位置放置 100,000 个随机颜色的像素,使用 putImage() 需要不到 100 毫秒,但使用 fillRect() 需要将近 900 毫秒。 (http://pastebin.com/4ijVKJcC 处的基准代码)。

相反,如果我在循环之外选择一种颜色并在随机位置绘制该颜色,则 putImage() 需要 59 毫秒,而 fillRect() 需要 102 毫秒。

似乎在 rgb(...) 语法中生成和解析 CSS 颜色规范的开销是造成大部分差异的原因。

另一方面,将原始 RGB 值直接放入 ImageData 块不需要字符串处理或解析。


我添加了一个 plunker,您可以在其中单击一个按钮并测试每个方法(PutImage、FillRect)以及 LineTo 方法。它表明 PutImage 和 FillRect 在时间上非常接近,但 LineTo 非常慢。在以下位置查看:plnkr.co/edit/tww6e1VY2OCVY4c4ECy3?p=preview 它基于您出色的 pastebin 代码。谢谢。
对于那个 plunker,我看到 PutImage 比 FillRect 稍慢(在最新的 Chrome 63 上),但是在我尝试 LineTo 之后,PutImage 比 FillRect 快得多。不知何故,他们似乎在干扰。
S
Sebastian Ortmann
function setPixel(imageData, x, y, r, g, b, a) {
    var index = 4 * (x + y * imageData.width);
    imageData.data[index+0] = r;
    imageData.data[index+1] = g;
    imageData.data[index+2] = b;
    imageData.data[index+3] = a;
}

变量索引 = (x + y * imageData.width) * 4;
应该在该函数之后调用 putImageData() 还是上下文将通过引用更新?
M
Matheus Dias de Souza

看起来很奇怪,但是 HTML5 支持画线、圆、矩形和许多其他基本形状,它没有任何适合绘制基本点的东西。这样做的唯一方法是用你拥有的任何东西来模拟点。

所以基本上有3种可能的解决方案:

将点画成一条线

将点绘制为多边形

将点画成圆

他们每个人都有自己的缺点

线

function point(x, y, canvas){
  canvas.beginPath();
  canvas.moveTo(x, y);
  canvas.lineTo(x+1, y+1);
  canvas.stroke();
}

请记住,我们正在向东南方向绘制,如果这是边缘,则可能会出现问题。但你也可以画在任何其他方向。

长方形

function point(x, y, canvas){
  canvas.strokeRect(x,y,1,1);
}

或者以更快的方式使用fillRect,因为渲染引擎只会填充一个像素。

function point(x, y, canvas){
  canvas.fillRect(x,y,1,1);
}

圆圈

圆圈的问题之一是引擎更难渲染它们

function point(x, y, canvas){
  canvas.beginPath();
  canvas.arc(x, y, 1, 0, 2 * Math.PI, true);
  canvas.stroke();
}

与使用填充可以实现的矩形相同的想法。

function point(x, y, canvas){
  canvas.beginPath();
  canvas.arc(x, y, 1, 0, 2 * Math.PI, true);
  canvas.fill();
}

所有这些解决方案的问题:

很难跟踪您要绘制的所有点。

当你放大时,它看起来很丑。

如果您想知道“绘制点的最佳方法是什么?”,我会选择填充矩形。你可以看到我的jsperf here with comparison tests


东南方向?什么?
D
Daniel

由于不同的浏览器似乎更喜欢不同的方法,也许在加载过程中对所有三种方法进行较小的测试以找出最好使用的方法,然后在整个应用程序中使用它是有意义的?


s
sdleihssirhc

长方形呢?这一定比创建 ImageData 对象更有效。


您会这样认为,它可能是针对单个像素的,但如果您预先创建图像数据并设置 1 个像素,然后使用 putImageData,则它比 Chrome 中的 fillRect 快 10 倍。 (有关更多信息,请参阅我的答案。)
e
erik

像 sdleihssirhc 说的那样画一个矩形!

ctx.fillRect (10, 10, 1, 1);

^-- 应该在 x:10, y:10 处绘制一个 1x1 矩形


C
Community

嗯,您也可以只制作一条长度为 1 像素的 1 像素宽的线,并使其方向沿单个轴移动。

            ctx.beginPath();
            ctx.lineWidth = 1; // one pixel wide
            ctx.strokeStyle = rgba(...);
            ctx.moveTo(50,25); // positioned at 50,25
            ctx.lineTo(51,25); // one pixel long
            ctx.stroke();

我将像素绘制实现为 FillRect、PutImage 和 LineTo,并在以下位置创建了一个 plunker:plnkr.co/edit/tww6e1VY2OCVY4c4ECy3?p=preview 检查一下,因为 LineTo 的速度呈指数级增长。可以在 0.25 秒内使用其他 2 种方法完成 100,000 个点,但使用 LineTo 完成 10,000 个点需要 5 秒。
好的,我犯了一个错误,我想关闭循环。 LineTo 代码缺少一个 - 非常重要的行 - 如下所示: ctx.beginPath();我更新了 plunker(在我其他评论的链接上)并添加了一行现在允许 LineTo 方法在 0.5 秒内平均生成 100,000 个。相当惊人。因此,如果您要编辑答案并将该行添加到您的代码中(在 ctx.lineWidth 行之前),我会支持您。我希望你觉得这很有趣,我为我原来的错误代码道歉。
K
KANJICODER

快速 HTML 演示代码:基于我对 SFML C++ 图形库的了解:

将其保存为带有 UTF-8 编码的 HTML 文件并运行它。随意重构,我只是喜欢使用日文变量,因为它们简洁且不占用太多空间

您很少会想要设置一个任意像素并将其显示在屏幕上。所以使用

PutPix(x,y, r,g,b,a) 

方法将许多任意像素绘制到后台缓冲区。 (廉价电话)

然后当准备好显示时,调用

Apply() 

方法来显示更改。 (昂贵的电话)

完整的 .HTML 文件代码如下:

<!DOCTYPE HTML >
<html lang="en">
<head>
    <title> back-buffer demo </title>
</head>
<body>

</body>

<script>
//Main function to execute once 
//all script is loaded:
function main(){

    //Create a canvas:
    var canvas;
    canvas = attachCanvasToDom();

    //Do the pixel setting test:
    var test_type = FAST_TEST;
    backBufferTest(canvas, test_type);
}

//Constants:
var SLOW_TEST = 1;
var FAST_TEST = 2;


function attachCanvasToDom(){
    //Canvas Creation:
    //cccccccccccccccccccccccccccccccccccccccccc//
    //Create Canvas and append to body:
    var can = document.createElement('canvas');
    document.body.appendChild(can);

    //Make canvas non-zero in size, 
    //so we can see it:
    can.width = 800;
    can.height= 600;

    //Get the context, fill canvas to get visual:
    var ctx = can.getContext("2d");
    ctx.fillStyle = "rgba(0, 0, 200, 0.5)";
    ctx.fillRect(0,0,can.width-1, can.height-1);
    //cccccccccccccccccccccccccccccccccccccccccc//

    //Return the canvas that was created:
    return can;
}

//THIS OBJECT IS SLOOOOOWW!
// 筆 == "pen"
//T筆 == "Type:Pen"
function T筆(canvas){


    //Publicly Exposed Functions
    //PEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPE//
    this.PutPix = _putPix;
    //PEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPE//

    if(!canvas){
        throw("[NilCanvasGivenToPenConstruct]");
    }

    var _ctx = canvas.getContext("2d");

    //Pixel Setting Test:
    // only do this once per page
    //絵  =="image"
    //資  =="data"
    //絵資=="image data"
    //筆  =="pen"
    var _絵資 = _ctx.createImageData(1,1); 
    // only do this once per page
    var _筆  = _絵資.data;   


    function _putPix(x,y,  r,g,b,a){
        _筆[0]   = r;
        _筆[1]   = g;
        _筆[2]   = b;
        _筆[3]   = a;
        _ctx.putImageData( _絵資, x, y );  
    }
}

//Back-buffer object, for fast pixel setting:
//尻 =="butt,rear" using to mean "back-buffer"
//T尻=="type: back-buffer"
function T尻(canvas){

    //Publicly Exposed Functions
    //PEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPE//
    this.PutPix = _putPix;
    this.Apply  = _apply;
    //PEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPE//

    if(!canvas){
        throw("[NilCanvasGivenToPenConstruct]");
    }

    var _can = canvas;
    var _ctx = canvas.getContext("2d");

    //Pixel Setting Test:
    // only do this once per page
    //絵  =="image"
    //資  =="data"
    //絵資=="image data"
    //筆  =="pen"
    var _w = _can.width;
    var _h = _can.height;
    var _絵資 = _ctx.createImageData(_w,_h); 
    // only do this once per page
    var _筆  = _絵資.data;   


    function _putPix(x,y,  r,g,b,a){

        //Convert XY to index:
        var dex = ( (y*4) *_w) + (x*4);

        _筆[dex+0]   = r;
        _筆[dex+1]   = g;
        _筆[dex+2]   = b;
        _筆[dex+3]   = a;

    }

    function _apply(){
        _ctx.putImageData( _絵資, 0,0 );  
    }

}

function backBufferTest(canvas_input, test_type){
    var can = canvas_input; //shorthand var.

    if(test_type==SLOW_TEST){
        var t筆 = new T筆( can );

        //Iterate over entire canvas, 
        //and set pixels:
        var x0 = 0;
        var x1 = can.width - 1;

        var y0 = 0;
        var y1 = can.height -1;

        for(var x = x0; x <= x1; x++){
        for(var y = y0; y <= y1; y++){
            t筆.PutPix(
                x,y, 
                x%256, y%256,(x+y)%256, 255
            );
        }}//next X/Y

    }else
    if(test_type==FAST_TEST){
        var t尻 = new T尻( can );

        //Iterate over entire canvas, 
        //and set pixels:
        var x0 = 0;
        var x1 = can.width - 1;

        var y0 = 0;
        var y1 = can.height -1;

        for(var x = x0; x <= x1; x++){
        for(var y = y0; y <= y1; y++){
            t尻.PutPix(
                x,y, 
                x%256, y%256,(x+y)%256, 255
            );
        }}//next X/Y

        //When done setting arbitrary pixels,
        //use the apply method to show them 
        //on screen:
        t尻.Apply();

    }
}


main();
</script>
</html>

y
yoniLavi

为了完成 Phrogz 非常彻底的答案,fillRect()putImageData() 之间存在关键区别。
第一个使用上下文通过添加一个矩形(不是像素)来上绘制,使用fillStyle alpha值和上下文< i>globalAlpha 和 变换矩阵line caps 等。
第二个替换整个像素集(也许是一个,但为什么?)
结果与您在 jsperf 上看到的不同。


没有人想一次设置一个像素(意思是在屏幕上绘制它)。这就是为什么没有特定的 API 可以做到这一点(这是正确的)。
性能方面,如果目标是生成图片(例如光线追踪软件),您总是希望使用由 getImageData() 获得的数组,它是一个优化的 Uint8Array。然后您使用 setTimeout/seTInterval 调用 putImageData() 一次或每秒几次。


我有一个案例,我想在图像中放置 100k 块,但不是以 1:1 像素比例。使用 fillRect 很痛苦,因为 Chrome 的硬件加速无法处理它需要的对 GPU 的单独调用。我最终不得不使用 1:1 的像素数据,然后使用 CSS 缩放来获得所需的输出。它很丑 :(
在 Firefox 42 上运行链接的基准测试我得到的 get/putImageData 只有 168 Ops/sec,但 fillRect 是 194,893。 1x1 image data 是 125,102 次操作/秒。所以 fillRect 在 Firefox 中获胜。因此,从 2012 年到今天,情况发生了很大变化。与往常一样,永远不要依赖旧的基准测试结果。
我想一次设置一个像素。我猜这个问题的标题其他人也这样做
M
Martin Ždila

如果您担心速度,那么您也可以考虑使用 WebGL。


P
Potherca

纯粹出于诊断目的,我使用这个简单的功能。

笔记。如果不使用整数坐标,则生成的图像会模糊。

setPixel (context, 100, 100, 'blue');

function setPixel (ctx, x, y, c) {

//  integer coordinates are required.

    ctx.save ();
    ctx.fillStyle = c;
    ctx.fillRect (x, y, 1, 1);
    ctx.restore ();

}

K
Kamil Kiełczewski

快速方便

以下类实现了 this article 中描述的快速方法并包含您需要的所有内容:readPixelputPixel、获取 width/height。调用 refresh() 方法后类更新画布。示例解决 2d wave equation 的简单情况

类屏幕{构造函数(canvasSelector){ this.canvas = document.querySelector(canvasSelector); this.width = this.canvas.width; this.height = this.canvas.height; this.ctx = this.canvas.getContext('2d'); this.imageData = this.ctx.getImageData(0, 0, this.width, this.height); this.buf = new ArrayBuffer(this.imageData.data.length); this.buf8 = new Uint8ClampedArray(this.buf); this.data = new Uint32Array(this.buf); } // r,g,b,a - red, gren, blue, alpha 分量在 0-255 范围内 putPixel(x,y,r,g,b,a=255) { this.data[y * this.width + x] = (a<<24) | (b<<16) | (g<<8) | r; } readPixel(x,y) { 让 p= this.data[y * this.width + x] 返回 [p&0xff, p>>8&0xff, p>>16&0xff, p>>>24]; } refresh() { this.imageData.data.set(this.buf8); this.ctx.putImageData(this.imageData, 0, 0); } } // -------- // 测试 // -------- let s= new Screen('#canvas'); // 初始化函数 draw() { for (var y = 1; y < s.height-1; ++y) { for (var x = 1; x < s.width-1; ++x) { let a = [[1,0],[-1,0],[0,1],[0,-1]].reduce((a,[xp,yp])=> a+= s.readPixel(x+ xp,y+yp)[0] // 读取像素 ,0);让 v= a/1.99446-tmp[x][y]; tmp[x][y]=v<0 ? 0:v; } } for (var y = 1; y < s.height-1; ++y) { for (var x = 1; x < s.width-1; ++x) { 让 v=tmp[x][ y]; tmp[x][y]= s.readPixel(x,y)[0]; // 读取像素 s.putPixel(x,y, v,0,0); // 放像素 } } s.refresh(); window.requestAnimationFrame(draw) } // 用于求解波动方程的临时 2d 缓冲区 () let tmp = [...Array(s.width)].map(x => Array(s.height).fill(0) );函数 move(e) { s.putPixel(ex-10, ey-10, 255,255,255);} draw();

在黑色方块上移动鼠标


M
Matheus Dias de Souza

putImageData 本身可能比 fillRect 快。我认为这是因为第五个参数可以有不同的分配方式(矩形颜色),使用必须解释的字符串。

假设您正在这样做:

context.fillRect(x, y, 1, 1, "#fff")
context.fillRect(x, y, 1, 1, "rgba(255, 255, 255, 0.5)")`
context.fillRect(x, y, 1, 1, "rgb(255,255,255)")`
context.fillRect(x, y, 1, 1, "blue")`

所以,线

context.fillRect(x, y, 1, 1, "rgba(255, 255, 255, 0.5)")`

是最重的。 fillRect 调用中的第五个参数是一个稍长的字符串。


哪些浏览器支持将颜色作为第五个参数传递?对于 Chrome,我不得不改用 context.fillStyle = ...developer.mozilla.org/en-US/docs/Web/API/…