Apa cara terbaik untuk menetapkan satu piksel dalam kanvas HTML5?

184

Kanvas HTML5 tidak memiliki metode untuk secara eksplisit mengatur satu piksel.

Dimungkinkan untuk mengatur piksel menggunakan garis yang sangat pendek, tetapi kemudian antialising dan tutup garis mungkin mengganggu.

Cara lain mungkin untuk membuat ImageDataobjek kecil dan menggunakan:

context.putImageData(data, x, y)

untuk meletakkannya di tempat.

Adakah yang bisa menggambarkan cara yang efisien dan andal dalam melakukan ini?

Alnitak
sumber

Jawaban:

292

Ada dua pesaing terbaik:

  1. Buat data gambar 1 × 1, atur warnanya, dan putImageDatadi lokasi:

    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. Gunakan fillRect()untuk menggambar piksel (seharusnya tidak ada masalah alias):

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

Anda dapat menguji kecepatannya di sini: http://jsperf.com/setting-canvas-pixel/9 atau di sini https://www.measurethat.net/Benchmarks/Show/1664/1

Saya merekomendasikan pengujian terhadap browser yang Anda pedulikan untuk kecepatan maksimum. Pada Juli 2017, fillRect()5-6 × lebih cepat pada Firefox v54 dan Chrome v59 (Win7x64).

Alternatif lain yang lebih lucu adalah:

  • gunakan getImageData()/putImageData()di seluruh kanvas; ini sekitar 100 × lebih lambat dari opsi lain.

  • membuat gambar khusus menggunakan url data dan menggunakan drawImage()untuk menampilkannya:

    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
  • membuat img atau kanvas lain yang diisi dengan semua piksel yang Anda inginkan dan gunakan drawImage()untuk mencit hanya piksel yang Anda inginkan. Ini mungkin akan sangat cepat, tetapi memiliki batasan yang Anda butuhkan untuk pra-menghitung piksel yang Anda butuhkan.

Perhatikan bahwa pengujian saya tidak mencoba untuk menyimpan dan mengembalikan konteks kanvas fillStyle; ini akan memperlambat fillRect()kinerja. Perhatikan juga bahwa saya tidak memulai dengan yang bersih atau menguji set piksel yang sama persis untuk setiap pengujian.

Phrogz
sumber
2
Saya akan memberi Anda +10 lagi jika saya bisa untuk mengajukan laporan bug! :)
Alnitak
51
Perhatikan bahwa pada komputer saya dengan driver GPU dan grafik saya, fillRect()semi-baru-baru ini menjadi hampir 10x lebih cepat daripada putimagedata 1x1 pada Chromev24. Jadi ... jika kecepatan sangat penting dan Anda tahu audiens target Anda, jangan mengambil kata dari jawaban yang sudah ketinggalan zaman (bahkan milikku). Sebaliknya: tes!
Phrogz
3
Harap perbarui jawabannya. Metode pengisian jauh lebih cepat di browser modern.
Buzzy
10
"Menulis PNGEncoder dibiarkan sebagai latihan untuk pembaca" membuatku tertawa keras.
Pascal Ganaye
2
Mengapa semua jawaban Canvas yang bagus saya dapatkan? :)
Domino
19

Salah satu metode yang belum disebutkan adalah menggunakan getImageData dan kemudian putImageData.
Metode ini bagus untuk saat Anda ingin menggambar banyak sekaligus, cepat.
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
sumber
13
@Alnitak Memberi saya neg karena tidak bisa membaca pikiran Anda, rendah .. Orang lain mungkin bisa mencari di sini untuk dapat merencanakan banyak piksel. Saya lakukan dan kemudian ingat cara yang lebih efisien, jadi bagikanlah.
PAEz
Ini adalah metode yang masuk akal ketika menyodok banyak piksel, untuk demo grafik tempat setiap piksel dihitung, atau serupa. Ini sepuluh kali lebih cepat daripada menggunakan fillRect untuk setiap piksel.
Sam Watkins
Ya, selalu agak menyadap saya bahwa jawaban yang dikecualikan mengatakan bahwa metode ini 100x lebih lambat daripada metode lainnya. Ini mungkin benar jika plot Anda kurang dari 1000, tetapi sejak saat itu metode ini mulai menang dan kemudian membantai metode lainnya. Berikut ini adalah test case .... ukurethat.net/Benchmarks/Show/8386/0/…
PAEz
17

Saya tidak mempertimbangkan fillRect(), tetapi jawabannya mendorong saya untuk membandingkannyaputImage() .

Menempatkan 100.000 piksel berwarna secara acak di lokasi acak, dengan Chrome 9.0.597.84 pada MacBook Pro (lama), membutuhkan waktu kurang dari 100 milidetik putImage(), tetapi hampir 900 milidetik fillRect(). (Kode benchmark di http://pastebin.com/4ijVKJcC ).

Jika sebaliknya saya memilih satu warna di luar loop dan hanya plot warna itu di lokasi acak, putImage()membutuhkan 59ms vs 102ms fillRect().

Tampaknya overhead menghasilkan dan menguraikan spesifikasi warna CSS dalam rgb(...)sintaks bertanggung jawab atas sebagian besar perbedaan.

Meletakkan nilai RGB mentah langsung ke ImageDatablok di sisi lain tidak memerlukan penanganan string atau penguraian.

Alnitak
sumber
2
Saya menambahkan plunker di mana Anda dapat mengklik tombol dan menguji setiap metode (PutImage, FillRect) dan juga metode LineTo. Ini menunjukkan bahwa PutImage dan FillRect sangat dekat tetapi LineTo sangat lambat. Lihat di: plnkr.co/edit/tww6e1VY2OCVY4c4ECy3?p=preview Ini didasarkan pada kode pastebin Anda yang hebat. Terima kasih.
raddevus
Untuk penyedot itu, saya melihat PutImage sedikit lebih lambat dari FillRect (di Chrome 63 terbaru), tetapi setelah saya mencoba LineTo, maka PutImage secara signifikan lebih cepat daripada FillRect. Entah bagaimana mereka tampaknya mengganggu.
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
sumber
var index = (x + y * imageData.width) * 4;
user889030
1
Haruskah memanggil putImageData() setelah fungsi itu atau konteksnya akan diperbarui dengan referensi?
Lucas Sousa
7

Karena browser yang berbeda tampaknya lebih suka metode yang berbeda, mungkin masuk akal untuk melakukan tes yang lebih kecil dengan ketiga metode sebagai bagian dari proses pemuatan untuk mengetahui mana yang terbaik untuk digunakan dan kemudian menggunakannya di seluruh aplikasi?

Daniel
sumber
5

Tampaknya aneh, tetapi meskipun demikian HTML5 mendukung menggambar garis, lingkaran, persegi panjang dan banyak bentuk dasar lainnya, itu tidak memiliki apa pun yang cocok untuk menggambar titik dasar. Satu-satunya cara untuk melakukannya adalah dengan mensimulasikan poin dengan apa pun yang Anda miliki.

Jadi pada dasarnya ada 3 solusi yang mungkin:

  • gambar titik sebagai garis
  • gambar titik sebagai poligon
  • gambar titik sebagai lingkaran

Masing-masing dari mereka memiliki kekurangan


Baris

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

Ingatlah bahwa kita sedang menuju ke arah Tenggara, dan jika ini adalah ujungnya, mungkin ada masalah. Tapi Anda juga bisa menggambar ke arah lain.


Empat persegi panjang

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

atau dengan cara yang lebih cepat menggunakan fillRect karena mesin render hanya akan mengisi satu piksel.

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

Lingkaran


Salah satu masalah dengan lingkaran adalah lebih sulit bagi mesin untuk membuat mereka

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

ide yang sama seperti dengan persegi panjang yang bisa Anda capai dengan mengisi.

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

Masalah dengan semua solusi ini:

  • sulit untuk melacak semua poin yang akan Anda gambar.
  • ketika Anda memperbesar, tampilannya jelek.

Jika Anda bertanya-tanya, "Apa cara terbaik untuk menggambar titik? ", Saya akan pergi dengan persegi panjang penuh. Anda dapat melihat jsperf saya di sini dengan tes perbandingan .

Salvador Dali
sumber
Arah tenggara? Apa?
LoganDark
4

Bagaimana dengan persegi panjang? Itu harus lebih efisien daripada menciptakan ImageDataobjek.

sdleihssirhc
sumber
3
Anda akan berpikir begitu, dan itu mungkin untuk satu piksel, tetapi jika Anda membuat pra-data data dan mengatur 1 piksel dan kemudian menggunakannya putImageData10x lebih cepat daripada fillRectdi Chrome. (Lihat jawaban saya untuk lebih lanjut.)
Phrogz
2

Gambar kotak seperti yang dikatakan sdleihssirhc!

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

^ - harus menggambar kotak 1x1 di x: 10, y: 10

engkau
sumber
1

Hmm, Anda juga bisa membuat garis lebar 1 piksel dengan panjang 1 piksel dan membuat arahnya bergerak di sepanjang sumbu tunggal.

            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
sumber
1
Saya menerapkan pengundian piksel sebagai FillRect, PutImage, dan LineTo, serta membuat plunker di: plnkr.co/edit/tww6e1VY2OCVY4c4ECy3?p=preview Periksa, karena LineTo secara eksponensial lebih lambat. Dapat melakukan 100.000 poin dengan 2 metode lain dalam 0,25 detik, tetapi 10.000 poin dengan LineTo membutuhkan waktu 5 detik.
raddevus
1
Oke, saya membuat kesalahan dan saya ingin menutup loop. Kode LineTo tidak ada - baris yang sangat penting - yang terlihat seperti berikut: ctx.beginPath (); Saya memperbarui plunker (di tautan dari komentar saya yang lain) dan menambahkan bahwa satu baris sekarang memungkinkan metode LineTo menghasilkan 100.000 dalam rata-rata 0,5 detik. Cukup mengagumkan. Jadi, jika Anda akan mengedit jawaban Anda dan menambahkan baris itu ke kode Anda (sebelum baris ctx.lineWidth) saya akan membesarkan hati Anda. Saya harap Anda menemukan ini menarik dan saya minta maaf atas kode kereta asli saya.
raddevus
1

Untuk melengkapi jawaban Phrogz dengan sangat teliti, ada perbedaan kritis antara fillRect()dan putImageData().
Konteks penggunaan pertama untuk menarik lebih dengan menambahkan sebuah persegi panjang (TIDAK pixel), dengan menggunakan fillStyle nilai alpha DAN konteks globalAlpha dan matriks transformasi , topi garis dll ..
menggantikan kedua seluruh set piksel (mungkin satu, tapi mengapa ?)
Hasilnya berbeda seperti yang Anda lihat di jsperf .


Tidak ada yang ingin mengatur satu piksel pada satu waktu (artinya menggambarnya di layar). Itu sebabnya tidak ada API khusus untuk melakukan itu (dan memang demikian).
Kinerja bijaksana, jika tujuannya adalah untuk menghasilkan gambar (misalnya perangkat lunak ray-tracing), Anda selalu ingin menggunakan array yang diperoleh dengan getImageData()Uint8Array yang dioptimalkan. Kemudian Anda menelepon putImageData()SEKALI atau beberapa kali per detik menggunakan setTimeout/seTInterval.

Boing
sumber
Saya punya kasus di mana saya ingin menempatkan 100k blok dalam gambar, tetapi tidak pada skala 1: 1 piksel. Penggunaannya fillRectmenyakitkan karena akselerasi h / w Chrome tidak dapat mengatasi setiap panggilan ke GPU yang diperlukan. Saya akhirnya harus menggunakan data piksel pada 1: 1 dan kemudian menggunakan penskalaan CSS untuk mendapatkan hasil yang diinginkan. Ini jelek :(
Alnitak
Menjalankan patokan tertaut Anda di Firefox 42 Saya hanya mendapatkan 168 ops / detik untuk get/putImageData, tetapi 194.893 untuk fillRect. 1x1 image dataadalah 125.102 Ops / dtk. Jadi fillRectmenang jauh di Firefox. Jadi banyak hal berubah antara 2012 dan hari ini. Seperti biasa, jangan pernah mengandalkan hasil tolok ukur lama.
Mecki
12
Saya ingin mengatur satu piksel pada satu waktu. Saya menduga dengan judul pertanyaan ini yang dilakukan orang lain juga
chasmani
1

Kode Demo HTML Cepat: Berdasarkan apa yang saya ketahui tentang perpustakaan grafik SFML C ++:

Simpan ini sebagai file HTML dengan Pengkodean UTF-8 dan jalankan. Merasa bebas untuk refactor, saya hanya suka menggunakan variabel Jepang karena mereka ringkas dan tidak memakan banyak ruang

Jarang Anda ingin mengatur SATU piksel sembarang dan menampilkannya di layar. Jadi gunakan

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

metode untuk menggambar banyak piksel sembarang ke buffer belakang. (panggilan murah)

Kemudian ketika siap untuk ditampilkan, panggil

Apply() 

metode untuk menampilkan perubahan. (panggilan mahal)

Kode file .HTML lengkap di bawah ini:

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

Jika Anda khawatir tentang kecepatan maka Anda juga bisa mempertimbangkan WebGL.

Martin Ždila
sumber
-1

HANDY dan proposisi fungsi put pixel (pp) (ES6) (baca-pixel di sini ):

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

Fungsi ini menggunakan putImageDatadan memiliki bagian inisialisasi (garis panjang pertama). Pada awalnya alih-alih s='.myCanvas'gunakan pemilih CSS Anda untuk kanvas Anda.

Jika Anda ingin menormalkan parameter menjadi nilai dari 0-1, Anda harus mengubah nilai default a=255menjadi a=1dan sejalan dengan: id.data.set([r,g,b,a]),ctx.putImageData(id, x, y)menjadi id.data.set([r*255,g*255,b*255,a*255]),ctx.putImageData(id, x*c.width, y*c.height)

Kode praktis di atas baik untuk menguji algoritma grafis ad-hoc atau membuat bukti konsep, tetapi tidak baik untuk digunakan dalam produksi di mana kode harus dapat dibaca dan jelas.

Kamil Kiełczewski
sumber
1
Terpilih untuk bahasa Inggris yang buruk dan kapal yang berantakan.
xavier
1
@ xavier - bahasa Inggris bukan bahasa ibu saya dan saya tidak pandai belajar bahasa asing, namun Anda dapat mengedit jawaban saya dan memperbaiki bug bahasa (Ini akan menjadi kontribusi positif dari Anda). Saya meletakkan ini satu-liner karena berguna dan mudah digunakan - dan bisa bagus misalnya bagi siswa untuk menguji beberapa algoritma grafis, namun itu bukan solusi yang baik untuk digunakan dalam produksi di mana kode harus dapat dibaca dan jelas.
Kamil Kiełczewski
3
@KamilKiełczewski Code mudah dibaca dan jelas sama pentingnya bagi siswa dan juga bagi para profesional.
Logan Pickup
-2

putImageData mungkin lebih cepat dari fillRect aslinya. Saya pikir ini karena parameter kelima dapat memiliki cara yang berbeda untuk ditugaskan (warna persegi panjang), menggunakan string yang harus ditafsirkan.

Misalkan Anda melakukan itu:

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

Jadi, garisnya

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

adalah yang paling berat di antara semua. Argumen kelima dalam fillRectpanggilan adalah string yang sedikit lebih panjang.

Hydroper
sumber
1
Browser mana yang mendukung warna sebagai argumen ke-5? Untuk Chrome saya harus menggunakannya context.fillStyle = ...sebagai gantinya. developer.mozilla.org/en-US/docs/Web/API/…
iX3