impress.js 中文注釋
/**
* 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`)返回在
//上下文(`context`,指定元素或者整個文檔)中
//匹配的元素數組
var $$ = function (selector, context) {
context = context || document;
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 = {
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");
// 獲取並設置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` 用於平移和旋轉.
// 二者開始變換的延時時間也不相同
// (為了變換過程在視覺效果上看起來自然、美觀),
// 所以我們需要知道二者的行為是否都結束了.
css(root, {
// 為了保證在不同的縮放時使透視效果看起來是一樣的
// 我們同時需要縮放透視(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),
transitionDuration: duration + "ms",
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);
}, false);
// 添加hash變化支持.
root.addEventListener("impress:init", function () {
// 被探測到的上一個hash
var lastHash = "";
// 使用`#/step-id` 替代 `#step-id`
//以阻止瀏覽器滾動到該id的元素
// 必須在動畫結束之后設置hash,
// 因為在google瀏覽器會導致變換的動畫延遲.
// 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已經被更新,
// (就是上面幾行代碼),
//所以hashchange事件被觸發,這導致同一step元素會被再次調用 `goto`
//
// 為了避免這一情況,我們存儲上次的hash值然后做比較
if (window.location.hash !== lastHash) {
goto(getElementFromHash());
}
}, false);
//
// 通過url或者選擇文檔中的第一個step開始播放
goto(getElementFromHash() || steps[0], 0);
}, false);
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提供的接口
//
//
// 在將來,我考慮將這部分代碼放到獨立的文件中
// 以插件的形式的存在.
(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;
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) {
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);
// 處理在當前演示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();
event.preventDefault();
}
}, false);
// 處理在stepi上的點擊
document.addEventListener("click", function (event) {
var target = event.target;
//查找最近的沒有被激活的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();
} elseif (x > window.innerWidth - width) {
result = api.next();
}
if (result) {
event.preventDefault();
}
}
}, false);
// 瀏覽器窗口改變時,重新展示當前step
window.addEventListener("resize", throttle(function () {
// 強制再次激活當前step
api.goto(document.querySelector(".step.active"), 500);
}, 250), false);
}, false);
})(document, window);
// 到此為止!
//
// 謝謝你把它全部讀完.
//即使你是直接滾到到此處,仍然感謝.
//
// 在編寫impress.js時,我學到了很多。希望這些代碼和注釋能對你有所幫助。
歡迎訪問玄魂的博客
ps:對此文章或者安全、安全編程感興趣的讀者,可以加qq群:Hacking:303242737;Hacking-2群:147098303;Hacking-3群:31371755;hacking-4群:201891680;Hacking-5群:316885176
