Bubbleriffic Image Gallery with jQuery

In this tutorial we will create a bubbly image gallery that shows your images in a unique way. The idea is to show the thumbnails of albums in a rounded fashion allowing the user to scroll them automatically by moving the mouse. Clicking on a thumbnail will zoom in a big circle […]


View demoDownload source

In this tutorial we will create a bubbly image gallery that shows your images in a unique way. The idea is to show the thumbnails of albums in a rounded fashion allowing the user to scroll them automatically by moving the mouse. Clicking on a thumbnail will zoom in a big circle and the full image which will be automatically resized to fit into the screen. Navigating through the images will slide the current image to the side and make the new one appear in a zoom like fashion.

We will be using Manos Malihu’s brilliant thumbnail scroller which you can find here: Manos Malihu’s thumbnail scroller

The beautiful images are by talented geishaboy500 and you can find more amazing photographs on his flickr photostream:
Photos by geishaboy500

Ok, let’s get started!

The Markup

Let’s start by creating our HTML structure. We will have quite a few elements, so we will begin with the top menu that appears when we view the full image:

<div class="top_menu" id="top_menu">
	<span id="description" class="description"></span>
	<a id="back" href="#" class="back"><span></span>back</a>
	<div class="info">
		<span class="album_info">Album 1</span>
		<span class="image_info"> / Shot 1</span>
	</div>
</div>

We need an option to go back to the thumbnails view, a description area and an information area that shows us which image we are currently viewing. So, this element is just going to show up when we click on a thumbnail. The empty span will be used to add a little back arrow.

We also need a loader element:

<div id="loader" class="loader"></div>

Now, let’s take a look at the heading element where we will put our main h1 element for the gallery title:

<div class="header" id="header" style="top:-90px;"><!--top 30 px to show-->
	<h1>Bubbleriffic<span>jQuery Image Gallery</span></h1>
</div>

We want to slide this element in from the top when we load the page, so we initially set the top value to -90 pixels. In the JavaScript we will then animate it to 30 pixels. You can also add this style to the stylesheet instead of having an inline style.

The next element will be the thumbnails wrapper that will have all the thumbnail albums. Since we will use Manos’ thumbnail scroller, we will base our markup on that structure. There will be three scrolling containers for the three albums which will have the class “tshf_container”:

<div id="thumbnails_wrapper" class="thumbnails_wrapper" style="top:-255px;">
	<!-- top:110px to show-->
	
	<div id="tshf_container1"  class="tshf_container">
		<div class="thumbScroller">
			<div class="container">
			
				<div class="content">
					<div>
						<a href="#">
							<img src="images/albums/album1/thumbs/1.jpg" alt="Description" class="thumb" />		
						</a>
						<span></span>
					</div>
				</div>
				
				<div class="content">
					...
				</div>
				
			</div>
		</div>
	</div>
	
	<div id="tshf_container2"  class="tshf_container">
		...
	</div>

	<div id="tshf_container3"  class="tshf_container">
		...
	</div>
</div>

The empty span inside of the div that holds the image will be used for overlaying an image with a hole in it, making the thumbnails appear like little bubbles.

Now we will add the yellow bubble that will be used to create the bubble like tunnel effect when we want to view a full image:

<div class="bubble">
	<img id="bubble" src="images/bubble.png" alt=""/>
</div>

The preview container for the full image view with the navigation will have the following structure:

<div id="preview" class="preview">
	<a id="prev_image" href="#" class="prev_image"></a>
	<a id="next_image" href="#" class="next_image"></a>
</div>

The footer can be used to add some information:

<div class="footer">
	<!-- Add something here -->
</div>

Let’s take a look at the style!

The CSS

First, we will reset the style and define some general properties for links, lists and the body:

*{
	margin:0;
	padding:0;
}
a{
	text-decoration:none;
	outline:none;
}
ul{
	list-style:none;
}
body{
	background:#222 url(../bg.jpg) repeat top left;
	font-family:"Trebuchet MS", "Myriad Pro", Helvetica, sans-serif;
	font-size:13px;
	color: #fff;
	text-transform:uppercase;
	text-shadow:0px 0px 1px #fff;
	letter-spacing:1px;
	overflow:hidden;
}

The top menu that appears when we view a full image will have the following style:

