Skip to content

Instantly share code, notes, and snippets.

@rynbyjn
Created May 13, 2011 21:49
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save rynbyjn/971374 to your computer and use it in GitHub Desktop.
Save rynbyjn/971374 to your computer and use it in GitHub Desktop.
AS3 LazySusan/Carousel class for moving objects around a circle with pseudo 3D
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 ];
}
}
}
@mkitt
Copy link

mkitt commented Jun 14, 2019

So good. 🤘

@gvarela
Copy link

gvarela commented Jun 14, 2019

so, many memories 🤘

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment