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

230
HTML5 画布没有明确设置单个像素的方法。 可能可以使用非常短的线来设置像素,但是会受到抗锯齿和线端点的干扰。 另一种方法可能是创建一个小的ImageData对象,并使用:
context.putImageData(data, x, y)

将其放置在适当的位置。

有人能描述一种高效可靠的方法来完成这个任务吗?

14个回答

336

有两个最佳选择:

  1. 创建一个1x1像素的图像数据,设置颜色,并使用putImageData放置在位置上:

    var id = myContext.createImageData(1,1); // only do this once per page
    var d  = id.data;                        // only do this once per page
    d[0]   = r;
    d[1]   = g;
    d[2]   = b;
    d[3]   = a;
    myContext.putImageData( id, x, y );     
    
    使用fillRect()绘制像素(不应该有锯齿问题):
  2. ctx.fillStyle = "rgba("+r+","+g+","+b+","+(a/255)+")";
    ctx.fillRect( x, y, 1, 1 );
    

您可以在这里测试它们的速度:http://jsperf.com/setting-canvas-pixel/9https://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 = "data:image/png;base64," + myPNGEncoder(r,g,b,a);
    // Writing the PNGEncoder is left as an exercise for the reader
    
  • 创建另一个填充有所需像素的 img 或 canvas,并使用 drawImage() 转移所需的像素。这可能非常快,但其限制在于您需要预先计算所需的像素。

请注意,我的测试未尝试保存和恢复画布上下文中的 fillStyle; 这会减慢 fillRect() 的性能。还请注意,我没有从一个干净的画布开始,也没有针对每个测试测试完全相同的像素集。


5
如果可能的话,我会再多给你 +10 分,以感谢你提交错误报告! :) - Alnitak
56
请注意,在我的电脑上,使用我的GPU和显卡驱动程序,fillRect()函数最近比Chromev24版本的1x1 putimagedata快了近10倍。因此,如果速度至关重要且您了解目标受众,请不要相信过时的答案(包括我的)。而是请进行测试!test! - Phrogz
3
请更新答案。在现代浏览器上,fill方法速度更快。 - Buzzy
13
“Writing the PNGEncoder is left as an exercise for the reader”翻译成中文是“编写PNG编码器留给读者作为练习”,这让我大声笑了起来。 - Pascal Ganaye
3
为什么我找到的所有好的 Canvas 相关答案都是你写的? :) - Domino
显示剩余13条评论

43

还有一种方法没有被提到,就是使用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);

18
因为我不能读懂你的心思而被你扣分,这太不公平了。其他人也许会来这里想要绘制许多像素点,我也曾这样做,但后来发现了更高效的方法,所以分享给大家了。 - PAEz
2
当处理大量像素时,这是一种明智的方法,例如在计算每个像素的图形演示中使用。与为每个像素使用fillRect相比,它快了十倍。 - Sam Watkins
2
是的,我一直有点不爽,因为接受的答案说这种方法比其他方法慢100倍。如果你绘制的少于1000个,这可能是正确的,但从那时起,这种方法开始获胜,然后屠杀其他方法。这是一个测试案例...https://www.measurethat.net/Benchmarks/Show/8386/0/setting-canvas-pixel-with-lots-of-iterations - PAEz

23

我之前没有考虑过fillRect(),但答案激励我将其与putImage()进行对比测试。

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

如果我选择循环外的单个颜色,并在随机位置绘制该颜色,则putImage()需要59ms,而fillRect()需要102ms。

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

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


3
我添加了一个Plunker,你可以在其中点击按钮并测试每种方法(PutImage,FillRect)以及LineTo方法。它显示PutImage和FillRect的时间非常接近,但LineTo非常慢。请查看:http://plnkr.co/edit/tww6e1VY2OCVY4c4ECy3?p=preview 这是基于您出色的pastebin代码。谢谢。 - raddevus
对于那个 Plunker,我发现在最新的 Chrome 63 上,PutImage 比 FillRect 稍微慢一些,但是当我尝试使用 LineTo 后,PutImage 的速度比 FillRect 快得多。不知何故它们似乎会相互干扰。 - mlepage
截至2023年,fillRect()方法在Firefox/Linux上运行速度提高了2倍。 fillRect()方法:363毫秒 putImage()方法:716毫秒 - undefined

18
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;
}

1
var index = (x + y * imageData.width) * 4; 变量索引 = (x + y * 图像数据宽度) * 4; - user889030
1
在调用该函数之后,应该调用 putImageData() 函数,还是上下文会通过引用更新? - Lucas Sousa

7
尽管HTML5支持绘制线条、圆形、矩形和许多其他基本形状,但它并没有适合绘制基本点的任何内容。唯一的方法是用现有的东西模拟点。
因此,基本上有三种可能的解决方案:
- 将点绘制为线条 - 将点绘制为多边形 - 将点绘制为圆形
每种方法都有其缺点。
线条的缺点如下:
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();
}

与矩形相同的思路,你可以通过使用fill实现。
function point(x, y, canvas){
  canvas.beginPath();
  canvas.arc(x, y, 1, 0, 2 * Math.PI, true);
  canvas.fill();
}

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

  • 很难跟踪您要绘制的所有点。
  • 当您缩放时,它看起来很丑陋。

如果你在想,“什么是绘制点的最佳方式?”,我会选择填充矩形。您可以查看我的jsperf测试结果进行比较


东南方向?什么? - LoganDark

7

由于不同的浏览器似乎更喜欢不同的方法,因此在加载过程中进行一次包含所有三种方法的小测试,以找出最佳方法并在整个应用程序中使用可能是有意义的。


4

那矩形呢?相比创建一个 ImageData 对象来说,这肯定更加高效。


3
你可能会认为这样做是可行的,对于单个像素来说也确实如此,但是如果你预先创建图像数据并设置1个像素,然后使用putImageData,在Chrome浏览器中比使用fillRect快10倍。(详情请参见我的回答。) - Phrogz

3

按照sdleihssirhc的说法,画一个矩形!

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

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


1

快速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>

1

嗯,你也可以制作一个长度为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();

1
我将像FillRect、PutImage和LineTo这些像素绘制功能实现了,并在http://plnkr.co/edit/tww6e1VY2OCVY4c4ECy3?p=preview上创建了一个Plunker。请查看一下,因为LineTo的速度呈指数级下降。使用其他两种方法可以在0.25秒内完成10万个点,但是使用LineTo绘制1万个点需要5秒钟。 - raddevus
1
好的,我犯了错误,我想结束这个循环。LineTo代码缺少一行非常重要的代码,如下所示: ctx.beginPath(); 我更新了plunker(在我的另一个评论中提供的链接),现在添加那一行可以使LineTo方法在平均0.5秒内生成100,000。非常惊人。所以,如果你编辑你的答案并将那一行代码添加到你的代码中(在ctx.lineWidth代码之前),我会给你点赞的。希望你觉得这很有趣,对我最初有漏洞的代码感到抱歉。 - raddevus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接