Каков наилучший способ установить один пиксель на холсте HTML5?


холст HTML5 не имеет метода для явной установки одного пикселя.

Это может быть возможным, чтобы установить точку, используя очень короткие строки, но тогда antialising и кепки линия может помешать.

другой способ может заключаться в создании небольшого ImageData объект и использование:

context.putImageData(data, x, y)

чтобы поставить его на место.

может ли кто-нибудь описать эффективный и надежный способ сделать это?

13 154

13 ответов:

есть два лучших претендентов:

  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, fillRect() это 5-6× быстрее на 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() производительность. Также обратите внимание, что я не начинаю с чистый лист или тестирование точно такого же набора пикселей для каждого теста.

Я не рассматривал fillRect(), но ответы побудили меня сравнить его с putImage().

размещение 100 000 случайно окрашенных пикселей в случайных местах, с Chrome 9.0.597.84 на (старом) MacBook Pro, занимает менее 100 мс с putImage(), но почти 900ms с помощью fillRect(). (Тестовый код на http://pastebin.com/4ijVKJcC).

если вместо этого я выберу один цвет за пределами петель и просто построю этот цвет в случайных местах,putImage() берет 59ms против 102ms для fillRect().

кажется, что накладные расходы на создание и анализ спецификации цвета CSS в rgb(...) синтаксис отвечает за большую часть разницы.

ввод необработанных значений RGB прямо в ImageData блок с другой стороны не требует обработки строк или разбора.

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

поскольку разные браузеры, похоже, предпочитают разные методы, может быть, имеет смысл сделать меньший тест со всеми тремя методами в рамках процесса загрузки, чтобы узнать, что лучше всего использовать, а затем использовать это во всем приложении?

Как насчет прямоугольника? Это должно быть более эффективным, чем создание

это кажется странным, но тем не менее 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 здесь с сравнительными тестами.

Нарисуйте прямоугольник, как сказал sdleihssirhc!

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

^-- должен нарисовать прямоугольник 1x1 в x:10, y: 10

Хм, вы также можете просто сделать линию шириной 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();

чтобы завершить Phrogz очень подробный ответ, есть критическая разница между fillRect() и putImageData().
первый использует контекст для рисования over by добавлять прямоугольник (не пиксель), используя fillStyle Альфа-значение и контекст globalAlpha и матрица преобразования, линия крышки etc..
второй заменяет целый набор пикселей (может быть, один, но почему ?)
результат отличается, как вы можете видеть на jsperf.


никто не хочет устанавливать один пиксель за раз (что означает рисование его на экране). Вот почему для этого нет специального API (и это правильно).
производительность мудрый, если целью является создание изображения (например, программное обеспечение для трассировки лучей), вы всегда хотите использовать массив, полученный с помощью getImageData(), который является оптимизированным Uint8Array. Тогда вы звоните putImageData() один или несколько раз в во-вторых, используя setTimeout/seTInterval.

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 вызов - это немного более длинная строка.

быстрый HTML демо-код: Основываясь на том, что я знаю о графической библиотеке SFML C++:

сохраните это как HTML-файл с кодировкой UTF-8 и запустите его. не стесняйтесь рефакторинг, мне просто нравится использовать японские переменные, потому что они лаконичны и не занимают много места

редко вы собираетесь установить один произвольный пиксель и дисплей это на экране. Так что используйте

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>

Если вы обеспокоены скоростью, то вы также можете рассмотреть WebGL.

один метод, который не был упомянут, использует 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);