.top_menu{
	height:30px;
	line-height:30px;
	position:absolute;
	top:-30px;
	left:0;
	width:100%;
	overflow:hidden;
	background:#010101;
	border-bottom:1px solid #000;
	z-index:100;
	-moz-box-shadow:0px 0px 4px #010101;
	-webkit-box-shadow:0px 0px 4px #010101;
	box-shadow:0px 0px 4px #010101;
}

(If you know that your text will only occupy one line in a specific element, then you can set that element’s line-height to its height in order to center the containing text vertically.)
We set the top to -30 pixels in order to hide the element.

The back link will have the following style:

.top_menu .back{
	position:absolute;
	top:0px;
	left:10px;
	height:30px;
	line-height:30px;
	cursor:pointer;
	color:#aaa;
}

We will add a little arrow icon for the back link that actually has the same background image like the navigation element. We use double of the width though, so that we can repeat the image twice:

.top_menu .back span{
	width:14px;
	height:30px;
	display:block;
	float:left;
	background:#000 url(../images/prev.png) repeat-x center left;
	margin-right:5px;
	opacity:0.5;
}
.top_menu .back:hover{
	color:#fff;
}
.top_menu .back:hover span{
	opacity:0.9;
}

The description will appear centered:

.top_menu span.description{
	font-style:italic;
	position:absolute;
	width:100%;
	text-align:center;
	top:0px;
	left:0px;
	height:30px;
	line-height:30px;
}

And the info will float right:

.top_menu .info{
	float:right;
	margin-right:10px;
}

Let’s style the main header now. We will position it absolutely since we want it to animate from the top and we will give it a yellow background color and some nice box shadow:

.header{
	height:80px;
	position:absolute;
	top:30px;
	left:0px;
	width:100%;
	overflow:hidden;
	z-index:90;
	background-color:#ffd800;
	-moz-box-shadow:0px 0px 10px #010101;
	-webkit-box-shadow:0px 0px 10px #010101;
	box-shadow:0px 0px 10px #010101;
}
h1{
	font-size:60px;
	padding-left:20px;
	color:#010101;
	line-height:80px;
	text-shadow:1px 1px 1px #000;
}
h1 span{
	font-size:16px;
	float:right;
	margin-right:20px;
}

Before we style the thumbnails wrapper, let’s first take a look at all the other elements’ style.

The full image will be positioned absolutely, but we will set its final position in the JavaScript:

.preview img{
	position:absolute;
	left:0;
	top:0;
	-moz-box-shadow:1px 1px 5px #111;
	-webkit-box-shadow:1px 1px 5px #111;
	box-shadow:21px 1px 5px #111;
	-webkit-box-reflect:
		below 5px
		-webkit-gradient(
		linear,
		left top,
		left bottom,
		from(transparent),
		color-stop(0.8, transparent),
		to(rgb(255, 216, 0))
		);
}

We will add some nice reflection for webkit browsers and some decent box shadow.

The footer will have almost the same style like the top menu, except the positioning and some other details, like the border:


.footer{
	z-index:100;
	height:30px;
	line-height:30px;
	text-align:center;
	font-size:10px;
	background:#010101;
	border-top:1px solid #000;
	position:absolute;
	bottom:0px;
	left:0px;
	width:100%;
	-moz-box-shadow:0px 0px 4px #010101;
	-webkit-box-shadow:0px 0px 4px #010101;
	box-shadow:0px 0px 4px #010101;
}
.footer a{
	color:#999;
	text-decoration:none;
	margin:40px;
}

The loading element will be placed in the middle of the page and we will make it round by setting the border radius to half of its width/height:

.loader{
	display:none;
	width:200px;
	height:200px;
	background: #000 url(../images/ajax-loader.gif) no-repeat center center;
	position:fixed;
	top:50%;
	left:50%;
	margin:-100px 0px 0px -100px;
	opacity: 0.7;
	z-index:9999;
	-moz-border-radius:100px;
	-webkit-border-radius:100px;
	border-radius:100px;
}

The navigation elements will have the following common style:

