Cách tốt nhất để đặt một pixel trong khung vẽ HTML5 là gì?


183

Canvas Canvas không có phương pháp để đặt rõ ràng một pixel.

Có thể thiết lập một pixel bằng cách sử dụng một dòng rất ngắn, nhưng sau đó tính năng chống phản xạ và giới hạn dòng có thể gây trở ngại.

Một cách khác có thể là tạo một ImageDatađối tượng nhỏ và sử dụng:

context.putImageData(data, x, y)

để đặt nó vào vị trí.

Bất cứ ai có thể mô tả một cách hiệu quả và đáng tin cậy để làm điều này?

Câu trả lời:


291

Có hai ứng cử viên tốt nhất:

  1. Tạo dữ liệu hình ảnh 1 × 1, đặt màu và putImageDatatại vị trí:

    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. Sử dụng fillRect()để vẽ pixel (không nên có vấn đề răng cưa):

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

Bạn có thể kiểm tra tốc độ của những thứ này tại đây: http://jsperf.com/setting-canvas-pixel/9 hoặc tại đây https://www.measurethat.net/Benchmark/Show/1664/1

Tôi khuyên bạn nên thử nghiệm các trình duyệt mà bạn quan tâm để có tốc độ tối đa. Kể từ tháng 7 năm 2017,fillRect() nhanh hơn 5-6 × trên Firefox v54 và Chrome v59 (Win7x64).

Khác, sillier thay thế là:

  • sử dụng getImageData()/putImageData()trên toàn bộ khung vẽ; điều này chậm hơn khoảng 100 × so với các tùy chọn khác.

  • tạo một hình ảnh tùy chỉnh bằng cách sử dụng url dữ liệu và sử dụng drawImage()để hiển thị nó:

    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
  • tạo một img hoặc canvas khác chứa đầy tất cả các pixel bạn muốn và sử dụng drawImage()để làm mờ chỉ pixel bạn muốn. Điều này có thể sẽ rất nhanh, nhưng có giới hạn là bạn cần tính toán trước các pixel bạn cần.

Lưu ý rằng các thử nghiệm của tôi không cố lưu và khôi phục bối cảnh canvas fillStyle; điều này sẽ làm chậm fillRect()hiệu suất. Cũng lưu ý rằng tôi không bắt đầu với một bảng xếp hạng sạch hoặc kiểm tra chính xác bộ pixel cho mỗi thử nghiệm.


2
Tôi sẽ cho bạn thêm 10 điểm nữa nếu tôi có thể nộp báo cáo lỗi! :)
Alnitak

51
Lưu ý rằng trên máy của tôi có trình điều khiển GPU và đồ họa của tôi, fillRect()gần đây đã trở nên nhanh hơn gần gấp 10 lần so với putimagedata 1x1 trên Chromev24. Vì vậy, ... nếu tốc độ là quan trọng và bạn biết đối tượng mục tiêu của mình, đừng lấy từ của một câu trả lời lỗi thời (ngay cả của tôi). Thay vào đó: kiểm tra!
Phrogz

3
Hãy cập nhật câu trả lời. Phương pháp điền nhanh hơn nhiều trên các trình duyệt hiện đại.
Buzzy

10
"Viết PNGEncoder là một bài tập cho người đọc" khiến tôi bật cười.
Pascal Ganaye

2
Tại sao tất cả các câu trả lời Canvas tuyệt vời mà tôi gặp phải lại xảy ra với bạn? :)
Domino

18

Một phương pháp đã không được đề cập là sử dụng getImageData và sau đó putImageData.
Phương pháp này tốt cho khi bạn muốn vẽ nhiều trong một lần, nhanh chóng.
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 Cung cấp cho tôi một phủ định vì không thể đọc được suy nghĩ của bạn, là thấp..Những người khác có thể đến đây để có thể vẽ nhiều pixel. Tôi đã làm và sau đó nhớ cách hiệu quả hơn, vì vậy chia sẻ nó.
PAEz

Đây là một phương pháp hợp lý khi chọc nhiều pixel, cho một bản demo đồ họa trong đó mỗi pixel được tính toán hoặc tương tự nhau. Nó nhanh hơn mười lần so với sử dụng fillRect cho mỗi pixel.
Sam Watkins

