Який найкращий спосіб встановити один піксель на полотні HTML5?


184

Полотно HTML5 не має методу явного встановлення одного пікселя.

Можливо, можливо встановити піксель, використовуючи дуже коротку лінію, але тоді антиалінізація та лінієві обмеження можуть заважати.

Іншим способом може бути створення невеликого 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()це на 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()продуктивність. Також зауважте, що я не починаю з чистого сланця або не тестую точно такий же набір пікселів для кожного тесту.


2
Я б дав вам ще +10, якщо зможу подати звіт про помилку! :)
Альнітак

51
Зауважте, що на моїй машині з моїм графічним процесором та графічними драйверами fillRect()останнім часом напівфабрикати стали майже в 10 разів швидшими, ніж 1x1 putimagedata на Chromev24. Отже ... якщо швидкість є критичною і ви знаєте свою цільову аудиторію, не сприймайте слово застарілої відповіді (навіть моєї). Замість цього: тест!
Фрогз

3
Будь ласка, оновіть відповідь. Спосіб заливки набагато швидший у сучасних браузерах.
Buzzy

10
"Написання PNGEncoder залишається як вправа для читача" змусило мене вголос сміятися.
Паскаль Ганае

2
Чому всі великі відповіді Canvas, на які я приземлююся, трапляються тобою? :)
Доміно

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 Надавши мені занепокоєння за те, що не в змозі прочитати ваші думки, це мало. Інші люди можуть потрапити сюди, шукаючи змогу побудувати багато пікселів. Я так і згадав більш ефективний спосіб, тому поділився ним.
ПАЕз

Це розумний метод, коли тикає багато пікселів, для графічної демонстрації, де кожен піксель обчислюється, або подібний. Це в десять разів швидше, ніж використання fillRect для кожного пікселя.
Сем Уоткінс

Так, мене завжди набридало, що у винятковій відповіді йдеться про те, що цей метод на 100 разів повільніше, ніж інші методи. Це може бути правдою, якщо ваш графік менше 1000, але з цього моменту цей метод починає вигравати, а потім забивати інші методи. Ось тест .... measurethat.net/Benchmarks/Show/8386/0 / ...
Паес

17

Я не розглядав fillRect(), але відповіді підштовхували мене до порівняння putImage().

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

Якщо замість цього я вибираю один колір поза петлями і просто putImage()малюю цей колір у випадкових місцях, займає 59 мс проти 102 мс fillRect().

Здається, що rgb(...)велика частина різниці є причиною генерації та аналізу специфікації кольору CSS у синтаксисі.

Переведення необроблених значень RGB прямо в ImageDataблок з іншого боку не вимагає обробки рядків чи їхнього розбору.


2
Я додав планкер, де можна натиснути кнопку і протестувати кожен із методів (PutImage, FillRect) і додатково метод LineTo. Це показує, що PutImage та FillRect дуже близькі за часом, але LineTo надзвичайно повільний. Перевірте це за адресою: plnkr.co/edit/tww6e1VY2OCVY4c4ECy3?p=preview Він заснований на вашому чудовому коді пастіна . Дякую.
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 index = (x + y * imageData.width) * 4;
користувач889030

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

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

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. (Дивіться мою відповідь для детальніше.)
Фрогз

2

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

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

^ - має намалювати прямокутник 1х1 на 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 балів з іншими 2-ма методами за 0,25 секунди, але 10000 балів за допомогою LineTo потрібно 5 секунд.
raddevus

1
Гаразд, я помилився і хотів би закрити петлю. У коді LineTo відсутній один - дуже важливий рядок - який виглядає наступним чином: ctx.beginPath (); Я оновив планкер (за посиланням з мого іншого коментаря) і додав, що одна лінія тепер дозволяє методу LineTo генерувати 100 000 в середньому за 0,5 секунди. Досить дивовижно. Тож якщо ви відредагуєте свою відповідь і додасте цей рядок до свого коду (до рядка ctx.lineWidth), я вас запрошу. Сподіваюся, ви знайшли це цікавим, і я вибачаюся за свій оригінальний код баггі.
raddevus

1

Щоб завершити Фрогз дуже ґрунтовною відповіддю, існує критична різниця між fillRect()та putImageData().
Спочатку використовує контекст , щоб намалювати над шляхом додавання прямокутника (НЕ піксель), використовуючи FillStyle значення альфа і контекст , globalAlpha і матрицю перетворення , лінійні ковпачки та т.д ..
Другим замінює весь набір пікселів (може бути один, але чому ?)
Результат відрізняється, як ви бачите на jsperf .


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


У мене був випадок, коли я хотів вкласти зображення в 100k, але не в масштабі 1: 1 пікселів. Використовувати fillRectбуло болісно, ​​тому що прискорення роботи ч / б Chrome не справляється з окремими дзвінками до GPU, які знадобляться. Мені довелося використовувати дані пікселів у 1: 1, а потім використовувати масштабування CSS, щоб отримати бажаний вихід. Це потворно :(
Alnitak

Запускаючи ваш зв'язаний орієнтир на Firefox 42, я отримую лише 168 Оп / сек за get/putImageData, але 194,893 за fillRect. 1x1 image dataстановить 125,102 Оп / сек. Тож fillRectперемагає далеко на Firefox. Тож справи сильно змінилися між 2012 і сьогодні. Як завжди, ніколи не покладайтеся на старі результати тестів.
Мецькі

12
Я хочу встановити один піксель одночасно. За назвою цього питання я здогадуюсь, що роблять і інші
chasmani

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 та пропозиція функції пікселя (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=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
Вниз голосував за погану англійську мову і забився один лайнер.
xavier

1
@xavier - англійська мова не є моєю рідною мовою, і я не добре вивчаю чужі мови, проте ви можете редагувати мою відповідь та виправляти мовні помилки (це буде позитивним внеском від вас). Я розміщую цей одноклапник, оскільки він зручний і простий у використанні - і може бути корисним для студентів, наприклад, для тестування деяких графічних алгоритмів, однак це не дуже гарне рішення використовувати у виробництві, де код повинен бути зрозумілим і зрозумілим.
Kamil Kiełczewski

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.