Quelle est la meilleure façon de définir un seul pixel dans un canevas HTML5?

184

Le HTML5 Canvas n'a pas de méthode pour définir explicitement un seul pixel.

Il peut être possible de définir un pixel en utilisant une ligne très courte, mais l'antialisation et les limites de ligne peuvent alors interférer.

Une autre façon pourrait être de créer un petit ImageDataobjet et d'utiliser:

context.putImageData(data, x, y)

pour le mettre en place.

Quelqu'un peut-il décrire une manière efficace et fiable de le faire?

Alnitak
la source

Réponses:

292

Il y a deux meilleurs prétendants:

  1. Créez des données d'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 y avoir aucun problème d'alias):

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

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

Je recommande de tester les navigateurs qui vous intéressent pour une vitesse maximale. Depuis juillet 2017, il fillRect()est 5 à 6 fois plus rapide sur Firefox v54 et Chrome v59 (Win7x64).

D'autres alternatives plus stupides sont:

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

  • création d'une image personnalisée à l'aide d'une URL de données et utilisation 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éer une autre image ou un autre canevas rempli de tous les pixels que vous souhaitez et utiliser drawImage()pour atténuer uniquement le pixel souhaité. Ce serait probablement très rapide, mais a la limitation dont vous avez besoin pour pré-calculer les pixels dont vous avez besoin.

Notez que mes tests n'essaient pas d'enregistrer et de restaurer le contexte du canevas fillStyle; cela ralentirait les fillRect()performances. Notez également que je ne commence pas avec une ardoise propre ou que je ne teste pas exactement le même ensemble de pixels pour chaque test.

Phrogz
la source
2
Je vous donnerais un autre +10 si je le pouvais pour le dépôt du rapport de bogue! :)
Alnitak
51
Notez que sur ma machine avec mon GPU et mes pilotes graphiques, fillRect()semi-récemment est devenu presque 10 fois plus rapide que le putimagedata 1x1 sur Chromev24. Donc ... si la vitesse est essentielle et que vous connaissez votre public cible, ne vous fiez pas à une réponse dépassée (même la mienne). Au lieu de cela: testez!
Phrogz
3
Veuillez mettre à jour la réponse. La méthode de remplissage est beaucoup plus rapide sur les navigateurs modernes.
Buzzy
10
«Écrire le PNGEncoder est laissé comme un exercice pour le lecteur» m'a fait rire à haute voix.
Pascal Ganaye
2
Pourquoi toutes les bonnes réponses Canvas sur lesquelles j'arrive sont-elles par vous? :)
Domino
19

Une méthode qui n'a pas été mentionnée utilise getImageData, puis putImageData.
Cette méthode est idéale 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);
PAEz
la source
13
@Alnitak Me donner un négatif pour ne pas être en mesure de lire dans vos pensées, c'est faible .. D'autres personnes pourraient arriver ici pour être en mesure de tracer de nombreux pixels. Je l'ai fait et je me suis souvenu de la manière la plus efficace, alors je l'ai partagé.
PAEz
C'est une méthode judicieuse pour piquer beaucoup de pixels, pour une démonstration graphique où chaque pixel est calculé, ou similaire. C'est dix fois plus rapide que d'utiliser fillRect pour chaque pixel.
Sam Watkins
Ouais, cela m'a toujours un peu dérangé que la réponse exceptée dise que cette méthode est 100 fois plus lente que les autres méthodes. Cela peut être vrai si vous tracez moins de 1000, mais à partir de là, cette méthode commence à gagner puis à abattre les autres méthodes. Voici un cas de test .... mesurethat.net/Benchmarks/Show/8386/0/…
PAEz
17

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

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

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

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

En ImageDatarevanche, placer les valeurs RVB brutes directement dans un bloc ne nécessite aucune gestion de chaîne ni analyse.

Alnitak
la source
2
J'ai ajouté un plunker où vous pouvez cliquer sur un bouton et tester chacune des méthodes (PutImage, FillRect) et en plus la méthode LineTo. Cela montre que PutImage et FillRect sont très proches dans le temps, mais LineTo est extrêmement lent. Vérifiez-le à: plnkr.co/edit/tww6e1VY2OCVY4c4ECy3?p=preview Il est basé sur votre excellent code pastebin. Merci.
raddevus
Pour ce plunker, je vois que PutImage est légèrement plus lent que FillRect (sur le dernier Chrome 63), mais après avoir essayé LineTo, PutImage est nettement plus rapide que FillRect. D'une manière ou d'une autre, ils semblent interférer.
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
la source
var index = (x + y * imageData.width) * 4;
user889030
1
Faut-il appeler putImageData() après cette fonction ou le contexte sera mis à jour par référence?
Lucas Sousa
7

É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 pour savoir laquelle est la meilleure à utiliser, puis de l'utiliser dans toute l'application?

Daniel
la source
5

Cela semble étrange, mais néanmoins HTML5 prend en charge le dessin de lignes, de cercles, de rectangles et de nombreuses autres formes de base, il n'a rien de convenable 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 sous forme de polygone
  • dessiner un point sous forme de cercle

Chacun d'eux a ses inconvénients


Ligne

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

Gardez à l'esprit que nous nous dirigeons vers le sud-est, et si c'est le bord, il peut y avoir un problème. Mais vous pouvez également dessiner dans n'importe quelle autre direction.


Rectangle

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

ou d'une 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 avec les 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 qu'avec le rectangle que vous pouvez réaliser avec un 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 tirer.
  • quand vous faites un zoom avant, ça a l'air moche.

Si vous vous demandez, "Quelle est la meilleure façon de dessiner un point? ", J'irais avec un rectangle rempli. Vous pouvez voir mon jsperf ici avec des tests de comparaison .

Salvador Dali
la source
La direction sud-est? Quoi?
LoganDark
4

Et un rectangle? Cela doit être plus efficace que de créer un ImageDataobjet.

sdleihssirhc
la source
3
Vous pensez que oui, et cela pourrait être pour un seul pixel, mais si vous pré-créez les données d'image et définissez le pixel 1, puis l'utilisez, putImageDatail est 10 fois plus rapide que fillRectdans Chrome. (Voir ma réponse pour plus.)
Phrogz
2

Dessinez un rectangle comme dit sdleihssirhc!

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

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

te
la source
1

Hmm, vous pouvez également simplement créer une ligne de 1 pixel de large avec une longueur de 1 pixel et faire bouger sa direction 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();
trusktr
la source
1
J'ai implémenté le pixel draw en tant que FillRect, PutImage et LineTo et j'ai créé un plunker à: plnkr.co/edit/tww6e1VY2OCVY4c4ECy3?p=preview Check it out, car LineTo est exponentiellement plus lent. Peut faire 100 000 points avec 2 autres méthodes en 0,25 seconde, mais 10 000 points avec LineTo prennent 5 secondes.
raddevus
1
D'accord, j'ai fait une erreur et j'aimerais boucler la boucle. Le code LineTo manquait une - ligne très importante - qui ressemble à ce qui suit: ctx.beginPath (); J'ai mis à jour le plunker (au lien de mon autre commentaire) et l'ajout d'une ligne permet maintenant à la méthode LineTo de générer 100000 en 0,5 seconde en moyenne. Tout à fait incroyable. Donc, si vous modifiez votre réponse et ajoutez cette ligne à votre code (avant la ligne ctx.lineWidth), je vous voterai. J'espère que vous avez trouvé cela intéressant et je m'excuse pour mon code buggy original.
raddevus
1

Pour compléter la réponse très approfondie de Phrogz, il y a une différence critique entre fillRect()et putImageData().
La première utilise le contexte pour dessiner sur par l' ajout d' un rectangle (pas un pixel), en utilisant la fillStyle valeur alpha et le contexte globalAlpha et la matrice de transformation , capuchons de ligne , etc ..
Le second remplace un ensemble de jeu de pixels (peut - être un, mais pourquoi ?)
Le résultat est différent comme vous pouvez le voir sur jsperf .


Personne ne veut définir 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).
En termes de performances, si l'objectif est de générer une image (par exemple un logiciel de lancer de rayons), vous souhaitez toujours utiliser un tableau obtenu par getImageData()lequel est un Uint8Array optimisé. Ensuite, vous appelez putImageData()UNE FOIS ou quelques fois par seconde en utilisant setTimeout/seTInterval.

Boing
la source
J'ai eu un cas où je voulais mettre 100k blocs dans une image, mais pas à l'échelle de pixels 1: 1. L'utilisation fillRectétait douloureuse car l'accélération h / w de Chrome ne peut pas faire face aux appels individuels au GPU dont elle aurait besoin. J'ai fini par devoir utiliser des données de pixels à 1: 1, puis utiliser la mise à l'échelle CSS pour obtenir la sortie souhaitée. C'est moche :(
Alnitak
Exécution de votre benchmark lié sur Firefox 42 Je n'obtiens que 168 Ops / sec pour get/putImageData, mais 194 893 pour fillRect. 1x1 image dataest de 125 102 Ops / sec. Alors fillRectgagne de loin dans Firefox. Les choses ont donc beaucoup changé entre 2012 et aujourd'hui. Comme toujours, ne vous fiez jamais aux anciens résultats de référence.
Mecki
12
Je veux définir un pixel à la fois. Je suppose que par le titre de cette question que d'autres personnes font aussi
chasmani
1

Code de démonstration HTML rapide: basé sur ce que je sais de la bibliothèque graphique SFML C ++:

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

Vous voudrez rarement définir UN pixel arbitraire et l'afficher à l'écran. Alors utilisez le

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

méthode pour dessiner de nombreux pixels arbitraires dans un back-buffer. (appels pas chers)

Ensuite, lorsque vous êtes prêt à montrer, appelez le

Apply() 

méthode pour afficher les modifications. (appel coûteux)

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>
JMI MADISON
la source
0

Si vous êtes préoccupé par la vitesse, vous pouvez également envisager WebGL.

Martin Ždila
la source
-1

HANDY et proposition de la fonction put pixel (pp) (ES6) (read-pixel ici ):

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

Cette fonction utilise putImageDataet a une partie d'initialisation (première longue ligne). Au début, s='.myCanvas'utilisez plutôt votre sélecteur CSS pour votre canevas.

Je vous veux pour normaliser les paramètres à la valeur de 0 à 1 , vous devez changer la valeur par défaut a=255pour a=1et avec la ligne: id.data.set([r,g,b,a]),ctx.putImageData(id, x, y)à id.data.set([r*255,g*255,b*255,a*255]),ctx.putImageData(id, x*c.width, y*c.height)

Le code pratique ci-dessus est bon pour tester des algorithmes graphiques ad hoc ou faire une preuve de concept, mais il n'est pas bon à utiliser en production où le code doit être lisible et clair.

Kamil Kiełczewski
la source
1
Voté à la baisse pour un anglais médiocre et une ligne encombrée.
xavier
1
@xavier - l'anglais n'est pas ma langue maternelle et je ne suis pas doué pour apprendre les langues précédentes, mais vous pouvez modifier ma réponse et corriger les bogues de langue (ce sera une contribution positive de votre part). J'ai mis ce one-liner parce qu'il est pratique et facile à utiliser - et peut être bon par exemple pour que les étudiants testent certains algorithmes graphiques, mais ce n'est pas une bonne solution à utiliser en production où le code doit être lisible et clair.
Kamil Kiełczewski
3
@ KamilKiełczewski Le code est lisible et clair est tout aussi important pour les étudiants que pour les professionnels.
Ramassage Logan le
-2

putImageDataest probablement plus rapide que fillRectnativement. Je pense que cela parce que le cinquième paramètre peut être attribué de différentes manières (la couleur du rectangle), en utilisant une chaîne qui doit être interprétée.

Supposons que vous fassiez cela:

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

Alors, la ligne

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

est le plus lourd de tous. Le cinquième argument de l' fillRectappel est une chaîne un peu plus longue.

Hydroper
la source
1
Quel (s) navigateur (s) prend en charge le passage d'une couleur comme 5ème argument? Pour Chrome, j'ai dû utiliser à la context.fillStyle = ...place. developer.mozilla.org/en-US/docs/Web/API/…
iX3