Каков наилучший способ установить один пиксель на холсте 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 года, 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
Обратите внимание, что на моей машине с моим графическим процессором и графическими драйверами за fillRect()последнее время он стал почти в 10 раз быстрее, чем предполагаемые данные 1x1 на Chromev24. Так что ... если скорость критична и вы знаете свою целевую аудиторию, не верьте слову устаревшего ответа (даже моего). Вместо этого: тест!
Phrogz

3
Пожалуйста, обновите ответ. Метод заполнения намного быстрее в современных браузерах.
Buzzy

10
«Написание PNGEncoder оставлено в качестве упражнения для читателя», - заставил меня смеяться вслух.
Паскаль Ганай

2
Почему все великие ответы Холста, на которые я приземляюсь, оказываются вами? :)
Домино

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 для каждого пикселя.
Сэм Уоткинс

Да, мне всегда нравилось, что исключенный ответ говорит, что этот метод в 100 раз медленнее, чем другие методы. Это может быть правдой, если ваш график составляет менее 1000, но с этого момента этот метод начинает побеждать, а затем убивать другие методы. Вот контрольный пример .... measurethat.net/Benchmarks/Show/8386/0/…
PAEz

17

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

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

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

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

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


2
Я добавил плункер, где вы можете нажать кнопку и протестировать каждый из методов (PutImage, FillRect), а также метод LineTo. Это показывает, что PutImage и FillRect очень близки во времени, но LineTo чрезвычайно медленный. Проверьте это по адресу: plnkr.co/edit/tww6e1VY2OCVY4c4ECy3?p=preview Это основано на вашем замечательном коде Pasbin. Спасибо.
Раддевус

Для этого плункера я вижу, что 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 index = (x + y * imageData.width) * 4;
user889030

1
Должен ли вызываться putImageData() после этой функции или контекст будет обновляться по ссылке?
Лукас Соуза

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

та же идея, что и с прямоугольником, которую вы можете реализовать с помощью fill.

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его в 10 раз быстрее, чем fillRectв Chrome. (Смотрите мой ответ больше.)
Phrogz

2

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

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

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


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 и создал плункер по адресу: plnkr.co/edit/tww6e1VY2OCVY4c4ECy3?p=preview Проверьте это, потому что LineTo экспоненциально медленнее. Может набрать 100 000 баллов w другими 2 способами за 0,25 секунды, но 10000 баллов с LineTo занимает 5 секунд.
Раддевус

1
Хорошо, я сделал ошибку, и я хотел бы замкнуть петлю. В коде LineTo отсутствовала одна - очень важная строка - которая выглядит следующим образом: ctx.beginPath (); Я обновил плункер (по ссылке из моего другого комментария) и добавил, что одна строка теперь позволяет методу LineTo генерировать 100 000 в среднем за 0,5 секунды. Довольно удивительно. Так что, если вы отредактируете свой ответ и добавите эту строку в свой код (перед строкой ctx.lineWidth), я вас поддержу. Я надеюсь, что вы нашли это интересным, и я прошу прощения за мой оригинальный код ошибки.
Раддевус

1

Чтобы завершить Phrogz очень подробный ответ, есть критическая разница между fillRect()и putImageData().
Сначала использует контекст , чтобы нарисовать над путем добавления прямоугольника (не пиксель), используя FillStyle значения альфа и контекст , globalAlpha и матрицу преобразования , линейные колпачки и т.д ..
Вторым заменяет весь набор пикселей (может быть один, но почему ?)
Результат отличается, как вы можете видеть на jsperf .


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


У меня был случай, когда я хотел поместить 100k блоков в изображение, но не в масштабе 1: 1. Использование fillRectбыло болезненным, потому что ч / б ускорение Chrome не справляется с отдельными вызовами графического процессора, которые ему потребуются. В итоге мне пришлось использовать данные пикселей в пропорции 1: 1, а затем использовать масштабирование CSS, чтобы получить желаемый результат. Это некрасиво :(
Альнитак

Запустив связанный тест на Firefox 42, я получаю только 168 get/putImageDataопераций в секунду , а 194 893 - fillRect. 1x1 image data125,102 Ops / сек. Так что fillRect, безусловно, побеждает в Firefox. Так что с 2012 года по сегодняшний день многое изменилось. Как всегда, никогда не полагайтесь на старые результаты тестов.
Меки

12
Я хочу установить один пиксель за раз. По названию этого вопроса я угадываю, что и другие люди делают то же
самое

1

Быстрый 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>


-1

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

Эта функция используется putImageDataи имеет часть инициализации (первая длинная строка). В начале вместоs='.myCanvas' используйте CSS-селектор для вашего холста.

Я хочу , чтобы вы нормализовать параметры значения от 0-1 вы должны изменить стандартное значение a=255для a=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 Код, читаемый и понятный, так же важен для студентов, как и для профессионалов.
Логан Пикап

-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
Какие браузеры поддерживают передачу цвета в качестве 5-го аргумента? Для Chrome мне пришлось использовать context.fillStyle = ...вместо этого. developer.mozilla.org/en-US/docs/Web/API/...
ix3
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.