Was ist der beste Weg, um ein einzelnes Pixel in einer HTML5-Zeichenfläche festzulegen?

183

Der HTML5-Canvas verfügt über keine Methode zum expliziten Festlegen eines einzelnen Pixels.

Es ist möglicherweise möglich, ein Pixel mit einer sehr kurzen Linie zu setzen, aber dann können Antialisierung und Linienkappen stören.

Eine andere Möglichkeit könnte darin bestehen, ein kleines ImageDataObjekt zu erstellen und Folgendes zu verwenden:

context.putImageData(data, x, y)

um es in Position zu bringen.

Kann jemand einen effizienten und zuverlässigen Weg beschreiben, dies zu tun?

Alnitak
quelle

Antworten:

291

Es gibt zwei beste Anwärter:

  1. Erstellen Sie 1 × 1-Bilddaten, stellen Sie die Farbe ein und legen Sie putImageDataFolgendes fest:

    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. Verwenden Sie fillRect()diese Option, um ein Pixel zu zeichnen (es sollten keine Aliasing-Probleme auftreten):

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

Sie können die Geschwindigkeit hier testen: http://jsperf.com/setting-canvas-pixel/9 oder hier https://www.measurethat.net/Benchmarks/Show/1664/1

Ich empfehle, mit Browsern zu testen, die Ihnen wichtig sind, um maximale Geschwindigkeit zu erzielen. Ab Juli 2017 fillRect()ist Firefox v54 und Chrome v59 (Win7x64) 5-6 × schneller.

Andere, albernere Alternativen sind:

  • Verwenden getImageData()/putImageData()auf der gesamten Leinwand; Dies ist ungefähr 100 × langsamer als bei anderen Optionen.

  • Erstellen eines benutzerdefinierten Bildes mithilfe einer Daten-URL und Anzeigen drawImage():

    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
  • Erstellen Sie ein weiteres Bild oder eine Leinwand, die mit allen gewünschten Pixeln gefüllt ist, und verwenden Sie drawImage()diese Option, um nur die gewünschten Pixel zu verteilen. Dies wäre wahrscheinlich sehr schnell, hat jedoch die Einschränkung, dass Sie die benötigten Pixel vorberechnen müssen.

Beachten Sie, dass meine Tests nicht versuchen, den Canvas-Kontext zu speichern und wiederherzustellen fillStyle. Dies würde die fillRect()Leistung verlangsamen . Beachten Sie auch, dass ich nicht mit einer sauberen Tafel beginne oder für jeden Test genau den gleichen Satz von Pixeln teste.

Phrogz
quelle
2
Ich würde dir noch +10 geben, wenn ich den Fehlerbericht einreichen könnte! :)
Alnitak
51
Beachten Sie, dass auf meinem Computer mit meinen GPU- und Grafiktreibern vor fillRect()kurzem fast 10-mal schneller als die 1x1-Putimagedata auf Chromev24. Also ... wenn Geschwindigkeit entscheidend ist und Sie Ihre Zielgruppe kennen, nehmen Sie nicht das Wort einer veralteten Antwort (auch meiner). Stattdessen: testen!
Phrogz
3
Bitte aktualisieren Sie die Antwort. Die Füllmethode ist in modernen Browsern viel schneller.
Buzzy
10
"Das Schreiben des PNGEncoders ist eine Übung für den Leser", brachte mich zum Lachen.
Pascal Ganaye
2
Warum sind alle großartigen Canvas-Antworten, auf denen ich lande, zufällig von Ihnen? :)
Domino
18

Eine Methode, die nicht erwähnt wurde, ist die Verwendung von getImageData und dann putImageData.
Diese Methode ist gut geeignet, wenn Sie schnell viel auf einmal zeichnen möchten.
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
quelle
13
@Alnitak Es ist gering, mir ein Neg zu geben, weil ich deine Gedanken nicht lesen kann. Andere Leute könnten hierher kommen, um viele Pixel zeichnen zu können. Ich tat es und erinnerte mich dann an den effizienteren Weg, also teilte ich ihn.
PAEz
Dies ist eine sinnvolle Methode, wenn viele Pixel gestochen werden, für eine Grafikdemo, bei der jedes Pixel berechnet wird, oder ähnlich. Es ist zehnmal schneller als die Verwendung von fillRect für jedes Pixel.
Sam Watkins
Ja, es hat mich immer irgendwie gestört, dass die ausgenommene Antwort besagt, dass diese Methode 100x langsamer ist als die anderen Methoden. Dies mag zutreffen, wenn Sie weniger als 1000 planen, aber von da an beginnt diese Methode zu gewinnen und dann die anderen Methoden zu schlachten. Hier ist ein Testfall .... messethat.net/Benchmarks/Show/8386/0/…
PAEz
17

