DisplayObject Quirks and Tips

08.28.2010

After having worked a great deal on the Rain SVG library, I’ve come to learn and re-learn some of the quirks and workarounds of DisplayObject, the fundamental UI class in ActionScript. For your benefit and mine, here they are. It’s only a short list of the many so if you’d like to add on, feel free to post a comment.

CacheAsBitmap with a mask breaks mouse events

If you have a sprite that has a mask and cacheAsBitmap set the true, the display object will no longer dispatch mouse events.

In this example, I’m using a PNG that’s partially transparent as a mask and it’s masking a black square. This type of mask only works when cacheAsBitmap is set to true for both the mask and the maskee. The result is a working mask, but the maskee will no longer dispatch mouse events.

[Embed(source="books.png")]
protected var Books:Class;
 
public function CacheAsBitmapAndMask()
{
	var books:Bitmap = new Books();
 
	var square:Sprite = new Sprite();
	square.graphics.beginFill(0x000000);
	square.graphics.drawRect(0, 0, books.width, books.height);
	square.graphics.endFill();
 
	square.addChild(books);
	square.mask = books;
	square.cacheAsBitmap = true;
	books.cacheAsBitmap = true;
	addChild(square);
 
	square.addEventListener(MouseEvent.CLICK, clickHandler);
}
 
protected function clickHandler(event:MouseEvent):void
{
	trace('click');
}

Filter and a mask breaks mouse events

At its core, this quirk seems to be the same as the previous one. In this example I have a shape masking a sprite and the sprite has a drop shadow. It’s important to remember that adding a filter to a display object will automatically set cacheAsBitmap to true. The display object again will not dispatch mouse events.

public class FilterAndMask extends Sprite
{
	public function FilterAndMask()
	{
		var mask:Shape = new Shape();
		mask.graphics.beginFill(0xff0000);
		mask.graphics.drawRect(25, 25, 50, 50);
		mask.graphics.endFill();
 
		var maskee:Sprite = new Sprite();
		maskee.graphics.beginFill(0x000000);
		maskee.graphics.drawRect(0, 0, 100, 100);
		maskee.graphics.endFill();
		maskee.filters = [new DropShadowFilter()];
 
		maskee.addChild(mask);
		maskee.mask = mask;
		addChild(maskee);
 
		maskee.addEventListener(MouseEvent.CLICK, clickHandler);
	}
 
	protected function clickHandler(event:MouseEvent):void
	{
		trace('click');
	}
}

See https://bugs.adobe.com/jira/browse/FP-61 and https://bugs.adobe.com/jira/browse/FP-3818 for the bug reports.

You can work around this issue by wrapping the maskee in another sprite. Rather than setting the filter on the maskee, set it on the wrapper. Here’s an example:

public function FilterAndMaskWorkaround()
{
	var mask:Shape = new Shape();
	mask.graphics.beginFill(0xff0000);
	mask.graphics.drawRect(0, 0, 100, 100);
	mask.graphics.endFill();
 
	var maskee:Sprite = new Sprite();
	maskee.graphics.beginFill(0x000000);
	maskee.graphics.drawRect(0, 0, 100, 100);
	maskee.graphics.endFill();
	maskee.addChild(mask);
	maskee.mask = mask;
 
	var maskeeWrapper:Sprite = new Sprite();
	maskeeWrapper.addChild(maskee);
	maskeeWrapper.filters = [new DropShadowFilter()];
 
	addChild(maskeeWrapper);
 
	maskee.addEventListener(MouseEvent.CLICK, clickHandler);
}
 
protected function clickHandler(event:MouseEvent):void
{
	trace('click');
}

CacheAsBitmap breaks concatenatedMatrix and hitTestPoint()

When an ancestor of a given display object has cacheAsBitmap set to true, the display object’s concatenated matrix is incorrect. This also affects the validity of hitTestPoint() and probably some other functions as well.

