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

160

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

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

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

context.putImageData(data, x, y)

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

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

Теги:
canvas
pixel

14 ответов

259
Лучший ответ

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

  • Создайте данные изображения 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
    Я бы дал вам еще +10, если бы я мог за сообщение об ошибке! :)
  • 48
    Обратите внимание, что на моей машине с моим графическим и графическим драйверами fillRect() последнее время стала почти в 10 раз быстрее, чем предполагаемые данные 1x1 на Chromev24. Так что ... если скорость критична и вы знаете свою целевую аудиторию, не верьте слову устаревшего ответа (даже моего). Вместо этого: тест!
Показать ещё 13 комментариев
14

Я не считал 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 не требуется обработка строк или синтаксический анализ.

  • 1
    Я добавил плункер, где вы можете нажать кнопку и протестировать каждый из методов (PutImage, FillRect), а также метод LineTo. Это показывает, что PutImage и FillRect очень близки во времени, но LineTo очень медленный. Проверьте это по адресу: plnkr.co/edit/tww6e1VY2OCVY4c4ECy3?p=preview Это основано на вашем замечательном коде Pasbin. Благодарю.
  • 0
    Для этого плункера я вижу, что PutImage немного медленнее, чем FillRect (в последней версии Chrome 63), но после того, как я попробую LineTo, PutImage будет значительно быстрее, чем FillRect. Почему-то они, кажется, мешают.
10
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;
}
  • 0
    var index = (x + y * imageData.width) * 4;
7

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

  • 2
    +1 теперь это умная идея
4

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

  • 0
    Юго-восточное направление? Какие?
4

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

  • 3
    Вы бы подумали, и это может быть для одного пикселя, но если вы предварительно создаете данные изображения и устанавливаете 1 пиксель, а затем используете putImageData это в 10 раз быстрее, чем fillRect в Chrome. (Смотрите мой ответ для более.)
  • 0
    да, я тоже пытался его сравнить, и нашел похожие результаты.
2

Один из методов, о котором не упоминалось, - использование 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);
  • 0
    Вопрос был конкретно об установке одного пикселя за раз.
  • 5
    @Alnitak Давать мне отрицание из-за того, что я не могу читать твои мысли, низок .. Другие люди могут оказаться здесь, пытаясь построить много пикселей. Я сделал, а потом вспомнил более эффективный способ, поэтому поделился им.
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
    Вау, интересно!
Показать ещё 1 комментарий
1

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


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

  • 0
    У меня был случай, когда я хотел поместить 100k блоков в изображение, но не в масштабе 1: 1. Использование fillRect было болезненным, потому что ч / б ускорение Chrome не справляется с отдельными вызовами графического процессора, которые ему потребуются. В итоге мне пришлось использовать данные пикселей в пропорции 1: 1, а затем использовать масштабирование CSS, чтобы получить желаемый результат. Это ужасно :(
  • 0
    Запустив связанный тест на Firefox 42, я получаю только 168 get/putImageData секунду для get/putImageData и 194 893 для fillRect . 1x1 image data - 125 102 операций в секунду. Так что fillRect выигрывает в Firefox. Так что с 2012 года по сегодняшний день многое изменилось. Как всегда, никогда не полагайтесь на старые результаты тестов.
Показать ещё 2 комментария
0

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

0

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

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

  • 0
    Какие браузеры поддерживают передачу цвета в качестве 5-го аргумента? Для Chrome мне пришлось использовать context.fillStyle = ... вместо этого. developer.mozilla.org/en-US/docs/Web/API/...
-2

ОТЛИЧНОЕ и эффективное предложение функции put pixel (pp) (ES6) (read-pixel here):

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>

Эта функция использует 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,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)

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

  • 0
    Голосовали за плохой английский и загроможденный лайнер.
  • 0
    @xavier - английский не является моим родным языком, и я не очень хорошо изучаю иностранные языки, однако вы можете редактировать мой ответ и исправлять языковые ошибки (это будет положительный вклад с вашей стороны). Я поместил этот однострочный текст, потому что он удобен и прост в использовании - и может быть полезен, например, для студентов, чтобы протестировать некоторые графические алгоритмы, однако это не очень хорошее решение для использования в производстве, где код должен быть читаемым и понятным.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню