Zur Übersicht

Flash und Bitmaps

Markus Fink
Markus Fink Aktualisiert am 17. Aug. 2020
flash-bitmaps

Mittels BitmapFilter lassen sich wunderbare Effekte erstellen, die man auf Bitmaps anwenden kann; wenn nötig auch auf DisplayObjects, bei denen die Eigenschaft cacheAsBitmap auf true gesetzt ist. Man kann auch ohne Weiteres eigene "Filter/Effekte" erzeugen, indem man zum Beispiel den ColorMatrixFilter verwendet.

In diesem Blogeintrag werden wir uns eine kleine Bibliothek erstellen, die uns die Handhabung der Bitmaps für die Zukunft erleichtert. Was soll unsere Bibliothek können? In erster Linie nur grundlegende Dinge:

  • Rotation
  • Skalierung
  • Platzierung
  • und diverse Filter

Ein leidiges Thema in Flash sind die "Registrierpunkte" von Objekten: Man kann diese nicht einfach in die Mitte setzen, um eine Rotation um den Mittelpunkt des Objektes zu erreichen. Eine gängige Variante ist, das Objekt versetzt in einen Container zu setzen und die Rotation über diesen Container zu machen. Dieselbe Problematik gibt es auch bei der Skalierung. Unsere Bibliothek soll uns diese Problematik abnehmen, in dem sie einen virtuellen Punkt hält, um den man skaliert bzw. rotiert.

Wir starten mit dem erstellen eines Testprojekts und der Bibliothek.
Unser erster Schritt: Erstellen eines Testprojektes, das eine Bitmap lädt und auf der Bühne plaziert. Dazu erstellen wir uns einen Projektordner "Testprojekt" und speichern darin die Flash-Datei als "test.fla" ab. Die flash-Datei hat als Basisklasse die Klasse "test" in den Eigenschaften eingestellt. Diese öffnen wir und fügen folgenden Code ein:

package  {
	
	import flash.display.MovieClip;
	import flash.display.Loader;
	import flash.display.Bitmap;
	
	import flash.net.URLRequest;
	
	import flash.events.Event;
	
	public class test extends MovieClip {
		
		private var imageLoader:Loader;
		
		public function test() {
			trace('test->__constructor()');
			imageLoader = new Loader();
			imageLoader.contentLoaderInfo.addEventListener(Event.COMPLETE, _imageLoaded);
			imageLoader.load(new URLRequest('media/path/to/the/image.jpg'));
		}
		
		private function _imageLoaded(e:Event):void {
			trace('test->_imageLoaded()');
			addChild(imageLoader);
		}
	}
}

Nun erweitern wir unsere Bibliothek, um uns das Handling der Bitmaps zu erleichtern. Dazu erstellen wir eine neue AS3-Datei und legen diese entsprechend ab. In meinem Fall: Testprojekt/com/i6w9/BitmapTransform.a

package com.i6w9 {
	
	public class BitmapTransform {
		public function BitmapTransform():void {
		
		}
	}
}
Diese Klasse importieren wir noch in unseren test.as; das sieht dann folgendermaßen aus:
import flash.display.MovieClip;
import flash.display.Loader;
import flash.display.Bitmap;

import flash.net.URLRequest;

import flash.events.Event;

import com.i6w9.BitmapTransform; // our Library

Nun geht es wieder zurück in unsere BitmapTransform.as. Wir erledigen das Laden der Bitmap und das Setzen des virtuellen Punktes. Dazu erweitern wir die Bibliothek wie folgt:

package com.i6w9 {

import flash.display.Bitmap;

import flash.geom.Point;

public class BitmapTransform {

private var bmSource:Bitmap;
private var centerPoint:Point;
private var _x:Number
private var _y:Number

public function BitmapTransform(bmSource:Bitmap = null):void {
if(bmSource == null)
return
load(bmSource);
}

public function load(bmSource:Bitmap):void {
this.bmSource = bmSource;
this.centerPoint = new Point(this.bmSource.width / 2, this.bmSource.height / 2);
this._x = 0;
this._y = 0;
}

public function getBitmap():Bitmap {
return bmSource;
}
}
}

Wir können unsere Bibliothek schon in unserem Testprojekt benutzen; wir machen das, indem wir wieder entsprechend umwandeln:

private function _imageLoaded(e:Event):void {
trace('test->_imageLoaded()');
addChild(imageLoader);
}
in

private function _imageLoaded(e:Event):void {
trace('test->_imageLoaded()');
var loadedBitmap:Bitmap = Bitmap(imageLoader.content);
var foobar:BitmapTransformer = new BitmapTransformer(loadedBitmap);
var myBitmap:Bitmap = foobar.getBitmap();
addChild(myBitmap);
}

Bisher verursachen wir mehr Aufwand als Aufwand zu verringern. NOCH ;-) Wir erstellen im nächsten Schritt die Methode, damit die Bitmaps rotieren.

public function rotate(numAngleDegrees:Number, blnReset:Boolean = false):void {
var matrix = bmSource.transform.matrix;
if(blnReset) matrix.identity();
matrix.translate(centerPoint.x * -1, centerPoint.y * -1);
matrix.rotate(numAngleDegrees * (Math.PI / 180)); 
matrix.translate(centerPoint.x, centerPoint.y);
bmSource.transform.matrix = matrix;
}

Hier verrücken wir das Objekt um den Centerpoint, rotieren und rücken mit dem Objekt zurück, damit das Objekt um dessen Mitte gedreht wird und nicht um den "Flash-internen-Registrierpunkt" (top,left). Der zweite Parameter würde die TransformMatrix wieder zurücksetzen (IdentityMatrix). Somit haben folgende Snippets dieselbe Auswirkung:

foobar.rotate(180);
foobar.rotate(-45);
//
foobar.rotate(180);
foobar.rotate(135, true);
//
foobar.rotate(135);

Nach demselben Prinzip erstellen wir uns noch die scale-Methode:


public function scale(numScaleX:Number, numScaleY:Number, blnReset:Boolean = false):void {
var matrix = bmSource.transform.matrix;
if(blnReset) matrix.identity();
matrix.translate(centerPoint.x * -1, centerPoint.y * -1);
matrix.scale(numScaleX, numScaleY); 
matrix.translate(centerPoint.x, centerPoint.y);
bmSource.transform.matrix = matrix;    
}

Snippets:

foobar.scale(4, 4);
foobar.scale(.5, .5);
//
foobar.scale(4, 4);
foobar.scale(2, 2, true);

Die Rotation bzw. Skalierung funktioniert aktuell nur, solange man die Bitmap nicht verschiebt. Wenn wir jetzt einfach nur die Position der Bitmap ändern, dann ist die nächste Drehung nicht mehr korrekt und unser virtueller Punkt liegt noch an der alten Stelle. Deshalb erstellen wir uns noch eine Methode, mit der wir unsere Bitmaps platzieren können ohne unseren virtuellen Punkt zu verlieren:

public function moveTo(numX:Number, numY:Number):void {
    var numDiffX:Number = x - _x;
    var numDiffY:Number = y - _y;
    _x += numDiffX;
    centerPoint.x += numDiffX;
    bmSource.x += numDiffX;
    _y += numDiffY;
    centerPoint.y += numDiffY;
    bmSource.y += numDiffY;
}

Snippets:

foobar.moveTo(50, 150);
foobar.rotate(45);

Nun verschiebt sich die Bitmap auf x = 50 und y = 150. Der virtuelle Punkt wird mitverschoben und die Klasseninternen _x und _y-Werte wurden aktualisiert. Was bei Flash auffällt, das ist der "Pixeleffekt", den die Bitmaps bei einer Rotation oder einer Skalierung erhalten. Glücklicherweise bietet Flash eine öffentliche Eigenschaft im Bitmap-Objekt an, mit der wir das Verhalten der Bitmap ändern können: Die Eigenschaft "smoothing". Man könnte diese Eigenschaft natürlich direkt auf die Bitmap setzten; wir benutzen dennoch unsere Klasse, damit alles an einem Platz ist:

public function setSmoothing(blnSmoothing:Boolean):void {
bmSource.smoothing = blnSmoothing;
} 

Snippets:

foobar.moveTo(50, 150);
foobar.rotate(45);
foobar.setSmoothing(true);

Das ist schön und gut. Nun wollen wir uns aber den "Filtern" widmen und uns einige Effekte fertigen. Ein wirklich einfacher Filter ist der Graustufen-Filter, den man mittels eines ColorMatrixFilter umsetzen kann. Wir erweitern nun unsere Klasse um folgende Eigenschaft:

private var filter:Object;

Wir erweitern die Klasse um 4 (noch leere) Methoden:

public function addFilter(strFilter:String, objParameter:Object = null):Boolean {
return true;
}
public function removeFilter(strFilter:String):Boolean {
return true;
}
public function hasFilter(strFilter:String):Boolean {
return true;
}
private function _appendFilters():void {

}

Und wir erweitern die load-Methode auf:

public function load(bmSource:Bitmap):void {
this.bmSource = bmSource;
this.centerPoint = new Point(this.bmSource.width / 2, this.bmSource.height / 2);
this._x = 0;
this._y = 0;
this.filter = {
  'greyscale': null
};
}

Die addFilter-Methode soll verwendet werden, um einen Filter zu erstellen und in unser filter-Objekt zu werfen. Um dies zu realisieren, prüfen wir, ob die Klasse eine Methode namens "_set" + Filtername hat. Wenn ja, dann wird diese aufgerufen und deren Rückgabewert in unser Filterobjekt geworfen und anschliessend die _appendFilters-Methode aufgerufen.

public function addFilter(strFilter:String, objParameter:Object = null):Boolean {
if(typeof this['_set' + strFilter.charAt(0).toUpperCase() + strFilter.substr(1, strFilter.length)] == 'function') {
filter[strFilter] = this['_set' + strFilter.charAt(0).toUpperCase() + strFilter.substr(1, strFilter.length)](objParameter);
_appendFilters();
return true;
}
return false;
}

Dies soll uns das Laden eines Filters über die addFilter-Methode ermöglichen. Die Verwendung würde wie folgt aussehen:

foobar.moveTo(50, 150);
foobar.rotate(45);
foobar.addFilter('grayscale');

Dieses Aufruf resultiert aktuell noch in einen Referenzerror, da die Methode _setGrayscale nicht gefunden wurde. Diese Methode wird folgendermaßen erstellt:

// das package benötigt noch folgende Importe:
import flash.filters.BitmapFilter;
import flash.filters.ColorMatrixFilter;

private function _setGreyscale(objParameter:Object):BitmapFilter {
var arrMatrix:Array = new Array(.5, .5, .5, 0, 0,
  .5, .5, .5, 0, 0,
  .5, .5, .5, 0, 0,
  0, 0, 0, 1, 0);
var greyscaleFilter:BitmapFilter = new ColorMatrixFilter(arrMatrix);
return greyscaleFilter;
}

Hier wird eine einfache 5er-Matrix erstellt und die RGB-Werte werden halbiert, das ist eine typische Graustufen-Matrix. Der Aufruf von .addFilter('grayscale'); wirft nun keine Fehler mehr auf, es hat aber auch noch keinen Effekt auf unsere Bitmap. Dazu müssen wir noch die Methode _appendFilters() erweitern:

private function _appendFilters():void {
var arrFilters = new Array();
for(var strIndex in filter) {
if(filter[strIndex] != null) arrFilters.push(filter[strIndex]);
}
bmSource.filters = arrFilters;
}

Nun funktioniert das folgende Snippet auch schon:

foobar.moveTo(50, 150);
foobar.rotate(45);
foobar.addFilter('grayscale');

Nun noch die kleine Methoden removeFilter() und hasFilter:

public function removeFilter(strFilter:String):Boolean {
if(filter[strFilter] != undefined) filter[strFilter] = null;
_appendFilters();
return true;
}

public function hasFilter(strFilter:String):Boolean {
return (filter[strFilter] != undefined && filter[strFilter] != null) ? true : false;
}

Snippet:

