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?

Alnitak
fonte

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.

Phrogz
fonte
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);
PAEz
fonte
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.

Alnitak
fonte
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;
}
Vit Kaspar
fonte
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?

Daniel
fonte
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 .

Salvador Dalí
fonte
A direção sudeste? O que?
LoganDark
4

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

sdleihssirhc
fonte
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

ti
fonte
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();
trusktr
fonte
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.

Boing
fonte
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>
JMI MADISON
fonte
0

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

Martin Ždila
fonte
-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.

Kamil Kiełczewski
fonte
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.

Hydroper
fonte
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