The goal of this project was to implement a carousel widget using vanilla JS. Originally developed for a graphics portfolio when similar projects weren’t readily available.
This implementation works with any set of images by initializing the Carousel(…) function with the container ID, desired pixel dimensions and relative speed of the animation. When all images are done loading, the container is made visible and this script dynamically generates reflections for each image using a combination of canvas elements and transparency gradients. The speed and direction of the animated rotation is determined by the relative position of the mouse cursor from the center of the carousel container.
Clicking on an image pauses the animation and enlarges the image into a display box. Optionally, a description for each image file can be specified in a separate JS file to be displayed along with the image. Finally, clicking on the enlarged image closes the display box and resumes the animation.
Live Demo
The functions responsible for this effect are implemented below:
//Carousel, by Vlad Turda
function Carousel(superContainerId, showCaseDivId, carouselDivId, carRadius, carXStretch, carYStretch) {
var superContainer = document.getElementById(superContainerId);
var showCaseDiv = document.getElementById(showCaseDivId);
var carouselDiv = document.getElementById(carouselDivId);
//Get image list
var imgNodeList = carouselDiv.getElementsByTagName('img');
var elementList = new Array();
//Carousel attributes
var rotationSpeed = 0.2;
var radius = 350;
var xStretch = 1;
var yStretch = 0.15;
//Display attributes
var maxThumbWidth = 240;
var maxThumbHeight = 160;
var minShowCaseWidth = 400;
var minShowCaseHeight = 300;
if (carRadius != undefined) { radius = carRadius; }
if (carXStretch != undefined) { xStretch = carXStretch; }
if (carYStretch != undefined) { yStretch = carYStretch; }
disableHighlighting(showCaseDiv);
disableHighlighting(carouselDiv);
superContainer.style.display = 'block';
setupCarouselDiv(carouselDiv);
carouselDiv.attachEventListener('mousemove', function (e) {
if (!e) { e = event; }
rotationSpeed = -(e.clientX - (carouselDiv.clientWidth / 2) - carouselDiv.offsetLeft) / radius;
if (rotationSpeed > 1 || rotationSpeed < -1) { rotationSpeed = rotationSpeed - rotationSpeed % 1; }
});
//Create Image list
for (var i = 0; i < imgNodeList.length; i++) {
elementList[i] = new imageElement(imgNodeList[i], showCaseDiv, maxThumbWidth, maxThumbHeight, minShowCaseWidth, minShowCaseHeight);
elementList[i].posIndex = parseInt(i * 360 / imgNodeList.length);
}
//Add shadows
for (var i = 0; i < elementList.length; i++) {
//Canvas Shadows/reflections are NOT for IE
if (navigator.appVersion.indexOf('MSIE') == -1)
{ elementList[i].setShadow(imgNodeList[i], carouselDiv); }
else
{ elementList[i].setShadowIE(imgNodeList[i], carouselDiv); }
}
//Place element and set main timer
rotateElements(elementList, rotationSpeed, radius, xStretch, yStretch);
carouselDiv.setAnimator = function () {
this.animator = setInterval(function () {
rotateElements(elementList, rotationSpeed, radius, xStretch, yStretch);
}, 33);
};
carouselDiv.setAnimator();
setupShowCase(showCaseDiv, carouselDiv, elementList[1].img, 'none', minShowCaseWidth, minShowCaseHeight);
//Everything is setup, show everything
superContainer.style.visibility = 'visible';
}
//Carousel Structures ------------------------------------
function imageElement(src, showCaseDiv, maxWidth, maxHeight, minWidth, minHeight) {
//Image Element Properties ----------------------
this.posIndex = 0;
this.img = src;
this.shadow = null;
this.shadowCtx = null;
this.showCaseDiv = showCaseDiv;
this.img.style.position = 'absolute';
this.img.style.border = '2px solid #bbbbbb';
//For whatever reason, IE needs the image to be re Image wrapped
var imgNode = new Image();
imgNode.src = src.src;
//Scale image within maxWidth and maxHeight
if (imgNode.width > maxWidth) {
var ratio = imgNode.width / imgNode.height;
this.img.width = maxWidth;
this.img.height = maxWidth / ratio; }
if (this.img.height > maxHeight) {
var ratio = this.img.width / this.img.height;
this.img.height = maxHeight;
this.img.width = maxHeight * ratio; }
this.size = new vector(this.img.width, this.img.height);
this.imgRatio = this.size.x / this.size.y;
this.shadowRatio = 0;
//Image Element Methods ----------------------------
this.img.onmousedown = function () {
clearInterval(this.parentNode.animator);
clearInterval(showCaseDiv.opacityTimer);
setupShowCase(showCaseDiv, this.parentNode, this, 'block', maxWidth, minHeight);
//Animate opacity for all except IE
if (navigator.appVersion.indexOf('MSIE') == -1) {
showCaseDiv.animateOpacity(0, 1, 0.1);
}
};
this.setShadow = function (reflectionImg, carouselDiv) {
//Create Canvas element
this.shadow = document.createElement('canvas');
carouselDiv.appendChild(this.shadow);
this.shadowCtx = this.shadow.getContext('2d');
//Setup Canvas attributes
this.shadow.width = reflectionImg.width;
this.shadow.height = reflectionImg.height;
this.shadowRatio = this.shadow.width / this.shadow.height;
this.shadow.style.position = 'absolute';
//Draw reflection image
this.shadowCtx.save();
this.shadowCtx.globalAlpha = 0.3;
this.shadowCtx.scale(1, -1);
this.shadowCtx.translate(0, -reflectionImg.height);
this.shadowCtx.drawImage(reflectionImg, 0, 0, reflectionImg.width, reflectionImg.height);
//Draw reflection gradient
this.shadowCtx.restore();
var reflectionGradient = this.shadowCtx.createLinearGradient(0, 0, 0, reflectionImg.height);
reflectionGradient.addColorStop(0, 'rgba(0, 0, 0, 0)');
reflectionGradient.addColorStop(0.5, 'rgba(0, 0, 0, 1)');
this.shadowCtx.fillStyle = reflectionGradient;
this.shadowCtx.fillRect(0, 0, this.shadow.width, this.shadow.height);
};
this.setShadowIE = function (reflectionImg, carouselDiv) {
//Create Canvas element
this.shadow = document.createElement('img');
this.shadow.src = reflectionImg.src;
carouselDiv.appendChild(this.shadow);
//Setup Canvas attributes
this.shadow.width = reflectionImg.width;
this.shadow.height = reflectionImg.height;
this.shadowRatio = this.shadow.width / this.shadow.height;
this.shadow.style.position = 'absolute';
this.shadow.style.filter = "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)" +
"progid:DXImageTransform.Microsoft.Alpha(opacity=30, finishOpacity=0, style=1, startX=0, startY=0, finishX=0, finishY=50)";
};
this.moveTo = function (x, y) {
this.img.style.left = x + 'px';
this.img.style.top = y + 'px';
if (this.shadow != null) {
this.shadow.style.left = (x + 2) + 'px';
this.shadow.style.top = (y + this.img.height + 12) + 'px';
}
};
this.setZ = function (zIndex) {
this.img.style.zIndex = zIndex;
if (this.shadow != null) {
this.shadow.style.zIndex = zIndex + 1;
}
};
this.scale = function (scale) {
this.img.style.height = this.size.y + scale + 'px';
this.img.style.width = ((this.size.y + scale) * this.imgRatio) + 'px';
if (this.shadow != null) {
this.shadow.style.width = this.img.width + 'px';
this.shadow.style.height = (this.shadow.clientWidth / this.shadowRatio) + 'px';
}
};
}
function rotateElements(elementList, rotationSpeed, radius, xStretch, yStretch) {
for (var i = 0; i < elementList.length; i++) {
var nextX = getCircleCoordAtAngle(elementList[i].posIndex, radius, xStretch, yStretch).x;
var nextY = getCircleCoordAtAngle(elementList[i].posIndex, radius, xStretch, yStretch).y;
if (elementList[i].posIndex + rotationSpeed < 0) { elementList[i].posIndex = 360; }
elementList[i].posIndex = (elementList[i].posIndex + rotationSpeed) % 360;
elementList[i].scale(nextY / 2);
elementList[i].moveTo((nextX + (elementList[i].img.parentNode.clientWidth / 2) - parseInt(elementList[i].img.width) / 2),
(50 + nextY + (elementList[i].img.parentNode.clientHeight / 2) - parseInt(elementList[i].img.height)));
elementList[i].setZ(nextY);
}
}
function setupCarouselDiv(carouselDiv) {
carouselDiv.style.position = 'absolute';
carouselDiv.style.backgroundColor = 'black';
carouselDiv.style.width = carouselDiv.parentNode.style.width;
carouselDiv.style.height = carouselDiv.parentNode.style.height;
carouselDiv.style.zIndex = 1;
carouselDiv.style.marginLeft = "auto";
carouselDiv.style.marginRight = "auto";
}
function setupShowCase(showCaseElement, carouselDiv, imgSrc, display, minWidth, minHeight) {
//Needs non-none display style for image sizing
showCaseElement.style.display = display;
//Setup Showcase Style
var descriptionHeight = 50;
var maxWidth = window.innerWidth;
var maxHeight = window.innerHeight;
//Deal with IE >:(
if (!maxWidth && !maxHeight) {
maxWidth = document.documentElement.clientWidth;
maxHeight = document.documentElement.clientHeight;
}
var showCaseImage = document.createElement('img');
showCaseImage.src = imgSrc.src;
getFileName(imgSrc.src);
var showCaseDescription = document.createElement('p');
showCaseDescription.style.height = descriptionHeight + 'px';
showCaseDescription.innerHTML = searchImageDescriptions(getFileName(imgSrc.src));
if (showCaseElement.getElementsByTagName('img')[0] == undefined) {
showCaseElement.appendChild(showCaseImage);
showCaseElement.appendChild(showCaseDescription);
}
else {
showCaseElement.getElementsByTagName('img')[0].src = showCaseImage.src;
showCaseElement.getElementsByTagName('img')[0].width = showCaseImage.width;
showCaseElement.getElementsByTagName('img')[0].height = showCaseImage.height;
showCaseElement.getElementsByTagName('p')[0].style.height = descriptionHeight + 'px';
showCaseElement.getElementsByTagName('p')[0].innerHTML = showCaseDescription.innerHTML;
}
var ratio = showCaseElement.getElementsByTagName('img')[0].width / showCaseElement.getElementsByTagName('img')[0].height;
//Make sure the image isn't too large
if (showCaseImage.width + 100 > maxWidth || showCaseImage.height + 100 > maxHeight - descriptionHeight) {
if (showCaseImage.width + 100 > maxWidth) {
showCaseElement.getElementsByTagName('img')[0].width = maxWidth - 100;
showCaseElement.getElementsByTagName('img')[0].height = showCaseElement.getElementsByTagName('img')[0].width / ratio;
}
if (showCaseElement.getElementsByTagName('img')[0].height + 100 > maxHeight - descriptionHeight) {
showCaseElement.getElementsByTagName('img')[0].height = maxHeight - 100 - descriptionHeight;
showCaseElement.getElementsByTagName('img')[0].width = showCaseElement.getElementsByTagName('img')[0].height * ratio;
}
}
//Make sure the window isn't too small
if (showCaseElement.getElementsByTagName('img')[0].width < minWidth || showCaseElement.getElementsByTagName('img')[0].height < minHeight) {
if (showCaseElement.getElementsByTagName('img')[0].width < minWidth) {
showCaseElement.getElementsByTagName('img')[0].width = minWidth;
showCaseElement.getElementsByTagName('img')[0].height = minWidth / ratio;
}
if (showCaseElement.getElementsByTagName('img')[0].height < minHeight) {
showCaseElement.getElementsByTagName('img')[0].height = minHeight;
showCaseElement.getElementsByTagName('img')[0].width = minHeight * ratio;
}
}
showCaseElement.style.zIndex = 1000;
showCaseElement.style.position = 'fixed';
showCaseElement.style.top = (maxHeight/2 - showCaseElement.clientHeight/2) + 'px';
showCaseElement.style.left = (maxWidth/2 - showCaseElement.clientWidth/2) + 'px';
showCaseElement.style.padding = '10px';
showCaseElement.style.backgroundColor = '#bbbbbb';
showCaseElement.style.opacity = 0;
showCaseElement.onmousedown = function () {
clearInterval(carouselDiv.animator);
clearInterval(this.opacityTimer);
//Animate opacity for all browsers except IE
if (navigator.appVersion.indexOf('MSIE') == -1) {
showCaseElement.animateOpacity(1, 0, -0.1);
}
else {
carouselDiv.setAnimator();
showCaseElement.style.display = 'none';
}
};
showCaseElement.animateOpacity = function (startOpacity, targetOpacity, itteration) {
this.style.opacity = startOpacity;
this.opacityTimer = setInterval(function () {
showCaseElement.style.opacity = (parseFloat(showCaseElement.style.opacity) + itteration).toFixed(1);
if (parseFloat(showCaseElement.style.opacity) == targetOpacity) {
clearInterval(showCaseElement.opacityTimer);
if (targetOpacity == 0) {
carouselDiv.setAnimator();
showCaseElement.style.display = 'none';
}
}
}, 33);
};
}