Tutorials, extensions, and source files for ActionScript, Flash, and other Adobe products.

 

Pages: 1 | 2 | 3 | 4 | 5

Shapes As Utilities

The basic task for an Auto Shape is to provide you with a customizable visual for your design or composition. This is a pretty common theme throughout all Auto Shapes, at least those covered up to now. They don't necessarily have to be all about their own visuals, however. Thanks to their interactive capabilities, they can be created and used as utilities too.

Auto Shape utilities don't provide imagery (at least not imagery you'd use in a design) but, rather, act more like Fireworks custom commands or panel extensions. They let you perform actions within Fireworks that help you manage other elements within your document or possibly even provide new ones for you. At any time an Auto Shape utility is of no more use, it can be easily deleted or hidden as to get it out of the way of your final design. As an example, we'll go through the creation of an element matcher Auto Shape. This utility shape will help you resize and position elements within your document to match another element targeted by the Auto Shape.

 

The Element Matcher shape will consist of a path element for shape, 2 control points and 2 text elements to label those control points. The first control point is the Get Size control point. What this does is "gets" the size of your current selection and sets the size of the utility, or, really, the its preview area, to match area defined by that selection The second control point is the Set Size control point. It takes your current selection and sets all elements within that selection to match the location and size defined by the Matcher shape. The idea is to be able to easily match locations and sizes of elements within your document with great flexibility and minimal hassle.

Now, because the Element Matcher shape works with selections, it too needs to be selected in order for the user to actually be able to use one of its control points since control points are only available for a shape when its selected. A small problem arises due to this, however. Since we want the shape to react to other elements in a selection, it needs de-select itself before it can work on that selection. Doing so is a two step process. First, the shape will need to be able to know when an element within the selection is itself. I've chosen to use a customData property for this, more specifically, the property isMatcherShape will be created and defined as true for the shape's customData object to identify the shape as being an Element Matcher. Next it will need to be able to alter the current Fireworks selection so that it doesn't exist within it. For the shape, this will be achieved with the following function.

RemoveFromSelection = function(){
	var sel = new Array();
	for (var i=0; i<fw.selection.length; i++){
		if (!fw.selection[i].customData.isMatcherShape){
			sel.push(fw.selection[i]);
		}
	}
	fw.selection = sel;
}

Here a new array, sel, is created to be a copy of the array in Fireworks representing the current selection (fw.selection). A loop is then used to go through each element in the selection checking to see if it has a valid true isMatcherShape. If not, the shape is added to the would be copy. Basically, any element in the selection that doesn't have a isMatcherShape set as true is assigned to the sel array. When finished cycling through all the elements in the selection, the copy, now void of any Matcher shapes, is assigned to replace the current selection effectively removing any Element Matcher shapes from the selection.

Note: due to the way interaction is handled through control points of Auto Shapes, the shape will be re-selected after the command is run.

After Fireworks's selection has been managed as needed, its then only a matter of calling your basic, run-of-the-mill Fireworks commands to take handle the remaining actions of the Element Matcher shape (positioning and resizing the selection).

Here is the final code.

Auto Shape: Element Matcher [ Show/Hide Code | Download ]

To use the shape, first select the shape along with one or more elements to retrieve a size from. Then click on the Get Size control point allowing the Element Matcher to obtain the size of that selection. Next, select the shape along with other other elements you wish to match that size obtained. Then, click Set Size and those elements will be placed within the area initially obtained from Get Size.

Most of what you see in the code is pretty standard for shapes. Target is the function that sets the shape around the selection (notice how grouping was used to more easily find the bounds of a selection). Once sized, and Set is clicked, a loop runs through all the other elements selected and resizes and moves them to the area defined by the shape.

