228

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 antialiasing 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?

Yves M.
  • 29,855
  • 23
  • 108
  • 144
Alnitak
  • 334,560
  • 70
  • 407
  • 495

14 Answers14

333

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.

Phrogz
  • 296,393
  • 112
  • 651
  • 745
  • 5
    I'd give you another +10 if I could for filing the bug report! :) – Alnitak Feb 08 '11 at 17:03
  • 56
    Note that on my machine with my GPU and graphics drivers, `fillRect()` semi-recently became almost 10x faster than the 1x1 putimagedata on Chromev24. So...if speed is critical and you know your target audience, don't take the word of an outdated answer (even mine). Instead: [test!](http://jsperf.com/setting-canvas-pixel/9) – Phrogz Jan 04 '13 at 22:11
  • It'd be nice if you could provide the speed estimate for the data url option. – trusktr Mar 14 '13 at 11:25
  • Hmm, you could also just make a 1 pixel wide line with a length of 1 pixel. – trusktr Mar 14 '13 at 11:29
  • 2
    note that if you are worried about performance because you're setting multiple pixels at a time, perhaps you sohuld getImageData for the whole canvas, set _all_ your pixels at once, and only `putImageData` once per frame. – John Dvorak Mar 30 '13 at 11:43
  • 3
    Please update the answer. The fill method is much faster on modern browsers. – Buzzy Sep 21 '13 at 08:25
  • @Buzzy True. Trouble is, on my modern computers all methods are so fast that they are artificially capped by the browser, not computationally limited. – Phrogz Sep 21 '13 at 13:58
  • @Phrogz Sorry, I saw your answer just now. I tried both methods and it was a huge difference for me. See my full experiment here: https://github.com/BorisKozo/SandPile – Buzzy Nov 13 '13 at 09:54
  • 13
    "Writing the PNGEncoder is left as an exercise for the reader" made me laugh aloud. – Pascal Ganaye Nov 15 '14 at 14:31
  • How about using a pre-created ImageData? – Lee Goddard Jan 20 '15 at 12:27
  • 1
    @LeeGee The tests (and suggestion above) to re-use the same ImageData, by allocating it once and then repeatedly setting the data values and then putting that data to the desired location. – Phrogz Jan 20 '15 at 20:44
  • 3
    Why do all great Canvas answers I land on happen to be by you? :) – Domino Nov 20 '15 at 05:05
  • It seems that `ImageData.data` is read-only, so the attempt to set the pixels in the array should not work, according to the spec. – amoe Jul 03 '16 at 11:36
  • 1
    Alternatively, you can do `new Uint8ClampedArray([r,g,b,a])`. – Conor O'Brien Dec 19 '16 at 19:41
  • 1
    This in an interesting one! I'm getting such different results in Firefox 57 and Chromium 63. Chromium is getting 956k ops/sec with fillRect, and only 108k with ImageData. Firefox meanwhile gets 857k with fillRect, and 9M (!) with ImageData. I'm on Linux x64. – tremby Dec 17 '17 at 04:33
  • 1
    Your comment that putimagedata/getimagedata is 100x slower than the other methods bugs me. Its true if your plotting less than 1000 or so, but after that the put/get method starts to be faster and at 5000 it slaughters. Any chance you could add that....right tool for the right job and all that. Here's a test https://www.measurethat.net/Benchmarks/Show/8386/0/setting-canvas-pixel-with-lots-of-iterations also added template literal, faster on FF but not Chrome. – PAEz May 27 '20 at 02:18
  • @amoe Do you have a reference for `ImageData.data` being read-only per the specs? I don't think it is. TypeScript types don't have it as read-only, and [MDN](https://developer.mozilla.org/en-US/docs/Web/API/ImageData/data) even has an example modifying it. – bluenote10 Mar 25 '21 at 09:52
  • 1
    @bluenote10 I was going by the MDN property list on the [ImageData page](https://developer.mozilla.org/en-US/docs/Web/API/ImageData), where it has "Read only" next to the property. Looking at it again now, I guess "readonly" in this context means that it can't be rebound, but elements of the array can still be modified. So my previous comment was wrong. – amoe Mar 25 '21 at 11:19
42

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);
Uwe Keim
  • 39,551
  • 56
  • 175
  • 291
PAEz
  • 8,366
  • 2
  • 34
  • 27
  • 18
    @Alnitak Giving me a neg for not being able to read your mind, is low..Other people might get here looking to be able to plot many pixels. I did and then remembered the more efficient way, so shared it. – PAEz Jul 09 '18 at 06:19
  • 2
    This is a sensible method when poking a lot of pixels, for a graphics demo where each pixel is calculated, or similar. It's ten times faster than using fillRect for each pixel. – Sam Watkins May 25 '20 at 14:12
  • 2
    Yeah, it always kinda bugged me that the excepted answer says that this method is 100x slower than the other methods. This may be true if your plotting less than 1000, but from there onwards this method starts winning and then slaughtering the other methods. Here's a test case.... https://www.measurethat.net/Benchmarks/Show/8386/0/setting-canvas-pixel-with-lots-of-iterations – PAEz May 27 '20 at 02:11
23

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.

thedayturns
  • 9,723
  • 5
  • 33
  • 41
Alnitak
  • 334,560
  • 70
  • 407
  • 495
  • 3
    I added a plunker where you can click a button and test each of the methods (PutImage, FillRect) and additionally the LineTo method. It shows that PutImage and FillRect are very close in times but LineTo is extremely slow. Check it out at: http://plnkr.co/edit/tww6e1VY2OCVY4c4ECy3?p=preview It's based upon your great pastebin code. Thanks. – raddevus Feb 16 '15 at 19:20
  • For that plunker, I see PutImage is slightly slower than FillRect (on latest Chrome 63), but after I try LineTo, then PutImage is significantly faster than FillRect. Somehow they seem to be interfering. – mlepage Jan 10 '18 at 04:54
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;
}
Vit Kaspar
  • 181
  • 1
  • 2
7

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?

Daniel
  • 71
  • 1
  • 1
7

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.

Salvador Dali
  • 214,103
  • 147
  • 703
  • 753
4

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

sdleihssirhc
  • 42,000
  • 6
  • 53
  • 67
  • 3
    You'd think so, and it might be for a single pixel, but if you pre-create the image data and set the 1 pixel and then use `putImageData` it is 10x faster than `fillRect` in Chrome. (See my answer for more.) – Phrogz Feb 04 '11 at 18:12
3

Draw a rectangle like sdleihssirhc said!

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

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

erik
  • 3,810
  • 6
  • 32
  • 63
1

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();
Community
  • 1
  • 1
trusktr
  • 44,284
  • 53
  • 191
  • 263
  • 1
    I implemented the pixel draw as FillRect, PutImage and LineTo and created a plunker at: http://plnkr.co/edit/tww6e1VY2OCVY4c4ECy3?p=preview Check it out, because LineTo is exponentially slower. Can do 100,000 points w other 2 methods in 0.25 seconds, but 10,000 points with LineTo takes 5 seconds. – raddevus Feb 16 '15 at 19:09
  • 1
    Okay, I made a mistake and I'd like to close the loop. The LineTo code was missing one -- very important line -- which looks like the following: ctx.beginPath(); I updated the plunker (at the link from my other comment) and adding that one line now allows the LineTo method to generate 100,000 in 0.5 seconds average. Quite amazing. So if you'll edit your answer and add that line to your code (before the ctx.lineWidth line) I will upvote you. I hope you found this interesting and I apologize for my original buggy code. – raddevus Feb 17 '15 at 13:26
1

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>
KANJICODER
  • 3,611
  • 30
  • 17
0

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

Martin Ždila
  • 2,998
  • 3
  • 31
  • 35
0

Purely for diagnostics purposes, I use this simple function.

Note. if integer coordinates are not used, the resulting image is blurred.

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

}
Potherca
  • 13,207
  • 5
  • 76
  • 94
-1

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.

yoniLavi
  • 2,624
  • 1
  • 24
  • 30
Boing
  • 246
  • 2
  • 4
  • I've had a case where I wanted to put 100k blocks in an image, but not at 1:1 pixel scale. Using `fillRect` was painful because Chrome's h/w acceleration can't cope with the individual calls to the GPU it would require. I ended up having to use pixel data at 1:1 and then use CSS scaling to get the desired output. It's ugly :( – Alnitak Mar 12 '13 at 15:21
  • Running your linked benchmark on Firefox 42 I get only 168 Ops/sec for `get/putImageData`, but 194,893 for `fillRect`. `1x1 image data` is 125,102 Ops/sec. So `fillRect` wins by far in Firefox. So things changed a lot between 2012 and today. As always, never rely on old benchmark results. – Mecki Dec 09 '15 at 21:08
  • 14
    I want to set one pixel at a time. I'm guessing by the title of this question that other people do as well – chasmani Jan 17 '17 at 15:08
-1

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(); 
  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>
Kamil Kiełczewski
  • 85,173
  • 29
  • 368
  • 345