terça-feira, 24 de maio de 2011

An easier BitmapData.draw

After lots of struggling with ActionScript's BitmapData "draw" function, with trying to render a tileset using a zoom factor (by applying a scale, and selecting the area inside a larger tileset image to draw from), I decided to create a couple of helper functions, so that I never have to figure out again what the variables do in the "draw" call.

Here, I present to you a couple of functions. The first, "DrawImageAreaScaled", basically draws an upscaled rectangular sub-area of a picture wherever on another image (or on the screen). The second, "DrawImageScaledAndRotated", draws a rotated and scaled picture on the screen.
Transparency is respected (if the image contains alpha information, transparent pixels are drawn correctly).

This can easily be applied to, say, Flixel, by passing as the first parameter "FlxG.buffer" as the destination image, and "your_FlxSprite.pixels" as source (see example below).

Below is the class, and under it, an example on how to use it using Flixel.

P.S.: don't expect too great a performance, since BitmapData.draw is a somewhat slow function, specially when called several times a second to display, say, a tileset.

Class "Render.as" in folder "Utils":

package Utils
{
import flash.display.BitmapData;
import flash.geom.Matrix;
import flash.geom.Point;
import flash.geom.Rectangle;

/**
* Contains helper Renderer functions.
* @author PJMendes aka TurboLento
*/
public class Render
{
// helper variables - initialized once, reused at every function called
private static var DrawClipRect:Rectangle = new Rectangle(0, 0, 0, 0); // area on screen to draw (sort of a mask)
private static var DrawMatrix:Matrix = new Matrix();

/**
* Helper for a simpler BitmapData.draw. Draws a rectangular area of an image on the screen, using syntax similar to copyPixels.
* Useful for drawing scaled tiles (performance isn't great though, due to "draw" being slow).
* @param imgSource The image to draw from.
* @param imgDestination The image to draw to
* @param areaSource The area in the source image to draw from.
* @param positionDestination The x,y position on the destination to draw to.
* @param scale The scaling to apply to the drawn image (areaSource will be multiplied by this dimension in the final drawing).
*/
public static function DrawImageAreaScaled(imgSource:BitmapData, imgDestination:BitmapData, areaSource:Rectangle, positionDestination:Point, scale:Point = null, rotationRadians:Number = 0): void
{
DrawMatrix.identity();
DrawMatrix.rotate(rotationRadians);

if (scale != null)
{
DrawMatrix.scale(scale.x, scale.y);

DrawClipRect.width = areaSource.width * scale.x; // i think this is it
DrawClipRect.height = areaSource.height * scale.y;

DrawClipRect.x = positionDestination.x; // i think this is it - making clipRect sort of a "window" on the destination where source will appear
DrawClipRect.y = positionDestination.y;

DrawMatrix.translate(positionDestination.x - areaSource.x * scale.x, positionDestination.y - areaSource.y * scale.y);
}
else
{
DrawClipRect.width = areaSource.width; // i think this is it
DrawClipRect.height = areaSource.height;

DrawClipRect.x = positionDestination.x; // i think this is it - making clipRect sort of a "window" on the destination where source will appear
DrawClipRect.y = positionDestination.y;

DrawMatrix.translate(positionDestination.x - areaSource.x, positionDestination.y - areaSource.y);
}

imgDestination.draw(imgSource, DrawMatrix, null, null, DrawClipRect);
}

/**
* Helper for a simpler BitmapData.draw. Draws an image on the screen, scaled and rotated, using syntax similar to copyPixels.
* Useful for drawing scaled tiles (performance isn't great though, due to "draw" being slow).
* @param imgSource The image to draw from.
* @param imgDestination The image to draw to
* @param positionDestination The x,y position on the destination to draw to.
* @param scale The scaling to apply to the drawn image (areaSource will be multiplied by this dimension in the final drawing).
* @param rotationRadians The rotation in radians to apply to the drawn image. Axis for rotation is the top left of the source image.
*/
public static function DrawImageScaledAndRotated(imgSource:BitmapData, imgDestination:BitmapData, positionDestination:Point, scale:Point = null, rotationRadians:Number = 0): void
{
DrawMatrix.identity();

DrawMatrix.rotate(rotationRadians);

if (scale != null)
{
DrawMatrix.scale(scale.x, scale.y);

DrawMatrix.translate(positionDestination.x, positionDestination.y);
}
else
{
DrawMatrix.translate(positionDestination.x, positionDestination.y);
}

imgDestination.draw(imgSource, DrawMatrix, null, null, null);
}
}
}


Example class:

package
{
import flash.display.BitmapData;
import flash.geom.Matrix;
import flash.geom.Point;
import flash.geom.Rectangle;
import org.flixel.FlxSprite;
import org.flixel.FlxState;
import org.flixel.FlxG;
import Utils.Render;

/**
* ...
* @author
*/
public class TestState extends FlxState
{
[Embed(source = "/../data/test.png")] protected var ImgTileset:Class;
[Embed(source = "/../data/player_sprite.png")] protected var ImgPlayer:Class;

private var img:FlxSprite;
private var img2:FlxSprite;

override public function create():void
{
super.create();
img = new FlxSprite(0, 0, ImgTileset);
img2 = new FlxSprite(0, 0, ImgPlayer);
}


override public function update():void
{
//trace("update");
super.update();
}

override public function render():void
{
//trace("render; DataManagerHasLoaded "+ DataManagerHasLoaded +" TilemapHasLoaded "+ TilemapHasLoaded);


// render method 1
var srcRect:Rectangle = new Rectangle(0, 0, img.width, img.height);
var destPoint:Point = new Point(320, 0);
FlxG.buffer.copyPixels(img.pixels, srcRect, destPoint);


// render method 2
var scale:Point = null;
var destPoint:Point = null;
var rectSource:Rectangle = null;

//rectSource = new Rectangle(0, 0, img.width, img.height);
//rectSource = new Rectangle(0, 0, 32, 32);

rectSource = new Rectangle(64, 32, 32, 32);
destPoint = new Point(FlxG.mouse.x, FlxG.mouse.y);

scale = new Point(2, 2);

Render.DrawImageAreaScaled(img.pixels, FlxG.buffer, rectSource, destPoint, scale);
//Render.DrawImageScaledAndRotated(img.pixels, FlxG.buffer, destPoint, scale, 0.2);


// test with img with transparency
rectSource = new Rectangle(0, 0, img2.width, img2.height);
//Render.DrawImageAreaScaled(img2.pixels, FlxG.buffer, rectSource, destPoint, scale);
Render.DrawImageScaledAndRotated(img2.pixels, FlxG.buffer, destPoint, scale, 0.2);

super.render();
}
}
}


Hope this is helpful!