One thing to watch out for with the Element Matcher shape is that you shouldn't allow the shape itself to be scaled or resized outside of its own Get Size behavior. The reason for this is that Auto Shapes have their own coordinate space when transformed--one separate from the main document but matching it when the shape is created. This is what lets you normally scale Auto Shapes to different sizes but still have programmed values within that shape maintain proportion within. Because the Element Matcher works with elements outside of its own coordinate space--those in the main document--it would need to retain a similar coordinate scale. Otherwise, when the shape is scaled, the results of using Set Size and Get Size will be distorted. Fireworks does provide some functions, globalToSmartShapeCoords and smartShapeToGlobalCoords, to help you convert locations within differing coordinate spaces like this, but it's easier just to not scale the shape and not have to worry about it.

 

How about another utility? This utility, called Chained Selection, will be tool that will let you position selected elements in a chain-like manner allowing you to change the position of one part of the chain and have all following "linked" parts react to that reposition similarly. As far as this utility goes, think of it more as an arm joint. You have your shoulder or base of the arm followed by the elbow and then the wrist or hand. It will provide an arm of that nature as a shape and will let you associate elements with those joints. Move those joints and the elements will follow.

Like the Element Matcher from before, this shape will make use of Fireworks selections, setting and applying actions to them. In doing so it will also make use of the customData object, but in a new, slightly more complicated way. Not only will the shape provide itself with a customData variable from which it identifies itself, but it will also need to assign customData properties to other elements within the document in order to associate those elements with a point (control point) in the chain. Elements associated in this manner will have a customData property that associates that element to not only the Chained Selection Auto Shape, but also to a specific point in the chain. That way, when the shape is altered, elements for each point in the shape can be found based on that custom data, be selected, and then move to match the new position specified by the chain position.

This shape also introduces the use different control point types. By default, all control points are yellow diamonds. They can also look like a blue diamond (inverted version of the default) and a cross hair. Here, the type is changed when a control point is associated with elements within the document to indicate its being in use.

One more addition is the use of checking for modifier keys during control point interaction. Before, with the Hollow Box Auto Shape, we've seen how keys like SHIFT could be use to constrain control point movement resulting from a register move function. This shape checks for a modifier key during BeginDragControlPoint and reacts based on that, registering control points for move or maybe doing something else. Chained Selection here uses the CTRL/CMD key to distinguish between actions for control points. If the CTRL/CMD key is down, smartShape.ctrlCmdKeyDown is true and, as specified in the code for this shape, anything else selected with the Auto Shape will be associated with that point. That is unless something is already associated with that point. If there is, then it is un-associated and released from the shape.

Note that CTRL/CMD is not needed to make the control point behave like a button. Here, I chose it because it makes it a little more difficult to accidentally add or remove element association. You could alternatively use no modifier keys at all just distinguish between normal control point movement and a simple click by checking to see if the position of the mouse has changed between BeginDragControlPoint and EndDragControlPoint. You can do this by comparing smartShape.currentMousePos (mouse) and smartShape.mouseDownPos.

operation.EndDragControlPoint = function(){
	if (smartShape.currentMousePos == smartShape.mouseDownPos){
		// control point clicked, not moved
	}
}

For this shape, though, the CTRL/CMD distinction alone makes this unnecessary. Here's the script.

Auto Shape: Chained Selection [ Show/Hide Code | Download ]

Test it out by creating another element in your document. This can be a rectangle, a circle, another Auto Shape, or even just a scribble with the pencil tool; just something to test with. Then select both the Chained Selection shape and the newly created element. Using CTRL/CMD, click on the right-most control point of the shape (it should be the one at the end of the "chain"). this should associate the other selected element(s) with that point and the point should become inverted. Next, without using any modifier keys, try moving the shape around using its control points. The associated element(s) should follow.

At present, this shape only contains 3 points to make up the chain. Also, the points are restricted to the same default distance away from the node before. There is no (currently) way to make that gap longer or shorter which could be restricting. Those issues can be resolved with some modifications.