a.next_image,
a.prev_image{
	width:50px;
	height:50px;
	position:fixed;
	top:50%;
	margin-top:-25px;
	cursor:pointer;
	opacity:0.7;
	z-index:999999;
	-moz-box-shadow:0px 0px 3px #000;
	-webkit-box-shadow:0px 0px 3px #000;
	box-shadow:0px 0px 3px #000;
	-moz-border-radius:25px;
	-webkit-border-radius:25px;
	border-radius:25px;
}
a.next_image:hover,
a.prev_image:hover
{
	opacity:0.9;
}

And the separate styles will be

a.next_image{
	background:#000 url(../images/next.png) no-repeat center center;
	right:-50px;
}
a.prev_image{
	background:#000 url(../images/prev.png) no-repeat center center;
	left:-50px;
}

Initially, they will both be hidden, that’s why we will set their left/right positioning to the negative value of their widths.

Now, we will style the thumbnails wrapper:

.thumbnails_wrapper{
	position:absolute;
	top:-255px;
	left:0px;
	width:100%;
	-moz-box-shadow:0px 3px 5px #000;
	-webkit-box-shadow:0px 3px 5px #000;
	box-shadow:0px 3px 5px #000;
}

The bubble image will be positioned in the center of the page and have a width and height of 0. We will animate it then in the JavaScript:

.bubble img{
	position:fixed;
	top:50%;
	left:50%;
	width:0px;
	height:0px;
}

Now, we will adapt the style of Manos’ thumbnail scroller:

.tshf_container{
	height:85px;
	position:relative;
	width:100%;
	background:#ffd800;
}
.tshf_container .thumbScroller{
	position:relative;
	width:100%;
	overflow:hidden;
}
.tshf_container .thumbScroller,
.tshf_container .thumbScroller .container,
.tshf_container .thumbScroller .content{
	height:85px;
}
.tshf_container .thumbScroller .container{
	position:relative;
	left:0;
}
.tshf_container .thumbScroller .content{
	float:left;
}
.tshf_container .thumbScroller .content div{
	height:100%;
	position:relative;
}
.tshf_container .thumbScroller img{
	border:none;
}
.tshf_container .thumbScroller .content div a{
	display:block;
	height:85px;
	width:85px;
}
.tshf_container .thumbScroller .content div a img{
	width:85px;
}
.tshf_container .thumbScroller .content div a:hover{
	border-color:#fff;
}

We will use the extra span to add our bubble overlay:

.tshf_container .thumbScroller .content span{
	cursor:pointer;
	position:absolute;
	width:85px;
	height:85px;
	top:0px;
	left:0px;
	background:transparent url(../images/thumb_overlay.png) no-repeat top left;
}
.tshf_container .thumbScroller .content div:hover span{
	background:transparent url(../images/thumb_overlay_hover.png) no-repeat top left;
}

And that’s all the style. Now, let’s add the magic!

The JavaScript

First, we will include the following scripts:

<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.3/jquery.min.js"></script>
<script type="text/javascript" src="js/jquery.easing.1.3.js"></script>
<script type="text/javascript" src="js/jquery.thumbnailScroller.js"></script>

We will start by caching some important elements and defining some indexes:

var $thumbnails_wrapper 	= $('#thumbnails_wrapper'),
	$thumbs					= $thumbnails_wrapper.find('.tshf_container').find('.content'),
	$top_menu				= $('#top_menu'),
	$header					= $('#header'),
	$bubble                 = $('#bubble'),
	$loader					= $('#loader'),
	$preview				= $('#preview'),
	$thumb_images			= $thumbnails_wrapper.find('img'),
	total_thumbs			= $thumb_images.length,
	$next_img				= $('#next_image'),
	$prev_img				= $('#prev_image'),
	$back					= $('#back'),
	$description			= $('#description'),
	//current album and current photo
	//(indexes of the tshf_container and content elements)					
	currentAlbum			= -1,
	currentPhoto			= -1;

Then, we will show the loading element (until all thumbnails are loaded) and call the function that opens the albums:

$loader.show();

//shows the main menu and thumbs menu
openPhotoAlbums();

And this is how that function looks like:

function openPhotoAlbums(){
	//preload all the thumb images
	var cnt_loaded = 0;
	$thumb_images.each(function(){
		var $thumb 		= $(this);
		var image_src 	= $thumb.attr('src');
		$('<img/>').load(function(){
			++cnt_loaded;
			if(cnt_loaded == total_thumbs){
				$loader.hide();
				createThumbnailScroller();
				//show the main menu and thumbs menu
				$header.stop()
					   .animate({'top':'30px'},700,'easeOutBack');
				$thumbnails_wrapper.stop()
								   .animate({'top':'110px'},700,'easeOutBack');
			}
		}).attr('src',image_src);
	});
}

