Flickr Filmstrips in JavascriptFiled: Sun, Dec 24 2006 under Programming|| Tags: drag filmstrip javascript scroll voodoo As an avid Flickr junkie, one of the things I wanted to do was to put a scrollable filmstrip on my site to show off a few Flickr RSS feeds. Little did I know what that undertaking would entail. Quite frankly, good DHTML & javascript filmstrips are a rarity on the net and all told I found only one library that was too limited for my uses and worse, not a tutorial! So I ended up writing my own, and then I sat down and wrote the tutorial I wish I had found when this all first started. Before we begin, you should realize this is a major undertaking. The technology behind the filmstrip isn't terribly advanced but there are many different kinds of technology which make the completed application non-trivial. The end-result however, will look something like this... http://www.hunlock.com/examples/filmstrip6.html It's recommended you play with the filmstrip a little before starting the tutorial so you're familiar with the end result as we delve into how we achieve that result. Step 1 -- The Data While you won't need access to server side scripting to make your filmstrip you will need to set up a small text file on your server, just like you'd set up a new html page. Create a file called "filmstrip.txt" on your server. Next you need to populate it with the flickr images you want in your filmstrip. We're going to use 3 characters to separate our data fields (~|~), 2 characters to represent the end of the record (``) and one line at the end to represent the end of the dataset (EOF*EOF*EOF*EOF*EOF). The format of the dataset will be as such.... Flickr Image, Flickr Web Page, a description, and the Image ID. The Flickr Image is the url of the image without a .jpg at the end. Basically if you right-click on a Flickr image then click properties you can copy the url of the image. Here's an example URL: http://farm1.static.flickr.com/137/327793459_73ee2f3f40_s.jpg What we want to discard is the .jpg and we also want to discard the _s. There can be several different codes right before the .jpg _s means thumbnail, _m means medium, _o is original size, _l is large size. Since our script will insert the size code we need the url stripped of the size code and .jpg. So our image url will look like http://farm1.static.flickr.com/137/327793459_73ee2f3f40 in our data file. The next field is the actual web page of the image, just navigate to the image and clip the url from the browser address field. Next is the comment for the picture. Finally we include the raw picture code which isn't used in this tutorial but which is handy if you decide to expand on this example. In your first field just strip everything off before the last forward slash. So http://farm1.static.flickr.com/137/327793459_73ee2f3f40 becomes just 327793459_73ee2f3f40. This is a lot of work but you only have to do it once. When you're done you should have a data file that looks something like this. ~|~http://static.flickr.com/137/322382057_ff8f1346df~|~http://www.flickr.com/photos/robertakalil/322382057/~|~525.600~|~322382057_ff8f1346df`` ~|~http://static.flickr.com/130/321528421_34deec5b3b~|~http://www.flickr.com/photos/andrewkennedy/321528421/~|~Old Barge~|~321528421_34deec5b3b`` ~|~http://static.flickr.com/142/323678243_81ce1a0e86~|~http://www.flickr.com/photos/trainorphans/323678243/~|~kathysnap~|~323678243_81ce1a0e86`` EOF*EOF*EOF*EOF*EOF To make things easy on you here is a sample data file. Just right click on the link, click "save link as", then upload it to your web-server until you're ready to start using your own images. Step 2 -- Import & Process The Data Now that we have our data file on the server we'll use a little AJAX call to import the data, process it, and place the thumbnails on the page. Whot? AJAX without a server side script? Why not? The web server will send our datafile just as nicely as a server-side script would do it. And doing it this way means we can have multiple data files on the server and just change the filename in the javascript code to access them. Simple, no? First we'll set up the AJAX call.
function importData(fileName) {
var AJAX = null; // Initialize the AJAX variable.
if (window.XMLHttpRequest) { // Are we working with mozilla?
AJAX=new XMLHttpRequest(); // Yes -- this is mozilla.
} else { // Not Mozilla, must be IE
AJAX=new ActiveXObject("Microsoft.XMLHTTP"); // Wheee, ActiveX, how do we format c: again?
} // End setup Ajax.
if (AJAX==null) { // If we couldn't initialize Ajax...
alert("Your browser doesn't support AJAX."); // Sorry msg.
return false // Return false (WARNING - SAME AS ALREADY PROCESSING!)
} else {
AJAX.onreadystatechange = function() { // When the browser has the request info..
if (AJAX.readyState==4 || AJAX.readyState=="complete") { // see if the complete flag is set.
processData(AJAX.responseText);
} // End Ajax readystate check.
} // End create post-process fucntion block.
AJAX.open("GET", fileName, true); // Open the url
AJAX.send(null); // Send the request.
return true; // Everything went a-ok.
} // End Ajax setup aok if/else block
}
function processData (dat) {
// we'll process the data here.
}
Now we'll need a division to hold our filmstrip. <div id='filmstrip' class='filmstrip'></div> The next step is to modify the processData function so it processes the data it received from the server and loads the thumbnails into our division. For a bit of efficiency we'll add three global variables, one that will store our processed image data (as an array), another which will hold the width (in pixels) of our filmstrip layer, and one that will store our filmstrip layer so we don't constantly have to do getElementById lookups on it.
var _allImages = new Array(); // Holds all of the image data from server
var _filmstrip = document.getElementById('filmstrip'); // Our filmstrip layer
var _filmWidth = 0; // Width of filmstrip in pixels.
function processData (dat) {
allImages = dat.split('``'); // Explode the server image data into array elements
var ktr=0; // Initialize our counter
_filmstrip.innerHTML=''; // Initialize our filmstrip
_filmWidth=0; // Initiaize the film width.
var tmp = ''; // Temp variable to hold our HTML as we build it.
while ((ktr<allImages.length)&&(allImages[ktr].indexOf('EOF*EOF')<0)) { // For each record in allImages.
record = allImages[ktr].split('~|~'); // Explode record into individual fields.
// The next line builds our HTML in a variable, this is faster than doing X innerHTML insertions.
tmp+='<img src="'+record[1]+'_s.jpg" class="thumbnail">';
_filmWidth+=80; // Increment our filmstrip Width counter.
ktr++; // Increment our counter
} // End WHILE loop
_filmstrip.innerHTML=tmp; // Insert tmp into our filmstrip layer.
}
"processData" receives the complete data file from the server. It's called automatically when the browser receives the data (as set up in importData()). "processData" then splits out each line into an individual array element in allImages. Then for each line/record/image in allImages we break out the fields (separated by ~|~) so we can get our image url and then insert the thumbnail image into our _filmstrip layer. Well actually we're inserting the thumbnail html into a temporary variable. Doing innerHTML injections is actually pretty slow. You will notice a SUBSTANTIAL increase in performance doing one BIG insertion as opposed to doing many SMALL ones. If you'll notice our url... record[1]+'_s.jpg', remember when you had to remove all that information from the data? That's so we could specifically request the _s code here which flickr will use to return a 75 pixel by 75 pixel thumbnail regardless of how large the original image actually was. So far we've set up a data file on the server, wrote a small ajax function to read that file and then a processData function which takes the server data, explodes it out and throws a bunch of thumbnails into our filmstrip division. Here's a sample of what we've accomplished so far: Filmstrip Example #1. Not much of a filmstrip eh? More like a blob of thumbnails! Step 3 -- Format the Filmstrip Our stylesheets will do the bulk of the work in making the filmstrip into an actual filmstrip instead of a blob of images. In our scripts and division we set up two stylesheet classes. Our filmstrip division uses the filmstrip class, and our individual images use the thumbnail class. There's not much to do with the thumbnails, just give it a small bit of whitespace to break up the images a little. Since Flickr thumbnails are 75 pixels by 75 pixels we'll go ahead and specify a width and a height too so the filmstrip will appear formatted properly a little faster.
<style type='text/css'>
.thumbnail {
margin-right: 5px;
width: 75px;
height: 75px;
}
</style>
The filmstrip class is a little bit more complicated. The first and most important thing is that we have to set the position to absolute. Since we'll be using the clip style to show only a little bit of the entire filmstrip at a time the filmstrip division has to be positioned absolutely (position: absolute). Next we specify that anything which won't fit on the screen we'll just hide, this keeps the browser from creating a horizontal scroll bar (overflow: hidden). Since flickr thumbnails are always 75 pixels in height we'll go ahead and fix the height of the division at 75 pixels, and so your visitors can see they can grab and drag the filmstrip we'll change the cursor to give them a visual queue when the mouse is over the filmstrip.
.filmstrip {
position: absolute;
overflow: hidden;
height: 75px;
cursor: w-resize;
}
See what a difference a style sheet makes! Step 4 -- Anchor the filmstrip to the web page When we made the filmstrip layer's position absolute we effectively let it float above the web page. While this is ok for our detail window (which we'll make a little lower down), we need to have the filmstrip more tangibly affixed to our web page. To this end we'll create a table and fix its width and height. We'll give it a name 'landingZone' and then set our filmstrip to match the dimensions and positions of the table. Later on, we'll check for page resize and moves to reposition our filmstrip so it always stays within the boundary of the table. The first step is to actually place our table somewhere on the web page. <table style='border: 1px solid black' width=400px height=75 cellspacing=0 cellpadding=0 id='landingZone'> <tr><td> </td></tr> </table> Next we'll add a new global variable to our javascript which is just the landingZone element. As a rule, whenever I'm going to be frequently accessing an element I make it a global variable to limit the number of getElementsByID calls that need to be made. To prevent confusion I also prefix global variable names with an underscore (_) to indicate that they are global and not local. We'll now add a function which will calculate the position of an element on the web page. This nifty function comes to us courtesy of quirksmode. It accepts an html element (landingZone in this case) and returns back an array with element [0] being the left position of the element, [1] being the top, and [2] being the width. Once we get the location information for landingZone we adjust filmstrip's top, left and width to match. Since they will be used elsewhere we'll also add two global variables that contain the last calculated _topOffset and _leftOffset of the landingZone table. With our new table comes our first clip style. clip is a css method which will crop a larger image and show only the section we define. The clip style accepts a rect definition which represents the top, right, bottom, left positions respectively. For this we need to define the global _totalOffset which marks the current X offset of the filmstrip. Since we're only scrolling left and right the top and bottom values are always constant.
var _landingZone= document.getElementById('landingZone'); // Table which creates the filmstrip bondary
var _topOffset = 0; // Y position of the table in the web page
var _leftOffset = 0; // X position of the table in the web page.
var _totalOffset = 0; // Current X offset of the filmstrip.
function findPos(obj) {
// Credit for this function: http://www.quirksmode.org/js/findpos.html
// Visit the URL for a complete tutorial on this function
var curleft = curtop = 0;
if (obj.offsetParent) {
curleft = obj.offsetLeft
curtop = obj.offsetTop
curwidth = obj.offsetWidth;
while (obj = obj.offsetParent) {
curleft += obj.offsetLeft
curtop += obj.offsetTop
}
}
return [curleft,curtop,curwidth];
}
function bindStrip() {
// moves filmstrip into the landingZone table.
ofs=findPos(_landingZone); // Find the left/top & width of table
_filmstrip.style.top=ofs[1]+'px'; // Set filmstrip's top location
_filmstrip.style.left=ofs[0]+'px'; // Set filmstrip's left location
_topOffset = ofs[1]; // Remember top offset globally
_leftOffset= ofs[0]; // Remember the left offset globally
// The next line crops the filmstrip and shows only a small section of it
_filmstrip.style.clip='rect(0px,'+(_landingZone.offsetWidth+_totalOffset)+'px,80px,'+_totalOffset+'px)';
_filmstrip.style.left=(_leftOffset-_totalOffset)+'px'; // set the left offset of the filmstrip
}
bindStrip();
Although we simulated placing the filmstrip inside the landingZone table, filmstrip isn't tightly bound, if the page is resized, the filmstrip will wander outside of landingZone's boundaries. So we'll modify our <body> tag to put an onresize event which will call our bindStrip() function to get the new values of our landingZone table. <BODY onresize='bindStrip()'> And here's an example page of where we are so far. Step 5 -- Grab and Drag This is where things get interesting and horrendously complicated. The good news is that if you make it past this section, it's all downhill from there. The bad news is that the section of code that makes grabbing and dragging the filmstrip possible has a really steep learning curve. Well, chin up, and good luck! Don't forget to pull the ripcord. To create a drag handler we'll need to define an event and then create a function to process it. We're also going to define a bunch of global variables to keep track of the relative movements of the mouse and the filmstrip.
var _totalChange = 0; // Total pixels moved/changed
var _savedTarget = null; // Target object of drag event
var _orgCursor = null; // Original cursor Style
var _dragXOffset = 0; // X offset of the move event
var _dragYOffset = 0; // Y offset of the move event
var _scrollOK = false; // True if we can scroll filmstrip
var _originalOffset=0; // Original offset (before drag event)
// Begin Drag/Scroll Handlers
function scrollHandler(e) {
// This function handles the draging of the filmstrip from left to right
if (e == null) { e = window.event; } // Get event data
_totalChange=(_dragXoffset-e.clientX); // Calculate total movement
if (e.button<=1&&_scrollOK){ // Is mouse down and scroll enabled?
_totalOffset=_originalOffset+(_dragXoffset-e.clientX); // Find the distance mouse has moved
if ((_totalOffset) < 0) { // Are we scrolling too far left?
_totalOffset=0; // Yes, set the offset to 0
_originalOffset=0; // and the original
_dragXoffset=e.clientX; // and reset the mouse.
_totalChange=20; // Simulate a move so we don't open a pic.
}
if ((_totalOffset+_landingZone.offsetWidth)>=(_filmWidth-5)) { // Are we too far right?
_totalOffset=(_filmWidth-_landingZone.offsetWidth-5); // Yes, set offset to max
_originalOffset=(_filmWidth-_landingZone.offsetWidth-5);// and the original
_dragXoffset=e.clientX; // reset the mouse
_totalChange=20; // Simulate a move so we don't open a pic.
}
// Set the visible clip here.
_filmstrip.style.clip='rect(0px,'+(_landingZone.offsetWidth+_totalOffset)+'px,80px,'+_totalOffset+'px)';
// And set the left offset
_filmstrip.style.left=(_leftOffset-_totalOffset)+'px';
// return false so the browser won't try to do it's events!
return false;
}
}
function cleanup(e) {
// called when user releases a mouse button after a drag event
_scrollOK = false; // Setting this to false disables scrollHandler
_savedTarget.style.cursor=_orgCursor; // Change back to the original cursor shape
document.onmousemove=null; // Disable the mousemove handler
document.onmouseup=null; // Disable the mouseup handler
}
function dragHandler(e) {
_totalChange=0; // Total pixels moved
var cursorType='-moz-grabbing'; // Set hand to "grab"
if (e == null) { e = window.event; cursorType='w-resize';} // Package event for IE
var target = e.target != null ? e.target : e.srcElement; // Get the target object
//Handle filmstrip
if (target.className=="filmstrip"||target.className=="thumbnail") { // Check if event over filmstrip
_savedTarget=target; // This is our target object
_orgCursor=target.style.cursor; // This was our orginal cursor
_originalOffset=_totalOffset; // Mark the current offset
target.style.cursor=cursorType; // Set mouse to grab if possible
_scrollOK=true; // flag filmstrip as scrollable
_dragXoffset=e.clientX; // remember original X position
document.onmousemove=scrollHandler; // set up a mouse-move handler
document.onmouseup=cleanup; // set up a button-up handler
return false; // return FALSE -- IMPORTANT!!!!
}
}
// End drag/scroll Handlers
document.onmousedown = dragHandler; // Call dragHandler on mouse down
Basically we define an onmousedown event that calls dragHandler whenever the user clicks the mouse button. Draghandler checks to see that the event happened over our filmstrip and if so enables the mousemove handler and mouseup handler. Now whenever the user moves the mouse "scrollHandler" will be called, and when then button is released "cleanup" will be called. Movehandler basically sees how much the mouse has been moved and then sets the visible clip of the filmstrip based on that value. We're not doing a carousel, the filmstrip will stop when either end has been reached. To do a carousel the complexity of the script increases dramatically as we add duplicate images to each side and then adjusting the clip to the opposite side once an end has been reached, something which is beyond the scope of this tutorial. Example 4 shows our progress to date. Note that you can now click on and drag the filmstrip on the example from left to right. Which means that we are almost done! Step 6 -- Setup a detail layer Now that we've got a filmstrip that we can drag around, it's time to allow the user to click on one of those nice thumbs and see a larger version of the picture. Here we'll set up a division (named detailFrame) to hold our detailed picture. We'll put a few menu links at the top, another division to hold our picture and then a final division to hold our comments. We'll get a little fancy and make the comments visible only when the user has the mouse over the picture. We'll also add a few styles to our style sheet to make this all look pretty. Feel free to adjust the styles to get the layers to integrate well into your site.
<style>
.detail {
position: absolute;
border: 1px solid black;
background-color: #DDDDDD;
display: none;
cursor:move;
}
.detailLink {
text-decoration: none;
color: black;
}
.detailLink:hover {
background-color: black;
color: white;
}
.detailComments {
font-family: verdana;
font-size: 9pt;
color: black;
display: none;
}
.detailImg {
cursor: move;
padding: 5px;
}
</style>
<div id='detailFrame' class='detail'>
<A HREF="javascript:flickrGo()" class='detailLink'>LINK</A>
<A HREF="javascript:flickrZoom()" class='detailLink'>ZOOM</A>
<A HREF="javascript:showPic()" class='detailLink'>CLOSE</A>
<div id='detailPic' onMouseOver='document.getElementById("comments").style.display="block"'
onMouseOut='document.getElementById("comments").style.display="none"'></div>
<div id='comments' class='detailComments'></div>
</div>
The detailFrame division starts out life hidden thanks to "display:none" in the style sheet. We'll need to write a function to make it visible when the user clicks on the thumbnail. And we'll be adding a new global variable to hold the index of the picture we're currently viewing.
var _currentPic = 0; // Index of current Picture
function showPic(idx) {
if ((_totalChange<-5)||(_totalChange>5)) { return false } // User scrolled, not a click event so return.
detail=document.getElementById('detailFrame'); // get the detail window
detailComments=document.getElementById('comments'); // get the comments window
detailImg=document.getElementById('detailPic'); // get the picture window
if (idx==null) {idx=_currentPic} // set idx if nothing was passed.
if ((detail.style.display=='block')&&(idx==_currentPic)) { // If the detail window is visible
detail.style.display='none'; // Make it invisible
detailImg.innerHTML=''; // Clear out last picture
detailComments.innerHTML=''; // Clear out last comments
} else { // Detail Window is not visible
var record = allImages[idx].split('~|~'); // Get the image record
detailImg.innerHTML='<img src="'+record[1]+'.jpg" class="detailImg">'; // Insert the pic
detailComments.innerHTML=record[3]; // Insert the comments
detail.style.display='block'; // Make the detail window visible
_currentPic=idx; // Remember the current picture.
}
}
}
Now to call showPic we need to modify processData so when it inserts the thumbnails it attaches an onmouseup event that calls showpic with the right index. One thing to note is the first line of showPic checks the _totalChange global variable and sees if it's greater than 5 or less than -5 this differentiates from a click event or a drag event. If the filmstrip was dragged more than 5 pixels showPic will not run, if was dragged less than five pixels which is usually the case when the user just taps the mouse button then showpic will run. So here's our modified processData, note the onMouseUp event in the img.
function processData (dat) {
allImages = dat.split('``'); // Explode the server image data into array elements
var ktr=0; // Initialize our counter
_filmstrip.innerHTML=''; // Initialize our filmstrip
_filmWidth=0; // Initiaize the film width.
var tmp = ''; // Temp variable to hold our HTML as we build it.
while ((ktr<allImages.length)&&(allImages[ktr].indexOf('EOF*EOF')<0)) { // For each record in allImages.
record = allImages[ktr].split('~|~'); // Explode record into individual fields.
// The next line builds our HTML in a variable, this is faster than doing X innerHTML insertions.
tmp+='<img src="'+record[1]+'_s.jpg" class="thumbnail" onMouseUp="showPic(\''+ktr+'\')">';
_filmWidth+=80; // Increment our filmstrip Width counter.
ktr++; // Increment our counter
} // End WHILE loop
_filmstrip.innerHTML=tmp; // Insert tmp into our filmstrip layer.
}
The close link already works, it just calls showPic again with no arguments which is processed as a close. The showPic routine is pretty smart, if the thumbnail is clicked and it's already being displayed in the detail window showPic will close the detail window. If a NEW thumbnail was clicked it will keep the detail window open and show the image the user requested. So all we need to do is to process "link" and "zoom". Since we keep track of the current index in _currentPic both of these functions are trivial.
function flickrGo() {
var record = allImages[_currentPic].split('~|~'); // Get the image record
document.location.href=record[2]; // Navigate to the photo's web page on flickr.
}
function flickrZoom() {
var record = allImages[_currentPic].split('~|~'); // Get the image record
document.location.href=record[1]+'_o.jpg'; // Navigate to the original resolution picture on flickr
}
If you think this is starting to look pretty done why then you'd be right! Take a look at how far we've come in example #5. Step 7 -- Wrapping it all up The detail window is absolutely positioned which means we can, and will, integrate it into our drag handlers so the user can toss the picture around on the screen with abandon. You had to know this was comming! First we'll add yet another global variable. A _dragOK variable that will be true when we can move the detail layer, At the bottom of our javascript we'll add the top and left initializations of our detail layer. var _dragOK=false; // True when we can move the detail window .. .. .. _detail.style.top=_topOffset+90+'px'; // Initialize the location of the detail window _detail.style.left='200px'; // Position detail window 200 pixels from left margin Finally we add a new function called "moveHandler" to handle moving our detail layer and then slightly modify dragHandler so it looks for clicks on our detail layer and picture.
function moveHandler(e){ // Called on mousemove when detail is dragged
if (e == null) { e = window.event; } // Get the event data
if (e.button<=1&&_dragOK){ // Make sure mouse button is down and ok to drag
_savedTarget.style.left=e.clientX-_dragXoffset+'px'; // Move to the new left offset
_savedTarget.style.top=e.clientY-_dragYoffset+'px'; // Move to the new top offset.
return false; // Return false so browser doesn't do its own thing
}
}
function dragHandler(e) {
_totalChange=0; // Total pixels moved
var cursorType='-moz-grabbing'; // Set hand to "grab"
if (e == null) { e = window.event; cursorType='w-resize';} // Package event for IE
var target = e.target != null ? e.target : e.srcElement; // Get the target object
//Handle filmstrip
if (target.className=="filmstrip"||target.className=="thumbnail") { // Check if event over filmstrip
_savedTarget=target; // This is our target object
_orgCursor=target.style.cursor; // This was our orginal cursor
_originalOffset=_totalOffset; // Mark the current offset
target.style.cursor=cursorType; // Set mouse to grab if possible
_scrollOK=true; // flag filmstrip as scrollable
_dragXoffset=e.clientX; // remember original X position
document.onmousemove=scrollHandler; // set up a mouse-move handler
document.onmouseup=cleanup; // set up a button-up handler
return false; // return FALSE -- IMPORTANT!!!!
} else {
if (target.className.indexOf('detail')>=0) {
_savedTarget=document.getElementById('detailFrame'); // This is our target object
_orgCursor=target.style.cursor; // This was our orginal cursor
target.style.cursor=cursorType; // Set mouse to grab if possible
_dragOK=true; // flag filmstrip as scrollable
_dragXoffset=e.clientX-parseInt(_savedTarget.style.left);
_dragYoffset=e.clientY-parseInt(_savedTarget.style.top);
document.onmousemove=moveHandler; // set up a mouse-move handler
document.onmouseup=cleanup; // set up a button-up handler
return false;
}
}
}
Step 8 -- The End Now as amazing as it may seem, we've actually come to the conclusion of this tutorial! You can see the full, working, example at http://www.hunlock.com/examples/filmstrip6.html and it should serve as a strong foundation for you to modify and extend the features of the library. There's of course a lot more that you can do to this framework. You can change it into a carousel so it doesn't stop when you reach the ends, it just circles around. You can add a slideshow. You can rotate the images and, well, your imagination is your only real limit here. One thing you can do right away, without needing to do any serious modifications to the script is to take advantage of the ajax routines to create multiple filmstrips! All you have to do is to create another filmstrip data file (just like above) and give it a different filename on your server. Once that is done you can do something like...
<A HREF="#" onClick="importData('filmstrip.txt'); return false">Filmstrip Set #1</A><BR><BR>
<A HREF="#" onClick="importData('anotherstrip.txt'); return false">Filmstrip Set #2</A><BR><BR>
Whenever the user clicks on one of the links the script will swap out the filmstrip
with the one the user has selected. It's pretty cool for being so easy. You can
see the final results at http://www.hunlock.com/examples/filmstrip7.html.
|