/* http://keith-wood.name/imageCube.html Image Cube for jQuery v1.2.1. Written by Keith Wood (kbwood{at}iinet.com.au) June 2008. Dual licensed under the GPL (http://dev.jquery.com/browser/trunk/jquery/GPL-LICENSE.txt) and MIT (http://dev.jquery.com/browser/trunk/jquery/MIT-LICENSE.txt) licenses. Please attribute the author if you use it. */ /* Rotate images (or other things) as if on the faces of a cube. $('div selector').imagecube(); Or with options like: $('div selector').imagecube({direction: 'left', speed: 1000}); */ (function($) { // Hide scope, no $ conflict /* Image cube manager. */ function ImageCube() { this._defaults = { direction: 'random', // Direction of rotation: random|up|down|left|right randomSelection: ['up', 'down', 'left', 'right'], // If direction is random, select one of these speed: 2000, // Time taken (milliseconds) to transition easing: 'linear', // Name of the easing to use during transitions repeat: true, // True to automatically trigger a new transition after a pause pause: 2000, // Time (milliseconds) between transitions selection: 'forward', // How to choose the next item to show: // 'forward', 'backward', 'random' shading: true, // True to add shading effects, false for no effects opacity: 0.8, // Maximum opacity (0.0 - 1.0) for highlights and shadows imagePath: '', // Any extra path to locate the highlight/shadow images full3D: true, // True to add cubic perspective, false for 2D rotation segments: 20, // The number of segments that make up each 3D face reduction: 30, // The amount (pixels) of reduction for far edges of the cube expansion: 10, // The amount (pixels) of expansion for the near edge of the cube lineHeight: [0.0, 1.25], // Hidden and normal line height (em) for text letterSpacing: [-0.4, 0.0], // Hidden and normal letter spacing (em) for text beforeRotate: null, // Callback before rotating afterRotate: null // Callback after rotating }; }; var UP = 0; var DOWN = 1; var LEFT = 2; var RIGHT = 3; var PROP_NAME = 'imageCube'; $.extend(ImageCube.prototype, { /* Class name added to elements to indicate already configured with image cube. */ markerClassName: 'hasImageCube', /* Override the default settings for all image cube instances. @param options (object) the new settings to use as defaults */ setDefaults: function(options) { extendRemove(this._defaults, options || {}); }, /* Attach the image cube functionality to a div. @param target (element) the containing division @param options (object) the settings for this image cube instance (optional) */ _attachImageCube: function(target, options) { target = $(target); if (target.hasClass(this.markerClassName)) { return; } var allOptions = $.extend({_position: target.css('position')}, this._defaults, options || {}); $.data(target[0], PROP_NAME, allOptions); target.addClass(this.markerClassName).css({position: 'relative'}). children().each(function() { var child = $(this); $.data(this, PROP_NAME, {width: child.css('width'), height: child.css('height'), position: child.css('position'), lineHeight: child.css('lineHeight'), letterSpacing: child.css('letterSpacing')}); child.css({width: target.css('width'), height: target.css('height'), position: 'absolute', lineHeight: allOptions.lineHeight[1], letterSpacing: allOptions.letterSpacing[1]}); }).not(':first').hide(); this._prepareRotation(target[0]); }, /* Note current visible child and schedule a repeat rotation (if required). @param target (element) the containing division */ _prepareRotation: function(target) { target = $(target); target.children('.imageCubeShading,.imageCubeFrom,.imageCubeTo').remove(); var options = $.data(target[0], PROP_NAME); options.current = target.children(':visible')[0]; var randomSelection = function(collection) { return (!collection.length ? collection : collection.filter( ':eq(' + Math.floor(Math.random() * collection.length) + ')')); }; options.next = (options.selection == 'random' ? randomSelection(target.children(':hidden')) : (options.selection == 'backward' ? $(options.current).prev() : $(options.current).next())); options.next = (options.next.length ? options.next : (options.selection == 'random' ? options.current : (options.selection == 'backward' ? target.children(':last') : target.children(':first'))))[0]; // Cycle around if at the end if (options.repeat && !options._timer) { options._timer = setTimeout(function() { $.imagecube._rotateImageCube(target[0]); }, options.pause); } $.data(target[0], PROP_NAME, options); }, /* Rotate the image cube to the next face. @param target (element) the containing division @param next (jQuery or element or string or number) next face to show (optional) @param callback (function) a function to call when finished with the rotation (optional) */ _rotateImageCube: function(target, next, callback) { if (typeof next == 'function') { callback = next; next = ''; } target = $(target); this._stopImageCube(target[0], true); var options = $.data(target[0], PROP_NAME); if (next != null) { next = (typeof next == 'number' ? target.children(':eq(' + next + ')') : $(next)); if (target.children().filter(function() { return this === next[0]; }).length > 0) { options.next = next; } } var callbackArgs = [options.current, options.next]; if (options.beforeRotate) { options.beforeRotate.apply(target[0], callbackArgs); } var animTo = {}; animTo[PROP_NAME] = 1.0; target.attr(PROP_NAME, 0.0).animate(animTo, options.speed, options.easing, function() { if (options.afterRotate) { options.afterRotate.apply(target[0], callbackArgs); } if (callback) { callback.apply(target[0]); } }); }, /* Retrieve the currently visible child of an image cube div. @param target (element) the containing division @return (element) the currently displayed child of target division */ _currentImageCube: function(target) { return ($(target).hasClass(this.markerClassName) ? $.data(target, PROP_NAME).current : null); }, /* Retrieve the next visible child of an image cube div. @param target (element) the containing division @return (element) the next to be displayed child of target division */ _nextImageCube: function(target) { return ($(target).hasClass(this.markerClassName) ? $.data(target, PROP_NAME).next : null); }, /* Stop the image cube automatically rotating to the next face. @param target (element) the containing division @param timerOnly (boolean) true if only temporarily stopping (optional) */ _stopImageCube: function(target, timerOnly) { var options = $.data(target, PROP_NAME); if (options._timer) { clearTimeout(options._timer); options._timer = null; } if (!timerOnly) { options.repeat = false; } $.data(target, PROP_NAME, options); }, /* Start the image cube automatically rotating to the next face. @param target (element) the containing division */ _startImageCube: function(target) { this._changeImageCube(target, {repeat: true}); }, /* Reconfigure the settings for an image cube div. @param target (element) the containing division @param options (object) the new settings for this image cube instance or (string) the name of the setting @param value (any, optional) the value of the setting */ _changeImageCube: function(target, options, value) { if (typeof options == 'string') { var opts = {}; opts[options] = value; options = opts; } var curOptions = $.data(target, PROP_NAME); extendRemove(curOptions || {}, options || {}); $.data(target, PROP_NAME, curOptions); this._prepareRotation(target); }, /* Remove the image cube functionality from a div. @param target (element) the containing division */ _destroyImageCube: function(target) { target = $(target); if (!target.hasClass(this.markerClassName)) { return; } this._stopImageCube(target[0]); var options = $.data(target[0], PROP_NAME); target.stop().css({position: options._position}). removeClass(this.markerClassName). children('.imageCubeShading,.imageCubeFrom,.imageCubeTo').remove(); target.children().each(function() { $(this).css($.data(this, PROP_NAME)).show(); $.removeData(this, PROP_NAME); }); $.removeData(target[0], PROP_NAME); }, /* Prepare the image cube for animation. @param target (element) the containing division */ _prepareAnimation: function(target) { var options = $.data(target, PROP_NAME); var target = $(target); var offset = {left: 0, top: 0}; target.parents().each(function() { // Check if this area is fixed var $this = $(this); if ($this.css('position') == 'fixed') { offset.left -= $this.offset().left; offset.top -= $this.offset().top; return false; } }); var dims = {width: target.width(), height: target.height()}; var direction = (options.direction != 'random' ? options.direction : options.randomSelection[Math.floor(Math.random() * options.randomSelection.length)]); direction = Math.max(0, $.inArray(direction, ['up', 'down', 'left', 'right'])); options._curDirection = direction; var upDown = (direction == UP || direction == DOWN); var leftRight = (direction == LEFT || direction == RIGHT); var upLeft = (direction == UP || direction == LEFT); var firstOpacity = (upLeft ? 0 : options.opacity); var pFrom = $(options.current); var pTo = $(options.next); // Calculate borders and padding for both elements var border = []; var parseBorders = function(p) { var b = [0, 0, 0, 0]; if (!$.browser.msie || p.css('border')) { for (var i = 0; i < 4; i++) { b[i] = p.css('border' + ['Left', 'Right', 'Top', 'Bottom'][i] + 'Width'); var extra = ($.browser.msie ? 1 : 0); b[i] = parseFloat( {thin: 1 + extra, medium: 3 + extra, thick: 5 + extra}[b[i]] || b[i]); } } return b; }; border[0] = parseBorders(pFrom); border[1] = parseBorders(pTo); var pad = []; pad[0] = [parseFloat(pFrom.css('padding-left')), parseFloat(pFrom.css('padding-right')), parseFloat(pFrom.css('padding-top')), parseFloat(pFrom.css('padding-bottom'))]; pad[1] = [parseFloat(pTo.css('padding-left')), parseFloat(pTo.css('padding-right')), parseFloat(pTo.css('padding-top')), parseFloat(pTo.css('padding-bottom'))]; var extras = []; extras[0] = [($.boxModel ? border[0][0] + border[0][1] + pad[0][0] + pad[0][1] : 0), ($.boxModel ? border[0][2] + border[0][3] + pad[0][2] + pad[0][3] : 0)]; extras[1] = [($.boxModel ? border[1][0] + border[1][1] + pad[1][0] + pad[1][1] : 0), ($.boxModel ? border[1][2] + border[1][3] + pad[1][2] + pad[1][3] : 0)]; // Define the property ranges per element var stepProps = []; stepProps[0] = {elem: pFrom[0], // Currently displayed element props: {left: {start: offset.left, end: offset.left + (direction == RIGHT ? dims.width : 0), units: 'px'}, width: {start: dims.width - extras[0][0], end: (upDown ? dims.width - extras[0][0] : 0), units: 'px'}, top: {start: offset.top, end: offset.top + (direction == DOWN ? dims.height : 0), units: 'px'}, height: {start: dims.height - extras[0][1], end: (upDown ? 0 : dims.height - extras[0][1]), units: 'px'}, paddingLeft: {start: pad[0][0], end: (leftRight ? 0 : pad[0][0]), units: 'px'}, paddingRight: {start: pad[0][1], end: (leftRight ? 0 : pad[0][1]), units: 'px'}, paddingTop: {start: pad[0][2], end: (upDown ? 0 : pad[0][2]), units: 'px'}, paddingBottom: {start: pad[0][3], end: (upDown ? 0 : pad[0][3]), units: 'px'}, borderLeftWidth: {start: border[0][0], end: (leftRight ? 0 : border[0][0]), units: 'px'}, borderRightWidth: {start: border[0][1], end: (leftRight ? 0 : border[0][1]), units: 'px'}, borderTopWidth: {start: border[0][2], end: (upDown ? 0 : border[0][2]), units: 'px'}, borderBottomWidth: {start: border[0][3], end: (upDown ? 0 : border[0][3]), units: 'px'}, lineHeight: {start: options.lineHeight[1], end: (upDown ? options.lineHeight[0] : options.lineHeight[1]), units: 'em'}, letterSpacing: {start: options.letterSpacing[1], end: (upDown ? options.letterSpacing[1] : options.letterSpacing[0]), units: 'em'}}}; stepProps[1] = {elem: pTo[0], // New element to be displayed props: {left: {start: offset.left + (direction == LEFT ? dims.width : 0), end: offset.left, units: 'px'}, width: {start: (upDown ? dims.width - extras[1][0] : 0), end: dims.width - extras[1][0], units: 'px'}, top: {start: offset.top + (direction == UP ? dims.height : 0), end: offset.top, units: 'px'}, height: {start: (upDown ? ($.browser.msie ? 1 : 0) : dims.height - extras[1][1]), end : dims.height - extras[1][1], units: 'px'}, paddingLeft: {start: (leftRight ? 0 : pad[1][0]), end: pad[1][0], units: 'px'}, paddingRight: {start: (leftRight ? 0 : pad[1][1]), end: pad[1][1], units: 'px'}, paddingTop: {start: (upDown ? 0 : pad[1][2]), end: pad[1][2], units: 'px'}, paddingBottom: {start: (upDown ? 0 : pad[1][3]), end: pad[1][3], units: 'px'}, borderLeftWidth: {start: (leftRight ? 0 : border[1][0]), end: border[1][0], units: 'px'}, borderRightWidth: {start: (leftRight ? 0 : border[1][1]), end: border[1][1], units: 'px'}, borderTopWidth: {start: (upDown ? 0 : border[1][2]), end: border[1][2], units: 'px'}, borderBottomWidth: {start: (upDown ? 0 : border[1][3]), end: border[1][3], units: 'px'}, lineHeight: {start: (upDown ? options.lineHeight[0] : options.lineHeight[1]), end: options.lineHeight[1], units: 'em'}, letterSpacing: {start: (upDown ? options.letterSpacing[1] : options.letterSpacing[0]), end: options.letterSpacing[1], units: 'em'}}}; if (options.shading) { // Initialise highlight and shadow objects (or colours on IE) var setHighShad = function(props, startOpacity, endOpacity) { return {left: {start: props.left.start, end: props.left.end, units: 'px'}, width: {start: props.width.start, end: props.width.end, units: 'px'}, top: {start: props.top.start, end: props.top.end, units: 'px'}, height: {start: props.height.start, end: props.height.end, units: 'px'}, paddingLeft: {start: props.paddingLeft.start + props.borderLeftWidth.start, end: props.paddingLeft.end + props.borderLeftWidth.end, units: 'px'}, paddingRight: {start: props.paddingRight.start + props.borderRightWidth.start, end: props.paddingRight.end + props.borderRightWidth.end, units: 'px'}, paddingTop: {start: props.paddingTop.start + props.borderTopWidth.start, end: props.paddingTop.end + props.borderTopWidth.end, units: 'px'}, paddingBottom: {start: props.paddingBottom.start + props.borderBottomWidth.start, end: props.paddingBottom.end + props.borderBottomWidth.end, units: 'px'}, opacity: {start: startOpacity, end: endOpacity, units: ''}}; }; stepProps[2] = {elem: // Highlight shading (up/left) $(($.browser.msie ? '' : '>'))[0], props: setHighShad(stepProps[upLeft ? 0 : 1].props, firstOpacity, options.opacity - firstOpacity)}; stepProps[3] = {elem: // Shadow shading (down/right) $(($.browser.msie ? '' : '>'))[0], props: setHighShad(stepProps[upLeft ? 1 : 0].props, options.opacity - firstOpacity, firstOpacity)}; } // Set up full 3D rotation if (options.full3D) { for (var i = 0; i < options.segments; i++) { target.append(pFrom.clone().addClass('imageCubeFrom'). css({display: 'block', position: 'absolute', overflow: 'hidden'})); if (options.shading) { target.append($(stepProps[upLeft ? 2 : 3].elem).clone()); } } for (var i = 0; i < options.segments; i++) { target.append(pTo.clone().addClass('imageCubeTo'). css({display: 'block', position: 'absolute', width: 0, overflow: 'hidden'})); if (options.shading) { target.append($(stepProps[upLeft ? 3 : 2].elem).clone()); } } pFrom.hide(); pTo.css({width: dims.width - extras[1][0], height: dims.height - extras[1][1]}); } else { // Initialise from and to objects var initCSS = function(props) { return {left: props.left.start + 'px', width: props.width.start + 'px', top: props.top.start + 'px', height: props.height.start + 'px', lineHeight: props.lineHeight.start + 'em', padding: props.paddingTop.start + 'px ' + props.paddingRight.start + 'px ' + props.paddingBottom.start + 'px ' + props.paddingLeft.start + 'px', borderLeftWidth: props.borderLeftWidth.start + 'px', borderRightWidth: props.borderRightWidth.start + 'px', borderTopWidth: props.borderTopWidth.start + 'px', borderBottomWidth: props.borderBottomWidth.start + 'px', letterSpacing: props.letterSpacing.start + 'em', overflow: 'hidden'}; }; pFrom.css(initCSS(stepProps[0].props)); pTo.css(initCSS(stepProps[1].props)).show(); if (options.shading) { target.append(stepProps[2].elem).append(stepProps[3].elem); } } // Pre-compute differences for (var i = 0; i < stepProps.length; i++) { for (var name in stepProps[i].props) { var prop = stepProps[i].props[name]; prop.diff = prop.end - prop.start; } } return stepProps; }, /* Draw one panel of the 3D perspective view of the cube. @param target (element) the container @param pos (number) the current position (0.0 - 1.0) @param stepProps (object[]) details about the items being animated @return (boolean) true if drawn in 3D, false if not */ _drawFull3D: function(target, pos, stepProps) { var options = $.data(target, PROP_NAME); if (!options.full3D) { return false; } var target = $(target); var direction = options._curDirection; var upDown = (direction == UP || direction == DOWN); var upLeft = (direction == UP || direction == LEFT); var width = target.width(); var height = target.height(); if (width == 0 || height == 0) { return true; } var current = (1 - pos) * (upDown ? height : width); var segments = options.segments; var maxExpand = options.expansion * (1 - Math.abs(2 * current - (upDown ? height : width)) / (upDown ? height : width)); var maxReduce = options.reduction - (options.reduction * current / (upDown ? height : width)); var update = function(className, al, at, bl, bt, cl, ct, dl, dt, opacity, props, attr) { var ws = [bl - al, cl - dl]; var w = Math.max(ws[0], ws[1]); var hs = [dt - at, ct - bt]; var h = Math.max(hs[0], hs[1]); var wStep = (upDown ? (ws[0] - ws[1]) / (segments - 1) / 2 : w / segments); var hStep = (upDown ? h / segments : (hs[0] - hs[1]) / (segments - 1) / 2); var pbw = props.paddingLeft[attr] + props.paddingRight[attr] + props.borderLeftWidth[attr] + props.borderRightWidth[attr]; var pbh = props.paddingTop[attr] + props.paddingBottom[attr] + props.borderTopWidth[attr] + props.borderBottomWidth[attr]; var ral = Math.round(al); var rat = Math.round(at); var thisLeft = ral; var thisTop = rat; var i = 0; for (var j = 0; j < target[0].childNodes.length; j++) { var child = target[0].childNodes[j]; if (child.className != className) { continue; } var nextLeft = Math.round(al + (i + 1) * wStep); var nextTop = Math.round(at + (i + 1) * hStep); var wCur = ws[0] - (upDown ? 2 * i * wStep : 0); var hCur = hs[0] - (upDown ? 0 : 2 * i * hStep); child.style.left = (upDown ? thisLeft : al) + 'px'; child.style.top = (upDown ? at : thisTop) + 'px'; child.style.width = Math.max(0, wCur - pbw) + 'px'; child.style.height = Math.max(0, hCur - pbh) + 'px'; child.style.letterSpacing = (upDown ? wCur / w * (options.letterSpacing[1] - options.letterSpacing[0]) + options.letterSpacing[0] : pos * props.letterSpacing.diff + props.letterSpacing.start) + props.letterSpacing.units; child.style.lineHeight = (!upDown ? hCur / h * (options.lineHeight[1] - options.lineHeight[0]) + options.lineHeight[0] : pos * props.lineHeight.diff + props.lineHeight.start) + props.lineHeight.units; child.style.clip = 'rect(' + (!upDown ? 'auto' : (thisTop - rat) + 'px') + ',' + (upDown ? 'auto' : (nextLeft - ral) + 'px') + ',' + (!upDown ? 'auto' : (nextTop - rat) + 'px') + ',' + (upDown ? 'auto' : (thisLeft - ral) + 'px') + ')'; if (options.shading) { var shading = child.nextSibling; shading.style.left = thisLeft + 'px'; shading.style.top = thisTop + 'px'; shading.style.width = (upDown ? ws[0] - 2 * i * wStep : nextLeft - thisLeft) + 'px'; shading.style.height = (upDown ? nextTop - thisTop : hs[0] - 2 * i * hStep) + 'px'; shading.style.opacity = opacity; if ($.browser.msie) { shading.style.filter = 'alpha(opacity=' + (opacity * 100) + ')'; } } thisLeft = nextLeft; thisTop = nextTop; i++; } }; update('imageCubeFrom', [maxReduce, -maxExpand, 0, width - current][direction], // top left [0, height - current, maxReduce, -maxExpand][direction], [width - maxReduce, width + maxExpand, current, width][direction], // top right [0, height - current, -maxExpand, maxReduce][direction], [width + maxExpand, width - maxReduce, current, width][direction], // bottom right [current, height, height + maxExpand, height - maxReduce][direction], [-maxExpand, maxReduce, 0, width - current][direction], // bottom left [current, height, height - maxReduce, height + maxExpand][direction], (!options.shading ? 0 : (upLeft ? pos : 1 - pos) * stepProps[2].props.opacity.diff + stepProps[2].props.opacity.start), stepProps[0].props, 'start'); update('imageCubeTo', [-maxExpand, options.reduction - maxReduce, current, 0][direction], // top left [current, 0, -maxExpand, options.reduction - maxReduce][direction], [width + maxExpand, width - (options.reduction - maxReduce), width, width - current][direction], // top right [current, 0, options.reduction - maxReduce, -maxExpand][direction], [width - (options.reduction - maxReduce), width + maxExpand, width, width - current][direction], // bottom right [height, height - current, height - (options.reduction - maxReduce), height + maxExpand][direction], [options.reduction - maxReduce, -maxExpand, current, 0][direction], // bottom left [height, height - current, height + maxExpand, height - (options.reduction - maxReduce)][direction], (!options.shading ? 0 : (upLeft ? pos : 1 - pos) * stepProps[3].props.opacity.diff + stepProps[3].props.opacity.start), stepProps[1].props, 'end'); return true; } }); /* jQuery extend now ignores nulls! @param target (object) the object to extend @param props (object) the attributes to modify @return (object) the updated target */ function extendRemove(target, props) { $.extend(target, props); for (var name in props) { if (props[name] == null) { target[name] = null; } } return target; } /* Attach the image cube functionality to a jQuery selection. @param command (string) the command to run (optional, default 'attach') @param options (object) the new settings to use for these image cube instances @return (jQuery) for chaining further calls */ $.fn.imagecube = function(options) { var otherArgs = Array.prototype.slice.call(arguments, 1); if (options == 'current' || options == 'next') { return $.imagecube['_' + options + 'ImageCube']. apply($.imagecube, [this[0]].concat(otherArgs)); } return this.each(function() { if (typeof options == 'string') { $.imagecube['_' + options + 'ImageCube']. apply($.imagecube, [this].concat(otherArgs)); } else { $.imagecube._attachImageCube(this, options); } }); }; /* Enable synchronised animation for all of the image cube properties. @param fx (object) the effects instance to animate */ $.fx.step[PROP_NAME] = function(fx) { if (fx.state == 0 || !fx.stepProps) { // Initialisation fx.start = 0.0; fx.end = 1.0; fx.stepProps = $.imagecube._prepareAnimation(fx.elem); var elem = fx.stepProps[0].elem; fx.saveCSS = {borderLeftWidth: elem.style.borderLeftWidth, borderRightWidth: elem.style.borderRightWidth, borderTopWidth: elem.style.borderTopWidth, borderBottomWidth: elem.style.borderBottomWidth, padding: elem.style.padding}; } if (!$.imagecube._drawFull3D(fx.elem, fx.pos, fx.stepProps)) { for (var i = 0; i < fx.stepProps.length; i++) { // Update all elements var comp = fx.stepProps[i]; for (var name in comp.props) { // Update all properties var prop = comp.props[name]; comp.elem.style[name] = (fx.pos * prop.diff + prop.start) + prop.units; if ($.browser.msie && name == 'opacity') { comp.elem.style.filter = 'alpha(opacity=' + ((fx.pos * prop.diff + prop.start) * 100) + ')'; } } } } if (fx.state == 1) { // Tidy up afterwards $(fx.stepProps[0].elem).hide().css(fx.saveCSS); $(fx.stepProps[1].elem).show(); $.imagecube._prepareRotation(fx.elem); } }; /* Initialise the image cube functionality. */ $.imagecube = new ImageCube(); // singleton instance })(jQuery);