JavaScript/Canvas/Pixel Manipulation

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

Neben den vorangegangenen Funktionen bietet Canvas schließlich noch die Möglichkeit, die einzelnen Pixel der Canvas direkt anzusprechen und sowohl in ihrer Farbe, als auch in ihrem Transparenzwert zu verändern. Hierzu wird ein sogenanntes ImageData-Objekt benötigt, das Informationen über Breite, Höhe und den eigentlichen Daten eines Bildes speichert. In Verbindung mit derartigen Objekten stehen folgende Funktionen zur Verfügung:

ImageData-Objekt

createImageData

Die createImageData(width, height)-Methode erzeugt ein neues ImageData-Objekt mit der angegebenen Breite und Höhe und belegt alle Pixel mit dem Wert rgb(0 0 0 / 0) vor. Berücksichtigen Sie, dass dabei innerhalb des ImageData-Objekts lediglich die als Argumente übergebenen Werte gespeichert werden, jedoch nicht solche Werte, die im jeweiligen Context-Objekt gespeichert sind; das erzeugte ImageData-Objekt ist demnach lediglich vom Context-Typ (also 2d), nicht jedoch vom konkreten Context-Objekt abhängig.

Die createImageData(imgData)-Methode erzeugt ein neues ImageData-Objekt mithilfe eines bereits bestehenden. Hierbei werden lediglich die Ausmaße, nicht jedoch der Inhalt von diesem übernommen. Danach wird das neue Objekts vollständig mit der Farbe rgb(0 0 0 / 0) vorbelegt.

getImageData

Die getImageData(x, y, width, height)-Methode gibt ein ImageData-Objekt zurück, das denjenigen Bereich repräsentiert, der durch die übergebenen Attribute spezifiziert wird. Beachten Sie jedoch, dass die Veränderung des zurückgegebenen Objektes keine Veränderung der Canvas-Zeichenoberfläche mit einschließt, beide sind unabhängig voneinander.

putImageData

Die putImageData(imgData, x, y)-Methode zeichnet das ImageData-Objekt in seiner vollen Größe an der angegebenen Position auf die Canvas. Bereiche, die außerhalb der Canvas liegen, werden dabei nicht gezeichnet.

putImageData(imgData, x, y, dx, dy, dwidth, dheight) zeichnet das ImageData-Objekt an die Position (x | y), wobei jedoch nur der Teil sichtbar ist, der sich innerhalb des ImageData-Objekts in demjenigen Rechteck befindet, das durch die letzten vier Parameter aufgespannt wird. Berücksichtigen Sie, dass der innerhalb des Rechtecks liegende Teil des Bildes tatsächlich an der Position (x + dx | y + dy) erscheint, nicht an (x | y).

Anwendungsbeispiele

Um den eigentlichen Inhalt eines ImageData-Objekts zu verändern, stehen die Membervariablen width, height und data zur Verfügung. Letztere speichert die Farbe jedes Punktes mithilfe eines Arrays. Dabei sind die Punkte von oben beginnend jeweils zeilenweise, innerhalb einer Zeile von links nach rechts, im Array abgelegt. Dies bedeutet, dass innerhalb des Arrays zuerst die Pixel von oben links nach oben rechts folgen, danach schließen sich die der zweiten, dritten, usw. Zeile an. Außerdem wird jedes Pixel durch vier Array-Elemente repräsentiert, ihre Werte sind ganzzahlig und liegen zwischen 0 und 255. Sie stellen den Rot-, Grün-, Blau- und Alphawert (in dieser Reihenfolge) dar.

Beispiel ansehen …
function drawCanvas()
{
  var element = document.getElementById('canvas');
  
  if(element.getContext)
  {
    var context = element.getContext('2d'),
        imgData;
    
    context.fillStyle = 'yellow';
    context.fillRect(0, 0, element.width, element.height);
    imgData = context.getImageData(10, 10, element.width - 20, element.height - 20);
    for(var y = 0; y < imgData.height; y++)
    {
      for(var x = 0; x < imgData.width; x++)
      {
        imgData.data[4 * (y * imgData.width + x)] = 255; // Rotwert
        imgData.data[4 * (y * imgData.width + x) + 1] = 0; // Grünwert
        imgData.data[4 * (y * imgData.width + x) + 2] = 0; // Blauwert
        imgData.data[4 * (y * imgData.width + x) + 3] = 255; // Alphawert
      }
    }
    context.putImageData(imgData, 20, 20);
  }
}
In diesem Beispiel wird auf die Canvas ein rotes Rechteck eingehüllt in einem gelben Rahmen gezeichnet.