The function “createThumbnailScroller()” will make the thumbnails be scrollable after Manos’ script, but we will get to that later.

Next, we will define what happens when we click on a thumbnail.

$thumbs.bind('click',function(e){
	//show loading image
	$loader.show();
	var $thumb	= $(this),
		//source of the corresponding large image
		img_src = $thumb.find('img.thumb')
						.attr('src')
						.replace('/thumbs','');
	
	//track the current album / photo
	currentPhoto= $thumb.index(),
	currentAlbum= $thumb.closest('.tshf_container')
						.index();
	//displays the current album and current photo
	updateInfo(currentAlbum,currentPhoto);
	//preload the large image
	$('<img/>').load(function(){
		var $this = $(this);
		//record the size that the large image 
		//should have when it is shown
		saveFinalPositions($this);
		//margin_circle is the diameter for the 
		//bubble image
		var w_w				= $(window).width(),
			w_h				= $(window).height(),
			margin_circle	= w_w + w_w/3;
		if(w_h>w_w)
			margin_circle	= w_h + w_h/3;
		
		//the image will be positioned on the center,
		//with width and height of 0px
		$this.css({
			'width'		: '0px',
			'height'	: '0px',
			'marginTop'	: w_h/2 +'px',
			'marginLeft': w_w/2 +'px'
		});
		$preview.append($this);
		
		//hide the header
		$header.stop().animate({'top':'-90px'},400, function(){
			$loader.hide();
			//show the top menu with the back button,
			//and current album/picture info
			$top_menu.stop()
					 .animate({'top':'0px'},400,'easeOutBack');
			//animate the bubble image
			$bubble.stop().animate({
				'width'		:	margin_circle + 'px',
				'height'	:	margin_circle + 'px',
				'marginTop'	:	-margin_circle/2+'px',
				'marginLeft':	-margin_circle/2+'px'
			},700,function(){
				//solve resize problem
				$('BODY').css('background','#FFD800');
			});
			//after 200ms animate the large image
			//and show the navigation buttons
			setTimeout(function(){
				var final_w	= $this.data('width'),
					final_h	= $this.data('height');
				$this.stop().animate({
						'width'		: final_w + 'px',
						'height'	: final_h + 'px',
						'marginTop'	: w_h/2 - final_h/2 + 'px',
						'marginLeft': w_w/2 - final_w/2 + 'px'
				},700,showNav);
				//show the description
				$description.html($thumb.find('img.thumb').attr('alt'));
			},200);
			
		});
		//hide the thumbs
		$thumbnails_wrapper.stop()
						   .animate({
							   'top' : w_h+'px'
						   },400,function(){
								//solve resize problem
								$(this).hide();
						   });
		
	}).attr('src',img_src);
});

When we click on the “next” element of the navigation, we want the following to happen:

$next_img.bind('click',function(){
	//increment the currentPhoto
	++currentPhoto;
	//current album:
	var $album		= $thumbnails_wrapper.find('.tshf_container')
										 .eq(currentAlbum),
		//the next element / thumb to show
		$next		= $album.find('.content').eq(currentPhoto),
		$current 	= $preview.find('img');
	if($next.length == 0 || $current.is(':animated')){
		--currentPhoto;
		return false;
	}
	else{
		$loader.show();
		updateInfo(currentAlbum,currentPhoto);
		//preload the large image
		var img_src = $next.find('img.thumb')
						   .attr('src')
						   .replace('/thumbs',''),
			w_w		= $(window).width(),
			w_h		= $(window).height();				   
	
		$('<img/>').load(function(){
			var $this = $(this);
			//record the size that the large image 
			//should have when it is shown
			saveFinalPositions($this);
			$loader.hide();
			$current.stop()
					.animate({'marginLeft':'-'+($current.width()+20)+'px'},500,function(){
						//the current image gets removed
						$(this).remove();	
					});
			//the new image will be positioned on the center,
			//with width and height of 0px
			$this.css({
				'width'		: '0px',
				'height'	: '0px',
				'marginTop'	: w_h/2 +'px',
				'marginLeft': w_w/2 +'px'
			});
			$preview.prepend($this);
			var final_w	= $this.data('width'),
				final_h	= $this.data('height');
			$this.stop().animate({
					'width'		: final_w + 'px',
					'height'	: final_h + 'px',
					'marginTop'	: w_h/2 - final_h/2 + 'px',
					'marginLeft': w_w/2 - final_w/2 + 'px'
			},700);
			//show the description
			$description.html($next.find('img.thumb').attr('alt'));
		}).attr('src',img_src);	
	}
});