First: adding more points. Though it may seem hard, it's really not that difficult. Just throw one more control point onto smartShape.elem.controlPoints (aka cps) and you got it. The difficult part in that is probably figuring out how the user intends to do that. Normally, this how is from the ALT/OPTION key. Like CTRL/CMD, this can easily be detected using a property. That property, smartShape.altOptKeyDown. With a little more delegation in where control points are clicked (BeginDragControlPoint), this can be checked and a new control point can be created on the end of the Chain Selection shape by the user using ALT/OPTION. But, let's not forget about removing points too. What goes up must come down; what gets created must get deleted. In order to make that a little harder for the user to accidentally do, ALT/OPTION in combination with CTRL/CMD can be used for for point deletion. To make things a little easier on us, these actions will only be performed from the last control point

Now, how about adjusting chain length? This can be handled on an individual point basis. In other words, the distance between points in the chain don't have to be uniform; one can be 10 pixels away from the point before it and the next can be 100. Still, we need to be able to know when to allow for this. Selection of modifier keys are running a little short, but there's still SHIFT left (which can be checked with smartShape.shiftKeyDown). Using SHIFT, the user can extend a point outward from its previous point to extend the length of the chain at that location. Now, how I want this to work is, I want a point moved in this manner to move in a linear fashion away from the point before it. This way rotation isn't changed. But, given that we are on a chain here, points following the point being moved would also need to move to so that their relation with each other is not corrupt. RegisterLinearMove is obviously a good candidate for this. Each point following the point being moved, however, would need to have developed for them individual points of reference so that they can move in the same manner (otherwise they would all extend outward from the same joint).

One last addition to make the shape more usable will be dynamic tool tips can be added to help with the interaction. Here is the final script.

Auto Shape: Chained Selection 2 [ Show/Hide Code | Download ]

Try it out for yourself.

 

Working With Circular Curves

Sometimes the most trying aspect of developing an Auto Shape can be figuring out how to translate a shape's path into JavaScript code. This becomes more difficult when the shape contains circular curves (or, really, any kind of curve). The more you work with them, the more you develop a feel for how they work and the more you start to develop techniques (and functions) to tackling problems in using them.

Fireworks uses cubic bezier curves for its paths. This means each node in a path consists of the node point itself and 2 control points (not to be confused with Auto Shape control points). These control points determine line curvature as one point connects to another. To create circles with those curves you would need to place node control points at specific locations so that the resulting curves have a consistently round shape. Circles with cubic bezier paths consist of 4 nodes, each evenly distributed along the circle's circumference.

The control points extend outward by a length based on the radius of the circle, or more specifically about 55.2% of the length of the radius. This value is more accurately provided by Fireworks in the decimal variable fw.ellipseBCPConst.

With that, we can accurately create a circular path. Here's an example of a function that would do that for you in an Auto Shape.

CreateCircleContour = function(elem, origin, radius){
var last = elem.contours.length;
elem.contours[last] = new Contour();
elem.contours[last].isClosed = true;
var nods = elem.contours[last].nodes;
nods.length = 4;
var cRadius = radius*fw.ellipseBCPConst;
SetBezierNodePosition(
nods[0],
AddPoints(origin, {x:-cRadius, y:-radius}),
AddPoints(origin, {x:0, y:-radius}),
AddPoints(origin, {x:cRadius, y:-radius})
);
SetBezierNodePosition(
nods[1],
AddPoints(origin, {x:radius, y:-cRadius}),
AddPoints(origin, {x:radius, y:0}),
AddPoints(origin, {x:radius, y:cRadius})
);
SetBezierNodePosition(
nods[2],
AddPoints(origin, {x:cRadius, y:radius}),
AddPoints(origin, {x:0, y:radius}),
AddPoints(origin, {x:-cRadius, y:radius})
);
SetBezierNodePosition(
nods[3],
AddPoints(origin, {x:-radius, y:cRadius}),
AddPoints(origin, {x:-radius, y:0}),
AddPoints(origin, {x:-radius, y:-cRadius})
);
}

Note that this function uses SetBezierNodePosition and AddPoints.

