impress.js的3D效果非常震撼,是一款不错的插件,可以拿来在web上实现ppt演示文稿的效果。
源码中本身就有不少注释,是学习源码的捷径。
只是,源码的注释是english,翻译一下,方便以后查找调用。
/** * impress.js * * impress.js 是基于现代浏览器中CSS3强大变换和渲染能力的一款展示工具 * 其灵感源自于 prezi.com. * * * 版权 2011-2012 Bartek Szopka (@bartaz) * * 基于 MIT 和 GPL 授权发布. * * ------------------------------------------------ * 作者: Bartek Szopka * 版本: 0.5.3 * 链接地址: http://bartaz.github.com/impress.js/ * 来源: 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 */ // 你也对impress的内部运行机理非常感兴趣? // 那我来告诉你impress是如何工作的... (function ( document, window ) { 'use strict'; // 辅助函数 // `pfx` 接受一个以标准CSS3属性名称的参数,返回其在当前浏览器中是否可用的信息 // 编码灵感源自 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` 接受一个类似数组的参数,返回一个具有数组全部属性的数组 var arrayify = function ( a ) { return [].slice.call( a ); }; // `css` 提供如下功能: // 添加`props` 对象的样式到`el`对象上. // 它通过调用`pfx`确保每一个样式属性都可用。 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` 接收一个类似数值的参数,并返回一个数值。 // 如果不成功,返回0(或者自定义的fallback参数) var toNumber = function (numeric, fallback) { return isNaN(numeric) ? (fallback || 0) : Number(numeric); }; // `byId` 返回一个给定id的元素 var byId = function ( id ) { return document.getElementById(id); }; // `$` 返回在参数context(默认为document)中的给定CSS选择器所选择的第一个元素 var $ = function ( selector, context ) { context = context || document; return context.querySelector(selector); }; // `$$` 返回在参数context(默认为document)中的给定CSS选择器所选择的一组元素 var $$ = function ( selector, context ) { context = context || document; return arrayify( context.querySelectorAll(selector) ); }; // `triggerEvent` 构造一个事件, // 该事件以`eventName`命名,用`detail`处理数据,并在el上触发 var triggerEvent = function (el, eventName, detail) { var event = document.createEvent("CustomEvent"); event.initCustomEvent(eventName, true, true, detail); el.dispatchEvent(event); }; // `translate` 对于给定数据构造平移变换 var translate = function ( t ) { return " translate3d(" + t.x + "px," + t.y + "px," + t.z + "px) "; }; // `rotate`对于给定数据构造旋转变换 // 默认旋转顺序为 X Y Z ,可以设定第二个参数为true来倒转顺序 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` 对于给定数据构造缩放变换 var scale = function ( s ) { return " scale(" + s + ") "; }; // `perspective` 对于给定数据构造透视变换 var perspective = function ( p ) { return " perspective(" + p + "px) "; }; // `getElementFromHash` 返回一个在window location的hash存储部分中以id为key的元素 var getElementFromHash = function () { // 从url中移除开头的“#”或“#/”以获得id // 所以"fallback" `#slide-id`和"enhanced" `#/slide-id`都可以 return byId( window.location.hash.replace(/^#\/?/,"") ); }; // `computeWindowScale` 计算在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; }; // 检查支持性 var body = document.body; var ua = navigator.userAgent.toLowerCase(); var impressSupported = // 浏览器需要支持CSS3D变换 ( pfx("perspective") !== null ) && // 浏览器需要支持classList和dataset接口API ( body.classList ) && ( body.dataset ) && // 但是一些不支持CSS 3D效果的浏览器及其移动设备就只能上黑名单了 ( ua.search(/(iphone)|(ipod)|(android)/) === -1 ); if (!impressSupported) { // 我不确定是否支持 classlist body.className += " impress-not-supported "; } else { body.classList.remove("impress-not-supported"); body.classList.add("impress-supported"); } // 全局设置和默认值 // root元素是所有impress实例都必须保存维护的根元素 // 在一个页面里你可以拥有2个以上的实例,但我不知这有什么意义 var roots = {}; // 默认值 var defaults = { width: 1024, height: 768, maxScale: 1, minScale: 0, perspective: 1000, transitionDuration: 1000 }; // 仅仅是个空函数(当然,这也是一句没用的注释 ^_^) var empty = function () { return false; }; // IMPRESS.JS 接口API // 你感兴趣的事情即将开始 // impress的核心模块就是下面这个 `impress` 函数 // 他返回一个以给定id(默认是'impress')命名的对象 // 该对象具有impress的所有接口。 var impress = window.impress = function ( rootId ) { // 如果浏览器不支持 impress ,那么,返回一个空接口 // 这可能不是最好的解决方案,但我们应该避免在不支持impress的浏览器里运行 if (!impressSupported) { return { init: empty, goto: empty, prev: empty, next: empty }; } rootId = rootId || "impress"; // 如果 root 已经初始化,返回这个API对象(单例) -----by yczhu if (roots["impress-root-" + rootId]) { return roots["impress-root-" + rootId]; } // 演示step的所有数据 var stepsData = {}; // 当前的活跃step var activeStep = null; // 当前演示的状态参数 (位置,角度,大小等) var currentState = null; // 演示step对象队列 var steps = null; // 选项配置 var config = null; // 浏览器窗口大小参数 var windowScale = null; // 演示根对象 root var root = byId( rootId ); var canvas = document.createElement("div"); var initialized = false; // step事件 // // 目前 impress.js 将触发2个事件 // `impress:stepenter` 将在当前step刚展示的时候(上一step已经结束)触发 // `impress:stepleave` 将在当前step结束的时候(下一step即将展示)触发 // 上一 step 的引用 var lastEntered = null; // `onStepEnter` 每当step被将要展示时调用 // 但当前step必须和上一step不同 var onStepEnter = function (step) { if (lastEntered !== step) { triggerEvent(step, "impress:stepenter"); lastEntered = step; } }; // `onStepLeave` 每当step即将结束时调用 // 当前step必须和上一step相同 // 在enter的时候,lastStep已经被重置 var onStepLeave = function (step) { if (lastEntered === step) { triggerEvent(step, "impress:stepleave"); lastEntered = null; } }; // `initStep` 通过读取在data属性中的数据初始化给定step对象,设置正确的样式 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 }; //如果未命名id,用step-N命名 -----by yczhu 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` 初始化impress API 以运行impress var init = function () { //已经初始化,返回(单例执行) -----by yczhu if (initialized) { return; } // 首先为移动设备设置视角 // 但出于某种未知原因,ipad在运行不正常时会卡住死掉 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); } // 初始化配置对象 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 ); // 将step用 "canvas" 元素包裹 arrayify( root.childNodes ).forEach(function ( el ) { canvas.appendChild( el ); }); root.appendChild(canvas); // 设置初始化样式 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); body.classList.remove("impress-disabled"); body.classList.add("impress-enabled"); // 获取并初始化steps steps = $$(".step", root); steps.forEach( initStep ); // 为 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` 是辅助函数,返回一个由参数定义的step对象 // 如果参数是数字,返回由"step-N"对象 // 如果参数是字符串,返回由该字符串作为id的step对象 // 如果参数为DOM元素,返回标记为“step”的元素对象(只要是正确且存在的DOM标签) 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; }; // 重置 `impress:stepenter` 事件计时器 var stepEnterTimeout = null; // `goto` API 跳转至以`el`参数(索引,id或元素名)标记的step , // `duration` 参数可选. var goto = function ( el, duration ) { if ( !initialized || !(el = getStep(el)) ) { // 演示没有初始化或者给定el不是step对象 return false; } // 有时候,可能需要用一些键盘操作使第一个链接获得焦点 // 但这种情况下,浏览器本身可能会滚动页面去显示其他元素 // (甚至直接将body的overflow属性设置为hidden都不行) // 这将干扰到我们的精心布局 // 所以,作为一个糟糕且简单的解决方案:不管滚动条是否获得焦点,我们都将页面回滚至顶端 // 如果你阅读到这里并且有更好的方式去处理它,我洗耳恭听! 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); // 计算给定 step 在canvas上的目标状态 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 }; // 检查变换是否缩放 // // 这些信息都将用于选择变换样式 // This information is used to alter the transition style: // 当进行缩放(zoomin)时,我们先进行平移和旋转,将缩放的操作置后 // 当缩放结束(zoomout)时,我们先进行缩放,将平移和旋转的操作置后 var zoomin = target.scale >= currentState.scale; duration = toNumber(duration, config.transitionDuration); var delay = (duration / 2); // 如果相同的step被再一次选中,强制计算窗口大小,以为这很可能由窗口大小改变引起 if (el === activeStep) { windowScale = computeWindowScale(config); } var targetScale = target.scale * windowScale; // 触发当前活动step的离开事件 (同样保证他不是重复选择的元素) if (activeStep && activeStep !== el) { onStepLeave(activeStep); } // 现在,我们调用`root` 和 `canvas`去执行变换 // // 这儿就是为什么我们要用`root`和`canvas`两个元素的原因 -- 他们分别独立动画 // `root`用于缩放,`canvas`用于平移和旋转. // 他们触发的延迟也不一样(这是为了使变换看起来过渡自然), // 所以我们需要知道他们是否都结束了 css(root, { // 使 透视 效果与众不同 // 同样,我们需要 'scale' 透视 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" }); // 接下来就是见证奇迹的时刻。。 // // 如果没有用到缩放平移旋转等变换,那意味着没有延迟---毕竟,`root`和`canvas`上没有变换 // 我们想在恰当的时机触发`impress:stepenter`事件,所以,我们要比较当前和目标值以确定是否需要考虑延迟 // 我知道这个“是否”听起来有点可怕,但如果你知道接下来会发生什么,一切都会变得很easy。 // 毕竟,比较所有的值还是很简单的。 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; } // 存储当前状态 currentState = target; activeStep = el; // 是时候触发 `impress:stepenter` 事件了. // 我们简单地设置了一个延时器去解决可能出现的变换延迟 // // 我非常想用一种更优雅简洁的方式去处理 `transitionend` 事件 // 但实际上,一、我在两个独立的元素上调用变换 二、只要有一个值的变化就会触发 `transitionend`事件 // 这导致了一些bug并使得代码变得很复杂(因为我需要将所有场景都分成两个部分独立考虑) // 而且,还需要一个延迟的回调函数去处理压根就没有变换的场景 // 所以,我决定选择使代码变得简单的方式,放弃那个“闪亮”的`transitionend`事件处理 // // 如果你想学习一些更加有趣的东西或者你想知道`transitionend`是怎么运行的,请访问: // 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 跳回上一 step (按文档顺序) var prev = function () { var prev = steps.indexOf( activeStep ) - 1; prev = prev >= 0 ? steps[ prev ] : steps[ steps.length-1 ]; return goto(prev); }; // `next` API 跳向下一 step (按文档顺序) var next = function () { var next = steps.indexOf( activeStep ) + 1; next = next < steps.length ? steps[ next ] : steps[ 0 ]; return goto(next); }; // 为 step 元素添加一些有用的类(样式) // // 所有尚未展示的 steps 我们都添加 `future` 类. // 当某一 step 被展示,`future`类会被移除,而且会被添加上 `present`类 // 当某一step结束时, `present` 又会被替换成`past`类 // // 所以,每一个step都处于这三种状态之一: // `future`, `present` 和 `past`. // // 这些类标签可以用在CSS中为step设置样式. // 例如`present`类可以在某一step在被展示的时候用于触发一些自定义动画 root.addEventListener("impress:init", function(){ //step 的类 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); // 添加hash变化支持. root.addEventListener("impress:init", function(){ // 将lasthash清空 var lastHash = ""; // `#step-id`将被替换成`#/step-id`以防止浏览器在默认状态下滚动至hash表中所保存元素所在位置 // // 而且添加hash的操作必须在动画结束以后进行, 因为在Chrome里会导致动画延迟 // 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 () { // 当某一step被展示时,location里的hash已经更新(就在上面几行) // 所以hash change事件被触发,我们将在同一step上再一次调用`goto`方法。 // 为了避免这一情况,我们将存储上一次 hash 的结果并比较. if (window.location.hash !== lastHash) { goto( getElementFromHash() ); } }, false); // 开始 // 选择记录在url中的step地址,或者演示文稿的第一张step goto(getElementFromHash() || steps[0], 0); }, false); body.classList.add("impress-disabled"); // 存储方法并返回保存这些API接口的impress.js根对象 return (roots[ "impress-root-" + rootId ] = { init: init, goto: goto, next: next, prev: prev }); }; // 这一标记可以在 JS 使用,以检查浏览器通过了支持性测试 impress.supported = impressSupported; })(document, window); // 导航事件 // 如你所见,这一部分脱离于impress的核心代码 // 那是因为:这一部分的导航动作仅仅依赖于impres所提供的接口,不属于核心代码 // // 在将来我会考虑将他们以配置项的方式做成独立模块,使得这一块看起来更像插件 (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); }; }; // 等待impress加载完成 document.addEventListener("impress:init", function (event) { // 从事件数据中获取API接口 // 所以,你不必在意impress根对象或其他对象的id // `impress:init` 事件数据为你提供了在演示中所需要的一切 var api = event.detail.api; // 键盘处理导航 // 防止不允许被按下的键被意外按下 document.addEventListener("keydown", function ( event ) { if ( event.keyCode === 9 || ( event.keyCode >= 32 && event.keyCode <= 34 ) || (event.keyCode >= 37 && event.keyCode <= 40) ) { event.preventDefault(); } }, false); // 当按键弹起时,触发impress动作(下一张或上一张) // 支持的按键如下: // [空格] - 在幻灯片演示软件中常用于切换下一页 // [上] [右] / [下] [左] - 与正常情况一致 // [下翻页] / [上翻页] - 通常被遥控器触发, // [tab键] - 这个按键颇具争议,理由不再赘述 // 这份列表是个颇为有趣的故事 // 记住在impress中诡异的那部分:窗口在每一个展示step都会滚动至(0,0), // 难道是由于元素获得焦点偶发导致浏览器滚动视角? // 好吧,[tab]在默认情况下导航至可定位焦点的元素, // 所以过于频繁的按下该键将导致滚动至获得焦点的元素并让演示效果大打折扣 // 我不想就此简单阻止这一默认动作,所以,我使用 [tab]作为移动至下一张step的手段 // 我知道,为了保持一致性,我应当再添加 [shift+tab]处理相反动作 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: // 上翻页 case 37: // 小键盘左 case 38: // 小键盘上 api.prev(); break; case 9: // tab键 case 32: // 空格 case 34: // 下翻页 case 39: // 小键盘右 case 40: // 小键盘下 api.next(); break; } event.preventDefault(); } }, false); // 处理在当前演示 steps 上产生的单击事件 document.addEventListener("click", function ( event ) { // 事件冒泡处理 // 检查单击的目标(及其祖先级容器)是否是超链接 var target = event.target; while ( (target.tagName !== "A") && (target !== document.documentElement) ) { target = target.parentNode; } if ( target.tagName === "A" ) { var href = target.getAttribute("href"); // 如果指向某一连接,跳转至这一连接 if ( href && href[0] === '#' ) { target = document.getElementById( href.slice(1) ); } } if ( api.goto(target) ) { event.stopImmediatePropagation(); event.preventDefault(); } }, false); // 处理在当前演示 steps 上产生的单击事件 document.addEventListener("click", function ( event ) { var target = event.target; // 查找距当前活跃step最近的不活跃step while ( !(target.classList.contains("step") && !target.classList.contains("active")) && (target !== document.documentElement) ) { target = target.parentNode; } if ( api.goto(target) ) { event.preventDefault(); } }, false); // 处理触摸屏上上轻击屏幕左边或者右边的事件 // 这部分工作完成于 @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); // 当窗口大小改变时,重新计算窗口大小 window.addEventListener("resize", throttle(function () { // 强制激活当前step api.goto( document.querySelector(".step.active"), 500 ); }, 250), false); }, false); })(document, window); // 就这些! // // 感谢您能把它全部读完. // 如果您是直接滚动鼠标跳转至这一段,也同样表示感谢! // // 在构建 impress.js 的代码的时候,我学习了很多东西 // 我希望这份代码和注释,可以对后来学习的人有所帮助