In this example the ancestor has a filter which in turn sets cacheAsBitmap to true. When clicking on the child, the concatenated matrix reports tx=1 and ty=1. In this case, tx and ty are incorrect.

public function CacheAsBitmapConcatMatrix()
{
	var ancestor:Sprite = new Sprite();
	ancestor.x = 50;
	ancestor.y = 50;
	ancestor.filters = [new DropShadowFilter()];
 
	addChild(ancestor);
 
	var child:Sprite = new Sprite();
	child.x = 25;
	child.y = 25;
 
	var g:Graphics = child.graphics;
 
	g.beginFill(0x00dd00);
	g.drawRect(0,0,200,200);
	g.endFill();
 
	child.cacheAsBitmap = true;
	child.addEventListener(MouseEvent.CLICK, child_clickHandler);
 
	ancestor.addChild(child);
}
 
protected function child_clickHandler(event:MouseEvent):void
{
	var child:DisplayObject = DisplayObject(event.target);
	trace(child.transform.concatenatedMatrix); 
}

The workaround is fairly simple:

protected function getConcatenatedMatrix(source:DisplayObject):Matrix
{
	var concatenated:Matrix = source.transform.concatenatedMatrix.clone(); 
	var p:Point = source.localToGlobal(new Point(0, 0)); 
	concatenated.tx = p.x; 
	concatenated.ty = p.y;
	return concatenated;
}

See https://bugs.adobe.com/jira/browse/FP-121 for the bug report. This post is also helpful: http://www.sephiroth.it/weblog/archives/2008/03/cacheasbitmap_hell.php.

Mouse events dispatched for transparent portions of bitmaps

Add a bitmap with some transparent pixels to a sprite. If you then click the tranparent portions of the bitmap, the sprite will dispatch a click event just as it would if you clicked opaque portions. This is often not desired behavior as you may want the click to “fall through” to whatever display object is behind it. There’s no easy way to toggle this functionality either. Most workarounds are limited by the other quirks mentioned so far or are not dynamic enough for general use. The best workaround I’ve used so far is InteractivePNG created by Moses Gunesch.

Possibly unexpected dimensions after rotation and scaling

Take a 100×100 square and rotated it 45 degrees. What’s the width of the shape? In Flash-world, it’s 141.4, or in other words, the “bounds” of the rotated rectangle is 141.4 pixels across. What if I want to access the unrotated width again? For one, you can set the rotation back to 0 and then request the width. That’s generally not the greatest option. Another option is to get the bounds of the square within its own coordinate space. Here’s an example:

public function RotatedDimensions()
{
	var square:Shape = new Shape();
	square.graphics.beginFill(0xff0000);
	square.graphics.drawRect(0, 0, 100, 100);
	square.graphics.endFill();
	square.rotation = 45;
	addChild(square);
	trace(square.width); // Traces 141.4
 
	var internalBounds:Rectangle = square.getBounds(square);
	trace(internalBounds.width); // Traces 100
}

The best way I can describe what getBounds() is doing here is that it’s seeing the square without regard to its scale or rotation within its parent. It’s important to understand what this does when scale is introduced. Let’s take this example:

public function RotatedScaledDimensions()
{
	var square:Shape = new Shape();
	square.graphics.beginFill(0xff0000);
	square.graphics.drawRect(0, 0, 100, 100);
	square.graphics.endFill();
	square.scaleX = square.scaleY = .5;
	square.rotation = 45;
	addChild(square);
	trace(square.width); // Traces 70.7
 
	var internalBounds:Rectangle = square.getBounds(square);
	trace(internalBounds.width); // Traces 100
	trace(internalBounds.width * square.scaleX); // Traces 50
}

In this case the square’s been scaled down to half its original size and is rotated 45 degrees. If you ask for the width, flash reports 70.7. If you get the dimensions of the square within its own coordinate space, it will report 100 for the width. In other words, these are the unscaled, unrotated dimensions of the square. The third trace is an example of how to get the scaled but unrotated width of the square.

Invisible children contribute to parent’s dimensions

