HTML5 캔버스에서 단일 픽셀을 설정하는 가장 좋은 방법은 무엇입니까?


184

HTML5 Canvas에는 단일 픽셀을 명시 적으로 설정하는 방법이 없습니다.

매우 짧은 선을 사용하여 픽셀을 설정할 수는 있지만 앤티 앨리어싱 및 선 캡이 방해 될 수 있습니다.

또 다른 방법은 작은 ImageData객체 를 만들고 다음을 사용하는 것입니다 .

context.putImageData(data, x, y)

그것을 제자리에 두십시오.

누구나 효율적이고 신뢰할 수있는 방법을 설명 할 수 있습니까?

답변:


292

두 가지 최고의 경쟁자가 있습니다.

  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
  • 원하는 모든 픽셀로 채워진 다른 이미지 나 캔버스를 만들고 원하는 픽셀 drawImage()만 블리 팅 하는 데 사용 합니다. 아마도 매우 빠를 것이지만 필요한 픽셀을 미리 계산해야하는 한계가 있습니다.

내 테스트는 캔버스 컨텍스트를 저장 및 복원하지 않습니다 fillStyle. fillRect()성능 이 저하 될 수 있습니다. 또한 깨끗한 슬레이트로 시작하거나 각 테스트에 대해 정확히 동일한 픽셀 세트를 테스트하지 않습니다.


2
버그 보고서를 제출할 수 있다면 +10을 더 줄 것입니다! :)
Alnitak

51
내 GPU 및 그래픽 드라이버 fillRect()가 장착 된 컴퓨터에서 Chromev24의 1x1 putimage 데이터보다 거의 10 배 빠른 속도를 기록했습니다. 따라서 ... 속도가 중요하고 대상 고객을 알고 있다면 구식 답변이라는 단어를 사용하지 마십시오. 대신 : 테스트!
Phrogz

3
답변을 업데이트하십시오. 최신 브라우저에서는 채우기 방법이 훨씬 빠릅니다.
Buzzy

10
"PNG Encoder를 작성하는 것은 독자를위한 연습으로 남겨졌다"고 큰 소리로 웃었다.
파스칼 가나 예

2
내가 찾은 모든 위대한 Canvas 답변은 왜 당신에 의해 이루어 집니까? :)
Domino

19

언급되지 않은 한 가지 방법은 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

이것은 많은 픽셀을 찌를 때, 각 픽셀이 계산되거나 유사한 그래픽 데모의 경우 합리적인 방법입니다. 각 픽셀에 fillRect를 사용하는 것보다 10 배 빠릅니다.
Sam Watkins

예, 예외적 인 대답에 따르면이 방법은 다른 방법보다 100 배 느리다는 것이 항상 나를 괴롭 혔습니다. 1000보다 작게 플로팅하면이 방법이 이기고 다른 방법을 도살하기 시작하는 것이 사실 일 수 있습니다. 다음은 테스트 사례입니다 .... measurethat.net/Benchmarks/Show/8386/0/…
PAEz

17

나는 고려하지 fillRect()않았지만, 대답은 그것을 벤치마킹하도록 자극했다 putImage().

맥북 프로 (구)에 크롬 9.0.597.84으로, 임의의 위치에서 10 만 개 무작위 색깔의 픽셀을 퍼팅과 100ms의 채 걸리지 putImage()만, 거의 사용 900ms fillRect(). ( http://pastebin.com/4ijVKJcC의 벤치 마크 코드 ).

대신 루프 외부에서 단일 색상을 선택하고 임의의 위치에 해당 색상을 플롯하면에 putImage()대해 59ms 대 102ms가 걸립니다 fillRect().

구문에서 CSS 색상 사양을 생성하고 구문 분석하는 오버 헤드가 rgb(...)대부분의 차이를 담당 하는 것으로 보입니다 .

ImageData반면에 원시 RGB 값을 블록에 똑바로 넣으면 문자열 처리 또는 구문 분석이 필요하지 않습니다.


2
버튼을 클릭하고 각 메소드 (PutImage, FillRect) 및 추가로 LineTo 메소드를 테스트 할 수있는 플 런처를 추가했습니다. PutImage와 FillRect는 시간이 매우 가깝지만 LineTo는 매우 느립니다. plnkr.co/edit/tww6e1VY2OCVY4c4ECy3?p=preview 에서 확인하십시오 . 훌륭한 pastebin 코드를 기반으로합니다. 감사.
raddevus

그 플 런처의 경우 PutImage가 FillRect (최신 Chrome 63)보다 약간 느리다는 것을 알지만 LineTo를 시도하면 PutImage가 FillRect보다 훨씬 빠릅니다. 어떻게 든 그들은 방해하는 것 같습니다.
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 인덱스 = (x + y * imageData.width) * 4;
user889030

1
putImageData() 해당 기능 이후에 호출해야합니까 아니면 컨텍스트가 참조로 업데이트됩니까?
Lucas Sousa

7

브라우저마다 다른 방법을 선호하는 것 같으므로 로딩 프로세스의 일부로 세 가지 방법 모두로 더 작은 테스트를 수행하여 어느 것이 가장 적합한 지 알아 낸 다음 응용 프로그램 전체에서 사용하는 것이 합리적 일 수 있습니다.


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);
}

또는 렌더링 엔진이 한 픽셀 만 채울 수 있기 때문에 fillRect를 사용하는 더 빠른 방법입니다.

function point(x, y, canvas){
  canvas.fillRect(x,y,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하는 경우 fillRectChrome 보다 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를 기하 급수적으로 느린이기 때문에, 이것 좀 봐. 0.25 초 안에 다른 두 가지 방법으로 100,000 포인트를 수행 할 수 있지만 LineTo로 10,000 포인트는 5 초가 걸립니다.
raddevus

1
알았어, 실수하고 루프를 닫고 싶어 LineTo 코드에는 다음과 같은 매우 중요한 행이 누락되었습니다. ctx.beginPath (); 플런저 (다른 주석의 링크에서)를 업데이트하고 한 줄을 추가하면 LineTo 메소드가 평균 0.5 초에 100,000을 생성 할 수 있습니다. 꽤 놀랍습니다. 따라서 답변을 편집하고 해당 줄을 코드에 추가하면 (ctx.lineWidth 줄 이전) 나는 당신을 찬성합니다. 이 흥미로운 사실을 발견하고 원래 버그가있는 코드에 대해 사과드립니다.
raddevus

1

전체 Phrogz 아주 철저한 대답에, 사이에 중요한 차이가 fillRect()하고 putImageData().
제 용도 컨텍스트 그릴 위에 의해 가산 은 USING 직사각형 (NOT 화소)를 fillStyle에서는 된 알파 값 컨텍스트 globalAlpha 상기 변환 행렬 , 라인 캡 등
번째 대체하는 전체 픽셀의 세트 (아마 하나지만 왜 ?)
결과는 jsperf에서 볼 수 있듯이 다릅니다 .


한 번에 한 픽셀 씩 설정하고 싶지 않습니다 (화면에 그림을 그리는 것을 의미). 그렇기 때문에 특정 API가없는 이유가 바로 그런 것입니다.
성능면에서 목표가 그림 (예 : 광선 추적 소프트웨어)을 생성하는 것이라면 항상getImageData() 최적화 된 Uint8Array로 얻은 배열을 사용하려고합니다 . 그런 putImageData()다음를 사용하여 ONCE 또는 초당 몇 번 전화 setTimeout/seTInterval합니다.


이미지에 100k 블록을 넣고 싶지만 1 : 1 픽셀 스케일이 아닌 경우가있었습니다. fillRectChrome의 하드웨어 가속이 필요한 GPU에 대한 개별 호출에 대처할 수 없기 때문에 사용 이 어려웠습니다. 1시 1 분에 픽셀 데이터를 사용한 다음 CSS 스케일링을 사용하여 원하는 출력을 얻었습니다. 추한 :(
Alnitak

Firefox 42에서 링크 된 벤치 마크를 실행하면에 대해 168 Ops / sec 만 get/putImageData,에 대해 194,893을 fillRect받습니다. 1x1 image data125,102 Ops / sec입니다. 지금 fillRect까지 파이어 폭스에 의해 승리. 2012 년과 오늘 사이에 많은 변화가있었습니다. 항상 그렇듯이 이전 벤치 마크 결과에 의존하지 마십시오.
Mecki

12
한 번에 하나의 픽셀을 설정하고 싶습니다. 이 질문의 제목으로 다른 사람들도
그렇습니다

1

빠른 HTML 데모 코드 : SFML C ++ 그래픽 라이브러리에 대해 알고있는 내용을 기반으로합니다.

UTF-8 인코딩으로 HTML 파일로 저장하고 실행하십시오. 리팩토링을 자유롭게하십시오. 간결하고 공간을 많이 차지하지 않기 때문에 일본어 변수를 사용하는 것을 좋아합니다.

드물게 하나의 임의의 픽셀을 설정하여 화면에 표시하려고합니다. 따라서

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
영어 실력이 떨어지고 정돈 된 라이너 하나에 대해 투표권이 있습니다.
xavier

1
@xavier-english는 내 모국어가 아니며 예전 언어를 배우는 데 좋지는 않지만 내 답변을 편집하고 언어 버그를 수정할 수 있습니다 (긍정적 인 기여가 될 것입니다). 편리하고 사용하기 쉽기 때문에이 하나의 라이너를 사용했습니다. 예를 들어 학생들이 일부 그래픽 알고리즘을 테스트하는 것이 좋을 수 있지만 코드를 읽고 명확하게 작성해야하는 프로덕션에는 사용하기에 좋은 솔루션이 아닙니다.
Kamil Kiełczewski

3
@ KamilKiełczewski 코드는 명확하고 읽기 쉬워야하며 전문가만큼이나 중요합니다.
Logan Pickup

-2

putImageData아마도 fillRect기본적으로 더 빠를 것 입니다. 다섯 번째 매개 변수는 해석 해야하는 문자열을 사용하여 다른 방법 (사각형 색상)을 할당 할 수 있기 때문에 이것을 생각합니다.

그렇게하고 있다고 가정 해보십시오.

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호출 의 다섯 번째 인수 는 조금 더 긴 문자열입니다.


1
다섯 번째 인수로 색상 전달을 지원하는 브라우저는 무엇입니까? Chrome의 경우 context.fillStyle = ...대신 사용해야 했습니다. developer.mozilla.org/en-US/docs/Web/API/…
iX3
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.