Qual è il modo migliore per impostare un singolo pixel in una tela HTML5?


184

La tela HTML5 non ha alcun metodo per impostare esplicitamente un singolo pixel.

Potrebbe essere possibile impostare un pixel usando una linea molto corta, ma quindi l'antializzazione e i limiti di linea potrebbero interferire.

Un altro modo potrebbe essere quello di creare un piccolo ImageDataoggetto e usare:

context.putImageData(data, x, y)

per metterlo in atto.

Qualcuno può descrivere un modo efficiente e affidabile per farlo?

Risposte:


292

Ci sono due migliori contendenti:

  1. Crea un dato immagine 1 × 1, imposta il colore e putImageDatanella posizione:

    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. Utilizzare fillRect()per disegnare un pixel (non dovrebbero esserci problemi di aliasing):

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

Puoi testare la velocità di questi qui: http://jsperf.com/setting-canvas-pixel/9 o qui https://www.measurethat.net/Benchmarks/Show/1664/1

Ti consiglio di testare con i browser che ti interessano per la massima velocità. A partire da luglio 2017,fillRect() è 5-6 × più veloce su Firefox v54 e Chrome v59 (Win7x64).

Altre alternative più sciocche sono:

  • usando getImageData()/putImageData()su tutta la tela; questo è circa 100 × più lento rispetto ad altre opzioni.

  • creando un'immagine personalizzata usando un url di dati e usando drawImage()per mostrarlo:

    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 un altro img o canvas riempito con tutti i pixel che vuoi e usa drawImage()per blitare solo il pixel che desideri. Questo sarebbe probabilmente molto veloce, ma ha il limite necessario per pre-calcolare i pixel necessari.

Nota che i miei test non tentano di salvare e ripristinare il contesto della tela fillStyle; questo rallenterebbe le fillRect()prestazioni. Si noti inoltre che non sto iniziando con una lavagna pulita o testando lo stesso identico set di pixel per ogni test.


2
Ti darei un altro +10 se potessi presentare la segnalazione di bug! :)
Alnitak,

51
Si noti che sulla mia macchina con la mia GPU e driver grafici, fillRect()semi-recentemente è diventato quasi 10 volte più veloce rispetto alla putimagedata 1x1 su Chromev24. Quindi ... se la velocità è fondamentale e conosci il tuo pubblico di destinazione, non prendere la parola di una risposta obsoleta (anche la mia). Invece: prova!
Phrogz,

3
Si prega di aggiornare la risposta. Il metodo di riempimento è molto più veloce sui browser moderni.
Buzzy,

10
"Scrivere il PNGEncoder è lasciato come esercizio per il lettore" mi ha fatto ridere ad alta voce.
Pascal Ganaye,

2
Perché tutte le grandi risposte su tela su cui atterro sono capitate da te? :)
Domino,

19

Un metodo che non è stato menzionato sta usando getImageData e quindi putImageData.
Questo metodo è utile quando vuoi disegnare molto in una volta sola, velocemente.
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 Dandomi un segno negativo per non essere in grado di leggere la tua mente, è basso ... Altre persone potrebbero arrivare qui cercando di essere in grado di tracciare molti pixel. L'ho fatto e poi mi sono ricordato del modo più efficiente, quindi l'ho condiviso.
PAEz,

Questo è un metodo sensato quando si prendono in giro molti pixel, per una demo grafica in cui viene calcolato ogni pixel o simile. È dieci volte più veloce dell'utilizzo di fillRect per ogni pixel.
Sam Watkins,

Sì, mi ha sempre infastidito il fatto che la risposta esclusa dica che questo metodo è 100 volte più lento degli altri metodi. Questo può essere vero se stai pianificando meno di 1000, ma da lì in poi questo metodo inizia a vincere e quindi a macellare gli altri metodi. Ecco un caso di prova .... measurethat.net/Benchmarks/Show/8386/0/…
PAEz

17

Non avevo considerato fillRect(), ma le risposte mi hanno spronato a confrontarlo putImage().