Hierzu wird zuerst, nachdem wie üblich das context-Objekt ermittelt wurde, das gelbe Rechteck mithilfe des Funktionsaufrufs context.fillRect(0, 0, canvas.width, canvas.height) über die gesamte Fläche der Canvas mit der vorher eingestellten gelben Farbe gezeichnet.
Danach wird mithilfe von context.getImageData(10, 10, element.width - 20, element.height - 20) ein imageData-Objekt erzeugt, das den rot auszufüllenden Bereich der gerade eingefärbten Canvas wiederspiegelt. Von diesem Objekt werden dann mithilfe zweier for-Schleifen die einzelnen Pixel angesprochen, hierzu werden seine Membervariablen width und height verwendet. Innerhalb der for-Schleifen werden die x- und y-Koordinaten der umzufärbenden Pixel mithilfe der Formel 4 * (y * imgData.width + x) (bzw. ihrer Variationen für Grün-, Blau- und Alphawert) in die Position des entsprechenden Farbwertes des gewünschten Pixels innerhalb des data-Arrays umgerechnet. Daraufhin folgt die Einfärbung dieser Farbwerte in der Farbe rgb(255 0 0), also rot. Die vorgestellte Formel kann dabei immer dann verwendet werden, wenn die x- und y-Koordinaten eines Pixels in dessen Position innerhalb des data-Arrays (bzw. in die Position eines seiner Farbwerte) umgerechnet werden soll.

Schließlich wird das so veränderte ImageData-Objekt mithilfe von context.putImageData(imgData, 10, 10) auf die Canvas übertragen. Dies ist nötig, da das Verändern des ImageData-Objekts nicht gleichzeitig auch die Canvas verändert.

Bildmanipulation

Eine praktischere Anwendung ist das Verfremden von bestehenden Rastergrafiken. Sie können dies entweder mithilfe von SVG-Filtern oder über CSS-Filter erreichen, aber die Bildwerte auch direkt in JavaScript umrechnen und auf dem canvas ausgeben:

Screenshot verschiedener Pixelmanipulationen

Beispiel ansehen …
// Bildfläche einrichten
canvas.height = img.height;
canvas.width = img.width;

context = canvas.getContext("2d");

// Originalbild zeichnen
context.drawImage(img, 0, 0, img.width, img.height);

// originale Bilddaten speichern
imgData = context.getImageData(0, 0, img.width, img.height);

// reduziere auf Ganzzahl zwischen 0 und 255
function byteRange (a) {

  if (a > 255) {
	a = 255;
  }

  if (a < 0) {
	a = 0;
  }

  return Math.floor(a);
}