And now we define what happens when we click to view the previous image:

$prev_img.bind('click',function(){
	--currentPhoto;
	//current album:
	var $album		= $thumbnails_wrapper.find('.tshf_container')
										 .eq(currentAlbum),
		$prev		= $album.find('.content').eq(currentPhoto),
		$current 	= $preview.find('img');
	if($prev.length == 0 || $current.is(':animated') || currentPhoto < 0){
		++currentPhoto;
		return false;
	}
	else{
		$loader.show();
		updateInfo(currentAlbum,currentPhoto);
		//preload the large image
		var img_src = $prev.find('img.thumb')
						   .attr('src')
						   .replace('/thumbs',''),
			w_w				= $(window).width(),
			w_h				= $(window).height();				   
	
		$('<img/>').load(function(){
			var $this = $(this);
			//record the size that the large image 
			//should have when it is shown
			saveFinalPositions($this);
			
			$loader.hide();
			$current.stop()
					.animate({'marginLeft':(w_w+20)+'px'},500,function(){
						//the current image gets removed
						$(this).remove();
					});
			//the new image will be positioned on the center,
			//with width and height of 0px
			$this.css({
				'width'		: '0px',
				'height'	: '0px',
				'marginTop'	: w_h/2 +'px',
				'marginLeft': w_w/2 +'px'
			});
			$preview.append($this);
			var final_w	= $this.data('width'),
				final_h	= $this.data('height');
			$this.stop().animate({
					'width'		: final_w + 'px',
					'height'	: final_h + 'px',
					'marginTop'	: w_h/2 - final_h/2 + 'px',
					'marginLeft': w_w/2 - final_w/2 + 'px'
			},700);
			//show the description
			$description.html($prev.find('img.thumb').attr('alt'));							
		}).attr('src',img_src);	
	}
});

When we resize the window, we need to recalculate the position and size of the image:

$(window).resize(function(){
	var $current = $preview.find('img'),
		w_w		 = $(window).width(),
		w_h		 = $(window).height();		
	if($current.length > 0){
		saveFinalPositions($current);
		var final_w	= $current.data('width'),
			final_h	= $current.data('height');
		$current.css({
			'width'		: final_w + 'px',
			'height'	: final_h + 'px',
			'marginTop'	: w_h/2 - final_h/2 + 'px',
			'marginLeft': w_w/2 - final_w/2 + 'px'
		});
	}
});

When we click on the back link, we want to close the preview of the image:

$back.bind('click',closePreview)

The functions for showing and hiding the navigation:

//shows the navigation buttons
function showNav(){
	$next_img.stop().animate({
		'right'	: '10px'
	},300);
	$prev_img.stop().animate({
		'left'	: '10px'
	},300);
}

//hides the navigation buttons
function hideNav(){
	$next_img.stop().animate({
		'right'	: '-50px'
	},300);
	$prev_img.stop().animate({
		'left'	: '-50px'
	},300);
}

The following function will update the album/image info:

//updates the current album and current photo info
function updateInfo(album,photo){
	$top_menu.find('.album_info')
			 .html('Album ' + (album+1))
			 .end()
			 .find('.image_info')
			 .html(' / Shot ' + (photo+1))
}

The next function calculates the final width and height of the full image that will be shown based on the window size:

