impress.js的3D效果很是震撼,是一款不錯的插件,能夠拿來在web上實現ppt演示文稿的效果。css
源碼中自己就有很多註釋,是學習源碼的捷徑。html
只是,源碼的註釋是english,翻譯一下,方便之後查找調用。android
/** * 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` 事件 // 但實際上,1、我在兩個獨立的元素上調用變換 2、只要有一個值的變化就會觸發 `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 的代碼的時候,我學習了不少東西 // 我但願這份代碼和註釋,能夠對後來學習的人有所幫助