Jak najlepiej ustawić pojedynczy piksel w obszarze roboczym HTML5?

184

W kanwie HTML5 nie ma metody jawnego ustawienia pojedynczego piksela.

Możliwe może być ustawienie piksela za pomocą bardzo krótkiej linii, ale wtedy może się pojawić zakłócanie funkcji antyaliasingu i ograniczania linii.

Innym sposobem może być utworzenie małego ImageDataobiektu i użycie:

context.putImageData(data, x, y)

umieścić to na miejscu.

Czy ktoś może opisać skuteczny i niezawodny sposób na zrobienie tego?

Alnitak
źródło

Odpowiedzi:

292

Są dwa najlepsze podmioty:

  1. Utwórz dane obrazu 1 × 1, ustaw kolor i putImageDataw miejscu:

    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. Użyj, fillRect()aby narysować piksel (nie powinno być problemów z aliasingiem):

    ctx.fillStyle = "rgba("+r+","+g+","+b+","+(a/255)+")";
    ctx.fillRect( x, y, 1, 1 );

Możesz przetestować ich szybkość tutaj: http://jsperf.com/setting-canvas-pixel/9 lub tutaj https://www.measurethat.net/Benchmarks/Show/1664/1

Polecam testowanie pod kątem przeglądarek, na których Ci zależy, pod kątem maksymalnej prędkości. Od lipca 2017 r. fillRect()Jest 5-6 razy szybszy w Firefoksie v54 i Chrome v59 (Win7x64).

Inne, głupsze alternatywy to:

  • używanie getImageData()/putImageData()na całym płótnie; jest to około 100 × wolniej niż w przypadku innych opcji.

  • tworzenie niestandardowego obrazu za pomocą adresu URL danych i drawImage()wyświetlanie go:

    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
  • tworząc kolejny obraz lub płótno wypełnione wszystkimi pikselami, których chcesz, i użyj, drawImage()aby przesunąć tylko piksel, który chcesz w poprzek. Byłoby to prawdopodobnie bardzo szybkie, ale ma ograniczenie, które wymaga wstępnego obliczenia potrzebnych pikseli.

Zauważ, że moje testy nie próbują zapisywać i przywracać kontekstu obszaru roboczego fillStyle; spowolniłoby to fillRect()wydajność. Zauważ też, że nie zaczynam od czystego konta ani nie testuję dokładnie tego samego zestawu pikseli dla każdego testu.

Phrogz
źródło
2
Dałbym ci kolejne +10, gdybym mógł za zgłoszenie błędu! :)
Alnitak,
51
Zauważ, że na mojej maszynie z moim GPU i sterownikami graficznymi, fillRect()ostatnio stał się prawie 10 razy szybszy niż putimagedata 1x1 na Chromev24. Więc ... jeśli prędkość ma kluczowe znaczenie i znasz swoich docelowych odbiorców, nie wierz w przestarzałą odpowiedź (nawet moją). Zamiast tego: przetestuj!
Phrogz
3
Zaktualizuj odpowiedź. Metoda wypełniania jest znacznie szybsza w nowoczesnych przeglądarkach.
Buzzy,
10
„Pisanie PNGEncodera to ćwiczenie dla czytelnika” rozśmieszyło mnie na głos.
Pascal Ganaye
2
Dlaczego wszystkie wspaniałe odpowiedzi na płótnie, na które trafiłem, są przez ciebie? :)
Domino,
19

Jedną z metod, o której nie wspomniano, jest użycie getImageData, a następnie putImageData.
Ta metoda jest dobra, gdy chcesz szybko narysować dużo za jednym razem.
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);
PAEz
źródło
13
@Alnitak Daje mi neg za to, że nie potrafię czytać w twoich myślach, jest niska .. Inni ludzie mogą się tu dostać, żeby wykreślić wiele pikseli. Zrobiłem, a potem przypomniałem sobie bardziej efektywny sposób, więc podzieliłem się tym.
PAEz
Jest to rozsądna metoda przy narzucaniu dużej liczby pikseli, w przypadku demonstracji graficznej, w której każdy piksel jest obliczany lub podobny. Jest dziesięć razy szybszy niż użycie fillRect dla każdego piksela.
Sam Watkins
Tak, zawsze trochę mnie denerwowało, że wyjątek mówi, że ta metoda jest 100 razy wolniejsza niż inne metody. Może to być prawdą, jeśli spiskujesz mniej niż 1000, ale odtąd ta metoda zaczyna wygrywać, a następnie zabija inne metody. Oto przypadek testowy .... Measurethat.net/Benchmarks/Show/8386/0/…
PAEz
17

