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


184

HTML5 Canvas没有用于显式设置单个像素的方法。

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

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

context.putImageData(data, x, y)

放置到位。

谁能描述一种有效且可靠的方法?

Answers:


292

有两个最佳竞争者:

  1. 创建1×1图像数据,设置颜色,并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 );     
    
  2. 使用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倍。

  • 使用数据网址创建自定义图片并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或画布,其中填充了您想要的所有像素,并用于drawImage()仅涂抹您想要的像素。这可能会非常快,但是有局限性,您需要预先计算所需的像素。

请注意,我的测试并不尝试保存和恢复canvas上下文fillStyle;这会降低fillRect()性能。还要注意,我不是从一开始就开始,也不是在每次测试中都测试完全相同的像素集。


2
如果可以提交错误报告,我会再给您+10!:)
Alnitak

51
请注意,在装有GPU和图形驱动程序的机器上,fillRect()最近的速度几乎比Chromev24上的1x1 putimagedata快10倍。因此...如果速度至关重要,并且您了解目标受众,请不要犹豫过时的答案(甚至是我的答案)。相反:测试!
Phrogz 2013年

3
请更新答案。在现代浏览器中,填充方法要快得多。
Buzzy 2013年

10
“写PNGEncoder留给读者练习”让我大声笑了。
Pascal Ganaye 2014年

2
为什么我能找到的所有出色的Canvas答案都在您的身边?:)
Domino

18

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

13
@Alnitak因为无法阅读您的想法而给我带来的负面情绪很低。.其他人可能会来这里寻找能够绘制许多像素的图形。我这样做了,然后想起了更有效的方法,所以与大家分享。
PAEz '18年

当戳很多像素时,对于计算每个像素的图形演示或类似方法,这是一种明智的方法。这比对每个像素使用fillRect快十倍。
Sam Watkins

是的,总是有点让我感到困惑的是,例外答案指出此方法比其他方法慢100倍。如果您的图样小于1000,则可能是正确的,但此后此方法开始获胜,然后宰杀其他方法。这是一个测试案例.... measurethat.net/Benchmarks/Show/8386/0/...
派斯

17

我没考虑过fillRect(),但是答案促使我以此为基准putImage()

在旧版MacBook Pro上使用Chrome 9.0.597.84将100,000个随机着色的像素放置在随机位置上,使用花费的时间少于100毫秒putImage(),但使用的花费不到900 毫秒fillRect()。(位于http://pastebin.com/4ijVKJcC的基准代码)。

相反,如果我在循环之外选择一种颜色,然后在随机位置绘制该颜色,则putImage()需要59ms vs 102ms fillRect()

似乎rgb(...)大多数语法差异都是由语法生成和解析CSS颜色规范引起的。

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


2
我添加了一个插件,您可以在其中单击按钮并测试每个方法(PutImage,FillRect)以及LineTo方法。它显示PutImage和FillRect时间非常接近,而LineTo则非常慢。在以下位置进行检查:plnkr.co/edit/tww6e1VY2OCVY4c4ECy3?p=preview这是基于您出色的pastebin代码的。谢谢。
raddevus

对于那个笨拙的人,我发现PutImage的速度比FillRect稍慢(在最新的Chrome 63上),但是在尝试LineTo之后,PutImage的速度明显比FillRect快。他们似乎以某种方式干扰。
mlepage

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

var index =(x + y * imageData.width)* 4;
user889030 '18

1
应该putImageData() 在该函数之后调用,否则上下文将通过引用进行更新?
卢卡斯·索萨

7

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


5

看起来很奇怪,但是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以及比较测试


东南方向?什么?
LoganDark

4

矩形呢?这必须比创建ImageData对象更有效。


3
您可能会这么认为,可能只用一个像素,但是如果您预先创建图像数据并设置1像素,然后使用putImageData它,则它的速度比fillRectChrome 快10倍。(有关更多信息,请参见我的回答。)
Phrogz,2011年

2

画一个像sdleihssirhc所说的矩形!

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

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


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,并在以下位置创建了一个插件:plnkr.co/edit/tww6e1VY2OCVY4c4ECy3?p=preview签 出它,因为LineTo的速度呈指数级降低。在0.25秒内可以用其他2种方法得到100,000点,而使用LineTo则需要5秒才能得到10,000点。
raddevus

1
好的,我犯了一个错误,我想结束循环。LineTo代码缺少一条-非常重要的行-类似于以下内容:ctx.beginPath(); 我更新了插件(在其他评论的链接处),并添加了一行内容,现在允许LineTo方法在0.5秒的平均时间内生成100,000。非常了不起。因此,如果您要编辑答案并将该行添加到代码中(在ctx.lineWidth行之前),我将投票给您。希望您觉得这很有趣,对于我的原始错误代码深表歉意。
raddevus

1

要完成Phrogz的非常详尽的答案,fillRect()和之间存在关键区别putImageData()
第一个通过使用fillStyle alpha值和上下文globalAlpha以及转换矩阵线帽等通过添加矩形(不是像素)来使用上下文进行绘制第二个模型替换了整个像素集(也许是一个,但是为什么) ?) 结果与jsperf所示不同