Ich hatte nicht darüber nachgedacht fillRect(), aber die Antworten spornten mich an, es zu vergleichen putImage().

Das Platzieren von 100.000 zufällig gefärbten Pixeln an zufälligen Orten mit Chrome 9.0.597.84 auf einem (alten) MacBook Pro dauert weniger als 100 ms putImage(), aber fast 900 ms fillRect(). (Benchmark-Code unter http://pastebin.com/4ijVKJcC ).

Wenn ich stattdessen eine einzelne Farbe außerhalb der Schleifen auswähle und diese Farbe nur an zufälligen Stellen putImage()zeichne , dauert dies 59 ms gegenüber 102 ms fillRect().

Es scheint, dass der Aufwand für das Generieren und Parsen einer CSS-Farbspezifikation in der rgb(...)Syntax für den größten Teil des Unterschieds verantwortlich ist.

Das direkte Einfügen von rohen RGB-Werten in einen ImageDataBlock erfordert hingegen keine Behandlung oder Analyse von Zeichenfolgen.

Alnitak
quelle
2
Ich habe einen Plunker hinzugefügt, in dem Sie auf eine Schaltfläche klicken und jede der Methoden (PutImage, FillRect) und zusätzlich die LineTo-Methode testen können. Es zeigt, dass PutImage und FillRect zeitlich sehr nahe beieinander liegen, LineTo jedoch extrem langsam ist. Überprüfen Sie es auf: plnkr.co/edit/tww6e1VY2OCVY4c4ECy3?p=preview Es basiert auf Ihrem großartigen Pastebin-Code. Vielen Dank.
Raddevus
Für diesen Plunker sehe ich, dass PutImage etwas langsamer als FillRect ist (auf dem neuesten Chrome 63), aber nachdem ich LineTo ausprobiert habe, ist PutImage deutlich schneller als FillRect. Irgendwie scheinen sie sich einzumischen.
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
quelle
var index = (x + y * imageData.width) * 4;
Benutzer889030
1
Sollte putImageData() nach dieser Funktion aufgerufen werden oder wird der Kontext durch Referenz aktualisiert?
Lucas Sousa
7

Da unterschiedliche Browser unterschiedliche Methoden zu bevorzugen scheinen, ist es möglicherweise sinnvoll, im Rahmen des Ladevorgangs einen kleineren Test mit allen drei Methoden durchzuführen, um herauszufinden, welche am besten zu verwenden ist, und diese dann in der gesamten Anwendung zu verwenden.

Daniel
quelle
5

Es scheint seltsam, aber dennoch unterstützt HTML5 das Zeichnen von Linien, Kreisen, Rechtecken und vielen anderen Grundformen. Es gibt nichts, was zum Zeichnen des Grundpunkts geeignet ist. Die einzige Möglichkeit, dies zu tun, besteht darin, Punkte mit allem zu simulieren, was Sie haben.

Grundsätzlich gibt es also 3 mögliche Lösungen:

  • Punkt als Linie zeichnen
  • Punkt als Polygon zeichnen
  • Punkt als Kreis zeichnen

Jeder von ihnen hat seine Nachteile


Linie

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

Denken Sie daran, dass wir in Richtung Südosten ziehen. Wenn dies der Rand ist, kann es ein Problem geben. Sie können aber auch in eine andere Richtung zeichnen.


Rechteck

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

oder schneller mit fillRect, da die Render-Engine nur ein Pixel ausfüllt.

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

Kreis


Eines der Probleme mit Kreisen ist, dass es für eine Engine schwieriger ist, sie zu rendern

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

die gleiche Idee wie mit Rechteck, die Sie mit Füllung erreichen können.

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

Probleme mit all diesen Lösungen:

  • Es ist schwierig, alle Punkte im Auge zu behalten, die Sie ziehen werden.
  • Wenn Sie hineinzoomen, sieht es hässlich aus.

Wenn Sie sich fragen: "Was ist der beste Weg, um einen Punkt zu zeichnen? ", Würde ich mit einem gefüllten Rechteck gehen. Sie können mein jsperf hier mit Vergleichstests sehen .

Salvador Dali
quelle
Die südöstliche Richtung? Was?
LoganDark
4

Was ist mit einem Rechteck? Das muss effizienter sein als das Erstellen eines ImageDataObjekts.

sdleihssirhc
quelle
3
Sie würden es glauben, und es könnte für ein einzelnes Pixel sein, aber wenn Sie die Bilddaten vorab erstellen und das 1 Pixel einstellen und es dann verwenden putImageData, ist es 10x schneller als fillRectin Chrome. (Siehe meine Antwort für mehr.)
Phrogz
2

Zeichne ein Rechteck wie sdleihssirhc sagte!

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

^ - sollte ein 1x1-Rechteck bei x: 10, y: 10 zeichnen

dich
quelle
1

Hmm, Sie können auch einfach eine 1 Pixel breite Linie mit einer Länge von 1 Pixel erstellen und die Richtung entlang einer einzelnen Achse verschieben.

            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
quelle
1
Ich habe die Pixelzeichnung als FillRect, PutImage und LineTo implementiert und einen Plunker erstellt unter: plnkr.co/edit/tww6e1VY2OCVY4c4ECy3?p=preview Probieren Sie es aus, da LineTo exponentiell langsamer ist. Kann 100.000 Punkte mit anderen 2 Methoden in 0,25 Sekunden erzielen, aber 10.000 Punkte mit LineTo dauern 5 Sekunden.
Raddevus
1
Okay, ich habe einen Fehler gemacht und möchte die Schleife schließen. Dem LineTo-Code fehlte eine - sehr wichtige - Zeile, die wie folgt aussieht: ctx.beginPath (); Ich habe den Plunker aktualisiert (unter dem Link aus meinem anderen Kommentar) und hinzugefügt, dass eine Zeile jetzt der LineTo-Methode ermöglicht, 100.000 in einem Durchschnitt von 0,5 Sekunden zu generieren. Ziemlich beeindruckend. Wenn Sie also Ihre Antwort bearbeiten und diese Zeile zu Ihrem Code hinzufügen (vor der Zeile ctx.lineWidth), werde ich Sie positiv bewerten. Ich hoffe, Sie fanden das interessant und entschuldige mich für meinen ursprünglichen Buggy-Code.
Raddevus
1

Um Phrogz sehr gründliche Antwort zu vervollständigen, gibt es einen kritischen Unterschied zwischen fillRect()und putImageData().
Der erste Einsatzkontext zu ziehen über durch das Hinzufügen eines Rechtecks (nicht um ein Pixel), die unter Verwendung von fillStyle Alpha - Wert und den Kontext globalAlpha und die Transformationsmatrix , Zeile Kappen etc ..
Die zweiten ersetzt eine ganze Reihe von Pixeln (vielleicht ein, aber warum ?)
Das Ergebnis ist anders, wie Sie auf jsperf sehen können .


Niemand möchte jeweils ein Pixel einstellen (dh auf dem Bildschirm zeichnen). Aus diesem Grund gibt es keine spezifische API, um dies zu tun (und das zu Recht).
In Bezug auf die Leistung möchten Sie, wenn das Ziel darin besteht, ein Bild zu generieren (z. B. eine Raytracing-Software), immer ein Array verwenden, das von getImageData()einem optimierten Uint8Array erhalten wird. Dann rufen Sie putImageData()EINMAL oder einige Male pro Sekunde mit an setTimeout/seTInterval.

Boing
quelle
Ich hatte einen Fall, in dem ich 100.000 Blöcke in ein Bild einfügen wollte, aber nicht im Maßstab 1: 1. Die Verwendung fillRectwar schmerzhaft, da die H / W-Beschleunigung von Chrome die einzelnen Aufrufe der erforderlichen GPU nicht bewältigen kann. Am Ende musste ich Pixeldaten im Verhältnis 1: 1 verwenden und dann die CSS-Skalierung verwenden, um die gewünschte Ausgabe zu erhalten. Es ist hässlich :(
Alnitak
Wenn Sie Ihren verknüpften Benchmark auf Firefox 42 get/putImageDataausführen, erhalte ich nur 168 Ops / Sek. , Aber 194.893 für fillRect. 1x1 image databeträgt 125.102 Ops / Sek. So fillRectgewinnt bei weitem in Firefox. Zwischen 2012 und heute haben sich die Dinge also sehr verändert. Verlassen Sie sich wie immer niemals auf alte Benchmark-Ergebnisse.
Mecki
12
Ich möchte jeweils ein Pixel einstellen. Ich vermute durch den Titel dieser Frage, dass andere Leute auch tun
Chasmani
1

Schneller HTML-Demo-Code: Basierend auf dem, was ich über die SFML C ++ - Grafikbibliothek weiß:

Speichern Sie diese als HTML-Datei mit UTF-8-Codierung und führen Sie sie aus. Fühlen Sie sich frei zu refaktorieren, ich mag es einfach, japanische Variablen zu verwenden, weil sie präzise sind und nicht viel Platz beanspruchen

In seltenen Fällen möchten Sie EIN beliebiges Pixel einstellen und auf dem Bildschirm anzeigen. Also benutze die

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

Verfahren zum Zeichnen zahlreicher beliebiger Pixel in einen Rückpuffer. (billige Anrufe)

Wenn Sie bereit sind zu zeigen, rufen Sie die

Apply() 

Methode zum Anzeigen der Änderungen. (teurer Anruf)

Vollständiger HTML-Dateicode unten:

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

Wenn Sie über die Geschwindigkeit besorgt sind, können Sie auch WebGL in Betracht ziehen.

Martin Ždila
quelle
-1

HANDY und Vorschlag der Put-Pixel (pp) -Funktion (ES6) ( Lesepixel hier ):

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

Diese Funktion verwendet putImageDataund hat Initialisierungsteil (erste lange Zeile). s='.myCanvas'Verwenden Sie zu Beginn stattdessen Ihren CSS-Selektor für Ihre Leinwand.

Wenn Sie die Parameter auf einen Wert von 0-1 normalisieren möchten, sollten Sie den Standardwert a=255in ändern a=1und mit: id.data.set([r,g,b,a]),ctx.putImageData(id, x, y)bis id.data.set([r*255,g*255,b*255,a*255]),ctx.putImageData(id, x*c.width, y*c.height)

Der obige praktische Code eignet sich gut zum Ad-hoc-Testen von Grafikalgorithmen oder zum Proof of Concept. Er eignet sich jedoch nicht für die Verwendung in der Produktion, in der Code lesbar und klar sein sollte.

Kamil Kiełczewski
quelle
1
Für schlechtes Englisch und einen überfüllten Einzeiler herabgestimmt.
Xavier
1
@xavier - Englisch ist nicht meine Muttersprache und ich kann keine guten Fremdsprachen lernen. Sie können jedoch meine Antwort bearbeiten und Sprachfehler beheben (dies wird ein positiver Beitrag von Ihnen sein). Ich habe diesen Einzeiler verwendet, weil er praktisch und einfach zu verwenden ist - und zum Beispiel für Schüler gut sein kann, um einige Grafikalgorithmen zu testen. Es ist jedoch keine gute Lösung, um in der Produktion verwendet zu werden, in der Code lesbar und klar sein sollte.
Kamil Kiełczewski
3
@ KamilKiełczewski Code lesbar und klar zu sein, ist für Studenten genauso wichtig wie für Profis.
Logan Pickup
-2

putImageDataist wahrscheinlich schneller als fillRectnativ. Ich denke, das liegt daran, dass der fünfte Parameter auf verschiedene Arten zugewiesen werden kann (die Rechteckfarbe), wobei eine Zeichenfolge verwendet wird, die interpretiert werden muss.

Angenommen, Sie tun das:

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

Also die Linie

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

ist das schwerste zwischen allen. Das fünfte Argument im fillRectAufruf ist eine etwas längere Zeichenfolge.

Hydroper
quelle
1
Welche Browser unterstützen die Übergabe einer Farbe als fünftes Argument? Für Chrome musste ich context.fillStyle = ...stattdessen verwenden. developer.mozilla.org/en-US/docs/Web/API/…
iX3