Carousel.js

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);
    };
}