Async Image Example

Overview

Basic usage of an AsyncImage object in Loom. This app contains 3 separate AsyncImage objects which will continuously cycle through async texture loads. These loads can be either from the local /asset folder in the example or remotely via HTTP requests, and can be toggled between at any time with the button at the bottom of the app. The HTTP images are public images requested from Flickr through their REST API, and the code to do so is freely availble in this app.

Try It

Use the following Loom CLI commands to run this example:

loom new MyAsyncImageExample --example AsyncImageExample
cd MyAsyncImageExample
loom run

Screenshot

AsyncImageExample Screenshot

Code

src/AsyncImageExample.ls

package
{

    import loom.Application;
    import loom.platform.Timer;
    import loom.gameframework.LoomGroup;
    import loom.gameframework.TimeManager;
    import loom2d.Loom2D;
    import loom2d.display.Stage;
    import loom2d.math.Point;
    import loom2d.events.Touch;
    import loom2d.events.TouchEvent;
    import loom2d.events.TouchPhase;
    import loom2d.display.StageScaleMode;
    import loom2d.display.Image;
    import loom2d.display.AsyncImage;
    import loom2d.display.MovieClip;
    import loom2d.textures.Texture;
    import loom2d.textures.ConcreteTexture;
    import loom2d.ui.SimpleLabel;
    import loom2d.ui.SimpleButton;
    import loom.HTTPRequest;


    public class TexBox
    {
        private const NUM_IMAGES:int        = 10;

        //texture state constants
        private const NOT_LOADED:String     = "Touch Me!";
        private const LOADING_DISK:String   = "Loading from Disk...";
        private const LOADING_HTTP:String   = "Loading from HTTP...";
        private const LOADED:String         = "Load Completed";
        private const USING_CACHED:String   = "Using Cached Texture";
        private const ERROR:String          = "ERROR Loading Texture (Touch to Retry)";
        private const HTTP_FAIL             = "FAILED HTTP LOAD (Touch to Retry)";

        private var _sprite:AsyncImage;
        private var _label:SimpleLabel;
        private var _name:SimpleLabel;
        private var _priorityLabel:SimpleLabel;
        private var _texBase:String;

        private var _curImage:int = 0;
        private var _lastTexture:Texture;
        private var _newTex:Texture;
        private var _startTime:int;
        private var _go:Boolean = false;
        private var _priority:Boolean = false;
        private var _requestTimer:Timer = null;
        private var _httpTextureURLs:Vector.<String> = null;

        private static var _textureCache:Dictionary.<Texture, int> = new Dictionary.<Texture, int>();


        public function TexBox(texPath:String, stage:Stage, x:int, y:int)
        {
            _texBase = texPath;

            //image
            _sprite = new AsyncImage(AsyncImageExample.LoadingAnim, null, 256, 256);
            _sprite.center();
            _sprite.x = x + _sprite.width / 2;
            _sprite.y = y + _sprite.height / 2;
            _sprite.touchable = true;
            stage.addChild(_sprite);

            //label
            _label = new SimpleLabel("assets/fonts/Curse-hd.fnt", stage.stageWidth, 64);
            _label.x = _sprite.x - (stage.stageWidth / 8);
            _label.y = _sprite.y + (_sprite.height / 2 + 10);
            _label.scale = 0.25;
            _label.text = NOT_LOADED;
            _label.touchable = false;
            stage.addChild(_label);

            //name
            _name = new SimpleLabel("assets/fonts/Curse-hd.fnt", stage.stageWidth, 256);
            _name.x = _sprite.x - (stage.stageWidth / 8);
            _name.y = _sprite.y - ((_sprite.height / 2) + 56);
            _name.scale = 0.25;
            _name.text = "...";
            _name.touchable = false;
            stage.addChild(_name);

            //priority button & label
            var priorityButton:SimpleButton = new SimpleButton();
            priorityButton.scaleX = 0.4;
            priorityButton.scaleY = 0.2;
            priorityButton.center();
            priorityButton.x = _sprite.x - (priorityButton.width / 2);
            priorityButton.y = _sprite.y - ((_sprite.height + priorityButton.width) / 2) - 48;
            priorityButton.upImage = "assets/up.png";
            priorityButton.downImage = "assets/down.png";
            priorityButton.onClick +=  function() { _priority = !_priority; _priorityLabel.text = (_priority) ? "High Priority" : "Low Priority";};
            stage.addChild(priorityButton);

            _priorityLabel = new SimpleLabel("assets/fonts/Curse-hd.fnt", 256, 64);
            _priorityLabel.x = priorityButton.x - 8;
            _priorityLabel.y = priorityButton.y;
            _priorityLabel.scale = 0.25;
            _priorityLabel.touchable = false;
            _priorityLabel.text = (_priority) ? "High Priority" : "Low Priority";
            stage.addChild(_priorityLabel);

            //listen to touch events
            _sprite.addEventListener(TouchEvent.TOUCH, onTouch);

            //request Flickr image URLS
            AsyncImageExample.requestFlickrImageURLs(NUM_IMAGES, flickrImagesStore);

            //start up the cache
            _lastTexture = _sprite.texture;
            if(_textureCache[_lastTexture] == null)
            {
                //start at 1 because we never want this texture to be destroyed...
                _textureCache[_lastTexture] = 1;
            }
            _textureCache[_lastTexture]++;

            //timer to delay the auto-loads
            _requestTimer = new Timer(500);
            _requestTimer.onComplete += requestAsyncTex;
        }


        //called when there is a new list of Flickr image URLs to use
        private function flickrImagesStore(imageURLs:Vector.<String>):void
        {
            if(imageURLs != null)
            {
                _httpTextureURLs = imageURLs;
            }
        }


        //sets the new texture for our image
        private function updateTexture(tex:Texture, text:String):void
        {
            //add new texture to the cache
            if(tex != _lastTexture)
            {
                if(_textureCache[tex] == null)
                {
                    _textureCache[tex] = 0;
                }
                _textureCache[tex]++;

                //dispose old tex?
                if(_lastTexture != null)
                {
                    _textureCache[_lastTexture]--;
                    if(_textureCache[_lastTexture] == 0)
                    {
                        _textureCache.deleteKey(_lastTexture);
                        _lastTexture.dispose();
                    }
                }
                _lastTexture = tex;
            }

            //update labels
            AsyncImageExample.setLabel(_label, text);
            AsyncImageExample.setLabel(_name, (tex as ConcreteTexture).assetPath);
        }


        //requests a new async texture load
        private function requestAsyncTex(timer:Timer=null):void
        {
            var texToLoad:String = null;

            _startTime = Platform.getTime();
            if(AsyncImageExample.LoadFromHTTP && (_httpTextureURLs != null))
            {
                //load from HTTP
                texToLoad = _httpTextureURLs[_curImage];
                _newTex = _sprite.loadTextureFromHTTP(texToLoad, asyncLoadCompleteCB, httpLoadFailureCB, false, _priority);
            }
            else
            {
                //load from disk
                texToLoad = _texBase + _curImage + ".png";
                _newTex = _sprite.loadTextureFromAsset(texToLoad, asyncLoadCompleteCB, _priority);
            }

            //wrap image
            if(++_curImage == NUM_IMAGES)
            {
                _curImage = 0;

                //repopulate the Flickr image list
                AsyncImageExample.requestFlickrImageURLs(NUM_IMAGES, flickrImagesStore);
            }

            if(_sprite.loadStatus == AsyncImage.TEXTURE_NOTLOADED)
            {
                AsyncImageExample.setLabel(_label, ERROR);
                _go = false;
            }
            else if(_sprite.loadStatus == AsyncImage.TEXTURE_LOADED)
            {
                updateTexture(_newTex, USING_CACHED);
                if(_go)
                {
                    _requestTimer.start();
                }
            }
            else
            {
                //note that we've started async loading...
                AsyncImageExample.setLabel(_label, (AsyncImageExample.LoadFromHTTP) ? LOADING_HTTP : LOADING_DISK);
            }
        }


        //called when a texture completes async loading
        private function asyncLoadCompleteCB(texture:Texture):void
        {
            updateTexture(texture, LOADED + ": " + (Platform.getTime() - _startTime) + "ms");

            //again!
            if(_go)
            {
                _requestTimer.start();
            }
        }


        //called on HTTP texture load failure
        private function httpLoadFailureCB(texture:Texture):void
        {
            trace("Failed to load texture via HTTP...");
            AsyncImageExample.setLabel(_label, HTTP_FAIL);
        }


        //touch input to start/stop image loading
        private function onTouch(e:TouchEvent)
        {
            var touch = e.getTouch(_sprite, TouchPhase.BEGAN);
            if (touch)
            {
                _go = !_go;
                if(_go)
                {
                    requestAsyncTex();
                }
                else
                {
                    _sprite.cancelHTTPLoad();
                    AsyncImageExample.setLabel(_label, NOT_LOADED);
                    _requestTimer.stop();
                }
            }
        }
    }


    /**
     *  Example showcasing how to use the AsyncImage class in Loom
     */
    public class AsyncImageExample extends Application
    {
        private var _fps:SimpleLabel;
        private var _lastUpdate:int = 0;
        private var _numTicks:int;

        private var _textureA:TexBox;
        private var _textureB:TexBox;
        private var _textureC:TexBox;

        private var _polySprite:Image;
        private var _polySpeed:Point = new Point(200, 200);

        private static var UpdateLabels:Boolean = false;
        public static var LoadFromHTTP:Boolean = false;
        public static var LoadingAnim:MovieClip = null;
        static private var _httpRequestCache:Vector.<HTTPRequest> = [];


        override public function run():void
        {
            stage.scaleMode = StageScaleMode.LETTERBOX;

            //create permanent loading movieclip to use
            LoadingAnim = MovieClip.fromSpritesheet("assets/loadanim.png", 60, 60, 30, 5, 12, Loom2D.juggler);

            //bouncing poly so we can feel the performance
            _polySprite = new Image(Texture.fromAsset("assets/logo.png"));
            _polySprite.center();
            _polySprite.x = stage.stageWidth / 2;
            _polySprite.y = stage.stageHeight / 2;
            _polySprite.touchable = false;
            stage.addChild(_polySprite);

            //button & label to toggle the load type with
            var loadTypeLabel:SimpleLabel = new SimpleLabel("assets/fonts/Curse-hd.fnt", 256, 64);
            var typeButton:SimpleButton = new SimpleButton();
            typeButton.scaleX = 0.8;
            typeButton.scaleY = 0.4;
            typeButton.center();
            typeButton.x = (stage.stageWidth - typeButton.width) / 2 - typeButton.width;
            typeButton.y = stage.stageHeight - typeButton.height - 48;
            typeButton.upImage = "assets/up.png";
            typeButton.downImage = "assets/down.png";
            typeButton.onClick +=  function() { LoadFromHTTP = !LoadFromHTTP; loadTypeLabel.text = (LoadFromHTTP) ? "HTTP Load" : "Asset Load";};
            stage.addChild(typeButton);

            loadTypeLabel.x = typeButton.x - 16;
            loadTypeLabel.y = typeButton.y;
            loadTypeLabel.scale = 0.5;
            loadTypeLabel.touchable = false;
            loadTypeLabel.text = (LoadFromHTTP) ? "HTTP Load" : "Asset Load";
            stage.addChild(loadTypeLabel);


            //button & label to toggle label updates
            var labelUpdateLabel:SimpleLabel = new SimpleLabel("assets/fonts/Curse-hd.fnt", 256, 64);
            var labelButton:SimpleButton = new SimpleButton();
            labelButton.scaleX = 0.8;
            labelButton.scaleY = 0.4;
            labelButton.center();
            labelButton.x = (stage.stageWidth - labelButton.width) / 2 + labelButton.width;
            labelButton.y = stage.stageHeight - labelButton.height - 48;
            labelButton.upImage = "assets/up.png";
            labelButton.downImage = "assets/down.png";
            labelButton.onClick +=  function() { UpdateLabels = !UpdateLabels; labelUpdateLabel.text = (UpdateLabels) ? "Stop Label Updates" : "Update Labels";};
            stage.addChild(labelButton);

            labelUpdateLabel.x = labelButton.x - 16;
            labelUpdateLabel.y = labelButton.y;
            labelUpdateLabel.scale = 0.5;
            labelUpdateLabel.touchable = false;
            labelUpdateLabel.text = (UpdateLabels) ? "Stop Label Updates" : "Update Labels";
            stage.addChild(labelUpdateLabel);

            var labelUpdateHeader:SimpleLabel = new SimpleLabel("assets/fonts/Curse-hd.fnt");
            labelUpdateHeader.x = stage.stageWidth / 2 - 208;
            labelUpdateHeader.y = labelUpdateLabel.y - 64;
            labelUpdateHeader.scale = 0.25;
            labelUpdateHeader.touchable = false;
            labelUpdateHeader.text = "(Updating Labels will generate garbage and cause slowdowns)";
            stage.addChild(labelUpdateHeader);


            //add FPS output to the app
            _fps = new SimpleLabel("assets/fonts/Curse-hd.fnt", 256, 64);
            _fps.x = stage.stageWidth - 80;
            _fps.y = 10;
            _fps.scale = 0.25;
            _fps.touchable = false;
            stage.addChild(_fps);

            //create the texture load boxes
            var top:int = 128;
            var left:int = 32;
            var gap:int = 16;
            _textureA = new TexBox("assets/stream1/img_", stage, left, top);
            _textureC = new TexBox("assets/stream2/img_", stage, 300+left+gap, top);
            _textureB = new TexBox("assets/stream3/img_", stage, 600+left+(gap*2), top);
        }


        //called once per tick to update all the things
        override public function onTick():void
        {
            //handle FPS updating
            _numTicks++;
            var currentTime:int = Platform.getTime();
            if((currentTime - _lastUpdate) >= 1000)
            {
                var dt:Number = 1.0 / (((currentTime - _lastUpdate) / 1000) / _numTicks);
                dt = int(dt * 100) / 100;
                _lastUpdate = currentTime;
                _numTicks = 0;

                AsyncImageExample.setLabel(_fps, "fps: " + dt.toString());
            }

            //update our bouncing poly
            updatePoly();

            //tick super
            super.onTick();
        }


        //udpates the bounding Poly around the screen so we can see framerate hitches
        private function updatePoly():void
        {
            ///update the app DT
            var timeManager:TimeManager = LoomGroup.rootGroup.getManager(TimeManager) as TimeManager;
            var dt:Number = timeManager.deltaTime;

            //bounce poly around the screen
            _polySprite.x += _polySpeed.x * dt;
            _polySprite.y += _polySpeed.y * dt;

            //check for collision with bounds
            //X
            if(_polySprite.x >= stage.stageWidth)
            {
                _polySprite.x = (2 * stage.stageWidth) - _polySprite.x;
                _polySpeed.x *= -1.0;
            }
            else if(_polySprite.x <= 0)
            {
                _polySprite.x *= -1;
                _polySpeed.x *= -1.0;
            }

            //Y
            if(_polySprite.y >= stage.stageHeight)
            {
                _polySprite.y = (2 * stage.stageHeight) - _polySprite.y;
                _polySpeed.y *= -1.0;
            }
            else if(_polySprite.y <= 0)
            {
                _polySprite.y *= -1;
                _polySpeed.y *= -1.0;
            }
        }


        //wrapper for setting label text
        static public function setLabel(label:SimpleLabel, str:String):void
        {
            label.text = (UpdateLabels) ? str : " ";
        }


        //reqeusts a lists of Flickr image URLs to use as HTTP texture sources
        static public function requestFlickrImageURLs(count:int, func:Function)
        {
            var perPage = count + "";
            var apiKey = "cd563a32f84911cc06cab523db607bae";
            var request:HTTPRequest = new HTTPRequest("https://api.flickr.com/services/rest/?method=flickr.photos.getRecent&api_key=" + apiKey + "&per_page=" + perPage + "&page=1&format=json&nojsoncallback=1");
            request.method = "GET";
            request.onSuccess += function(data:ByteArray)
                                 {
                                    _httpRequestCache.remove(request);
                                    var imageUrls:Vector.<String> = null;
                                    var str:String = data.readUTFBytes(data.length);
                                    var json = new JSON();
                                    json.loadString(str);

                                    var photosObj:JSON = json.getObject("photos");
                                    if(photosObj != null)
                                    {
                                        var photos:JSON = photosObj.getArray("photo");
                                        if(photos)
                                        {
                                            imageUrls = new Vector.<String>();
                                            for (var i = 0; i < photos.getArrayCount(); i++)
                                            {
                                                var photo:JSON = photos.getArrayObject(i);
                                                var farmId = photo.getInteger("farm");
                                                var serverId = photo.getString("server");
                                                var id = photo.getString("id");
                                                var secret = photo.getString("secret");
                                                var url = "https://farm" + farmId + ".staticflickr.com/" + serverId + "/" + id + "_" + secret + ".jpg";
                                                imageUrls.pushSingle(url);
                                            }
                                        }
                                    }
                                    else
                                    {
                                        var errorCode:int = json.getInteger("code");
                                        var errorMessage:String = json.getString("message");
                                        trace("ERROR! Flickr API call failed with code: " + errorCode + " and message: " + errorMessage);
                                    }
                                    func(imageUrls);
                                 };
            request.onFailure += function(str:String) { trace("Flickr ERROR: " + str); _httpRequestCache.remove(request); func(null); };
            request.send();
            _httpRequestCache.pushSingle(request);
        }
    }
}

: