Archive for February, 2009

One of the problems I experienced while creating generative art with Flash, was that I couldn’t create BitmapData bigger then 2048 by 2048 pixels. That is in Flash 9 offcourse. In Flash 10 it is now possible to create bitmapdata instances of 4095 by 4095 pixels… but still… what if you want to go beyond the limits of Flash?
So, that’s why I wrote a class called “ChunckedBitmapRenderer”. This class allows you to render any Sprite, no matter its size, to an array of BitmapData instances. I also wrote a PNG encoder to be able to export this to a PNG (so no photoshop is required to stitch the chuncks together):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
//copyright 2009 Ward De Langhe
package
{
    import flash.display.BitmapData;
    import flash.display.BlendMode;
    import flash.display.Sprite;
    import flash.filesystem.File;
    import flash.filesystem.FileMode;
    import flash.filesystem.FileStream;
    import flash.geom.Matrix;
    import flash.geom.Rectangle;
    import flash.utils.ByteArray;
   
    public class ChunckedBitmapRenderer
    {
        private var bitmapArray:Array;
        private var _chunckSize:Number;
       
        public function ChunckedBitmapRenderer(width:Number,height:Number,transparent:Boolean=false,fillColor:Number=0x000000,cSize:Number=2048)
        {
            _chunckSize=cSize;
            bitmapArray=new Array();
            var numHorizontalTiles:Number=Math.ceil(width/_chunckSize)
            var numVerticalTiles:Number=Math.ceil(height/_chunckSize);
            var widthCounter:Number=0;
            var heightCounter:Number=0;
            for(var x:Number=0;x<numHorizontalTiles;x++){
                bitmapArray.push(new Array());
                var tileWidth:Number;
                tileWidth=width-widthCounter;
                if(tileWidth>_chunckSize)
                    tileWidth=_chunckSize;
                widthCounter+=tileWidth;
                for(var y:Number=0;y<numVerticalTiles;y++){
                    var tileHeight:Number;
                    tileHeight=height-heightCounter;
                    if(tileHeight>_chunckSize)
                        tileHeight=_chunckSize;
                    heightCounter+=tileHeight;
                    var bm:BitmapData=new BitmapData(tileWidth,tileHeight,transparent,fillColor);
                    bitmapArray[x].push(bm);
                }
                heightCounter=0;
            }
        }
        public function get chunkSize():Number{
            return _chunckSize;
        }
        public function render(source:Sprite,blendMode:String=BlendMode.NORMAL):void{
            for(var x:Number=0;x<bitmapArray.length;x++){
                for(var y:Number=0;y<bitmapArray[x].length;y++){
                    var bm:BitmapData=bitmapArray[x][y] as BitmapData;
                    var region:Rectangle=new Rectangle(x*_chunckSize,y*_chunckSize,bm.width,bm.height);
                    var m:Matrix=new Matrix(1,0,0,1, 0-region.x, 0-region.y);
                    source.transform.matrix = m;
                    bm.draw(source,m,null,blendMode,null,true);
                    m=new Matrix(1,0,0,1, region.x, region.y);
                    source.transform.matrix = m;
                }
            }
        }
        public function getPNG():ByteArray{
            return ChunckedBitmapToPNGEncoder.encodeChunckedBitmap(bitmapArray,chunkSize);
        }

    }
}

