Mouse Driven 3D JavaScript Carousel

08 September 2008

Permalink: http://douggreenall.co.uk/site/?p=232

Right, well I think I'm slowly starting to get the hang of this whole blogging thing - I keep having ideas for lots of extremely impractical but fun things to do with JavaScript that I'll write up just for the sake of making them work and posting them here. Actually, I'm going to post my (considerably more practical) window manager within the next week or so, but today I'll explain how the carousel that I demonstrated in my second post works. Let's start off with a working example:

Item Count:
Click And Drag To Spin

Click Here To Add Item
Click Here To Reset

Very nice, I'm sure you'll agree. You can spin the carousel in different directions by holding the mouse button down and dragging across it (trying to click and drag it directly can cause problems, especially in Firefox where the browser may try to drag one of the images and hence won't fire the mousemove event - if you click adjacent to the carousel itself and then move the mouse across, you shouldn't have any problems... or you could do what I do and just use Opera) - its rotation speed is determined by how quickly you move the mouse and once moving, it will slowly decelerate to a complete halt. Alternatively, you can click it while it's moving and it will stop immediately. Additionally, when it is stopped each item acts as a hyperlink which you can use to... well you know what hyperlinks do.

So how does it work? Well what we're essentially doing is rotating a single vector for each carousel item around a common axis and using the components of those vectors to position and scale the elements that represent the items (in this case, images). Now there's actually a number of ways we can achieve this effect - one approach we could take would be to implement some 3D maths, setting up a rotation matrix to transform each vector around the y-axis and subsequently using the x-component of each transformed vector to position the respective image and the z-component to scale its dimensions (similar to a 3D to 2D projection). To visualise this, let's have a quick look at 3D axes and a projection of the carousel path within the 3D space:

3D axes3D axes with carousel path


Now although this would work, the approach is actually somewhat naive. If we observe the vector component values through the rotation, it soon becomes apparent that the y-component remains constant and isn't actually being used for transforming the carousel items. It's always good practice in programming to eliminate complexity as much as possible and as 3D matrix transformations require a number of calculations to perform, it would be far better if we could find a way to remove the redundancy of the unused y-component by converting the process to a 2D space. Fortunately, the Cartesian equation of a circle allows us to do just that, by calculating an [x,y] coordinate pair and transforming the carousel items with these values instead - let's have a look at the equation (angles are in radians):

x' = x * cos(angle) - y * sin(angle)
y' = x * sin(angle) + y * cos(angle)

This formula will, given an existing point on the circle boundary, give us the coordinates of that point after it has been rotated around the circle by a particular angle. For example, the centre of the circle is at [0,0] so if we consider a vector at [0,1] (which given a circle radius of 1, lies at the very top or bottom of the circle depending on the orientation of your y-axis), applying this calculation with an angle of PI will invert its position on the edge of the circle - perhaps a diagram will help:

Diagram showing the transformation of a point around the edge of a circle

