Build a 360 view image slider with JavaScript

Robert Pataki of Waste Creative demonstrates how to show-off great looking products and keep users interested by making your own 360 view image slider in JavaScript.

  • Knowledge needed: Basic HTML5, intermediate jQuery, JavaScript and CSS
  • Requires: jQuery, CanvasLoader, pre-rendered image sequence
  • Project Time: 1 hour
  • Support file

This article first appeared in issue 224 of .net magazine the world's best-selling magazine for web designers and developers.

We’re going to build a 360 view image slider. What can you use it for? Well, it comes in very handy when your client wants to show their product from every angle rather than showing just a couple of simple angle shots.

We’ve seen quite a few nice interactive examples from car and phone manufacturers showcasing their products using this technique, but in most cases they use Flash.

I love interactivity, but I don’t really like using plug-ins unless it’s essential, so in this tutorial we’ll build our own 360 view application using HTML, CSS and JavaScript.

How will it work?

We’re going to be using a pre-rendered image sequence, displaying a 3D object rotated around its y-axis. The sequence contains 180 images to show an animation as smoothly as possible. The user can ‘rotate the object’ by dragging the mouse horizontally, or by swiping their fingers over the image slider on a touchscreen.

How will we do it?

First we’ll build our HTML markup, then we’ll add some custom CSS including media queries, then we’ll make it groovy with JavaScript. To make it more exciting not only will we make it work on desktop computers but also on iPhones and iPads because the app works very well with touch events.

Before we start, let me say a massive thank you to Mateusz Sypien and Maya Prodanova for their amazing 360 artwork!

01. Folders

In the project folder we have a css folder, a js folder and an img folder. The css folder contains the reset.css file, the img folder contains the image sequence and the js folder contains the jQuery and Heartcode CanvasLoader libraries.

02. New project

Create a new HTML file and save it to the project root as index.html. In the <head> we set the viewport for mobile devices by making our content non- scaleable. We include two CSS files: Eric Meyer’s reset.css and the threesixty.css, which will contain our custom styles.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0" />
<title>360</title>
<link rel="stylesheet" href="css/reset.css" media="screen" type="text/css" />
<link rel="stylesheet" href="css/threesixty.css" media="screen" type="text/css" />
</head>
<body>
<div id="threesixty">
<div id="spinner">
<span>0%</span>
</div>
<ol id="threesixty_images"></ol>
</div>
</body>
</html>

03. Loading percentage

Create a wrapper <div> for the slider. It contains an <ol>, which will hold the image sequence as <li>s, and also holds the preloader <div> holding a <span> for the loading percentage display. We will add the images dynamically using JavaScript.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0" />
<title>360</title>
<link rel="stylesheet" href="css/reset.css" media="screen" type="text/css" />
<link rel="stylesheet" href="css/threesixty.css" media="screen" type="text/css" />
</head>
<body>
<div id="threesixty">
<div id="spinner">
<span>0%</span>
</div>
<ol id="threesixty_images"></ol>
</div>
</body>
</html>

04. Adding interaction

Just before the </body>, include the JS files we’ll be using. jQuery helps us to add interaction quickly, the Heartcode CanvasLoader will add a smooth preloader animation. The threesixty.js file will contain the JavaScript that controls the image slider.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0" />
<title>360</title>
<link rel="stylesheet" href="css/reset.css" media="screen" type="text/css" />
<link rel="stylesheet" href="css/threesixty.css" media="screen" type="text/css" />
</head>
<body>
<div id="threesixty">
<div id="spinner">
<span>0%</span>
</div>
<ol id="threesixty_images"></ol>
</div>

<script src="js/heartcode-canvasloader-min.js"></script>
<script src="js/jquery-1.7.min.js"></script>
<script src="js/threesixty.js"></script>
</body>
</html>

05. Styling

Now create the threesixty.css file. The reset.css file sets all the default behaviours, so we can move on to the fun bits. Style the #threesixty wrapper first. The default image slider will be 960px by 540px, centred horizontally and vertically. We also set the <ol> element to be hidden.

#threesixty {
position:absolute;
overflow:hidden;
top:50%;
left:50%;
width:960px;
height:540px;
margin-left:-480px;
margin-top:-270px;
}
#threesixty_images {
display: none;
}

06. Set the display

To make the interface work on different screens while keeping proportions consistent, we’re going to control the display using media queries. You can see in the snippet above how we define our different resolution and orientation device criteria using the max-device-width and orientation properties combined with the and operator. The snippet above sets the display for iPad (1024px wide) and iPhone/iPod touchscreens, both in landscape and portrait modes (480px wide). The images will take up all of the available space inside the wrapper.

