我对 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         width: 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                 width: 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 没有半毛钱关系。 

 

posted @ 2016-09-01 15:59  Aaron_Xiao  阅读(341)  评论(0编辑  收藏  举报