I am loading a batch of 150 HD images into my app - it is basically a 3D view of an object. Once I load the image files using Loader instances I store the loaders' first child's bitmapdata in a Vector. When all of the loaded, I want to begin to "rotate" the object = meaning I am simply swapping the images. I take the Vector where I have the bitmapdatas and draw them onto a canvas bitmapdata one after the other. No science there, it all works as intended.
The problem is that once all the images are loaded and stored in a vector and BEFORE they are drawn to the canvas, they are not in the memory. That means that the first rotation of my 3D object (-> all 150 images drawn) is really slow. After the first rotation there is no problem and all is fluid. My question is: is there a way to force the images to get loaded into the memory without drawing them onto the stage? I expected that they would simply get loaded to memory once they are loaded to the app (Wrong!).
I tried to use addChild() instead of drawing them to a canvas bitmap, same result. I don't think the code is necessary but just in case:
private var _loaders:Vector.<Loader>;
private static const NAME:String = "img_00";
private static const MIN:uint = 0;
private static const MAX:uint = 150;
private var _loaded:uint = 0;
private var _currentFrameIndex:uint = 0;
private var _canvas:Bitmap;
private var _bitmaps:Vector.<BitmapData>;
private var _destPoint:Point;
public function loadImages():void {
var s:String;
for(var i:int=MIN; i<=MAX; i++) {
if(i < 10) s = "00" + i;
else if(i < 100) s = "0" + i;
else s = i.toString();
var loader:Loader = new Loader();
loader.contentLoaderInfo.addEventListener(Event.COMPLETE, loadHandler);
loader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, loadHandler);
loader.contentLoaderInfo.addEventListener(SecurityErrorEvent.SECURITY_ERROR, loadHandler);
loader.load(new URLRequest("images/JPEG/"+ NAME + s + ".jpg"));
_loaders.push(loader);
}
}
private function loadHandler(e:Event):void {
_loaded++;
if(_loaded > (MAX - MIN)) {
_bitmaps = new Vector.<BitmapData>(_loaders.length);
for(var i:int=0; i<_loaders.length; i++) {
var loader:Loader = _loaders[i];
_bitmaps[i] = Bitmap(loader.getChildAt(0)).bitmapData;
loader.contentLoaderInfo.removeEventListener(Event.COMPLETE, loadHandler);
loader.contentLoaderInfo.removeEventListener(IOErrorEvent.IO_ERROR, loadHandler);
loader.contentLoaderInfo.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, loadHandler);
}
setFrame(0);
dispatchEvent(new Event(LOAD_COMPLETE));
}
}
public function setFrame(frame:uint):void {
if(frame >= 0 && frame < _bitmaps.length) {
_currentFrameIndex = frame;
var bmpData:BitmapData = _bitmaps[_currentFrameIndex];
_canvas.bitmapData.copyPixels(bmpData, bmpData.rect, _destPoint);
}
}
"Not in the memory" means that the images are loaded, but not yet decoded, and this decode is done on the fly, and this takes the time you observe as slowness. You can attempt to "virtually" rotate the image by having a bitmap that's not yet added to stage to be the reference to each of the bitmapDatas of your vector. Make a progress bar that shows how much of the vector has already been decoded, and once this happens, display the bitmap and give the user smooth rotation.
addEventListener(Event.ENTER_FRAME,prerender);
var b:Bitmap=new Bitmap();
/* optional
b.x=stage.stageWidth;
b.y=stage.stageHeight;
addChild(b);
*/
var vi:int=0;
var sh:Shape=new Shape();
sh.graphics.lineStyle(4,0,1); // a simple progress bar
sh.graphics.moveTo(0,0);
sh.graphics.lineTo(100,0);
sh.scaleX=0;
sh.x=stage.stageWidth/2-50; // centered by X
sh.y=stage.stageHeight/2;
addChild(sh);
function prerender(e:Event):void {
if (vi==_bitmaps.length) {
// finished prerender
removeEventListener(Event.ENTER_FRAME, prerender);
removeChild(sh);
// removeChild(b); if optional enabled
setFrame(0);
return;
}
b.bitmapData=_bitmaps[vi];
vi++;
}
Also, it's always better to assign the bitmapData property to a Bitmap object if you don't plan to have that bitmapdata changed. So, instead of your _canvas.bitmapData.copyPixels(bmpData, bmpData.rect, _destPoint); you just do _canvas.bitmapData = bmpData; and it'll work.
UPDATE: Your issue might as well nail to the last point, that is assigning instead of copying. If your destPoint is something else than (0,0), you just make another Bitmap object on top of your _canvas with desired offset, and assign bitmapdatas in there. I have remembered that when I first made multiple animated objects based on a single Vector.<BitmapData> like yours, and tried doing copyPixels(), my animations were jittering and not displaying proper frames, but once I did _bitmap.bitmapData=_bitmaps[currentFrame] everything went as smooth as it should be.
Related
I'm making a Flash AS3 based game, and I'm building a custom font system. It works like this: a BitmapData class takes a PNG file from the library (FontSource), loops between every character in a given string and then gets its x, y, width and height inside the image from a function (getOffset), and then it uses copyPixels to draw the custom-font text.
Here's the code:
public function draw(str) {
var png = new FontSource(480, 32);
var offsets;
var char;
var rect;
var x;
var y;
this.lock();
for (var i = 0; i < str.length; i++) {
char = str.substr(i, 1);
offsets = getOffsets(char);
rect = new Rectangle(offsets[0], offsets[1], offsets[2], offsets[3]);
this.copyPixels(png, rect, new Point(x, y));
x += offsets[2];
}
this.unlock();
}
Now, the question here is: I'm building a typewriter effect class based on the ENTER_FRAME event; each new frame, a new character is added to a string. So, I wanted to ask which one of these approaches would do better related in a matter of performance:
1) Call draw() to redraw the whole BitmapData each frame.
2) Making an alpha mask and expand its width each frame.
3) Making separate Objects and adding them to stage each frame.
Thank you for your time.
As an alternative, you don't need to redraw the entire string. You can just draw more characters at its end. I'd implement this as follows: You give your bitmapped textfield a string it should draw per frame, once. It clears, then at each enter frame event it just adds 1 to the length of the string drawn, and draws only one single character. Just save more data in your class for this. For example:
public class BTF extends Sprite {
private var str:String;
private var source:FontSource; // set once!
private var bd:BitmapData; // the one that'll get drawn
private var lastIndex:int;
private var lastX:int; // coords to draw next symbol to
// assuming Y is always 0 - adjust per font
public function BTF(source:FontSource,width:int,height:int) {
this.source=source;
bd=new BitmapData(width,height,0x00808080,true); // check if I didn't mix up parameters
addChild(new Bitmap(bd)); // make it be drawn on your BTF
}
... // rest of class skipped
public function onEnterFrame(e:Event):void {
if (lastIndex>=str.length) return; // drawn all text already
var c:char=str.charAt(lastIndex++); // get next char to draw
var r:Rectangle=source.getOffsets(c); // you need to specify source for that - it's font that gives you bounds
bd.copyPixels(source.fontBitmapData,r,new Point(lastX,0)); // draw that char
lastX+=r.width; // move "cursor"
}
I'm calling images from library to be use. But because I will be constantly add and remove the images, so I tried to copy the bitmapdata to a sprite and reuse the sprite.
private function loadImage():void {
for (var i:int = 1; i < typeAmount + 1; i++) {
SlotClass = Main.queue.getLoader('main_uiMC').getClass('slot' + i + 'bmp') as Class;
bmpDSlot = new SlotClass() as BitmapData;
bmpDSlotV.push(bmpDSlot)
}
}
private function bitmaping():void {
for (var i:int = 1; i < typeAmount + 1; i++) {
slotS = new Sprite()
slotS.graphics.beginBitmapFill(bmpDSlotV[i - 1], new Matrix, false, true)
slotS.graphics.drawRect(0, 0, bmpDSlotV[i - 1].width, bmpDSlotV[i - 1].height);
slotS.graphics.endFill();
bmpV.push(slotS)
}
Every time I duplicate the sprite, flashdevelop's profiler showed that the bitmapdata is being added as well. Even when I use removeChild to remove the Sprite, the memory usage won't decrease.
Is there a way to better copy the content of the bitmapdata, and can be completely remove when I remove the sprite?
*i will still be using the image, just that on that particular round i would like to remove the sprite that has the image.
You should use a single BitmapData object, create numerous Bitmap objects and operate these.
private function bitmaping():void {
for (var i:int = 1; i < typeAmount + 1; i++) {
slotS = new Bitmap(bmpDSlotV[i-1]);
bmpV.push(slotS);
}
Creating another Bitmap like this does not add another BitmapData, it uses existing one as reference.
Well your choice is to reuse the BitmapData. And because you need multiple instances of the same image, you need to create new one for each object that you would like to add. And this would be a Bitmap with the old BitmapData:
var originalBMD:BitmapData = instantiateItSomehow();
var first:Bitmap = new Bitmap(originalBMD);
var second:Bitmap = new Bitmap(originalBMD);
This way you would reuse the BitmapData and I think the memory will increase only because of the Bitmap objects, but the data itself should not be duplicated.
I am successful in loading a single image and creating it using addChild(). Now I am trying to load multiple images into a sprite "Container" using a forEach loop increasing the X value for each image so they are displayed in a row. The imageloader is referenced to linkage within an XML document. If I testrun this code, this error pops up at the point when the image is loaded and I try to removeChild() the loadBar Animation.
ArgumentError: Error #2025: The supplied DisplayObject must be a child of the caller.
at flash.display::DisplayObjectContainer/removeChild()
Here is the AS3:
private function loadBG():void {
var artGrab:Number = 0;
var artX:Number = 0;
for each (var albumData:XML in mainXML.artist[artistID].album) {
imgURL = new URLRequest(mainXML.artist[artistID].album[artGrab].art);
imgLdr = new Loader();
//if you're loading a bigger image or need a preloader
imgLdr.contentLoaderInfo.addEventListener(Event.COMPLETE,onBGLoaded);
imgLdr.contentLoaderInfo.addEventListener(ProgressEvent.PROGRESS, onLoading);
//add loader animation "All Loader"
ldrAnim = new AllLoader();
albumContainer.addChild(ldrAnim);
ldrAnim.x = artX;
ldrAnim.y = 200;
imgLdr.load(imgURL);
artGrab++;
artX + 481;
ldrAnim.x + 481;
}
}
private function onLoading(evt:ProgressEvent):void {
var bytesToLoad:Number = imgLdr.contentLoaderInfo.bytesTotal;
var numberLoaded:Number = imgLdr.contentLoaderInfo.bytesLoaded;
ldrAnim.progBar.scaleX = numberLoaded / bytesToLoad;
var loadedPercent = Math.round(numberLoaded / bytesToLoad * 100);
ldrAnim.progPercent.text = loadedPercent + " %";
trace("Loading..." + loadedPercent + "%");
}
private function onBGLoaded(evt:Event):void {
trace("image loaded!");
//image setup
addChildAt(imgLdr,0);
//now that its 100% loaded, you can resize it , etc.
removeChild(ldrAnim);
//use cross multiplying of fractions to maintain aspect ratio
var origW = imgLdr.contentLoaderInfo.width;
var origH = imgLdr.contentLoaderInfo.height;
trace("orig width: "+ origW + "orig height: " + origH);
//set new width
imgLdr.width = 481;
var newH:Number = 481 * origH / origW;
imgLdr.height = newH;
//may wish to do positioning AFTER resizing
imgLdr.x = stage.stageWidth / 2 - imgLdr.width / 2;
imgLdr.x = 0;
imgLdr.y = 0;
imgLdr.width = 480;
imgLdr.height = 480;
imgLdr.alpha = 1;
imgLdr.z = 0;
}
Bless you for reading this all, I don't understand what is causing this error so comments are appreciated!
You need to have an array of animations for your loaders to hold with the loaders themselves, then when another ProgressEvent.PROGRESS event will arrive, query the array for event.target index, grab corresponding animation and adjust that, and stop relying on single-valued global vars once you put a single listener onto multiple different objects!
var animations:Vector.<AllLoader>;
var loaders:Vector.<LoaderInfo>;
private function loadBG():void {
var artGrab:int=0;
var artX:int=0;
animations=new Vector.<AllLoader>();
loaders=new Vector.<LoaderInfo>;
for each (var albumData:XML in mainXML.artist[artistID].album) {
imgURL = new URLRequest(mainXML.artist[artistID].album[artGrab].art);
imgLdr = new Loader();
//if you're loading a bigger image or need a preloader
imgLdr.contentLoaderInfo.addEventListener(Event.COMPLETE,onBGLoaded);
imgLdr.contentLoaderInfo.addEventListener(ProgressEvent.PROGRESS, onLoading);
//add loader animation "All Loader"
ldrAnim = new AllLoader();
albumContainer.addChild(ldrAnim);
anomations.push(ldrAnim)
loaders.push(imgLdr.contentLoaderInfo); // fill the arrays
ldrAnim.x = artX;
ldrAnim.y=200;
imgLdr.load(imgURL);
artGrab++;
artX+=481;
}
}
private function onLoading(evt:ProgressEvent):void{
var bytesToLoad:Number=evt.target.bytesTotal;
var numberLoaded:Number=evt.target.bytesLoaded; // note it now refers to target of event
var index:int=loaders.indexOf(evt.target); // should be valid
var ldrAnim:AllLoader=animations[index]; // grab corresponding animation
ldrAnim.progBar.scaleX = numberLoaded/bytesToLoad;
var loadedPercent=Math.round(numberLoaded/bytesToLoad*100);
ldrAnim.progPercent.text = loadedPercent +" %";
trace("Loading..."+loadedPercent +"%");
}
Do the same trick with your onBGLoaded function yourself, as a lesson. Note, you have to retrieve imgLdr value correctly from the event.
What's happening is that you have populated the variable ldrAnim multipe times, creating a new AllLoader each time. When you call removeChild(), this works fine the first time (sort of--it will remove the last one you created, whether it matches the image that loaded or not). When you call removeChild() again, you're calling it for the same one you just removed (which is no longer a child of the object you're calling it on).
One way to fix this is to use a Dictionary and associate each AllLoader with the Loader for that image. When the COMPLETE event fires, you can then look up the Alloader based on the event's properties and remove it.
Another solution is to write a Class that wraps an AllLoader and a Loader and then handles the transition between the two itself when the Loader has finished loading.
That might look something like this:
public class LoadSwitcher extends Sprite{
protected var loader:Loader;
protected var allLoader:AllLoader;
public function loadSwitcher(url) {
super();
loader = new Loader();
var request:URLRequest = new URLRequest(url);
loader.contentLoaderInfo.addEventListener(Event.COMPLETE, switchLoaders);
allLoader = new AllLoader(loader);//assume AllLoader now has logic to watch the loader for % complete
loader.load(request);
addChild(allLoader);
}
protected function switchLoaders(e:Event):void {
removeChild(allLoader);
addChild(loader);
}
}
Then just create and position one of these for each one of your albums.
I got a problem with AS3 and AIR. I'm working on a side-scrolling game for smartphones with a plane and I use different backgrounds as layers.
Before all other: I use GPU and only bitmaps, quality is set to low. So Performance settings are all set for smartphone use.
I putted them into a rectangle using the drawing API and move the background with a matrix:
protected var scrollingBitmap:BitmapData;
protected var canvas:Graphics;
protected var matrix:Matrix;
public function move(dx:Number, dy:Number):void {
matrix.translate(dx, dy);
if(dx != 0) matrix.tx %= scrollingBitmap.width;
if(dy != 0) matrix.ty %= scrollingBitmap.height;
drawCanvas();
}
protected function drawCanvas():void {
canvas.clear();
canvas.beginBitmapFill(scrollingBitmap, matrix, true, true);
canvas.drawRect(0, -scrollingBitmap.height, 1404, scrollingBitmap.height);
}
UPDATE2 (
Take a look at this: http://plasticsturgeon.com/2010/06/infinite-scrolling-bitmap-backgrounds-in-as3/
I used this to create my backgrounds.
With this I can simulate that my plane is flying to the right without moving the whole background and I can use a small single graphic which repeats every time (for the foreground layer).
For the background layer I use this method, too, but with a much larger graphic and I move it only with less the speed of my plane to simulate a far background.
My move-method is on an enterframe event. So I can update the background every frame with the "movement" of my plane.
)
The plane can exceed the height of the bitmaps. Everytime the bitmap comes back into the window/screen a real long lag occurs. And when the plane flies very fast, the game start to lag, too.
My first approach was to use .PNG files (but they are very big: 1-3MB size).
My next approach was to use .GIF files (much less size).
With both it's the same. So it can't be that.
I read about draw() and copyPixels() but I don't know, how I can use those to repeat the image.
UPDATE1:
protected var scrollingBitmap:BitmapData;
protected var canvas:Bitmap;
protected function init(e:Event):void {
removeEventListener(Event.ADDED_TO_STAGE, init);
canvas = new Bitmap(new BitmapData(1404, scrollingBitmap.height, true), "auto", true);
this.addChild(canvas);
drawCanvas();
}
public function move(dx:Number, dy:Number):void {
if(dx != 0) dx %= scrollingBitmap.width;
if(dy != 0) dy %= scrollingBitmap.height;
drawCanvas(dx, dy);
}
protected function drawCanvas(xPos:Number = 0, yPos:Number = 0):void {
canvas.bitmapData.copyPixels(scrollingBitmap, new Rectangle(0, 0, 1404, scrollingBitmap.height), new Point(xPos, yPos), scrollingBitmap);
}
I think you'd be better off with a Bitmap instead of using the graphics object with fill. copyPixels is very fast. So what you'd do is simply copyPixels over the top of whatever was there before, presuming everything is opaque. If everything is not opaque, you'll need to use your source bitmap as its own alpha data so previously drawn pixels don't show through.
Let's reframe your canvas so it is a Bitmap and not a MC. your new code will look like:
protected function drawCanvas():void {
canvas.bitmapData.copyPixels(scrollingBitmap, new Rectangle(0, 0, scrollingBitmap.width, scrollingBitmap.height), new Point(0,0), scrollingBitmap);
}
Oh, and look at that! Not only is this code faster, it's only one line of code!
EDIT: Added working code
package {
import flash.display.MovieClip;
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.events.Event;
import flash.geom.Rectangle;
import flash.geom.Point;
public class EndlessBG extends MovieClip{
//this one stays stationary, we're getting the pixels for the right side of the pic from here
private var _source:BitmapData;
//this is the one moving to the left (the pixels for the right side are not visible except for once a cycle);
private var _movingPixels:BitmapData;
private var _canvas:Bitmap;
private var _xOffset:int = 0;
private var _rect:Rectangle = new Rectangle();;
private var _point:Point = new Point();
public function EndlessBG() {
super();
_source = new BathroomStillLife();
_canvas = new Bitmap(new BitmapData(_source.width, _source.height));
_canvas.bitmapData.draw(_source);
_canvas.x = stage.stageWidth/2 - _canvas.width/2;
_canvas.y = 5;
addChild(_canvas);
addEventListener(Event.ENTER_FRAME, gameLoop);
setGeometryDefaults();
_movingPixels = new BitmapData(_source.width, _source.height);
_movingPixels.copyPixels(_source, _rect, _point);
//turn this on to watch red pixels be drawn where the source pixels are coming in
//_source = new BitmapData(_source.width, _source.height, false, 0xFF0000);
}
private function gameLoop(e:Event):void {
_xOffset--;//where the background is moving to
if (_xOffset < -_source.width) {
_xOffset = 0;
//this doesn't seem to work correctly:
//_movingPixels.scroll(_source.width, 0);
_movingPixels = new BitmapData(_source.width, _source.height);
_movingPixels.copyPixels(_source, _rect, _point);
}
trace(_xOffset);
setGeometryDefaults();
_movingPixels.scroll(-1, 0);
//draw the moved part of the canvas
_canvas.bitmapData.copyPixels(_movingPixels, _rect, _point);
//If we stop here, we get a smear to the right
//so, get the remaining pixels directly from the source
//1) reset our rect and point to be to the right side
_rect.x = 0;
_rect.width = -_xOffset;
_point.x = _source.width + _xOffset;
//2) copy from the source
_canvas.bitmapData.copyPixels(_source, _rect, _point);
}
private function setGeometryDefaults():void {
_rect.x=0;
_rect.y=0;
_rect.width = _source.width;
_rect.height = _source.height;
_point.x = 0;
_point.y = 0;
}
}
}
Not ideal, and not polished enough yet for a blog post, but should get you started.
Edit:
Eventually I did write that blog post.
http://www.greensock.com/blitmask
This might help although not free
Im currently using the following function to load an image, however i could not figure out a way to find the width of the loaded image, which i intend to use before placing the next image using the same function.
Note that q is a a variable (a number) which is used to load differant images.
=X i need help obtainning the loaded image width...
function LoadImage(q)
{
var imageLoader:Loader = new Loader();
var image:URLRequest = new URLRequest("GalleryImages/Album1/"+q+".jpg");
imageLoader.load(image);
addChild (imageLoader);
imageLoader.x = 0 + CurrentXLength;
imageLoader.y = 0;
imageLoader.name = "image"+q;
trace(imageLoader.x)
}
You can't know the width of the bitmap until it's actually loaded:
function LoadImage(q)
{
var imageLoader:Loader = new Loader();
var image:URLRequest = new URLRequest("GalleryImages/Album1/"+q+".jpg");
imageLoader.contentLoader.addEventListener(Event.COMPLETE, ImageLoaded);
imageLoader.load(image);
addChild (imageLoader);
...
private function ImageLoaded(e:Event):void
{
var imageLoader:Loader = Loader(e.target.loader);
var bm:Bitmap = Bitmap(imageLoader.content);
var CurrentXLength = bm.width;
....
Alternativly this link might be helpful? Haven't tried it myself ...
I just asked for de width property of loader object:
var loader:Loader;
function loadImage(dir:String):void {
loader = new Loader();
loader.contentLoaderInfo.addEventListener(Event.COMPLETE, placeImage);
var urlReq:URLRequest = new URLRequest(direccion);
loader.load(urlReq);
}
function placeImage(o:Event):void {
loader.x = (1360 - loader.width)/2;
loader.y = (768 - loader.height)/2;
addChild(loader);
}
Where 1360 and 768 are the canvas dimensions...
To access the width you must do it within the function assigned to handle Event.COMPLETE.
"You will want an array containing the items you wish to load. You should probably load this data in with XML so it is dynamic and scalable. Once the xml data is loaded it should be assigned to an array in whatever fashion you like. The reason that we must use an array in this situation, rather then just using the XML object, which is essentially an array, is because you need the know an objects width so that you can base the next objects X position off of the last objects X position plus its WIDTH.
With XML it is common to use a for loop and just iterate through "x" amount of times. We do not want this, in this case. To obtain the "WIDTH" property of the loaded asset, it must be accessed from within the function assigned to fire when the loader fires Event.COMPLETE. Once the image has completed it will remove the item from the array, set a variable as to the lastX and lastWidth, and then get the next item in the array and start all over. Eventually the array is empty and the process is complete.
-Note: I will skip loading the XML and just inject the data into the array myself.
package
{
import flash.display.Sprite;
import flash.display.Loader;
import flash.net.URLRequest;
public class DocumentClass extends Sprite
{
private var _array:Array;
private var _lastX:Number;
private var _lastWidth:Number;
public function DocumentClass():void
{
_array = new Array();
//Add Items to an array however you wish. I did it
//this way to make it easier to read on this site.
_array.push({name: "image1", path: "path/to/image1"});
_array.push({name: "image2", path: "path/to/image2"});
_array.push({name: "image3", path: "path/to/image3"});
loadImage();
}
private function loadImage():void
{
if(_array.length > 0)
{
var loader:Loader = new Loader();
addChild(loader);
var request:URLRequest = new URLRequest(_array[0].path); //Loads first item in the array each time.
loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onImageLoaded);
loader.x = _lastX + _lastWidth;
laoder.load(request);
_lastX = loader.x; //We set after so we are ready for the next.
_array.shift(); //Remove the first item from the array.
}
}
function onImageLoaded(e:Event):void
{
_lastWidth = e.target.width;
loadImage(); //Attempt to load another image if the array isn't empty.
}
}
}
I hope this helps, the code isn't tested, but the concept seems valid.
Yeah I used scott's answer but it's worth noting that 'imageLoader.contentLoader' should be 'imageLoader.contentLoaderInfo' in the LoadImage() function. Solved the width prob for me-- thanks mang