@media screen and (max-device-width: 1024px) and (orientation:portrait) {
#threesixty {
width:720px;
height:450px;
margin-left:-360px;
margin-top:-225px;
}
}
@media screen and (max-device-width: 480px) and (orientation:landscape),
screen and (-webkit-min-device-pixel-ratio: 2) and (orientation:landscape) {
#threesixty {
width:360px;
height:225px;
margin-left:-180px;
margin-top:-113px;
}
}
@media screen and (max-device-width: 480px) and (orientation:portrait),
screen and (-webkit-min-device-pixel-ratio: 2) and (orientation:portrait) {
#threesixty {
width:320px;
height:200px;
margin-left:-160px;
margin-top:-100px;
}
}

07. Images

All the images will be placed in the <ol>. First we set the style for every image in the wrapper. We don’t want the images to all be visible, so we define the current-image and the previous-image classes that control their visibility. We’ll swap between these states with JavaScript.

#threesixty img {
position:absolute;
top:0;
width:100%;
height:auto;
}
.current-image {
visibility:visible;
width:100%;
}
.previous-image {
visibility:hidden;
width:0;
}

08. Preloader style

Style our preloader by making #spinner hidden, setting its dimensions and placing it in the centre of the wrapper. We also set the styles of the span inside the #spinner to be horizontally and vertically centred so the text will be in the middle of the circular animation.

#spinner { position:absolute; left:50%; top:50%; width:90px; height:90px; margin-left:-45px; margin-top:-50px; display:none;}#spinner span { position:absolute; top:50%; width:100%; color:#333; font:0.8em Arial, Verdana, sans; text-align:center; line-height:0.6em; margin-top:-0.3em;}

09. Getting ready

Create a new JS file and save it as threesixty.js in the js folder. Place the code into the jQuery DOM-Ready function, so that by the time the script starts running, the DOM elements are already in place. ready will enable the user interaction when our app is ready.

Dragging tells us if the user is using the pointer. With speedMultiplier we set the speed of the image sliding; we’ve got some variables to store the pointer positions – the timers will track the pointer changes – and we define some variables to keep track of the frame calculations and image loading.

$(document).ready(function () {
var ready = false,
dragging = false,
pointerStartPosX = 0,
pointerEndPosX = 0,
pointerDistance = 0,

monitorStartTime = 0,
monitorInt = 10,
ticker = 0,
speedMultiplier = 10,
spinner,

totalFrames = 180,
currentFrame = 0,
frames = [],
endFrame = 0,
loadedImages = 0;
});

10. Spinner

We create the addSpinner function that adds a CanvasLoader instance with custom settings inside the #spinner. The spinner will be a 90x90px, spiral shaped loader with a smooth circular animation. Call its show method, then display it by using the jQuery fadeIn.

function addSpinner () { spinner = new CanvasLoader("spinner"); spinner.setShape("spiral"); spinner.setDiameter(90); spinner.setDensity(90); spinner.setRange(1); spinner.setSpeed(4); spinner.setColor("#333333"); spinner.show(); $("#spinner").fadeIn("slow");};

11. Image loading and frames array

The load image function creates a <li> with an <img> inside. Hide the image with the previous-image class. The loadedImages variable generates the image name, which increments each time a new image is loaded; if successful, we call the imageLoaded function.

In one single line of code we create a new <img>, point its source to the generated file name, then hide it by applying the previous-image class. All in one line thanks to jQuery! We store each image object returned by jQuery in the frames array, which will be handy when it comes to animation.

function loadImage() {
var li = document.createElement("li");
var imageName = "img/threesixty_" + (loadedImages + 1) + ".jpg";
var image = $('<img>').attr('src', imageName).addClass("previous-image").appendTo(li);
frames.push(image);
$("#threesixty_images").append(li);
$(image).load(function() {
imageLoaded();
});
};

12. Image overload

There are too many images to load all at once, so we call loadImage recursively. The image loading process is shown by writing the percentage text into the #spinner <span>. Once all the images are loaded, we make the first image visible and hide the preloader.

function imageLoaded() {
loadedImages++;
$("#spinner span").text(Math.floor(loadedImages / totalFrames * 100) + "%");
if (loadedImages == totalFrames) {
frames[0].removeClass("previous-image").addClass("current-image");
$("#spinner").fadeOut("slow", function(){
spinner.hide();
showThreesixty();
});
} else {
loadImage();
}
};

13. Smooth transition

Display the image slider with a smooth transition using the showThreesixty function. We launch the app by calling addSpinner() and loadImage(). When tested, the preloader fades in, and when the images loaded, a transition hides it and displays the first image.

function imageLoaded() {
loadedImages++;
$("#spinner span").text(Math.floor(loadedImages / totalFrames * 100) + "%");
if (loadedImages == totalFrames) {
frames[0].removeClass("previous-image").addClass("current-image");
$("#spinner").fadeOut("slow", function(){
spinner.hide();
showThreesixty();
});
} else {
loadImage();
}
};

function showThreesixty () {
$("#threesixty_images").fadeIn("slow");
ready = true;
};

addSpinner();
loadImage();

14. Frame values

The slider animation is going to be simple: we tween the current frame value to a set end frame value and display the current image. A custom easing method calculates the distance between the frames and creates a smooth spinning animation in either direction.

