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 mirror private var _sourceClipParentBounds:Rectangle = null; //the source bounds as interpreted by the parent private var _sourceClipLocalBounds:Rectangle = null; //the source bounds as interpreted by source itself //same as parentbounds for nonscaled, nontranslated clips private var _reflectionClip:MovieClip = null; //contains _reflectionClipContent and _reflectionClipMask private var _reflectionClipContent:MovieClip = null; //contains resultbitmap into which source reflection is drawn private var _reflectionClipMask:MovieClip = null; //contains alpha mask used as fade out effect private var _redrawMatrix:Matrix = null; //contains all transformation necessary to draw a scaled, //translated and flipped reflection of the source into the dest private var _redrawRectangle:Rectangle = null; //specifies clip on redrawing method private var _resultBitmap:BitmapData = null; //contains result of drawing source into dest through matrix private var _updateIntervalId:Number = null; //redraw interval, use null for static images private 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 well var 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 bounds var 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 first var lBoundsDetectionBitmap:BitmapData = new BitmapData (_sourceClipLocalBounds.width, _sourceClipLocalBounds.height, true, 0x0); var lBoundMatrix:Matrix = new Matrix(); //enforce top left lBoundMatrix.translate ( -_sourceClipLocalBounds.x, -_sourceClipLocalBounds.y); lBoundsDetectionBitmap.draw (_sourceClip, lBoundMatrix); //now detect visible area in the bitmap, and translate it back to the original origin var lVisibleBounds:Rectangle = lBoundsDetectionBitmap.getColorBoundsRect(0xff000000, 0xff000000, true); lVisibleBounds.offset (_sourceClipLocalBounds.x, _sourceClipLocalBounds.y); //dispose tha bitmap lBoundsDetectionBitmap.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 do var 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 reflection var 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 started redraw(); } 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 intervals if (_updateIntervalId != null) { clearInterval (_updateIntervalId); _updateIntervalId = null; } //if no interval specified exit if (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(); } }