天天看點

impress.js 中文注釋 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`)傳回在

    //比對的元素數組

    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,如需轉載請自行聯系原作者

繼續閱讀