Create the render function that updates the frame animations. If the current frame hasn’t reached the end frame, move it closer by changing its value: when it reaches the end frame, stop the rendering. Update the images with the hidePreviousFrame and showCurrentFrame functions.

function render () {
if(currentFrame !== endFrame)
{
var frameEasing = endFrame < currentFrame ? Math.floor((endFrame - currentFrame) * 0.1) : Math.ceil((endFrame - currentFrame) * 0.1);
hidePreviousFrame();
currentFrame += frameEasing;
showCurrentFrame();
} else {
window.clearInterval(ticker);
ticker = 0;
}
};

15. Ticker

The refresh function calls our render function, which creates a setInterval that we store in the ticker variable. To make sure we don’t make any unwanted calls, check to see if the ticker is already running. I set the FPS value to 60, so we can have a nice, smooth animation.

function refresh () {
if (ticker === 0) {
ticker = self.setInterval(render, Math.round(1000 / 60));
}
};

16. Normalised value

hidePreviousFrame and showCurrentFrame swap the states of the current image. Call getNormalizedCurrentFrame() to get the frame value between 1-180, which returns the ‘normalised’ value of currentFrame that we use to manipulate the current image.

To get a ‘swooshy’ effect the current frame value can’t go out of the range defined by totalFrames. With the getNormalizedCurrentFrame function commands, we can calculate the values within the totalFrames range so the animation moves accurately.

function hidePreviousFrame() {
frames[getNormalizedCurrentFrame()].removeClass("current-image").addClass("previous-image");
};

function showCurrentFrame() {
frames[getNormalizedCurrentFrame()].removeClass("previous-image").addClass("current-image");
};

function getNormalizedCurrentFrame() {
var c = -Math.ceil(currentFrame % totalFrames);
if (c < 0) c += (totalFrames - 1);
return c;
};

17. Testing

Let’s give it a quick test. In the showThreesixty function we will add two things: first we set the endFrame to -720, and second, we call the refresh method. If you test the application, you will see the images quickly swooshing around as they’re fading in.

function showThreesixty () {
$("#threesixty_images").fadeIn("slow");
ready = true;
endFrame = -720;
refresh();
};

18. User interaction

Add the user interaction. Start with the mouse event listeners, then add the custom function getPointerEvent, which tells us if the user uses a mouse or finger. On mouse down, we store the pointer X position, and on mouse move we call trackPointer().

function getPointerEvent(event) {
return event.originalEvent.targetTouches ? event.originalEvent.targetTouches[0] : event;
};

$("#threesixty").mousedown(function (event) {
event.preventDefault();
pointerStartPosX = getPointerEvent(event).pageX;
dragging = true;
});

$(document).mouseup(function (event){
event.preventDefault();
dragging = false;
});

$(document).mousemove(function (event){
event.preventDefault();
trackPointer(event);
});

19. Touch events

Now add the touch events. We use the getPointerEvent() to pass the correct event to the trackPointer function. For all events, we prevent the defaultbehaviour and set the dragging value to true if the user is dragging the pointer, and to false for when they release.

$("#threesixty").live("touchstart", function (event) {
event.preventDefault();
pointerStartPosX = getPointerEvent(event).pageX;
dragging = true;
});

$("#threesixty").live("touchmove", function (event) {
event.preventDefault();
trackPointer(event);
});

$("#threesixty").live("touchend", function (event) {
event.preventDefault();
dragging = false;
});

20. Tracking movement

In trackPointer() we track the pointer movement periodically to make the frameanimation flow with the pointer dragging. As you can see, we only track the pointer if dragging is true and the application is ready. We do this because the rendering can be quite CPU intense and we have to be smart with the processing.

Store the start and end x pointer positions, then check the distance between them within the time periods. Using pointerDistance, calculate the animation endFrame and update it by calling the refresh function. speedMultiplier gives us full control over the spinning speed.

function trackPointer(event) {
if (ready && dragging) {
pointerEndPosX = getPointerEvent(event).pageX;
if(monitorStartTime < new Date().getTime() - monitorInt) {
pointerDistance = pointerEndPosX - pointerStartPosX;
endFrame = currentFrame + Math.ceil((totalFrames - 1) * speedMultiplier * (pointerDistance / $("#threesixty").width()));
refresh();
monitorStartTime = new Date().getTime();
pointerStartPosX = getPointerEvent(event).pageX;
}
}
};

21. We're done!

If you test the application you can see the percentage loader animation fading in and out, then the images showing up with the ‘swooshy’ spin. Now you can use the image slider with your mouse, or a touch on mobile devices, to make the 360 image slider spin with a responsive, smooth animation.

The best way to test the application on mobile devices is to place the project files onto a server, or share your localhost with the device. You can also see how different device screen resolutions and orientations are handled with media queries.

I have decided to make this slider open source, and created a git repository to allow others to collaborate and help to make it awesome.

Róbert Pataki is a creative technologist with years of experience in Flash technologies and creative advertising and currently works for digital agency Waste Creative in London.

Liked this? Read these!