Nie zastanawiałem się fillRect(), ale odpowiedzi zachęciły mnie do porównaniaputImage() .

Umieszczenie 100 000 losowo kolorowych pikseli w losowych lokalizacjach z Chrome 9.0.597.84 na (starym) MacBooku Pro zajmuje mniej niż 100 ms putImage(), ale prawie 900 ms fillRect(). (Kod testu na stronie http://pastebin.com/4ijVKJcC ).

Jeśli zamiast tego wybiorę pojedynczy kolor poza pętlami i po prostu putImage()wykreślę ten kolor w losowych miejscach, zajmie to 59 ms w porównaniu do 102 ms fillRect().

Wygląda na to, że narzut związany z generowaniem i analizowaniem specyfikacji kolorów CSS w rgb(...)składni jest odpowiedzialny za większość różnic.

ImageDataZ drugiej strony wstawianie surowych wartości RGB bezpośrednio do bloku nie wymaga obsługi ciągów ani parsowania.

Alnitak
źródło
2
Dodałem plunker, w którym możesz kliknąć przycisk i przetestować każdą z metod (PutImage, FillRect) oraz dodatkowo metodę LineTo. Pokazuje, że PutImage i FillRect są bardzo zbliżone w czasie, ale LineTo jest bardzo powolny. Sprawdź to na: plnkr.co/edit/tww6e1VY2OCVY4c4ECy3?p=preview Oparte na twoim świetnym kodzie pastebin. Dzięki.
raddevus
W przypadku tego narzędzia widzę, że PutImage jest nieco wolniejszy niż FillRect (w najnowszym Chrome 63), ale po wypróbowaniu LineTo, PutImage jest znacznie szybszy niż FillRect. W jakiś sposób wydają się przeszkadzać.
mlepage 10.01.18
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;
}
Vit Kaspar
źródło
var index = (x + y * imageData.width) * 4;
user889030
1
Czy wywołanie putImageData() po tej funkcji lub kontekst będzie aktualizowany przez odniesienie?
Lucas Sousa
7

Ponieważ różne przeglądarki wydają się preferować różne metody, może warto przeprowadzić mniejszy test przy użyciu wszystkich trzech metod w ramach procesu ładowania, aby dowiedzieć się, który najlepiej użyć, a następnie użyć go w aplikacji?

Daniel
źródło
5

Wydaje się to dziwne, ale mimo to HTML5 obsługuje rysowanie linii, okręgów, prostokątów i wielu innych podstawowych kształtów, nie ma nic odpowiedniego do rysowania podstawowego punktu. Jedynym sposobem na to jest symulacja punktu z tym, co masz.

Zasadniczo istnieją 3 możliwe rozwiązania:

  • narysuj punkt jako linię
  • narysuj punkt jako wielokąt
  • narysuj punkt jako okrąg

Każda z nich ma swoje wady


Linia

function point(x, y, canvas){
  canvas.beginPath();
  canvas.moveTo(x, y);
  canvas.lineTo(x+1, y+1);
  canvas.stroke();
}

Pamiętaj, że zbliżamy się do kierunku południowo-wschodniego, a jeśli jest to krawędź, może być problem. Ale możesz także rysować w dowolnym innym kierunku.


Prostokąt

function point(x, y, canvas){
  canvas.strokeRect(x,y,1,1);
}

lub w szybszy sposób przy użyciu fillRect, ponieważ silnik renderujący wypełnia tylko jeden piksel.

function point(x, y, canvas){
  canvas.fillRect(x,y,1,1);
}

okrąg


Jednym z problemów z kręgami jest to, że silnik jest trudniejszy do ich renderowania

function point(x, y, canvas){
  canvas.beginPath();
  canvas.arc(x, y, 1, 0, 2 * Math.PI, true);
  canvas.stroke();
}