// Bildmanipulation ausführen
function applyFilter () {
  var data, mod, x, y, r, g, b, a, l, offset, delta, n;

  // Bildüberschrift anpassen
  cap.innerHTML = sel.options[sel.selectedIndex].value;

  // neue Bilddaten anlegen
  mod = context.createImageData(img.width, img.height);

  // Bilddaten pixelweise abarbeiten
  for (x = 0; x < imgData.width; x++) {

	for (y = 0; y < imgData.height; y++) {

	  offset = (imgData.width * y + x) * 4;
	  r = imgData.data[offset];   // rot
	  g = imgData.data[offset + 1]; // grün
	  b = imgData.data[offset + 2]; // blau
	  a = imgData.data[offset + 3]; // Transparenz
	  l = 0.299*r + 0.587*g + 0.114*b; // (NTSC-Standard für Luminanz)

	  // jeweiligen Filter anwenden
	  switch (sel.options[sel.selectedIndex].value) {

		default:
		  mod.data[offset]     = r;
		  mod.data[offset + 1] = g;
		  mod.data[offset + 2] = b;
		  mod.data[offset + 3] = a;
		break;

		case "Verrauscht":
		  mod.data[offset]     = byteRange(r*.8 + 150*Math.random());
		  mod.data[offset + 1] = byteRange(g*.8 + 150*Math.random());
		  mod.data[offset + 2] = byteRange(b*.8 + 150*Math.random());
		  mod.data[offset + 3] = a;
		break;

		case "Schwarzweiß":
		  mod.data[offset]     = byteRange(l);
		  mod.data[offset + 1] = byteRange(l);
		  mod.data[offset + 2] = byteRange(l);
		  mod.data[offset + 3] = byteRange(a);
		break;

		case "Negativ":
		  mod.data[offset]     = byteRange(255 - r);
		  mod.data[offset + 1] = byteRange(255 - g);
		  mod.data[offset + 2] = byteRange(255 - b);
		  mod.data[offset + 3] = byteRange(a);
		break;

		case "Differenziert":
		  // 0 = r, 1 = g, 2=  b
		  [0, 1, 2].forEach(function (rgb) {

			// 2*(rgb-rechts - rgb-links + rgb-oben - rgb-unten) +128
			mod.data[offset + rgb] = byteRange(
			  2 * (
				// rgb-rechts
				imgData.data[(imgData.width * (y-1) + x) * 4]
				// rgb-links
				- imgData.data[(imgData.width * (y+1) + x) * 4]
				// rgb-oben
				+ imgData.data[(imgData.width * y + x-1) * 4]
				// rgb-unten
				- imgData.data[(imgData.width * y + x+1) * 4]
			  ) + 128
			);
		  });

		  // Transparenz
		  mod.data[offset + 3] = imgData.data[offset + 3];
		break;

		case "Rot":
		  mod.data[offset]     = r;
		  mod.data[offset + 1] = 0;
		  mod.data[offset + 2] = 0;
		  mod.data[offset + 3] = a;
		break;

		case "Grün":
		  mod.data[offset]     = 0;
		  mod.data[offset + 1] = g;
		  mod.data[offset + 2] = 0;
		  mod.data[offset + 3] = a;
		break;

		case "Blau":
		  mod.data[offset]     = 0;
		  mod.data[offset + 1] = 0;
		  mod.data[offset + 2] = b;
		  mod.data[offset + 3] = a;
		break;

		case "Pixelig":
		  // Pixel in Gruppen von n*n behandeln und
		  // jedem Pixel den durchschnittlichen RGBa-Wert
		  // dieser Gruppe geben:
		  n = 5;

		  delta = {
			// Zähler für die tatsächliche Anzahl der Pixel im n*n-Quadrat
			c: 0,
			// Abstand zur linken oberen Ecke des n*n-Quadrates
			dx: 0,
			dy: 0,
			// RGBa-Werte
			r: 0,
			g: 0,
			b: 0,
			a: 0,
			// Offset in imgData für originalen RGB-Wert
			o: 0,
			// X-Koordinate der linken oberen Ecke des n*n-Quadrates
			x: Math.floor(x / n) * n,
			// Y-Koordinate der linken oberen Ecke des n*n-Quadrates
			y: Math.floor(y / n) * n
		  };

		  while (delta.dy < n) {

			while (delta.dx < n) {

			  // RGB-Werte dieses Pixels aufaddieren
			  if (delta.x + delta.dx < imgData.width
				&& delta.y + delta.dy < imgData.height
			  ) {

				// Offset eines Pixels im n*n-Raster
				delta.o = (
				  imgData.width * (delta.y + delta.dy)
				  + delta.dx
				  + delta.x
				) * 4;

				delta.c++;

				delta.r += imgData.data[delta.o];
				delta.g += imgData.data[delta.o + 1];
				delta.b += imgData.data[delta.o + 2];
				delta.a += imgData.data[delta.o + 3];
			  }

			  delta.dx++;
			}

			delta.dx = 0;
			delta.dy++;
		  }

		  mod.data[offset]     = byteRange(delta.r / delta.c);
		  mod.data[offset + 1] = byteRange(delta.g / delta.c);
		  mod.data[offset + 2] = byteRange(delta.b / delta.c);
		  mod.data[offset + 3] = byteRange(delta.a / delta.c);
		break;
	  }
	}
  }

  // veränderte Bilddaten ins Bild schreiben
  context.putImageData(mod, 0, 0);
}

In diesem Beispiel wird das Bild durch mehrere Arten der Pixelmanipulation verändert dargestellt, indem die Farbwerte (RGBa) eines jeden Pixels mit JavaScript neu berechnet werden.

Canvas-Bild als Rastergrafik speichern

Die toDataURL()-Methode gibt das im Canvas erzeugte Bild als Data-URL in einer Auflösung von 96 dpi zurück.

  • canvas.toDataURL('image/png'): Standardwert, erzeugt ein png-Bild
  • canvas.toDataURL('image/jpeg', quality): erzeugt ein jpg-Bild, ein optionaler Parameter regelt die unterschiedliche Komprimierung, 1 ist die größte Qualität, 0 die kleinste Datengröße.
Beispiel
// wandelt canvas in eine Rastergrafik um
function convertCanvasToImage(canvas, callback) {
  var image = new Image();
  image.onload = function(){
    callback(image);
  }
  image.src = canvas.toDataURL("image/png");
}

Das so erzeugte Bild kann entweder in der Webseite referenziert oder mittels eines Download-Links heruntergeladen werden.

Ein Anwendungsfall könnte ein Bild-Upload sein, bei dem zu große Bilder bereits clientseitig komprimiert würden.

Siehe auch:

canvas.toBlob

Die HTMLCanvasElement.toBlob()-Methode erzeugt aus dem im canvas gezeichneten Bild ein Blob-Objekt, das auf der Festplatte oder im Cache des Benutzers gespeichert werden kann.

void canvas.toBlob(callback, mimeType, qualityArgument);

Folgende Angaben sind möglich:

  • callback: Rückgabefunktion
  • mimeType: optionale Angabe des MIME-Types, Standardwert ist png
  • qualityArgument: optionale Angabe der Kompression (0-1)


Weblinks