• 我对 impress.js 源码的理解


    源码看了两天,删掉了一些优化,和对 ipad 的支持,仅研究了其核心功能的实现,作以下记录。

    HTML 结构如下:

     1 <!doctype html>
     2 
     3 <html lang="zh-cn">
     4 <head>
     5     <meta charset="utf-8" />
     6     <title>impress.js</title>
     7     <link href="css/impress-demo.css" rel="stylesheet" />
     8 </head>
     9 
    10 <body>
    11 
    12 <div id="impress">
    13 
    14     <div class="step" data-x="1000" data-rotate-y="45" data-scale="3">第一幕</div> 
    15     <div class="step"  data-z="1000" data-rotate-y="45" data-rotate-z="90" data-scale="2">第二幕</div>
    16     <div class="step" data-x="0" data-z="1000" data-rotate-y="45" data-rotate-z="80" data-scale="1">第三幕</div>
    17     <div id="overview" class="step" data-x="3000" data-y="1500" data-scale="10"></div>
    18 
    19 </div>
    20 
    21 <script src="js/impress.js"></script>
    22 
    23 </body>
    24 </html>
    View Code

    在 HTML 中,每一张显示的幕布都有一个 step 的类,并且所有的 step 类都被包含在一个 id 为 impress 的容器(舞台)中。

    而在每一个 step 中,利用 data 自定义每一个 step 的 translate ,rotate ,和 scale 。

    最后一个 id 为 overview 的 div ,也同时是一个 step 类,用于在一张幕布上显示所有的演示元素,不是必需的。

    impress.js 核心代码

    注意,此代码经过我的大量删除,几乎没有经过优化,仅完成核心功能,便于对 impress.js 核心逻辑的理解。

      1 (function(document, window) {
      2 
      3     /**
      4      * 在需要的时候,为 CSS 属性添加当前浏览器能够识别的前缀
      5      * @param  prop  一定要记住,参数是一个字符串,所以传入的 CSS 属性一定要加引号
      6      * @return       返回当前浏览器能够识别的 CSS 属性
      7      */
      8     var pfx = (function() {
      9         var prefixes = "Moz Webkit O ms".split(" ");
     10         var style = document.createElement("dummy").style;
     11         var memory = {};
     12 
     13         return function(prop) {
     14             var uProp = prop.charAt(0).toUpperCase() + prop.slice(1);
     15             var props = (prop + " " + prefixes.join(uProp + " ") + uProp).split(" ");
     16 
     17             memory[prop] = null;
     18             for (var i in props) {
     19                 if (style[props[i]] !== undefined) {
     20                     memory[prop] = props[i];
     21                     break;
     22                 }
     23             }
     24             return memory[prop];
     25         }
     26     })();
     27 
     28     /**
     29      * 为指定的元素添加一组 CSS 样式
     30      * @param  ele    指定的元素
     31      * @param  props  一组 CSS 属性和值,JSON 的形式,属性名和属性值都要加引号
     32      * @return        返回指定的元素
     33      */
     34     var css = function(ele, props) {
     35         var key, pkey;
     36         for (key in props) {
     37             if (props.hasOwnProperty(key)) {
     38                 pkey = pfx(key);
     39                 if (pkey !== null) {
     40                     ele.style[pkey] = props[key];
     41                 }
     42             }
     43         }
     44         return ele;
     45     }
     46 
     47     /**
     48      * 将传入的参数转换为数值
     49      * @param  numeric  要转换为数值的参数
     50      * @param  fallback 传入的参数不能转换为数值时返回的值,可以省略,如果省略则返回 0
     51      * @return          返回一个数值或者 0
     52      */
     53     var toNumber = function(numeric, fallback) {
     54         return isNaN(numeric) ? fallback || 0 : Number(numeric);
     55     }
     56 
     57     /**
     58      * 设置 3D 转换元素的 translate 值
     59      * @param  t 位移值,以对象字面量的形式,属性值不需要带单位
     60      * @return   返回 "translate3d() "
     61      */
     62     var translate = function(t) {
     63         return "translate3d(" + t.x + "px," + t.y + "px," + t.z + "px) ";
     64     }
     65 
     66     /**
     67      * 设置 3D 转换元素的 rotate 值
     68      * @param  r 旋转值,以对象字面量的形式,属性值不需要带单位
     69      * @return   返回 "rotateX() rotateY() rotateZ() " 
     70      */
     71     var rotate = function(r, revert) {
     72         var rX = " rotateX(" + r.x + "deg) ",
     73             rY = " rotateY(" + r.y + "deg) ",
     74             rZ = " rotateZ(" + r.z + "deg) ";
     75 
     76         return revert ? rZ + rY + rX : rX + rY + rZ;
     77     };
     78 
     79     // 设置 3D 转换元素的 scale 值
     80     var scale = function(s) {
     81         return "scale(" + s + ") ";
     82     }
     83 
     84     // 设置 3D 转换元素的 perspective 值
     85     var perspective = function(p) {
     86         return "perspective(" + p + "px) ";
     87     }
     88 
     89     /**
     90      * 计算缩放因子,并限定其最大最小值
     91      * @param  config 配置信息
     92      * @return        返回缩放因子
     93      */
     94     var computeWindowScale = function(config) {
     95         var hScale = window.innerHeight / config.height;
     96         var wScale = window.innerWidth / config.width;
     97         var scale = hScale > wScale ? wScale : hScale;
     98         if (config.maxScale && scale > config.maxScale) {
     99             scale = config.maxScale;
    100         }
    101         if (config.minScale && scale < config.minScale) {
    102             scale = config.minScale;
    103         }
    104         return scale;
    105     }
    106 
    107     /**
    108      * 自定义事件并立即触发
    109      * @param  el        触发事件的元素
    110      * @param  eventName 事件名
    111      * @param  detail    事件信息
    112      */
    113     var triggerEvent = function(el, eventName, detail) {
    114         var event = document.createEvent("CustomEvent");
    115         // 事件冒泡,并且可以取消冒泡
    116         event.initCustomEvent(eventName, true, true, detail);
    117         el.dispatchEvent(event);
    118     };
    119 
    120     // 通过 hash 值取得元素
    121     var getElementFromHash = function() {
    122         return document.getElementById(window.location.hash.replace(/^#/?/, ""));
    123     };
    124 
    125     // 定义 empty 函数,只是为了书写方便
    126     var empty = function() {
    127         return false;
    128     };
    129 
    130     var body = document.body;
    131 
    132     // 定义一个 defaults 对象,保存着一些默认值
    133     var defaults = {
    134          1024,
    135         height: 768,
    136         maxScale: 1,
    137         minScale: 0,
    138         perspective: 1000,
    139         transitionDuration: 1000
    140     };
    141 
    142     // 变量 roots ,保存着 impress 的实例
    143     var roots = {};
    144 
    145 
    146     var impress = window.impress = function(rootId) {
    147         rootId = rootId || "impress";
    148 
    149         // 保存所有 step 的 translate rotate scale 属性
    150         var stepsData = {};
    151 
    152         // 当前展示的 step
    153         var activeStep = null;
    154 
    155         // canvas 的当前状态
    156         var currentState = null;
    157 
    158         // 包含所有 step 的数组
    159         var steps = null;
    160 
    161         // 配置信息
    162         var config = null;
    163 
    164         // 浏览器窗口的缩放因子
    165         var windowScale = null;
    166 
    167         // Presentation 的根元素
    168         var root = document.getElementById(rootId);
    169 
    170         // 创建一个 div 元素,保存在变量 canvas 中
    171         // 注意这只是一个 dic ,只是名字好听而已
    172         var canvas = document.createElement("div");
    173 
    174         // 初始化状态为 false
    175         var initialized = false;
    176 
    177         // 这个变量关系到 hash 值的改变,
    178         var lastEntered = null;
    179 
    180         /**
    181          * 初始化函数
    182          * 引用 impress.js 之后单独调用
    183          */
    184         var init = function() {
    185             if (initialized) {
    186                 return;
    187             }
    188 
    189             // Presentation 的根元素的 dataset 属性
    190             var rootData = root.dataset;
    191 
    192             // 定义配置信息,如果在根元素上有定义相关属性,则取根元素上定义的值,如果没有在根元素上定义,则取默认值
    193             config = {
    194                  toNumber(rootData.width, defaults.width),
    195                 height: toNumber(rootData.height, defaults.height),
    196                 maxScale: toNumber(rootData.maxScale, defaults.maxScale),
    197                 minScale: toNumber(rootData.minScale, defaults.minScale),
    198                 perspective: toNumber(rootData.perspective, defaults.perspective),
    199                 transitionDuration: toNumber(
    200                     rootData.transitionDuration, defaults.transitionDuration
    201                 )
    202             };
    203 
    204             // 传入配置信息,计算浏览器窗口的缩放因子
    205             windowScale = computeWindowScale(config);
    206 
    207             // 将所有的 step 都放在 canvas 中,将 canvas 放在根元素下
    208             var stepArr = Array.prototype.slice.call(root.childNodes);
    209             for (var i = 0; i < stepArr.length; i++) {
    210                 canvas.appendChild(stepArr[i]);
    211             }
    212             root.appendChild(canvas);
    213 
    214             // 设置 html body #impress canvas 的初始样式
    215             document.documentElement.style.height = "100%";
    216 
    217             css(body, {
    218                 height: "100%",
    219                 overflow: "hidden"
    220             });
    221 
    222             var rootStyles = {
    223                 position: "absolute",
    224                 transformOrigin: "top left",
    225                 transition: "all 0s ease-in-out",
    226                 transformStyle: "preserve-3d"
    227             };
    228 
    229             css(root, rootStyles);
    230             css(root, {
    231                 top: "50%",
    232                 left: "50%",
    233                 transform: perspective(config.perspective / windowScale) + scale(windowScale)
    234             });
    235             css(canvas, rootStyles);
    236 
    237             // 获取每一个 step ,调用 initStep() 函数初始化它们的样式
    238             steps = Array.prototype.slice.call(root.querySelectorAll(".step"));
    239             for (var i = 0; i < steps.length; i++) {
    240                 initStep(steps[i], i);
    241             }
    242 
    243             // 设置 canvas 的初始状态
    244             currentState = {
    245                 translate: {
    246                     x: 0,
    247                     y: 0,
    248                     z: 0
    249                 },
    250                 rotate: {
    251                     x: 0,
    252                     y: 0,
    253                     z: 0
    254                 },
    255                 scale: 1
    256             };
    257 
    258             // 更新初始化状态为 true
    259             initialized = true;
    260 
    261             // 自定义事件 impress:init 并触发
    262             triggerEvent(root, "impress:init", {
    263                 api: roots["impress-root-" + rootId]
    264             });
    265         };
    266 
    267         /**
    268          * 初始化 step 的样式
    269          * @param  el  当前 step 元素
    270          * @param  i 数字值    
    271          */
    272         var initStep = function(el, i) {
    273             // 获取当前 step 的dataset 属性,保存在变量 data 中
    274             // 根据 data 的属性值,拿到 translate rotate scale 的完整属性,保存在变量 step 中
    275             var data = el.dataset,
    276                 step = {
    277                     translate: {
    278                         x: toNumber(data.x),
    279                         y: toNumber(data.y),
    280                         z: toNumber(data.z)
    281                     },
    282                     rotate: {
    283                         x: toNumber(data.rotateX),
    284                         y: toNumber(data.rotateY),
    285                         z: toNumber(data.rotateZ || data.rotate)
    286                     },
    287                     scale: toNumber(data.scale, 1),
    288                     el: el
    289                 };
    290 
    291             // 根据变量 step 中保存的数据,为当前 step 定义样式
    292             css(el, {
    293                 position: "absolute",
    294                 transform: "translate(-50%,-50%)" +
    295                     translate(step.translate) +
    296                     rotate(step.rotate) +
    297                     scale(step.scale),
    298                 transformStyle: "preserve-3d"
    299             });
    300 
    301             // 检测当前 step 是否有 id 属性,如果没有,则添加 id ,格式为 step-* 
    302             if (!el.id) {
    303                 el.id = "step-" + (i + 1);
    304             }
    305 
    306             // 将当前 step 的相关数据保存到变量 stepsData 中。
    307             // 在 stepsData 这个对象中,属性名就是 "impress" + el.id ,属性值就是相对应的 translate rotate scale 属性
    308             stepsData["impress-" + el.id] = step;
    309         };
    310 
    311         // 定时器,用于改变 hash 值
    312         var stepEnterTimeout = null;
    313 
    314         /**
    315          * 自定义 impress:stepenter 事件并触发
    316          * @param  {[type]} step [description]
    317          */
    318         var onStepEnter = function(step) {
    319             if (lastEntered !== step) {
    320                 triggerEvent(step, "impress:stepenter");
    321                 lastEntered = step;
    322             }
    323         };
    324 
    325         /**
    326          * 自定义 impress:stepleave 事件并触发
    327          * @param  {[type]} step [description]
    328          */
    329         var onStepLeave = function(step) {
    330             if (lastEntered === step) {
    331                 triggerEvent(step, "impress:stepleave");
    332                 lastEntered = null;
    333             }
    334         };
    335 
    336 
    337         /**
    338          * 切换到下一张幕布的函数
    339          * @param  el       下一个 step 所在的元素
    340          * @param  duration 过渡时间
    341          * @return          [description]
    342          */
    343         var goto = function(el, duration) {
    344 
    345             window.scrollTo(0, 0);
    346 
    347             // 获取当前 step 的数据
    348             var step = stepsData["impress-" + el.id];
    349 
    350             // 根据下一个 step 的数据,计算 canvas 和 root 的目标状态,作出相应调整
    351             var target = {
    352                 rotate: {
    353                     x: -step.rotate.x,
    354                     y: -step.rotate.y,
    355                     z: -step.rotate.z
    356                 },
    357                 translate: {
    358                     x: -step.translate.x,
    359                     y: -step.translate.y,
    360                     z: -step.translate.z
    361                 },
    362                 scale: 1 / step.scale
    363             };
    364 
    365             // 检测幕布之间的切换 scale 是变大还是变小,从而定义动画的延迟时间
    366             var zoomin = target.scale >= currentState.scale;
    367 
    368             duration = toNumber(duration, config.transitionDuration);
    369             var delay = (duration / 2);
    370 
    371             var targetScale = target.scale * windowScale;
    372 
    373             // 在 root 元素上调整 perspective 和 scale ,让每一张幕布看起来大小都一样
    374             // root 的调整是动画的一部分(二分之一)
    375             css(root, {
    376                 transform: perspective(config.perspective / targetScale) + scale(targetScale),
    377                 transitionDuration: duration + "ms",
    378                 transitionDelay: (zoomin ? delay : 0) + "ms"
    379             });
    380             // 在 canvas 元素上进行与当前幕布反方向的位移和旋转,保证当前总是正对着我们
    381             // canvas 的调整是动画的一部分(二份之二)
    382             css(canvas, {
    383                 transform: rotate(target.rotate, true) + translate(target.translate),
    384                 transitionDuration: duration + "ms",
    385                 transitionDelay: (zoomin ? 0 : delay) + "ms"
    386             });
    387 
    388             // 相关变量的更新
    389             currentState = target;
    390             activeStep = el;
    391 
    392             // 首先清除定时器,在下一张幕布进入的 duration + delay 毫秒之后,自定义一个 impress:stepenter 事件并触发
    393             // 这个事件触发之后会被 root 接收,用于改变 hash 值
    394             window.clearTimeout(stepEnterTimeout);
    395             stepEnterTimeout = window.setTimeout(function() {
    396                 onStepEnter(activeStep);
    397             }, duration + delay);
    398 
    399             return el;
    400         };
    401 
    402         // 定义切换幕布的 api
    403         var prev = function() {
    404             var prev = steps.indexOf(activeStep) - 1;
    405             prev = prev >= 0 ? steps[prev] : steps[steps.length - 1];
    406 
    407             return goto(prev);
    408         };
    409 
    410         var next = function() {
    411             var next = steps.indexOf(activeStep) + 1;
    412             next = next < steps.length ? steps[next] : steps[0];
    413             return goto(next);
    414         };
    415 
    416         // impress:init 事件被 root 接收,改变 hash 值
    417         root.addEventListener("impress:init", function() {
    418 
    419             // Last hash detected
    420             var lastHash = "";
    421 
    422 
    423             // 当 step 进入时,触发 impress:stepenter 事件,这个事件被 root 接收,获取进入的 step 的 id ,从而改变 hash 值为当前 step 的 id
    424             root.addEventListener("impress:stepenter", function(event) {
    425                 window.location.hash = lastHash = "#/" + event.target.id;
    426             }, false);
    427 
    428             // 初始化之后,展示第一张 step
    429             goto(getElementFromHash() || steps[0], 0);
    430         }, false);
    431 
    432         // 整个 impress() 函数的返回值,这样 impress().init() 函数才能在外部被调用
    433         return (roots["impress-root-" + rootId] = {
    434             init: init,
    435             goto: goto,
    436             next: next,
    437             prev: prev
    438         });
    439     }
    440 
    441 
    442 })(document, window);
    443 
    444 
    445 (function(document, window) {
    446     "use strict";
    447 
    448     // impress:init 事件触发之后执行
    449     // impress:init 事件在执行 init() 函数完成初始化之后自定义并且立即触发
    450     document.addEventListener("impress:init", function(event) {
    451 
    452 
    453         var api = event.detail.api;
    454 
    455         // 阻止键位的默认行为
    456         // 默认情况下,按下向下方向键会导致页面滚动,取消这个默认行为
    457         document.addEventListener("keydown", function(event) {
    458             if (event.keyCode === 9 ||
    459                 (event.keyCode >= 32 && event.keyCode <= 34) ||
    460                 (event.keyCode >= 37 && event.keyCode <= 40)) {
    461                 event.preventDefault();
    462             }
    463         }, false);
    464 
    465         // 通过键盘事件进行 step 之间的切换
    466         document.addEventListener("keyup", function(event) {
    467 
    468             if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) {
    469                 return;
    470             }
    471 
    472             if (event.keyCode === 9 ||
    473                 (event.keyCode >= 32 && event.keyCode <= 34) ||
    474                 (event.keyCode >= 37 && event.keyCode <= 40)) {
    475                 switch (event.keyCode) {
    476                     case 33: // Page up
    477                     case 37: // Left
    478                     case 38: // Up
    479                         api.prev();
    480                         break;
    481                     case 9: // Tab
    482                     case 32: // Space
    483                     case 34: // Page down
    484                     case 39: // Right
    485                     case 40: // Down
    486                         api.next();
    487                         break;
    488                 }
    489 
    490                 event.preventDefault();
    491             }
    492         }, false);
    493 
    494     }, false);
    495 
    496 })(document, window);
    497 
    498 // 调用 impress().init()
    499 impress().init();
    View Code

    我个人对 impress.js 核心思想的理解:

    【1】在 HTML 中,通过 data 自定义 每个 step 的 translate ,rotate ,和 scale ;调用 impress().init() 函数,这个 函数会调用 initStep() 函数,读取 step 的 data 数据,为 每一个 step 添加 相应的 样式。

    【2】当 切换 step 的时候,比如进入视窗的这个 step translateX(500px) ,如果不作调整,那么它会偏离屏幕中心,所以 impress.js 的处理方法是,给所有的 step 添加一个包裹层,这个包裹层反向移动,也就是 translateX(-500px) ,从而让当前 step 居中,而包裹层移动的过程,就是实质上的动画。

    【3】包裹层实际上只是一个 div ,但是保存在一个 叫 canvas 的变量中,所以只是名字好听,跟真正意义上的 canvas 没有半毛钱关系。 

  • 相关阅读:
    【VS开发】CListCtrl控件使用方法总结
    【VS开发】CListCtrl控件使用方法总结
    【VS开发】CTabView多页卡界面
    【VS开发】CTabView多页卡界面
    【VS开发】关于在CFormView中实现CListCtrl控件的注意事项
    【VS开发】关于在CFormView中实现CListCtrl控件的注意事项
    【VS开发】list控件的InsertColumn方法出错
    【VS开发】list控件的InsertColumn方法出错
    【VS开发】CListCtrl控件使用
    【VS开发】CListCtrl控件使用
  • 原文地址:https://www.cnblogs.com/cc156676/p/5830233.html
Copyright © 2020-2023  润新知