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

时间:2011-02-04 15:38:33

标签: html5 canvas pixel

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

可能可以使用非常短的线设置像素,但随后消除噪音和线路上限可能会产生干扰。

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

context.putImageData(data, x, y)

把它放到位。

有人能描述一种有效可靠的方法吗?

14 个答案:

答案 0 :(得分:264)

有两个最好的竞争者:

  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 );
    
  3. 您可以在此处测试这些速度:http://jsperf.com/setting-canvas-pixel/9或此处https://www.measurethat.net/Benchmarks/Show/1664/1

    我建议针对您最关心的浏览器进行测试。截至2017年7月,{vv}和Chrome v59(Win7x64)的fillRect()速度提高了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()来显示您想要的像素。这可能会非常快,但是您需要预先计算所需的像素。

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

答案 1 :(得分:14)

我没有考虑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块中不需要字符串处理或解析。

答案 2 :(得分:11)

function setPixel(imageData, x, y, r, g, b, a) {
    index = (x + y * imageData.width) * 4;
    imageData.data[index+0] = r;
    imageData.data[index+1] = g;
    imageData.data[index+2] = b;
    imageData.data[index+3] = a;
}

答案 3 :(得分:7)

由于不同的浏览器似乎更喜欢不同的方法,因此可能需要使用所有三种方法进行较小的测试作为加载过程的一部分,以找出哪种方法最适合使用,然后在整个应用程序中使用它? / p>

答案 4 :(得分:4)

矩形怎么样?这比创建ImageData对象更有效。

答案 5 :(得分:4)

看起来很奇怪,但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();
}

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

  • 很难跟踪你要绘制的所有点。
  • 当你放大时,它看起来很难看。

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

答案 6 :(得分:3)

尚未提及的一种方法是使用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);

答案 7 :(得分:2)

画一个像sdleihssirhc说的矩形!

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

^ - 应在x:10,y:10

处绘制1x1矩形

答案 8 :(得分:1)

要完成Phrogz非常彻底的回答,fillRect()putImageData()之间存在重大差异。
第一个使用上下文通过添加矩形(不是像素)来绘制 over ,使用 fillStyle alpha值和上下文 globalAlpha 转换矩阵行上限等。
第二个替换整个像素集(可能是一个,但为什么?)
结果与jsperf上的结果不同。


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

答案 9 :(得分: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();

答案 10 :(得分:-1)

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

答案 11 :(得分:-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>

答案 12 :(得分:-1)

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

答案 13 :(得分:-1)

HANDY 和放置像素(pp)功能(ES6)(读取像素here)的有效命题:

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

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))()

// draw shape
for(let i=0; i<800; i++) {
   let a = Math.PI*4*i/800;
   let x=16*Math.sin(a)**3;
   let y=13*Math.cos(a)-5*Math.cos(2*a)-2*Math.cos(3*a)-Math.cos(4*a);   
   pp(100+2.5*x,45-2.5*y,255,0,0,255)   
}
<canvas class="myCanvas" width=200 height=100 style="background: black"></canvas>

此函数使用putImageData,因此效率高且具有初始化部分(第一行)。首先,s='.myCanvas'使用CSS选择器选择画布。

如果您想将参数标准化为0-1的值,则应将默认值a=255更改为a=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)

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