function saveFinalPositions($image){
	var theImage 	= new Image();
	theImage.src 	= $image.attr("src");
	var imgwidth 	= theImage.width;
	var imgheight 	= theImage.height;
	
	//140 is 2*60 of next/previous buttons plus 20 of extra margin
	var containerwidth 	= $(window).width() - 140;
	//150 is 30 of header + 30 of footer + extra 90 
	var containerheight = $(window).height() - 150;
	
	if(imgwidth	> containerwidth){
		var newwidth = containerwidth;
		var ratio = imgwidth / containerwidth;
		var newheight = imgheight / ratio;
		if(newheight > containerheight){
			var newnewheight = containerheight;
			var newratio = newheight/containerheight;
			var newnewwidth =newwidth/newratio;
			theImage.width = newnewwidth;
			theImage.height= newnewheight;	
		}
		else{
			theImage.width = newwidth;
			theImage.height= newheight;	
		}
	}
	else if(imgheight > containerheight){
		var newheight = containerheight;
		var ratio = imgheight / containerheight;
		var newwidth = imgwidth / ratio;
		if(newwidth > containerwidth){
			var newnewwidth = containerwidth;
			var newratio = newwidth/containerwidth;
			var newnewheight =newheight/newratio;
			theImage.height = newnewheight;
			theImage.width= newnewwidth;	
		}
		else{
			theImage.width = newwidth;
			theImage.height= newheight;	
		}
	}
	$image.data({'width':theImage.width,'height':theImage.height});		
}

When we hit the back link we want the current image to zoom out again with the big yellow bubble. Then we will show the thumbnails again:

function closePreview(){
	var $current = $preview.find('img'),
		w_w		 = $(window).width(),
		w_h		 = $(window).height(),
		margin_circle	= w_w + w_w/3;
		
	if(w_h>w_w)
		margin_circle	= w_h + w_h/3;
	
	if($current.is(':animated'))
		return false;
	//hide the navigation
	hideNav();
	//hide the topmenu
	$top_menu.stop()
			 .animate({'top':'-30px'},400,'easeOutBack');
	//hide the image
	$current.stop().animate({
		'width'		: '0px',
		'height'	: '0px',
		'marginTop'	: w_h/2 +'px',
		'marginLeft': w_w/2 +'px'
	},700,function(){
		$(this).remove();
	});
	//animate the bubble image
	//first set the positions correctly - 
	//it could have changed on a window resize
	setTimeout(function(){
		$bubble.css({
			'width'		 :	margin_circle + 'px',
			'height'	 :	margin_circle + 'px',
			'margin-top' :	-margin_circle/2+'px',
			'margin-left':	-margin_circle/2+'px'
		});
		$('BODY').css('background','url("bg.jpg") repeat scroll left top #222222');
		$bubble.animate({
			'width'		:	'0px',
			'height'	:	'0px',
			'marginTop'	:	'0px',
			'marginLeft':	'0px'
		},500);
	},200);
	setTimeout(function(){
		$header.stop()
			   .animate({'top':'30px'},700,'easeOutBack');
		$thumbnails_wrapper.stop()
						   .show()	
						   .animate({'top':'110px'},700,'easeOutBack');
	},600);
}

And finally, we define the function that will make the thumbnail albums scrollable by applying Manos’ thumbnail scroller:

function createThumbnailScroller(){
	/*
	ThumbnailScroller function parameters:
	1) id of the container (div id)
	2) thumbnail scroller type. Values: "horizontal", "vertical"
	3) first and last thumbnail margin (for better cursor interaction)
	4) scroll easing amount (0 for no easing)
	5) scroll easing type
	6) thumbnails default opacity
	7) thumbnails mouseover fade speed (in milliseconds)
	*/
 ThumbnailScroller("tshf_container1","horizontal",10,800,"easeOutCirc",0.5,300);
 ThumbnailScroller("tshf_container2","horizontal",10,800,"easeOutCirc",0.5,300);
 ThumbnailScroller("tshf_container3","horizontal",10,800,"easeOutCirc",0.5,300);
}

And that’s it! We hope you liked the tutorial and found it useful!

View demoDownload source

Previous:
Next:

Tagged with:

Mary Lou (Manoela Ilic) is a freelance web designer and developer with a passion for interaction design. She studied Cognitive Science and Computational Logic and has a weakness for the smell of freshly ground peppercorns.

View all contributions by

Website: http://tympanus.net/

Related Articles

Feedback 43

Comments are closed.
  1. 2

    Is it possible to add content under the gallery? I have some problem with a content div to place after …it disappears under the footer div…is it normal? is there something to do for adding normal content after the gallery?
    Thank you!

    p.s.: i’m sorry for my english!

Comments are closed.