Now imagine we wanted to place n equally spaced items around the circle - to achieve this we need the same angular separation between each consecutive item with respect to the centre of the circle - calculating this separation is simply a case of dividing 2 * PI (the total number of radians within a circle) by n (the number of items) - we'll call this value d. Then, starting at 0 radians (the top of the circle), we calculate each item's [x,y] coordinates using the circle equation and the product of d and i (the item's respective index). This will produce the following (where n == 8):

Diagram showing the uniform distribution of 8 points around the edge of a circle

At this point we need to take a quick look at our coordinate system and the individual frames of reference for elements within it. First of all we should note that for a computer display, our y-axis is inverted - that is, the origin [0,0] is at the top left of the screen and the coordinates increase as we move down and right across the plane. Also, each element within the plane that can contain other elements (divs for example) has its own local coordinate system, with its origin at the top left of the element. This can be seen in the following diagram:

Diagram showing the different frames of reference within our screen coordinate system

This is important to understand because when we position our items around the circle, we have to introduce an offset to each to reposition the local origins to the centre of the item, like so:

Items placed around the edge of the circleItems placed around the edge of the circle, offset so their respective origins lie at their centres

You may have noticed that I reintroduced the z-axis to these diagrams even though we're only working with a 2D space - the reason for this is simple:if we rotate the whole coordinate system around the x-axis, we can create the illusion of 3D depth:

Diagram showing the offset items within the coordinate system rotated around the x-axis

We do this by using the y-component of each vector to change the size and z-index of its respective image, not its position. We should bear in mind of course that this is only an illusion of 3D and as a result, those items nearer the front of the carousel appear to be closer together than those at the back - this is caused by only scaling the items themselves and not the spacing between them, but I consider this to be an acceptable trade-off.

Now before we continue, in my last post I explained how to track the mouse and presented two classes that are used to achieve it:Vector2D and MouseTracker. Both of these classes are used again for the carousel so if they're unfamiliar, it might be worth reading up on them. Also, Vector2D has been augmented with the following additional methods:

  this.GetXInt = function() { return Math.round(this._x); };
  this.GetYInt = function() { return Math.round(this._y); };

  this.GetXPxl = function() { return Math.round(this._x) + 'px'; };
  this.GetYPxl = function() { return Math.round(this._y) + 'px'; };


These simply provide us with convenient values for inline styles on DOM elements (Vector2D uses floats, and styles such as left, top, width and height expect integers, potentially followed by a quantifier like px or %).

Okay, so now we have a method for implementing the carousel, it's probably time to examine how we translate this to actual code. We use two classes to do this, one to encapsulate an individual carousel item and one for the carousel itself. Let's first look at the carousel item class:

///-class-/////////////////////////////////////////////////////////////////////
// CarouselItem
///////////////////////////////////////////////////////////////////////////////
function CarouselItem(Parent, HREF, ImageSRC)
{
  ///-data-//////////////////////////////////////////////////////////////////
  ///////////////////////////////////////////////////////////////////////////

  this._parent = Parent;
  this._ID = this._parent._ID + 'Item' + this._parent._items.length;

  this._href = HREF;
  this._element = null;

  this._position = new Vector2D(0, 0);
  this._size = this._parent._itemSize.Clone();

  this._image = new Image();
  this._image.src = ImageSRC;

  ///-methods-///////////////////////////////////////////////////////////////
  ///////////////////////////////////////////////////////////////////////////

  this.GetElement = function()
  {
    if (!this._element)
      this._element = wGet(this._ID);

    return this._element;
  };

  this.GetMarkup = function()
  {
    var screenSize = this.GetScreenSize();
    var screenPosition = this.GetScreenPosition();

    return '<a href="javascript:' + this._parent._ID + '.ClickItem(\'' + this._href + '\');">\n' +
       '  <img id="' + this._ID + '" style="position:absolute; ' +
       'left:' + screenPosition.GetXPxl() + '; ' +
       'top:' + screenPosition.GetYPxl() + '; ' +
       'border:none; z-index:' + this.GetZIndex() + ';" ' +
       'src="' + this._image.src + '" ' +
       'width="' + screenSize.GetXInt() + '" ' +
       'height="' + screenSize.GetYInt() + '" ' +
       'alt="" title="" /></a>\n';
  };

  this.GetScreenPosition = function()
  {
    var returnVal = this._parent._itemPosition.Clone();
    var offset = (this._parent._itemSize._x - this._size._x) / 2;

    returnVal._x += this._position._x * (returnVal._x + offset) + offset;
    returnVal._y += this._position._y * returnVal._y;

    return returnVal;
  };

  this.GetScreenSize = function()
  {
    this._size = this._parent._itemSize.Clone();

    this._size.MulVal((this._position._y + 1.5) / 2.5);


    return this._size;
  };

  this.GetZIndex = function()
  {
    return Math.round((this._position._y + 2) * 10000);
  };

  this.Update = function()
  {
    this._parent.Rotate(this._position);

    var screenSize = this.GetScreenSize();
    var screenPosition = this.GetScreenPosition();

    if (this.GetElement())
    {
      this._element.style.left = screenPosition.GetXPxl();
      this._element.style.top = screenPosition.GetYPxl();
      this._element.width = screenSize.GetXInt();
      this._element.height = screenSize.GetYInt();

      this._element.style.zIndex = this.GetZIndex();
    }
  };
}


So CarouselItem consists of seven member variables and six methods. The variables are as follows:

  this._parent = Parent;
  this._ID = this._parent._ID + 'Item' + this._parent._items.length;

  this._href = HREF;
  this._element = null;

  this._position = new Vector2D(0, 0);
  this._size = this._parent._itemSize.Clone();

  this._image = new Image();
  this._image.src = ImageSRC;


"_parent" is the instance of the carousel class which contains the item

"_ID" is the ID of the image that we manipulate on screen

"_href" is what the item links to when clicked

"_element" is the actual DOM image that we manipulate (we store it here so we don't have to traverse the DOM on every animation frame)

"_position" is the position of the item, relative to the circle it traverses (so this is a normalised vector)

"_size" is the actual size of the item on screen (we store this to help with adjusting the display position of the item)

"_image" is stored to force JavaScript to load the item image into the browser dynamically - failing to do this can result in the image not loading properly and we'll end up with empty spaces in the carousel

Now we'll look at each of the methods in turn.

GetElement() recovers the actual element that we're going to manipulate from the DOM and returns it to the caller. We store this object internally to minimise DOM traversals. As you can see, the only time we actually recover the element is when we haven't already got a valid reference to it (the wGet function is shorthand for window.document.getElementById() by the way and is defined in Support.js along with Vector2D). Otherwise, we just return our member variable:

  this.GetElement = function()
  {
    if (!this._element)
      this._element = wGet(this._ID);

    return this._element;
  };


GetMarkup() returns the raw HTML that draws the image - we use this by dumping it straight into the div containing the carousel using its innerHTML property:

  this.GetMarkup = function()
  {
    var screenSize = this.GetScreenSize();
    var screenPosition = this.GetScreenPosition();

    return '<a href="javascript:' + this._parent._ID + '.ClickItem(\'' + this._href + '\');">\n' +
       '  <img id="' + this._ID + '" style="position:absolute; ' +
       'left:' + screenPosition.GetXPxl() + '; ' +
       'top:' + screenPosition.GetYPxl() + '; ' +
       'border:none; z-index:' + this.GetZIndex() + ';" ' +
       'src="' + this._image.src + '" ' +
       'width="' + screenSize.GetXInt() + '" ' +
       'height="' + screenSize.GetYInt() + '" ' +
       'alt="" title="" /></a>\n';
  };


Next up is GetScreenPosition() - this translates our position vector into actual screen coordinates:

  this.GetScreenPosition = function()
  {
    var returnVal = this._parent._itemPosition.Clone();
    var offset = (this._parent._itemSize._x - this._size._x) / 2;

    returnVal._x += this._position._x * (returnVal._x + offset) + offset;
    returnVal._y += this._position._y * returnVal._y;

    return returnVal;
  };


It's probably worth illustrating what the calculations above are actually doing - first off all we place the item according to the base position defined in the parent carousel class. Then we offset the item according to its position on the circle:


baseScreenX + (circleX * baseScreenX)

Next we shift the item according to the difference between its base size and its current size - we divide the difference by two in order to position the item's local origin at its centre (as described above):


baseScreenX + (circleX * baseScreenX) + ((baseSizeX - thisSizeX) / 2)

Finally we apply another offset to the item so it reaches the boundary of the carousel layer:


baseScreenX + (circleX * baseScreenX) + ((baseSizeX - thisSizeX) / 2) + (circleX * ((baseSizeX - thisSizeX) / 2))

As we use the value ((baseSizeX - thisSizeX) / 2) twice, we precalculate it and store it in a local variable called "offset". After rearranging the equation, we get the following final calculation for the item's onscreen x-coordinate:

offset = (baseSizeX - thisSizeX) / 2
position = baseScreenX + (circleX * (baseScreenX + offset)) + offset

The y-coordinate is somewhat simpler to calculate - we simply add to the item's initial screen y-coordinate, its current y-position on the circle multiplied by its initial screen y-position. Doing this gives us a range of screen locations from 0 to 2 * the initial screen y-position:

baseScreenY == 10
circleY == -1:10 + (-1 * 10) = 10 - 10 = 0
circleY == 1:10 + (1 * 10) = 10 + 10 = 20

Next we have GetScreenSize() which calculates the size of the item on screen, based on its position:

  this.GetScreenSize = function()
  {
    this._size = this._parent._itemSize.Clone();

    this._size.MulVal((this._position._y + 1.5) / 2.5);

    return this._size;
  };


Remember that our position vector y-component ranges from 1 to -1, depending on where it is on the circle. As we're going to multiply the item's base size by this value and we can't have a size greater than the base or smaller than 0, we need to apply a scalar to the y-component before we perform the multiplication. Here I've added 1.5 and then divided by 2.5 - let's look at what this will do to the extremes of the y range:

y == -1: (-1 + 1.5) / 2.5 = 0.5 / 2.5 = 0.2
y == 1:(1 + 1.5) / 2.5 = 2.5 / 2.5 = 1

As you can see, this will cause an item right at the front of the carousel to be scaled to its base size (so not scaled at all), while an item right at the back will be scaled to 1/5 of its base size. You can change these values arbitrarily as long as the divisor is equal to one plus the offset.

GetZIndex() calculates the z-index of the item - again we scale the y-component before use so as not to have a negative value, but we also multiply it by 10000 before rounding it. We do this to acquire a large integer range of z values from the original floating point value:

  this.GetZIndex = function()
  {
    return Math.round((this._position._y + 2) * 10000);
  };


Finally, Update() is called by the parent class on every frame of animation and is responsible for actually moving the item around the carousel. First it rotates the item's position based on the current rotation speed, which is set in the parent carousel class. It then recovers the updated position, size and z-index of the item and applies them to the DOM element:

  this.Update = function()
  {
    this._parent.Rotate(this._position);

    var screenSize = this.GetScreenSize();
    var screenPosition = this.GetScreenPosition();

    if (this.GetElement())
    {
      this._element.style.left = screenPosition.GetXPxl();
      this._element.style.top = screenPosition.GetYPxl();
      this._element.width = screenSize.GetXInt();
      this._element.height = screenSize.GetYInt();

      this._element.style.zIndex = this.GetZIndex();
    }
  };


Right, so that's the individual items taken care of - now we'll have a look at the carousel itself:

///-class-/////////////////////////////////////////////////////////////////////
// Carousel
///////////////////////////////////////////////////////////////////////////////
function Carousel(ID, LayerID, MouseTracker, CarouselWidth, ItemWidth, ItemHeight)
{
  ///-data-//////////////////////////////////////////////////////////////////
  ///////////////////////////////////////////////////////////////////////////

  this._ID = ID;
  this._layerID = LayerID;
  this._interval = '';
  this._items = new Array();
  this._width = CarouselWidth;
  this._itemSize = new Vector2D(ItemWidth, ItemHeight);
  this._itemPosition = new Vector2D((this._width - this._itemSize._x) / 2, 10);
  this._rotateSpeed = 0;
  this._rotateCos = 0;
  this._rotateSin = 0;
  this._mouseTracker = MouseTracker;

  ///-methods-///////////////////////////////////////////////////////////////
  ///////////////////////////////////////////////////////////////////////////

  this.AddItem = function(HREF, ImageSRC)
  {
    this._items[this._items.length] = new CarouselItem(this, HREF, ImageSRC);
  };

  this.DrawItems = function()
  {
    var layer = wGet(this._layerID);
    if (layer)
    {
      layer.style.width = this._width + 'px';
      layer.style.height = (this._itemPosition._y * 2 + this._itemSize._y) + 'px';

      this.SetRotate((2 * Math.PI) / this._items.length);

      var itemPosition = new Vector2D(0, 1);
      var markup = '';

      for (var count = 0; count < this._items.length; ++count)
      {
        this._items[count]._element = null;
        this._items[count]._position = itemPosition.Clone();
        markup += this._items[count].GetMarkup();

        this.Rotate(itemPosition);
      }

      layer.innerHTML = markup;
    }
  };

  this.Animate = function()
  {
    this.Decelerate();

    for (var count = 0; count < this._items.length; ++count)
      this._items[count].Update();

    if (this._rotateSpeed >= 0 && this._rotateSpeed < 0.1 ||
      this._rotateSpeed < 0 && this._rotateSpeed > -0.1)
      this.Stop();
  };

  this.Start = function()
  {
    if (this._interval == '')
      this._interval = setInterval(this._ID + '.Animate()', 10);
  };

  this.Stop = function()
  {
    if (this._interval != '')
      clearInterval(this._interval);

    this._interval = '';
  };

  this.SetRotate = function(Angle)
  {
    this._rotateCos = Math.cos(Angle);
    this._rotateSin = Math.sin(Angle);
  };

  this.Rotate = function(Vector)
  {
    var clone = Vector.Clone();
    Vector._x = this._rotateCos * clone._x - this._rotateSin * clone._y;
    Vector._y = this._rotateSin * clone._x + this._rotateCos * clone._y;
  };

  this.Decelerate = function()
  {
    this._rotateSpeed *= 0.96;
    this.SetRotate((this._rotateSpeed * Math.PI) / 500);
  };

  this.MouseDown = function()
  {
    this._rotateSpeed = 0;
  };

  this.Drag = function()
  {
    if (this._mouseTracker._buttonDown)
    {
      this._rotateSpeed = -this._mouseTracker._acceleration._x;

      this._mouseTracker.Reset();
      this.Start();
    }
  };

  this.ClickItem = function(ItemID)
  {
    if (this._rotateSpeed == 0 && !this._mouseTracker._buttonDown)
      alert('You clicked item ' + ItemID);
  };
}


We'll start as ever by examining the class member variables:

  this._ID = ID;
  this._layerID = LayerID;
  this._interval = '';
  this._items = new Array();
  this._width = CarouselWidth;
  this._itemSize = new Vector2D(ItemWidth, ItemHeight);
  this._itemPosition = new Vector2D((this._width - this._itemSize._x) / 2, 10);
  this._rotateSpeed = 0;
  this._rotateCos = 0;
  this._rotateSin = 0;
  this._mouseTracker = MouseTracker;


"_ID" is the name of the variable used to hold our instance of Carousel - we store this so the JavaScript engine can refer to the correct instance during the animation loop

"_layerID" is the ID of the carousel div element, which looks something like the following:

<div id="carouselDiv" class="noSelect" style="position:relative; left:0px; top:0px; width:1px; height:1px;" onmousedown="javascript:carousel.MouseDown(event);" onmousemove="javascript:carousel.Drag();"></div>


"_interval" stores the string name of the animation interval and is returned from the setInterval() JavaScript function

"_items" is an array to hold CarouselItem instances

"_width" is the onscreen width of the carousel

"_itemSize" is a vector to hold the base size of an item (when it is scaled at 1:1)

"_itemPosition" is a vector to hold the base position of the item - the y-component is set to 10 here and determines how far the items at the front of the carousel are from the top of the carousel div, while the x-component is offset to shift the origin to the middle of the item, as described above

"_rotateSpeed" is the speed at which the carousel rotates

"_rotateCos" and "_rotateSin" are the cosine and sine values used in the circle equation - the equation is used for each item individually on every animation frame, so we calculate and store these values whenever the rotation speed changes to reduce the total number of calculations we have to perform

"_mouseTracker" is an instance of our MouseTracker class so we can have the carousel react to the mouse

Let's have a look at Carousel's methods:

AddItem() does what you might expect and adds a new CarouselItem instance to the carousel:

  this.AddItem = function(HREF, ImageSRC)
  {
    this._items[this._items.length] = new CarouselItem(this, HREF, ImageSRC);
  };


Likewise, DrawItems() is pretty self-explanatory and draws the items to the screen:

  this.DrawItems = function()
  {
    var layer = wGet(this._layerID);
    if (layer)
    {
      layer.style.width = this._width + 'px';
      layer.style.height = (this._itemPosition._y * 2 + this._itemSize._y) + 'px';

      this.SetRotate((2 * Math.PI) / this._items.length);

      var itemPosition = new Vector2D(0, 1);
      var markup = '';

      for (var count = 0; count < this._items.length; ++count)
      {
        this._items[count]._element = null;
        this._items[count]._position = itemPosition.Clone();
        markup += this._items[count].GetMarkup();

        this.Rotate(itemPosition);
      }

      layer.innerHTML = markup;
    }
  };


First it checks that the div that holds the carousel exists and if so, gets a reference to it. Then it sets the width and height of the div before looping through the carousel items, uniformly positioning each one and collating the markup into a string. It also resets the internal item element references to null - it does this because when we output the new markup to the div, any previously created element references will become invalid and will need to be recreated - manually setting these to null here will force the item's GetElement() method to create a reference to the new element when we start the animation. Finally, we dump the complete carousel markup to the carousel div using its innerHTML property.

Animate() is the main carousel animation loop - this is called on every frame and starts off by calling Decelerate() to slow the rotation of the carousel. Next it loops through the items, forcing each to update in turn. It then compares the carousel speed to a threshold to determine whether to stop the animation loop:

  this.Animate = function()
  {
    this.Decelerate();

    for (var count = 0; count < this._items.length; ++count)
      this._items[count].Update();

    if (this._rotateSpeed >= 0 && this._rotateSpeed < 0.1 ||
      this._rotateSpeed < 0 && this._rotateSpeed > -0.1)
      this.Stop();
  };


Start() initialises the animation loop. It first tests if the carousel is already moving by checking whether we have a valid interval (the JavaScript function setInterval() executes code passed as a string at a regular interval and returns a string to identify that interval). If not, it creates one to call the Animate() method every 10 milliseconds.

Note how we've used "_ID" here to reference the current Carousel instance as a string - "_ID" must contain the name of the variable which holds our Carousel instance or this code won't execute:

  this.Start = function()
  {
    if (this._interval == '')
      this._interval = setInterval(this._ID + '.Animate()', 10);
  };


Stop() is quite simple really - if we have an active interval, it stops it being executed using the JavaScript clearInterval() function and then empties the string containing the interval identifier:

  this.Stop = function()
  {
    if (this._interval != '')
      clearInterval(this._interval);

    this._interval = '';
  };


SetRotate() calculates the cosine and sine values of the angle which it is passed - these are used in the circle equation to rotate the carousel items:

  this.SetRotate = function(Angle)
  {
    this._rotateCos = Math.cos(Angle);
    this._rotateSin = Math.sin(Angle);
  };


Rotate() applies the circle equation to a passed Vector2D instance using the current instance values:

  this.Rotate = function(Vector)
  {
    var clone = Vector.Clone();
    Vector._x = this._rotateCos * clone._x - this._rotateSin * clone._y;
    Vector._y = this._rotateSin * clone._x + this._rotateCos * clone._y;
  };


Decelerate() simply applies a scalar to the speed of the carousel and forces the calculation of the new rotation values:

  this.Decelerate = function()
  {
    this._rotateSpeed *= 0.96;
    this.SetRotate((this._rotateSpeed * Math.PI) / 500);
  };


MouseDown() stops the carousel by setting its speed to 0:

  this.MouseDown = function()
  {
    this._rotateSpeed = 0;
  };


Drag() first checks to see if the mouse button is depressed - if so it sets the rotation speed of the carousel to the acceleration of the mouse, before resetting the mouse tracker and starting the animation:

  this.Drag = function()
  {
    if (this._mouseTracker._buttonDown)
    {
      this._rotateSpeed = -this._mouseTracker._acceleration._x;

      this._mouseTracker.Reset();
      this.Start();
    }
  };


Last but not least, ClickItem() simply responds to an item being clicked - we could redirect the page here to the target URL of the clicked item, but I'm just showing an alert box for illustrative purposes:

  this.ClickItem = function(ItemID)
  {
    if (this._rotateSpeed == 0 && !this._mouseTracker._buttonDown)
      alert('You clicked item ' + ItemID);
  };
}


Finally, to initialise the carousel we use the following code:

<script language="JavaScript" type="text/JavaScript" src="js/Support.js"></script>
<script language="JavaScript" type="text/JavaScript" src="js/Carousel.js"></script>
<script language="JavaScript" type="text/JavaScript">

var mouseTracker = new MouseTracker();
var carousel = new Carousel('carousel', 'carouselDiv', mouseTracker, 500, 50, 50);

carousel.AddItem('1', 'images/icon1.gif');
carousel.AddItem('2', 'images/icon2.gif');
carousel.AddItem('3', 'images/icon3.gif');
carousel.AddItem('4', 'images/icon4.gif');

window.onload = function()
{
  carousel.DrawItems();
};
</script>

<div id="carouselDiv" style="position:relative; left:0px; top:0px; width:1px; height:1px;" onmousedown="javascript:carousel.MouseDown(event);" onmousemove="javascript:carousel.Drag();">
</div>


Okay well I think that about covers it - if you have any questions / problems etc. feel free to drop me a line. Also, I'd be interested to find out how many items you can run the carousel with on different platforms and with different image resolutions - as an example I can run it quite nicely in Opera on an Athlon 64 3000, WinXP, 1GB RAM ___^^___ 500x64x64 (the above configuration) with about 150 items. Have fun.

Download Support.js
Download Carousel.js


Addendum

I received a comment from Michael asking how to add some sort of mechanism to allow the items to rotate to the front when clicked - this involves just a couple of minor changes to the code. First of all, let's look at the differences in CarouselItem:

///-class-/////////////////////////////////////////////////////////////////////
// CarouselItem
///////////////////////////////////////////////////////////////////////////////
function CarouselItem(Parent, HREF, ImageSRC)
{
  ///-data-//////////////////////////////////////////////////////////////////
  ///////////////////////////////////////////////////////////////////////////

  .
  .
  .
  this._index = this._parent._items.length;

  ///-methods-///////////////////////////////////////////////////////////////
  ///////////////////////////////////////////////////////////////////////////

  .
  .
  .

  this.GetMarkup = function()
  {
    .
    .
    .

    return ..ClickItem(' + this._index + ')..;
  };

  .
  .
  .

  this.InPlace = function()
  {
    return (Math.round(this._position._y * 1000) == 1000);
  };
}


As you can see, we now store the parent's array index of the item which we pass back to the parent as an argument in GetMarkup(). We also add a method called InPlace() that calculates whether or not the item is at the front of the carousel - we know that the item is at the front of the carousel when its y-position is equal to one, however because we're using floats, we'll rarely, if ever, get a value of exactly one in this variable. We therefore multiply by 1000 and round it to make the test a little more forgiving. This calculation needs tweaking depending on how many items are in the carousel and how quickly it rotates. You could instead do something like testing between a range or calculate the difference between the item's current y-position and its target y-position (1) and if the difference is less than the distance it will move on the next frame, rotate the whole carousel with that value instead - bear in mind however, that you'll perform this operation on every frame so consider performance implications before implementing it.

Now the changes to Carousel:

///-class-/////////////////////////////////////////////////////////////////////
// Carousel
///////////////////////////////////////////////////////////////////////////////
function Carousel(ID, LayerID, CarouselWidth, ItemWidth, ItemHeight)
{
  ///-data-//////////////////////////////////////////////////////////////////
  ///////////////////////////////////////////////////////////////////////////

  .
  .
  .
  this._clickedItem = 0;

  ///-methods-///////////////////////////////////////////////////////////////
  ///////////////////////////////////////////////////////////////////////////

  .
  .
  .

  this.Animate = function()
  {
    for (var count = 0; count < this._items.length; ++count)
      this._items[count].Update();

    if (this._items[this._clickedItem].InPlace())
      this.Stop();
  };

  .
  .
  .

  this.ClickItem = function(Index)
  {
    this._clickedItem = Index;

    if (this._items[this._clickedItem].InPlace())
    {
      window.open(this._items[this._clickedItem]._href);
    }
    else
    {
      if (this._items[this._clickedItem]._position._x < 0)
        this.SetRotate(-Math.PI / 50);
      else if (this._items[this._clickedItem]._position._x >= 0)
        this.SetRotate( Math.PI / 50);

      this.Start();
    }
  };
}


We keep track of the most recent item clicked in a new variable called "_clickedItem". In the Animate() method we remove the call to Decelerate() and change the stop condition to test whether the clicked item is in position or not. Finally, ClickItem() now sets our new "_clickedItem" variable to the array index of the most recently clicked item and either initialises the animation with a uniform rotation, or opens a new window with the HREF of the clicked item (if it's already at the front).

Finally we should disable the mouse behaviour - to do this we remove the MouseDown() and Drag() methods and the "_mouseTracker" variable from Carousel. Then we can create the carousel div as follows:

<div id="carouselDiv" style="position:relative; left:0px; top:0px; width:1px; height:1px;">
</div>


Mouse Driven 3D JavaScript Carousel
Status: Ready...
Close Window Maximise Window Minimise Window Minimise Window
Contact