没有人愿意一次设置一个像素(意味着在屏幕上绘制)。这就是为什么没有特定的API可以这样做的原因(正确的做法是这样做)。
在性能方面,如果目标是生成图片(例如,光线跟踪软件),则始终希望使用通过getImageData()优化Uint8Array 获得的数组。然后,您使用呼叫putImageData()一次或每秒几次setTimeout/seTInterval


我曾经想在一个图像中放置100k块,但不是以1:1像素比例。使用起来fillRect很痛苦,因为Chrome的硬件加速无法满足它对GPU的单独调用。我最终不得不使用1:1的像素数据,然后使用CSS缩放来获得所需的输出。丑陋的:(
Alnitak

在Firefox 42上运行链接的基准测试,我的速度仅为168 Ops / sec get/putImageData,而速度为194,893 fillRect1x1 image data是125,102 Ops / sec。因此fillRect,到目前为止,在Firefox中胜出。因此,从2012年到今天,情况发生了很大变化。与往常一样,永远不要依赖旧的基准测试结果。
Mecki 2015年

12
我想一次设置一个像素。我猜这个问题的标题,其他人也一样
chasmani

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

HANDY和放置像素(pp)函数(ES6)的命题(此处为 read-pixel ):

let pp= ((s='.myCanvas',c=document.querySelector(s),ctx=c.getContext('2d'),id=ctx.createImageData(1,1)) => (x,y,r=0,g=0,b=0,a=255)=>(id.data.set([r,g,b,a]),ctx.putImageData(id, x, y),c))()

pp(10,30,0,0,255,255);    // x,y,r,g,b,a ; return canvas object

该功能使用putImageData并具有初始化部分(第一行)。在开始时,请s='.myCanvas'使用CSS选择器选择画布。

如果您想将参数标准化为0-1的值,则应将默认值更改a=255a=1并与: id.data.set([r,g,b,a]),ctx.putImageData(id, x, y)匹配 id.data.set([r*255,g*255,b*255,a*255]),ctx.putImageData(id, x*c.width, y*c.height)

上面方便的代码适合临时测试图形算法或进行概念验证,但不适用于代码应易读且清晰的生产环境。


1
因英语不佳和班轮混乱而投下反对票。
xavier

1
@xavier-英语不是我的母语,我也不擅长学习外语,但是您可以编辑我的答案并修复语言错误(这将对您有所帮助)。我放这条线是因为它方便且易于使用-例如可以帮助学生测试一些图形算法,但是在代码应该可读且清晰的生产环境中使用它并不是一个好的解决方案。
卡米尔·基列夫斯基(KamilKiełczewski),

3
@KamilKiełczewski代码的可读性和清晰性对学生和专业人士同样重要。
Logan Pickup

-2

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调用中的第五个参数是更长的字符串。


1
哪些浏览器支持将颜色用作第五个参数?对于Chrome,我不得不使用它context.fillStyle = ...developer.mozilla.org/en-US/docs/Web/API/…–
iX3
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.