views:

154

answers:

3

I have imported a lot of paths from an Adobe Illustrator document right into a flash file. The paths exists as Drawing Objects inside the scene. Using pure actionscript, how can I move a symbol following each of the lines, without using predefined Motion Guides.

EDIT: I've attached the Flash file, full of drawing objects.

http://rapidshare.com/files/406497264/pather.fla.html

The question is: Are these drawing objects accessible through AS3 or I should convert them to symbols / whatever format necessary. And please give some AS examples.

Thanks!

+1  A: 

As I see it, this breaks down into two main steps:

  1. Moving the symbol onscreen
  2. Determining the motion path


Moving The Symbol Onscreen

To control the animation, you will use the ENTER_FRAME event to move the symbol onscreen. As in:

stage.addEventListener(Event.ENTER_FRAME, doMotion);

...

public function doMotion(event:Event):void {
   //animate along the path
   targetSymbol.x = someNewValue;
   targetSymbol.y = someNewValue;
}    

Another approach (one I admittedly use pretty often) is to use a Timer to trigger the animation calls, in this case the "doMotion" function. As in:

var FPS:Integer = 30; //frames per second (approx)

var animationTimer:Timer = new Timer(1/FPS * 1000); //converted from SEC to MS
animationTimer.addEventListener(TimerEvent.TIMER, doMotion);
animationTimer.start();


Determining The Motion Path

The tricky part is determining the motion path. There's no way to know the best approach for this from your description. But, to take a stab at it, it seems all the paths are straight lines.

-One Approach-
If so, then you can calculate where your symbol needs to be using the x, y, width, height and rotation values of the lines. Note that rotation is expressed in radians. So, at the beginning of your animation, you can use basic trigonometry (arcsin, arccos, arctan) to find the start and end points of the lines.

Once you know the endpoints, you can start your targetSymbol at the begining x/y and move it to the ending x/y in any fashion you want. The simplest being to just divide the total change in x by the number of frames you want to take to complete animation and repeat for y. Then add those tiny numbers to the x and y values in each iteration. You can also add effects like "easing" by tweaking the "velocity" of your motion so it starts and stops "gently."

Since the lines are imported into flash, you can give them instance names to make coding easier. Or, if there are too many, you can iterate over all the display objects storing all the lines in some type of collection.

-Another Approach-
You may also be able to create a single animation as a movieclip and then duplicate that movieclip everywhere else the animation is needed. So you could "manually" draw/tween an impressive animation, then programatically place transformed instances of the animation in the proper positions. These positions would be determined by the existing lines.

I hope all that helps in some way,

--gMale

gmale
A: 

The question is: Are these drawing objects accessible through AS3 or I should convert them to symbols / whatever format necessary. And please give some AS examples.

I'm not aware of any way to access the symbols directly in AS3. From what I can tell they are elements of their parent MovieClip's graphics property. For example, you can write code like this:

var drawOnMe:Sprite = new Sprite();

drawOnMe.graphics.beginFill(0xCC0000);
drawOnMe.graphics.drawRect(0,0,100,100);
drawOnMe.graphics.endFill();

However once you have drawn this shape on the drawOnMe.graphics object you can't access it anymore because it has no identifier. Your imported artwork is essentially in the same boat.

Since you can't access the artwork, it will be impossible to get points from the imported outlines and animate a symbol along the path. In short, I don't really think this can be done.

More info on the Graphics property:

http://help.adobe.com/en_US/AS3LCR/Flash_10.0/

Casey
+3  A: 

Hello Jauzsika,

Nice question +1

I've seen the fla at work, don't have cs5 home, but I understand what you're trying to achieve.

My approaches were these:

  1. Motion Paths:

Since Flash CS4 you can copy a path, and paste it onto a Motion Tween. This will work similar to the Classic Tween's Motion Guide feature. There are quite a few problems with this:

  • You might need to manually do the cut/paste
  • Not all paths can be pasted onto a motion
  • You can use the AnimationFactory class and add a target, but the problem is, while the target is animating, you have no actionscript control over it. You can set a timer for the duration of the AnimationFactory's motion, but it gets to cumbersome.

