¿Cuál es la mejor manera de establecer un solo píxel en un lienzo HTML5?


184

HTML5 Canvas no tiene ningún método para configurar explícitamente un solo píxel.

Es posible establecer un píxel con una línea muy corta, pero luego la intercalación y los límites de línea pueden interferir.

Otra forma podría ser crear un ImageDataobjeto pequeño y usar:

context.putImageData(data, x, y)

para ponerlo en su lugar.

¿Alguien puede describir una manera eficiente y confiable de hacer esto?

Respuestas:


292

Hay dos mejores contendientes:

  1. Cree datos de imagen 1 × 1, establezca el color y putImageDataen la ubicación:

    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. Úselo fillRect()para dibujar un píxel (no debería haber problemas de alias):

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

Puede probar la velocidad de estos aquí: http://jsperf.com/setting-canvas-pixel/9 o aquí https://www.measurethat.net/Benchmarks/Show/1664/1

Recomiendo probar con los navegadores que le interesan para obtener la máxima velocidad. A partir de julio de 2017, fillRect()es 5-6 veces más rápido en Firefox v54 y Chrome v59 (Win7x64).

Otras alternativas más tontas son:

  • utilizando getImageData()/putImageData()en todo el lienzo; Esto es aproximadamente 100 veces más lento que otras opciones.

  • crear una imagen personalizada usando una url de datos y usarla drawImage()para mostrarla:

    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
  • creando otro img o lienzo lleno de todos los píxeles que desee y use drawImage()para borrar solo el píxel que desee. Esto probablemente sería muy rápido, pero tiene la limitación que necesita para calcular previamente los píxeles que necesita.

Tenga en cuenta que mis pruebas no intentan guardar y restaurar el contexto del lienzo fillStyle; Esto ralentizaría el fillRect()rendimiento. También tenga en cuenta que no estoy comenzando con una pizarra limpia o probando exactamente el mismo conjunto de píxeles para cada prueba.


2
¡Te daría otro +10 si pudiera por presentar el informe de error! :)
Alnitak

51
Tenga en cuenta que en mi máquina con mi GPU y controladores de gráficos, fillRect()recientemente se volvió casi 10 veces más rápido que los 1x1 putimagedata en Chromev24. Entonces ... si la velocidad es crítica y conoce a su público objetivo, no tome la palabra de una respuesta desactualizada (incluso la mía). En cambio: prueba!
Phrogz

3
Por favor actualice la respuesta. El método de relleno es mucho más rápido en los navegadores modernos.
Buzzy

10
"Escribir el PNGEncoder se deja como un ejercicio para el lector" me hizo reír en voz alta.
Pascal Ganaye

2
¿Por qué todas las excelentes respuestas de Canvas en las que aterrizo son tuyas? :)
Domino

19

Un método que no se ha mencionado es usar getImageData y luego putImageData.
Este método es bueno para cuando quieres dibujar mucho de una vez, rápido.
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 Darme una negación por no poder leer tu mente, es bajo ... Otras personas podrían llegar buscando trazar muchos píxeles. Lo hice y luego recordé la forma más eficiente, así que la compartí.
PAEz

Este es un método sensato cuando se pinchan muchos píxeles, para una demostración de gráficos donde se calcula cada píxel o similar. Es diez veces más rápido que usar fillRect para cada píxel.
Sam Watkins

Sí, siempre me molestó que la respuesta exceptuada diga que este método es 100 veces más lento que los otros métodos. Esto puede ser cierto si está tramando menos de 1000, pero a partir de ahí este método comienza a ganar y luego mata a los otros métodos. Aquí hay un caso de prueba ... measurethat.net/Benchmarks/Show/8386/0/…
PAEz

17

No lo había considerado fillRect(), pero las respuestas me impulsaron a compararlo putImage().

