/**
* impress.js
*(本翻譯并未完全遵照原作者的注釋翻譯)
* Impress.js 是受 Prezi啟發,基于現代浏覽器的 CSS3 與 JavaScript
*語言完成的一個可供開發者使用的表現層架構.
*
* Copyright 2011-2012 Bartek Szopka (@bartaz)
* Released under the MIT and GPL Licenses.
* ------------------------------------------------
* 作者: Bartek Szopka
* 版本: 0.5.3
* url: 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.js内部工作原理嗎?
// 現在從使impress.js開始運轉的齒輪開始為您介紹...
(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 = {};
returnfunction (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` 函數確定樣式的每一個屬性可用。
// 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` 函數提供類型強制轉換功能,将傳入參數轉換為數值類型。
// 如果轉換失敗,傳回0 (或者自定義的
// `fallback`回調函數).
var toNumber = function (numeric, fallback) {
return isNaN(numeric) ? (fallback || 0) : Number(numeric);
// `byId` 傳回給定`id`的元素 - 你懂得 ;)
var byId = function (id) {
return document.getElementById(id);
// `$` 通過給定的 CSS選擇器(`selector`)傳回在
//上下文(`context`,指定元素或者整個文檔)中
//第一個比對的元素
var $ = function (selector, context) {
context = context || document;
return context.querySelector(selector);
// `$$`通過給定的 CSS選擇器(`selector`)傳回在
//比對的元素數組
var $$ = function (selector, context) {
return arrayify(context.querySelectorAll(selector));
// `triggerEvent` 為指定元素建構事件
// 該函數有三個參數:
// el:目标元素
// eventName:事件名稱
//detail:傳入資料
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軸順序進行,
// 可以通過設定第二個參數‘revert’來改變旋轉順序
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`
// 以ID為key,從window location中取得資料
var getElementFromHash = function () {
// 從url中取得id,通過删除開頭的 `#` 或者 `#/`
// 是以 "fallback" `#slide-id` 和 "enhanced" `#/slide-id` 都合乎規範
return byId(window.location.hash.replace(/^#\/?/, ""));
// `computeWindowScale` 通過定義在config檔案中的 window size 和 size,
//計算實際的縮放效果,
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 =
// 浏覽器應該支援 CSS 3D 變換、classList和dataset APIs
(pfx("perspective") !== null) &&
(body.classList) &&
(body.dataset) &&
// 但是一些移動裝置得上黑名單了,
// 因為他們對CSS 3D的支援和硬體特性不足以運作impress.js
// sorry...
(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.js執行個體都必須保持并維護.
// 是的!在一個頁面裡我們可以擁有多個執行個體,但是
// 我不知道多個執行個體的意義在哪裡;)
var roots = {};
// 預設配置.
var defaults = {
width: 1024,
height: 768,
maxScale: 1,
minScale: 0,
perspective: 1000,
transitionDuration: 1000
// 僅僅是一個空方法 ... 好吧,這個注釋也很無聊^_^.
var empty = function () { returnfalse; };
// IMPRESS.JS API
// 你感興趣的地方,才剛剛開始.
// `impress` function 是整個impress元件的核心,它傳回一個指定id的對象,預設為
//該對象包含所有impress API 接口。
//
var impress = window.impress = function (rootId) {
// 如果impress.js不被浏覽器看支援,它傳回一個假的對象
// 這可能不是一個好的解決方案,但是這可以避免程式繼續運作而出錯
if (!impressSupported) {
return {
init: empty,
goto: empty,
prev: empty,
next: empty
};
rootId = rootId || "impress";
// 如果root已經初始化,傳回該api
if (roots["impress-root-" + rootId]) {
return roots["impress-root-" + rootId];
// 所有示範步驟(step)的資料
var stepsData = {};
// 目前正在播放的step所在的html元素
var activeStep = null;
// 目前示範的狀态資料 (position(位置),
//rotation(旋轉)和 scale(縮放))
var currentState = null;
// step 元素數組
var steps = null;
// 配置項
var config = null;
// 浏覽器縮放效果配置參數
var windowScale = null;
// 根對象
var root = byId(rootId);
var canvas = document.createElement("div");
var initialized = false;
// step 事件
//
// 在 impress.js中,有兩個事件會被觸發
// 當step在螢幕中被展現時将觸發 `impress:stepenter`事件
// (上一個step展現結束) ;
// `impress:stepleave` 目前step展現結束,下一個step即将開始時将觸發
//`impress:stepleave`事件
// 上一個step的引用
var lastEntered = null;
// step剛被展現的時候,onStepEnter會被調用
// 前提是目前step必須和上一step不同
var onStepEnter = function (step) {
if (lastEntered !== step) {
triggerEvent(step, "impress:stepenter");
lastEntered = step;
// step結束的時候,onStepLeave會被調用
// 前提是目前step必須和上一step相同
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
};
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:初始化并運作目前示範文檔.
var init = function () {
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 = {
transformOrigin: "top left",
transition: "all 0s ease-in-out",
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");
// 擷取并設定step
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對象(隻要對象存在).
var getStep = function (step) {
if (typeof step === "number") {
step = step < 0 ? steps[steps.length + step] : steps[step];
} elseif (typeof step === "string") {
step = byId(step);
return (step && step.id && stepsData["impress-" + step.id]) ? step :null;
// 為 `impress:stepenter` 事件設定timeout值
var stepEnterTimeout = null;
// `goto` API 函數,根據傳入的‘el’,跳轉至指定的step (索引值,id或者元素),
// `duration` 可選,機關為秒.
var goto = function (el, duration) {
if (!initialized || !(el = getStep(el))) {
// presentation not initialized or given element is not a step
returnfalse;
// 有時候通過鍵盤操作來使第一個連結獲得焦點是有必要的.
// 浏覽器此時可能會滾動頁面顯示這個元素
// (甚至直接将body的overflow屬性設定為hidden都不行) 這将影響我們的布局效果.
//
// 最簡單的,在任何一個step顯示的時候,我們都将頁面滾動到頂端
// 如果你有更好的解決方案,請聯系我,洗耳恭聽!
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
// 确定變換是否縮放(zooming in)(逐漸放大過程).
// 下面的資訊用于修改變換時的樣式:
// 當元素逐漸放大的時候 - 首先進行平移和旋轉
// 之後才進行縮放, 當逐漸縮小(zooming out)時,
// 先進行向内縮放,然後做平移和旋轉.
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的離開(leave)事件 (隻有不是重複選擇的step才觸發該事件)
if (activeStep && activeStep !== el) {
onStepLeave(activeStep);
// 現在我們修改 `root` 和 `canvas` 的變換屬性,觸發變換.
// 存在root和canvas這兩個對象原因---
// 它們獨立進行動畫:
// `root`用于縮放而 `canvas` 用于平移和旋轉.
// 二者開始變換的延時時間也不相同
// (為了變換過程在視覺效果上看起來自然、美觀),
// 是以我們需要知道二者的行為是否都結束了.
// 為了保證在不同的縮放時使透視效果看起來是一樣的
// 我們同時需要縮放透視(perspective)
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),
transitionDelay: (zoomin ? 0 : delay) + "ms"
// 最複雜的部分到了...
// 如果在縮放、平移、旋轉屬性上無任何變化, 就意味着沒有延遲
// - 因為在 `root` 或者`canvas`元素上沒有任何變換.
// 我們需要在恰當的時刻觸發 `impress:stepenter` 事件,
// 是以我們比較目前和目标值進而确定是否需要計算延遲.
// 我隻懂這個‘if’聽起來很可怕, 但是當你知道即将要發生什麼,這一切都變得很簡單,
// - 簡單到隻需要比較下面這些值.
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`事件在隻要有一個值發生變化就會被觸發 (change in the values)
// 這引發了一些bug,并且使代碼變得複雜, 因為我必須對所有場景都單獨考慮.
// 而且當根本沒有變換發生的時候,仍然需要一個 `setTimeout`延遲回調函數, .
// 是以我決定選擇寫更簡單的代碼而不是使用看起來更酷的 `transitionend`事件.
// 如果你想學習一些有意思的内容,
//去看impress.js 的0.5.2版本: 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 函數,跳轉到下一個step (在文檔中的順序)
var next = function () {
var next = steps.indexOf(activeStep) + 1;
next = next < steps.length ? steps[next] : steps[0];
return goto(next);
// 為step元素添加一些有用的類.
// 所有未被展示的step都被添加 `future` 類.
// 當step被播放時, `future`類被移除, `present`類被添加
//step結束時, `present` 類被‘past’類替換
// 是以每一個step都具有下面三種狀态:
// `future`, `present` and `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);
// 添加hash變化支援.
// 被探測到的上一個hash
var lastHash = "";
// 使用`#/step-id` 替代 `#step-id`
//以阻止浏覽器滾動到該id的元素
// 必須在動畫結束之後設定hash,
// 因為在google浏覽器會導緻變換的動畫延遲.
// BUG: http://code.google.com/p/chromium/issues/detail?id=62820
window.location.hash = lastHash = "#/" + event.target.id;
window.addEventListener("hashchange", function () {
// 當step開始展現, location 中的hash已經被更新,
// (就是上面幾行代碼),
//是以hashchange事件被觸發,這導緻同一step元素會被再次調用 `goto`
//
// 為了避免這一情況,我們存儲上次的hash值然後做比較
if (window.location.hash !== lastHash) {
goto(getElementFromHash());
// 通過url或者選擇文檔中的第一個step開始播放
goto(getElementFromHash() || steps[0], 0);
body.classList.add("impress-disabled");
// 存儲并傳回root對象
return (roots["impress-root-" + rootId] = {
init: init,
goto: goto,
next: next,
prev: prev
});
// 浏覽器是否支援impress.js的标記
impress.supported = impressSupported;
})(document, window);
// 導航事件
// 如你所見,這一部分代碼和impress.js核心代碼相分離.
// 這是因為這一部分代碼僅僅需要impress.js提供的接口
//
// 在将來,我考慮将這部分代碼放到獨立的檔案中
// 以插件的形式的存在.
// throttling function calls, by Remy Sharp
// http://remysharp.com/2010/07/21/throttling-function-calls/
var throttle = function (fn, delay) {
var timer = null;
returnfunction () {
var context = this, args = arguments;
clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(context, args);
}, delay);
// 等待 impress.js 初始化完畢
document.addEventListener("impress:init", function (event) {
// 從eventdata中擷取api接口.
// 是以你不必在意impress.js的root元素的id或者其他屬性是什麼。
// `impress:init` event data給你所有想要的資料去控制播放
// .
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);
// 當按鍵彈起事,觸發按鍵事件 (← 或者 → )
// 支援的按鍵:
// [空格] - 跳到下一頁
// [↑] [→] / [↓] [←] - 上,右,下,左,
// [下一頁] / [下一頁] - 通常被遙控器觸發,
// [tab] -很有争議,理由就不讨論了
// 備忘錄... 記得詭異的部分:
// 每一個step播放時,頁面視窗都從(0,0)開始
//
// 嗯, [tab] 鍵在預設情況下導航至可定位焦點的元素,
// 是以頻繁的點選此鍵,會破壞示範效果
// 我不想簡單的禁用[tab], 是以我使用 [tab]
// 作為跳到下一個step的另一種方法... 當然, 為了保持一緻性
// 我應該添加 [shift+tab] 作為回退操作...
document.addEventListener("keyup", function (event) {
switch (event.keyCode) {
case 33: // pg up
case 37: // left
case 38: // up
api.prev();
case 9: // tab
case 32: // space
case 34: // pg down
case 39: // right
case 40: // down
api.next();
// 處理在目前示範step中産生的單擊事件
document.addEventListener("click", function (event) {
// 處理事件冒泡( "bubbling")
// 是否是超連結
var target = event.target;
while ((target.tagName !== "A") &&
(target !== document.documentElement)) {
target = target.parentNode;
if (target.tagName === "A") {
var href = target.getAttribute("href");
//如果指向某一step,直接到該step
if (href && href[0] === '#') {
target = document.getElementById(href.slice(1));
if (api.goto(target)) {
event.stopImmediatePropagation();
// 處理在stepi上的點選
//查找最近的沒有被激活的step
while (!(target.classList.contains("step") && !target.classList.contains("active")) &&
// 處理觸摸屏上的輕敲螢幕左右邊緣的事件
// 參考 @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();
} elseif (x > window.innerWidth - width) {
result = api.next();
if (result) {
event.preventDefault();
// 浏覽器視窗改變時,重新展示目前step
window.addEventListener("resize", throttle(function () {
// 強制再次激活目前step
api.goto(document.querySelector(".step.active"), 500);
}, 250), false);
}, false);
// 到此為止!
// 謝謝你把它全部讀完.
//即使你是直接滾到到此處,仍然感謝.
// 在編寫impress.js時,我學到了很多。希望這些代碼和注釋能對你有所幫助。
ps:對此文章或者安全、安全程式設計感興趣的讀者,可以加qq群:Hacking:303242737;Hacking-2群:147098303;Hacking-3群:31371755;hacking-4群:201891680;Hacking-5群:316885176
本文轉自玄魂部落格園部落格,原文連結:http://www.cnblogs.com/xuanhun/p/3641153.html,如需轉載請自行聯系原作者