Obviously this is a no-no.

  1. Using JSFL to traverse the paths inside the IDE:

    I stumbled upon this very handy jsfl script by ericlin which traverses all shapes selected on stage. If you select your paths and run the script(you can just double click the jsfl file), you will get the parsed coordinates.

I did a simple test using TweenLite:

import com.greensock.*;
import com.greensock.easing.*;
import com.greensock.plugins.*;

TweenPlugin.activate([BezierPlugin]);

graphics.lineStyle(0.05);
var index:int = 0;
var ball:Sprite = new Sprite();
ball.graphics.beginFill(0x009900,.75);ball.graphics.drawCircle(-2,-2,4);ball.graphics.endFill();
addChild(ball);

drawLines();

function drawLines():void{
    var t:Number = .01;
    var timeline:TimelineLite = new TimelineLite();
    var i:int = index;
    for(index; index <= ptArray.length; index += 12){
        timeline.append( new TweenLite(ball, t, {x:ptArray[i],y:ptArray[i+1]}) );
        timeline.append( new TweenLite(ball, t, {bezier:[{x:ptArray[i+2], y:ptArray[i+3]}, {x:ptArray[i+4], y:ptArray[i+5]}]}) );
        this.graphics.moveTo(ptArray[i], ptArray[i+1]);
        this.graphics.curveTo(ptArray[i+2], ptArray[i+3], ptArray[i+4], ptArray[i+5]);
        i += 6;
        timeline.append( new TweenLite(ball, t, {x:ptArray[i],y:ptArray[i+1]}) );
        timeline.append( new TweenLite(ball, t, {bezier:[{x:ptArray[i+2], y:ptArray[i+3]}, {x:ptArray[i+4], y:ptArray[i+5]}]}) );
        this.graphics.moveTo(ptArray[i], ptArray[i+1]);
        this.graphics.curveTo(ptArray[i+2], ptArray[i+3], ptArray[i+4], ptArray[i+5]);
    }
}

*Note:*The ptArray isn't shown here because it would waste too much space. The result isn't that great though. You can have a look at the fla to see what I mean. The jsfl script could be altered, but I saw you emphasised actionscript usage, so this is a no no as well.

  1. Using AS3SWF to decompile the swf at runtime and access the shapes:

Claus Wahlers developed an amazing as3 library called as3swf which allows flash/flex developers to decompile swfs at runtime. Here is an awesome article explaining the ins and outs of shapes inside swfs. There are quite a few exporters already written.

I just duplicated the AS3ShapeExporter and changed the as3 draw commands to TweenLite code. Basically I replaced moveTo with a fast tween to position, lineTo, to a regular tween and curveTo with a bezier tween. Tween Lite's BezierPlugin luckily used quadratic bezier, just like curveTo does.

Here is the code you will need to paste inside the fla that holds the shapes:

import com.codeazur.as3swf.*;
import com.codeazur.as3swf.tags.*;
import com.codeazur.as3swf.exporters.*;

this.loaderInfo.addEventListener(Event.COMPLETE, completeHandler);

function completeHandler(e:Event):void {
    var swf:SWF = new SWF(this.loaderInfo.bytes);//new SWF(URLLoader(e.target).data as ByteArray);
    var doc:AS3ShapeTweenLiteExporter = new AS3ShapeTweenLiteExporter(swf,"ball",.01);
    // Loop over all tags
    for (var i:uint = 0; i < swf.tags.length; i++) {
        var tag:ITag = swf.tags[i];
        // Check if tag is a DefineShape
        if (tag is TagDefineShape) {
          // Export shape tween
          TagDefineShape(tag).export(doc);

        }
    }
    trace(doc.actionScript);
}

Basically I load the swf, once it's ready, I pass it's bytes to as3swf and use the AS3ShapeTweenLiteExporter to parse shape tags and spit out actionscript. The 3 paramaters I pass to the constructor are : the swf instance, a name for the tween target and a time for each tween.

Here's how my hacked together class looks like:

