Archive for November, 2008

Visual Reflections in AS2

Sunday, November 23rd, 2008

I’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:

  • 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
  • A couple samples:

    The code:

    1. import flash.display.BitmapData;
    2. import flash.geom.Matrix;
    3. import flash.geom.Rectangle;
    4.  
    5. /**
    6. * ReflectionManager manages the reflection for one clip.
    7. *
    8. * Short feature set:
    9. * - simple setup by default. In most cases doing new ReflectionManager (sourceClip) will suffice.
    10. * - default setup is a clip that fades from 40 to 0 alpha at 40% of the image.
    11. * - you can change the falloff height, overall alpha, and the redraw interval (eg to mirror video)
    12. * - retrievable reflection and source clips to apply filters or otherwise control clips
    13. * - redraw interval is disconnected from framerate, specify a low redraw interval to save system resources
    14. * - works with scaled, non top left regpoints, and masked clip, using an autobounds detection algorithm
    15. * - works with negative scaled clips
    16. * - tries to warn you in most cases when the result is likely to be different from what you expect
    17. * - very high performance
    18. * - no dependencies on non flash classes
    19. *
    20. * @author J.C. Wichman, TriMM Interactive Media (www.trimm.nl / j.c.wichman@trimm.nl),
    21. * ObjectPainters.com (blog.objectpainters.com / j.c.wichman@objectpainters.com)
    22. */
    23. class nl.trimm.movieclip.Reflection {
    24.  
    25. private var _sourceClip:MovieClip = null; //reference to the source you want to mirror
    26. private var _sourceClipParentBounds:Rectangle = null; //the source bounds as interpreted by the parent
    27. private var _sourceClipLocalBounds:Rectangle = null; //the source bounds as interpreted by source itself
    28. //same as parentbounds for nonscaled, nontranslated clips
    29.  
    30. private var _reflectionClip:MovieClip = null; //contains _reflectionClipContent and _reflectionClipMask
    31. private var _reflectionClipContent:MovieClip = null; //contains resultbitmap into which source reflection is drawn
    32. private var _reflectionClipMask:MovieClip = null; //contains alpha mask used as fade out effect
    33.  
    34. private var _redrawMatrix:Matrix = null; //contains all transformation necessary to draw a scaled,
    35. //translated and flipped reflection of the source into the dest
    36. private var _redrawRectangle:Rectangle = null; //specifies clip on redrawing method
    37.  
    38. private var _resultBitmap:BitmapData = null; //contains result of drawing source into dest through matrix
    39. private var _updateIntervalId:Number = null; //redraw interval, use null for static images
    40.  
    41. private var _autoClear:Boolean = false; //auto clear the bitmap before redrawing
    42.  
    43. /**
    44. * The purpose of this reflection class is to make it as simple as possible to provide a working reflection,
    45. * yet give you the freedom to adjust what's needed.
    46. *
    47. * Restrictions:
    48. * - source and reflection share the same parent
    49. * - source should not be scaled, if you want to scale, either wrap the scaled clip, or scale the parent
    50. *
    51. * @param pTarget the object you want to create a reflection for
    52. * @param pAlpha the alpha of the reflection 0..1
    53. * @param pHeight the height of the reflection, 1 is the whole image height is used as fall off, 0 is no reflection, and
    54. * everything in between (0 .. 1)
    55. * @param pFrameRate the framerate for the redraw method
    56. * @param pBounds explicit bounds that define the area to be reflected, if these are not passed, bounds are auto detected,
    57. * based on what's visible at the moment of creation. The bounds are relative to the source clips coordinate
    58. * system. In other words, IMAGINE you edit the source clip, draw a rectangle around the area you want to see
    59. * reflected, and write down those bounds, THAT area defines the boundsrectangle.
    60. * @param pCustomClip an empty clip on the same parent as pSource that you would like to display the reflection.
    61. * The only reason I can think of to do so, is that this will allow you to apply filters to this
    62. * clip through the IDE instead of filter classes, which might be convenient for non coders.
    63. * @param pAutoClear often it's not necessary to auto clear the canvas again before redrawing, UNLESS you are moving
    64. * your content, and your content is transparent. By default autoclear is off, but you might want
    65. * to enable it.
    66. */
    67. public function Reflection(pSource:MovieClip, pAlpha:Number, pHeight:Number, pFrameRate:Number, pBounds:Rectangle, pCustomClip:MovieClip, pAutoClear:Boolean) {
    68. if (pAlpha != null && (pAlpha < 0 || pAlpha > 1)) trace ("pAlpha has to be > 0 < 1");
    69. if (pHeight != null && (pHeight < 0 || pHeight > 1)) trace ("pHeight has to be > 0 < 1");
    70.  
    71. if (_global.warn == null) _global.warn = trace;
    72.  
    73. _sourceClip = pSource;
    74. _setupReflection(
    75. pAlpha == null?40:pAlpha * 100,
    76. pHeight == null?0.4:pHeight,
    77. pBounds,
    78. pCustomClip
    79. );
    80. if (pFrameRate != null) setAutoUpdateFrameRate (pFrameRate);
    81. if (pAutoClear != null) _autoClear = pAutoClear;
    82. }
    83.  
    84. /**
    85. * Create a gradient required to create the fading out alpha effect
    86. */
    87. private function _setupReflection(pAlpha:Number, pHeight:Number, pBounds:Rectangle, pCustomClip:MovieClip) {
    88. //store short reference to the parent that contains our source, it will contain the reflection and
    89. //reflection alpha mask as well
    90. var lReflectionParent:MovieClip = _sourceClip._parent;
    91. if (pCustomClip == null) {
    92. _reflectionClip = lReflectionParent.createEmptyMovieClip (
    93. "reflection_[" + _sourceClip._name + "]",
    94. lReflectionParent.getNextHighestDepth()
    95. );
    96. } else {
    97. if (pCustomClip._parent != lReflectionParent) {
    98. _global.warn ("WARNING:the clip you have passed is not on the same timeline as the clip to reflect.");
    99. }
    100. _reflectionClip = pCustomClip;
    101. }
    102.  
    103. if (pBounds == null || (pBounds.width == 0 && pBounds.height == 0)) {
    104. //now get the local bounds
    105. var lBounds:Object = _sourceClip.getBounds(_sourceClip);
    106. _sourceClipLocalBounds = new Rectangle (lBounds.xMin, lBounds.yMin, lBounds.xMax - lBounds.xMin, lBounds.yMax - lBounds.yMin);
    107.  
    108. //an important thing to note is that flash returns a _width and _height based on unmasked content.
    109. //if you have a 100x100 clip, with a topleft mask of 50x50, the bounds will be based on the 100x100 size,
    110. //not on the 50x50. This causes an issue in a number of cases, and reflecting the clip is one of them:)
    111. //therefore we are going to check the visible area using the bounds we just established to check if we need to
    112. //adjust our bounds
    113.  
    114. //we create a bitmap to draw the clip into first
    115. var lBoundsDetectionBitmap:BitmapData = new BitmapData (_sourceClipLocalBounds.width, _sourceClipLocalBounds.height, true, 0x0);
    116. var lBoundMatrix:Matrix = new Matrix();
    117. //enforce top left
    118. lBoundMatrix.translate ( -_sourceClipLocalBounds.x, -_sourceClipLocalBounds.y);
    119. lBoundsDetectionBitmap.draw (_sourceClip, lBoundMatrix);
    120. //now detect visible area in the bitmap, and translate it back to the original origin
    121. var lVisibleBounds:Rectangle = lBoundsDetectionBitmap.getColorBoundsRect(0xff000000, 0xff000000, true);
    122. lVisibleBounds.offset (_sourceClipLocalBounds.x, _sourceClipLocalBounds.y);
    123. //dispose tha bitmap
    124. lBoundsDetectionBitmap.dispose();
    125.  
    126. lVisibleBounds.width = Math.ceil (lVisibleBounds.width);
    127. _sourceClipLocalBounds = lVisibleBounds;
    128.  
    129. } else {
    130. _sourceClipLocalBounds = pBounds;
    131. }
    132.  
    133.  
    134. //we can establish parent bounds using _sourceClip.getBounds(lReflectionParent), but the bounds might have
    135. //been prepassed or altered by getColorBoundRect detection, established etc, so we do it through globalToLocal,
    136. //localToGlobal so instead of
    137. //var lBounds:Object = _sourceClip.getBounds(lReflectionParent);
    138. //_sourceClipParentBounds = new Rectangle (lBounds.xMin, lBounds.yMin, lBounds.xMax - lBounds.xMin, lBounds.yMax - lBounds.yMin);
    139. //we do
    140. var xMinyMin:Object = { x: _sourceClipLocalBounds.x, y: _sourceClipLocalBounds.y };
    141. var xMaxyMax:Object = { x: _sourceClipLocalBounds.x+_sourceClipLocalBounds.width, y: _sourceClipLocalBounds.y+_sourceClipLocalBounds.height};
    142. _sourceClip.localToGlobal (xMinyMin);
    143. _sourceClip.localToGlobal (xMaxyMax);
    144. lReflectionParent._parent.globalToLocal (xMinyMin);
    145. lReflectionParent._parent.globalToLocal (xMaxyMax);
    146.  
    147. var lWidth:Number = xMaxyMax.x - xMinyMin.x;
    148. var lHeight:Number = xMaxyMax.y - xMinyMin.y;
    149. _sourceClipParentBounds = new Rectangle (xMinyMin.x, xMinyMin.y, Math.abs(lWidth), Math.abs(lHeight));
    150. //note that for an unscaled clip with left top registration these bounds will be equal
    151. //we will use the parent bounds as much as possible, in short this means that if you are reflecting a movieclip
    152. //that is 1000x1000 but scaled down to 10%, the reflection will only create a bitmap sized 100x100
    153.  
    154. //beside offsetting, we need to establish a reflection height, since we might want a greater falloff than a complete reflection
    155. var lReflectionHeight:Number = _sourceClipParentBounds.height * pHeight;
    156.  
    157. //we create a clip to be used as mask, and we draw an alpha gradient on it (yes it's possible:))
    158. _reflectionClipMask = _reflectionClip.createEmptyMovieClip ("mask", 0);
    159. _reflectionClipMask.lineStyle (0, 0x0, 0);
    160. var lBottomGradientMatrix:Matrix = new Matrix();
    161. lBottomGradientMatrix.createGradientBox(_sourceClipParentBounds.width, lReflectionHeight, Math.PI/2, 0, 0);
    162. _reflectionClipMask.beginGradientFill ("linear", [0xffffff, 0xffffff], [pAlpha, 0], [0, 255], lBottomGradientMatrix);
    163. _reflectionClipMask.moveTo (0, 0);
    164. _reflectionClipMask.lineTo (_sourceClipParentBounds.width, 0);
    165. _reflectionClipMask.lineTo (_sourceClipParentBounds.width, lReflectionHeight);
    166. _reflectionClipMask.lineTo (0, lReflectionHeight);
    167. _reflectionClipMask.lineTo (0, 0);
    168. _reflectionClipMask.endFill();
    169.  
    170. //now create reflection container
    171. _reflectionClipContent = _reflectionClip.createEmptyMovieClip ("content", 1);
    172. //and we set up our resulting bitmap
    173. _resultBitmap = new BitmapData (_sourceClipParentBounds.width, lReflectionHeight, true, 0x0);
    174. _reflectionClipContent.attachBitmap (_resultBitmap, 0, "none", true);
    175.  
    176. //align the reflection clip to the source
    177. _reflectionClip._x = _sourceClipParentBounds.x;
    178. _reflectionClip._y = _sourceClipParentBounds.y + _sourceClipParentBounds.height;
    179.  
    180. if (lWidth < 0) {
    181. _global.warn (
    182. "Source clip has been scaled negatively horizontally, trying to adjust, this will hurt performance." +
    183. "Try to wrap the source clip in another clip."
    184. );
    185. _reflectionClipContent._xscale = -100;
    186. _reflectionClipContent._x += _reflectionClipContent._width;
    187. _reflectionClip._x -= _reflectionClipContent._width;
    188. }
    189. if (lHeight < 0) {
    190. _global.warn ("Source clip has been scaled negatively vertically, trying to adjust, this will hurt performance." +
    191. "Try to wrap the source clip in another clip."
    192. );
    193. _reflectionClipContent._yscale = -100;
    194. _reflectionClipContent._y += _reflectionClipContent._height;
    195. _reflectionClip._y -= _reflectionClipContent._height;
    196. }
    197.  
    198. //hook up the mask and set cacheAsBitmap to true so the alpha in the mask is applied to
    199. _reflectionClipContent.setMask (_reflectionClipMask);
    200. _reflectionClipContent.cacheAsBitmap = true;
    201. _reflectionClipMask.cacheAsBitmap = true;
    202.  
    203. //the redraw matrix is just one matrix, no matter how many operations you add. So it doesn't hurt
    204. //to keep operations separated for clarity. View the redraw matrix from the perspective of the source clip:
    205. _redrawMatrix = new Matrix();
    206. //first we 'enforce' a top left registration point
    207. _redrawMatrix.translate ( -_sourceClipLocalBounds.x, -_sourceClipLocalBounds.y);
    208. //then we scale the relation between parent and child, if the parent bounds for clip a say the clip is 200 wide,
    209. //and the local bounds say 400, appearently is has been scaled by 50 percent
    210. _redrawMatrix.scale (_sourceClipParentBounds.width/_sourceClipLocalBounds.width, _sourceClipParentBounds.height/_sourceClipLocalBounds.height);
    211. //now flip the whole thing upside down
    212. _redrawMatrix.scale (1, -1);
    213. //and move it down again to we can see it
    214. _redrawMatrix.translate (0, _sourceClipParentBounds.height);
    215.  
    216. //everything is tranformed so it fits within this rectangle:
    217. _redrawRectangle = new Rectangle (0, 0, _sourceClipParentBounds.width, lReflectionHeight);
    218.  
    219. //if original clip has filters attached, warn.
    220. if (_reflectionClip.filters.length > 0) {
    221. _global.warn (
    222. "Warning source clip has filters. You might want to wrap this clip in a parent clip without filters, " +
    223. "or pass correct bounds. You might want to add yourReflection.getReflection().filters = yourSourceClip.filters."
    224. );
    225. }
    226.  
    227. //redraw once to get started
    228. redraw();
    229. }
    230.  
    231. public function redraw() {
    232. if (_autoClear) {
    233. //seems faster than a fillRect but fillRect less processor intensive
    234. //_resultBitmap.dispose();
    235. //_resultBitmap = new BitmapData (_redrawRectangle.width, _redrawRectangle.height, true, 0x0);
    236. //_reflectionClipContent.attachBitmap (_resultBitmap, 0, "none", true);
    237. _resultBitmap.fillRect (_redrawRectangle, 0x0);
    238. }
    239. _resultBitmap.draw (_sourceClip, _redrawMatrix, null, null, _redrawRectangle, true);
    240. }
    241.  
    242. public function setAutoUpdateFrameRate(pFrameRate:Number) {
    243. //clear any old intervals
    244. if (_updateIntervalId != null) {
    245. clearInterval (_updateIntervalId);
    246. _updateIntervalId = null;
    247. }
    248.  
    249. //if no interval specified exit
    250. if (pFrameRate == null) return;
    251.  
    252. _updateIntervalId = setInterval (this, "redraw", Math.round (1000/pFrameRate));
    253. }
    254.  
    255. public function setAutoClear (pAutoClear:Boolean) {
    256. _autoClear = pAutoClear;
    257. }
    258.  
    259. public function getReflection():MovieClip {
    260. return _reflectionClip;
    261. }
    262.  
    263. public function getSourceClip():MovieClip {
    264. return _sourceClip;
    265. }
    266.  
    267. public function finalize() {
    268. setAutoUpdateFrameRate (null);
    269. _sourceClip = null;
    270. _reflectionClipContent.setMask (null);
    271. _reflectionClipContent = null;
    272. _reflectionClipMask = null;
    273.  
    274. _reflectionClip.removeMovieClip();
    275. _reflectionClip.filters = null;
    276. _reflectionClip = null;
    277.  
    278. _resultBitmap.dispose();
    279. }
    280.  
    281. }

    A couple of screenshots of the other demo’s:
    Difference process