/** * impress.js * * impress.js is a presentation tool based on the power of CSS3 transforms and transitions * in modern browsers and inspired by the idea behind prezi.com. * * * Copyright 2011-2012 Bartek Szopka (@bartaz) * * Released under the MIT and GPL Licenses. * * ------------------------------------------------ * author: Bartek Szopka * version: 0.5dev (currently in development) * url: http://bartaz.github.com/impress.js/ * source: http://github.com/bartaz/impress.js/ */ /*jshint bitwise:true, curly:true, eqeqeq:true, forin:true, latedef:true, newcap:true, noarg:true, noempty:true, undef:true, strict:true, browser:true */ (function ( document, window ) { 'use strict'; // HELPER FUNCTIONS var pfx = (function () { var style = document.createElement('dummy').style, prefixes = 'Webkit Moz O ms Khtml'.split(' '), memory = {}; return function ( prop ) { if ( typeof memory[ prop ] === "undefined" ) { var ucProp = prop.charAt(0).toUpperCase() + prop.substr(1), props = (prop + ' ' + prefixes.join(ucProp + ' ') + ucProp).split(' '); memory[ prop ] = null; for ( var i in props ) { if ( style[ props[i] ] !== undefined ) { memory[ prop ] = props[i]; break; } } } return memory[ prop ]; }; })(); var arrayify = function ( a ) { return [].slice.call( a ); }; var css = function ( el, props ) { var key, pkey; for ( key in props ) { if ( props.hasOwnProperty(key) ) { pkey = pfx(key); if ( pkey !== null ) { el.style[pkey] = props[key]; } } } return el; }; var toNumber = function (numeric, fallback) { return isNaN(numeric) ? (fallback || 0) : Number(numeric); }; var byId = function ( id ) { return document.getElementById(id); }; var $ = function ( selector, context ) { context = context || document; return context.querySelector(selector); }; var $$ = function ( selector, context ) { context = context || document; return arrayify( context.querySelectorAll(selector) ); }; var translate = function ( t ) { return " translate3d(" + t.x + "px," + t.y + "px," + t.z + "px) "; }; var rotate = function ( r, revert ) { var rX = " rotateX(" + r.x + "deg) ", rY = " rotateY(" + r.y + "deg) ", rZ = " rotateZ(" + r.z + "deg) "; return revert ? rZ+rY+rX : rX+rY+rZ; }; var scale = function ( s ) { return " scale(" + s + ") "; }; var perspective = function ( p ) { return " perspective(" + p + "px) "; }; var getElementFromUrl = function () { // get id from url # by removing `#` or `#/` from the beginning, // so both "fallback" `#slide-id` and "enhanced" `#/slide-id` will work return byId( window.location.hash.replace(/^#\/?/,"") ); }; var computeWindowScale = function ( config ) { var hScale = window.innerHeight / config.height, wScale = window.innerWidth / config.width, scale = hScale > wScale ? wScale : hScale; if (config.maxScale && scale > config.maxScale) { scale = config.maxScale; } if (config.minScale && scale < config.minScale) { scale = config.minScale; } return scale; }; // CHECK SUPPORT var body = document.body; var ua = navigator.userAgent.toLowerCase(); var impressSupported = ( pfx("perspective") !== null ) && ( body.classList ) && ( body.dataset ) && ( ua.search(/(iphone)|(ipod)|(android)/) === -1 ); if (!impressSupported) { // we can't be sure that `classList` is supported body.className += " impress-not-supported "; return; } else { body.classList.remove("impress-not-supported"); body.classList.add("impress-supported"); } // cross-browser transitionEnd event name // based on https://developer.mozilla.org/en/CSS/CSS_transitions var transitionEnd = ({ 'transition':'transitionEnd', 'OTransition':'oTransitionEnd', 'msTransition':'MSTransitionEnd', // who knows how it will end up? 'MozTransition':'transitionend', 'WebkitTransition':'webkitTransitionEnd' })[pfx("transition")]; var roots = {}; var defaults = { width: 1024, height: 768, maxScale: 1, minScale: 0, perspective: 1000, transitionDuration: 1000 }; var impress = window.impress = function ( rootId ) { rootId = rootId || "impress"; // if already initialized just return the API if (roots["impress-root-" + rootId]) { return roots["impress-root-" + rootId]; } // DOM ELEMENTS var root = byId( rootId ); // viewport updates for iPad var meta = $("meta[name='viewport']") || document.createElement("meta"); // hardcoding these values looks pretty bad, as they kind of depend on the content // so they should be at least configurable meta.content = "width=device-width, minimum-scale=1, maximum-scale=1, user-scalable=no"; if (meta.parentNode !== document.head) { meta.name = 'viewport'; document.head.appendChild(meta); } // initialize configuration object var rootData = root.dataset; var config = { width: toNumber(rootData.width, defaults.width), height: toNumber(rootData.height, defaults.height), maxScale: toNumber(rootData.maxScale, defaults.maxScale), minScale: toNumber(rootData.minScale, defaults.minScale), perspective: toNumber(rootData.perspective, defaults.perspective), transitionDuration: toNumber(rootData.transitionDuration, defaults.transitionDuration) }; var canvas = document.createElement("div"); canvas.className = "canvas"; arrayify( root.childNodes ).forEach(function ( el ) { canvas.appendChild( el ); }); root.appendChild(canvas); var steps = $$(".step", root); var windowScale = computeWindowScale( config ); // SETUP // set initial values and defaults document.documentElement.style.height = "100%"; css(body, { height: "100%", overflow: "hidden" }); var props = { position: "absolute", transformOrigin: "top left", transition: "all 0s ease-in-out", transformStyle: "preserve-3d" }; css(root, props); css(root, { top: "50%", left: "50%", transform: perspective( config.perspective/windowScale ) + scale( windowScale ) }); css(canvas, props); var current = { translate: { x: 0, y: 0, z: 0 }, rotate: { x: 0, y: 0, z: 0 }, scale: 1 }; var stepData = {}; var isStep = function ( el ) { return !!(el && el.id && stepData["impress-" + el.id]); }; steps.forEach(function ( el, idx ) { var data = el.dataset, step = { translate: { x: toNumber(data.x), y: toNumber(data.y), z: toNumber(data.z) }, rotate: { x: toNumber(data.rotateX), y: toNumber(data.rotateY), z: toNumber(data.rotateZ || data.rotate) }, scale: toNumber(data.scale, 1), el: el }; if ( !el.id ) { el.id = "step-" + (idx + 1); } stepData["impress-" + el.id] = step; css(el, { position: "absolute", transform: "translate(-50%,-50%)" + translate(step.translate) + rotate(step.rotate) + scale(step.scale), transformStyle: "preserve-3d" }); }); var active = null; // step events var triggerEvent = function (el, eventName) { var event = document.createEvent("CustomEvent"); event.initCustomEvent(eventName, true, true); el.dispatchEvent(event); }; var lastEntered = null; var onStepEnter = function (step) { if (lastEntered !== step) { triggerEvent(step, "impressStepEnter"); lastEntered = step; } }; var onStepLeave = function (step) { if (lastEntered === step) { triggerEvent(step, "impressStepLeave"); } }; // transitionEnd event handler var expectedTransitionTarget = null; var onTransitionEnd = function (event) { // we only care about transitions on `root` and `canvas` elements if (event.target === expectedTransitionTarget) { onStepEnter(active); event.stopPropagation(); // prevent propagation from `canvas` to `root` } }; root.addEventListener(transitionEnd, onTransitionEnd, false); canvas.addEventListener(transitionEnd, onTransitionEnd, false); var stepTo = function ( el, force ) { if ( !isStep(el) || (el === active && !force) ) { // selected element is not defined as step or is already active return false; } // Sometimes it's possible to trigger focus on first link with some keyboard action. // Browser in such a case tries to scroll the page to make this element visible // (even that body overflow is set to hidden) and it breaks our careful positioning. // // So, as a lousy (and lazy) workaround we will make the page scroll back to the top // whenever slide is selected // // If you are reading this and know any better way to handle it, I'll be glad to hear about it! window.scrollTo(0, 0); var step = stepData["impress-" + el.id]; if ( active ) { active.classList.remove("active"); body.classList.remove("impress-on-" + active.id); } el.classList.add("active"); body.classList.add("impress-on-" + el.id); var target = { rotate: { x: -step.rotate.x, y: -step.rotate.y, z: -step.rotate.z }, translate: { x: -step.translate.x, y: -step.translate.y, z: -step.translate.z }, scale: 1 / step.scale }; // check if the transition is zooming in or not var zoomin = target.scale >= current.scale; // if presentation starts (nothing is active yet) // don't animate (set duration to 0) var duration = (active) ? config.transitionDuration : 0, delay = (config.transitionDuration / 2); if (force) { windowScale = computeWindowScale(); } var targetScale = target.scale * windowScale; expectedTransitionTarget = target.scale > current.scale ? root : canvas; if (active) { onStepLeave(active); } css(root, { // to keep the perspective look similar for different scales // we need to 'scale' the perspective, too transform: perspective( config.perspective / targetScale ) + scale( targetScale ), transitionDuration: duration + "ms", transitionDelay: (zoomin ? delay : 0) + "ms" }); css(canvas, { transform: rotate(target.rotate, true) + translate(target.translate), transitionDuration: duration + "ms", transitionDelay: (zoomin ? 0 : delay) + "ms" }); current = target; active = el; if (duration === 0) { onStepEnter(active); } return el; }; var prev = function () { var prev = steps.indexOf( active ) - 1; prev = prev >= 0 ? steps[ prev ] : steps[ steps.length-1 ]; return stepTo(prev); }; var next = function () { var next = steps.indexOf( active ) + 1; next = next < steps.length ? steps[ next ] : steps[ 0 ]; return stepTo(next); }; // STEP CLASSES steps.forEach(function (step) { step.classList.add("future"); }); root.addEventListener("impressStepEnter", function (event) { event.target.classList.remove("past"); event.target.classList.remove("future"); event.target.classList.add("present"); }, false); root.addEventListener("impressStepLeave", function (event) { event.target.classList.remove("present"); event.target.classList.add("past"); }, false); // HASH CHANGE // `#/step-id` is used instead of `#step-id` to prevent default browser // scrolling to element in hash // // and it has to be set after animation finishes, because in Chrome it // causes transtion being laggy // BUG: http://code.google.com/p/chromium/issues/detail?id=62820 root.addEventListener("impressStepEnter", function (event) { window.location.hash = "#/" + event.target.id; }, false); window.addEventListener("hashchange", function () { stepTo( getElementFromUrl() ); }, false); window.addEventListener("orientationchange", function () { window.scrollTo(0, 0); }, false); // START // by selecting step defined in url or first step of the presentation stepTo(getElementFromUrl() || steps[0]); return (roots[ "impress-root-" + rootId ] = { stepTo: stepTo, next: next, prev: prev }); }; })(document, window); // EVENTS (function ( document, window ) { 'use strict'; var impress = window.impress; // throttling function calls, by Remy Sharp // http://remysharp.com/2010/07/21/throttling-function-calls/ var throttle = function (fn, delay) { var timer = null; return function () { var context = this, args = arguments; clearTimeout(timer); timer = setTimeout(function () { fn.apply(context, args); }, delay); }; }; // keyboard navigation handlers // prevent default keydown action when one of supported key is pressed document.addEventListener("keydown", function ( event ) { if ( event.keyCode === 9 || ( event.keyCode >= 32 && event.keyCode <= 34 ) || (event.keyCode >= 37 && event.keyCode <= 40) ) { event.preventDefault(); } }, false); // trigger impress action on keyup document.addEventListener("keyup", function ( event ) { if ( event.keyCode === 9 || ( event.keyCode >= 32 && event.keyCode <= 34 ) || (event.keyCode >= 37 && event.keyCode <= 40) ) { switch( event.keyCode ) { case 33: // pg up case 37: // left case 38: // up impress().prev(); break; case 9: // tab case 32: // space case 34: // pg down case 39: // right case 40: // down impress().next(); break; } event.preventDefault(); } }, false); // delegated handler for clicking on the links to presentation steps document.addEventListener("click", function ( event ) { // event delegation with "bubbling" // check if event target (or any of its parents is a link) var target = event.target; while ( (target.tagName !== "A") && (target !== document.documentElement) ) { target = target.parentNode; } if ( target.tagName === "A" ) { var href = target.getAttribute("href"); // if it's a link to presentation step, target this step if ( href && href[0] === '#' ) { target = document.getElementById( href.slice(1) ); } } if ( impress().stepTo(target) ) { event.stopImmediatePropagation(); event.preventDefault(); } }, false); // delegated handler for clicking on step elements document.addEventListener("click", function ( event ) { var target = event.target; // find closest step element while ( !target.classList.contains("step") && (target !== document.documentElement) ) { target = target.parentNode; } if ( impress().stepTo(target) ) { event.preventDefault(); } }, false); // touch handler to detect taps on the left and right side of the screen document.addEventListener("touchstart", function ( event ) { if (event.touches.length === 1) { var x = event.touches[0].clientX, width = window.innerWidth * 0.3, result = null; if ( x < width ) { result = impress().prev(); } else if ( x > window.innerWidth - width ) { result = impress().next(); } if (result) { event.preventDefault(); } } }, false); // rescale presentation when window is resized window.addEventListener("resize", throttle(function () { // force going to active step again, to trigger rescaling impress().stepTo( document.querySelector(".active"), true ); }, 250), false); })(document, window);