package com.codeazur.as3swf.exporters
{
    import com.codeazur.as3swf.SWF;
    import com.codeazur.utils.StringUtils;

    import flash.display.CapsStyle;
    import flash.display.InterpolationMethod;
    import flash.display.JointStyle;
    import flash.display.LineScaleMode;
    import flash.display.SpreadMethod;
    import flash.geom.Matrix;
    import com.codeazur.as3swf.exporters.core.DefaultShapeExporter;

    public class AS3ShapeTweenLiteExporter extends DefaultShapeExporter
    {
        protected var _actionScript:String;
        protected var _target:String;
        protected var _time:Number;

        public function AS3ShapeTweenLiteExporter(swf:SWF,target:String,time:Number) {
            super(swf);
            _target = target;
            _time = time;
        }

        public function get actionScript():String { return _actionScript; }

        override public function beginShape():void {
            _actionScript = "import com.greensock.*;\rimport com.greensock.plugins.*;\r\rTweenPlugin.activate([BezierPlugin]);\r\rvar shapeTimeline:TimelineLite = new TimelineLite()\r";
        }

        override public function beginFills():void {
            //_actionScript += "// Fills:\rgraphics.lineStyle();\r";
        }

        override public function beginLines():void {
            //_actionScript += "// Lines:\r";
        }

        override public function beginFill(color:uint, alpha:Number = 1.0):void {
            if (alpha != 1.0) {
                _actionScript += StringUtils.printf("graphics.beginFill(0x%06x, %f);\r", color, alpha);
            } else {
                _actionScript += StringUtils.printf("graphics.beginFill(0x%06x);\r", color);
            }
        }

        override public function beginGradientFill(type:String, colors:Array, alphas:Array, ratios:Array, matrix:Matrix = null, spreadMethod:String = SpreadMethod.PAD, interpolationMethod:String = InterpolationMethod.RGB, focalPointRatio:Number = 0):void {
            var asMatrix:String = "null";
            if (matrix != null) {
                asMatrix = "new Matrix(" +
                    matrix.a + "," +
                    matrix.b + "," +
                    matrix.c + "," +
                    matrix.d + "," +
                    matrix.tx + "," +
                    matrix.ty + ")";
            }
            var asColors:String = "";
            for (var i:uint = 0; i < colors.length; i++) {
                asColors += StringUtils.printf("0x%06x", colors[i]);
                if (i < colors.length - 1) { asColors += ","; }
            }
            if (focalPointRatio != 0.0) {
                _actionScript += StringUtils.printf("graphics.beginGradientFill('%s', [%s], [%s], [%s], %s, '%s', '%s', %s);\r",
                    type,
                    asColors,
                    alphas.join(","),
                    ratios.join(","),
                    asMatrix,
                    spreadMethod,
                    interpolationMethod,
                    focalPointRatio.toString());
            } else if (interpolationMethod != InterpolationMethod.RGB) {
                _actionScript += StringUtils.printf("graphics.beginGradientFill('%s', [%s], [%s], [%s], %s, '%s', '%s'\r);",
                    type,
                    asColors,
                    alphas.join(","),
                    ratios.join(","),
                    asMatrix,
                    spreadMethod,
                    interpolationMethod);
            } else if (spreadMethod != SpreadMethod.PAD) {
                _actionScript += StringUtils.printf("graphics.beginGradientFill('%s', [%s], [%s], [%s], %s, '%s');\r",
                    type,
                    asColors,
                    alphas.join(","),
                    ratios.join(","),
                    asMatrix,
                    spreadMethod);
            } else if (matrix != null) {
                _actionScript += StringUtils.printf("graphics.beginGradientFill('%s', [%s], [%s], [%s], %s);\r",
                    type,
                    asColors,
                    alphas.join(","),
                    ratios.join(","),
                    asMatrix);
            } else {
                _actionScript += StringUtils.printf("graphics.beginGradientFill('%s', [%s], [%s], [%s]);\r",
                    type,
                    asColors,
                    alphas.join(","),
                    ratios.join(","));
            }
        }

        override public function beginBitmapFill(bitmapId:uint, matrix:Matrix = null, repeat:Boolean = true, smooth:Boolean = false):void {
            var asMatrix:String = "null";
            if (matrix != null) {
                asMatrix = "new Matrix(" +
                    matrix.a + "," +
                    matrix.b + "," +
                    matrix.c + "," +
                    matrix.d + "," +
                    matrix.tx + "," +
                    matrix.ty + ")";
            }
            if (smooth) {
                _actionScript += StringUtils.printf("// graphics.beginBitmapFill(%d, %s, %s, %s);\r", bitmapId, asMatrix, repeat, smooth);
            } else if (!repeat) {
                _actionScript += StringUtils.printf("// graphics.beginBitmapFill(%d, %s, %s, %s);\r", bitmapId, asMatrix, repeat);
            } else {
                _actionScript += StringUtils.printf("// graphics.beginBitmapFill(%d, %s, %s, %s);\r", bitmapId, asMatrix);
            }
        }

        override public function endFill():void {
            _actionScript += "graphics.endFill();\r";
        }

        override public function lineStyle(thickness:Number = NaN, color:uint = 0, alpha:Number = 1.0, pixelHinting:Boolean = false, scaleMode:String = LineScaleMode.NORMAL, startCaps:String = null, endCaps:String = null, joints:String = null, miterLimit:Number = 3):void {
            /*
            if (miterLimit != 3) {
                _actionScript += StringUtils.printf("graphics.lineStyle(%f, 0x%06x, %f, %s, %s, %s, %s, %f);\r",
                    thickness, color, alpha, pixelHinting.toString(),
                    (scaleMode == null ? "null" : "'" + scaleMode + "'"),
                    (startCaps == null ? "null" : "'" + startCaps + "'"),
                    (joints == null ? "null" : "'" + joints + "'"),
                    miterLimit);
            } else if (joints != null && joints != JointStyle.ROUND) {
                _actionScript += StringUtils.printf("graphics.lineStyle(%f, 0x%06x, %f, %s, %s, %s, %s);\r",
                    thickness, color, alpha, pixelHinting.toString(),
                    (scaleMode == null ? "null" : "'" + scaleMode + "'"),
                    (startCaps == null ? "null" : "'" + startCaps + "'"),
                    "'" + joints + "'");
            } else if(startCaps != null && startCaps != CapsStyle.ROUND) {
                _actionScript += StringUtils.printf("graphics.lineStyle(%f, 0x%06x, %f, %s, %s, %s);\r",
                    thickness, color, alpha, pixelHinting.toString(),
                    (scaleMode == null ? "null" : "'" + scaleMode + "'"),
                    "'" + startCaps + "'");
            } else if(scaleMode != LineScaleMode.NORMAL) {
                _actionScript += StringUtils.printf("graphics.lineStyle(%f, 0x%06x, %f, %s, %s);\r",
                    thickness, color, alpha, pixelHinting.toString(),
                    (scaleMode == null ? "null" : "'" + scaleMode + "'"));
            } else if(pixelHinting) {
                _actionScript += StringUtils.printf("graphics.lineStyle(%f, 0x%06x, %f, %s);\r",
                    thickness, color, alpha, pixelHinting.toString());
            } else if(alpha != 1.0) {
                _actionScript += StringUtils.printf("graphics.lineStyle(%f, 0x%06x, %f);\r", thickness, color, alpha);
            } else if(color != 0) {
                _actionScript += StringUtils.printf("graphics.lineStyle(%f, 0x%06x);\r", thickness, color);
            } else if(!isNaN(thickness)) {
                _actionScript += StringUtils.printf("graphics.lineStyle(%f);\r", thickness);
            } else {
                _actionScript += "graphics.lineStyle();\r";
            }
            */
        }

        override public function moveTo(x:Number, y:Number):void {
            //_actionScript += StringUtils.printf("graphics.moveTo(%f, %f);\r", x, y);
            //_actionScript += StringUtils.printf(_target+".x = %f;\r"+_target+".y = %f;\r", x, y);
            _actionScript += StringUtils.printf("shapeTimeline.append(new TweenLite("+_target+",0.001,{x:%f,y: %f}));\r", x, y);
        }

        override public function lineTo(x:Number, y:Number):void {
            //_actionScript += StringUtils.printf("graphics.lineTo(%f, %f);\r", x, y);
            _actionScript += StringUtils.printf("shapeTimeline.append(new TweenLite("+_target+","+_time+",{x:%f,y: %f}));\r", x, y);
        }

        override public function curveTo(controlX:Number, controlY:Number, anchorX:Number, anchorY:Number):void {
            //_actionScript += StringUtils.printf("graphics.curveTo(%f, %f, %f, %f);\r", controlX, controlY, anchorX, anchorY);
            _actionScript += StringUtils.printf("shapeTimeline.append(new TweenLite("+_target+","+_time+",{bezier:[{x:%f,y: %f},{x:%f,y: %f}]}));\r", controlX, controlY, anchorX, anchorY);
        }
    }
}

