Visual Reflections in AS2
Sunday, November 23rd, 2008I’m currently working on a project where I’m using a bit of reflection, and although I found a couple of reflection classes on the web, I was looking for something that worked out of the box without too much hassle.
Basically I was looking for something that allowed me to do new Reflection(myClip) and be done with it 9 times out of 10.
Making a reflection is not that hard (although I need to work on the alpha and falloff, will post a new version later), but getting it to work right with scaled clips with weird registration points was a little harder.
Anyway I think I came up with something nice, and it performs quite well. You can download the source and a bunch of demo’s here Reflection demo’s
Here is a list of features:
A couple samples:
The code:
import flash.display.BitmapData;import flash.geom.Matrix;import flash.geom.Rectangle;/*** ReflectionManager manages the reflection for one clip.** Short feature set:* - simple setup by default. In most cases doing new ReflectionManager (sourceClip) will suffice.* - default setup is a clip that fades from 40 to 0 alpha at 40% of the image.* - you can change the falloff height, overall alpha, and the redraw interval (eg to mirror video)* - retrievable reflection and source clips to apply filters or otherwise control clips* - redraw interval is disconnected from framerate, specify a low redraw interval to save system resources* - works with scaled, non top left regpoints, and masked clip, using an autobounds detection algorithm* - works with negative scaled clips* - tries to warn you in most cases when the result is likely to be different from what you expect* - very high performance* - no dependencies on non flash classes** @author J.C. Wichman, TriMM Interactive Media (www.trimm.nl / j.c.wichman@trimm.nl),* ObjectPainters.com (blog.objectpainters.com / j.c.wichman@objectpainters.com)*/class nl.trimm.movieclip.Reflection {private var _sourceClip:MovieClip = null; //reference to the source you want to mirrorprivate var _sourceClipParentBounds:Rectangle = null; //the source bounds as interpreted by the parentprivate var _sourceClipLocalBounds:Rectangle = null; //the source bounds as interpreted by source itself//same as parentbounds for nonscaled, nontranslated clipsprivate var _reflectionClip:MovieClip = null; //contains _reflectionClipContent and _reflectionClipMaskprivate var _reflectionClipContent:MovieClip = null; //contains resultbitmap into which source reflection is drawnprivate var _reflectionClipMask:MovieClip = null; //contains alpha mask used as fade out effectprivate var _redrawMatrix:Matrix = null; //contains all transformation necessary to draw a scaled,//translated and flipped reflection of the source into the destprivate var _redrawRectangle:Rectangle = null; //specifies clip on redrawing methodprivate var _resultBitmap:BitmapData = null; //contains result of drawing source into dest through matrixprivate var _updateIntervalId:Number = null; //redraw interval, use null for static imagesprivate var _autoClear:Boolean = false; //auto clear the bitmap before redrawing/*** The purpose of this reflection class is to make it as simple as possible to provide a working reflection,* yet give you the freedom to adjust what's needed.** Restrictions:* - source and reflection share the same parent* - source should not be scaled, if you want to scale, either wrap the scaled clip, or scale the parent** @param pTarget the object you want to create a reflection for* @param pAlpha the alpha of the reflection 0..1* @param pHeight the height of the reflection, 1 is the whole image height is used as fall off, 0 is no reflection, and* everything in between (0 .. 1)* @param pFrameRate the framerate for the redraw method* @param pBounds explicit bounds that define the area to be reflected, if these are not passed, bounds are auto detected,* based on what's visible at the moment of creation. The bounds are relative to the source clips coordinate* system. In other words, IMAGINE you edit the source clip, draw a rectangle around the area you want to see* reflected, and write down those bounds, THAT area defines the boundsrectangle.* @param pCustomClip an empty clip on the same parent as pSource that you would like to display the reflection.* The only reason I can think of to do so, is that this will allow you to apply filters to this* clip through the IDE instead of filter classes, which might be convenient for non coders.* @param pAutoClear often it's not necessary to auto clear the canvas again before redrawing, UNLESS you are moving* your content, and your content is transparent. By default autoclear is off, but you might want* to enable it.*/public function Reflection(pSource:MovieClip, pAlpha:Number, pHeight:Number, pFrameRate:Number, pBounds:Rectangle, pCustomClip:MovieClip, pAutoClear:Boolean) {if (pAlpha != null && (pAlpha < 0 || pAlpha > 1)) trace ("pAlpha has to be > 0 < 1");if (pHeight != null && (pHeight < 0 || pHeight > 1)) trace ("pHeight has to be > 0 < 1");if (_global.warn == null) _global.warn = trace;_sourceClip = pSource;_setupReflection(pAlpha == null?40:pAlpha * 100,pHeight == null?0.4:pHeight,pBounds,pCustomClip);if (pFrameRate != null) setAutoUpdateFrameRate (pFrameRate);if (pAutoClear != null) _autoClear = pAutoClear;}/*** Create a gradient required to create the fading out alpha effect*/private function _setupReflection(pAlpha:Number, pHeight:Number, pBounds:Rectangle, pCustomClip:MovieClip) {//store short reference to the parent that contains our source, it will contain the reflection and//reflection alpha mask as wellvar lReflectionParent:MovieClip = _sourceClip._parent;if (pCustomClip == null) {_reflectionClip = lReflectionParent.createEmptyMovieClip ("reflection_[" + _sourceClip._name + "]",lReflectionParent.getNextHighestDepth());} else {if (pCustomClip._parent != lReflectionParent) {_global.warn ("WARNING:the clip you have passed is not on the same timeline as the clip to reflect.");}_reflectionClip = pCustomClip;}if (pBounds == null || (pBounds.width == 0 && pBounds.height == 0)) {//now get the local boundsvar lBounds:Object = _sourceClip.getBounds(_sourceClip);_sourceClipLocalBounds = new Rectangle (lBounds.xMin, lBounds.yMin, lBounds.xMax - lBounds.xMin, lBounds.yMax - lBounds.yMin);//an important thing to note is that flash returns a _width and _height based on unmasked content.//if you have a 100x100 clip, with a topleft mask of 50x50, the bounds will be based on the 100x100 size,//not on the 50x50. This causes an issue in a number of cases, and reflecting the clip is one of them:)//therefore we are going to check the visible area using the bounds we just established to check if we need to//adjust our bounds//we create a bitmap to draw the clip into firstvar lBoundsDetectionBitmap:BitmapData = new BitmapData (_sourceClipLocalBounds.width, _sourceClipLocalBounds.height, true, 0x0);var lBoundMatrix:Matrix = new Matrix();//enforce top leftlBoundMatrix.translate ( -_sourceClipLocalBounds.x, -_sourceClipLocalBounds.y);lBoundsDetectionBitmap.draw (_sourceClip, lBoundMatrix);//now detect visible area in the bitmap, and translate it back to the original originvar lVisibleBounds:Rectangle = lBoundsDetectionBitmap.getColorBoundsRect(0xff000000, 0xff000000, true);lVisibleBounds.offset (_sourceClipLocalBounds.x, _sourceClipLocalBounds.y);//dispose tha bitmaplBoundsDetectionBitmap.dispose();lVisibleBounds.width = Math.ceil (lVisibleBounds.width);_sourceClipLocalBounds = lVisibleBounds;} else {_sourceClipLocalBounds = pBounds;}//we can establish parent bounds using _sourceClip.getBounds(lReflectionParent), but the bounds might have//been prepassed or altered by getColorBoundRect detection, established etc, so we do it through globalToLocal,//localToGlobal so instead of//var lBounds:Object = _sourceClip.getBounds(lReflectionParent);//_sourceClipParentBounds = new Rectangle (lBounds.xMin, lBounds.yMin, lBounds.xMax - lBounds.xMin, lBounds.yMax - lBounds.yMin);//we dovar xMinyMin:Object = { x: _sourceClipLocalBounds.x, y: _sourceClipLocalBounds.y };var xMaxyMax:Object = { x: _sourceClipLocalBounds.x+_sourceClipLocalBounds.width, y: _sourceClipLocalBounds.y+_sourceClipLocalBounds.height};_sourceClip.localToGlobal (xMinyMin);_sourceClip.localToGlobal (xMaxyMax);lReflectionParent._parent.globalToLocal (xMinyMin);lReflectionParent._parent.globalToLocal (xMaxyMax);var lWidth:Number = xMaxyMax.x - xMinyMin.x;var lHeight:Number = xMaxyMax.y - xMinyMin.y;_sourceClipParentBounds = new Rectangle (xMinyMin.x, xMinyMin.y, Math.abs(lWidth), Math.abs(lHeight));//note that for an unscaled clip with left top registration these bounds will be equal//we will use the parent bounds as much as possible, in short this means that if you are reflecting a movieclip//that is 1000x1000 but scaled down to 10%, the reflection will only create a bitmap sized 100x100//beside offsetting, we need to establish a reflection height, since we might want a greater falloff than a complete reflectionvar lReflectionHeight:Number = _sourceClipParentBounds.height * pHeight;//we create a clip to be used as mask, and we draw an alpha gradient on it (yes it's possible:))_reflectionClipMask = _reflectionClip.createEmptyMovieClip ("mask", 0);_reflectionClipMask.lineStyle (0, 0x0, 0);var lBottomGradientMatrix:Matrix = new Matrix();lBottomGradientMatrix.createGradientBox(_sourceClipParentBounds.width, lReflectionHeight, Math.PI/2, 0, 0);_reflectionClipMask.beginGradientFill ("linear", [0xffffff, 0xffffff], [pAlpha, 0], [0, 255], lBottomGradientMatrix);_reflectionClipMask.moveTo (0, 0);_reflectionClipMask.lineTo (_sourceClipParentBounds.width, 0);_reflectionClipMask.lineTo (_sourceClipParentBounds.width, lReflectionHeight);_reflectionClipMask.lineTo (0, lReflectionHeight);_reflectionClipMask.lineTo (0, 0);_reflectionClipMask.endFill();//now create reflection container_reflectionClipContent = _reflectionClip.createEmptyMovieClip ("content", 1);//and we set up our resulting bitmap_resultBitmap = new BitmapData (_sourceClipParentBounds.width, lReflectionHeight, true, 0x0);_reflectionClipContent.attachBitmap (_resultBitmap, 0, "none", true);//align the reflection clip to the source_reflectionClip._x = _sourceClipParentBounds.x;_reflectionClip._y = _sourceClipParentBounds.y + _sourceClipParentBounds.height;if (lWidth < 0) {_global.warn ("Source clip has been scaled negatively horizontally, trying to adjust, this will hurt performance." +"Try to wrap the source clip in another clip.");_reflectionClipContent._xscale = -100;_reflectionClipContent._x += _reflectionClipContent._width;_reflectionClip._x -= _reflectionClipContent._width;}if (lHeight < 0) {_global.warn ("Source clip has been scaled negatively vertically, trying to adjust, this will hurt performance." +"Try to wrap the source clip in another clip.");_reflectionClipContent._yscale = -100;_reflectionClipContent._y += _reflectionClipContent._height;_reflectionClip._y -= _reflectionClipContent._height;}//hook up the mask and set cacheAsBitmap to true so the alpha in the mask is applied to_reflectionClipContent.setMask (_reflectionClipMask);_reflectionClipContent.cacheAsBitmap = true;_reflectionClipMask.cacheAsBitmap = true;//the redraw matrix is just one matrix, no matter how many operations you add. So it doesn't hurt//to keep operations separated for clarity. View the redraw matrix from the perspective of the source clip:_redrawMatrix = new Matrix();//first we 'enforce' a top left registration point_redrawMatrix.translate ( -_sourceClipLocalBounds.x, -_sourceClipLocalBounds.y);//then we scale the relation between parent and child, if the parent bounds for clip a say the clip is 200 wide,//and the local bounds say 400, appearently is has been scaled by 50 percent_redrawMatrix.scale (_sourceClipParentBounds.width/_sourceClipLocalBounds.width, _sourceClipParentBounds.height/_sourceClipLocalBounds.height);//now flip the whole thing upside down_redrawMatrix.scale (1, -1);//and move it down again to we can see it_redrawMatrix.translate (0, _sourceClipParentBounds.height);//everything is tranformed so it fits within this rectangle:_redrawRectangle = new Rectangle (0, 0, _sourceClipParentBounds.width, lReflectionHeight);//if original clip has filters attached, warn.if (_reflectionClip.filters.length > 0) {_global.warn ("Warning source clip has filters. You might want to wrap this clip in a parent clip without filters, " +"or pass correct bounds. You might want to add yourReflection.getReflection().filters = yourSourceClip.filters.");}//redraw once to get startedredraw();}public function redraw() {if (_autoClear) {//seems faster than a fillRect but fillRect less processor intensive//_resultBitmap.dispose();//_resultBitmap = new BitmapData (_redrawRectangle.width, _redrawRectangle.height, true, 0x0);//_reflectionClipContent.attachBitmap (_resultBitmap, 0, "none", true);_resultBitmap.fillRect (_redrawRectangle, 0x0);}_resultBitmap.draw (_sourceClip, _redrawMatrix, null, null, _redrawRectangle, true);}public function setAutoUpdateFrameRate(pFrameRate:Number) {//clear any old intervalsif (_updateIntervalId != null) {clearInterval (_updateIntervalId);_updateIntervalId = null;}//if no interval specified exitif (pFrameRate == null) return;_updateIntervalId = setInterval (this, "redraw", Math.round (1000/pFrameRate));}public function setAutoClear (pAutoClear:Boolean) {_autoClear = pAutoClear;}public function getReflection():MovieClip {return _reflectionClip;}public function getSourceClip():MovieClip {return _sourceClip;}public function finalize() {setAutoUpdateFrameRate (null);_sourceClip = null;_reflectionClipContent.setMask (null);_reflectionClipContent = null;_reflectionClipMask = null;_reflectionClip.removeMovieClip();_reflectionClip.filters = null;_reflectionClip = null;_resultBitmap.dispose();}}- Download this code: Reflection.as
A couple of screenshots of the other demo’s:
