Wire beetle animation : shape object in JSFL api
by ericlin

This is an excercise about the shape object defined in JSFL API.

Basicaly, I retrieve the edges data out of the beetle shape symbol then draw it on a blank background one edge per frame. After all the edges are drawn, I attach a colorful beetle and alpha tween it out.


About the shape object (contours, vertices and edges)

There are 3 main properties defined in shape object : the contours, vertices and edges. Contours is an array of contour object which represent a region or area. Vertices is an array of vertix object which represent the coordinate x,y of a nodal point. Edges is an array of edge which represent a drawing segment. Check the movie below:  

The first shape is just a red square. It defines two regions, the red region and a blank region outside of the red. We say the shape defines 2 contour. There are 4 corners. We say the shape defines 4 vertices. To connect these 4 cornors, we draw 4 sides. We say the shape defines 4 edges.

The second shape is divided in the middle into one red and one blue rectangle. It defines 3 regions, the red region, the blue region and a blank region out side of those colored region. We divided it in the middle. By this, 2 new nodal points are created and the total vertix points counts 6.  To connect those 6 points to form our shape, we need 7 lines. We say that shape defines 7 edges. At the left part, there are 3 edges between the red contour and the blank contour. At the middle, there is one edge between red contour and blue contour. At the right part, there are 3 edges between blue contour and blank contour. So, each edge is "between" two defined contours, that is why one edge contains two "halfEdges" with each halfEdge in its own contour.

The third shape is the result of further dividing from the second shape. There are 5 contours and 9 vertices. Could you count how many edges there are ?  The answer is obviously 12. Among them 8 owns the halfEdge of blank contour, 4 are between different colors.


The controls of the edge object

To create a movie about wire beetle, we need the edges of the shape. The edge object stores the coordinate x,y data of 3 control points. The first is the starting point, the second is the control point and the third is the end point. This can be translated into action script as:

moveTo(pt0.x, pt0.y); 
curveTo(pt1.x,pt1.y,pt2.x,pt2.y);

This part is quiet easy. Before we animate the drawing, we try draw it in one step. Lets try this JSFL file:

// with a shape selected
fl.trace("lineStyle(0.1);");
var elt = fl.getDocumentDOM().selection[0];
elt.beginEdit();
for (i=0; i<elt.edges.length; i++) {
	var ed = elt.edges[i];
	var pt0 = ed.getControl(0);
	var pt1 = ed.getControl(1);
	var pt2 = ed.getControl(2);
	fl.trace("moveTo("+pt0.x+","+pt0.y+");");
	fl.trace("curveTo("+pt1.x+","+pt1.y+","+pt2.x+","+pt2.y+");");
}
elt.endEdit();

If we select a shape and run this JSFL file, we get many moveTo and curveTo command traced out in output panel. Copy those script into frame 1, and we see wire outline  of our shape.


The layers

The JSFL script shown above only parse the selection[0]. If we apply this commands to a shape, the result might show only a part of the shape. When our selection of the shape contains grouped elements or sub-shapes distributed in layers, then they are elements selected. We need to parse all the elements in selection array.

So, lets modify our JSFL , to loop all elements. :

var sel=fl.getDocumentDom().seletion;
for(var i=0;i<sel.length;i++){
    var elt=sel[i];
    // .....  do the parse script for shape element here
}

Well, we get a marked improvement now. Most outlines of the shape are drawn out.


The grouped elements

Unfortunately, we noticed that, several elements such as "antena" and "white spots" are drawn in wrong places. These incorrectly drawn elements are the elements that get grouped.

grouped elements are treated as a whole unit similar to a symbol. They gets their own coordinates relationship. They can be skewed, scaled and moved. The appearance and position get changed but the stored coordinates of vertices of the shape remains no changed. So, we get to peek the element.matrix to correct our data. This correction is similar to get the coordinates from  localToGlobal function.

Grouped elements has a property : matrix. The fields in matrix object are a,b,c,d,tx,ty;

