203
votes

The HTML5 Canvas has no method for explicitly setting a single pixel.

It might be possible to set a pixel using a very short line, but then antialising and line caps might interfere.

Another way might be to create a small ImageData object and using:

context.putImageData(data, x, y)

to put it in place.

Can anyone describe an efficient and reliable way of doing this?

14

14 Answers

311
votes

There are two best contenders:

  1. Create a 1×1 image data, set the color, and putImageData at the location:

    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. Use fillRect() to draw a pixel (there should be no aliasing issues):

    ctx.fillStyle = "rgba("+r+","+g+","+b+","+(a/255)+")";
    ctx.fillRect( x, y, 1, 1 );
    

You can test the speed of these here: http://jsperf.com/setting-canvas-pixel/9 or here https://www.measurethat.net/Benchmarks/Show/1664/1

I recommend testing against browsers you care about for maximum speed. As of July 2017, fillRect() is 5-6× faster on Firefox v54 and Chrome v59 (Win7x64).

Other, sillier alternatives are:

  • using getImageData()/putImageData() on the entire canvas; this is about 100× slower than other options.

  • creating a custom image using a data url and using drawImage() to show it:

    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
    
  • creating another img or canvas filled with all the pixels you want and use drawImage() to blit just the pixel you want across. This would probably be very fast, but has the limitation that you need to pre-calculate the pixels you need.

Note that my tests do not attempt to save and restore the canvas context fillStyle; this would slow down the fillRect() performance. Also note that I am not starting with a clean slate or testing the exact same set of pixels for each test.

26
votes

One method that hasnt been mentioned is using getImageData and then putImageData.
This method is good for when you want to draw a lot in one go, fast.
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);
22
votes

I hadn't considered fillRect(), but the answers spurred me to benchmark it against putImage().

Putting 100,000 randomly coloured pixels in random locations, with Chrome 9.0.597.84 on an (old) MacBook Pro, takes less than 100ms with putImage(), but nearly 900ms using fillRect(). (Benchmark code at http://pastebin.com/4ijVKJcC).

If instead I choose a single colour outside of the loops and just plot that colour at random locations, putImage() takes 59ms vs 102ms for fillRect().

It seems that the overhead of generating and parsing a CSS colour specification in rgb(...) syntax is responsible for most of the difference.

Putting raw RGB values straight into an ImageData block on the other hand requires no string handling or parsing.

15
votes
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;
}
7
votes

Since different browsers seems to prefer different methods, maybe it would make sense to do a smaller test with all three methods as a part of the loading process to find out which is best to use and then use that throughout the application?

6
votes

It seems strange, but nonetheless HTML5 supports drawing lines, circles, rectangles and many other basic shapes, it does not have anything suitable for drawing the basic point. The only way to do so is to simulate point with whatever you have.

So basically there are 3 possible solutions:

  • draw point as a line
  • draw point as a polygon
  • draw point as a circle

Each of them has their drawbacks


Line

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

Keep in mind that we are drawing to South-East direction, and if this is the edge, there can be a problem. But you can also draw in any other direction.


Rectangle

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

or in a faster way using fillRect because render engine will just fill one pixel.

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

Circle


One of the problems with circles is that it is harder for an engine to render them

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

the same idea as with rectangle you can achieve with fill.

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

Problems with all these solutions:

  • it is hard to keep track of all the points you are going to draw.
  • when you zoom in, it looks ugly.

If you are wondering, "What is the best way to draw a point?", I would go with filled rectangle. You can see my jsperf here with comparison tests.

4
votes

What about a rectangle? That's got to be more efficient than creating an ImageData object.

2
votes

Draw a rectangle like sdleihssirhc said!

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

^-- should draw a 1x1 rectangle at x:10, y:10

1
votes

Hmm, you could also just make a 1 pixel wide line with a length of 1 pixel and make it's direction move along a single axis.

            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();
0
votes

To complete Phrogz very thorough answer, there is a critical difference between fillRect() and putImageData().
The first uses context to draw over by adding a rectangle (NOT a pixel), using the fillStyle alpha value AND the context globalAlpha and the transformation matrix, line caps etc..
The second replaces an entire set of pixels (maybe one, but why ?)
The result is different as you can see on jsperf.


Nobody wants to set one pixel at a time (meaning drawing it on screen). That is why there is no specific API to do that (and rightly so).
Performance wise, if the goal is to generate a picture (for example a ray-tracing software), you always want to use an array obtained by getImageData() which is an optimized Uint8Array. Then you call putImageData() ONCE or a few times per second using setTimeout/seTInterval.

0
votes

Fast HTML Demo code: Based on what I know about SFML C++ graphics library:

Save this as an HTML file with UTF-8 Encoding and run it. Feel free to refactor, I just like using japanese variables because they are concise and don't take up much space

Rarely are you going to want to set ONE arbitrary pixel and display it on the screen. So use the

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

method to draw numerous arbitrary pixels to a back-buffer. (cheap calls)

Then when ready to show, call the

Apply() 

method to display the changes. (expensive call)

Full .HTML file code below:

<!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>
0
votes

If you are concerned about the speed then you could also consider WebGL.

-1
votes

Fast and handy

Following class implements fast method described in this article and contains all you need: readPixel, putPixel, get width/height. Class update canvas after calling refresh() method. Example solve simple case of 2d wave equation

class Screen{
  constructor(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 components in range 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) {
    let p= this.data[y * this.width + x]
    return [p&0xff, p>>8&0xff, p>>16&0xff, p>>>24];
  }

  refresh() {
    this.imageData.data.set(this.buf8);
    this.ctx.putImageData(this.imageData, 0, 0);
  }
}




// --------
// TEST
// --------

let s= new Screen('#canvas');                // initialise

function 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]        // read pixel
      ,0);
      let 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) {  
      let v=tmp[x][y];
      tmp[x][y]= s.readPixel(x,y)[0];        // read pixel
      s.putPixel(x,y, v,0,0);                // put pixel
    }
  }

  s.refresh(); 
  frame++;
  window.requestAnimationFrame(draw)
}

// temporary 2d buffer ()for solving wave equation)
let tmp = [...Array(s.width)].map(x => Array(s.height).fill(0));

function move(e) { s.putPixel(e.x-10, e.y-10, 255,255,255);}

draw();
<canvas id="canvas" height="150" width="512" onmousemove="move(event)"></canvas>
<div>Move mouse on black square</div>
-2
votes

putImageData is probably faster than fillRect natively. I think this because the fifth parameter can have different ways to be assigned (the rectangle color), using a string that must be interpreted.

Suppose you're doing that:

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")`

So, the line

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

is the most heavy between all. The fifth argument in the fillRect call is a bit longer string.