Saturday, July 28, 2012

Image Browser

Not so long ago, photo collections were stored in photo albums, sometimes with carefully written captions, or as slide shows ready made to strike fear into the hearts of many a social guest. Lately, these photo albums are going high-tech as people put their fondest memories on the web.


A quick look through some of these photo album sites indicates that there is some room for improvement in presentation. While there are lots of photo slide show servers on the web – and some that will even store and show your photos for free, the slide-show format limits the size of the images and there is not so much room for personalization. Additionally, it seems that the only other option is to display all the images at full size with a little text accompanying each image. This has three significant disadvantages: with the images being so large there is little room left for layout and the images end up in a long list resulting in a very tall page. Also, the number of bytes making up each image can be quite large, adding to the time taken for the page to download.


In this article is an effective solution to this problem. Images are displayed in thumbnail size giving much more room for layout and surrounding text. Also, many images can be seen at once giving the reader a fuller presentation. If readers wish to see more detail on a specific image, they can click on it and it will expand to its full size.

The images are presented in the document in thumbnail form as in the following HTML:

<a href="spike.jpg" onclick="this.href = 'javascript:void(0);';">
   <img src="spike_thumb.jpg" title="click to expand." style="float:right;"
         onclick="new ImageExpander(this, 'spike.jpg');">
</a>

 Notice that the <img> tag displays a thumbnail version of the image. The full size image is only downloaded and displayed when the user clicks on the image. Thumbnail images can be created from their originals by most image processing applications. I have also wrapped the image in a link tag to enable users without JavaScript to view the image. If JavaScript is enabled, the onclick of the link tag disables its own link so that it doesn’t interfere with the JavaScript code.

The onclick handler of the thumbnail image creates a new ImageExpander object passing itself and the URL of the full size image as arguments.

function ImageExpander(oThumb, sImgSrc)
{
   // store thumbnail image and overwrite its onclick handler.
   this.oThumb = oThumb;
   this.oThumb.expander = this;
   this.oThumb.onclick = function() { this.expander.expand(); }
  
   // record original size
   this.smallWidth = oThumb.offsetWidth;
   this.smallHeight = oThumb.offsetHeight;    

   // initial settings
   this.bExpand = true;
   this.bTicks = false;
  
   // insert into self organized list
   if ( !window.aImageExpanders )
      window.aImageExpanders = new Array();
   window.aImageExpanders.push(this);

   // create the full sized image and automatically expand when loaded
   this.oImg = new Image();
   this.oImg.expander = this;
   this.oImg.onload = function(){this.expander.onload();}
   this.oImg.src = sImgSrc;
}

The ImageExpander constructor takes ownership of the thumbnail object and reassigns the onclick handler to call its own expand. The this.bTicks flag is used to indicate whether or not the animation engine is active. Initially, this value is false as the ImageExpander must wait for the image to download before starting the animation.

Once expanded, the image will take up a large portion of the visible area of the browser so to avoid confusion; only one ImageExpander should be allowed to expand at any one time. To enforce this, the ImageExpander inserts itself into an array held in the window object. We’ll see how this is used later.

Lastly, the full size image is loaded into a new Image object. The onload handler on the image is set to call the onload method on the ImageExpander.

ImageExpander.prototype.onload = function()
{
   this.oDiv = document.createElement("div");
   document.body.appendChild(this.oDiv);
   this.oDiv.appendChild(this.oImg);
   this.oDiv.style.position = "absolute";
   this.oDiv.expander = this;
   this.oDiv.onclick = function(){this.expander.toggle();};
   this.oImg.title = "Click to reduce.";
   this.bigWidth = this.oImg.width;
   this.bigHeight = this.oImg.height;
  
   if ( this.bExpand )
   {
      this.expand();
   }
   else
   {
      this.oDiv.style.visibility = "hidden";
      this.oImg.style.visibility = "hidden";
   }
}

Once the image has loaded, the ImageExpander object can display the image in the browser. First a <div> element is created to contain the image. The <div> element is used to position the image on the screen and to handle the onclick event as once the image has grown to full size, the user will want to be able to reduce it by clicking on it again. At this point, the width and height of the full size image can be retrieved, these values will be used to calculate how big to make the image when it is expanded.

Now, it is possible that while the image was downloading, the user may have clicked on another thumbnail, so the this.bExpand flag must be tested to see whether to go on with the expansion. If not, the image and its enclosing <div> are quickly hidden from view.

ImageExpander.prototype.toggle = function()
{
   this.bExpand = !this.bExpand;
   if ( this.bExpand )
   {
      for ( var i in window.aImageExpanders )
         if ( window.aImageExpanders[i] !== this )
            window.aImageExpanders[i].reduce();
   }
}

The toggle method is called by the onclick handler of the full size image. All this function does is to reverse the direction of the image so the users can change their minds if they desire. It is not necessary to start the animation because this function can only be called while the animation is already running. So all that is required is to change the this.bExpand flag and force all other ImageExpander objects to reduce.
ImageExpander.prototype.expand = function()
{
   // set direction of expansion.
   this.bExpand = true;

   // set all other images to reduce
   for ( var i in window.aImageExpanders )
      if ( window.aImageExpanders[i] !== this )
         window.aImageExpanders[i].reduce();

   // if not loaded, don't continue just yet
   if ( !this.oDiv ) return;
  
   // hide the thumbnail
   this.oThumb.style.visibility = "hidden";
  
   // calculate initial dimensions
   this.x = this.oThumb.offsetLeft;
   this.y = this.oThumb.offsetTop;
   this.w = this.oThumb.clientWidth;
   this.h = this.oThumb.clientHeight;
  
   this.oDiv.style.left = this.x + "px";
   this.oDiv.style.top = this.y + "px";
   this.oImg.style.width = this.w + "px";
   this.oImg.style.height = this.h + "px";
   this.oDiv.style.visibility = "visible";
   this.oImg.style.visibility = "visible";
  
   // start the animation engine.
   if ( !this.bTicks )
   {
      this.bTicks = true;
      var pThis = this;
      window.setTimeout(function(){pThis.tick();},25);    
   }
}