ten sam pomysł jak w przypadku prostokąta, który można osiągnąć dzięki wypełnieniu.

function point(x, y, canvas){
  canvas.beginPath();
  canvas.arc(x, y, 1, 0, 2 * Math.PI, true);
  canvas.fill();
}

Problemy ze wszystkimi tymi rozwiązaniami:

  • trudno jest śledzić wszystkie punkty, które zamierzasz wylosować.
  • podczas powiększania wygląda brzydko.

Jeśli zastanawiasz się: „Jak najlepiej narysować punkt? ”, Wybrałbym wypełniony prostokąt. Możesz zobaczyć mój jsperf tutaj z testami porównawczymi .

Salvador Dali
źródło
Kierunek południowo-wschodni? Co?
LoganDark,
4

Co z prostokątem? To musi być bardziej wydajne niż tworzenie ImageDataobiektu.

sdleihssirhc
źródło
3
Można by tak pomyśleć, i może to dotyczyć pojedynczego piksela, ale jeśli wstępnie utworzysz dane obrazu i ustawisz 1 piksel, a następnie użyjesz putImageDatago, jest 10 razy szybszy niż fillRectw Chrome. (Zobacz moją odpowiedź, aby dowiedzieć się więcej.)
Phrogz
2

Narysuj prostokąt, jak powiedział sdleihssirhc!

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

^ - powinien narysować prostokąt 1x1 o wymiarach x: 10, y: 10

ciebie
źródło
1

Hmm, możesz też po prostu utworzyć linię o szerokości 1 piksela i długości 1 piksela, a jej kierunek przesunie się wzdłuż jednej osi.

            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();
trusktr
źródło
1
Zaimplementowałem rysowanie pikseli jako FillRect, PutImage i LineTo i utworzyłem plunker na: plnkr.co/edit/tww6e1VY2OCVY4c4ECy3?p=preview Sprawdź to, ponieważ LineTo jest wykładniczo wolniejszy. Może wykonać 100 000 punktów w 2 innych metodach w 0,25 sekundy, ale 10 000 punktów w LineTo zajmuje 5 sekund.
raddevus
1
OK, popełniłem błąd i chciałbym zamknąć pętlę. W kodzie LineTo brakowało jednego - bardzo ważnego wiersza - który wygląda następująco: ctx.beginPath (); Zaktualizowałem plunker (pod linkiem z mojego innego komentarza) i dodając, że jedna linia pozwala teraz na wygenerowanie 100 000 w metodzie LineTo średnio w 0,5 sekundy. Całkiem niesamowite. Więc jeśli zredagujesz swoją odpowiedź i dodasz tę linię do swojego kodu (przed linią ctx.lineWidth), będę cię głosować. Mam nadzieję, że uważasz to za interesujące i przepraszam za mój oryginalny kod buggy.
raddevus
1

Aby udzielić bardzo dokładnej odpowiedzi na pytanie, istnieje krytyczna różnica między fillRect()i putImageData().
Pierwszy kontekst zastosowania wyciągnąć ponad poprzez dodanie prostokąt (nie pikseli), stosując fillStyle wartość alfa i kontekst globalAlpha oraz macierz transformacji , czapki linii itp ..
The drugi zastępuje cały zestaw pikseli (może jeden, ale dlaczego ?)
Wynik jest inny, jak widać na jsperf .


Nikt nie chce ustawiać jednego piksela na raz (co oznacza rysowanie go na ekranie). Dlatego nie ma specyficznego API do tego (i słusznie).
Jeśli chodzi o wydajność, jeśli celem jest wygenerowanie obrazu (na przykład oprogramowania do śledzenia promieni), zawsze chcesz użyć tablicy uzyskanej za getImageData()pomocą zoptymalizowanej macierzy Uint8Array. Następnie dzwonisz putImageData()RAZ lub kilka razy na sekundę za pomocą setTimeout/seTInterval.

