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

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

Возможно, можно установить пиксель с использованием очень короткой строки, но тогда могут возникнуть помехи и штрихи.

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

context.putImageData(data, x, y)

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

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

Ответ 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 );     
    
  • Используйте 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% быстрее, чем в 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(), чтобы блистить только пиксель, который вы хотите. Это, вероятно, будет очень быстро, но имеет ограничение, необходимое для предварительного расчета необходимых вам пикселей.

Обратите внимание, что мои тесты не пытаются сохранить и восстановить контекст canvas fillStyle; это замедлит производительность fillRect(). Также обратите внимание, что я не начинаю с чистого листа или испытываю точно такой же набор пикселей для каждого теста.

Ответ 2

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

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

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

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

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

Ответ 3

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

Ответ 4

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

Ответ 5

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

Ответ 6

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

Ответ 7

Один из методов, о котором не упоминалось, - использование 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);

Ответ 8

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

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

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

Ответ 9

Чтобы завершить очень тщательный ответ Phrogz, существует критическое различие между fillRect() и putImageData().
Первый использует контекст для рисования over с помощью добавления прямоугольника (НЕ пикселя), используя значение fillStyle alpha и контекст globalAlpha и матрица преобразования , и т.д.
Второй заменяет весь набор пикселей (возможно, один, но почему?)
Результат отличается, как вы можете видеть на jsperf.


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

Ответ 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();

Ответ 11

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

Сохраните это как 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>

Ответ 12

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 является более длинной строкой.

Ответ 13

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

Ответ 14

HANDY и предложение из пут пикселя (рр) функции (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

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))()

// draw shape
for(let i=0; i<800; i++) {
   let a = Math.PI*4*i/800;
   let x=16*Math.sin(a)**3;
   let y=13*Math.cos(a)-5*Math.cos(2*a)-2*Math.cos(3*a)-Math.cos(4*a);   
   pp(100+2.5*x,45-2.5*y,255,0,0,255)   
}
<canvas class="myCanvas" width=200 height=100 style="background: black"></canvas>