For any path element passed into CreateCircleContour a new contour is created in the shape of a circle at location origin with a radius of radius. You can also use this method for creating rounded corners for rectangular objects The difference would be that you'd only use one-quarter of a circle for each corner (2 nodes). We can create a quick Auto Shape to show this.

 

A shape that may be able to show both a circle and a round corner might be an iconic figure not unlike those seen on bathroom doors. A circle would represent the head while rounded corners could be used to determine shoulder broadness. We'll add control points to affect both the circle and the rounded corners.

The fw.ellipseBCPConst variable will play an important role in this shape, not only in creating the curves, but in moving them too. Because fw.ellipseBCPConst is a decimal percentage, it works perfectly with register move parameters like deltaXtoX since those values are based on 1. Moving the main point of a node by a factor of 1 and its predecessor and/or successor by a factor of fw.ellipseBCPConst would move the point perfectly to maintain circular proportion. You sill see this in both instances of moving either of the control points present in this shape.

What this means, however, is that moving a single node would require two types of movement, one for its main point and one for one or both of its control points. But, register move functions can only be used on a whole node and not just one of those points that make up a node. That won't be a problem thanks to movePt, movePred, and moveSucc, register move parameters properties that let you specify what parts a register move function will operate on. Additionally, though maybe not otherwise apparent, you would need to use a register move function twice on the same point to be able to move its parts separately. Using a register move function on a single point twice, will not cause the second to overwrite the first. Instead, both behaviors will be applied. This way, the main point of a node can be moved one way (a ratio of 1:1 for example) and one or both of the control points another (1:fw.ellipseBCPConst). Figuring out these ratios and differences in movement really is the hardest part of this particular shape. Just see for yourself.

Auto Shape: Figure Icon [ Show/Hide Code | Download ]

Movement for the Head control point is the most straightforward. When dragged, it moves the top normally and only the predecessor and successor points of the left and right nodes of the circle respectively. The shoulders are a little more complicated as horizontal movement is being translated in left and right as well as up and down and doing so with 4 different nodes (with the control point) each of which need two register move functions called for them to move both their main point and a predecessor or successor.

You'll notice that RegisterMove was used instead of RegisterLinearMove too. Given the circumstances of the movement, especially for the shoulders, it was just easier since movement was strictly horizontal or vertical with the need to translate that horizontal to a synchronous vertical.

 

Some circular segments may not fit so well into the nodes of a circular path as a rounded corner does. For instance, what if you need a curve that looks like this?

Some extra calculating will be needed to get the curves necessary to fit such a segment, or really any segment which may have variable locations for its start and end point. I've created the following function to ease the pain of doing just so.

AddCircleSegment = function(contour, origin, radius, startAngle, endAngle){
	var arc = endAngle - startAngle;
	if (!arc) return false;
	else if (arc < 0){
		arc = -arc;
		var stored = endAngle;
		endAngle = startAngle;
		startAngle = stored;
	}
	var pos = contour.nodes.length;
	if (pos == 1) pos = 0;
	var rad90 = Math.PI/2;
	var div = arc/rad90;
	var steps = Math.floor(div);
	var rem = arc - steps*rad90;
	var cRadius = radius*fw.ellipseBCPConst;
	var angle, cAngle, x, cx, y, cy, pt, pred, succ;
	for (var i=0; i<=steps; i++){
		contour.nodes[pos+i] = new ContourNode();
		angle = startAngle + i*rad90;	cAngle = angle + rad90;
		x = Math.cos(angle)*radius;	cx = Math.cos(cAngle)*cRadius;
		y = Math.sin(angle)*radius;	cy = Math.sin(cAngle)*cRadius;
		pt = {x:origin.x + x, y:origin.y + y};
		pred = {x:pt.x - cx, y:pt.y - cy};
		succ = {x:pt.x + cx, y:pt.y + cy};
		if (steps && !i) SetBezierNodePosition(contour.nodes[pos+i], pt, pt, succ);
		else if (steps && i < steps) SetBezierNodePosition(contour.nodes[pos+i], pred, pt, succ);
		else{
			if (rem){
				var remRadius = cRadius*rem/rad90;
				cx = Math.cos(cAngle)*remRadius;
				cy = Math.sin(cAngle)*remRadius;
				succ = {x:pt.x + cx, y:pt.y + cy};
if (steps) SetBezierNodePosition(contour.nodes[pos+i], pred, pt, succ);
else SetBezierNodePosition(contour.nodes[pos+i], pt, pt, succ); i++; contour.nodes[pos+i] = new ContourNode(); angle += rem; cAngle = angle + rad90; x = Math.cos(angle)*radius; cx = Math.cos(cAngle)*remRadius; y = Math.sin(angle)*radius; cy = Math.sin(cAngle)*remRadius; pt = {x:origin.x + x, y:origin.y + y}; pred = {x:pt.x - cx, y:pt.y - cy}; } SetBezierNodePosition(contour.nodes[pos+i], pred, pt, pt); } }
return true; }

