Qual é a melhor maneira de definir um único pixel em uma tela HTML5?


184

O HTML5 Canvas não tem método para definir explicitamente um único pixel.

Pode ser possível definir um pixel usando uma linha muito curta, mas o antialising e os limites de linha podem interferir.

Outra maneira pode ser criar um ImageDataobjeto pequeno e usar:

context.putImageData(data, x, y)

para colocá-lo no lugar.

Alguém pode descrever uma maneira eficiente e confiável de fazer isso?

Respostas:


292

Existem dois melhores candidatos:

  1. Crie dados de imagem 1 × 1, defina a cor e putImageDatano local:

    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()para desenhar um pixel (não deve haver problemas de alias):

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

Você pode testar a velocidade destes aqui: http://jsperf.com/setting-canvas-pixel/9 ou aqui https://www.measurethat.net/Benchmarks/Show/1664/1

Eu recomendo testar nos navegadores de que você gosta para obter a velocidade máxima. Em julho de 2017, fillRect()é 5-6 × mais rápido no Firefox v54 e Chrome v59 (Win7x64).

Outras alternativas mais bobas são:

  • usando getImageData()/putImageData()em toda a tela; isso é cerca de 100 × mais lento que as outras opções.

  • criando uma imagem personalizada usando um URL de dados e usando drawImage()para mostrá-lo:

    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
  • criando outro img ou tela preenchida com todos os pixels desejados e usados drawImage()para mesclar apenas o pixel desejado. Provavelmente seria muito rápido, mas tem a limitação de que você precisa pré-calcular os pixels necessários.

Observe que meus testes não tentam salvar e restaurar o contexto da tela fillStyle; isso diminuiria o fillRect()desempenho. Observe também que não estou começando com uma lista limpa ou testando exatamente o mesmo conjunto de pixels para cada teste.


2
Eu daria a você mais 10, se pudesse, pelo preenchimento do relatório de erro! :)
— Alnitak

51
Observe que, na minha máquina com minha GPU e drivers gráficos, fillRect()semi-recentemente se tornou quase 10x mais rápido que os dados de imagem 1x1 no Chromev24. Então ... se a velocidade é crítica e você conhece seu público-alvo, não tome a palavra de uma resposta desatualizada (até a minha). Em vez disso: teste!
— Phrogz

3
Atualize a resposta. O método de preenchimento é muito mais rápido nos navegadores modernos.
— Buzzy

10
"Escrever o PNGEncoder é deixado como um exercício para o leitor" me fez rir alto.
— Pascal Ganaye

2
Por que todas as ótimas respostas do Canvas em que apareço são suas? :)
— Domino

19

Um método que não foi mencionado é usar getImageData e, em seguida, putImageData.
Este método é bom para quando você deseja desenhar muito de uma só 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 Dando-me um negativo por não conseguir ler sua mente, é baixo ... Outras pessoas podem chegar aqui procurando ser capazes de plotar muitos pixels. Eu fiz e depois me lembrei da maneira mais eficiente, então a compartilhei.
— PAEz

Este é um método sensato ao cutucar muitos pixels, para uma demonstração gráfica em que cada pixel é calculado ou semelhante. É dez vezes mais rápido do que usar fillRect para cada pixel.
— Sam Watkins

Sim, sempre meio que me incomodou que a resposta exceto diz que esse método é 100x mais lento que os outros métodos. Isso pode ser verdade se você estiver plotando menos de 1000, mas a partir daí, esse método começará a ganhar e a abater os outros métodos. Aqui está um caso de teste .... measurethat.net/Benchmarks/Show/8386/0/...
— Páez

17

Eu não tinha considerado fillRect(), mas as respostas me levaram a compará-lo putImage().