Create a child shape, set it to be invisible, and add it to a sprite. Even though the child is not visible, its dimensions still contribute to the parent’s dimensions. This may or may not be what you’re expecting, but there’s no way to easily toggle the inclusion of invisible children when calculating a display object’s dimensions.

Take this example:

public function InvisibleBounds()
{
	var container:Sprite = new Sprite();
 
	var left:Shape = new Shape();
	left.graphics.beginFill(0xff0000);
	left.graphics.drawRect(0, 0, 50, 50);
	left.graphics.endFill();
	container.addChild(left);
 
	var right:Shape = new Shape();
	right.graphics.beginFill(0xff0000);
	right.graphics.drawRect(0, 0, 50, 50);
	right.graphics.endFill();
	right.x = 50;
	right.visible = false;
	container.addChild(right);
 
	trace(container.width); // Traces 100
}

Even though the child on the right is invisible, the container still includes it in its dimensions.

See https://bugs.adobe.com/jira/browse/FP-741 for the bug report and some workarounds that work well but can be slow.

I hope that helps anyone running into the same issues. Please post a comment if you have quirks or tips of your own.

Tags: , , , , , , , , ,


Comments

09.09.2010 / AaronHardy.com :: For all your Aaron Hardy needs. » Blog Archive » CabMaskableSprite: CacheAsBitmap and Mask Workaround said:

[...] DisplayObject Quirks and Tips, I described a quirk where a sprite with both a mask and a filter would cease dispatching mouse [...]

09.12.2010 / Filippo Gregoretti said:

Nice post,
I would add the impossibility to retrieve stage dimensions of a loaded swf.
At least I didn’t find a way to.
light
Filippo

09.13.2010 / Claus Wahlers said:

Filippo,

you get the stage dimensions of a loaded SWF from Loader::contentLoaderInfo:

https://gist.github.com/ed6bc89a28d1934f8ca0

Cheers,
Claus.

09.13.2010 / Filippo Gregoretti said:

@Claus,
I wish it was so simple :)
thats the width of a loaded movieclip. Not the size of the stage of that movieclip. If something goes out of the stage area, width will return a higher value that stageWidth itself…
cheers for the suggestion
F

09.13.2010 / Claus Wahlers said:

@Filippo uhm.. no, it doesn’t.. it is the stage size, no matter how big the content is. Did you actually tried it?

09.13.2010 / Filippo Gregoretti said:

Claus,
I did actually, had very bad results…
It was 1 year ago at the startup of a project targeting player 9 and I spent some time trying to solve this with other developers… there was no way… at least we couldn’t find one…
Actually retrieving width and height of the loaded swf was the most logic thing to do, but it didnt work… Even masking the entire content inside the loaded swf didnt work.
The only thing we could do was to place a _bg clip on the bottom of the loaded one, the exact dimensions of the stage, and retrieve loaded._bg dimensions.
Maybe something was upgraded in player 10.1? not sure…
Anyway I faced this issue and we are still producing content for that application using _bg.width
Some wrappers such as Zinc and SWFStudio allow to retrieve it, so probably decoding bytecode of loded swf there can be a way to find it…
Our content has several graphic assets, and masked content that need to bleed out of the stage, and however we tried it always returned the width of the elements contained in the page, not the stage…
maybe on player 10.1 now it works?
I don’t have really time to give it a try now :)
will do eventually…
thanks!
F

09.13.2010 / Claus Wahlers said:

Are you sure you looked at width/height in LoaderInfo? Maybe you looked at width/height in Loader, which indeed hold the size of the content. LoaderInfo has the size of the stage. Has always been like that afaik.

09.13.2010 / Filippo Gregoretti said:

mhhh possibly… maybe we looked in the wrong place?
that would be fun :)
thank you

10.04.2010 / hnb said:

Any workaround for the first quirk (“CacheAsBitmap with a mask breaks mouse events”)?


Leave a Comment

Your email address is required but will not be published.




Comment