Note that this function uses SetBezierNodePosition.

This function takes a contour in a path and adds to it the nodes needed to append a circular segment defined by the origin, radius, startAngle, and endAngle arguments. Note that, unlike the CreateCircleContour function, this adds points onto a path contour as opposed to creating a new contour to be used strictly for the curves created. Here is a simple shape that puts the function to good use.

Auto Shape: Circle Segment [ Show/Hide Code | Download ]

Extending on this, you can create your own shape that works with circular curves of varying arcs.

 

As one last example, here is a Rounded Polygon shape that lets you create a polygon with anywhere from 3 to 12 sides whose corners you can round.

Auto Shape: Rounded Polygon [ Show/Hide Code | Download ]

 

Bugs/Concerns

Rarely is any new feature like Auto Shapes in an application like Fireworks (from Macromedia) without its issues. The following list contains some warnings about some difficulties you may experience when designing Auto Shapes.

• Warning: JavaScript is case-sensitive; smartShape (a variable you will see a lot of later on) and SmartShape are two different variables. You will not be able to obtain the expected properties from the smartShape object if capitalization for its spelling is incorrect. This applies to all variable names as well as functions.

• Warning: if you attempt to use an undefined variable inappropriately your script will fail. For example, if you try to get the value from a variable that does not exist, an error will occur. The example above using SmartShape would be a prime example. Since SmartShape does not exist (unless you define it yourself, remember, the real smartShape object starts with a lowercase "s"), trying to access operation from is would result in an error.

var shape = SmartShape.elem; // error

• Warning: definitions (variables and functions) within a script remain active in Fireworks for as long as the application remains open. Once your Auto Shape JavaScript file is run by Fireworks, all variables and functions from that file actually get "remembered" by Fireworks and are retained in memory for as long as it remains open. This isn't normally a big deal but can be problematic in debugging your scripts if somewhere in your script you are using a variable you might have since deleted. Having used that variable earlier would define it within Fireworks. Deleting it from your script doesn't delete it from memory. If that script still takes advantage of that variable somewhere (some place you may have forgotten about though is important to the successful operation of your script), should you reuse the shape later after closing and reopening Fireworks, your script will fail--the variable will no longer be defined.

• Bug: RegisterPolygonMove has a bug where minRadius is not respected when the mouse is directly over the center point provided for the call. Instead of being constrained, the point will move to that center location. You can probably see this in the second Register Moves example.

• Annoyance: when a control point resides on the corner of the bounding box of the shape, the small bounding square representing that corner (which allows you to resize the shape) can interfere with the control point's tool tip. Most of the time, if a control point with a tool tip, and they all should have a tool tip, is on a corner, rolling over it will show the tool tip, but it will disappear much faster than other control points. In fact, other tool tips don't disappear at all until you move your mouse off of them (or drag the point if tool tip tracks drag is false). Take a hint from the supplied rounded rectangle shape and do your best to keep control points likely to sit on a corner off that corner.

