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


184

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

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

別の方法は、小さなImageDataオブジェクトを作成して使用することかもしれません:

context.putImageData(data, x, y)

それを所定の位置に配置します。

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

回答:


292

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)では5〜6倍高速です。

その他、愚かな代替案は次のとおりです。

  • 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()パフォーマンスが低下します。また、私は白紙の状態から始めたり、各テストでまったく同じピクセルセットをテストしたりしていないことにも注意してください。


2
バグレポートを提出していただければ、さらに+10差し上げます。:)
アルニタク

51
GPUとグラフィックスドライバーを搭載した私のマシンでは、fillRect()Chromev24の1x1 putimagedataと比べて、最近ではほぼ10倍高速になったことに注意してください。だから...スピードが重要で、あなたがあなたのターゲットオーディエンスを知っているなら、時代遅れの答えの言葉を取らないでください(私のものでも)。代わりに:テストしてください!
Phrogz

3
回答を更新してください。最近のブラウザでは、fillメソッドの方がはるかに高速です。
バジー

10
「PNGEncoderの記述は読者の練習問題として残されています」と大声で笑わせました。
Pascal Ganaye 2014年

2
なぜ私が上に着くすべての素晴らしいCanvasの答えがあなたによってたまたまあるのですか?:)
ドミノ

19

言及されていない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);

13
@Alnitakあなたの心を読むことができないという否定を私に与えることは低いです。私はそれをし、より効率的な方法を思い出したので、それを共有しました。
PAEz 2018

これは、各ピクセルが計算されるグラフィックデモなどで、多くのピクセルを突っ込むときに賢明な方法です。各ピクセルにfillRectを使用するよりも10倍高速です。
サムワトキンス

ええ、例外的な答えがこの方法は他の方法よりも100倍遅いと言っていることはいつも私をちょっと悩ませました。これは、プロットが1000未満の場合に当てはまる可能性がありますが、それ以降、このメソッドは勝ち始め、他のメソッドを殺します。ここではテストケースが....だ measurethat.net/Benchmarks/Show/8386/0/...
パエス

17

私は考慮していませんでしたfillRect()が、答えはそれをベンチマークに駆り立てましたputImage()

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

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

rgb(...)違いのほとんどは、構文でのCSSカラー仕様の生成と解析のオーバーヘッドが原因であると思われます。

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


2
ボタンをクリックして各メソッド(PutImage、FillRect)およびLineToメソッドをテストできるプランカーを追加しました。これは、PutImageとFillRectが非常に接近しているが、LineToが非常に遅いことを示しています。でそれをチェックアウト:plnkr.co/edit/tww6e1VY2OCVY4c4ECy3?p=previewそれはあなたの偉大なペーストビンコードに基づいています。ありがとう。
raddevus

そのプランカーの場合、PutImageは(最新のChrome 63では)FillRectよりも少し遅いことがわかりますが、LineToを試した後、PutImageはFillRectよりも大幅に高速です。どういうわけか、彼らは干渉しているようです。
mlepage '10年

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.width)* 4;
user889030 2018年

1
putImageData() その関数の後に呼び出す必要がありますか、参照によってコンテキストが更新されますか?
Lucas Sousa

7

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


5

奇妙に思われるかもしれませんが、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は、比較テストで確認できます。


南東方向?何?
LoganDark

4

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


3
あなたはそう思うだろうし、それは単一のピクセルに対するものかもしれないが、画像データを事前に作成して1ピクセルを設定し、それを使用putImageDataするとfillRect、Chromeの場合よりも10倍速くなります。(詳細については、私の回答を参照してください。)
Phrogz

2

sdleihssirhcが言ったように長方形を描きます!

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

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


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
私はFillRect、PutImageとLINETOとしてピクセル描画を実装し、でplunkerを作成:plnkr.co/edit/tww6e1VY2OCVY4c4ECy3?p=previewは LINETOが指数関数的に遅いので、それをチェックアウト。他の2つの方法で100,000ポイントを0.25秒で実行できますが、LineToでの10,000ポイントは5秒かかります。
raddevus