Colocar 100,000 píxeles de colores aleatorios en ubicaciones aleatorias, con Chrome 9.0.597.84 en una (antigua) MacBook Pro, requiere menos de 100 ms putImage(), pero casi 900 ms de uso fillRect(). (Código de referencia en http://pastebin.com/4ijVKJcC ).

Si, en cambio, elijo un solo color fuera de los bucles y simplemente trazo ese color en ubicaciones aleatorias, putImage()toma 59 ms frente a 102 ms fillRect().

Parece que la sobrecarga de generar y analizar una especificación de color CSS en rgb(...)sintaxis es responsable de la mayor parte de la diferencia.

Por otro lado, poner valores RGB sin procesar directamente en un ImageDatabloque no requiere manejo ni análisis de cadenas.


2
Agregué un plunker donde puedes hacer clic en un botón y probar cada uno de los métodos (PutImage, FillRect) y adicionalmente el método LineTo. Muestra que PutImage y FillRect están muy cerca en el tiempo, pero LineTo es extremadamente lento. Compruébelo en: plnkr.co/edit/tww6e1VY2OCVY4c4ECy3?p=preview Está basado en su excelente código pastebin. Gracias.
raddevus

Para ese plunker, veo que PutImage es un poco más lento que FillRect (en el último Chrome 63), pero después de probar LineTo, PutImage es significativamente más rápido que FillRect. De alguna manera parecen estar interfiriendo.
mlepage el

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

índice var = (x + y * imageData.width) * 4;
user889030

1
¿Debería llamar putImageData() después de esa función o el contexto se actualizará por referencia?
Lucas Sousa

7

Dado que los diferentes navegadores parecen preferir métodos diferentes, ¿quizás tenga sentido hacer una prueba más pequeña con los tres métodos como parte del proceso de carga para averiguar cuál es el mejor para usar y luego usarlo en toda la aplicación?


5

Parece extraño, pero no obstante HTML5 admite líneas de dibujo, círculos, rectángulos y muchas otras formas básicas, no tiene nada adecuado para dibujar el punto básico. La única forma de hacerlo es simulando el punto con lo que tenga.

Básicamente, hay 3 soluciones posibles:

  • dibujar punto como una línea
  • dibujar punto como un polígono
  • dibujar punto como un círculo

Cada uno de ellos tiene sus inconvenientes.


Línea

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

Tenga en cuenta que estamos dibujando en dirección sureste, y si este es el borde, puede haber un problema. Pero también puedes dibujar en cualquier otra dirección.


Rectángulo

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

o de una manera más rápida usando fillRect porque el motor de renderizado solo llenará un píxel.

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

Circulo


Uno de los problemas con los círculos es que es más difícil que un motor los procese

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

la misma idea que con el rectángulo que puedes lograr con el relleno.

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

Problemas con todas estas soluciones:

  • Es difícil hacer un seguimiento de todos los puntos que va a dibujar.
  • cuando acercas, se ve feo.

Si se pregunta "¿Cuál es la mejor manera de dibujar un punto? ", Iría con un rectángulo lleno. Puedes ver mi jsperf aquí con pruebas de comparación .


¿La dirección sureste? ¿Qué?
LoganDark

4

¿Qué tal un rectángulo? Eso tiene que ser más eficiente que crear un ImageDataobjeto.


3
Creería que sí, y podría ser para un solo píxel, pero si crea previamente los datos de la imagen y establece el píxel 1 y luego lo usa putImageData, es 10 veces más rápido que fillRecten Chrome. (Vea mi respuesta para más información)
Phrogz

2

¡Dibuja un rectángulo como dijo sdleihssirhc!

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

^ - debería dibujar un rectángulo 1x1 en x: 10, y: 10


1

Hmm, también podrías hacer una línea de 1 píxel de ancho con una longitud de 1 píxel y hacer que su dirección se mueva a lo largo de un solo eje.

            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
Implementé el dibujo de píxeles como FillRect, PutImage y LineTo y creé un plunker en: plnkr.co/edit/tww6e1VY2OCVY4c4ECy3?p=preview Compruébelo, porque LineTo es exponencialmente más lento. Puede hacer 100,000 puntos con otros 2 métodos en 0.25 segundos, pero 10,000 puntos con LineTo toma 5 segundos.
raddevus

1
Bien, cometí un error y me gustaría cerrar el ciclo. Al código de LineTo le faltaba una, una línea muy importante, que se parece a la siguiente: ctx.beginPath (); Actualicé el plunker (en el enlace de mi otro comentario) y agregué que una línea ahora permite que el método LineTo genere 100,000 en un promedio de 0.5 segundos. Bastante sorprendente. Entonces, si edita su respuesta y agrega esa línea a su código (antes de la línea ctx.lineWidth), lo votaré. Espero que hayas encontrado esto interesante y pido disculpas por mi código de error original.
raddevus

1

Para completar la respuesta muy completa de Phrogz, hay una diferencia crítica entre fillRect()y putImageData().
El primer contexto usos para dibujar sobre por la adición de un rectángulo (no un pixel), utilizando el fillStyle valor alfa y el contexto globalAlpha y la matriz de transformación , tapas de línea etc ..
La segunda sustituye a un entero conjunto de píxeles (tal vez uno, pero ¿por qué ?)
El resultado es diferente como se puede ver en jsperf .


Nadie quiere establecer un píxel a la vez (es decir, dibujarlo en la pantalla). Es por eso que no hay una API específica para hacerlo (y con razón).
En cuanto al rendimiento, si el objetivo es generar una imagen (por ejemplo, un software de trazado de rayos), siempre desea utilizar una matriz obtenida por medio de getImageData()un Uint8Array optimizado. Luego llamas putImageData()UNA VEZ o unas pocas veces por segundo usando setTimeout/seTInterval.


He tenido un caso en el que quería poner 100k bloques en una imagen, pero no a escala 1: 1 de píxeles. El uso fillRectfue doloroso porque la aceleración h / w de Chrome no puede hacer frente a las llamadas individuales a la GPU que requeriría. Terminé teniendo que usar datos de píxeles a 1: 1 y luego usar la escala CSS para obtener la salida deseada. Es feo :(
Alnitak

Al ejecutar su punto de referencia vinculado en Firefox 42, obtengo solo 168 Ops / seg get/putImageData, pero 194,893 para fillRect.1x1 image dataes 125,102 Ops / seg. Entonces fillRectgana con diferencia en Firefox. Así que las cosas cambiaron mucho entre 2012 y hoy. Como siempre, nunca confíe en los viejos resultados de referencia.
Mecki

12
Quiero establecer un píxel a la vez. Supongo por el título de esta pregunta que otras personas también lo hacen
chasmani

1

Código de demostración HTML rápido: según lo que sé sobre la biblioteca de gráficos SFML C ++:

Guarde esto como un archivo HTML con codificación UTF-8 y ejecútelo. Siéntase libre de refactorizar, simplemente me gusta usar variables japonesas porque son concisas y no ocupan mucho espacio

Rara vez querrá establecer UN píxel arbitrario y mostrarlo en la pantalla. Entonces usa el

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

Método para dibujar numerosos píxeles arbitrarios en un búfer de respaldo. (llamadas baratas)

Luego, cuando esté listo para mostrar, llame al

Apply() 

Método para mostrar los cambios. (llamada costosa)

Código de archivo .HTML completo a continuación:

<!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 y propuesta de la función put pixel (pp) (ES6) (lea el píxel aquí ):

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

Esta función utiliza putImageDatay tiene una parte de inicialización (primera línea larga). Al principio, en s='.myCanvas'su lugar, use su selector CSS en su lienzo.

I ¿Quieres para normalizar los parámetros de valor entre 0-1 que debe cambiar el valor por defecto a=255de a=1y de acuerdo con: id.data.set([r,g,b,a]),ctx.putImageData(id, x, y)a id.data.set([r*255,g*255,b*255,a*255]),ctx.putImageData(id, x*c.width, y*c.height)

El práctico código anterior es bueno para los algoritmos gráficos de prueba ad-hoc o para hacer una prueba de concepto, pero no es bueno usarlo en producción donde el código debe ser legible y claro.


1
Votaron a favor de un inglés pobre y un trazador de líneas desordenado.
xavier

1
@xavier: el inglés no es mi idioma nativo y no soy bueno para aprender idiomas extranjeros, sin embargo, puede editar mi respuesta y corregir los errores del idioma (será una contribución positiva de su parte). Puse esta línea porque es práctica y fácil de usar, y puede ser bueno, por ejemplo, para que los estudiantes prueben algunos algoritmos gráficos, sin embargo, no es una buena solución para usarse en producción donde el código debe ser legible y claro.
Kamil Kiełczewski

3
@ KamilKiełczewski El código legible y claro es tan importante para los estudiantes como para los profesionales.
Recogida de Logan el

-2

putImageData es probablemente más rápido que fillRect forma nativa. Creo que esto se debe a que el quinto parámetro puede tener diferentes formas de ser asignado (el color del rectángulo), utilizando una cadena que debe ser interpretada.

Supongamos que estás haciendo eso:

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

Entonces, la línea

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

Es el más pesado entre todos. El quinto argumento en la fillRectllamada es una cadena un poco más larga.


1
¿Qué navegador (s) admite pasar un color como el quinto argumento? Para Chrome tuve que usar context.fillStyle = ...en su lugar. developer.mozilla.org/en-US/docs/Web/API/…
iX3
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.