/** * 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.3 * 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 */ // you are one of those who like to know how thing work inside? // let me show you the cogs that make impress.js run... (function ( document, window ) { 'use strict'; // helper functions // `pfx` is a function that takes a standard css property name as a parameter // and returns it's prefixed version valid for current browser it runs in. // the code is heavily inspired by modernizr http://www.modernizr.com/ 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 ]; }; })(); // `arraify` takes an array-like object and turns it into real array // to make all the array.prototype goodness available. var arrayify = function ( a ) { return [].slice.call( a ); }; // `css` function applies the styles given in `props` object to the element // given as `el`. it runs all property names through `pfx` function to make // sure proper prefixed version of the property is used. 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; }; // `tonumber` takes a value given as `numeric` parameter and tries to turn // it into a number. if it is not possible it returns 0 (or other value // given as `fallback`). var tonumber = function (numeric, fallback) { return isnan(numeric) ? (fallback || 0) : number(numeric); }; // `byid` returns element with given `id` - you probably have guessed that ;) var byid = function ( id ) { return document.getelementbyid(id); }; // `$` returns first element for given css `selector` in the `context` of // the given element or whole document. var $ = function ( selector, context ) { context = context || document; return context.queryselector(selector); }; // `$$` return an array of elements for given css `selector` in the `context` of // the given element or whole document. var $$ = function ( selector, context ) { context = context || document; return arrayify( context.queryselectorall(selector) ); }; // `triggerevent` builds a custom dom event with given `eventname` and `detail` data // and triggers it on element given as `el`. var triggerevent = function (el, eventname, detail) { var event = document.createevent("customevent"); event.initcustomevent(eventname, true, true, detail); el.dispatchevent(event); }; // `translate` builds a translate transform string for given data. var translate = function ( t ) { return " translate3d(" + t.x + "px," + t.y + "px," + t.z + "px) "; }; // `rotate` builds a rotate transform string for given data. // by default the rotations are in x y z order that can be reverted by passing `true` // as second parameter. 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; }; // `scale` builds a scale transform string for given data. var scale = function ( s ) { return " scale(" + s + ") "; }; // `perspective` builds a perspective transform string for given data. var perspective = function ( p ) { return " perspective(" + p + "px) "; }; // `getelementfromhash` returns an element located by id from hash part of // window location. var getelementfromhash = 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(/^#\/?/,"") ); }; // `computewindowscale` counts the scale factor between window size and size // defined for the presentation in the config. 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 = // browser should support css 3d transtorms ( pfx("perspective") !== null ) && // and `classlist` and `dataset` apis ( body.classlist ) && ( body.dataset ) && // but some mobile devices need to be blacklisted, // because their css 3d support or hardware is not // good enough to run impress.js properly, sorry... ( 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"); } // globals and defaults // this is were the root elements of all impress.js instances will be kept. // yes, this means you can have more than one instance on a page, but i'm not // sure if it makes any sense in practice ;) var roots = {}; // some default config values. var defaults = { width: 1024, height: 768, maxscale: 1, minscale: 0, perspective: 1000, transitionduration: 1000 }; // it's just an empty function ... and a useless comment. var empty = function () { return false; }; // impress.js api // and that's where interesting things will start to happen. // it's the core `impress` function that returns the impress.js api // for a presentation based on the element with given id ('impress' // by default). 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 given root is 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 // // there are currently two step events triggered by impress.js // `impress:stepenter` is triggered when the step is shown on the // screen (the transition from the previous one is finished) and // `impress:stepleave` is triggered when the step is left (the // transition to next step just starts). // reference to last entered step var lastentered = null; // `onstepenter` is called whenever the step element is entered // but the event is triggered only if the step is different than // last entered step. var onstepenter = function (step) { if (lastentered !== step) { triggerevent(step, "impress:stepenter"); lastentered = step; } }; // `onstepleave` is called whenever the step element is left // but the event is triggered only if the step is the same as // last entered step. var onstepleave = function (step) { if (lastentered === step) { triggerevent(step, "impress:stepleave"); lastentered = null; } }; // `initstep` initializes given step element by reading data from its // data attributes and setting correct styles. 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" }); }; // `init` api function that initializes (and runs) the presentation. var init = function () { if (initialized) { return; } // first we set up the viewport for mobile devices. // for some reason ipad goes nuts when it is not done properly. 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%" }); 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); body.classlist.remove("impress-disabled"); body.classlist.add("impress-enabled"); // get and init steps steps = $$(".step", root); steps.foreach( initstep ); // set a default initial state of the canvas 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 ] }); }; // `getstep` is a helper function that returns a step element defined by parameter. // if a number is given, step with index given by the number is returned, if a string // is given step element with such id is returned, if dom element is given it is returned // if it is a correct step element. 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; }; // used to reset timeout for `impress:stepenter` event var stepentertimeout = null; // `goto` api function that moves to step given with `el` parameter (by index, id or element), // with a transition `duration` optionally given as second parameter. 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); // compute target state of the canvas based on given step 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. // // this information is used to alter the transition style: // when we are zooming in - we start with move and rotate transition // and the scaling is delayed, but when we are zooming out we start // with scaling down and move and rotation are delayed. var zoomin = target.scale >= currentstate.scale; duration = tonumber(duration, config.transitionduration); var delay = (duration / 2); // if the same step is re-selected, force computing window scaling, // because it is likely to be caused by window resize if (el === activestep) { windowscale = computewindowscale(config); } var targetscale = target.scale * windowscale; // trigger leave of currently active element (if it's not the same step again) if (activestep && activestep !== el) { onstepleave(activestep); } // now we alter transforms of `root` and `canvas` to trigger transitions. // // and here is why there are two elements: `root` and `canvas` - they are // being animated separately: // `root` is used for scaling and `canvas` for translate and rotations. // transitions on them are triggered with different delays (to make // visually nice and 'natural' looking transitions), so we need to know // that both of them are finished. 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" }); // here is a tricky part... // // if there is no change in scale or no change in rotation and translation, it means there was actually // no delay - because there was no transition on `root` or `canvas` elements. // we want to trigger `impress:stepenter` event in the correct moment, so here we compare the current // and target values to check if delay should be taken into account. // // i know that this `if` statement looks scary, but it's pretty simple when you know what is going on // - it's simply comparing all the values. if ( currentstate.scale === target.scale || (currentstate.rotate.x === target.rotate.x && currentstate.rotate.y === target.rotate.y && currentstate.rotate.z === target.rotate.z && currentstate.translate.x === target.translate.x && currentstate.translate.y === target.translate.y && currentstate.translate.z === target.translate.z) ) { delay = 0; } // store current state currentstate = target; activestep = el; // and here is where we trigger `impress:stepenter` event. // we simply set up a timeout to fire it taking transition duration (and possible delay) into account. // // i really wanted to make it in more elegant way. the `transitionend` event seemed to be the best way // to do it, but the fact that i'm using transitions on two separate elements and that the `transitionend` // event is only triggered when there was a transition (change in the values) caused some bugs and // made the code really complicated, cause i had to handle all the conditions separately. and it still // needed a `settimeout` fallback for the situations when there is no transition at all. // so i decided that i'd rather make the code simpler than use shiny new `transitionend`. // // if you want learn something interesting and see how it was done with `transitionend` go back to // version 0.5.2 of impress.js: http://github.com/bartaz/impress.js/blob/0.5.2/js/impress.js window.cleartimeout(stepentertimeout); stepentertimeout = window.settimeout(function() { onstepenter(activestep); }, duration + delay); return el; }; // `prev` api function goes to previous step (in document order) var prev = function () { var prev = steps.indexof( activestep ) - 1; prev = prev >= 0 ? steps[ prev ] : steps[ steps.length-1 ]; return goto(prev); }; // `next` api function goes to next step (in document order) var next = function () { var next = steps.indexof( activestep ) + 1; next = next < steps.length ? steps[ next ] : steps[ 0 ]; return goto(next); }; // adding some useful classes to step elements. // // all the steps that have not been shown yet are given `future` class. // when the step is entered the `future` class is removed and the `present` // class is given. when the step is left `present` class is replaced with // `past` class. // // so every step element is always in one of three possible states: // `future`, `present` and `past`. // // there classes can be used in css to style different types of steps. // for example the `present` class can be used to trigger some custom // animations when step is shown. 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); // adding hash change support. root.addeventlistener("impress:init", function(){ // last hash detected 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 // makes transtion 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 () { // when the step is entered hash in the location is updated // (just few lines above from here), so the hash change is // triggered and we would call `goto` again on the same element. // // to avoid this we store last entered hash and compare. if (window.location.hash !== lasthash) { goto( getelementfromhash() ); } }, false); // start // by selecting step defined in url or first step of the presentation goto(getelementfromhash() || steps[0], 0); }, false); body.classlist.add("impress-disabled"); // store and return api for given impress.js root element return (roots[ "impress-root-" + rootid ] = { init: init, goto: goto, next: next, prev: prev }); }; // flag that can be used in js to check if browser have passed the support test impress.supported = impresssupported; })(document, window); // navigation events // as you can see this part is separate from the impress.js core code. // it's because these navigation actions only need what impress.js provides with // its simple api. // // in future i think about moving it to make them optional, move to separate files // and treat more like a 'plugins'. (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); }; }; // wait for impress.js to be initialized document.addeventlistener("impress:init", function (event) { // getting api from event data. // so you don't event need to know what is the id of the root element // or anything. `impress:init` event data gives you everything you // need to control the presentation that was just initialized. 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 (next or prev) on keyup. // supported keys are: // [space] - quite common in presentation software to move forward // [up] [right] / [down] [left] - again common and natural addition, // [pgdown] / [pgup] - often triggered by remote controllers, // [tab] - this one is quite controversial, but the reason it ended up on // this list is quite an interesting story... remember that strange part // in the impress.js code where window is scrolled to 0,0 on every presentation // step, because sometimes browser scrolls viewport because of the focused element? // well, the [tab] key by default navigates around focusable elements, so clicking // it very often caused scrolling to focused element and breaking impress.js // positioning. i didn't want to just prevent this default action, so i used [tab] // as another way to moving to next step... and yes, i know that for the sake of // consistency i should add [shift+tab] as opposite action... 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 // based on awesome work of @hakimel: https://github.com/hakimel/reveal.js 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); //代码整理:懒人之家 www.lanrenzhijia.com // that's all folks! // // thanks for reading it all. // or thanks for scrolling down and reading the last part. // // i've learnt a lot when building impress.js and i hope this code and comments // will help somebody learn at least some part of it.