JavaScript/Canvas/Pixel Manipulation
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:
Inhaltsverzeichnis
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.
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);
}
}
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:
// 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-Bildcanvas.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.
// 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:
- MDN: Saving images
- David Walsh: JavaScript Canvas Image Conversion
- Chris Heilmannn: Canvas and Images with Pixels
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ückgabefunktionmimeType
: optionale Angabe des MIME-Types, Standardwert ist pngqualityArgument
: optionale Angabe der Kompression (0-1)
Weblinks
- W3C: Pixel manipulation
- WHATWG: canvas-Element
- WHATWG: Pixel manipulation
Hierzu wird zuerst, nachdem wie üblich das context-Objekt ermittelt wurde, das gelbe Rechteck mithilfe des Funktionsaufrufs
Schließlich wird das so veränderte ImageData-Objekt mithilfe voncontext.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 Membervariablenwidth
undheight
verwendet. Innerhalb der for-Schleifen werden die x- und y-Koordinaten der umzufärbenden Pixel mithilfe der Formel4 * (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 desdata
-Arrays umgerechnet. Daraufhin folgt die Einfärbung dieser Farbwerte in der Farbergb(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 desdata
-Arrays (bzw. in die Position eines seiner Farbwerte) umgerechnet werden soll.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.