| /** |
| * 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.5.2 (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 triggerEvent = function (el, eventName, detail) { |
| var event = document.createEvent("CustomEvent"); |
| event.initCustomEvent(eventName, true, true, detail); |
| el.dispatchEvent(event); |
| }; |
| |
| 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 "; |
| } 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 empty = function () { return false; }; |
| |
| var impress = window.impress = function ( rootId ) { |
| |
| // if impress.js is not supported by the browser return a dummy API |
| // it may not be a perfect solution but we return early and avoid |
| // running code that may use features not implemented in the browser |
| if (!impressSupported) { |
| return { |
| init: empty, |
| goto: empty, |
| prev: empty, |
| next: empty |
| }; |
| } |
| |
| rootId = rootId || "impress"; |
| |
| // if already initialized just return the API |
| if (roots["impress-root-" + rootId]) { |
| return roots["impress-root-" + rootId]; |
| } |
| |
| // data of all presentation steps |
| var stepsData = {}; |
| |
| // element of currently active step |
| var activeStep = null; |
| |
| // current state (position, rotation and scale) of the presentation |
| var currentState = null; |
| |
| // array of step elements |
| var steps = null; |
| |
| // configuration options |
| var config = null; |
| |
| // scale factor of the browser window |
| var windowScale = null; |
| |
| // root presentation elements |
| var root = byId( rootId ); |
| var canvas = document.createElement("div"); |
| |
| var initialized = false; |
| |
| // step events |
| |
| var lastEntered = null; |
| var onStepEnter = function (step) { |
| if (lastEntered !== step) { |
| triggerEvent(step, "impress:stepenter"); |
| lastEntered = step; |
| } |
| }; |
| |
| var onStepLeave = function (step) { |
| if (lastEntered === step) { |
| triggerEvent(step, "impress:stepleave"); |
| lastEntered = null; |
| } |
| }; |
| |
| // 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(activeStep); |
| event.stopPropagation(); // prevent propagation from `canvas` to `root` |
| } |
| }; |
| |
| var initStep = 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); |
| } |
| |
| stepsData["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 init = function () { |
| if (initialized) { return; } |
| |
| // setup viewport for mobile devices |
| var meta = $("meta[name='viewport']") || document.createElement("meta"); |
| 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; |
| 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 ) |
| }; |
| |
| windowScale = computeWindowScale( config ); |
| |
| // wrap steps with "canvas" element |
| arrayify( root.childNodes ).forEach(function ( el ) { |
| canvas.appendChild( el ); |
| }); |
| root.appendChild(canvas); |
| |
| // set initial styles |
| document.documentElement.style.height = "100%"; |
| |
| css(body, { |
| height: "100%", |
| overflow: "hidden" |
| }); |
| |
| var rootStyles = { |
| position: "absolute", |
| transformOrigin: "top left", |
| transition: "all 0s ease-in-out", |
| transformStyle: "preserve-3d" |
| }; |
| |
| css(root, rootStyles); |
| css(root, { |
| top: "50%", |
| left: "50%", |
| transform: perspective( config.perspective/windowScale ) + scale( windowScale ) |
| }); |
| css(canvas, rootStyles); |
| |
| root.addEventListener(transitionEnd, onTransitionEnd, false); |
| canvas.addEventListener(transitionEnd, onTransitionEnd, false); |
| |
| body.classList.remove("impress-disabled"); |
| body.classList.add("impress-enabled"); |
| |
| // get and init steps |
| steps = $$(".step", root); |
| steps.forEach( initStep ); |
| |
| currentState = { |
| translate: { x: 0, y: 0, z: 0 }, |
| rotate: { x: 0, y: 0, z: 0 }, |
| scale: 1 |
| }; |
| |
| initialized = true; |
| |
| triggerEvent(root, "impress:init", { api: roots[ "impress-root-" + rootId ] }); |
| }; |
| |
| var getStep = function ( step ) { |
| if (typeof step === "number") { |
| step = step < 0 ? steps[ steps.length + step] : steps[ step ]; |
| } else if (typeof step === "string") { |
| step = byId(step); |
| } |
| return (step && step.id && stepsData["impress-" + step.id]) ? step : null; |
| }; |
| |
| var goto = function ( el, duration ) { |
| |
| if ( !initialized || !(el = getStep(el)) ) { |
| // presentation not initialized or given element is not a step |
| 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 = stepsData["impress-" + el.id]; |
| |
| if ( activeStep ) { |
| activeStep.classList.remove("active"); |
| body.classList.remove("impress-on-" + activeStep.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 >= currentState.scale; |
| |
| duration = toNumber(duration, config.transitionDuration); |
| var delay = (duration / 2); |
| |
| if (el === activeStep) { |
| windowScale = computeWindowScale(config); |
| } |
| |
| var targetScale = target.scale * windowScale; |
| |
| expectedTransitionTarget = target.scale > currentState.scale ? root : canvas; |
| |
| if (activeStep && activeStep !== el) { |
| onStepLeave(activeStep); |
| } |
| |
| 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" |
| }); |
| |
| currentState = target; |
| activeStep = el; |
| |
| if (duration === 0) { |
| onStepEnter(activeStep); |
| } |
| |
| return el; |
| }; |
| |
| var prev = function () { |
| var prev = steps.indexOf( activeStep ) - 1; |
| prev = prev >= 0 ? steps[ prev ] : steps[ steps.length-1 ]; |
| |
| return goto(prev); |
| }; |
| |
| var next = function () { |
| var next = steps.indexOf( activeStep ) + 1; |
| next = next < steps.length ? steps[ next ] : steps[ 0 ]; |
| |
| return goto(next); |
| }; |
| |
| root.addEventListener("impress:init", function(){ |
| // STEP CLASSES |
| steps.forEach(function (step) { |
| step.classList.add("future"); |
| }); |
| |
| root.addEventListener("impress:stepenter", function (event) { |
| event.target.classList.remove("past"); |
| event.target.classList.remove("future"); |
| event.target.classList.add("present"); |
| }, false); |
| |
| root.addEventListener("impress:stepleave", function (event) { |
| event.target.classList.remove("present"); |
| event.target.classList.add("past"); |
| }, false); |
| |
| }, false); |
| |
| root.addEventListener("impress:init", function(){ |
| // HASH CHANGE |
| |
| var lastHash = ""; |
| |
| // `#/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("impress:stepenter", function (event) { |
| window.location.hash = lastHash = "#/" + event.target.id; |
| }, false); |
| |
| window.addEventListener("hashchange", function () { |
| // don't go twice to same element |
| if (window.location.hash !== lastHash) { |
| goto( getElementFromUrl() ); |
| } |
| }, false); |
| |
| // START |
| // by selecting step defined in url or first step of the presentation |
| goto(getElementFromUrl() || steps[0], 0); |
| }, false); |
| |
| body.classList.add("impress-disabled"); |
| |
| return (roots[ "impress-root-" + rootId ] = { |
| init: init, |
| goto: goto, |
| next: next, |
| prev: prev |
| }); |
| |
| }; |
| |
| impress.supported = impressSupported; |
| |
| })(document, window); |
| |
| // EVENTS |
| |
| (function ( document, window ) { |
| 'use strict'; |
| |
| // 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); |
| }; |
| }; |
| |
| document.addEventListener("impress:init", function (event) { |
| var api = event.detail.api; |
| |
| // 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 |
| api.prev(); |
| break; |
| case 9: // tab |
| case 32: // space |
| case 34: // pg down |
| case 39: // right |
| case 40: // down |
| api.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 ( api.goto(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 that is not active |
| while ( !(target.classList.contains("step") && !target.classList.contains("active")) && |
| (target !== document.documentElement) ) { |
| target = target.parentNode; |
| } |
| |
| if ( api.goto(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 = api.prev(); |
| } else if ( x > window.innerWidth - width ) { |
| result = api.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 |
| api.goto( document.querySelector(".active"), 500 ); |
| }, 250), false); |
| |
| }, false); |
| |
| })(document, window); |
| |