web-dev-qa-db-fra.com

Quel est le meilleur moyen de définir un seul pixel dans un canevas HTML5?

Le canevas HTML5 ne contient aucune méthode permettant de définir explicitement un seul pixel.

Il est peut-être possible de définir un pixel en utilisant une ligne très courte, mais l'antialising et les fins de ligne risquent d'interférer.

Une autre solution pourrait être de créer un petit objet ImageData et d’utiliser:

context.putImageData(data, x, y)

pour le mettre en place.

Quelqu'un peut-il décrire un moyen efficace et fiable de le faire?

170
Alnitak

Il y a deux meilleurs prétendants:

  1. Créez une image 1 × 1, définissez la couleur et putImageData à l'emplacement:

    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. Utilisez fillRect() pour dessiner un pixel (il ne devrait pas y avoir de problème de repliement du spectre):

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

Vous pouvez tester leur vitesse ici: http://jsperf.com/setting-canvas-pixel/9 ou ici https://www.measurethat.net/Benchmarks/Show/1664/1

Je recommande de tester contre les navigateurs qui vous intéressent pour une vitesse maximale. À partir de juillet 2017, fillRect() est 5-6 × plus rapide sur Firefox. v54 et Chrome v59 (Win7x64).

Les autres solutions les plus saines sont:

  • en utilisant getImageData()/putImageData() sur toute la toile; c'est environ 100 × plus lent que les autres options.

  • créer une image personnalisée en utilisant une URL de données et en utilisant drawImage() pour l'afficher:

    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
    
  • créez une autre img ou une toile remplie de tous les pixels souhaités et utilisez drawImage() pour insérer le pixel de votre choix. Ce serait probablement très rapide, mais avec la limitation que vous devez pré-calculer les pixels dont vous avez besoin.

Notez que mes tests ne tentent pas de sauvegarder et de restaurer le contexte de la toile fillStyle; cela ralentirait la performance fillRect(). Notez également que je ne commence pas avec une table rase ni ne teste exactement le même ensemble de pixels pour chaque test.

270
Phrogz

Je n'avais pas envisagé fillRect(), mais les réponses m'ont incité à le comparer à putImage().

Placer 100 000 pixels de couleur aléatoire à des emplacements aléatoires, avec Chrome 9.0.597.84 sur un (ancien) MacBook Pro, prend moins de 100 ms avec putImage(), mais près de 900 ms avec fillRect(). (Code de référence à http://Pastebin.com/4ijVKJcC ).

Si, au lieu de cela, je choisis une seule couleur en dehors des boucles et que je trace simplement cette couleur à des emplacements aléatoires, putImage() prend 59 ms contre 102 ms pour fillRect().

Il semble que la surcharge de la génération et de l'analyse d'une spécification de couleur CSS dans la syntaxe rgb(...) soit responsable de la majeure partie de la différence.

Mettre les valeurs RVB brutes directement dans un bloc ImageData ne nécessite en revanche aucune manipulation ou analyse de chaîne.

15
Alnitak
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;
}
11
Vit Kaspar

Étant donné que différents navigateurs semblent préférer des méthodes différentes, il serait peut-être judicieux de faire un test plus petit avec les trois méthodes dans le cadre du processus de chargement afin de déterminer lequel est le mieux à utiliser, puis de l'utiliser dans l'application.

7
Daniel

Qu'en est-il d'un rectangle? Cela doit être plus efficace que de créer un objet ImageData.

4
sdleihssirhc

Une méthode qui n'a pas été mentionnée utilise getImageData puis putImageData.
Cette méthode est utile lorsque vous souhaitez dessiner beaucoup en une seule fois, rapidement.
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);
4
PAEz

Cela semble étrange, mais malgré tout, HTML5 prend en charge le dessin de lignes, de cercles, de rectangles et de nombreuses autres formes de base, il n’a rien qui convienne pour dessiner le point de base. La seule façon de le faire est de simuler le point avec tout ce que vous avez.

Donc, fondamentalement, il y a 3 solutions possibles:

  • dessiner un point comme une ligne
  • dessiner un point comme un polygone
  • dessiner un point comme un cercle

Chacun a ses inconvénients


Ligne

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

N'oubliez pas que nous nous dirigeons vers le sud-est et que, s'il s'agit du bord, il peut y avoir un problème. Mais vous pouvez aussi dessiner dans une autre direction.


Rectangle

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

ou de manière plus rapide en utilisant fillRect car le moteur de rendu ne remplira qu'un pixel.

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

Cercle


L’un des problèmes des cercles est qu’il est plus difficile pour un moteur de les rendre

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

la même idée que pour le rectangle que vous pouvez réaliser avec le remplissage.

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

Problèmes avec toutes ces solutions:

  • il est difficile de garder une trace de tous les points que vous allez dessiner.
  • lorsque vous effectuez un zoom avant, cela semble moche.

Si vous vous demandez, "Quel est le meilleur moyen de dessiner un point? ", je choisirais un rectangle plein. Vous pouvez voir mon jsperf ici avec des tests de comparaison .

4
Salvador Dali

Dessine un rectangle comme dit sdleihssirhc!

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

^ - devrait dessiner un rectangle 1x1 à x: 10, y: 10

2
the_e

Pour compléter la réponse très complète de Phrogz, il existe une différence critique entre fillRect() et putImageData().
Le premier utilise le contexte pour dessiner plus de par ajouter un rectangle (PAS un pixel), en utilisant le fillStyle valeur alpha ET le contexte globalAlpha et le matrice de transformationbouchons de ligne, etc.
Le second remplace tout un ensemble de pixels (peut-être un, mais pourquoi?)
Le résultat est différent comme vous pouvez le voir sur jsperf .


Personne ne veut régler un pixel à la fois (c'est-à-dire le dessiner à l'écran). C'est pourquoi il n'y a pas d'API spécifique pour le faire (et à juste titre).
Performance sage, si l’objectif est de générer une image (par exemple un logiciel de traçage de rayons), vous toujours vouloir utiliser un tableau obtenu par getImageData() qui est un Uint8Array optimisé. Ensuite, vous appelez putImageData() UNE FOIS ou quelques fois par seconde en utilisant setTimeout/seTInterval.

1
Boing

Hmm, vous pouvez également créer une ligne de 1 pixel de large avec une longueur de 1 pixel et faire en sorte que sa direction se déplace le long d'un seul axe.

            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
trusktr

Code de démonstration HTML rapide: D'après ce que je sais de la bibliothèque graphique SFML C++:

Enregistrez ceci en tant que fichier HTML avec encodage UTF-8 et exécutez-le. N'hésitez pas à refactoriser, j'aime bien utiliser les variables japonaises car elles sont concises et ne prennent pas beaucoup de place

Il est rare que vous souhaitiez définir UN pixel arbitraire et l'afficher à l'écran. Alors utilisez le

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

méthode permettant d’attirer de nombreux pixels arbitraires dans une mémoire tampon. (appels pas chers)

Puis, quand vous êtes prêt à montrer, appelez le

Apply() 

méthode pour afficher les modifications. (appel cher)

Code de fichier .HTML complet ci-dessous:

<!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
J.M.I. MADISON