negrita - ¿Cuál es la mejor manera de establecer un solo píxel en un lienzo de HTML5?
negrita en html5 (12)
¿Qué tal un rectángulo? Eso tiene que ser más eficiente que crear un objeto ImageData
.
HTML5 Canvas no tiene ningún método para establecer explícitamente un solo píxel.
Podría ser posible establecer un píxel utilizando una línea muy corta, pero luego antializar y límites de línea podrían interferir.
Otra forma podría ser crear un pequeño objeto ImageData
y usar:
context.putImageData(data, x, y)
para ponerlo en su lugar.
¿Alguien puede describir una manera eficiente y confiable de hacer esto?
Dado que diferentes navegadores parecen preferir métodos diferentes, tal vez tendría 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 usar eso en toda la aplicación.
Dibuja un rectángulo como dijo sdleihssirhc!
ctx.fillRect (10, 10, 1, 1);
^ - debería dibujar un rectángulo de 1x1 en x: 10, y: 10
Hay dos mejores contendientes:
Cree datos de imagen 1 × 1, configure el color y
putImageData
en 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 );
Use
fillRect()
para dibujar un píxel (no debería haber problemas de aliasing):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 contra los navegadores que te 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:
usando
getImageData()/putImageData()
en todo el lienzo; esto es aproximadamente 100 veces más lento que otras opciones.creando una imagen personalizada usando una url de datos y usando
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 con todos los píxeles que desee y use
drawImage()
para ajustar solo el píxel que desee. Esto probablemente sea muy rápido, pero tiene la limitación de que necesita precalcular los píxeles que necesita.
Tenga en cuenta que mis pruebas no intentan guardar y restaurar el contexto canvas fillStyle
; esto ralentizaría el rendimiento de fillRect()
. También tenga en cuenta que no estoy empezando con un borrón y cuenta nueva o probando el mismo conjunto de píxeles para cada prueba.
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();
No había considerado fillRect()
, pero las respuestas me putImage()
a compararlo con putImage()
.
Al colocar 100.000 píxeles de colores aleatorios en ubicaciones aleatorias, con Chrome 9.0.597.84 en una (vieja) MacBook Pro, toma menos de putImage()
con putImage()
, pero casi 900ms con 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 para fillRect()
.
Parece que la sobrecarga de generar y analizar una especificación de color CSS en la sintaxis rgb(...)
es responsable de la mayor parte de la diferencia.
Sin ImageData
poner valores RGB sin ImageData
directamente en un bloque ImageData
no requiere manejo de cadenas o análisis sintáctico.
Para completar la respuesta completa de Phrogz, hay una diferencia crítica entre fillRect()
y putImageData()
.
El primero usa el contexto para dibujar agregando un rectángulo (NO un píxel), usando el valor alfa de fillStyle Y el contexto global Alfa y la matriz de transformación , límites de línea, etc.
El segundo reemplaza un conjunto completo de píxeles (tal vez uno, pero ¿por qué?)
El resultado es diferente, como puedes 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 hacer eso (y con razón).
En lo que respecta al rendimiento, si el objetivo es generar una imagen (por ejemplo, un software de trazado de rayos), siempre debe usar una matriz obtenida por getImageData()
que es una Uint8Array optimizada. A continuación, llama a putImageData()
ONCE o unas pocas veces por segundo usando setTimeout/seTInterval
.
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 simular el punto con lo que tenga.
Entonces, 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 Sudeste, y si este es el límite, 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 para un motor representarlos
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 vas a dibujar.
- cuando haces zoom, se ve feo.
Si te estás preguntando, "¿Cuál es la mejor manera de dibujar un punto? ", Me gustaría ir con el rectángulo lleno. Puedes ver mi jsperf aquí con pruebas de comparación .
Si le preocupa la velocidad, también podría considerar WebGL.
putImageData
es probablemente más rápido que fillRect
forma nativa. Creo que esto porque el quinto parámetro puede tener diferentes formas de ser asignado (el color del rectángulo), usando 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 linea
context.fillRect(x, y, 1, 1, "rgba(255, 255, 255, 0.5)")`
es el más pesado entre todos. El quinto argumento en la llamada fillRect
es una cadena un poco más larga.
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 UTF-8 Encoding y ejecútelo. No dude en refactorizar, solo me gusta usar variables japonesas porque son concisas y no ocupan mucho espacio
Raramente va a querer 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 a un back-buffer. (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>
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;
}