So to get the real position for some point x,y in grouped elements, the function  localToGlobal is :

pt.x=x*scaleX+y*skewHorizontal+transitionX;
pt.y=y*scaleY+x*skewVertical+transisionY;

or

pt.x= x*matrix.a+y*matrix.c+matrix.tx;
pt.y= y*matrix.d+x*matrix.b+matrix.ty; 

All right, all things are done. Of-course, here we see a limitation, our localToGlobal function only work for one level. We have assume that, only simple shape are grouped. If the grouped elements contain grouped elements then we need to do recursive call. In our example, I did not attemp to solve this complex situation.


The animation

We can draw the wire beetle in one step. The appearance shall be the same as it is viewed in outline mode. This is not what we want. We want an animating effect showing the drawing steps.

The simplest way would be walk through shape.edges, retrieve coordinates x,y of the control point , push them sequentially into an array. Then create a movie to draw edges sequentially.

I did it. However, something does not satisfy me. The sequence of edges in Shape object is a little bit bizzare. Flash create the sequence of edges in the most economical way. For a concentric multiple rings, Flash might draw first part of the arc in the inner most ring and the second, third till the outmost ring and then back to the inner most ring to draw another part of the arc. It appears like drawing a piece of pie and then another piece of pie and at last the result of concentric rings comes out. 

No, I dont like it. I would rather it draw one contour then another contour. For a concentric rings, I would rather draw a complete inner most ring and then complete second ring , then the third until the out most ring. Such animation would be more acceptable.

So lets go back to study the contour object. Contour object contains sequential halfEdges. These halfEdges are not stored sequentially in an array. But we can use halfEdges.getNext() to get the following halfEdge. HalfEdge object does not store the coordinates data in it, so we get to call halfEdge.getEdge() to obtain the control points from edge object.

We modify the script and get the effect we want. However, the control point data is double sized because each Edge gets called twice by its halfEdges. When we implement that script, each edge is drawn twice. So, we need a function to screen out what edges we have drawn already. 

Usually we may set a flag in Edge object, so that if the flag is true, we omit the control points retrieving process. Unfortunately, Edge class is not a dynamic one. Adding a custom property to Edge object is forbidden. So, I adopt a brutal way. Each time I retrieve the data from some Edge, I save the Edge.id to an array. When I access and Edge, I would check that array for existing id or not.

OK, this is nearly the whole job.

In the final script, I also round the coordinate into TWIP units. That shortens the array data in actionscript panel.


The final JSFL to retrieve edges in a shape

// with a shape selected
ptArray = [];
doneEdge = [];
fl.trace("//---------------------------");
function isDrawn(id) {
	for (var k = 0; k<doneEdge.length; k++) {
		if (doneEdge[k] == id) {
			return true;
		}
	}
	return false;
}
sel = fl.getDocumentDOM().selection;
for (var n = 0; n<sel.length; n++) {
	var elt = sel[n];
	if (elt.elementType != 'shape') {
		continue;
	}
	var mat = elt.matrix;
	elt.beginEdit();
	for (i=0; i<elt.contours.length; i++) {
		var cont = elt.contours[i];
		var he = cont.getHalfEdge();
		var startId = he.id;
		var id = 0;
		while (id != startId) {
			var ed = he.getEdge();
			if (!isDrawn(ed.id)) {
				doneEdge.push(ed.id);
				for (var j = 0; j<3; j++) {
					var pt = ed.getControl(j);
					if (elt.matrix) {
						var ptx = pt.x;
						var pty = pt.y;
						pt.x = Math.round((ptx*mat.a+pty*mat.c+mat.tx)*20)/20;
						pt.y = Math.round((pty*mat.d+ptx*mat.b+mat.ty)*20)/20;
					}
					ptArray.push(pt.x, pt.y);
				}
			}
			he = he.getNext();
			id = he.id;
		}
	}
	elt.endEdit();
}
//fl.trace(ptArray);¡@

download files used in this article

email: ericlin@ms1.hinet.net