HTML5 tuvalinde tek bir piksel ayarlamanın en iyi yolu nedir?


185

HTML5 Canvas'ın tek bir pikseli açıkça ayarlama yöntemi yoktur.

Çok kısa bir çizgi kullanarak bir piksel ayarlamak mümkün olabilir, ancak daha sonra kenar yumuşatma ve satır büyüklükleri karışabilir.

Başka bir yol, küçük bir ImageData nesne ve kullanmak olabilir:

context.putImageData(data, x, y)

yerine koymak için.

Bunu yapmanın etkili ve güvenilir bir yolunu tanımlayan var mı?

Yanıtlar:


293

En iyi iki yarışmacı var:

  1. 1 × 1 görüntü verileri oluşturun, rengi ayarlayın ve putImageDatakonumda:

    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. fillRect()Bir piksel çizmek için kullanın (diğer adlandırma sorunları olmamalıdır):

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

Bunların hızını buradan test edebilirsiniz: http://jsperf.com/setting-canvas-pixel/9 veya burada https://www.measurethat.net/Benchmarks/Show/1664/1

Maksimum hız için önem verdiğiniz tarayıcılara karşı test etmenizi öneririz. Temmuz 2017 itibariyle,fillRect() Firefox v54 ve Chrome v59'da (Win7x64) 5-6 × daha hızlıdır.

Diğer, sillier alternatifleri:

  • getImageData()/putImageData()tüm tuval üzerinde kullanma ; bu diğer seçeneklerden yaklaşık 100 × daha yavaştır.

  • bir veri URL'si kullanarak özel bir resim oluşturma ve drawImage()bunu göstermek için kullanma:

    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
  • istediğiniz tüm piksellerle dolu başka bir img veya tuval oluşturmak ve drawImage()yalnızca istediğiniz pikseli karıştırmak için kullanmak . Bu muhtemelen çok hızlı olacaktır, ancak ihtiyacınız olan pikselleri önceden hesaplamanız için gereken sınırlamaya sahiptir.

Testlerimin tuval içeriğini kaydetmeye ve geri yüklemeye çalışmadığını unutmayın fillStyle; bu fillRect()performansı yavaşlatır . Ayrıca temiz bir sayfa ile başlamadığımı veya her test için aynı piksel kümesini test etmediğimi unutmayın.


2
Hata raporunu dosyalayabilseydim sana +10 daha verirdim! :)
Alnitak

51
GPU ve grafik sürücülerimle makinemde, fillRect()yakın zamanda Chromev24'teki 1x1 putimagedata'dan neredeyse 10 kat daha hızlı hale geldiğini unutmayın. Yani ... eğer hız kritikse ve hedef kitlenizi biliyorsanız, eski bir cevaptan söz etmeyin (hatta benimkini). Yerine: test edin!
Phrogz

3
Lütfen cevabı güncelleyin. Doldurma yöntemi modern tarayıcılarda çok daha hızlıdır.
Buzzy

10
"PNGEncoder'ı yazmak okuyucu için bir egzersiz olarak bırakıldı" beni yüksek sesle güldürdü.
Pascal Ganaye

2
Neden verdiğim tüm harika Canvas yanıtları sizin tarafınızdan oluyor? :)
Domino

19

Bahsedilmeyen bir yöntem getImageData ve sonra putImageData kullanmaktır.
Bu yöntem, tek seferde çok hızlı bir şekilde çizmek istediğinizde iyidir.
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 Aklını okuyamadığım için bana bir olumsuzluk veriyor, düşük ... Diğer insanlar buraya birçok piksel çizebilmek için gelebilir. Yaptım ve daha verimli bir şekilde hatırladım, paylaştım.
PAEz

Bu, her pikselin hesaplandığı veya benzer bir grafik demosu için çok sayıda piksel atarken mantıklı bir yöntemdir. Her piksel için fillRect kullanmaktan on kat daha hızlıdır.
Sam Watkins