1
さて、私は間違いを犯し、ループを閉じたいと思います。LineToコードに次のような1つの非常に重要な行がありませんでした:ctx.beginPath(); (他のコメントからのリンクで)プランカーを更新し、その1行を追加すると、LineToメソッドで0.5秒の平均で100,000を生成できるようになりました。かなり素晴らしい。したがって、回答を編集してその行をコードに追加する場合(ctx.lineWidth行の前)、私はあなたに賛成票を投じます。私はあなたがこれが興味深いものであることを望みます、そして私の元のバグのあるコードをお詫び申し上げます。
raddevus

1

Phrogzの非常に完全な答えを完了するには、との間に重要な違いがfillRect()ありputImageData()ます。
1つ目は、コンテキストを使用して、fillStyleアルファ値とコンテキストglobalAlpha変換行列ラインキャップなどを使用し、四角形(ピクセルではない)を追加することで描画ます。2つ目は、ピクセルセット全体(おそらく1つ)を置き換えます?) jsperfで確認できるように、結果は異なります。


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


1対1のピクセルスケールではなく、画像に100kのブロックを配置したい場合がありました。fillRectChromeのハードウェアアクセラレーションは、必要なGPUへの個々の呼び出しに対応できないため、使用は苦痛でした。ピクセルデータを1:1で使用し、CSSスケーリングを使用して目的の出力を取得する必要がありました。醜い:(
Alnitak

Firefox 42でリンクされたベンチマークを実行すると、には168 get/putImageDataオペレーション/秒しか得られませんが、には194,893が得られfillRectます。1x1 image data125,102 Ops /秒です。それでfillRect、Firefoxで圧倒的に勝ちました。したがって、2012年と今日の間で状況は大きく変化しました。いつものように、決して古いベンチマーク結果に依存しないでください。
Mecki

12
一度に1ピクセルずつ設定したい。私は他の人がやることは、この質問のタイトルで推測している
chasmani

1

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

これをHTMLファイルとしてUTF-8エンコーディングで保存し、実行します。 自由にリファクタリングしてください。日本語の変数を使用するのは好きです。なぜなら、それらは簡潔であり、あまりスペースを取らないからです。

まれに、任意のピクセルを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>


-1

ハンディとプットピクセル(pp)関数(ES6)の命題(ここでピクセルを読み取る):

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

この関数は使用しputImageData、初期化部分(最初の長い行)を持っています。最初に、代わりにs='.myCanvas'キャンバスにCSSセレクターを使用します。

あなたがしたい私はあなたがデフォルト値を変更する必要が0-1からの値にパラメータを正常化するためa=255a=1持つとライン: 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)

上記の便利なコードは、グラフィックスアルゴリズムのアドホックテストや概念実証に適していますが、コードを読みやすく明確にする必要がある本番環境での使用には適していません。


1
貧しい英語と散らかったワンライナーのために反対票を投じました。
ザビエル

1
@xavier-英語は私の母国語ではなく、私は外国語を学ぶのが苦手ですが、私の回答を編集して言語のバグを修正できます(あなたからの積極的な貢献になります)。このワンライナーは便利で使いやすいので入れました。たとえば、学生がいくつかのグラフィックアルゴリズムをテストするのに適していますが、コードを読みやすく明確にする必要がある本番環境で使用するのは適切なソリューションではありません。
カミルキエチェフスキ

3
@KamilKiełczewskiコードが読みやすく明確であることは、専門家にとっても学生にとっても同じくらい重要です。
Logan Pickup

-2

putImageDataおそらくfillRectネイティブよりも高速です。これは、解釈する必要がある文字列を使用して、5番目のパラメーターに異なる方法(四角形の色)を割り当てることができるためです。

あなたがそうしているとしましょう:

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

だから、ライン

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

すべての中で最も重いです。fillRect呼び出しの5番目の引数は少し長い文字列です。


1
5番目の引数として色を渡すことをサポートしているブラウザーはどれですか?Chromeの場合はcontext.fillStyle = ...代わりに使用する必要がありました。developer.mozilla.org/en-US/docs/Web/API/...
IX3
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.