Boing
źródło
Miałem przypadek, w którym chciałem umieścić 100 000 bloków na obrazie, ale nie w skali 1: 1. Korzystanie fillRectbyło bolesne, ponieważ przyspieszenie sprzętowe Chrome nie radzi sobie z poszczególnymi połączeniami do GPU, których wymagałoby. Skończyło się na tym, że musiałem użyć danych pikselowych w stosunku 1: 1, a następnie użyć skalowania CSS, aby uzyskać pożądany wynik. To brzydkie :(
Alnitak,
Uruchamiając połączony test porównawczy w Firefox 42, otrzymuję tylko 168 get/putImageDataoperacji na sekundę , ale 194,893 za fillRect. 1x1 image datawynosi 125 102 Oper / s. Tak fillRectwygrywa zdecydowanie w Firefoksie. Więc wiele się zmieniło między 2012 r. A dniem dzisiejszym. Jak zawsze, nigdy nie polegaj na starych wynikach testów.
Mecki,
12
Chcę ustawić jeden piksel na raz. Domyślam się z tytułu tego pytania, że ​​robią to również inni ludzie
chasmani
1

Szybki kod demonstracyjny HTML: Na podstawie tego, co wiem o bibliotece graficznej SFML C ++:

Zapisz to jako plik HTML z kodowaniem UTF-8 i uruchom. Zapraszam do refaktoryzacji, po prostu lubię używać japońskich zmiennych, ponieważ są one zwięzłe i nie zajmują dużo miejsca

Rzadko będziesz chciał ustawić JEDEN dowolny piksel i wyświetlić go na ekranie. Więc użyj

PutPix(x,y, r,g,b,a) 

metoda narysowania wielu dowolnych pikseli w buforze wstecznym. (tanie rozmowy)

Następnie, gdy będziesz gotowy do pokazu, zadzwoń pod numer

Apply() 

metoda wyświetlania zmian. (kosztowne połączenie)

Pełny kod pliku .HTML poniżej:

<!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>
JMI MADISON
źródło
0

Jeśli obawiasz się o szybkość, możesz również rozważyć WebGL.

Martin Ždila
źródło
-1

HANDY i propozycja funkcji put pixel (pp) (ES6) (read-pixel tutaj ):

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

Ta funkcja korzysta z putImageDataczęści inicjalizacyjnej (pierwsza długa linia). Na początku zamiast tego s='.myCanvas'użyj selektora CSS na swoim płótnie.

Chcę normalizować parametry do wartości od 0-1, powinieneś zmienić wartość domyślną a=255na a=1i dopasować do: id.data.set([r,g,b,a]),ctx.putImageData(id, x, y)do id.data.set([r*255,g*255,b*255,a*255]),ctx.putImageData(id, x*c.width, y*c.height)

Przydatny powyższy kod jest przydatny do testowania algorytmów graficznych ad hoc lub sprawdzania koncepcji, ale nie jest dobry do użycia w produkcji, w której kod powinien być czytelny i przejrzysty.

Kamil Kiełczewski
źródło
1
Obniżony głos za słaby angielski i zagracony jeden linijka.
Xavier
1
@xavier - angielski nie jest moim językiem ojczystym i nie jestem dobry w nauce języków obcych, jednak możesz edytować moją odpowiedź i naprawiać błędy językowe (to będzie pozytywny wkład od ciebie). Umieściłem ten jeden wiersz, ponieważ jest poręczny i łatwy w użyciu - i może być dobry na przykład dla studentów do testowania niektórych algorytmów graficznych, jednak nie jest dobrym rozwiązaniem do zastosowania w produkcji, w której kod powinien być czytelny i przejrzysty.
Kamil Kiełczewski,
3
@ KamilKiełczewski Czytelność i przejrzystość kodu jest tak samo ważna dla studentów, jak i dla profesjonalistów.
Logan Pickup
-2

putImageDatajest prawdopodobnie szybszy niż fillRectnatywnie. Myślę, że to dlatego, że piąty parametr może mieć różne sposoby przypisywania (kolor prostokąta), używając ciągu, który należy interpretować.

Załóżmy, że robisz to:

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")`

Więc linia

context.fillRect(x, y, 1, 1, "rgba(255, 255, 255, 0.5)")`

jest najcięższy ze wszystkich. Piąty argument w fillRectwywołaniu jest nieco dłuższy.

Hydroper
źródło
1
Które przeglądarki obsługują przekazywanie koloru jako piąty argument? W przypadku Chrome musiałem context.fillStyle = ...zamiast tego użyć . developer.mozilla.org/en-US/docs/Web/API/…
iX3