ChatGPT解决这个技术问题 Extra ChatGPT

What's the best way to set a single pixel in an HTML5 canvas?

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?


P
Phrogz

There are two best contenders:

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


I'd give you another +10 if I could for filing the bug report! :)
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!
Please update the answer. The fill method is much faster on modern browsers.
"Writing the PNGEncoder is left as an exercise for the reader" made me laugh aloud.
Why do all great Canvas answers I land on happen to be by you? :)
U
Uwe Keim

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

@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.
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.
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.... measurethat.net/Benchmarks/Show/8386/0/…
t
thedayturns

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.


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: plnkr.co/edit/tww6e1VY2OCVY4c4ECy3?p=preview It's based upon your great pastebin code. Thanks.
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.
S
Sebastian Ortmann
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;
Should call putImageData() after that function or the context will updates by reference?
M
Matheus Dias de Souza

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.


The south-east direction? What?
D
Daniel

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?


s
sdleihssirhc

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


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.)
e
erik

Draw a rectangle like sdleihssirhc said!

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

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


C
Community

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

I implemented the pixel draw as FillRect, PutImage and LineTo and created a plunker at: 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.
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.
K
KANJICODER

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>

y
yoniLavi

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.


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 :(
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.
I want to set one pixel at a time. I'm guessing by the title of this question that other people do as well
M
Martin Ždila

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


P
Potherca

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

}

K
Kamil Kiełczewski

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

Move mouse on black square


M
Matheus Dias de Souza

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.


Which browser(s) support passing a color as the 5th argument? For Chrome I had to use context.fillStyle = ... instead. developer.mozilla.org/en-US/docs/Web/API/…