Pages

Showing posts with label slide picture. Show all posts
Showing posts with label slide picture. Show all posts

Wednesday, September 15, 2010

Horizontal Scrolling Menu made with CSS and jQuery

There are a lot of cool flash scrolling menus out there, but I decided to make a similarly looking menu with just CSS and jQuery. I couldn't achieve the same smoothness in animation, but anyway I'm really satisfied with the result. My menu works fine in all major browsers and degrades gracefully when Javascript is turned off.
In case you need a vertical version of a scrolling menu, please go to my newer tutorial.

View the Result

Creating markup

Let's begin by creating the necessary HTML structure. We will use an unordered list with each list item containing an image and a caption. We will also add an additional wrapper element.
<div class="sc_menu">
  <ul class="sc_menu">
    <li><a href="#">
      <img src="img/1.jpg" alt="Menu"/><span>Menu</span>
    </a></li>
    <li><a href="#">
      <img src="img/2.jpg" alt="Navigation"/><span>Navigation</span>
    </a></li>
    <li><a href="#">
      <img src="img/3.jpg" alt="Scrolling"/><span>Scrolling</span>
    </a></li>
    <li><a href="#">
      <img src="img/4.jpg" alt="jQuery"/><span>jQuery</span>
    </a></li>
  </ul>
</div>

Adding basic styling

Now we need to add some some CSS rules.
div.sc_menu {
  /* Set it so we could calculate the offsetLeft */
  position: relative;
  height: 145px;
  width: 500px;
  /* Add scroll-bars */
  overflow: auto;
}
ul.sc_menu {
  display: block;
  height: 110px;
  /* Max width here, for users without Javascript */
  width: 1500px;
  padding: 15px 0 0 15px;
  /* Remove default margin */
  margin: 0;
  background: url('navigation.png');
  list-style: none;
}
.sc_menu li {
  display: block;
  float: left;
  padding: 0 4px;
}
.sc_menu a {
  display: block;
  text-decoration: none;
}
.sc_menu span {
  /* We want a caption to display on the next line */
  display: block;
  margin-top: 3px;
  text-align: center;
  font-size: 12px;
  color: #fff;
}
The "width" and "overflow" properties are used to add a scroll-bar to the wrapper div. We set the "position" property for easier offset calculation with Javascipt. Don't forget that offset is always calculated relative to the positioned parent. You can view what we've done so far here

Adding hover effects and borders

The "display: none" property hides captions, and we will add "display:block" to ":hover" rule to show them when the mouse is above images.
The "-webkit-border-radius" and "-moz-border-radius" properties add a rounded corners for Firefox, Safari and Chrome. Unfortunately, Internet Explorer doesn't support it and will display regular corners instead.
That's how menu will look if Javascript is disabled.
.sc_menu span {
  display: none;
  margin-top: 3px;
  text-align: center;
  font-size: 12px;
  color: #fff;
}
.sc_menu a:hover span {
  display: block;
}
.sc_menu img {
  border: 3px #fff solid;
  -webkit-border-radius: 3px;
  -moz-border-radius: 3px;
}
.sc_menu a:hover img {
  filter:alpha(opacity=50);
  opacity: 0.5;
}

jQuery

We will need to add a jQuery to our document first. I use version hosted on Google API, because it is often already saved in the browser cache which noticeably reduces page load time.
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.1/jquery.min.js" type="text/javascript"></script>
Here is what you should know to understand the code:
$() is a shorthand for $(document).ready(), the most commonly used jQuery function. It allows you to bind a function to be executed when the DOM document has finished loading.
$(function(){
  // Your code here
});
We will use the "mousemove" event to bind a function to be fired when the mouse is moved over menu.
The "ul.width()" doesn't return real width of all images, because we have set an unordered list's width with CSS to make all images display in 1 line. We can get the real width adding last list item's width to it's left offset.
We will use "lastLi[0]" to get the DOM element from the jQuery collection and "offsetLeft" to get the position of the upper left edge of the list item relatively to wrapper div.
The event's "pageX" attribute returns the horizontal coordinate of the mouse relative to the whole document, but we need position relative to the wrapper div, so we will subtract "div.offset().left" from it.
List must scroll faster than the mouse is moved to make that we use "(ulWidth-divWidth) / divWidth" proportion.
Here is the Javascript:
$(function(){
    //Get our elements for faster access and set overlay width
    var div = $('div.sc_menu'),
                 ul = $('ul.sc_menu'),
                 // unordered list's left margin
                 ulPadding = 15;

    //Get menu width
    var divWidth = div.width();

    //Remove scrollbars
    div.css({overflow: 'hidden'});

    //Find last image container
    var lastLi = ul.find('li:last-child');

    //When user move mouse over menu
    div.mousemove(function(e){

      //As images are loaded ul width increases,
      //so we recalculate it each time
      var ulWidth = lastLi[0].offsetLeft + lastLi.outerWidth() + ulPadding;

      var left = (e.pageX - div.offset().left) * (ulWidth-divWidth) / divWidth;
      div.scrollLeft(left);
    });
});
We are done! You can now view the final result.

