/src/com/greensock/BlitMask.as
ActionScript | 892 lines | 560 code | 58 blank | 274 comment | 197 complexity | 0af6dbad5db5a563a7b6f1a451da3188 MD5 | raw file
1/**
2 * VERSION: 0.6
3 * DATE: 2012-01-20
4 * AS3
5 * UPDATES AND DOCS AT: http://www.greensock.com
6 **/
7package com.greensock {
8 import flash.display.BitmapData;
9 import flash.display.DisplayObject;
10 import flash.display.Graphics;
11 import flash.display.Sprite;
12 import flash.events.Event;
13 import flash.events.MouseEvent;
14 import flash.geom.ColorTransform;
15 import flash.geom.Matrix;
16 import flash.geom.Point;
17 import flash.geom.Rectangle;
18 import flash.geom.Transform;
19/**
20 * A BlitMask is basically a rectangular Sprite that acts as a high-performance mask for a DisplayObject
21 * by caching a bitmap version of it and blitting only the pixels that should be visible at any given time,
22 * although its <code>bitmapMode</code> can be turned off to restore interactivity in the DisplayObject
23 * whenever you want. When scrolling very large images or text blocks, a BlitMask can greatly improve
24 * performance, especially on mobile devices that have weaker processors. <br /><br />
25 *
26 * Here are some of the conveniences BlitMask offers:<br />
27 * <ul>
28 * <li>Excellent scrolling performance</li>
29 * <li>You don't need to do anything special with your target DisplayObject - move/scale/rotate it
30 * however you please and then <code>update()</code> the BlitMask and it syncs the pixels.
31 * The BlitMask basically sits on top of the DisplayObject in the display list and you can
32 * move it independently too if you want.</li>
33 * <li>Use the BlitMask's <code>scrollX</code> and <code>scrollY</code> properties to move the
34 * target DisplayObject inside the masked area. For example, to scroll from top to bottom over
35 * the course of 2 seconds, simply do: <br /><code>myBlitMask.scrollY = 0;<br />
36 * TweenLite.to(myBlitMask, 2, {scrollY:1});</code> </li>
37 * <li>Use the "wrap" feature to make the bitmap wrap around to the opposite side when it scrolls
38 * off one of the edges (only in <code>bitmapMode</code> of course), as though the BlitMask is
39 * filled with a grid of bitmap copies of the target.</li>
40 * <li>For maximum performance in bitmapMode, set <code>smoothing</code> to <code>false</code> or
41 * for maximum quality, set it to <code>true</code></li>
42 * <li>You can toggle the <code>bitmapMode</code> to get either maximum performance or interactivity
43 * in the target DisplayObject anytime. (some other solutions out there are only viable for
44 * non-interactive bitmap content) </li>
45 * <li>MouseEvents are dispatched by the BlitMask, so you can listen for clicks, rollovers, rollouts, etc.</li>
46 * </ul>
47 *
48 * @example Example AS3 code:<listing version="3.0">
49 import com.greensock.~~;
50
51 //create a 200x200 BlitMask positioned at x:20, y:50 to mask our "mc" object and turn smoothing on:
52 var blitMask:BlitMask = new BlitMask(mc, 20, 50, 200, 200, true);
53
54 //position mc at the top left of the BlitMask using the scrollX and scrollY properties
55 blitMask.scrollX = 0;
56 blitMask.scrollY = 0;
57
58 //tween the scrollY to make mc scroll to the bottom over the course of 3 seconds and then turn off bitmapMode so that mc becomes interactive:
59 TweenLite.to(blitMask, 3, {scrollY:1, onComplete:blitMask.disableBitmapMode});
60
61 //or simply position mc manually and then call update() to sync the display:
62 mc.x = 350;
63 blitMask.update();
64
65 </listing>
66 *
67 * Notes:
68 * <ul>
69 * <li>BlitMasks themselves should not be rotated or scaled (although technically you can alter the scaleX and scaleY
70 * but doing so will only change the width or height instead). You can, of course, alter their x, y, width,
71 * or height properties as much as you want. </li>
72 * <li>BlitMasks don't perform nearly as well in bitmapMode when the <code>target</code> is being scaled or rotated
73 * because it forces a flushing and recapture of the internal bitmap. BlitMasks are <b>MUCH</b> better when you are
74 * simply changing x/y properties (scrolling) because it can reuse the same cached bitmap over and over.</li>
75 * <li>If the target content is changing frequently (like if it has nested MovieClips that are animating on every frame),
76 * you'd need to call update(null, true) each time you want the BlitMask to redraw itself to sync with the changes
77 * in the target, but that's a relatively expensive operation so it's not a great use case for BlitMask. You may
78 * be better off just turning off bitmapMode during that animation sequence.</li>
79 * </ul><br /><br />
80 *
81 * <b>Copyright 2011-2012, GreenSock. All rights reserved.</b> This work is subject to the terms in <a href="http://www.greensock.com/terms_of_use.html">http://www.greensock.com/terms_of_use.html</a> or for corporate Club GreenSock members, the software agreement that was issued with the corporate membership.
82 *
83 * @author Jack Doyle, jack@greensock.com
84 **/
85 public class BlitMask extends Sprite {
86 /** @private **/
87 public static var version:Number = 0.6;
88
89 // In order to conserve memory and improve performance, we create a few instances of Rectangles, Sprites, Points, Matrices, and Arrays and reuse them rather than creating new instances over and over.
90 /** @private **/
91 protected static var _tempContainer:Sprite = new Sprite();
92 /** @private **/
93 protected static var _sliceRect:Rectangle = new Rectangle();
94 /** @private **/
95 protected static var _drawRect:Rectangle = new Rectangle();
96 /** @private **/
97 protected static var _destPoint:Point = new Point();
98 /** @private **/
99 protected static var _tempMatrix:Matrix = new Matrix();
100 /** @private **/
101 protected static var _emptyArray:Array = [];
102 /** @private **/
103 protected static var _colorTransform:ColorTransform = new ColorTransform();
104 /** @private **/
105 protected static var _mouseEvents:Array = [MouseEvent.CLICK, MouseEvent.DOUBLE_CLICK, MouseEvent.MOUSE_DOWN, MouseEvent.MOUSE_MOVE, MouseEvent.MOUSE_OUT, MouseEvent.MOUSE_OVER, MouseEvent.MOUSE_UP, MouseEvent.MOUSE_WHEEL, MouseEvent.ROLL_OUT, MouseEvent.ROLL_OVER];
106
107 /** @private **/
108 protected var _target:DisplayObject;
109 /** @private **/
110 protected var _fillColor:uint;
111 /** @private **/
112 protected var _smoothing:Boolean;
113 /** @private **/
114 protected var _width:Number;
115 /** @private **/
116 protected var _height:Number;
117 /** @private **/
118 protected var _bd:BitmapData;
119 /** @private maximum number of pixels (minus one) that each BitmapData cell in the grid can be **/
120 protected var _gridSize:int = 2879;
121 /** @private **/
122 protected var _grid:Array;
123 /** @private **/
124 protected var _bounds:Rectangle;
125 /** @private **/
126 protected var _clipRect:Rectangle;
127 /** @private **/
128 protected var _bitmapMode:Boolean;
129 /** @private **/
130 protected var _rows:int;
131 /** @private **/
132 protected var _columns:int;
133 /** @private **/
134 protected var _scaleX:Number;
135 /** @private **/
136 protected var _scaleY:Number;
137 /** @private **/
138 protected var _prevMatrix:Matrix;
139 /** @private **/
140 protected var _transform:Transform;
141 /** @private **/
142 protected var _prevRotation:Number;
143 /** @private **/
144 protected var _autoUpdate:Boolean;
145 /** @private **/
146 protected var _wrap:Boolean;
147 /** @private **/
148 protected var _wrapOffsetX:Number = 0;
149 /** @private **/
150 protected var _wrapOffsetY:Number = 0;
151
152 /**
153 * Constructor
154 *
155 * @param target The DisplayObject that will be masked by the BlitMask
156 * @param x x coorinate of the upper left corner of the BlitMask. If <code>smoothing</code> is <code>false</code>, the x coordinate will be rounded to the closest integer.
157 * @param y y coordinate of the upper right corner of the BlitMask
158 * @param width width of the BlitMask (in pixels)
159 * @param height height of the BlitMask (in pixels)
160 * @param smoothing If <code>false</code> (the default), the bitmap (and the BlitMask's x/y coordinates) will be rendered only on whole pixels which is faster in terms of processing. However, for the best quality and smoothest animation, set <code>smoothing</code> to <code>true</code>.
161 * @param autoUpdate If <code>true</code>, the BlitMask will automatically watch the <code>target</code> to see if its position/scale/rotation has changed on each frame (while <code>bitmapMode</code> is <code>true</code>) and if so, it will <code>update()</code> to make sure the BlitMask always stays synced with the <code>target</code>. This is the easiest way to use BlitMask but it is slightly less efficient than manually calling <code>update()</code> whenever you need to. Keep in mind that if you're tweening with TweenLite or TweenMax, you can simply set its <code>onUpdate</code> to the BlitMask's <code>update()</code> method to keep things synced. Like <code>onUpdate:myBlitMask.update</code>.
162 * @param fillColor The ARGB hexadecimal color that should fill the empty areas of the BlitMask. By default, it is transparent (0x00000000). If you wanted a red color, for example, it would be <code>0xFFFF0000</code>.
163 * @param wrap If <code>true</code>, the bitmap will be wrapped around to the opposite side when it scrolls off one of the edges (only in <code>bitmapMode</code> of course), like the BlitMask is filled with a grid of bitmap copies of the target. Use the <code>wrapOffsetX</code> and <code>wrapOffsetY</code> properties to affect how far apart the copies are from each other.
164 */
165 public function BlitMask(target:DisplayObject, x:Number=0, y:Number=0, width:Number=100, height:Number=100, smoothing:Boolean=false, autoUpdate:Boolean=false, fillColor:uint=0x00000000, wrap:Boolean=false) {
166 super();
167 if (width < 0 || height < 0) {
168 throw new Error("A FlexBlitMask cannot have a negative width or height.");
169 }
170 _width = width;
171 _height = height;
172 _scaleX = _scaleY = 1;
173 _smoothing = smoothing;
174 _fillColor = fillColor;
175 _autoUpdate = autoUpdate;
176 _wrap = wrap;
177 _grid = [];
178 _bounds = new Rectangle();
179 if (_smoothing) {
180 super.x = x;
181 super.y = y;
182 } else {
183 super.x = (x < 0) ? (x - 0.5) >> 0 : (x + 0.5) >> 0;
184 super.y = (y < 0) ? (y - 0.5) >> 0 : (y + 0.5) >> 0;
185 }
186 _clipRect = new Rectangle(0, 0, _gridSize + 1, _gridSize + 1);
187 _bd = new BitmapData(width + 1, height + 1, true, _fillColor);
188 _bitmapMode = true;
189 this.target = target;
190 }
191
192 /** @private **/
193 protected function _captureTargetBitmap():void {
194 if (_bd == null || _target == null) { //must have been disposed, so don't update.
195 return;
196 }
197
198 _disposeGrid();
199
200 //capturing when the target is masked (or has a scrollRect) can cause problems.
201 var prevMask:DisplayObject = _target.mask;
202 if (prevMask != null) {
203 _target.mask = null;
204 }
205 var prevScrollRect:Rectangle = _target.scrollRect;
206 if (prevScrollRect != null) {
207 _target.scrollRect = null;
208 }
209 var prevFilters:Array = _target.filters;
210 if (prevFilters.length != 0) {
211 _target.filters = _emptyArray;
212 }
213
214 _grid = [];
215 if (_target.parent == null) {
216 _tempContainer.addChild(_target);
217 }
218 _bounds = _target.getBounds(_target.parent);
219 var w:Number = 0;
220 var h:Number = 0;
221 _columns = Math.ceil(_bounds.width / _gridSize);
222 _rows = Math.ceil(_bounds.height / _gridSize);
223 var cumulativeHeight:Number = 0;
224 var matrix:Matrix = _transform.matrix;
225 var xOffset:Number = matrix.tx - _bounds.x;
226 var yOffset:Number = matrix.ty - _bounds.y;
227 if (!_smoothing) {
228 xOffset = (xOffset + 0.5) >> 0;
229 yOffset = (yOffset + 0.5) >> 0;
230 }
231
232 var bd:BitmapData, cumulativeWidth:Number;
233 for (var row:int = 0; row < _rows; row++) {
234 h = (_bounds.height - cumulativeHeight > _gridSize) ? _gridSize : _bounds.height - cumulativeHeight;
235 matrix.ty = -cumulativeHeight + yOffset;
236 cumulativeWidth = 0;
237 _grid[row] = [];
238 for (var column:int = 0; column < _columns; column++) {
239 w = (_bounds.width - cumulativeWidth > _gridSize) ? _gridSize : _bounds.width - cumulativeWidth;
240 _grid[row][column] = bd = new BitmapData(w + 1, h + 1, true, _fillColor);
241 matrix.tx = -cumulativeWidth + xOffset;
242 bd.draw(_target, matrix, null, null, _clipRect, _smoothing);
243 cumulativeWidth += w;
244 }
245 cumulativeHeight += h;
246 }
247
248 if (_target.parent == _tempContainer) {
249 _tempContainer.removeChild(_target);
250 }
251
252 if (prevMask != null) {
253 _target.mask = prevMask;
254 }
255 if (prevScrollRect != null) {
256 _target.scrollRect = prevScrollRect;
257 }
258 if (prevFilters.length != 0) {
259 _target.filters = prevFilters;
260 }
261 }
262
263 /** @private **/
264 protected function _disposeGrid():void {
265 var i:int = _grid.length, j:int, r:Array;
266 while (--i > -1) {
267 r = _grid[i];
268 j = r.length;
269 while (--j > -1) {
270 BitmapData(r[j]).dispose();
271 }
272 }
273 }
274
275 /**
276 * Updates the BlitMask's internal bitmap to reflect the <code>target's</code> current position/scale/rotation.
277 * This is a very important method that you'll need to call whenever visual or transformational changes are made
278 * to the target so that the BlitMask remains synced with it.
279 *
280 * @param event An optional Event object (which isn't used at all internally) in order to make it easier to use <code>update()</code> as an event handler. For example, you could <code>addEventListener(Event.ENTER_FRAME, myBlitMask.update)</code> to make sure it is updated on every frame (although it would be more efficient to simply set <code>autoUpdate</code> to <code>true</code> in this case).
281 * @param forceRecaptureBitmap Normally, the cached bitmap of the <code>target</code> is only recaptured if its scale or rotation changed because doing so is rather processor-intensive, but you can force a full update (and regeneration of the cached bitmap) by setting <code>forceRecaptureBitmap</code> to <code>true</code>.
282 */
283 public function update(event:Event=null, forceRecaptureBitmap:Boolean=false):void {
284 if (_bd == null) {
285 return;
286 } else if (_target == null) {
287 _render();
288 } else if (_target.parent) {
289 _bounds = _target.getBounds(_target.parent);
290 if (this.parent != _target.parent) {
291 _target.parent.addChildAt(this, _target.parent.getChildIndex(_target));
292 }
293 }
294 if (_bitmapMode || forceRecaptureBitmap) {
295 var m:Matrix = _transform.matrix;
296 if (forceRecaptureBitmap || _prevMatrix == null || m.a != _prevMatrix.a || m.b != _prevMatrix.b || m.c != _prevMatrix.c || m.d != _prevMatrix.d) {
297 _captureTargetBitmap();
298 _render();
299 } else if (m.tx != _prevMatrix.tx || m.ty != _prevMatrix.ty) {
300 _render();
301 } else if (_bitmapMode && _target != null) {
302 this.filters = _target.filters;
303 this.transform.colorTransform = _transform.colorTransform;
304 }
305 _prevMatrix = m;
306 }
307 }
308
309 /** @private **/
310 protected function _render(xOffset:Number=0, yOffset:Number=0, clear:Boolean=true, limitRecursion:Boolean=false):void {
311 //note: the code in this method was optimized for speed rather than readability or succinctness (since the whole point of this class is to help things perform better)
312 if (clear) {
313 _sliceRect.x = _sliceRect.y = 0;
314 _sliceRect.width = _width + 1;
315 _sliceRect.height = _height + 1;
316 _bd.fillRect(_sliceRect, _fillColor);
317
318 if (_bitmapMode && _target != null) {
319 this.filters = _target.filters;
320 this.transform.colorTransform = _transform.colorTransform;
321 } else {
322 this.filters = _emptyArray;
323 this.transform.colorTransform = _colorTransform;
324 }
325 }
326
327 if (_bd == null) {
328 return;
329 } else if (_rows == 0) { //sometimes (especially in Flex) objects take a frame or two to render in Flash and properly report their width/height. Before that, their width/height is typically 0. This works around that issue and forces a refresh if we didn't capture any pixels last time we did a capture.
330 _captureTargetBitmap();
331 }
332
333 var x:Number = super.x + xOffset;
334 var y:Number = super.y + yOffset;
335
336
337 var wrapWidth:int = (_bounds.width + _wrapOffsetX + 0.5) >> 0;
338 var wrapHeight:int = (_bounds.height + _wrapOffsetY + 0.5) >> 0;
339 var g:Graphics = this.graphics;
340
341 if (_bounds.width == 0 || _bounds.height == 0 || (_wrap && (wrapWidth == 0 || wrapHeight == 0)) || (!_wrap && (x + _width < _bounds.x || y + _height < _bounds.y || x > _bounds.right || y > _bounds.bottom))) {
342 g.clear();
343 g.beginBitmapFill(_bd);
344 g.drawRect(0, 0, _width, _height);
345 g.endFill();
346 return;
347 }
348
349 var column:int = int((x - _bounds.x) / _gridSize);
350 if (column < 0) {
351 column = 0;
352 }
353 var row:int = int((y - _bounds.y) / _gridSize);
354 if (row < 0) {
355 row = 0;
356 }
357
358 var maxColumn:int = int(((x + _width) - _bounds.x) / _gridSize);
359 if (maxColumn >= _columns) {
360 maxColumn = _columns - 1;
361 }
362 var maxRow:uint = int(((y + _height) - _bounds.y) / _gridSize);
363 if (maxRow >= _rows) {
364 maxRow = _rows - 1;
365 }
366
367 var xNudge:Number = (_bounds.x - x) % 1;
368 var yNudge:Number = (_bounds.y - y) % 1;
369
370 if (y <= _bounds.y) {
371 _destPoint.y = (_bounds.y - y) >> 0;
372 _sliceRect.y = -1; //subtract 1 to make sure the whole image gets included - without this, a very slight vibration can occur on the edge during animation.
373
374 } else {
375 _destPoint.y = 0;
376 _sliceRect.y = Math.ceil(y - _bounds.y) - (row * _gridSize) - 1; //subtract 1 to make sure the whole image gets included - without this, a very slight vibration can occur on the edge during animation.
377 if (clear && yNudge != 0) {
378 yNudge += 1;
379 }
380
381 }
382 if (x <= _bounds.x) {
383 _destPoint.x = (_bounds.x - x) >> 0;
384 _sliceRect.x = -1; //subtract 1 to make sure the whole image gets included - without this, a very slight vibration can occur on the edge during animation.
385
386 } else {
387 _destPoint.x = 0;
388 _sliceRect.x = Math.ceil(x - _bounds.x) - (column * _gridSize) - 1; //subtract 1 to make sure the whole image gets included - without this, a very slight vibration can occur on the edge during animation.
389 if (clear && xNudge != 0) {
390 xNudge += 1;
391 }
392 }
393
394 if (_wrap && clear) {
395 //make sure to offset appropriately so that we start drawing directly on the image. We must use consistent xNudge and yNudge values across all the recursive calls too, otherwise the copies may vibrate visually a bit as they move
396 _render(Math.ceil((_bounds.x - x) / wrapWidth) * wrapWidth, Math.ceil((_bounds.y - y) / wrapHeight) * wrapHeight, false, false);
397 } else if (_rows != 0) {
398 var xDestReset:Number = _destPoint.x;
399 var xSliceReset:Number = _sliceRect.x;
400 var columnReset:int = column;
401 var bd:BitmapData;
402 while (row <= maxRow) {
403 bd = _grid[row][0];
404 _sliceRect.height = bd.height - _sliceRect.y;
405 _destPoint.x = xDestReset;
406 _sliceRect.x = xSliceReset;
407 column = columnReset;
408 while (column <= maxColumn) {
409 bd = _grid[row][column];
410 _sliceRect.width = bd.width - _sliceRect.x;
411
412 _bd.copyPixels(bd, _sliceRect, _destPoint);
413
414 _destPoint.x += _sliceRect.width - 1;
415 _sliceRect.x = 0;
416 column++;
417 }
418 _destPoint.y += _sliceRect.height - 1;
419 _sliceRect.y = 0;
420 row++;
421 }
422
423 }
424
425 if (clear) {
426 _tempMatrix.tx = xNudge - 1; //subtract 1 to compensate for the pixel we added above.
427 _tempMatrix.ty = yNudge - 1;
428 g.clear();
429 g.beginBitmapFill(_bd, _tempMatrix, false, _smoothing);
430 g.drawRect(0, 0, _width, _height);
431 g.endFill();
432 } else if (_wrap) {
433 //if needed, recursively call _render() and adjust the offset(s) to wrap the bitmap.
434 if (x + _width > _bounds.right) {
435 _render(xOffset - wrapWidth, yOffset, false, true);
436 }
437 if (!limitRecursion && y + _height > _bounds.bottom) {
438 _render(xOffset, yOffset - wrapHeight, false, false);
439 }
440 }
441 }
442
443 /**
444 * Sets the width and height of the BlitMask.
445 * Keep in mind that a BlitMask should not be rotated or scaled.
446 * You can also directly set the <code>width</code> or <code>height</code> properties.
447 *
448 * @param width The width of the BlitMask
449 * @param height The height of the BlitMask
450 * @see #width
451 * @see #height
452 **/
453 public function setSize(width:Number, height:Number):void {
454 if (_width == width && _height == height) {
455 return;
456 } else if (width < 0 || height < 0) {
457 throw new Error("A BlitMask cannot have a negative width or height.");
458 } else if (_bd != null) {
459 _bd.dispose();
460 }
461 _width = width;
462 _height = height;
463 _bd = new BitmapData(width + 1, height + 1, true, _fillColor);
464 _render();
465 }
466
467 /** @private **/
468 protected function _mouseEventPassthrough(event:MouseEvent):void {
469 if (this.mouseEnabled && (!_bitmapMode || this.hitTestPoint(event.stageX, event.stageY, false))) {
470 dispatchEvent(event);
471 }
472 }
473
474 /**
475 * Identical to setting <code>bitmapMode = true</code> but this method simplifies adding that
476 * functionality to tweens or using it as an event handler. For example, to enable bitmapMode at
477 * the beginning of a tween and then disable it when the tween completes, you could do: <br /><br /><code>
478 *
479 * TweenLite.to(mc, 3, {x:400, onStart:myBlitMask.enableBitmapMode, onUpdate:myBlitMask.update, onComplete:myBlitMask.disableBitmapMode});
480 * </code>
481 *
482 * @param event An optional Event that isn't used internally but makes it possible to use the method as an event handler like <code>addEventListener(MouseEvent.CLICK, myBlitMask.enableBitmapMode)</code>.
483 * @see #disableBitmapMode()
484 * @see #bitmapMode
485 */
486 public function enableBitmapMode(event:Event=null):void {
487 this.bitmapMode = true;
488 }
489
490 /**
491 * Identical to setting <code>bitmapMode = false</code> but this method simplifies adding that
492 * functionality to tweens or using it as an event handler. For example, to enable bitmapMode at
493 * the beginning of a tween and then disable it when the tween completes, you could do: <br /><br /><code>
494 *
495 * TweenLite.to(mc, 3, {x:400, onStart:myBlitMask.enableBitmapMode, onUpdate:myBlitMask.update, onComplete:myBlitMask.disableBitmapMode});
496 * </code>
497 *
498 * @param event An optional Event that isn't used internally but makes it possible to use the method as an event handler like <code>addEventListener(MouseEvent.CLICK, myBlitMask.disableBitmapMode)</code>.
499 * @see #enableBitmapMode()
500 * @see #bitmapMode
501 */
502 public function disableBitmapMode(event:Event=null):void {
503 this.bitmapMode = false;
504 }
505
506 /**
507 * Repositions the <code>target</code> so that it is visible within the BlitMask, as though <code>wrap</code>
508 * was enabled (this method is called automatically when <code>bitmapMode</code> is disabled while <code>wrap</code>
509 * is <code>true</code>). For example, if you tween the <code>target</code> way off the edge of the BlitMask and
510 * have <code>wrap</code> enabled, it will appear to come back in from the other side even though the raw coordinates
511 * of the target would indicate that it is outside the BlitMask. If you want to force the coordinates to normalize
512 * so that they reflect that wrapped position, simply call <code>normalizePosition()</code>. It will automatically
513 * choose the coordinates that would maximize the visible portion of the target if a seam is currently showing.
514 **/
515 public function normalizePosition():void {
516 if (_target && _bounds) {
517 var wrapWidth:int = (_bounds.width + _wrapOffsetX + 0.5) >> 0;
518 var wrapHeight:int = (_bounds.height + _wrapOffsetY + 0.5) >> 0;
519 var offsetX:Number = (_bounds.x - this.x) % wrapWidth;
520 var offsetY:Number = (_bounds.y - this.y) % wrapHeight;
521
522 if (offsetX > (_width + _wrapOffsetX) / 2) {
523 offsetX -= wrapWidth;
524 } else if (offsetX < (_width + _wrapOffsetX) / -2) {
525 offsetX += wrapWidth;
526 }
527 if (offsetY > (_height + _wrapOffsetY) / 2) {
528 offsetY -= wrapHeight;
529 } else if (offsetY < (_height + _wrapOffsetY) / -2) {
530 offsetY += wrapHeight;
531 }
532
533 _target.x += this.x + offsetX - _bounds.x;
534 _target.y += this.y + offsetY - _bounds.y;
535 }
536 }
537
538 /** Disposes of the BlitMask and its internal BitmapData instances, releasing them for garbage collection. **/
539 public function dispose():void {
540 if (_bd == null) { //already disposed.
541 return;
542 }
543 _disposeGrid();
544 _bd.dispose();
545 _bd = null;
546 this.bitmapMode = false;
547 this.autoUpdate = false;
548 if (_target != null) {
549 _target.mask = null;
550 }
551 if (this.parent != null) {
552 this.parent.removeChild(this);
553 }
554 this.target = null;
555 }
556
557//---- GETTERS / SETTERS --------------------------------------------------------------------
558
559 /**
560 * When <code>true</code>, the BlitMask optimizes itself for performance by setting the <code>target's</code>
561 * <code>visible</code> property to <code>false</code> (greatly reducing the load on Flash's graphics rendering
562 * routines) and uses its internally cached bitmap version of the <code>target</code> to redraw only the necessary
563 * pixels inside the masked area. Since only a bitmap version of the <code>target</code> is shown while in bitmapMode,
564 * the <code>target</code> won't be interactive. So if you have buttons and other objects that normally react to
565 * MouseEvents, they won't while in bitmapMode. If you need the interactivity, simply set <code>bitmapMode</code>
566 * to <code>false</code> and then it will turn the <code>target's</code> <code>visible</code> property back to <code>true</code>
567 * and its <code>mask</code> property to the BlitMask itself. Typically it is best to turn bitmapMode on at least when you're
568 * animating the <code>target</code> or the BlitMask itself, and then when the tween/animation is done and you need
569 * interactivity, set bitmapMode back to false. For example: <br /><br /><code>
570 *
571 * var bm:BlitMask = new BlitMask(mc, 0, 0, 300, 200, true);<br /><br />
572 *
573 * TweenLite.to(mc, 3, {x:200, onUpdate:bm.update, onComplete:completeHandler});<br /><br />
574 *
575 * function completeHandler():void {<br />
576 * bm.bitmapMode = false;<br />
577 * }<br />
578 * </code><br /><br />
579 *
580 * @see #enableBitmapMode()
581 * @see #disableBitmapMode()
582 **/
583 public function get bitmapMode():Boolean {
584 return _bitmapMode;
585 }
586 public function set bitmapMode(value:Boolean):void {
587 if (_bitmapMode != value) {
588 _bitmapMode = value;
589 if (_target != null) {
590 _target.visible = !_bitmapMode;
591 update(null);
592 if (_bitmapMode) {
593 this.filters = _target.filters;
594 this.transform.colorTransform = _transform.colorTransform;
595 this.blendMode = _target.blendMode;
596 _target.mask = null;
597 } else {
598 this.filters = _emptyArray;
599 this.transform.colorTransform = _colorTransform;
600 this.blendMode = "normal";
601 this.cacheAsBitmap = false; //if cacheAsBitmap is true on both the _target and the FlexBlitMask instance, the transparent areas of the mask will be...well...transparent which isn't what we want when bitmapMode is false (it could hide visible areas unless update(null, true) is called regularly, like if the target has animated children and bitmapMode is false)
602 _target.mask = this;
603 if (_wrap) {
604 normalizePosition();
605 }
606 }
607 if (_bitmapMode && _autoUpdate) {
608 this.addEventListener(Event.ENTER_FRAME, update, false, -10, true);
609 } else {
610 this.removeEventListener(Event.ENTER_FRAME, update);
611 }
612 }
613 }
614 }
615
616 /**
617 * If <code>true</code>, the BlitMask will automatically watch the <code>target</code> to see if
618 * its position/scale/rotation has changed on each frame (while <code>bitmapMode</code> is <code>true</code>)
619 * and if so, it will <code>update()</code> to make sure the BlitMask always stays synced with the <code>target</code>.
620 * This is the easiest way to use BlitMask but it is slightly less efficient than manually calling <code>update()</code>
621 * whenever you need to. Keep in mind that if you're tweening with TweenLite or TweenMax, you can simply set
622 * its <code>onUpdate</code> to the BlitMask's <code>update()</code> method to keep things synced.
623 * Like <code>onUpdate:myBlitMask.update</code>.
624 **/
625 public function get autoUpdate():Boolean {
626 return _autoUpdate;
627 }
628 public function set autoUpdate(value:Boolean):void {
629 if (_autoUpdate != value) {
630 _autoUpdate = value;
631 if (_bitmapMode && _autoUpdate) {
632 this.addEventListener(Event.ENTER_FRAME, update, false, -10, true);
633 } else {
634 this.removeEventListener(Event.ENTER_FRAME, update);
635 }
636 }
637 }
638
639 /** The target DisplayObject that the BlitMask should mask **/
640 public function get target():DisplayObject {
641 return _target;
642 }
643 public function set target(value:DisplayObject):void {
644 if (_target != value) {
645 var i:int = _mouseEvents.length;
646 if (_target != null) {
647 while (--i > -1) {
648 _target.removeEventListener(_mouseEvents[i], _mouseEventPassthrough);
649 }
650 }
651 _target = value;
652 if (_target != null) {
653 i = _mouseEvents.length;
654 while (--i > -1) {
655 _target.addEventListener(_mouseEvents[i], _mouseEventPassthrough, false, 0, true);
656 }
657 _prevMatrix = null;
658 _transform = _target.transform;
659 _bitmapMode = !_bitmapMode;
660 this.bitmapMode = !_bitmapMode; //forces a refresh (applying the mask, doing an update(), etc.)
661 } else {
662 _bounds = new Rectangle();
663 }
664 }
665 }
666
667 /** x coordinate of the BlitMask (it will automatically be forced to whole pixel values if <code>smoothing</code> is <code>false</code>). **/
668 override public function get x():Number {
669 return super.x;
670 }
671 override public function set x(value:Number):void {
672 if (_smoothing) {
673 super.x = value;
674 } else if (value >= 0) {
675 super.x = (value + 0.5) >> 0;
676 } else {
677 super.x = (value - 0.5) >> 0;
678 }
679 if (_bitmapMode) {
680 _render();
681 }
682 }
683
684 /** y coordinate of the BlitMask (it will automatically be forced to whole pixel values if <code>smoothing</code> is <code>false</code>). **/
685 override public function get y():Number {
686 return super.y;
687 }
688 override public function set y(value:Number):void {
689 if (_smoothing) {
690 super.y = value;
691 } else if (value >= 0) {
692 super.y = (value + 0.5) >> 0;
693 } else {
694 super.y = (value - 0.5) >> 0;
695 }
696 if (_bitmapMode) {
697 _render();
698 }
699 }
700
701 /** Width of the BlitMask **/
702 override public function get width():Number {
703 return _width;
704 }
705 override public function set width(value:Number):void {
706 setSize(value, _height);
707 }
708
709 /** Height of the BlitMask **/
710 override public function get height():Number {
711 return _height;
712 }
713 override public function set height(value:Number):void {
714 setSize(_width, value);
715 }
716
717 /** scaleX (warning: altering the scaleX won't actually change its value - instead, it affects the <code>width</code> property accordingly) **/
718 override public function get scaleX():Number {
719 return 1;
720 }
721 override public function set scaleX(value:Number):void {
722 var oldScaleX:Number = _scaleX;
723 _scaleX = value;
724 setSize(_width * (_scaleX / oldScaleX), _height);
725 }
726
727 /** scaleY (warning: altering the scaleY won't actually change its value - instead, it affects the <code>height</code> property accordingly) **/
728 override public function get scaleY():Number {
729 return 1;
730 }
731 override public function set scaleY(value:Number):void {
732 var oldScaleY:Number = _scaleY;
733 _scaleY = value;
734 setSize(_width, _height * (_scaleY / oldScaleY));
735 }
736
737 /** Rotation of the BlitMask (always 0 because BlitMasks can't be rotated!) **/
738 override public function set rotation(value:Number):void {
739 if (value != 0) {
740 throw new Error("Cannot set the rotation of a BlitMask to a non-zero number. BlitMasks should remain unrotated.");
741 }
742 }
743
744 /**
745 * Typically a value between 0 and 1 indicating the <code>target's</code> position in relation to the BlitMask
746 * on the x-axis where 0 is at the beginning, 0.5 is scrolled to exactly the halfway point, and 1 is scrolled
747 * all the way. This makes it very easy to animate the scroll. For example, to scroll from beginning to end
748 * over 5 seconds, you could do: <br /><br /><code>
749 *
750 * myBlitMask.scrollX = 0; <br />
751 * TweenLite.to(myBlitMask, 5, {scrollX:1});
752 * </code>
753 * @see #scrollY
754 **/
755 public function get scrollX():Number {
756 return (super.x - _bounds.x) / (_bounds.width - _width);
757 }
758 public function set scrollX(value:Number):void {
759 if (_target != null && _target.parent) {
760 _bounds = _target.getBounds(_target.parent);
761 var dif:Number;
762 dif = (super.x - (_bounds.width - _width) * value) - _bounds.x;
763 _target.x += dif;
764 _bounds.x += dif;
765 if (_bitmapMode) {
766 _render();
767 }
768 }
769 }
770
771 /**
772 * Typically a value between 0 and 1 indicating the <code>target's</code> position in relation to the BlitMask
773 * on the y-axis where 0 is at the beginning, 0.5 is scrolled to exactly the halfway point, and 1 is scrolled
774 * all the way. This makes it very easy to animate the scroll. For example, to scroll from beginning to end
775 * over 5 seconds, you could do: <br /><br /><code>
776 *
777 * myBlitMask.scrollY = 0; <br />
778 * TweenLite.to(myBlitMask, 5, {scrollY:1});
779 * </code>
780 * @see #scrollX
781 **/
782 public function get scrollY():Number {
783 return (super.y - _bounds.y) / (_bounds.height - _height);
784 }
785 public function set scrollY(value:Number):void {
786 if (_target != null && _target.parent) {
787 _bounds = _target.getBounds(_target.parent);
788 var dif:Number = (super.y - (_bounds.height - _height) * value) - _bounds.y;
789 _target.y += dif;
790 _bounds.y += dif;
791 if (_bitmapMode) {
792 _render();
793 }
794 }
795 }
796
797 /**
798 * If <code>false</code> (the default), the bitmap (and the BlitMask's x/y coordinates)
799 * will be rendered only on whole pixels which is faster in terms of processing. However,
800 * for the best quality and smoothest animation, set <code>smoothing</code> to <code>true</code>.
801 **/
802 public function get smoothing():Boolean {
803 return _smoothing;
804 }
805 public function set smoothing(value:Boolean):void {
806 if (_smoothing != value) {
807 _smoothing = value;
808 _captureTargetBitmap();
809 if (_bitmapMode) {
810 _render();
811 }
812 }
813 }
814
815 /**
816 * The ARGB hexadecimal color that should fill the empty areas of the BlitMask. By default,
817 * it is transparent (0x00000000). If you wanted a red color, for example, it would be
818 * <code>0xFFFF0000</code>.
819 **/
820 public function get fillColor():uint {
821 return _fillColor;
822 }
823 public function set fillColor(value:uint):void {
824 if (_fillColor != value) {
825 _fillColor = value;
826 if (_bitmapMode) {
827 _render();
828 }
829 }
830 }
831
832 /**
833 * If <code>true</code>, the bitmap will be wrapped around to the opposite side when it scrolls off
834 * one of the edges (only in <code>bitmapMode</code> of course), like the BlitMask is filled with a
835 * grid of bitmap copies of the target. Use the <code>wrapOffsetX</code> and <code>wrapOffsetY</code>
836 * properties to affect how far apart the copies are from each other. You can reposition the
837 * <code>target</code> anywhere and BlitMask will align the copies accordingly.
838 * @see #wrapOffsetX
839 * @see #wrapOffsetY
840 **/
841 public function get wrap():Boolean {
842 return _wrap;
843 }
844 public function set wrap(value:Boolean):void {
845 if (_wrap != value) {
846 _wrap = value;
847 if (_bitmapMode) {
848 _render();
849 }
850 }
851 }
852
853 /**
854 * When <code>wrap</code> is <code>true</code>, <code>wrapOffsetX</code> controls how many pixels
855 * along the x-axis the wrapped copies of the bitmap are spaced. It is essentially the gap between
856 * the copies (although you can use a negative value or 0 to avoid any gap).
857 * @see #wrap
858 * @see #wrapOffsetY
859 **/
860 public function get wrapOffsetX():Number {
861 return _wrapOffsetX;
862 }
863 public function set wrapOffsetX(value:Number):void {
864 if (_wrapOffsetX != value) {
865 _wrapOffsetX = value;
866 if (_bitmapMode) {
867 _render();
868 }
869 }
870 }
871
872 /**
873 * When <code>wrap</code> is <code>true</code>, <code>wrapOffsetY</code> controls how many pixels
874 * along the y-axis the wrapped copies of the bitmap are spaced. It is essentially the gap between
875 * the copies (although you can use a negative value or 0 to avoid any gap).
876 * @see #wrap
877 * @see #wrapOffsetX
878 **/
879 public function get wrapOffsetY():Number {
880 return _wrapOffsetY;
881 }
882 public function set wrapOffsetY(value:Number):void {
883 if (_wrapOffsetY != value) {
884 _wrapOffsetY = value;
885 if (_bitmapMode) {
886 _render();
887 }
888 }
889 }
890
891 }
892}