Mettere 100.000 pixel a colori casuali in posizioni casuali, con Chrome 9.0.597.84 su un (vecchio) MacBook Pro, richiede meno di 100ms putImage(), ma quasi 900ms usando fillRect(). (Codice benchmark su http://pastebin.com/4ijVKJcC ).

Se invece scelgo un singolo colore al di fuori dei loop e lo putImage()ploto solo in posizioni casuali, impiega 59ms contro 102ms perfillRect() .

Sembra che il sovraccarico di generare e analizzare una specifica di colore CSS in rgb(...) sintassi sia responsabile della maggior parte della differenza.

ImageDataD'altra parte, l' inserimento di valori RGB grezzi in un blocco non richiede alcuna gestione o analisi delle stringhe.


2
Ho aggiunto un plunker in cui è possibile fare clic su un pulsante e testare ciascuno dei metodi (PutImage, FillRect) e inoltre il metodo LineTo. Mostra che PutImage e FillRect sono molto vicini nel tempo ma LineTo è estremamente lento. Dai un'occhiata a: plnkr.co/edit/tww6e1VY2OCVY4c4ECy3?p=preview È basato sul tuo ottimo codice pastebin. Grazie.
Raddevus,

Per quel plunker, vedo PutImage è leggermente più lento di FillRect (sull'ultimo Chrome 63), ma dopo aver provato LineTo, quindi PutImage è significativamente più veloce di FillRect. In qualche modo sembrano interferire.
mlepage del

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

indice var = (x + y * imageData.width) * 4;
utente889030

1
Dovresti chiamare putImageData() dopo quella funzione o il contesto verrà aggiornato per riferimento?
Lucas Sousa

7

Poiché diversi browser sembrano preferire metodi diversi, forse avrebbe senso fare un test più piccolo con tutti e tre i metodi come parte del processo di caricamento per scoprire quale sia il migliore da utilizzare e quindi utilizzarlo in tutta l'applicazione?


5

Sembra strano, ma HTML5 supporta comunque il disegno di linee, cerchi, rettangoli e molte altre forme di base, non ha nulla di adatto per disegnare il punto base. L'unico modo per farlo è simulare il punto con qualunque cosa tu abbia.

Quindi sostanzialmente ci sono 3 possibili soluzioni:

  • disegnare il punto come una linea
  • disegnare il punto come un poligono
  • disegnare il punto come un cerchio

Ognuno di loro ha i suoi svantaggi


Linea

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

Tieni presente che stiamo andando verso la direzione sud-est e, se questo è il limite, potrebbe esserci un problema. Ma puoi anche disegnare in qualsiasi altra direzione.


Rettangolo

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

o in un modo più veloce usando fillRect perché il motore di rendering riempirà solo un pixel.

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

Cerchio


Uno dei problemi con i cerchi è che per un motore è più difficile renderli

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

la stessa idea del rettangolo che puoi ottenere con il riempimento.

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

Problemi con tutte queste soluzioni:

  • è difficile tenere traccia di tutti i punti che traccerai.
  • quando ingrandisci, sembra brutto.

Se ti stai chiedendo "Qual è il modo migliore per disegnare un punto? ", Andrei con il rettangolo pieno. Puoi vedere il mio jsperf qui con i test di confronto .


La direzione sud-est? Che cosa?
Logan Dark

4

Che dire di un rettangolo? Deve essere più efficiente della creazione di un ImageDataoggetto.


3
Penseresti di sì, e potrebbe essere per un singolo pixel, ma se pre-crei i dati dell'immagine e imposti il ​​1 pixel e poi lo usi putImageDataè 10 volte più veloce rispetto fillRecta Chrome. (Vedi la mia risposta per ulteriori informazioni.)
Phrogz,

2

Disegna un rettangolo come diceva sdleihssirhc!

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

^ - dovrebbe disegnare un rettangolo 1x1 in x: 10, y: 10


1

Hmm, potresti anche solo fare una linea larga 1 pixel con una lunghezza di 1 pixel e far muovere la sua direzione lungo un singolo asse.

            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
Ho implementato il pixel draw come FillRect, PutImage e LineTo e ho creato un plunker su: plnkr.co/edit/tww6e1VY2OCVY4c4ECy3?p=preview Dai un'occhiata, perché LineTo è esponenzialmente più lento. Può fare 100.000 punti con altri 2 metodi in 0,25 secondi, ma 10.000 punti con LineTo impiegano 5 secondi.
Raddevus,

1
Ok, ho fatto un errore e vorrei chiudere il circuito. Nel codice LineTo mancava uno - riga molto importante - che assomiglia al seguente: ctx.beginPath (); Ho aggiornato il plunker (al link del mio altro commento) e aggiungendo che una riga ora consente al metodo LineTo di generare 100.000 in 0,5 secondi in media. Abbastanza sorprendente Quindi se modificherai la tua risposta e aggiungerai quella riga al tuo codice (prima della riga ctx.lineWidth) ti voterò. Spero che tu l'abbia trovato interessante e mi scuso per il mio codice buggy originale.
Raddevus,

1

Per completare la risposta molto approfondita di Phrogz, esiste una differenza critica tra fillRect()e putImageData().
Il primo contesto usi disegnare sopra da aggiungendo un rettangolo (non un pixel), utilizzando il fillStyle valore alfa e il contesto globalAlpha e la matrice di trasformazione , cappellini pubblicitari ecc ..
Il secondo sostituisce un intero insieme di pixel (forse uno, ma perché ?)
Il risultato è diverso come puoi vedere su jsperf .


Nessuno vuole impostare un pixel alla volta (ovvero disegnarlo sullo schermo). Ecco perché non esiste un'API specifica per farlo (e giustamente).
Per quanto riguarda le prestazioni, se l'obiettivo è generare un'immagine (ad esempio un software di ray-tracing), si desidera sempre utilizzare un array ottenuto da getImageData()un Uint8Array ottimizzato. Quindi si chiama putImageData()ONCE o alcune volte al secondo utilizzando setTimeout/seTInterval.


Ho avuto un caso in cui volevo mettere 100k blocchi in un'immagine, ma non in scala 1: 1 pixel. L'uso è fillRectstato doloroso perché l'accelerazione h / w di Chrome non può far fronte alle singole chiamate alla GPU che richiederebbe. Ho finito per usare i dati pixel a 1: 1 e quindi usare il ridimensionamento CSS per ottenere l'output desiderato. È brutto :(
Alnitak,

Eseguendo il benchmark collegato su Firefox 42 ottengo solo 168 Ops / sec per get/putImageData, ma 194.893 per fillRect. 1x1 image dataè 125.102 Ops / sec. Quindi fillRectvince di gran lunga su Firefox. Quindi le cose sono cambiate molto tra il 2012 e oggi. Come sempre, non fare mai affidamento sui vecchi risultati di benchmark.
Mecki,

12
Voglio impostare un pixel alla volta. Sto indovinando dal titolo di questa domanda che lo fanno anche altre persone
Chasmani,

1

Codice demo HTML veloce: basato su ciò che so della libreria grafica SFML C ++:

Salvalo come file HTML con codifica UTF-8 ed eseguilo. Sentiti libero di refactoring, mi piace semplicemente usare le variabili giapponesi perché sono concise e non occupano molto spazio

Raramente vorrai impostare UN pixel arbitrario e visualizzarlo sullo schermo. Quindi usa il

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

metodo per disegnare numerosi pixel arbitrari su un back-buffer. (chiamate economiche)

Quindi, quando sei pronto per mostrare, chiama il

Apply() 

metodo per visualizzare le modifiche. (chiamata costosa)

Codice file .HTML completo di seguito:

<!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 sei preoccupato per la velocità, puoi anche prendere in considerazione WebGL.


-1

HANDY e proposta della funzione put pixel (pp) (ES6) (leggi il pixel qui ):

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

Questa funzione usa putImageDatae ha una parte di inizializzazione (prima linea lunga). All'inizio invece s='.myCanvas'usa il tuo selettore CSS sulla tua tela.

Se vuoi normalizzare i parametri per un valore compreso tra 0 e 1, dovresti cambiare il valore predefinito a=255in a=1e allineare 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)

Il pratico codice sopra è utile per testare algoritmi di grafica ad hoc o per rendere l'idea del concetto, ma non è buono da usare in produzione dove il codice dovrebbe essere leggibile e chiaro.


1
Sotto votato a favore dell'inglese povero e di una fodera ingombra.
xavier,

1
@xavier - l'inglese non è la mia lingua madre e non sono bravo a imparare le lingue scontate, tuttavia puoi modificare la mia risposta e correggere i bug di lingua (sarà un tuo contributo positivo). Ho messo questo one-liner perché è pratico e facile da usare e può essere utile, ad esempio, per gli studenti per testare alcuni algoritmi grafici, tuttavia non è una buona soluzione da utilizzare nella produzione in cui il codice dovrebbe essere leggibile e chiaro.
Kamil Kiełczewski,

3
@ KamilKiełczewski La lettura e la chiarezza del codice è tanto importante per gli studenti quanto per i professionisti.
Logan Pickup,

-2

putImageDataè probabilmente più veloce che fillRectnativamente. Penso che questo perché il quinto parametro può avere diversi modi di essere assegnato (il colore del rettangolo), usando una stringa che deve essere interpretata.

Supponiamo che lo stia facendo:

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

Quindi, la linea

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

è il più pesante tra tutti. Il quinto argomento della fillRectchiamata è una stringa un po 'più lunga.


1
Quale browser supporta il passaggio di un colore come quinto argomento? Per Chrome ho dovuto usare context.fillStyle = ...invece. developer.mozilla.org/en-US/docs/Web/API/…
iX3
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.