Create a sliding viewer to accommodate any number of photos

The example on the loadjpg page can be extended to load and place any number of photos (as specified in the picnames array) into a code-generated slider at the bottom of the movie. The photos are loaded with MovieClipLoader as before, but this time, a double number of thumbnail holders is created so that a slider can be made which will scroll continuously. A slider is created to be double the width of all the thumbnails plus a specified margin between each. Photos are loaded into thumbnails on the left half of the slider, and again into duplicate thumbnails on the right half.
Here is where each element on stage will be, as defined by the constants we set up in the beginning of the movie:
And this is the actual content of the movie when the user has moused over the right side of the mask (shown in yellow -- can be any size over the slider, as defined in the movie constants):
After all pictures have been loaded and the slider movieclip is built, a setupHandlers function is called to set up onRollOver, onRollOut, and onRelease handlers for the thumbnails, and an additional handler for thumbs_mc.onEnterFrame, to control its scrolling when moused over. The rate of scrolling (set by variable npixels, and reaching a maximum of MAXPIXELS), is proportional to the x position of the mouse, relative to its position over the mask -- slow scrolling when in the middle, faster when over the edges. The slider is placed under a mask to keep only the desired part of it showing.
Following is the complete code (to go in frame 1 of an empty 420 x 290 movie, black background, 20fps, photos in a folder under the swf named flowers).
/*********  DECLARE AND INITIALIZE VARIABLES  **************************/
// names of folder and pictures, in the order to put them into the slider
var picnames:Array = [
 "flower_orange", 
 "flower_yellow", 
 "flower_pink", 
 "flower_red", 
 "flower_purple"
 ];

// constants
var PICPATH:String = "flowers/"; // folder with jpgs
var NPICS:Number = picnames.length; // number of pictures to load
var PICX:Number = 10;               // x loc of big picture
var PICY:Number = 10;               // y loc
var THUMBHOLDERX:Number = 0;        // x location of thumbnail holder movieclip
var THUMBHOLDERY:Number = 240;      // y location
var THUMBW:Number = 72;             // width of each thumbnail
var THUMBH:Number = 40;             // height
var MARGIN:Number = 10;             // margin between thumbnails
var TOTALBYTES:Number = 212000;     // approx sum of bytes in all jpgs (x 2)
var MAXPIXELS:Number = 12;          // max number of pixels to move slider per frame

// mask definition; mask is assumed to cover some part of the thumbnail slider (here the numbers
// were chosen so that there are margins between the mask and the right and left edges of the movie
// (which is 420 x 290), and enough space above and below the thumbs to show them when they 'grow'
// on mouseover
var MASKX:Number = 10;    // start x location of mask
var MASKW:Number = 400;    // mask width
var MASKY:Number = 230;    // start y location of mask
var MASKH:Number = 60;    // mask height

var totalloaded:Number = 0;         // running tally of bytes loaded from all pics

// index into pictures array, used for loading
var ipic:Number;

// set up loader, an instance of MovieClipLoader
var loader:MovieClipLoader = new MovieClipLoader();

// use the main timeline to listen to and respond to loader's broadcast events
loader.addListener(this);

/*********  DEFINE FUNCTIONS, INCLUDING INIT FOR MOVIE SETUP  **********/
// thumbnail rollover handler

function grow() {
   this.onEnterFrame = function() {
      if (this._width < THUMBW * 1.2) {
         this._x -= this._width * .025;
         this._y -= this._height * .025;
         this._width *= 1.05;
         this._height *= 1.05;
      } else delete this.onEnterFrame;
   };
}

// thumbnail rollout handler

function shrink() {
   this.onEnterFrame = function() {
      if (this._width > THUMBW) {
         this._width /= 1.05;
         this._height /= 1.05;
         this._x += this._width * .025;
         this._y += this._height * .025;
      } else delete this.onEnterFrame;
   };
}

// function to move thumbnail slider ("this" = thumbs_mc)

function sliderControl() {
   var w:Number = this._width/2;
   var hw:Number = mask_mc._width/2;
   var npixels:Number;
   // only do when mouse over slider mask
   if (_ymouse > mask_mc._y && _ymouse < mask_mc._y + mask_mc._height) {
      // mouse over left half of slider:
      if (_xmouse > mask_mc._x && _xmouse < mask_mc._x + hw) {
         npixels = (hw - _xmouse) / hw * MAXPIXELS;
         this._x += npixels;
         if (this._x >= 0) this._x = this._x - w;
      // mouse over right half of slider:
      } else if (_xmouse > mask_mc._x + hw && _xmouse < mask_mc._x + mask_mc._width) {
         npixels = (_xmouse - hw) / hw * MAXPIXELS;
         this._x -= npixels;
         if (this._x <= -w) this._x = this._x + w;
      }
   }
}

// thumbnail click (onrelease) handler

function openPic() {
   pic_mc.loadMovie(PICPATH + picnames[this.i] + ".jpg");
}