Colocar 100.000 pixels de cores aleatórias em locais aleatórios, com o Chrome 9.0.597.84 em um (antigo) MacBook Pro, leva menos de 100ms putImage(), mas quase 900ms de uso fillRect(). (Código de referência em http://pastebin.com/4ijVKJcC ).

Se, em vez disso, eu escolher uma única cor fora dos loops e apenas traçar essa cor em locais aleatórios, putImage()leva 59ms vs 102ms fillRect().

Parece que a sobrecarga de gerar e analisar uma especificação de cor CSS na rgb(...)sintaxe é responsável pela maior parte da diferença.

Colocar valores RGB brutos diretamente em um ImageDatabloco, por outro lado, não requer manipulação ou análise de string.


2
Adicionei um plunker onde você pode clicar em um botão e testar cada um dos métodos (PutImage, FillRect) e, adicionalmente, o método LineTo. Isso mostra que PutImage e FillRect são muito próximos, mas o LineTo é extremamente lento. Confira em: plnkr.co/edit/tww6e1VY2OCVY4c4ECy3?p=preview É baseado no seu ótimo código pastebin. Obrigado.
— Raddevus

Para esse afunilador, vejo que o PutImage é um pouco mais lento que o FillRect (no Chrome 63 mais recente), mas depois de experimentar o LineTo, o PutImage é significativamente mais rápido que o FillRect. De alguma forma, eles parecem estar interferindo.
— Mlepage

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

var index = (x + y * imageData.width) * 4;
— user889030

1
Deve chamar putImageData() depois dessa função ou o contexto será atualizado por referência?
— Lucas Sousa

7

Como navegadores diferentes parecem preferir métodos diferentes, talvez faça sentido fazer um teste menor com todos os três métodos como parte do processo de carregamento para descobrir qual é o melhor uso e usá-lo em todo o aplicativo?


5

Parece estranho, mas, mesmo assim, o HTML5 suporta o desenho de linhas, círculos, retângulos e muitas outras formas básicas; ele não possui nada adequado para desenhar o ponto básico. A única maneira de fazer isso é simular o ponto com o que você tiver.

Então, basicamente, existem 3 soluções possíveis:

  • desenhar ponto como uma linha
  • desenhar ponto como um polígono
  • desenhar ponto como um círculo

Cada um deles tem suas desvantagens


Linha

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

Lembre-se de que estamos seguindo na direção sudeste e, se esse for o limite, pode haver um problema. Mas você também pode desenhar em qualquer outra direção.


Retângulo

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

ou de maneira mais rápida usando fillRect, porque o mecanismo de renderização preencherá apenas um pixel.

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

Círculo


Um dos problemas com os círculos é que é mais difícil para um mecanismo processá-los

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

a mesma idéia do retângulo que você pode obter com o preenchimento.

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

Problemas com todas estas soluções:

  • é difícil acompanhar todos os pontos que você vai desenhar.
  • quando você aumenta o zoom, parece feio.

Se você está se perguntando: "Qual é a melhor maneira de desenhar um ponto? ", Eu usaria um retângulo preenchido. Você pode ver meu jsperf aqui com testes de comparação .


A direção sudeste? O que?
— LoganDark

4

Que tal um retângulo? Isso precisa ser mais eficiente do que criar um ImageDataobjeto.


3
Você acha que sim, e pode ser para um único pixel, mas se você pré-criar os dados da imagem e definir o 1 pixel e usá- putImageDatalo, é 10 vezes mais rápido que fillRectno Chrome. (Veja a minha resposta para mais.)
— Phrogz

2

Desenhe um retângulo como o sdleihssirhc disse!

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

^ - deve desenhar um retângulo 1x1 em x: 10, y: 10


1

Hmm, você também pode fazer uma linha de 1 pixel de largura com 1 pixel de comprimento e fazer com que a direção se mova ao longo de um único eixo.

            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
Eu implementei o desenho de pixel como FillRect, PutImage e LineTo e criei um plunker em: plnkr.co/edit/tww6e1VY2OCVY4c4ECy3?p=preview Confira, porque LineTo é exponencialmente mais lento. Pode fazer 100.000 pontos com outros 2 métodos em 0,25 segundos, mas 10.000 pontos com o LineTo leva 5 segundos.
— Raddevus

1
Ok, cometi um erro e gostaria de fechar o ciclo. Faltava o código LineTo - uma linha muito importante - que se parece com o seguinte: ctx.beginPath (); Atualizei o plunker (no link do meu outro comentário) e adicionando que uma linha agora permite que o método LineTo gere 100.000 em média de 0,5 segundos. Bastante surpreendente. Portanto, se você editar sua resposta e adicionar essa linha ao seu código (antes da linha ctx.lineWidth), eu votarei em você. Espero que você tenha achado isso interessante e peço desculpas pelo meu código de buggy original.
— Raddevus

1

Para completar a resposta completa do Phrogz, há uma diferença crítica entre fillRect()e putImageData().
O primeiro contexto usos para desenhar sobre por adição de um retângulo (não um pixel), usando o fillStyle valor alfa eo contexto globalAlpha ea matriz de transformação , tampas de linha etc ..
O segundo substitui todo um conjunto de pixels (talvez uma, mas por ?)
O resultado é diferente, como você pode ver no jsperf .


Ninguém deseja definir um pixel de cada vez (ou seja, desenhá-lo na tela). É por isso que não há API específica para fazer isso (e com razão).
Em termos de desempenho, se o objetivo é gerar uma imagem (por exemplo, um software de rastreamento de raios), você sempre deseja usar uma matriz obtida pela getImageData()qual é um Uint8Array otimizado. Então você liga putImageData()UMA VEZ ou algumas vezes por segundo usando setTimeout/seTInterval.


Eu tive um caso em que queria colocar 100 mil blocos em uma imagem, mas não na escala de 1: 1 pixel. O uso fillRectfoi doloroso porque a aceleração h / w do Chrome não consegue lidar com as chamadas individuais para a GPU que seriam necessárias. Acabei tendo que usar dados de pixel em 1: 1 e depois usar o dimensionamento CSS para obter a saída desejada. É feio :(
— Alnitak 12/03/2013

Executando seu benchmark vinculado no Firefox 42, recebo apenas 168 Ops / s por get/putImageData, mas 194.893 por fillRect. 1x1 image dataé 125.102 Ops / s. Então fillRectganha de longe no Firefox. Então, as coisas mudaram muito entre 2012 e hoje. Como sempre, nunca confie nos resultados de benchmark antigos.
— Mecki

12
Eu quero definir um pixel de cada vez. Eu estou supondo pelo título desta pergunta que outras pessoas também fazem
— chasmani

1

Código de demonstração HTML rápido: Com base no que sei sobre a biblioteca de gráficos SFML C ++:

Salve isso como um arquivo HTML com codificação UTF-8 e execute-o. Sinta-se livre para refatorar, eu apenas gosto de usar variáveis ​​japonesas porque elas são concisas e não ocupam muito espaço

Raramente você deseja definir UM pixel arbitrário e exibi-lo na tela. Então use o

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

método para desenhar vários pixels arbitrários em um buffer de fundo. (chamadas baratas)

Quando estiver pronto para mostrar, ligue para o

Apply() 

método para exibir as alterações. (chamada cara)

Código de arquivo .HTML completo abaixo:

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

Se você está preocupado com a velocidade, também pode considerar o WebGL.


-1

HANDY e proposição da função colocar pixel (pp) (ES6) (ler pixel aqui ):

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 função usa putImageDatae possui parte de inicialização (primeira linha longa). No início, em vezs='.myCanvas' use o seletor CSS na tela.

Se você deseja normalizar os parâmetros para valores de 0-1, deve alterar o valor padrão a=255para a=1e alinhar com: id.data.set([r,g,b,a]),ctx.putImageData(id, x, y) para id.data.set([r*255,g*255,b*255,a*255]),ctx.putImageData(id, x*c.width, y*c.height)

O código útil acima é bom para algoritmos gráficos de teste ad-hoc ou para fazer prova de conceito, mas não é bom usar na produção em que o código deve ser legível e claro.


1
Votação negativa para inglês ruim e um forro desordenado.
— xavier

1
@xavier - inglês não é minha língua nativa e eu não sou bom em aprender línguas estrangeiras, no entanto, você pode editar minha resposta e corrigir erros de idioma (será uma contribuição positiva sua). Eu coloquei esse verso porque é prático e fácil de usar - e pode ser bom, por exemplo, para os alunos testarem alguns algoritmos gráficos, no entanto, não é uma boa solução a ser usada na produção, onde o código deve ser legível e claro.
— Kamil Kiełczewski

3
@ KamilKiełczewski O código legível e claro é exatamente tão importante para os estudantes quanto para os profissionais.
— Logan Captura

-2

putImageData provavelmente é mais rápido que fillRect nativamente. Eu acho que isso porque o quinto parâmetro pode ter maneiras diferentes de serem atribuídas (a cor do retângulo), usando uma string que deve ser interpretada.

Suponha que você esteja fazendo isso:

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

Então a linha

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

é o mais pesado entre todos. O quinto argumento na fillRectchamada é uma string um pouco mais longa.


1
Quais navegadores suportam passar uma cor como o quinto argumento? Para o Chrome, tive que usar context.fillStyle = .... developer.mozilla.org/pt-BR/docs/Web/API/…
— iX3 4/17/17
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.