200

07 Jan
0
COMMENTS



    Zoom Into Your Own Canvas of Content

    Source: http://bit.ly/eChBJX

    Some paintings require you take a closer look to see all the details. Now instead of a painting, imagine a blank canvas that can be filled with images and text and dynamic descriptions.

    Note: This tutorial has nothing to do with the HTML5 canvas element!


    The demo is a viewer application that loads its data from XML. Content is placed on the “canvas” and the user can zoom into the canvas or click on any of the images to center it on the screen. Settings such as page size, background color/texture and other settings are loaded dynamically. As a bonus the user can create multiple pages.

    In this tutorial we will be looking at the basic concepts of creating an application like this. It is advisable to keep the source code next to you while reading the tutorial because not every part is explained. Source code is available for use with FlashDevelop or with Flash CS3 Professional and higher.


    Note: Opening the Application Locally

    Please be aware that before trying to open the application from your hard disk, you will need to have it added to the “trusted locations” list specified in the Global Security Settings panel shown below. Otherwise, no access is granted to grab the images from your hard disk. This is not needed when uploaded to a website as long as the images are located on the same web server.

    Global Security Settings panel

    Step 1: Setting up the XML file

    In case you are unfamiliar with XML, check out Dru Kepple’s AS3 101: XML. Shown below is the XML data corresponding to one page filled with one image and one text field. Settings specified here apply to all pages. As you can see, creating multiple pages and adding content to them is made easy. To images you can add a title and description which is seen when hovered over with the mouse. Multi-line text be added and size and style can be customized. Every text field uses a custom font found in the .fla file.

    01
    02
    03
    04
    05
    06
    07
    08
    09
    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
    <?xml version="1.0" encoding="utf-8" ?>
    <data>
    <settings>
    <bg_color>0x000000</bg_color>
    <image_path>../images/</image_path>
    <viewer_width>580</viewer_width>
    <viewer_height>380</viewer_height>
    <content_width>2000</content_width>
    <content_height>1300</content_height>
    <viewer_bg_color>0xEEDEC0</viewer_bg_color>
    <viewer_line_color>0xC1C59C</viewer_line_color>
    <viewer_offset_x>10</viewer_offset_x>
    <viewer_offset_y>10</viewer_offset_y>
    </settings>
    <page id="0" name="Holiday in Copenhagen">
    <text>
    <content>
    Hello there and welcome to the demonstration of the canvas application.
    Used as a subject here is my holiday to Copenhagen. anno 2010.
    Feel free to read the texts and sniff up the culture.
    </content>
    <size>23</size>
    <x>470</x>
    <y>50</y>
    </text>
    <image>
    <title>Church and lake</title>
    <description>The beautiful enviroment in Kastelskirken.</description>
    <url>Church_And_Lake.jpg</url>
    <scale>1</scale>
    <x>750</x>
    <y>500</y>
    </image>
    </page>
    </data>

    Step 2: The Document and Canvas Class

    Use the following code to set up our application. It is almost completely generated by FlashDevelop using the “AS3 Project” template. Main is the document class – which is the class first loaded when starting flash – and inside Canvas we are building our application.

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    15
    16
    public class Main extends Sprite
    {
    public function Main():void
    {
    if (stage) init();
    else addEventListener(Event.ADDED_TO_STAGE, init);
    }
    private function init(e:Event = null):void
    {
    removeEventListener(Event.ADDED_TO_STAGE, init);
    // entry point
    var canvas:Canvas = new Canvas(this);
    }
    }

    And a simplistic version of Canvas. Note that we use a display object container as a parameter to add that extra bit of control to add/remove the Canvas.

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    public class Canvas extends Sprite
    {
    public function Canvas(container:DisplayObjectContainer)
    {
    // Save parameter locally
    this.container = container;
    // Add to container
    container.addChild(this);
    }
    }

    Step 3: Using Bulk Loader to Load Assets

    In this application we are using the so called BulkLoader: an AS3 library that aims to make loading and managing complex loading requirements easier and faster. When first loading up, we use it to grab the XML file and a background image. The following code shows how to boot up the BulkLoader and add two items to the loading queue. The Complete function is called when all items are loaded.

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    // Init bulk loader
    loader = new BulkLoader("Canvas");
    loader.add("../background.jpg", { id:"background" } );
    loader.add("../settings.xml",   { id:"xmldata" } );
    // Loader events
    loader.addEventListener(BulkLoader.COMPLETE, Complete, false , 0, true);
    loader.addEventListener(BulkLoader.ERROR, HandleError, false, 0, true);
    // Start loader
    loader.start();

    Step 4: Saving the Loaded Assets

    We save the loaded assets inside the class for later use. The BulkLoader provides an easy way to grab the items we need. Just call the function corresponding to the type of object and use the object’s ID string. Static variables are used to make the XML data globally available (e.g. accessing a static variable using Canvas.viewerWidth). The path variable specifies where the externally loaded images will be located. Images can only be loaded from the own webserver due to security restrictions.

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    // Get background image
    g_background = (loader.getBitmap("background")).bitmapData;
    // Get XML data
    xmldata = loader.getXML("xmldata");
    // Set static vars for easy access
    viewerWidth = int(xmldata.settings.viewer_width);
    viewerHeight = int(xmldata.settings.viewer_height);
    viewerBgColor = uint(xmldata.settings.viewer_bg_color);
    path = String(xmldata.settings.image_path);
    customFont = new customFontClass();
    contentWidth = int(xmldata.settings.content_width);
    contentHeight = int(xmldata.settings.content_height);
    // Remove complete listener
    loader.removeEventListener(BulkLoader.COMPLETE, Complete);
    // Remove all data references stored in the loader
    loader.clear();
    // Remove loader from memory
    loader = null;
    // Set up viewer
    InitialiseViewer();

    Step 5: Introducing the Virtual Camera

    As you may or may not have guessed, the application uses a virtual camera to extract a portion of the canvas and show to the user. The camera applies scaling and translation to a displayobject container such that only the area under the camera is seen on the screen. The following demonstration gives you a better idea of how it works.

    The source code of this demonstration is also included in the archive. Let’s explain this example by starting with configurating the settings. We would like the viewer region to be 305 x 230, and the content region – which are the dimensions of the flower image – should try to keep the aspect ratio of the viewer region so it does not deform too much. Our flower is 1000 x 750, which is close enough. In order to show the entire flower in the viewer region, the camera must be the same size as the flower, which is 1000 x 750.

    The following code sets the dimension settings. In this example they are hardcoded, but otherwise it is taken from XML.

    1
    2
    3
    4
    5
    // Settings
    viewerWidth = 305;
    viewerHeight = 230;
    contentWidth = 1000;
    contentHeight = 750;

    Next up we create a viewer container.The option scrollRect is used to clip off anything outside a specified rectangle. This is very useful as only a portion of the canvas must be shown when zoomed in. It is a neater solution than for example putting a black border around the application.

    1
    2
    3
    4
    // Viewer container
    viewerContainer = new Sprite();
    viewerContainer.scrollRect = new Rectangle(0,0, viewerWidth, viewerHeight);
    addChild(viewerContainer);

    To the viewer container we add another Sprite called viewerScroller. The function of viewerScroller is simply to act as a container for anything that must serve as content for the camera. It makes it easier to add multiple items to the content region.

    1
    2
    3
    // Page scroller
    var viewerScroller:Sprite = new Sprite();
    viewerContainer.addChild(viewerScroller);

    Add content to the viewerScroller. In this case the flower image.

    1
    2
    3
    4
    5
    6
    // Content
    content = new Sprite();
    viewerScroller.addChild(content);
    var bmp:Bitmap = new image();
    content.addChild(bmp);

    Now add the Camera class. Although we have yet to make the Camera class, its constructor will take the viewer dimensions as parameter, and the function SetTarget has the content Sprite as parameter. We position the Camera in the middle of the content and give it a first update.

    1
    2
    3
    4
    5
    6
    // Add virtual camera
    cam = new Camera(viewerWidth,viewerHeight);
    cam.SetTarget(viewerScroller);
    cam.x = contentWidth / 2;
    cam.y = contentHeight / 2;
    cam.Update();

    With all this code in place we are able to render and manipulate the camera. Inside a Event.ENTER_FRAME loop you could run cam.Update() to refresh the camera view. It is however more efficient to only update upon a change. By changing cam.x and cam.y you can move the camera around, and by changing cam.scaleX and cam.scaleY you can change the scale. This is exactly what the keyboard keys in the example do.


    Step 6: Camera Class A Closer Look

    Here we take a look at the internals of the Camera. In the constructor function we add some graphics which are needed for the manipulation of scale of scale of the camera. We also store the viewer content dimensions locally inside the class.

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    15
    public function Camera(width:int,height:int,initialZoom:Number = 1)
    {
    // Sprite used to give the camera a width and height.
    g = new Shape();
    g.graphics.drawRect(0, 0, 10, 10);
    g.graphics.endFill();
    addChild(g);
    // Set dimensions of viewer rectangle
    tw = width;
    th = height;
    // Initial zoom
    this.scaleX = this.scaleY = initialZoom;
    }

    Next up, attach the camera to an object. The camera becomes the same scale as the object.

    1
    2
    3
    4
    5
    6
    7
    8
    public function SetTarget(target:Sprite):void
    {
    this.target = target;
    // adjust camera dimensions
    g.width = target.width;
    g.height = target.height;
    }

    This is the most interesting part and the engine of the camera. Scaling and translation is applied to the object attached to the camera by giving it a new transform matrix. Check out the Matrix class in the AS3 documentation for more information on the use of matrices.

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public function Update():void
    {
    cw = this.width;
    ch = this.height;
    tscaleX = tw / cw;
    tscaleY = th / ch;
    // put new scaling values
    mat.a = tscaleX;
    mat.d = tscaleY;
    // put new position (translation) values.
    // the camera position is made negative, because e.g. when the camera moves right, the page has to move left to accomodate.
    // cw and ch are added to move the page to the center of the viewing area
    // all positions are scaled accordingly
    mat.tx = (-mat.tx + cw / 2) * tscaleX;
    mat.ty = (-mat.ty + ch / 2) * tscaleY;
    target.transform.matrix = mat;
    }

    Step 7: Setting up the Viewer

    We return to our application. In this step we lay down the foundation for so called Page objects that contain the canvas with pictures and text. First, create a background for the whole application. The color is specified from XML.

    1
    2
    3
    4
    5
    6
    // Background
    var bg:Sprite = new Sprite();
    bg.graphics.beginFill(xmldata.settings.bgcolor);
    bg.graphics.drawRect(0, 0, stage.stageWidth, stage.stageHeight);
    bg.graphics.endFill();
    addChild(bg);

    Like in the example with the Camera and the flower, we are using pageContainer and pageScroller and Camera. This should look pretty familar.

    Next up is adding all the Page objects required as specified in the XML. In our example we have only created one page. Note that the Page class does not exist yet as we are creating this one the next step. pages is an array that will hold all Page references for later use.

    Parameters used by Page are

    • pageScroller: the Sprite to which the Page will be added to
    • p: an XML data object containing all information within a certain page (all images and text)
    • g_background: the background texture
    1
    2
    3
    4
    5
    6
    7
    // Add pages
    pages = new Array(xmldata.page.length());
    for each(var p:XML in xmldata.page)
    {
    var id:int = int(p.attributes()[0]);
    pages[id] = new Page(pageScroller, p, g_background);
    }

    Step 8: Creating a Page

    Displayed below is the constructor of Page. We simply save all the parameters locally in the class for later use.

    1
    2
    3
    4
    5
    6
    7
    8
    public function Page(container:DisplayObjectContainer,data:XML,background:BitmapData)
    {
    this.id = id;
    this.container = container;
    this.data = data;
    this.background = background;
    this.title = String(data.attributes()[1]);
    }

    Now a bit more interesting stuff. The Load function of Page starts off by taking the background texture we loaded before and wrapping it over the canvas. To do this we need to know the size of the background image we are looping and the size of the entire canvas. Create a Sprite called g_background which functions as a graphics container. Add a solid background color to prevent lines between the bitmap images to show.

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    15
    var b:int = background.width;
    var h:int = background.height;
    var trueWidth:int = Canvas.contentWidth;
    var trueHeight:int = Canvas.contentHeight;
    // background layer
    g_background = new Sprite();
    addChild(g_background);
    // A solid background color
    var bg:Sprite = new Sprite();
    bg.graphics.beginFill(Canvas.viewerBgColor);
    bg.graphics.drawRect(0, 0, trueWidth, trueHeight);
    bg.graphics.endFill();
    g_background.addChild(bg);

    Wrapping the background texture uses two loops, only vertically and one horizontally. It keeps on looping until the edge is reached. Every every tile at an even position – like (2,2), (4,2), (4,6) et cetera – is flipped around using scaleX or scaleY. This makes the texture flow over to the next one seamlessly. Checking if a number is even is done using the modulo operator (%). If the remainder of a number after deviding by 2 is zero then that number must be even. Around the edges we clip off any texture that goes outside the content dimensions as specified in trueWidth and trueHeight. It is important that the Page object stays this size as making it larger will change the aspect ratio and cause the screen to deform.

    01
    02
    03
    04
    05
    06
    07
    08
    09
    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
    // Add looped background image
    var i:int, j:int;
    while (i * b < trueWidth)
    {
    j = 0;
    while (j * h < trueHeight)
    {
    // new bitmap
    var s:Bitmap = new Bitmap(background);
    // position
    s.x = i * b;
    s.y = j * h;
    // alternate horizontal and vertical flip
    if (i % 2 != 0)
    {
    s.scaleX *= -1;
    s.x += b;
    }
    if (j % 2 != 0)
    {
    s.scaleY *= -1;
    s.y += h;
    }
    // clip
    if (i * b + b > trueWidth || j * h + h > trueHeight)
    {
    var clipw:int = Math.min(trueWidth - i * b, b);
    var cliph:int = Math.min(trueHeight - j * h, h);
    var nbd:BitmapData = new BitmapData(clipw, cliph);
    nbd.copyPixels(background, new Rectangle(0, 0, clipw, cliph), new Point());
    s.bitmapData = nbd;
    if (s.scaleX == -1)
    s.x -= b - clipw;
    if (s.scaleY == -1)
    s.y -= h - cliph;
    }
    // add bitmap to display list
    g_background.addChild(s);
    j++;
    }
    i++;
    }

    The result of the repeating background should be something like this. Dark borders are added for clarity.

    Repeating background

    Step 9: Loading Text

    Let’s start our page-filling journey by adding text. Go through all XML entries labelled “text” and pass their data to the AddText function.

    1
    2
    3
    4
    5
    texts = new Array();
    for each(var text:XML in data.text)
    {
    AddText(text);
    }

    The AddText function looks like this. The first portion of the code validates the XML data. In case some fields aren’t filled in it will append a standard value. QuickText is a class used to create a textfield with certain options. Finally, the text must be excluded from mouse interaction by using mouseEnabled = false and mouseChildren = false.

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    private function AddText(text:XML):void
    {
    if (!text.font)
    text.font = null;
    if (!text.size)
    text.size = 12;
    if (!text.color)
    text.color = 0x000000;
    if (!text.bold)
    text.bold = 0;
    if (!text.italic)
    text.italic = 0;
    var qt:QuickText = new QuickText(text.x, text.y, String(text.content), Canvas.customFont, int(text.size), uint(text.color), Boolean(text.bold), Boolean(text.italic));
    qt.blendMode = BlendMode.LAYER;
    texts.push(qt);
    addChild(qt);
    qt.mouseEnabled = false;
    qt.mouseChildren = false;
    }

    The following image shows all the options of the QuickText class:

    The options of the QuickText class.

    And the result of the new page including text:

    Page with text

    Step 10: Loading Images

    The first step here is to create Picture objects that will hold the XML data, the bitmap and some descriptive text. Listeners are directly applied for mouse interaction. All these Picture objects are then stored in an array for easy access later.

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    // grab all images
    pictures = new Array();
    for each(var image:XML in data.image)
    {
    // new picture object with information in it
    var picture:Picture = new Picture(image);
    pictures.push(picture);
    // add listeners to picture
    picture.addEventListener(MouseEvent.MOUSE_OVER, PictureMouseOver);
    picture.addEventListener(MouseEvent.MOUSE_OUT, PictureMouseOut);
    picture.addEventListener(MouseEvent.MOUSE_DOWN, PictureMouseDown);
    }

    And here is the basic version of the Picture class, just holding the XML data. Picture extends Sprite, we can already position it somewhere. The scale setting is verified before being used, because if the user omits it from the XML it will return 0. The standard value for scale is 1.

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    public function Picture(data:XML)
    {
    title = data.title;
    description = data.description;
    url = data.url;
    page = data.page;
    x = data.x;
    y = data.y;
    if (Number(data.scale) != 0)
    imgscale = Number(data.scale);
    }

    Create a new BulkLoader just like the first time, but now to load batches of images. We are going to be loading 5 images at a time and displaying those when done. It will keep fetching new images until all are done. The function Complete is called at the completion of each batch.

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    15
    16
    17
    // Create bulk loader.
    loader = new BulkLoader("page" + id);
    loader.addEventListener(BulkLoader.COMPLETE, Complete, false, 0, true);<br /><br />
    // set total amount of pictures
    totalPictures = pictures.length;
    // grab the first 5 pictures or the total amount of pictures if there are less.
    i = 0;
    while (i < Math.min(totalPictures,5))
    {
    loader.add(String(Canvas.path + pictures[i].url), { id: "img" + i } );
    i++;
    }
    // Start loader
    loader.start();

    Step 11: Loading Images (Continued)

    In this step we’re putting the loaded data inside Picture objects. The items property of the loader holds all objects that have been loaded in. Loop over all those objects and check whether they are an image. Each Bitmap is given to the corresponding Picture object and the Picture is added to the canvas. We also delete the loaded image from the loader items list to prevent conflicts with a later batch of loaded images.

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    // Image batch loaded. save the data to individual Picture objects.
    i = amountPicturesLoaded;
    for each(var item:LoadingItem in loader.items)
    {
    if (item.isImage())
    {
    pictures[i].SetImage(loader.getBitmap(item.id));
    loader.remove(item.id);
    AddPicture(pictures[i]);
    i++;
    amountPicturesLoaded++;
    }
    }

    And a closer look at the SetImage function of Picture.

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    15
    public function SetImage(ob:Bitmap):void
    {
    // if no image data is loaded, show nothing
    if (ob == null)
    return;
    // save the data inside the class
    img = ob;
    // show image
    addChild(img);
    // scale
    img.scaleX = img.scaleY = imgscale;
    }

    And adding a picture to the canvas is as easy as calling addChild.

    1
    2
    3
    4
    private function AddPicture(pict:Picture):void
    {
    addChild(pict);
    }

    The result now becomes:

    Page with text and images

    Step 12: Adding the Page

    Adding something to the camera view is done by adding a child to the viewerScroller container. We’ve added the viewerScroller to each Page object as a parameter so that we can add it as a child by calling the Show() function of Page.

    1
    2
    3
    4
    public function Show():void
    {
    container.addChild(this);
    }

    Return to the Canvas class and call the Load() and Show() functions when we’d like to a show the user a page. In case the Page is already loaded, Load() will return directly so no unnecessary actions are done. The current Page we are showing is saved in the class as page. This reference will be important for page interaction.

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    private function ShowPage(nr:int):void
    {
    // hide old page
    if (page)
    page.Hide();
    // set new page
    page = pages[nr];
    page.Load();
    page.Show();
    }

    Step 13: Zoom Modes

    Now that we’ve created our page with images and text, it’s necessary to scale it so it will fit inside our viewer region. We are using the public static variables zoom and magnifyStep for this purpose. magnifyStep is an array holding all the different scale values of the camera and zoom is the current position of magnifyStep the camera is scaled to. To know which scaling value is needed to fit the content inside the viewer we need to know the ratio between the content and viewer regions. To account for wrong aspect ratios we take the minimal ratio between width and heights as follows:

    1
    2
    // Set magnify steplist
    magnifyStepList[0] = Math.min(viewerWidth / contentWidth, viewerHeight / contentHeight);

    We’d like to zoom in when clicking on the canvas. Add a mouse event to the hitfield of Page. The hitfield is basically just the graphics in the background of Page and because it is a Sprite we can put mouse interaction on it.

    1
    page.hitField.addEventListener(MouseEvent.MOUSE_DOWN, MouseZoomIn);

    Upon a mouse click we would like the camera to scale down to the position zoom in the magnifyStepList and move to the point on the canvas that we clicked. Remember from the example that as the camera becomes smaller (scales down), the zoom on the canvas becomes larger. This is why we decrease the integer zoom by the value one. Getting the mouse position we clicked on the canvas is easy by using page.mouseX and page.mouseY. Flash automatically converts the numbers so that it appears local – meaning that if the page is for example 2000 px scaled down by 50% and you click halfway, it returns 1000 px, even though the mouse position on scale coordinates is a lot smaller.

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    15
    private function MouseZoomIn(e:MouseEvent):void
    {
    var pt:Point;
    if (!cam.bZooming && zoom > 0)
    {
    // Zoom in one step
    zoom--;
    // New camera point. Correct bounds.
    pt = new Point(page.mouseX, page.mouseY);
    CameraBounds(pt);
    cam.Zoom(pt.x, pt.y, magnifyStepList[zoom]);
    }
    }

    Step 14: Correcting Camera Position

    To keep the area of the camera inside the canvas we will need to correct the position within bounds of the camera. Looking at the camera example again for a demonstration of this. The camera is centered around itself so the position horizontally for example will need to stay within 0 + camera half-width and contentWidth - camera half-width. A reliable way to calculate the width of the camera when zoomed in is contentWidth/2 * magnifyStepList[zoom], because the camera in its initial unzoomed condition has the size contentWidth (same size as the canvas).

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    private function CameraBounds(pt:Point):void
    {
    // horizontally
    if (pt.x < contentWidth/2 * magnifyStepList[zoom])
    pt.x = contentWidth/2 * magnifyStepList[zoom];
    else if (pt.x > contentWidth - contentWidth/2 * magnifyStepList[zoom])
    pt.x = contentWidth - contentWidth/2 * magnifyStepList[zoom];
    // vertically
    if (pt.y < contentHeight/2 * magnifyStepList[zoom])
    pt.y = contentHeight/2 * magnifyStepList[zoom];
    else if (pt.y > contentHeight - contentHeight/2 * magnifyStepList[zoom])
    pt.y = contentHeight - contentHeight/2 * magnifyStepList[zoom];
    }

    Shown in the image below is the camera and the canvas, zoomed in one time. The red lines show the borders that the camera can not cross and must stay within.

    Camera bounds

    Step 15: Making the Zoom Work

    Zooming is performed by adding scale to the camera. We are using the Tweener class and the "easyOutQuint" transition to make this happen in a smooth way. bZooming is a variable used to see if the camera is already zooming or not. You cannot zoom on the page again while it is still busy scaling up or down. At each update of the camera the function Update is called, which performs the scaling on the content.

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    public function Zoom(newx:int, newy:int, newscale:Number):void
    {
    bZooming = true;
    Tweener.addTween(this, { time:2, x:newx, y:newy, transition:"easeOutQuint", scaleX:newscale, scaleY:newscale,
    onComplete:
    function():void
    {
    bZooming = false;
    },
    onUpdate: Update
    });
    }

    Step 16: A Closer Look at Pictures

    Remember that we added MouseEvent listeners to all the pictures inside the page. What we’d like to do is zoom in on a picture when one is clicked on and make sure it fits within the viewer region well. Images smaller than the actual viewer region must not be scaled beyond their resolution.

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    private function PictureMouseDown(e:MouseEvent):void
    {
    var newScale:Number;
    var screenRatio:Number = Canvas.viewerWidth / Canvas.viewerHeight;
    var imgW:Number = Math.max(e.target.width * 1.05, Canvas.viewerWidth);
    var imgH:Number = Math.max(e.target.height * 1.05, Canvas.viewerHeight);
    var imgRatio:Number = e.target.img.width / e.target.img.height
    // Calculate image scaling
    if (imgRatio < 1)
    newScale = imgH / Canvas.contentHeight;
    else
    newScale = imgW / Canvas.contentWidth;
    Canvas.cam.Zoom(e.target.x + e.target.width/2,
    e.target.y + e.target.height/2,
    newScale);
    Canvas.cam.bLocked = true;
    PictureMouseDisable();
    }

    The basic concept here is that the aspect ratio of an image must first be determined. If the width of an image is higher than the height then imgRatio < 1 will hold true and vice-versa if the height is larger than the width. We will always scale to the largest part of an image, meaning that if the image is for example 200x400px, we will treat the image like a 400×400 square. Another addition here is that we scale the image with 1.05 first, meaning the image becomes 5% larger. This way the image does not touch the sides when zoomed in. To calculate the scale of an image in comparison to the content size we divide it by the height or width of the content.

    Call the Zoom function of the camera and move to the middle of the picture we’re focussing on and apply the new scale that we’ve calculated.

    Here is the image zooming process shown in action. Notice how the camera is kept within the bounds of the page, and how the entire image including description fits inside the screen perfectly.

    Image zooming in action

    Step 17: Scrolling the Page

    If you hadn’t noticed, when zoomed in on a page you can move the mouse cursor to the edges of the screen to scroll around and view more of the page. The code shown below may look a little bizarre to you; it is something I’ve written a while ago for a RTS style game engine and have been re-using it ever since for anything that needs scrolling. Basic principles here is that you check where the mouse position is, and in case it moves without a certain range around the sizes (mouse_scroll_areax_reduced and mouse_scroll_areay_reduced) then it will start moving to one side at a rate proportionate to how far you are inside that range. When the mouse cursor is not inside the range it will put a drag on the scrolling to slow it down eventually.

    01
    02
    03
    04
    05
    06
    07
    08
    09
    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
    // Get the amount of scolling needed based on the mouse position
    mx = viewerContainer.mouseX;
    my = viewerContainer.mouseY;
    if (mx < mouse_scroll_areax && mx > 0)
    {
    scrollAmountX = ((((mx - mouse_scroll_areax) / mouse_scroll_areax_reduced) * mouse_scroll_factor) + .5) << 0;
    }
    else if ((viewerContainer.width - mx) < mouse_scroll_areax && mx < viewerContainer.width)
    {
    scrollAmountX = (((1 - (viewerContainer.width - mx) / mouse_scroll_areax_reduced) * mouse_scroll_factor) + .5) << 0;
    }
    if (my < mouse_scroll_areay && my > 0)
    {
    scrollAmountY = ((((my - mouse_scroll_areay) / mouse_scroll_areay_reduced) * mouse_scroll_factor) + .5) << 0;
    }
    else if ((viewerContainer.height - my) < mouse_scroll_areay && my < viewerContainer.height)
    {
    scrollAmountY = (((1 - (viewerContainer.height - my) / mouse_scroll_areay_reduced) * mouse_scroll_factor) + .5) << 0;
    }
    // Put drag on the scroll, so it does not keep on moving forever and slows down smoothly.
    scollAmountX *= .95;
    scrollAmountY *= .95;
    // Update camera position
    cam.x += int(scrollAmountX);
    cam.y += int(scrollAmountY);
    // Make sure the camera is within bounds
    var pt:Point = new Point(cam.x, cam.y);
    CameraBounds(pt);
    cam.x = pt.x;
    cam.y = pt.y;
    // Update the camera view
    cam.Update();

    Shown below is the area where mouse interaction for scrolling occurs. Remember that it can only happen when zoomed in.

    The scrolling borders visualised.

    Conclusion

    I believe this sums up all the learning goals I set out for this tutorial. Unfortunately, not everything shown in the application could be discussed because of the scope of the tutorial, but I hope you managed to learn the basics about XML data, page filling and camera manipulation. Thanks for reading!