Vâng, nó luôn làm tôi bực mình vì câu trả lời bị loại trừ nói rằng phương pháp này chậm hơn 100 lần so với các phương pháp khác. Điều này có thể đúng nếu âm mưu của bạn dưới 1000, nhưng từ đó trở đi phương pháp này bắt đầu chiến thắng và sau đó tàn sát các phương thức khác. Dưới đây là một trường hợp thử nghiệm .... measurethat.net/Benchmarks/Show/8386/0/...
Paez

17

Tôi đã không cân nhắc fillRect(), nhưng câu trả lời đã thúc đẩy tôi đánh giá nó putImage().

Đặt 100.000 pixel màu ngẫu nhiên vào các vị trí ngẫu nhiên, với Chrome 9.0.597.84 trên MacBook Pro (cũ), chỉ mất chưa đến 100ms putImage(), nhưng sử dụng gần 900ms fillRect(). (Mã điểm chuẩn tại http://pastebin.com/4ijVKJcC ).

Nếu thay vào đó, tôi chọn một màu duy nhất bên ngoài các vòng lặp và chỉ vẽ màu đó tại các vị trí ngẫu nhiên, putImage()mất 59ms so với 102ms cho fillRect().

Dường như chi phí tạo và phân tích cú pháp màu CSS theo rgb(...)cú pháp chịu trách nhiệm cho phần lớn sự khác biệt.

Đặt các giá trị RGB thô thẳng vào một ImageDatakhối mặt khác không yêu cầu xử lý chuỗi hoặc phân tích cú pháp.


2
Tôi đã thêm một plunker nơi bạn có thể nhấp vào nút và kiểm tra từng phương thức (PutImage, FillRect) và thêm vào đó là phương thức LineTo. Nó cho thấy rằng PutImage và FillRect rất gần nhau nhưng LineTo cực kỳ chậm. Hãy xem thử tại: plnkr.co/edit/tww6e1VY2OCVY4c4ECy3?p=preview Nó dựa trên mã pastebin tuyệt vời của bạn. Cảm ơn.
raddevus 16/2/2015

Đối với trình duyệt này, tôi thấy PutImage chậm hơn một chút so với FillRect (trên Chrome 63 mới nhất), nhưng sau khi tôi thử LineTo, thì PutImage nhanh hơn đáng kể so với FillRect. Bằng cách nào đó họ dường như đang can thiệp.
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 index = (x + y * imageData. thong) * 4;
dùng889030

1
Nên gọi putImageData() sau chức năng đó hoặc bối cảnh sẽ cập nhật bằng cách tham khảo?
Lucas Sousa

7

Vì các trình duyệt khác nhau dường như thích các phương thức khác nhau, có lẽ sẽ có ý nghĩa khi thực hiện một thử nghiệm nhỏ hơn với cả ba phương thức như một phần của quá trình tải để tìm ra cách tốt nhất để sử dụng và sau đó sử dụng trong suốt ứng dụng?


5

Có vẻ lạ, nhưng dù sao HTML5 cũng hỗ trợ vẽ các đường, hình tròn, hình chữ nhật và nhiều hình dạng cơ bản khác, nó không có gì phù hợp để vẽ điểm cơ bản. Cách duy nhất để làm như vậy là mô phỏng điểm với bất cứ thứ gì bạn có.

Vì vậy, về cơ bản có 3 giải pháp có thể:

  • vẽ điểm như một đường thẳng
  • vẽ điểm như một đa giác
  • vẽ điểm như một vòng tròn

Mỗi người trong số họ có nhược điểm của họ


Hàng

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

Hãy nhớ rằng chúng ta đang vẽ theo hướng Đông Nam, và nếu đây là cạnh, có thể có một vấn đề. Nhưng bạn cũng có thể vẽ theo bất kỳ hướng nào khác.


Hình chữ nhật

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

hoặc theo cách nhanh hơn bằng cách sử dụng fillRect vì công cụ kết xuất sẽ chỉ lấp đầy một pixel.

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

Vòng tròn


Một trong những vấn đề với vòng tròn là động cơ khó kết xuất chúng hơn

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

ý tưởng tương tự như với hình chữ nhật bạn có thể đạt được với điền.

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

Vấn đề với tất cả các giải pháp sau:

  • thật khó để theo dõi tất cả các điểm bạn sẽ vẽ.
  • Khi bạn phóng to, nó trông xấu xí.

Nếu bạn đang tự hỏi, " cách tốt nhất để vẽ một điểm là gì? ", Tôi sẽ đi với hình chữ nhật đầy. Bạn có thể thấy jsperf của tôi ở đây với các bài kiểm tra so sánh .


Hướng đông nam? Gì?
LoganDark

4

Một hình chữ nhật thì sao? Điều đó phải hiệu quả hơn việc tạo ra một ImageDatađối tượng.


3
Bạn sẽ nghĩ như vậy và nó có thể chỉ dành cho một pixel, nhưng nếu bạn tạo trước dữ liệu hình ảnh và đặt 1 pixel rồi sử dụng putImageDatathì nó nhanh hơn gấp 10 lần so với fillRectChrome. (Xem câu trả lời của tôi để biết thêm.)
Phrogz

2

Vẽ một hình chữ nhật như sdleihssirhc đã nói!

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

^ - nên vẽ hình chữ nhật 1x1 tại x: 10, y: 10


1

Hmm, bạn cũng có thể tạo một đường rộng 1 pixel với chiều dài 1 pixel và làm cho hướng của nó di chuyển dọc theo một trục.

            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
Tôi đã triển khai vẽ pixel dưới dạng FillRect, PutImage và LineTo và tạo ra một plunker tại: plnkr.co/edit/tww6e1VY2OCVY4c4ECy3?p=preview Kiểm tra xem, vì LineTo chậm hơn theo cấp số nhân. Có thể thực hiện 100.000 điểm với 2 phương thức khác trong 0,25 giây, nhưng 10.000 điểm với LineTo mất 5 giây.
raddevus 16/2/2015

1
Được rồi, tôi đã phạm sai lầm và tôi muốn đóng vòng lặp. Mã LineTo bị thiếu một - dòng rất quan trọng - trông giống như sau: ctx.beginPath (); Tôi đã cập nhật trình tải xuống (tại liên kết từ nhận xét khác của tôi) và thêm rằng một dòng hiện cho phép phương thức LineTo tạo 100.000 trong trung bình 0,5 giây. Khá tuyệt. Vì vậy, nếu bạn sẽ chỉnh sửa câu trả lời của mình và thêm dòng đó vào mã của bạn (trước dòng ctx.lineWidth), tôi sẽ nâng cấp bạn. Tôi hy vọng bạn thấy điều này thú vị và tôi xin lỗi vì mã lỗi ban đầu của tôi.
raddevus 17/2/2015

1

Để hoàn thành câu trả lời rất kỹ lưỡng của Phrogz, có một sự khác biệt quan trọng giữa fillRect()putImageData().
Việc sử dụng bối cảnh đầu tiên để vẽ trên bằng cách thêm một hình chữ nhật (KHÔNG phải là một pixel), sử dụng fillstyle giá trị alpha VÀ bối cảnh globalAlphama trận chuyển đổi , mũ dòng vv ..
này thay thế thứ hai toàn bộ một bộ pixel (có thể một, nhưng tại sao ?)
Kết quả khác nhau như bạn có thể thấy trên jsperf .


Không ai muốn đặt một pixel mỗi lần (nghĩa là vẽ nó trên màn hình). Đó là lý do tại sao không có API cụ thể để làm điều đó (và đúng như vậy).
Hiệu suất khôn ngoan, nếu mục tiêu là tạo ra một hình ảnh (ví dụ như phần mềm dò tia), bạn luôn muốn sử dụng một mảng thu được từ getImageData()đó là một Uint8Array được tối ưu hóa. Sau đó, bạn gọi putImageData()ONCE hoặc một vài lần mỗi giây bằng cách sử dụng setTimeout/seTInterval.


Tôi đã có một trường hợp tôi muốn đặt các khối 100k vào một hình ảnh, nhưng không phải ở tỷ lệ 1: 1 pixel. Việc sử dụng fillRectrất đau đớn vì khả năng tăng tốc h / w của Chrome không thể đối phó với các cuộc gọi riêng lẻ tới GPU mà nó sẽ yêu cầu. Cuối cùng tôi đã phải sử dụng dữ liệu pixel theo tỷ lệ 1: 1 và sau đó sử dụng tỷ lệ CSS để có được đầu ra mong muốn. Thật xấu xí :(
Alnitak

Chạy điểm chuẩn được liên kết của bạn trên Firefox 42 tôi chỉ nhận được 168 Ops / giây get/putImageData, nhưng 194.893 cho fillRect. 1x1 image datalà 125.102 Ops / giây. Vì vậy, fillRectchiến thắng trong Firefox. Vì vậy, mọi thứ đã thay đổi rất nhiều giữa năm 2012 và ngày nay. Như mọi khi, không bao giờ dựa vào kết quả điểm chuẩn cũ.
Mecki

12
Tôi muốn đặt một pixel mỗi lần. Tôi đoán theo tiêu đề của câu hỏi này mà những người khác cũng làm như vậy
chasmani

1

Mã trình diễn HTML nhanh: Dựa trên những gì tôi biết về thư viện đồ họa SFML C ++:

Lưu tệp này dưới dạng tệp HTML với Mã hóa UTF-8 và chạy tệp đó. Vui lòng cấu trúc lại, tôi chỉ thích sử dụng các biến tiếng Nhật vì chúng ngắn gọn và không chiếm nhiều không gian

Hiếm khi bạn muốn đặt MỘT pixel tùy ý và hiển thị nó trên màn hình. Vì vậy, sử dụng

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

phương pháp để vẽ nhiều pixel tùy ý vào bộ đệm ngược. (cuộc gọi giá rẻ)

Sau đó khi sẵn sàng để hiển thị, hãy gọi

Apply() 

phương pháp để hiển thị các thay đổi. (cuộc gọi đắt tiền)

Mã tệp .HTML đầy đủ bên dưới:

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

Nếu bạn lo ngại về tốc độ thì bạn cũng có thể xem xét WebGL.


-1

HANDY và đề xuất các đặt pixel (pp) function (ES6) (đọc-pixel ở đây ):

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

Hàm này sử dụng putImageDatavà có phần khởi tạo (dòng dài đầu tiên). Lúc đầu, thay vào đó hãy s='.myCanvas'sử dụng bộ chọn CSS cho khung vẽ của bạn.

Tôi muốn bạn để bình thường hóa các thông số giá trị 0-1 bạn nên thay đổi giá trị mặc định a=255để a=1và phù hợp với: 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)

Mã tiện dụng ở trên là tốt cho các thuật toán đồ họa thử nghiệm đặc biệt hoặc làm bằng chứng về khái niệm, nhưng nó không tốt để sử dụng trong sản xuất, nơi mã phải dễ đọc và rõ ràng.


1
Bỏ phiếu cho tiếng Anh kém và một lớp lót lộn xộn.
xavier

1
@xavier - Tiếng Anh không phải là ngôn ngữ mẹ đẻ của tôi và tôi không giỏi trong việc học ngôn ngữ foregin, tuy nhiên bạn có thể chỉnh sửa câu trả lời của tôi và sửa lỗi ngôn ngữ (Nó sẽ là một đóng góp tích cực từ bạn). Tôi đặt lớp lót này vì tiện dụng và dễ sử dụng - và có thể tốt cho sinh viên kiểm tra một số thuật toán đồ họa, tuy nhiên nó không phải là giải pháp tốt để sử dụng trong sản xuất, nơi mã phải dễ đọc và rõ ràng.
Kamil Kiełczewski

3
@ KamilKiełczewski Mã có thể đọc và rõ ràng là chính xác quan trọng đối với sinh viên cũng như đối với các chuyên gia.
Pickup Logan

-2

putImageData có lẽ nhanh hơn fillRect nguyên bản. Tôi nghĩ điều này bởi vì tham số thứ năm có thể có các cách khác nhau được gán (màu hình chữ nhật), sử dụng một chuỗi phải được giải thích.

Giả sử bạn đang làm điều đó:

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

Vì vậy, dòng

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

là nặng nhất giữa tất cả. Đối số thứ năm trong fillRectcuộc gọi là một chuỗi dài hơn một chút.


1
Trình duyệt nào hỗ trợ chuyển màu như đối số thứ 5? Đối với Chrome, tôi phải sử dụng context.fillStyle = ...thay thế. developer.mozilla.org/en-US/docs/Web/API/ từ
iX3
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.