web-dev-qa-db-ja.com

HTML5キャンバスに単一のピクセルを設定する最良の方法は何ですか?

HTML5 Canvasには、単一のピクセルを明示的に設定する方法がありません。

非常に短いラインを使用してピクセルを設定することは可能かもしれませんが、アンチエイリアスとラインキャップが干渉する可能性があります。

別の方法は、小さなImageDataオブジェクトを作成し、以下を使用することです。

context.putImageData(data, x, y)

所定の位置に配置します。

誰でもこれを行う効率的で信頼できる方法を説明できますか?

170
Alnitak

2つの最高の候補があります。

  1. 1×1の画像データを作成し、色を設定し、putImageDataの場所に:

    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()を使用してピクセルを描画します(エイリアスの問題はないはずです):

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

これらの速度はここでテストできます: http://jsperf.com/setting-canvas-pixel/9 またはここ https://www.measurethat.net/Benchmarks/Show/1664/1

最大速度を気にするブラウザに対してテストすることをお勧めします。2017年7月現在、fillRect()はFirefox v54およびChrome v59(Win7x64)。

他の愚かな代替手段は次のとおりです。

  • キャンバス全体でgetImageData()/putImageData()を使用します。これは、他のオプションよりも約100倍遅いです。

  • データURLを使用してカスタム画像を作成し、drawImage()を使用して表示します。

    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
    
  • 必要なすべてのピクセルで満たされた別のimgまたはキャンバスを作成し、drawImage()を使用して、必要なピクセルのみをブリットします。これはおそらく非常に高速ですが、必要なピクセルを事前に計算する必要があるという制限があります。

テストでは、キャンバスコンテキストfillStyle;を保存および復元しようとしないことに注意してください。これにより、fillRect()パフォーマンスが低下します。また、私は白紙から始めたり、各テストでまったく同じピクセルのセットをテストしたりしないことに注意してください。

270
Phrogz

私はfillRect()を考慮していませんでしたが、答えはputImage()に対してベンチマークするように駆り立てました。

(古い)MacBook ProでChrome 9.0.597.84を使用して、ランダムな場所に100,000個のランダムに色付けされたピクセルを配置すると、putImage()では100ms未満ですが、fillRect()では900ms近くかかります。 ( http://Pastebin.com/4ijVKJcC のベンチマークコード)。

代わりに、ループの外側で単一の色を選択し、その色をランダムな位置にプロットするだけの場合、putImage()fillRect()で59ms対102msかかります。

rgb(...)構文でCSSカラー仕様を生成および解析するオーバーヘッドが、ほとんどの違いの原因となっているようです。

一方、生のRGB値をImageDataブロックに直接配置する場合、文字列の処理や解析は必要ありません。

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

異なるブラウザは異なる方法を好むようであるため、ロードプロセスの一部として3つすべての方法で小規模なテストを行い、どちらを使用するのが最適かを見つけ、それをアプリケーション全体で使用するのは理にかなっているでしょうか?

7
Daniel

長方形はどうですか? ImageDataオブジェクトを作成するよりも効率的でなければなりません。

4
sdleihssirhc

言及されていない方法の1つは、getImageDataを使用してからputImageDataを使用することです。
この方法は、一度にたくさんの絵をすばやく描く場合に適しています。
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

奇妙に思えますが、HTML5は線、円、長方形、その他多くの基本的な図形の描画をサポートしていますが、基本的な点を描画するのに適したものは何もありません。そうする唯一の方法は、あなたが持っているものでポイントをシミュレートすることです。

したがって、基本的には3つの解決策があります。

  • 点を線として描く
  • 点を多角形として描く
  • ポイントを円として描く

それぞれに欠点があります


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

南東方向に描画していることに注意してください。これがエッジの場合、問題が発生する可能性があります。ただし、他の方向に描画することもできます。


長方形

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

または、レンダリングエンジンは1ピクセルだけを埋めるため、fillRectを使用してより高速に処理します。

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


サークルの問題の1つは、エンジンがサークルをレンダリングするのが難しいことです。

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

塗りつぶしで達成できる長方形と同じアイデア。

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

これらすべてのソリューションの問題:

  • 描画しようとしているすべてのポイントを追跡するのは困難です。
  • ズームインすると、見苦しくなります。

ポイントを描く最良の方法は何ですか?」と思ったら、塗りつぶされた長方形で行きます。あなたは私の 比較テストでここにjsperf を見ることができます。

4
Salvador Dali

Sdleihssirhcが言ったような長方形を描く!

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

^-x:10、y:10に1x1の長方形を描画する必要があります

2
the_e

Phrogzの非常に徹底した答えを完成させるために、fillRect()putImageData()の間には重大な違いがあります。
最初はコンテキストを使用して描画します 以上 沿って 追加 長方形(ピクセルではなく)、 fillStyle アルファ値とコンテキスト globalAlpha そしてその 変換行列、 ラインキャップなど。
2番目は全体を置き換えます ピクセルのセット (1つかもしれませんが、なぜですか?)
jsperf でわかるように、結果は異なります。


一度に1つのピクセルを設定する必要はありません(画面にピクセルを描画することを意味します)。それが、それを行うための特定のAPIが存在しない理由です(当然です)。
パフォーマンスに関しては、目標が画像の生成(レイトレーシングソフトウェアなど)である場合、 常に 最適化されたUint8ArrayであるgetImageData()によって取得された配列を使用したい。その後、putImageData() ONCEを呼び出すか、setTimeout/seTIntervalを使用して1秒間に数回呼び出します。

1
Boing

うーん、長さ1ピクセルで幅1ピクセルの線を作成し、その方向を1つの軸に沿って移動させることもできます。

            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

高速HTMLデモコード: SFML C++グラフィックスライブラリについて知っていることに基づいて:

これをUTF-8エンコードのHTMLファイルとして保存し、実行します。 簡単にリファクタリングできます。日本語の変数は簡潔でスペースをあまりとらないため、使用したいだけです

まれに、1つの任意のピクセルを設定して画面に表示することはできません。だから使用する

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

多数の任意のピクセルをバックバッファーに描画するメソッド。 (格安通話)

次に、表示する準備ができたら、

Apply() 

変更を表示するメソッド。 (高価な通話)

以下の完全な.HTMLファイルコード:

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