Limitation: when an Auto Shape script is called by means of a SmartShapeEdited event, contours within your paths cannot be edited. You can create new path elements and you can modify contour nodes, but you cannot create new contours or add nodes to existing contours. This can be very frustrating if you want to think that you can redraw and fix an Auto Shape within that event as a result of the user deleting nodes or paths. It just isn't going to happen. This is also the reason you haven't seen it been used in any of the prior examples.

• Non-intuitive Annoyance: some elements or their properties can't be altered through a reference. A good example is text elements. If you want to move a text element by altering its rawLeft and rawTop properties, you cannot do so through a direct variable reference of that text element. Instead, you would at least have to be working off of a reference of the elements array containing that element. In other words, where you might be able yo use myTextElement.rawLeft = value; you instead have to use elements[textIndex].rawLeft = value; to alter that property. Same can be said about text and textRuns.

• Unexpected Behavior: When you set the visible property of a control point to true, it does only that, makes the control point invisible, or, actually, more like setting its opacity to 0. The control point, however, is still there and can even be clicked despite it not being able to be seen.

• Limitation: Most Fireworks's own functions won't be able to use objects other than generic point objects (generic objects with x and y properties) for passing point location values into the call. For instance, you're not able to pass in a control point object in a RegisterCircularMove function for the center parameter even though the control point is an object with x and y properties. You would need to create a new object and copy the control point's x and y properties into it and use that instead.

node.RegisterCircularMove(contolPoint, moveParms); // incorrect
node.RegisterCircularMove({x:contolPoint.x, y:controlPoint.y}, moveParms); // correct

• Limitation: because Fireworks uses generic objects for its point objects instead of having its own Point class, it has a peculiar behavior of automatically stripping a generic object of all other properties if/when that object contains both x and y properties. This means two things. First, no object used as a point can contain any other properties other than x and y. Secondly, if you are working with a custom generic object that contains many property values, you will need to be sure to not include both an x and y property into that object or all other properties will be deleted.

var obj = {prop:"value", x:100};
obj.y = 100;
obj.prop; // undefined

• Bug: array values stored into customData objects will be converted into a string the next time the document is saved and reopened and that value is re-accessed. Though rare that an Auto Shape would run into this, it can be frustrating and difficult to debug (especially since both arrays and strings have length properties). An example would be the array value [1,2,3]. If this was assigned to a customData object and the Fireworks document was closed and reopened, that value would have changed from [1,2,3] to "[ 1, 2, 3 ]". Luckily, this does not occur with generic object definitions, just arrays. So {x:100, y:100} would remain {x:100, y:100}.

Bug: using var nodes in a script can sometimes, under specific conditions, cause an error. This particular bug I think Macromedia created just in spite of me since it coheres so well to my particular style of coding. If you've noticed throughout all the examples, when a contours nodes were assigned to a variable name, that name was nods and not nodes. The reason for that is because of this bug. How it works is, if you declare or define a variable with the name "nodes" in a function defined in the style name = function(){}, and an event causes the Auto Shape script to run but no function is actually called, the script will result in an error. A quick and minimalist example would be:

anything = function(){
	var nodes;
}

Run that script as an Auto Shape and an error will occur. However, consider each of the following variations. None of these result in error.

anything = function(){
	var Nodes; // different variable name
}
function anything(){ // another style of function definition
	var nodes;
}
anything = function(){
	var nodes;
}
anything(); // a function is called

So, assuming I used var nodes to retain the value of a nodes array in a function such as operation.InsertSmartShapeAt (which I used to do before switching to nods), and some event like SmartShapeEdited causes that script to run but is not defined in the operation object resulting in the script being run without a function being called, the Auto Shape would produce an error. As far as I know, "nodes" is the only variable name that will cause this. As far as I know I'm the only person in the world who would be affected by it. From my experience, its a good idea not to have like names for variables anyway.

Continue reading »

Pages: 1 | 2 | 3 | 4 | 5