// assign event handlers (called when all jpgs are loaded)

function setupHandlers() {
   pct_txt.removeTextField();  // don't need loading indicator any more
   thumbs_mc.onEnterFrame = sliderControl;
   for (var i:Number = 0; i < NPICS*2; i++) {
      thumbs_mc["mc"+i].onRollOver = grow;
      thumbs_mc["mc"+i].onRollOut = shrink;    
      thumbs_mc["mc"+i].onRelease = openPic;
   }
}

// listener function for broadcast 'done' message (for each pic)
// onLoadInit gets executed when the movieclip has been loaded into _mc AND 
//   its width and height data are available.
//   (_mc = the movieclip being loaded into)
// this routine sets the size and position of each thumbnail clip as its jpg
//   is loaded and starts the next one loading.  When all have been loaded, 
//   a random picture is loaded into pic_mc and setupHandlers is called to 
//   assign handlers to each thumbnail movieclip

function onLoadInit(_mc:MovieClip) {
   // this gets done when the jpg is completely loaded:
   _mc._width = THUMBW;
   _mc._height = THUMBH;
   _mc._alpha = 99;  // for image clarity
   // give the movieclip a property to remind it who it is
   // (used by openPic to know which big picture to open)
   _mc.i = (ipic >= NPICS ? ipic-NPICS : ipic);
 
   // add picture size to totalloaded variable
   totalloaded += loader.getProgress(_mc).bytesTotal;

   // now load the next one (if there are more) or set up handlers if done
   ipic++;
   if (ipic == NPICS * 2) {
      // start with a random photo displayed when all thumbs loaded
      pic_mc.loadMovie(PICPATH + picnames[Math.floor(Math.random()*NPICS)] + ".jpg");
      setupHandlers();
   } else if (ipic >= NPICS) {
      // load jpg into duplicate thumbnail (will already be cached)
      loader.loadClip(PICPATH + picnames[ipic-NPICS] + ".jpg",  thumbs_mc["mc"+ipic]);
   } else {
      // load jpg into thumbnail
      loader.loadClip(PICPATH + picnames[ipic] + ".jpg",  thumbs_mc["mc"+ipic]);
   }
}

// listener function to handle broadcast progress messages
// make pct_txt show cumulative loading progress

function onLoadProgress(_mc:MovieClip, loaded:Number) {
   var loadedsofar:Number = totalloaded + loaded; 
   pct_txt.text = Math.floor(loadedsofar / TOTALBYTES * 100) + "%";
}

function init() {
   // create holder for pictures
   createEmptyMovieClip("pic_mc", 1);
   pic_mc._x = PICX;
   pic_mc._y = PICY;

   // create (and draw) holder for thumbnails 
   createEmptyMovieClip("thumbs_mc", 2);
   thumbs_mc.beginFill(0, 100); // black
   thumbs_mc.moveTo(0, 0);
      thumbs_mc.lineTo(2 * (MARGIN + THUMBW) * NPICS, 0);
   thumbs_mc.lineTo(2 * (MARGIN + THUMBW) * NPICS, THUMBH);
   thumbs_mc.lineTo(0, THUMBH);
   thumbs_mc.endFill();
   // drawing the thumb holder at 0, 0 and then moving it makes its reg point = upper left
   thumbs_mc._x = THUMBHOLDERX;
   thumbs_mc._y = THUMBHOLDERY;

   // create, draw and enable mask over thumbs (could use different variables to define mask
   // if desired)
   createEmptyMovieClip("mask_mc", 3);
   mask_mc.beginFill(0x0000cc, 100);
   mask_mc.moveTo(0, 0);
   mask_mc.lineTo(MASKW, 0);
   mask_mc.lineTo(MASKW, MASKH);
   mask_mc.lineTo(0, MASKH);
   mask_mc.endFill();
   mask_mc._x = MASKX;
   mask_mc._y = MASKY;
   thumbs_mc.setMask(mask_mc);

   // create loading textfield indicator
   createTextField("pct_txt", 4, 200, 100, 40, 100);
   var tf:TextFormat = new TextFormat();
   tf.align = "center";
   tf.size = 12;
   tf.font = "Verdana";
   tf.color = 0xFFFF00;
   pct_txt.setNewTextFormat(tf);

   // make empty movieclips in thumbs_mc for each pic to go into
   // make double the number so the slider can move continuously and show content
   for (var i:Number = 0; i < NPICS * 2; i++) {
      var mc:MovieClip = thumbs_mc.createEmptyMovieClip("mc"+i, i+1);
      mc._x = i*(MARGIN + THUMBW);
      mc._y = 0;
   }
 
   // set the pointer to the first jpg in the array picnames
   ipic = 0;
   // start loading jpgs (ipic is initialized to 0)
   loader.loadClip(PICPATH + picnames[ipic] + ".jpg", thumbs_mc["mc"+ipic]);
}

/*********  CALL THE INIT FUNCTION TO START THE MOVIE  *****************/
init();