-
-
Save jdeagle/3125713 to your computer and use it in GitHub Desktop.
AS3 LazySusan/Carousel class for moving objects around a circle with pseudo 3D
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.boyajian.ui | |
{ | |
import com.greensock.TweenLite; | |
import flash.display.DisplayObject; | |
import flash.display.DisplayObjectContainer; | |
import flash.display.Sprite; | |
import flash.display.Stage; | |
import flash.events.Event; | |
import flash.events.MouseEvent; | |
import flash.filters.BlurFilter; | |
/** | |
* Creates a pseudo 3D lazy susan effect with a set of display objects. | |
* | |
* <p>This class uses TweenLite (http://www.greensock.com/tweenlite/)</p> | |
* | |
* <hr /> | |
* | |
* @author Ryan Boyajian | |
* @version 1.0.0 :: May 13, 2011 | |
*/ | |
public class LazySusan | |
extends Sprite | |
{ | |
// constants for use with spin() function | |
public static const CLOCKWISE :String = 'LazySusan.CLOCKWISE'; | |
public static const COUNTERCLOCKWISE :String = 'LazySusan.COUNTERCLOCKWISE'; | |
// event constants | |
public static const SPIN_START :String = 'LazySusan.SPIN_START'; | |
public static const SPIN_COMPLETE :String = 'LazySusan.SPIN_COMPLETE'; | |
public static const CLICK_SELECTED :String = 'LazySusan.CLICK_SELECTED'; | |
// default radius of the susan | |
private static const DEFAULT_RADIUS :uint = 300; | |
// default tilt percent determines how flat the susan rests, a value of 1 would look like a circle on the screen | |
private static const DEFAULT_TILT_PERCENT :Number = .04; | |
// makes sure when scaling that we are always visible | |
private static const SCALE_OFFSET :Number = .1; | |
// speed of the rotations, click is affected by how far back the object is from the selected object | |
private static const CLICK_TWEEN_SPEED :Number = 1; | |
private static const TWEEN_SPEED :Number = .5; | |
// sets sensitivity of the mouse down drag | |
private static const SLOW_DOWN_DRAG_FACTOR :Number = .15; | |
// determines when to stop a velocity spin and rest at a final spot | |
private static const MIN_STOP_VELOCITY :Number = 3; | |
// factor at which to slow down the velocity spin needs to be < 1 | |
private static const VELOCITY_FACTOR :Number = .8; | |
// if blur is less than this amt just set the blur amt to 0 | |
private static const BLUR_THRESHOLD :Number = 1.5; | |
// Math constants to help with performance when ther is a lot of objects | |
private static const PI :Number = 3.14159265; | |
private static const TWO_PI :Number = 6.28318531; | |
private static const RADIANS_360 :Number = TWO_PI; | |
private static const RADIANS_180 :Number = RADIANS_360 * .5; | |
private static const RADIANS_90 :Number = RADIANS_180 * .5; | |
// stage reference for mouse up action | |
private var _stage :Stage; | |
// objects to be placed in the susan | |
private var _displayObjects :Vector.<DisplayObject>; | |
// current object that is closest to the center of the screen | |
private var _selectedDisplayObject :DisplayObject; | |
// storage of the tilt percent | |
private var _tiltPercent :Number; | |
// data for all of the objects for moving around the susan | |
private var _data :Vector.<LazySusanData>; | |
// single blur filter for all objects | |
private var _blurFilter :BlurFilter; | |
// total number of objects in the susan | |
private var _numObjects :uint; | |
// the current amt of distrubution between all objects | |
private var _distributeAngle :Number; | |
// determines the amt of movement when dragging | |
private var _moveAngle :Number; | |
// for determining velocity of a spin maneuver | |
private var _velocity :Number; | |
private var _previousMouseX :Number; | |
// determine if the susan is moving | |
private var _isRotating :Boolean; | |
private var _isMouseMoving :Boolean; | |
// save current index that is selected | |
private var _currentIndex :uint; | |
// access to the radius of the susan | |
private var _radius :uint; | |
public function get radius() :Number { return _radius; } | |
public function set radius( $radius :Number ) :void | |
{ | |
_radius = $radius; | |
} | |
/** | |
* @param $container Container for the button. | |
* @param $init Initialization objects. | |
*/ | |
public function LazySusan( $container :DisplayObjectContainer = null, $init :Object = null, $radius :uint = DEFAULT_RADIUS, $tiltPercent :Number = DEFAULT_TILT_PERCENT ) | |
{ | |
if( $container ) $container.addChild( this ); | |
if( $init ) { | |
for( var it in $init ) { | |
if( this.hasOwnProperty( it ) ) this[ it ] = $init[ it ]; | |
} | |
} | |
_radius = $radius; | |
_tiltPercent = $tiltPercent; | |
init(); | |
} | |
public override function toString() :String | |
{ | |
return 'com.boyajian.ui.LazySusan'; | |
} | |
/* | |
* API | |
**************************************************************************************************** */ | |
// spins the susan one position over | |
public function spin( $direction :String = CLOCKWISE, $speed :Number = TWEEN_SPEED ) :void | |
{ | |
if( _isRotating ) return; | |
if( $direction == CLOCKWISE ) spinTo( -_distributeAngle, $speed ); | |
else spinTo( _distributeAngle, $speed ); | |
} | |
// spins the susan to a specific angle - be careful when using this as you could end up in a position outside of the distribution angle | |
public function spinTo( $deltaAngle :Number, $speed :Number = TWEEN_SPEED, $dispatchComplete :Boolean = true ) :void | |
{ | |
if( _isRotating ) return; | |
_isRotating = true; | |
dispatchEvent( new Event( LazySusan.SPIN_START ) ); | |
for( var i :uint = 0; i < _numObjects; i++ ) | |
{ | |
var newAngle :Number = _data[ i ].tweenAngle + $deltaAngle; | |
var obj :Object = { tweenAngle: newAngle, onUpdate: distributeObject, onUpdateParams: [ _data[ i ] ] }; | |
if( i == _numObjects - 1 && $dispatchComplete ) obj[ 'onComplete' ] = spinComplete; | |
TweenLite.to( _data[ i ], $speed, obj ); | |
} | |
} | |
// spins the susan to a particular index in the array of objects that this has reference to | |
public function spinToIndex( $index :uint, $speed :Number = TWEEN_SPEED ) :void | |
{ | |
if( _isRotating ) return; | |
if( _numObjects == 0 || $index < 0 || $index > _numObjects - 1 ) return; | |
var lsd :LazySusanData = _data[ $index ]; | |
if( lsd ) | |
{ | |
var delta :Number = RADIANS_90 - lsd.tweenAngle; | |
if( abs( delta ) > 0 ) spinTo( delta, $speed ); | |
} | |
} | |
// probably want all of the objects to have their registration point in the center, or add that into here... | |
public function setObjects( $displayObjects :Vector.<DisplayObject> ) :void | |
{ | |
while( this.numChildren > 0 ) this.removeChildAt( 0 ); | |
if( _data ) | |
{ | |
_data.splice( 0, _data.length ); | |
_data = null; | |
} | |
_displayObjects = $displayObjects; | |
_selectedDisplayObject = _displayObjects[ 0 ]; | |
_numObjects = _displayObjects.length; | |
_distributeAngle = -( RADIANS_360 / _numObjects ); | |
build(); | |
} | |
public function dispose() :void | |
{ | |
// kill listeners | |
disable(); | |
removeClickListeners(); | |
this.removeEventListener( Event.ENTER_FRAME, onVelocitySpin ); | |
} | |
public function enable() :void | |
{ | |
_stage.addEventListener( MouseEvent.MOUSE_DOWN, onMouseDown ); | |
} | |
public function disable() :void | |
{ | |
_stage.removeEventListener( MouseEvent.MOUSE_DOWN, onMouseDown ); | |
_stage.removeEventListener( MouseEvent.MOUSE_MOVE, onMouseMove ); | |
_stage.removeEventListener( MouseEvent.MOUSE_UP, onMouseUp ); | |
_stage.removeEventListener( Event.MOUSE_LEAVE, onMouseUp ); | |
} | |
public function get selectedObject() :DisplayObject | |
{ | |
return _selectedDisplayObject; | |
} | |
/* | |
* PRIVATE METHODS | |
**************************************************************************************************** */ | |
private function init() :void | |
{ | |
_blurFilter = new BlurFilter( 0, 0 ); | |
_moveAngle = RADIANS_360 / 360; | |
_isRotating = false; | |
if( this.stage) _stage = this.stage; | |
else this.addEventListener( Event.ADDED_TO_STAGE, onAddedToStage ); | |
} | |
private function onAddedToStage() :void | |
{ | |
this.removeEventListener( Event.ADDED_TO_STAGE, onAddedToStage ); | |
_stage = this.stage; | |
} | |
private function build() :void | |
{ | |
if( !_displayObjects ) return; | |
_data = new Vector.<LazySusanData>(); | |
var lsd :LazySusanData; | |
for( var i :uint = 0; i < _numObjects; i++ ) | |
{ | |
var zPos :Number = ( i * _distributeAngle ) + RADIANS_90; | |
var dispObj :DisplayObject = _displayObjects[ i ] as DisplayObject; | |
lsd = new LazySusanData( dispObj, zPos ); | |
this.addChild( dispObj ); | |
_data.push( lsd ); | |
} | |
// need to do this here to stack properly | |
for each( lsd in _data ) distributeObject( lsd ); | |
addClickListeners(); | |
// enable | |
enable(); | |
} | |
private function addClickListeners() :void | |
{ | |
for( var i :uint = 0; i < _numObjects; i++ ) | |
{ | |
if( _data[ i ].displayObject != _selectedDisplayObject ) _data[ i ].displayObject.addEventListener( MouseEvent.CLICK, onClickObject ); | |
else _selectedDisplayObject.addEventListener( MouseEvent.CLICK, onClickSelected ); | |
} | |
} | |
private function removeClickListeners() :void | |
{ | |
for( var i :uint = 0; i < _numObjects; i++ ) _data[ i ].displayObject.removeEventListener( MouseEvent.CLICK, onClickObject ); | |
_selectedDisplayObject.removeEventListener( MouseEvent.CLICK, onClickSelected ); | |
} | |
private function onClickSelected( $e :MouseEvent ) :void | |
{ | |
dispatchEvent( new Event( LazySusan.CLICK_SELECTED )); | |
} | |
private function onClickObject( $e :MouseEvent ) :void | |
{ | |
// if object is current selected object dispatch selected | |
// else move to the clicked on object | |
if( $e.target == _selectedDisplayObject ) onClickSelected( null ); | |
else { | |
// need to set this to false for the case where the conditional below doesn't end up moving susan | |
_isRotating = false; | |
removeClickListeners(); | |
var lsd :LazySusanData; | |
for( var i :uint = 0; i < _numObjects; i++ ) | |
{ | |
if( $e.currentTarget == _data[ i ].displayObject ) | |
{ | |
lsd = _data[ i ]; | |
break; | |
} | |
} | |
if( lsd ) | |
{ | |
// if we have 3 items in the carousel the two in the bg should be equidistant | |
// so we need to manually tell it to go counter/clockwise | |
// only move if we aren't the selected | |
if( lsd.displayObject != _selectedDisplayObject ) { | |
if( _numObjects == 3 ) { | |
//we're on the right! | |
if( lsd.displayObject.x > 0 ) spin( CLOCKWISE ); | |
// we're on the left! | |
else spin( COUNTERCLOCKWISE ); | |
} | |
else { | |
var delta :Number; | |
if( lsd.tweenAngle < RADIANS_360 && lsd.tweenAngle > RADIANS_360 - RADIANS_90 ) delta = ( RADIANS_360 + RADIANS_90 ) - lsd.tweenAngle; | |
else delta = RADIANS_90 - lsd.tweenAngle; | |
// might want to change the speed dynamically based on how far away the object is from | |
if( abs( delta ) > 0 ) spinTo( delta, CLICK_TWEEN_SPEED * ( 1 + abs( 1 - lsd.storageAngle ) ) ); | |
} | |
} | |
} | |
} | |
} | |
private function onMouseDown( $e :MouseEvent ) :void | |
{ | |
if( _isRotating ) return; | |
// remove clicks and down | |
_stage.removeEventListener( MouseEvent.MOUSE_DOWN, onMouseDown ); | |
// set props | |
_isMouseMoving = false; | |
_velocity = 0; | |
_previousMouseX = _stage.mouseX; | |
// stop tweens | |
for( var i :uint = 0; i < _numObjects; i++ ) TweenLite.killTweensOf( _data[ i ] ); | |
// add mouse move and up | |
_stage.addEventListener( MouseEvent.MOUSE_MOVE, onMouseMove ); | |
_stage.addEventListener( MouseEvent.MOUSE_UP, onMouseUp ); | |
_stage.addEventListener( Event.MOUSE_LEAVE, onMouseUp ); | |
} | |
private function onMouseMove( $e :MouseEvent ) :void | |
{ | |
_isRotating = true; | |
var mouseX :Number = _stage.mouseX; | |
_velocity = mouseX - _previousMouseX; | |
_previousMouseX = mouseX; | |
// don't move unless velocity is greater than a certain threshold | |
// only move if velocity is high enough or if we aren't currently moving | |
if( abs( _velocity ) > MIN_STOP_VELOCITY * 3 || _isMouseMoving ) | |
{ | |
if( !_isMouseMoving ) dispatchEvent( new Event( LazySusan.SPIN_START ) ); | |
_isMouseMoving = true; | |
// remove clicks | |
removeClickListeners(); | |
var angle :Number = _moveAngle * ( _velocity * SLOW_DOWN_DRAG_FACTOR ); | |
for( var i :uint = 0; i < _numObjects; i++ ) | |
{ | |
_data[ i ].tweenAngle -= angle; | |
distributeObject( _data[ i ] ); | |
} | |
} | |
} | |
private function onMouseUp( $e :Event ) :void | |
{ | |
// remove up listeners | |
_stage.removeEventListener( MouseEvent.MOUSE_MOVE, onMouseMove ); | |
_stage.removeEventListener( MouseEvent.MOUSE_UP, onMouseUp ); | |
_stage.removeEventListener( Event.MOUSE_LEAVE, onMouseUp ); | |
// only add the velocity spin if we are moving faster than the threshold | |
// else just stop at the nearest resting place | |
if( _isMouseMoving ) { | |
if( abs( _velocity ) > MIN_STOP_VELOCITY ) this.addEventListener( Event.ENTER_FRAME, onVelocitySpin ); | |
else stopAtNearestPosition(); | |
} else enable(); | |
} | |
private function onVelocitySpin( $e :Event ) :void | |
{ | |
_isRotating = true; | |
// tween velocity to close to zero | |
// then go to closest selected product | |
var angle :Number = _moveAngle * ( _velocity * SLOW_DOWN_DRAG_FACTOR ); | |
for( var i :uint = 0; i < _numObjects; i++ ) | |
{ | |
_data[ i ].tweenAngle -= angle; | |
distributeObject( _data[ i ] ); | |
} | |
if( abs( _velocity ) <= MIN_STOP_VELOCITY ) | |
{ | |
_velocity = 0; | |
this.removeEventListener( Event.ENTER_FRAME, onVelocitySpin ); | |
stopAtNearestPosition(); | |
} | |
// slowly decrease velocity based on factor | |
_velocity *= VELOCITY_FACTOR; | |
} | |
private function stopAtNearestPosition() :void | |
{ | |
// set tweening to false | |
_isRotating = false; | |
var selectedStorageAngle :Number = 0; | |
var selectedData :LazySusanData = _data[ 0 ]; | |
var selectedIndex :uint; | |
for( var i :uint = 0; i < _numObjects; i++ ) | |
{ | |
if( selectedStorageAngle < _data[ i ].storageAngle ) | |
{ | |
selectedStorageAngle = _data[ i ].storageAngle; | |
selectedData = _data[ i ]; | |
selectedIndex = i; | |
} | |
} | |
var delta :Number = RADIANS_90 - selectedData.tweenAngle; | |
if( selectedIndex != _currentIndex ) spinTo( delta, TWEEN_SPEED * .5 ); | |
else | |
{ | |
spinTo( delta, TWEEN_SPEED * .5, false ); | |
dispatchComplete( selectedData.displayObject ); | |
} | |
} | |
private function spinComplete() :void | |
{ | |
var selectedAngle :Number = 0; | |
var dispObj :DisplayObject; | |
for( var i :uint = 0; i < _numObjects; i++ ) | |
{ | |
if( _data[ i ].storageAngle > selectedAngle ) | |
{ | |
selectedAngle = _data[ i ].storageAngle; | |
dispObj = _data[ i ].displayObject; | |
_currentIndex = i; | |
} | |
} | |
dispatchComplete( dispObj ); | |
} | |
// this should happen every time the susan stops spinning | |
private function dispatchComplete( $dispObj :DisplayObject ) :void | |
{ | |
_isRotating = false; | |
_selectedDisplayObject = $dispObj; | |
addClickListeners(); | |
enable(); | |
_selectedDisplayObject.removeEventListener( MouseEvent.CLICK, onClickObject ); | |
dispatchEvent( new Event( LazySusan.SPIN_COMPLETE ) ); | |
} | |
/* | |
* FUNCTIONS FOR MOVING THE OBJECTS | |
**************************************************************************************************** */ | |
private function distributeObject( $data :LazySusanData ) :void | |
{ | |
// keeps the angle between 0 and 2 * PI | |
if( $data.tweenAngle >= RADIANS_360 ) $data.tweenAngle -= RADIANS_360; | |
else if( $data.tweenAngle < 0 ) $data.tweenAngle += RADIANS_360; | |
// faster way to calculate sin/cos | |
var sin :Number; | |
var cos :Number; | |
var angle :Number = $data.tweenAngle; | |
//always wrap input angle to -PI..PI | |
if( angle < -PI ) angle += TWO_PI; | |
else if( angle > PI ) angle -= TWO_PI; | |
// computing sin and cos manually is much faster than using the Math class functions | |
// compute sine | |
if( angle < 0 ) sin = 1.27323954 * angle + 0.405284735 * angle * angle; | |
else sin = 1.27323954 * angle - 0.405284735 * angle * angle; | |
// compute cosine: sin(angle + PI/2) = cos(angle) | |
angle += 1.57079632; | |
if( angle > PI ) angle -= TWO_PI; | |
if( angle < 0 ) cos = 1.27323954 * angle + 0.405284735 * angle * angle; | |
else cos = 1.27323954 * angle - 0.405284735 * angle * angle; | |
// will give us a number between 0 and 1 with 1 being the closest object to bottom / middle | |
var newZ :Number = sin * .5 + .5; | |
// will give us a reversed tilt factor so that as the circle becomes more upright the blur and scale are effected by it. | |
var tiltFactor :Number = abs( _tiltPercent - 1 ); | |
// set blur and distribute | |
var revZ :Number = abs( newZ - 1 ); | |
var blurAmt :Number = interp( 0, 3 * tiltFactor, revZ ); | |
blurAmt = blurAmt <= BLUR_THRESHOLD ? 0 : blurAmt; | |
_blurFilter.blurX = _blurFilter.blurY = blurAmt; | |
// the closer _tiltPercent is to 1 the closer newScale should be to 1 for all objects | |
// TODO: this feels weird because the selected in the front never gets to the full scale + SCALE_OFFSET, unless _tiltPercent = 1 | |
var newScale :Number = ( _tiltPercent * ( _tiltPercent - newZ ) ) + newZ + ( SCALE_OFFSET * ( 1 + tiltFactor ) ); | |
// var newScale :Number = newZ + SCALE_OFFSET; | |
$data.distribute( cos * _radius, sin * ( _radius * _tiltPercent ), newScale, _blurFilter ); | |
// figure out which level to add these | |
stack( $data, newZ ); | |
// reset pos | |
$data.storageAngle = newZ; | |
} | |
private function stack( $data :LazySusanData, $newZ :Number ) :void | |
{ | |
var level :uint = Math.floor( interp( 0, _numObjects - 1, $newZ ) ); | |
if( this.getChildIndex( $data.displayObject ) != level ) this.addChildAt( $data.displayObject, level ); | |
} | |
private function abs( $value :Number ) :Number | |
{ | |
return ( $value < 0 ) ? $value * -1 : $value; | |
} | |
private function interp( $lower :Number, $upper :Number, $n :Number ) :Number | |
{ | |
return ( ( $upper - $lower ) * $n ) + $lower; | |
} | |
} | |
} | |
import flash.display.DisplayObject; | |
import flash.filters.BlurFilter; | |
/** | |
* Data object for use with the LazySusan. | |
* | |
* <p>Holds onto properties and does the distribution of the objects.</p> | |
* | |
* @author Ryan Boyajian | |
* @version 1.0.0 :: May 13, 2011 | |
*/ | |
class LazySusanData | |
{ | |
public var displayObject :DisplayObject; | |
public var tweenAngle :Number; | |
public var storageAngle :Number; | |
public function LazySusanData( $dispObj :DisplayObject, $tweenAngle :Number ) | |
{ | |
this.displayObject = $dispObj; | |
this.tweenAngle = this.storageAngle = $tweenAngle; | |
} | |
public function distribute( $xp :Number, $yp :Number, $scale :Number, $blurFilter :BlurFilter ) :void | |
{ | |
this.displayObject.x = $xp; | |
this.displayObject.y = $yp; | |
this.displayObject.scaleX = this.displayObject.scaleY = $scale; | |
if( $blurFilter.blurX == 0 && $blurFilter.blurY == 0 ) { | |
this.displayObject.filters = []; | |
} else { | |
this.displayObject.filters = [ $blurFilter ]; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment