In this tutorial you’re going to learn how to create a virtual light table using jQuery and CSS3. A traditional light table is a flat illuminated panel that you can place transparent slides on for viewing. Our “light table” is going to be an image of a wooden kitchen table, and the photos will be prints rather than transparencies, but the basic idea is the same!
The aim is to make our table and photos look as realistic as possible. Here’s our desired feature list:
- A wooden table background image for the photos to sit on
- Each photo “floats” onto the table as it loads
- Each photo is given a random position and orientation to create a “scattered” effect
- The photos have a white border and drop shadow to add a 3D effect, and are ever-so-slightly transparent
- Once on the table, a photo can be dragged around by using the mouse
Ready? Let’s go!
Step 1. The markup
First let’s look at the HTML for our light table:
<body>
<div id="wooden-table"><img src="images/wooden-table.jpg" alt="Wooden table image" /></div>
<div id="description">
<h1>Kitchen Table | <span style="font-weight: normal;">A "light table" with draggable photos using CSS3 and jQuery | Β© Elated.com | <a href="http://www.elated.com/articles/kitchen-table-slick-photo-light-table-css3-jquery/">Back to Tutorial</a></span></h1>
</div>
<div id="lighttable">
<img src="slides/bird-marking.jpg" alt="Bird Marking" />
<img src="slides/double-propellors.jpg" alt="Double Propellors" />
<img src="slides/moody-listener-satellite-dish.jpg" alt="Moody Listener Satellite Dish" />
<img src="slides/orange-hatches.jpg" alt="Orange Hatches" />
<img src="slides/plane-nose-closeup.jpg" alt="Plane Nose Closeup" />
</div>
</body>
As you can see, the markup is very straightforward. It consists of 3 parts:
- An
img
tag for the wooden table image, stored inside a#wooden-table
div
. In the final page, this image is actually displayed as a background image of the#lighttable
div
. The reason for loading it via animg
tag too is so that we can attach aload
event to it to detect when the image has been loaded. (More on this later.) - The page title and description.
- The
#lighttable
div
itself, containing the 5 photos that we want to display on the table.
Step 2. The CSS
Now let’s look at the CSS. This styles everything from the page background through to the wooden light table and the photos themselves.
The page body
/* The page body */
html, body {
background: #333;
overflow: hidden;
font-family: Helvetica, Arial, sans-serif;
}
This is given a dark grey background and a default font. overflow: hidden
ensures that scrollbars don’t appear. (We also set this on the html
element, since IE needs this to prevent the scrollbars.)
The div
holding the wooden table img
tag
/* The div holding the wooden table img tag */
#wooden-table {
position: absolute;
left: -5000px;
}
This img
tag is there purely so we can attach a jQuery load
event to it to determine when the wooden table background image has loaded. Since we don’t actually want to display the image here, we position it way to the left of the browser viewport, thereby making it effectively invisible.
The light table itself
/* The light table itself */
#lighttable {
position: relative;
width: 800px;
height: 450px;
background: #eee url(images/wooden-table.jpg);
padding: 70px;
margin: 0 auto;
border: 20px solid #111;
display: none;
}
This is set to position: relative
so that the absolutely-positioned photos sit inside it. It’s given a suitable width and height, as well as the wooden table background image mentioned above.
We add quite a lot of padding (70px) around the table. This ensures that the photos can’t be dragged over the edges of the table. If the images were all upright then this could be set to zero; however, when rotating images using CSS3 transforms, there’s no easy way to determine the size of the resulting (larger) bounding box (at least, as far as I know). Therefore we add 70 pixels of leeway to be on the safe side.
Finally we use margin: 0 auto
to centre the table horizontally, and give it a nice thick dark border. Then we hide it with display: none
. We’ll fade it in with jQuery once the background wooden table image has loaded.
Since I’m sure you’re wondering β yes, it is a photo of my kitchen table. π
Photos on the light table
/* Photos on the light table */
#lighttable img {
border: 10px solid #fff;
box-shadow: 0 0 1em rgba(0, 0, 0, 0.9);
-moz-box-shadow: 0 0 1em rgba(0, 0, 0, 0.9);
-webkit-box-shadow: 0 0 1em rgba(0, 0, 0, 0.9);
position: absolute;
left: -9999px;
top: -9999px;
}
Each photo is given a white border and a CSS3 drop shadow. We set the drop shadow using the standards approach (box-shadow
), as well as the vendor-specific methods for Mozilla and WebKit browsers (-moz-box-shadow
and -webkit-box-shadow
respectively), to ensure that the drop shadows work across as many browsers as possible. At the time of writing, only Opera 10.5 uses the official standards approach.
Finally, we set position: absolute
on the photo so we can position it easily, then move it way to the left and above the browser viewport, so that it’s initially hidden as it loads.
Step 3. The JavaScript
Let the fun begin! The JavaScript code is the meat of the light table, and is responsible for:
- Loading the jQuery and jQuery UI libraries
- Fading in the light table when the background image has loaded
- Randomly positioning and rotating each photo
- Making the photos draggable
- “Floating” each photo down to the table as it loads
Let’s take a look at the JavaScript a chunk at a time.
Loading the libraries
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript" src="jquery-ui-1.8.2.custom.min.js"></script>
You first need to load the jQuery library and jQuery UI plugin. The jQuery UI plugin implements the draggable()
method, which you’ll use to make the photos draggable with the mouse.
If you haven’t already done so, download the jQuery library and jQuery UI plugin, and place the 2 files in the same folder as your light table page. Update the filenames in the <script>
tags above if necessary.
Setting up the light table
var newImageZIndex = 1; // To make sure newly-loaded images land on top of images on the table
var loaded = false; // Used to prevent initPhotos() running twice
// When the document is ready, fire up the table!
$( init );
// When the wooden table image has loaded, start bringing in the photos
function init() {
var woodenTable = $('#wooden-table img');
woodenTable.load( initPhotos );
// Hack for browsers that don't fire load events for cached images
if ( woodenTable.get(0).complete ) $(woodenTable).trigger("load");
}
The first chunk of code sets up various variables and event handlers to initialise the light table.
You’ll use the newImageZIndex
variable to give each photo a successively higher z-index
property as it loads. This ensures that each newly-loaded photo always sits on top of any photos already on the table.
The loaded
flag will be used to determine if the event handler to initialise the photos has already been called. This works around problems with certain browsers that fire the load
event for the wooden table image twice.
If you’ve been following our other jQuery tutorials then you’ll be familiar with the line:
$( init );
This tells jQuery to call the init()
function once the document is ready β that is, when the DOM tree has been constructed. It’s a good idea to wait until the document is ready before you run the main part of your JavaScript, since you know that all the elements in the page will be available to your code.
The init()
function itself adds a load
event handler, initPhotos()
, to the wooden table image (the img
tag that you created in the markup earlier). This means that, when the table image finishes loading, the initPhotos()
function is called to start bringing in the photos. You do this to ensure that the photos don’t start floating down onto an invisible table!
Unfortunately some browsers β notably Opera and IE7/8 β don’t always fire a load
event for images that are already in their cache. To work around this, the function inspects the complete
property of the wooden table img
object. complete
is a DOM image property that is set to true
if the image is fully loaded. If this is the case, the function triggers the load
event to force initPhotos()
to run.
woodenTable.
get
(0)
retrieves the first DOM element in the set of elements stored in the woodenTable
jQuery object β in other words, the wooden table img
element.
Setting up the photos
The initPhotos()
function forms the bulk of the JavaScript code. Its job is to fade in the table once its image has loaded, then go through each photo image in the #lighttable
div
and set it up as follows:
- It’s positioned and rotated randomly within the table area
- It’s made draggable
- It’s initially hidden
- Once the photo image has loaded, it’s made 50% bigger, then gradually faded in and reduced back to normal size, giving the impression of it floating down onto the table
Let’s see how each chunk of this function works:
1. Fading in the table
function initPhotos() {
// (Ensure this function doesn't run twice)
if ( loaded ) return;
loaded = true;
// The table image has loaded, so bring in the table
$('#lighttable').fadeIn('fast');
First the function uses the loaded
variable to check that it hasn’t been called already β this works around browsers that fire the load
event more than once. Assuming this is the first time it’s been called, it then calls the jQuery fadeIn()
method on the #lighttable
div
. This causes the div
β and the now-loaded table image inside it β to appear.
2. Randomly positioning and rotating the photos
// Process each photo in turn...
$('#lighttable img').each( function(index) {
// Set a random position and angle for this photo
var left = Math.floor( Math.random() * 450 + 100 );
var top = Math.floor( Math.random() * 100 + 100 );
var angle = Math.floor( Math.random() * 60 - 30 );
$(this).css( 'left', left+'px' );
$(this).css( 'top', top+'px' );
$(this).css( 'transform', 'rotate(' + angle + 'deg)' );
$(this).css( '-moz-transform', 'rotate(' + angle + 'deg)' );
$(this).css( '-webkit-transform', 'rotate(' + angle + 'deg)' );
$(this).css( '-o-transform', 'rotate(' + angle + 'deg)' );
Next the function starts looping through each photo. To do this, it selects all images in the #lighttable
div
, then calls the jquery each()
method on the images. each()
works much like for..in
in JavaScript and foreach
in PHP, looping through each img
element in the set. You pass each()
a callback function, and that function is called for each image, setting the variable this
to the current img
element.
Within the loop function, we set a random position and rotation for the photo. We use the Math.random()
function to generate these values, allowing plenty of padding around the outside of the table (in case we’re working with very wide or long photos). For the rotation, we’ll tilt the photo no more than 30 degrees clockwise or anticlockwise, so that the photo is still easy to view.
Once we’ve generated the values, we plug them into various CSS properties for the photo. left
and top
position the top left hand corner of the photo within the table, and we use the CSS3 transform
property to rotate the photo. Since not all modern browsers support the standard transform
property, we add vendor prefixes for the common Mozilla, WebKit and Opera browser engines.
Why have we written $(this).css()
, rather than just this.css()
? Well, this
holds the raw DOM img
element object. So in order to call a jQuery method on the image β in this case, css()
β we first need to wrap the img
element in a jQuery object by writing $(this)
. (Remember that you can create a jQuery object from a DOM element object by passing the element object to the $()
function.)
3. Making the photos draggable
// Make the photo draggable
$(this).draggable( { containment: 'parent', stack: '#lighttable img', cursor: 'pointer' } );
Making objects draggable in the browser can be quite challenging, but fortunately the jQuery UI plugin makes it very easy, thanks to its draggable()
method. All you need to do is call draggable()
on the element(s) you want to make draggable. That’s it!
We pass a few options to the draggable()
method to control the dragging behaviour:
containment: 'parent'
- This ensures that the images can’t be dragged outside the parent element, in this case
#lighttable
. (The 70px padding we added to#lighttable
in the CSS ensures that the photos can’t be dragged within 70 pixels of the edge, which gives us some leeway when it comes to rotated photos.) stack: '#lighttable img'
- This very handy option lets you specify a set of elements that should be considered part of a stack along with the current element. The
draggable()
method then ensures that, when the current element is dragged, itsz-index
property is set higher than all otherz-indexes
in the stack. So by specifying all the images in#lighttable
as the stack, we ensure that the currently-dragged photo is always brought to the front. cursor: 'pointer'
- Turns the mouse cursor into a pointing hand when dragging.
4. Floating the photos onto the table
// Hide the photo for now, in case it hasn't finished loading
$(this).hide();
// When the photo image has loaded...
$(this).load( function() {
// (Ensure this function doesn't run twice)
if ( $(this).data('loaded') ) return;
$(this).data('loaded', true);
// Record the photo's true dimensions
var imgWidth = $(this).width();
var imgHeight = $(this).height();
// Make the photo bigger, so it looks like it's high above the table
$(this).css( 'width', imgWidth * 1.5 );
$(this).css( 'height', imgHeight * 1.5 );
// Make it completely transparent, ready for fading in
$(this).css( 'opacity', 0 );
// Make sure its z-index is higher than the photos already on the table
$(this).css( 'z-index', newImageZIndex++ );
// Gradually reduce the photo's dimensions to normal, fading it in as we go
$(this).animate( { width: imgWidth, height: imgHeight, opacity: .95 }, 1200 );
} );
// Hack for browsers that don't fire load events for cached images
if ( this.complete ) $(this).trigger("load");
The last chunk of the loop deals with floating each photo onto the table as it loads.
First the photo is hidden by calling the jQuery hide()
method. This ensures that we don’t see the image appear on the page while it’s still loading.
Next, we attach a load
event handler to the photo. This is triggered once the image has finished loading.
The event handler function first attaches a loaded
data value to the image, and uses this flag to prevent the handler running more than once for this image (since some browsers trigger a load
event more than once).
jQuery’s data()
method lets you attach arbitrary data to an element β very useful for storing and tracking additional information associated with the element.
Next, the handler stores the photo’s existing width and height in the variables imgWidth
and imgHeight
, so that it can restore the photo to the correct dimensions once it’s “landed” on the table.
Then the photo is increased to 1.5 times its width and height so that it looks like it’s floating above the table, and it’s also made transparent so it can be faded in. We also use the newImageZIndex
variable to set the photo’s z-index
so that it’s higher than any photos that have already been added to the table β this prevents the new photo disappearing behind existing photos on the table when it lands.
Now we’re all set to float the photo down to the table. We do this by calling the very powerful jQuery animate()
method, passing in the CSS properties that we’d like the final photo to have: the image’s true width and height, and an opacity of .95. The last argument to animate()
specifies how long we want the animation to last (1200 milliseconds). jQuery then does its stuff and gradually fades in the photo, reducing its width and height as it goes, until it ends up “landing” on the table with the correct width, height, and opacity value.
Finally, after setting up our event handler, we add the same hack that we used in the init()
function to ensure that the load
events are triggered even if the photos are cached.
That’s it!
Problems and Future Enhancements
While this script works pretty well, there are a few issues:
- No image rotation in IE. Since IE6/7/8 don’t support CSS transforms, the images aren’t rotated at all in these browsers. I assume it’ll work in IE9, but I don’t have this browser to test against. On the plus side, the basic light table works even in IE6 (which, frankly, surprised me!).
- It’s a bit jerky. At least, it is on my 2006 iMac. I don’t think there’s much I can do about this β browsers aren’t (yet) quite up to moving large images smoothly around the page. That said, maybe it’s fine on a more modern computer?
- Doesn’t work on iPhone/iPad. Not surprising, since
draggable()
doesn’t support swipe actions, although it may well do so in the future. Meanwhile, I might try some sort of workaround (any suggestions?).
In addition, since the photos on the table are rather small, I’ve written another tutorial that explains how to add a lightbox that shows a bigger version of a photo when you click it.
Anyway, I hope you like the script and found the tutorial useful. Please let me know your thoughts, as well as any suggestions for improving it, in the comments below!
seb1979 says
wow,I dont know when that was posted but I m surprised nobody realized how good this tutorial is.It’s clear,concise and explain what is going on.Great!
matt says
@seb1979: Thanks for your kind words – you made my day! Really pleased you liked the tutorial. I’m quite proud of that one. π
You might also like the follow-up tutorials, where I show how to add a lightbox and make the photos rotatable:
http://www.elated.com/articles/kitchen-table-lightbox/
http://www.elated.com/articles/smooth-rotatable-images-css3-jquery/
Cheers!
Matt
laffan says
Hi Matt –
First off, fantastic job with this tutorial . I agree with seb1979 – it’s everything one could help for as a beginner: clear, concise, simple and leaves you with a fantastic product.
I’m having difficulty with a slight modification. I would like to have several different piles (several different ‘kitchen tables’ if you will) of thumbnails on the same page. Do you know of a simple way to do this?
Thanks!
Nate
matt says
@laffan: Messy. You’d need to change the #lighttable div to several .lighttable divs, then hack the code to init all the tables, storing values like ‘newImageZIndex’ and ‘loaded’ in arrays instead of single variables. Doable, but it’s a fair bit of work.
A “quick and dirty” approach that might do the trick would be to create individual iframes in your page, one for each light table. Each iframe would contain its own page, with its own version of the light table JavaScript. Should work in theory! π
laffan says
Ah well, I’ll give the ‘real’ method a shot. Let me know if you ever try and tackle the multiple ‘buckets of thumbnails’ approach. Thanks for posting!
matt says
@laffan: You’re welcome. Let us know how it works out!
laffan says
Matt –
After much tinkering, I’ve decided to go another route and use the script as it was supposed to be used π However, I’ve got one further hitch that I’m curious to hear your advice on :
The moment I switch jquery.js with anything newer (my site uses http://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js) everything breaks!! The images don’t even show up. Any thoughts?
Thank you!
matt says
@laffan: Check your browser’s JavaScript console for any errors. What’s the URL of your page?
laffan says
Hi matt –
The error is :
Where line 48 = trigger(“load”);
It’s a client project that isn’t online yet so I can’t send a url, but does it work with modern versions of jQuery for you?
Thanks!
Nate
[Edited by laffan on 20-Oct-11 13:18]
matt says
@ laffan: I can get it to work with the following:
and changing the line:
to:
I think something changed regarding the way hide() interacts with CSS opacity a couple of jQuery versions back, which might explain the problem.
Anyway, the above changes work fine for me. I don’t see any JS errors.
laffan says
Hi Matt –
For some reason I didn’t see that you’d responded. Thank you!
Because of the bug I wasn’t able to use it on my client’s site, but I like the idea so much that I’ve actually implemented it on my own website http://www.natelaffan.com/photography (the ‘Scraps’ album).
Unfortunately, your solution isn’t working out for me. I mean, the script itself works wonderfully, but the moment I put in the modern version of jQuery none of the images appear. Anyways, if you happen to have a moment, I’d love to hear your thoughts, otherwise, thank you (again) for such a wonderful script!
Nate
matt says
@laffan: Nice use of the script – looks great π
No idea what the problem is with your page – like I say, it works fine for me with the latest jQuery using the above modifications. Can you get the original light table script to work with the latest jQuery and these mods? If so then it must be something else on your page that’s breaking it.
Cheers,
Matt
Mark5555 says
nice
jesper says
Hi,
Thanks a lot! I was just wondering if it is possible to add draggable text (words) and place it in the container?
Thank you.