Offcourse this code is useless if you don’t have a way to encode this data into a .png or a .jpg file. So that’s why I took Tinic Uro’s PNG encoder class, and changed it a bit to be able to encode an array of bitmaps, provided by the ChunckedBitmapRenderer class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
package
{
import flash.display.*;
import flash.geom.*;
import flash.utils.ByteArray;

public class ChunckedBitmapToPNGEncoder {
   
    public static function encodeChunckedBitmap(bmArray:Array,chunckSize:Number=2048):ByteArray{
        var imgWidth:Number=0;
        var imgHeight:Number=0;
        for(var x:Number=0;x<bmArray.length;x++){
            imgWidth+=(bmArray[x][0] as BitmapData).width;
            if(x==0){
                for(var y:Number=0;y<bmArray[x].length;y++){
                    imgHeight+=(bmArray[x][y] as BitmapData).height;
                }
            }
        }
        // Create output byte array
        var png:ByteArray = new ByteArray();
        // Write PNG signature
        png.writeUnsignedInt(0x89504e47);
        png.writeUnsignedInt(0x0D0A1A0A);
        // Build IHDR chunk
        var IHDR:ByteArray = new ByteArray();
        IHDR.writeInt(imgWidth);
        IHDR.writeInt(imgHeight);
        IHDR.writeUnsignedInt(0x08060000); // 32bit RGBA
        IHDR.writeByte(0);
        writeChunk(png,0x49484452,IHDR);
        // Build IDAT chunk
        var IDAT:ByteArray= new ByteArray();
        var img:BitmapData;
        var transparent:Boolean=(bmArray[0][0] as BitmapData).transparent;
       
        for(var i:int=0;i < imgHeight;i++) {
            // no filter
            IDAT.writeByte(0);
            var p:uint;
            if ( !transparent ) {
                for(var j:int=0;j < imgWidth;j++) {
                    img=bmArray[Math.floor(j/2048)][Math.floor(i/chunckSize)];
                    p = img.getPixel(j % chunckSize,i%chunckSize);
                    IDAT.writeUnsignedInt(
                        uint(((p&0xFFFFFF) << 8)|0xFF));
                }
            } else {
                for(j=0;j < imgWidth;j++) {
                    img=bmArray[Math.floor(j/2048)][Math.floor(i/chunckSize)];
                    p = img.getPixel32(j%chunckSize,i%chunckSize);
                    IDAT.writeUnsignedInt(
                        uint(((p&0xFFFFFF)<< 8)|((p>>>24))));
                }
            }
        }
        IDAT.compress();
        writeChunk(png,0x49444154,IDAT);
        // Build IEND chunk
        writeChunk(png,0x49454E44,null);
        // return PNG
        return png;
    }
    public static function encode(img:BitmapData):ByteArray {
        // Create output byte array
        var png:ByteArray = new ByteArray();
        // Write PNG signature
        png.writeUnsignedInt(0x89504e47);
        png.writeUnsignedInt(0x0D0A1A0A);
        // Build IHDR chunk
        var IHDR:ByteArray = new ByteArray();
        IHDR.writeInt(img.width);
        IHDR.writeInt(img.height);
        IHDR.writeUnsignedInt(0x08060000); // 32bit RGBA
        IHDR.writeByte(0);
        writeChunk(png,0x49484452,IHDR);
        // Build IDAT chunk
        var IDAT:ByteArray= new ByteArray();
        for(var i:int=0;i < img.height;i++) {
            // no filter
            IDAT.writeByte(0);
            var p:uint;
            if ( !img.transparent ) {
                for(var j:int=0;j < img.width;j++) {
                    p = img.getPixel(j,i);
                    IDAT.writeUnsignedInt(
                        uint(((p&0xFFFFFF) << 8)|0xFF));
                }
            } else {
                for(j=0;j < img.width;j++) {
                    p = img.getPixel32(j,i);
                    IDAT.writeUnsignedInt(
                        uint(((p&0xFFFFFF)<< 8)|((p>>>24))));
                }
            }
        }
        IDAT.compress();
        writeChunk(png,0x49444154,IDAT);
        // Build IEND chunk
        writeChunk(png,0x49454E44,null);
        // return PNG
        return png;
    }

    private static var crcTable:Array;
    private static var crcTableComputed:Boolean = false;

    private static function writeChunk(png:ByteArray, type:uint, data:ByteArray):void {
        if (!crcTableComputed) {
            crcTableComputed = true;
            crcTable = [];
            for (var n:uint = 0; n < 256; n++) {
                var c:uint = n;
                for (var k:uint = 0; k < 8; k++) {
                    if (c & 1) {
                        c = uint(uint(0xedb88320) ^
                            uint(c >>> 1));
                    } else {
                        c = uint(c >>> 1);
                    }
                }
                crcTable[n] = c;
            }
        }
        var len:uint = 0;
        if (data != null) {
            len = data.length;
        }
        png.writeUnsignedInt(len);
        var p:uint = png.position;
        png.writeUnsignedInt(type);
        if ( data != null ) {
            png.writeBytes(data);
        }
        var e:uint = png.position;
        png.position = p;
        c= 0xffffffff;
        for (var i:int = 0; i < (e-p); i++) {
            c = uint(crcTable[
                (c ^ png.readUnsignedByte()) &
                uint(0xff)] ^ uint(c >>> 8));
        }
        c = uint(c^uint(0xffffffff));
        png.position = e;
        png.writeUnsignedInt(c);
    }
}
}

So, how do you use these classes? Here’s a quick example:
Let’s create a sprite, and draw a rectangle in there that is 3000 by 3000 pixels. Normally we wouldn’t be able to render this sprite to a bitmap (in Flash 9).
create the sprite and draw a rectangle:

1
2
3
4
5
var testSprite:Sprite=new Sprite();
var g:Graphics=testSprite.graphics;
g.beginFill(0xCCCCCC,1);
g.drawRect(0,0,3000,3000);
g.endFill();

Now we use the ChunckedBitmapRenderer to render this Sprite to BitmapData:

1
2
3
4
5
6
7
var chunckedRenderer:ChunckedBitmapRenderer;
chunckedRenderer=new ChunckedBitmapRenderer(3000,
                         3000,
                         false,
                         0xFFFFFF,
                         2048);
chunckedRenderer.render(testSprite);

Offcourse once your sprite is rendered to bitmapdata, you want to convert it to an actual image file. Using the PNG encoder you can convert it to a .PNG:

1
var imgByteArray:ByteArray = chunckedRenderer.getPNG();

You probably want to be able to save this ByteArray to disk. If you are creating an AIR application, you can do it this way:

1
2
3
4
5
6
7
8
9
10
//save the image to the desktop as "snapshot.png"
var fl:File = File.desktopDirectory.resolvePath("snapshot.png");
var fs:FileStream = new FileStream();
try{
   fs.open(fl,FileMode.WRITE);
   fs.writeBytes(imgByteArray);
   fs.close();
}catch(e:Error){
   trace(e.message);
}

I do not recommend trying to render high resolution bitmaps realtime. What I do when creating generative art for example, is record all the mouse actions while creating a low resolution “painting”, and then re-run these actions on the high resolution version.