Once you download as3swf, you need to save this class in the exporter's package. Here is the result. You can download its fla and also the fla for that generated the code.

This is a pure actionscript version and has decent result.

The animation looks jerky because it has uses the same amount of time to tween for each of the line segments. some are shorter while others are longer. You could store the previous position and use it with the current position to calculate the distance, and based on that generate a decent for each TweenLite instance. Also feel free to modify that class any way you need(say you want to use a Timer instead, etc.)

Update

I had time to tinker with this a bit more. I changed the exporter a bit, now it also expects a maximum distance for your target object to travel. That will be either the width or height of the selection(all the lines), depending on which one is larger(width vs height). The previous x and y values are stored and used to calculate the distance, then that distance is divided by the maximum distance to travel. This in turn is used to scale the time on each tween. Also, I've set the easing to Linear, because the default(Quad.easeOut) added to the jitter. The timing isn't very accurate, but looks a bit better. Updated fla's here and here

The updated timeline code:

import com.codeazur.as3swf.*;
import com.codeazur.as3swf.tags.*;
import com.codeazur.as3swf.exporters.*;

this.loaderInfo.addEventListener(Event.COMPLETE, completeHandler);

function completeHandler(e:Event):void {
    var swf:SWF = new SWF(this.loaderInfo.bytes);//new SWF(URLLoader(e.target).data as ByteArray);
    var doc:AS3ShapeTweenLiteExporter = new AS3ShapeTweenLiteExporter(swf,"ball",1,300);
    // Loop over all tags
    for (var i:uint = 0; i < swf.tags.length; i++) {
        var tag:ITag = swf.tags[i];
        // Check if tag is a DefineShape
        if (tag is TagDefineShape) {
          // Export shape tween
          TagDefineShape(tag).export(doc);

        }
    }
    trace(doc.actionScript);
    System.setClipboard(doc.actionScript);
} 