Evet, her zaman istisna cevabın bu yöntemin diğer yöntemlerden 100 kat daha yavaş olduğunu söylediğini söyledi. 1000'den az planlamanız varsa bu doğru olabilir, ancak oradan bu yöntem kazanmaya ve sonra diğer yöntemleri katletmeye başlar. İşte bir test case .... var measurethat.net/Benchmarks/Show/8386/0/...
Paez

17

Bunu dikkate almadığım fillRect()ama cevaplar karşı kriter bunu bana teşvik putImage().

(Eski) bir MacBook Pro'da Chrome 9.0.597.84 ile rastgele rastgele yerlere 100.000 rastgele renkli piksel koymak, 100ms'den az putImage(), ancak yaklaşık 900ms kullanıyor fillRect(). (Karşılaştırma kodu http://pastebin.com/4ijVKJcC ).

Bunun yerine döngülerin dışında tek bir renk seçersem ve bu rengi rastgele konumlarda putImage()çizersem, 59ms vs 102ms alır fillRect().

rgb(...)Sözdiziminde bir CSS renk belirtimi oluşturma ve ayrıştırma yükünün , farkın çoğundan sorumlu olduğu görülmektedir.

ImageDataDiğer yandan ham RGB değerlerini doğrudan bir bloğa koymak dize işleme veya ayrıştırma gerektirmez.


2
Bir düğmeyi tıklatıp yöntemlerin her birini (PutImage, FillRect) ve ayrıca LineTo yöntemini test edebileceğiniz bir dalgıç ekledim. PutImage ve FillRect'in zamana çok yakın olduğunu, ancak LineTo'nun son derece yavaş olduğunu gösterir. Şuradan kontrol edin: plnkr.co/edit/tww6e1VY2OCVY4c4ECy3?p=preview Harika macun kodunuza dayanmaktadır. Teşekkürler.
raddevus

Bu dalgıç için PutImage'ın FillRect'ten (en son Chrome 63'te) biraz daha yavaş olduğunu görüyorum, ancak LineTo'yu denedikten sonra PutImage, FillRect'den önemli ölçüde daha hızlı. Her nasılsa müdahale ediyor gibi görünüyorlar.
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;
}

var indeksi = (x + y * imageData.width) * 4;
user889030

1
putImageData() Bu fonksiyondan sonra mı çağırmalı yoksa bağlam referans olarak güncellenir mi?
Lucas Sousa

7

Farklı tarayıcılar farklı yöntemleri tercih ediyor gibi göründüğünden, hangisinin en iyi olduğunu bulmak ve daha sonra bunu uygulama boyunca kullanmak için yükleme işleminin bir parçası olarak her üç yöntemle daha küçük bir test yapmak mantıklı olur?


5

Tuhaf görünüyor, ancak yine de HTML5 çizim çizgilerini, daireleri, dikdörtgenleri ve diğer birçok temel şekli destekliyor, temel noktayı çizmek için uygun bir şey yok. Bunu yapmanın tek yolu, sahip olduğunuz şeyle noktayı simüle etmektir.

Temel olarak 3 olası çözüm vardır:

  • çizgi olarak nokta çiz
  • çokgen olarak nokta çiz
  • daire olarak nokta çiz

Her birinin dezavantajları var


Hat

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

Güney-Doğu yönünü çizdiğimizi ve bu kenar ise, bir sorun olabileceğini unutmayın. Ancak başka herhangi bir yönde de çizim yapabilirsiniz.


Dikdörtgen

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

veya fillRect kullanarak daha hızlı bir şekilde sunun, çünkü oluşturma motoru yalnızca bir pikseli dolduracaktır.

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

Daire


Dairelerle ilgili sorunlardan biri, bir motorun bunları oluşturmasının daha zor olmasıdır

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

dolgu ile elde edebilirsiniz dikdörtgen ile aynı fikir.

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

Tüm bu çözümlerle ilgili sorunlar:

  • çizeceğiniz tüm noktaları takip etmek zordur.
  • yakınlaştırdığınızda çirkin görünüyor.

Eğer " Bir nokta çizmenin en iyi yolu nedir ? " Diye merak ediyorsanız, dolu dikdörtgenle giderdim. Jsperf'imi burada karşılaştırma testleri ile görebilirsiniz .


Güneydoğu yönü? Ne?
LoganDark

4

Dikdörtgenden ne haber? Bu bir ImageDatanesne yaratmaktan daha etkili olmalı .


3
Bunu düşünürdünüz ve tek bir piksel için olabilir, ancak görüntü verilerini önceden oluşturup 1 pikseli ayarlarsanız ve putImageDatabunu kullanırsanız fillRect, Chrome'dan 10 kat daha hızlıdır . (Daha fazla bilgi için
cevabıma

2

Sdleihssirhc gibi bir dikdörtgen çizin!

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

^ - x: 10, y: 10'da 1x1 dikdörtgen çizmelidir


1

Hmm, ayrıca 1 piksel uzunluğunda 1 piksel genişliğinde bir çizgi yapabilir ve yönünün tek bir eksen boyunca hareket etmesini sağlayabilirsiniz.

            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
Pill çizimini FillRect, PutImage ve LineTo olarak uyguladım ve şurada bir dalgıç oluşturdum: plnkr.co/edit/tww6e1VY2OCVY4c4ECy3?p=preview Kontrol edin, çünkü LineTo katlanarak daha yavaştır. 0.25 saniyede diğer 2 yöntemle 100.000 puan yapabilir, ancak LineTo ile 10.000 puan 5 saniye sürer.
raddevus

1
Tamam, bir hata yaptım ve döngüyü kapatmak istiyorum. LineTo kodu aşağıdaki gibi görünen bir - çok önemli bir çizgi - eksikti: ctx.beginPath (); Plunker'ı güncelledim (diğer yorumumdaki bağlantıda) ve bir satırın şimdi LineTo yönteminin ortalama 0,5 saniyede 100.000 üretmesine izin verdiğini ekledim. Oldukça harika. Cevabınızı düzenleyip bu satırı kodunuza eklerseniz (ctx.lineWidth satırından önce) sizi oylayacağım. Umarım bunu ilginç bulmuşsundur ve orijinal buggy kodum için özür dilerim.
raddevus

1

Phrogz çok kapsamlı bir cevap tamamlamak için fillRect()ve arasında kritik bir fark vardır putImageData().
İlk kullanım içeriği çekmek için fazla göre ilave kullanarak, bir dikdörtgen (bir piksel) fillStyle alfa değeri ve bağlam globalAlpha ve dönüşüm matrisi , satır kapakları vs ..
İkinci yerine geçer bütün bir piksel kümesini (belki bir, ama neden ?)
Sonuç, jsperf'de gördüğünüz gibi farklı .


Hiç kimse bir kerede bir piksel ayarlamak istemez (yani ekranda çizmek anlamına gelir). Bu yüzden bunu yapmak için belirli bir API yoktur (ve haklı olarak).
Performans açısından, eğer bir resim (örneğin bir ışın izleme yazılımı) oluşturmaksa, her zaman elde edilen getImageData()ve optimize edilmiş bir Uint8Array olan bir dizi kullanmak istersiniz. Sonra putImageData()ONCE veya saniyede birkaç kez kullanarak arayın setTimeout/seTInterval.


Bir görüntüye 100k blok koymak istediğim bir durum vardı, ancak 1: 1 piksel ölçeğinde değil. Kullanılması fillRecthızlanma w Chrome'un h / o gerektirecektir GPU'ya bireysel görüşmeleri ile başa çıkamaz, çünkü acı oldu. 1: 1'de piksel verilerini kullanmak zorunda kaldım ve sonra istenen çıktıyı elde etmek için CSS ölçeklemeyi kullandım. Çirkin :(
Alnitak

Bağlantılı karşılaştırmanızı Firefox 42'de çalıştırıyorum Sadece 168 Ops / sn get/putImageData, ancak 194.893 için fillRect. 1x1 image data125,102 Ops / sn'dir. Bu yüzden fillRectFirefox'ta açık ara kazanır. 2012 ile bugün arasında işler çok değişti. Her zaman olduğu gibi, asla eski kıyaslama sonuçlarına güvenmeyin.
Mecki

12
Her seferinde bir piksel ayarlamak istiyorum. Diğer insanların da yaptığı bu soru başlığı ile tahmin ediyorum
chasmani

1

Hızlı HTML Demo kodu: SFML C ++ grafik kütüphanesi hakkında bildiklerime dayanarak:

Bunu UTF-8 Kodlaması ile bir HTML dosyası olarak kaydedin ve çalıştırın. Refactor'dan çekinmeyin, Japon değişkenlerini kullanmayı seviyorum çünkü özlü ve fazla yer kaplamazlar

Nadiren BİR keyfi piksel ayarlamak ve ekranda görüntülemek isteyeceksiniz. Bu yüzden

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

bir ara arabelleğe çok sayıda keyfi piksel çizme yöntemi. (ucuz çağrılar)

Sonra göstermeye hazır olduğunuzda,

Apply() 

değişiklikleri görüntüleme yöntemi. (pahalı çağrı)

Aşağıdaki tam .HTML dosya kodu:

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


-1

HANDY ve put pixel (pp) fonksiyonunun önerisi (ES6) ( burada pixel okuyun ):

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

Bu işlev putImageData, başlatma parçası (ilk uzun çizgi) kullanır ve kullanır . Bunun yerine başlangıçtas='.myCanvas' tuvaliniz için CSS seçiciyi kullanın.

Parametreleri 0-1 değerine normalleştirmek istiyorsanız, varsayılan değeri şu a=255şekilde değiştirmelisiniz a=1: id.data.set([r,g,b,a]),ctx.putImageData(id, x, y) hiç id.data.set([r*255,g*255,b*255,a*255]),ctx.putImageData(id, x*c.width, y*c.height)

Yukarıdaki kullanışlı kod, test grafik algoritmalarını geçici hale getirmek veya kavram kanıtı oluşturmak için iyidir, ancak kodun okunabilir ve net olması gereken üretimde kullanmak iyi değildir.


1
Yoksul İngilizce ve dağınık bir astar için aşağı oy kullandı.
xavier

1
@xavier - ingilizce benim ana dilim değil ve ön dil öğrenmek konusunda iyi değilim, ancak cevabımı düzenleyebilir ve dil hatalarını düzeltebilirsiniz (Sizden olumlu bir katkı olacaktır). Bu tek astarı koydum çünkü kullanışlı ve kullanımı kolay - ve örneğin öğrencilerin bazı grafik algoritmalarını test etmeleri için iyi olabilir, ancak kodun okunabilir ve net olması gereken üretimde kullanılmak için iyi bir çözüm değildir.
Kamil Kiełczewski

3
@ KamilKiełczewski Kodun okunabilir ve net olması, profesyoneller için olduğu kadar öğrenciler için de önemlidir.
Logan Pickup

-2

putImageData muhtemelen daha hızlı fillRect yerelden . Beşinci parametre yorumlanması gereken bir dize kullanarak atanması (dikdörtgen rengi) farklı yolları olabilir, çünkü bu düşünüyorum.

Bunu yaptığınızı varsayalım:

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

Yani, çizgi

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

en ağır olanıdır. Çağrıdaki beşinci argüman fillRectbiraz daha uzun bir dizedir.


1
Hangi tarayıcı (lar) 5. argüman olarak bir rengin geçmesini destekliyor? Chrome için context.fillStyle = ...bunun yerine kullanmak zorunda kaldım . developer.mozilla.org/tr-TR/docs/Web/API/…
iX3
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.