foobar.moveTo(50, 150);
foobar.rotate(45);
foobar.addFilter('grayscale');
trace(foobar.hasFilter('greyscale'));
trace(foobar.hasFilter('foobar'));
foobar.removeFilter('greyscale');
trace(foobar.hasFilter('greyscale'));

Dies hätte folgende Ausgabe:

true
false
false

Nun da die Klasse die Handhabung der Filter schon integriert hat, können wir uns nun neuen Filtern zuwenden. Der Graustufen-Filter ist sehr simpel gestrickt und braucht auch keine Parameter. Dies ist auch der Grund, wieso die Methode _setGrayscale zwar ein Objekt als Parameter erwartet, aber es nicht verwendet. Unser nächster Filter, ein Weichzeichner, soll aber zwei Parameter verarbeiten, um die Stärke des Effekts zu definieren.

//imports für das package
import flash.filters.BlurFilter;
import flash.filters.BitmapFilterQuality;

// erweitern des filter-Objekts in der load-Methode
this.filter = {
'greyscale': null,
'blur': null
}

private function _setBlur(objParameter:Object):BitmapFilter {
return new BlurFilter(objParameter.blurX, objParameter.blurY, BitmapFilterQuality.HIGH);
}
Snippet:

foobar.addFilter('blur', {blurX:5, blurY:10});

Im nächsten Schritt erstellen wir uns einen "Scharfzeichner". Es gibt leider keinen von Flash vorgefertigten Scharfzeichner wie es zum Beispiel beim Weichzeichner der Fall ist. Es gibt aber zwei gängige Methoden, den Filter so umzusetzen. Einmal mittels eines ConvolutionFilters und einmal als Binär-Datei in Verbindung mit dem ShaderFilter. Wir verwenden für diesen Blogeintrag die Methode mit dem ConvolutionFilter.

//imports
import flash.filters.ConvolutionFilter;

// erweitern des filter-Objekts in der load-Methode
this.filter = {
'greyscale': null,
'blur': null,
'sharpen': null
}

private function _setSharpen(objParameter:Object):ConvolutionFilter {
var convolutionFilter:ConvolutionFilter = new ConvolutionFilter();
convolutionFilter.matrixX = 3;
convolutionFilter.matrixY = 3;
convolutionFilter.matrix = new Array(0, -1, 0, -1, 5, -1, 0, -1, 0);
convolutionFilter.divisor = 1;
return convolutionFilter;
}

Snippet:

foobar.addFilter('sharpen');

Der Nachteil dieses Scharfzeichners ist jener, dass wir keinen Parameter übergeben können, der die Stärke des Effekts definiert. Ein Vorteil wiederum ist jener, dass er ohne das Laden einer Binär-Datei auskommt und dadurch um einiges schneller wird! Das Anlegen neuer Filter ist also keinerlei Aufwand und die Handhabung ist wirklich einfach gehalten. Flash bietet noch viele andere Filter an, die ohne viel Aufwand integriert werden können. Zum Abschluss fügen wir noch einen simplen Filter hinzu um die Sättigung eines Bildes zu bearbeiten:

// erweitern des filter-Objekts in der load-Methode
this.filter = {
'greyscale': null,
'blur': null,
'sharpen': null,
'saturation': null
}

private function _setSaturation(objParameter:Object):BitmapFilter {
var saturationMatrix = new Array(objParameter.saturation,0,0,0,0,0,objParameter.saturation,0,0,0,0,0,objParameter.saturation,0,0,0,0,0,1,0);
return new ColorMatrixFilter(saturationMatrix);
}

Snippet:

foobar.addFilter('saturation', {saturation:1.7});

In einem weiterem Schritt könnte man die Filter noch in eigene Klassen auslagern, um dadurch die Filter noch effektiver zu nutzen. Dadurch könnte man auf bestehende Filtereigenschaften zugreifen und / oder bestehende Filter verhändern.

Wie immer sind alle herzlichst eingeladen in den Kommentaren Fragen zu stellen oder Anmerkungen zu teilen. Die Beispielklasse, die für diesen Blogeintrag erstellt wurde, ist hier zu finden:

paste2.org

Markus Fink
Markus Fink
Web Developer