the updated exporter:

Again, feel free to tinker.

UPDATE 2:

Ok, Here's yet another method...

  1. Using AS3 to parse FXG exported straight from Illustrator:

Since Illustrator CS4, you can save graphics as FXG via File > Save a Copy > select FXG filetype Here's the fxg file I used.

Japan's done it again :) The amazing Lib Spark contains an FXG Parser. There is also an SVGParser, but for now I've only played with fxg.

So the first step is to download the library:

svn export http://www.libspark.org/svn/as3/FxgParser

You might be fine using the sample, since you use Flash CS5. The parser uses TLF for text. I didn't bother to download the whole flex4 sdk to get the swc and setup. I just commented out the Text parser since we're concerned with paths. The commented out class is at the bottom.

The library contains a Path parser which is cloned and modified to get some animation code: PathTween.as You might recognize some of the variables from the as3swf classes. Here are some explanation for some of the variables I added:

  • ID - is static and is a counter for the number of Path parsed, this means we can animate each path individually
  • CODE - static, contains the import code + the timeline lite code for each path instance, this is THE CODE :)
  • MAX_DISTANCE - similar to the as3swf approach, this is used to alter the time of each tween, based on the distance travelled
  • TIME - a generic time for each tween, handy to be set from outside the class
  • TARGET - a name that will be incremented for each path and is used as the tween target.
  • _code,_distance,_x,_y,_timeScale - the same as in the as3swf approach
  • _timeRel - the relative time of each time(e.g. after it's been adjusted)

Also, I've done a quickfix, added a default winding, since sometimes, the winding attribute might be missing from the .fxg file and that breaks the parser.

In order to use you need to make a minor change to FxgFactory.as so it uses the PathTween parser instead of the default Path class.

private static const PARSERS:Array = [  Graphic , Group , Library, 
                                                Path , Ellipse, Rect, Line, 
                                                BitmapGraphic, BitmapImage, 
                                                TextGraphic, RichText ];

becomes:

private static const PARSERS:Array = [  Graphic , Group , Library, 
                                                PathTweenTracer , Ellipse, Rect, Line, 
                                                BitmapGraphic, BitmapImage, 
                                                TextGraphic, RichText ];

Finally, some basic timeline code that uses all these:

import fxgparser.*
import fxgparser.parser.*;

var fxgurl:String = "fingerprint.fxg";
var fxgSprite:FxgDisplay;

var loader:URLLoader = new URLLoader( new URLRequest( fxgurl ) );
    loader.addEventListener( Event.COMPLETE , displayData );

//some setup
PathTween.MAX_DISTANCE = 360;//change this to fit your shape's largest dimension(width || height)
PathTween.TIME = 2;//change this to your needs
PathTween.TARGET = "ball";//a name of a target clip that will be incremented for each move,line,curve


function displayData( e:Event ):void {
    var fxgxml:XML = XML( e.currentTarget.data );

    fxgSprite = new FxgDisplay( fxgxml );   //parse SVG
    System.setClipboard(PathTween.CODE);
    //make some clips for the tester
    trace(getClips());

    addChild( fxgSprite );  
}

function getClips():String {
    var result:String = 'this.filters = [new GlowFilter(0x00ff99)]\r';
    var clipsNum:int = PathTween.ID;
    var target:String = PathTween.TARGET;
    for(var i:int = 0 ; i < clipsNum ; i++)
        result += 'var '+(target+i)+':Sprite = new Sprite();\r'+(target+i)+'.graphics.beginFill(0x00ff00);\r'+(target+i)+'.graphics.drawCircle(-2,-2,4);\r'+(target+i)+'.graphics.endFill();\raddChild('+(target+i)+');\r';
    return result;
}

This is fairly simple:

  • setup the constants in PathTween
  • load the fxg
  • once it's loaded, draw it. While it's drawing, code is generated in the background
  • once it's drawn, put the code in the clipboard. For my path I had about 11K of generated lines, so tracing isn't a good idea here
  • too keep just the tweening code in PathTween, I generate some code(getClips()) to make target movieclips here. Feel free to add this kind of functionality in PathTween as you need it.

Then I opened up a fresh fla file and:

  • pasted the clip that was already in the clipboard (a few thousand lines of code)
  • copied the code from the Output Panel and pasted it after "TweenPlugin.activate([BezierPlugin]);"

You can see the result and get the fla.

So far, as3swf is cool once you've got the fla ready with pasted illustrator paths, could be faster since as3swf works with bytes. What I like about the FXG approach:

  • you skip the step where you paste the graphics into an fla, you just save a copy as FXG. You can use one fla to generate all the code you need, just change the path to the fxg file you want to animate.
  • each path is parsed individually, so this is a bit more flexible.
  • although it generates more code than the as3swf version, cubic beziers, arcs and other curves are broken into lineTo commands which even out the animation a bit.

This actually got fun, with individual timelines, so I made yet another copy that draws some cheesy trails into a bitmap data.

Here's the PathTweenTracer class, like the previous, place this in the parser package. Again, the PARSERS constant needs to be updated inside FxgFactory:

private static const PARSERS:Array = [  Graphic , Group , Library, 
                                                PathTweenTracer , Ellipse, Rect, Line, 
                                                BitmapGraphic, BitmapImage, 
                                                TextGraphic, RichText ];

The timeline code is pretty much the same. The result looks nice (source)

Here are some screenshots of the generated animation: fxg anim 1

fxg anim 2

fxg anim 3

Commented out TextGraphic.as

The FXG approach would fit the question better('Using pure actionscript, how can I move a symbol following each of the lines, without using predefined Motion Guides ?')

As for the nested question('Are these drawing objects accessible through AS3 or I should convert them to symbols / whatever format necessary ?' ) :

As @Casey mentioned, you cannot access the graphics once they're set. Using the updated Graphics API you can copy graphics from one Graphics instance into another, but that doesn't expose the commands. I remember Tink had something way before Flash Player 10, but I don't know what's the progress on that.

HTH

George Profenza
Wow, thanks mate, this was an exhaustive and proper answer. Thank you very much!
Jauzsika
@Jauzsika Glad I could help and thanks for the rep.
George Profenza