The expand method first sets the this.bExpand flag to true so that the animation engine knows which direction it is going and then forces all other ImageExpander objects to reduce. It is possible that this method may be called before the image has downloaded so a check is done at this point and the method is exited if not ready. Otherwise the thumbnail is hidden (although it will continue to take up browser space) and the initial position and size of the full size image are set to match the thumbnail. Then the animation engine is started to manage the expansion of the image to full size.

ImageExpander.prototype.reduce = function()
{
   // set direction of expansion.
   this.bExpand = false;
}

All that is needed to reduce an image is to set the this.bExpand flag to false. If the full size image is visible, then the animation engine will already be running and changing this flag will control its direction. If the thumbnail image is visible, then there is nothing to do as the image is already reduced as far as it should go.

The animation engine is contained in a single method of the ImageExpander object called tick. As its name suggests, this function is called repeatedly at fast enough intervals to make the image movements it controls appear to be smooth.

ImageExpander.prototype.tick = function()
{
   // calculate screen dimensions
   var cw = document.body.clientWidth;
   var ch = document.body.clientHeight;
   var cx = document.body.scrollLeft + cw / 2;
   var cy = document.body.scrollTop + ch / 2;

The first thing the tick does is to calculate the browser page dimensions – at least that part that is visible to the user.
The next task is to calculate the target size and position; this will depend on the direction of the animation defined by the this.bExpand flag.

 If the image is expanding then the original dimensions of the image are taken and then reduced to fit the page if necessary:

   // calculate target
   var tw,th,tx,ty;
   if ( this.bExpand )
   {
      // start with the full size dimensions
      tw = this.bigWidth;
      th = this.bigHeight;

      // reduce to fit the screen
      if ( tw > cw )
      {
         th *= cw / tw;
         tw = cw;
      } 
      if ( th > ch )
      {
         tw *= ch / th;
         th = ch;
      }
      // then center it on the page
      tx = cx - tw / 2;
      ty = cy - th / 2;
   }

Otherwise the current size and location of the thumbnail image are used. Note that if the browser window changes size, then the actual position of the thumbnail can change accordingly, so this must be calculated for each tick.

   else
   {
      tw = this.smallWidth;
      th = this.smallHeight;
      tx = this.oThumb.offsetLeft;
      ty = this.oThumb.offsetTop;
   }

Once the target size and position are calculated, we can use some algorithm to move the full size image towards the target. In the following code, each dimension; left, top, width and height are moved 10% closer to its target. While 10% might sound like a lot, the size of movement will reduce as the target comes closer, so the effect is to start with an initial burst of speed and then slowing down as it approaches the target position and size.

The algorithm below is contained inside a nested function which is assigned to the variable fMove. This function operates on each dimension and does two things: it calculates the value 10% closer to the target and counts the dimensions that a getting very close to the target value – less than three pixels away. This count is declared outside the function but it is still accessible as nested functions have access to the context in which they are defined.

   // move 10% closer to target
   var nHit = 0;
   var fMove = function(n,tn)
   {
      var dn = tn - n;
      if ( Math.abs(dn) < 3 )
      {
         nHit++;
         return tn;
      }
      else
      {
         return n + dn / 10;
      }
   }
   this.x = fMove(this.x, tx);
   this.y = fMove(this.y, ty);
   this.w = fMove(this.w, tw);
   this.h = fMove(this.h, th);
  
   this.oDiv.style.left = this.x + "px";
   this.oDiv.style.top = this.y + "px";
   this.oImg.style.width = this.w + "px";
   this.oImg.style.height = this.h + "px";
If the image is reducing and all four dimensions (left, top, width and height) have hit their target, then the animation engine is stopped by switching off the this.bTicks flag, hiding the full size image and showing the thumbnail again. Expanding images stay active in case the user changes the size of the browser window.

   // if reducing and size/position is a match, stop the tick   
   if ( !this.bExpand && (nHit == 4) )
   {
      this.oImg.style.visibility = "hidden";
      this.oDiv.style.visibility = "hidden";
      this.oThumb.style.visibility = "visible";

      this.bTicks = false;
   }

Finally, if still active, the animation engine schedules another tick a short time in the future.

   if ( this.bTicks )
   {
      var pThis = this;
      window.setTimeout(function(){pThis.tick();},25);
   }
}

Now, with this code in place, all that is left is to design the layout of the web page, taking advantage of the extra space resulting from the smaller thumbnail images. Remember to enclose each thumbnail in a link tag to cater for those users who do not have JavaScript.
Conclusion
In this article I have presented a JavaScript class that will manage a pair of images; a full-sized image and its thumbnail. When the user clicks on the thumbnail, the ImageExpander code will enlarge the image until it either fills the browser or reaches its full size – whichever comes first. A second click on the image or clicking on another thumbnail reduces the image back to its original position.

No comments:

Post a Comment