我对 impress.js 源码的理解
源码看了两天,删掉了一些优化,和对 ipad 的支持,仅研究了其核心功能的实现,作以下记录。
HTML 结构如下:
<!doctype html> <html lang="zh-cn">
<head>
<meta charset="utf-8" />
<title>impress.js</title>
<link href="css/impress-demo.css" rel="stylesheet" />
</head> <body> <div id="impress"> <div class="step" data-x="1000" data-rotate-y="45" data-scale="3">第一幕</div>
<div class="step" data-z="1000" data-rotate-y="45" data-rotate-z="90" data-scale="2">第二幕</div>
<div class="step" data-x="0" data-z="1000" data-rotate-y="45" data-rotate-z="80" data-scale="1">第三幕</div>
<div id="overview" class="step" data-x="3000" data-y="1500" data-scale="10"></div> </div> <script src="js/impress.js"></script> </body>
</html>
在 HTML 中,每一张显示的幕布都有一个 step 的类,并且所有的 step 类都被包含在一个 id 为 impress 的容器(舞台)中。
而在每一个 step 中,利用 data 自定义每一个 step 的 translate ,rotate ,和 scale 。
最后一个 id 为 overview 的 div ,也同时是一个 step 类,用于在一张幕布上显示所有的演示元素,不是必需的。
impress.js 核心代码
注意,此代码经过我的大量删除,几乎没有经过优化,仅完成核心功能,便于对 impress.js 核心逻辑的理解。
 (function(document, window) {
     /**
      * 在需要的时候,为 CSS 属性添加当前浏览器能够识别的前缀
      * @param  prop  一定要记住,参数是一个字符串,所以传入的 CSS 属性一定要加引号
      * @return       返回当前浏览器能够识别的 CSS 属性
      */
     var pfx = (function() {
         var prefixes = "Moz Webkit O ms".split(" ");
         var style = document.createElement("dummy").style;
         var memory = {};
         return function(prop) {
             var uProp = prop.charAt(0).toUpperCase() + prop.slice(1);
             var props = (prop + " " + prefixes.join(uProp + " ") + uProp).split(" ");
             memory[prop] = null;
             for (var i in props) {
                 if (style[props[i]] !== undefined) {
                     memory[prop] = props[i];
                     break;
                 }
             }
             return memory[prop];
         }
     })();
     /**
      * 为指定的元素添加一组 CSS 样式
      * @param  ele    指定的元素
      * @param  props  一组 CSS 属性和值,JSON 的形式,属性名和属性值都要加引号
      * @return        返回指定的元素
      */
     var css = function(ele, props) {
         var key, pkey;
         for (key in props) {
             if (props.hasOwnProperty(key)) {
                 pkey = pfx(key);
                 if (pkey !== null) {
                     ele.style[pkey] = props[key];
                 }
             }
         }
         return ele;
     }
     /**
      * 将传入的参数转换为数值
      * @param  numeric  要转换为数值的参数
      * @param  fallback 传入的参数不能转换为数值时返回的值,可以省略,如果省略则返回 0
      * @return          返回一个数值或者 0
      */
     var toNumber = function(numeric, fallback) {
         return isNaN(numeric) ? fallback || 0 : Number(numeric);
     }
     /**
      * 设置 3D 转换元素的 translate 值
      * @param  t 位移值,以对象字面量的形式,属性值不需要带单位
      * @return   返回 "translate3d() "
      */
     var translate = function(t) {
         return "translate3d(" + t.x + "px," + t.y + "px," + t.z + "px) ";
     }
     /**
      * 设置 3D 转换元素的 rotate 值
      * @param  r 旋转值,以对象字面量的形式,属性值不需要带单位
      * @return   返回 "rotateX() rotateY() rotateZ() "
      */
     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;
     };
     // 设置 3D 转换元素的 scale 值
     var scale = function(s) {
         return "scale(" + s + ") ";
     }
     // 设置 3D 转换元素的 perspective 值
     var perspective = function(p) {
         return "perspective(" + p + "px) ";
     }
     /**
      * 计算缩放因子,并限定其最大最小值
      * @param  config 配置信息
      * @return        返回缩放因子
      */
     var computeWindowScale = function(config) {
         var hScale = window.innerHeight / config.height;
         var wScale = window.innerWidth / config.width;
         var 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;
     }
     /**
      * 自定义事件并立即触发
      * @param  el        触发事件的元素
      * @param  eventName 事件名
      * @param  detail    事件信息
      */
     var triggerEvent = function(el, eventName, detail) {
         var event = document.createEvent("CustomEvent");
         // 事件冒泡,并且可以取消冒泡
         event.initCustomEvent(eventName, true, true, detail);
         el.dispatchEvent(event);
     };
     // 通过 hash 值取得元素
     var getElementFromHash = function() {
         return document.getElementById(window.location.hash.replace(/^#\/?/, ""));
     };
     // 定义 empty 函数,只是为了书写方便
     var empty = function() {
         return false;
     };
     var body = document.body;
     // 定义一个 defaults 对象,保存着一些默认值
     var defaults = {
         width: 1024,
         height: 768,
         maxScale: 1,
         minScale: 0,
         perspective: 1000,
         transitionDuration: 1000
     };
     // 变量 roots ,保存着 impress 的实例
     var roots = {};
     var impress = window.impress = function(rootId) {
         rootId = rootId || "impress";
         // 保存所有 step 的 translate rotate scale 属性
         var stepsData = {};
         // 当前展示的 step
         var activeStep = null;
         // canvas 的当前状态
         var currentState = null;
         // 包含所有 step 的数组
         var steps = null;
         // 配置信息
         var config = null;
         // 浏览器窗口的缩放因子
         var windowScale = null;
         // Presentation 的根元素
         var root = document.getElementById(rootId);
         // 创建一个 div 元素,保存在变量 canvas 中
         // 注意这只是一个 dic ,只是名字好听而已
         var canvas = document.createElement("div");
         // 初始化状态为 false
         var initialized = false;
         // 这个变量关系到 hash 值的改变,
         var lastEntered = null;
         /**
          * 初始化函数
          * 引用 impress.js 之后单独调用
          */
         var init = function() {
             if (initialized) {
                 return;
             }
             // Presentation 的根元素的 dataset 属性
             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 中,将 canvas 放在根元素下
             var stepArr = Array.prototype.slice.call(root.childNodes);
             for (var i = 0; i < stepArr.length; i++) {
                 canvas.appendChild(stepArr[i]);
             }
             root.appendChild(canvas);
             // 设置 html body #impress 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);
             // 获取每一个 step ,调用 initStep() 函数初始化它们的样式
             steps = Array.prototype.slice.call(root.querySelectorAll(".step"));
             for (var i = 0; i < steps.length; i++) {
                 initStep(steps[i], i);
             }
             // 设置 canvas 的初始状态
             currentState = {
                 translate: {
                     x: 0,
                     y: 0,
                     z: 0
                 },
                 rotate: {
                     x: 0,
                     y: 0,
                     z: 0
                 },
                 scale: 1
             };
             // 更新初始化状态为 true
             initialized = true;
             // 自定义事件 impress:init 并触发
             triggerEvent(root, "impress:init", {
                 api: roots["impress-root-" + rootId]
             });
         };
         /**
          * 初始化 step 的样式
          * @param  el  当前 step 元素
          * @param  i 数字值
          */
         var initStep = function(el, i) {
             // 获取当前 step 的dataset 属性,保存在变量 data 中
             // 根据 data 的属性值,拿到 translate rotate scale 的完整属性,保存在变量 step 中
             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
                 };
             // 根据变量 step 中保存的数据,为当前 step 定义样式
             css(el, {
                 position: "absolute",
                 transform: "translate(-50%,-50%)" +
                     translate(step.translate) +
                     rotate(step.rotate) +
                     scale(step.scale),
                 transformStyle: "preserve-3d"
             });
             // 检测当前 step 是否有 id 属性,如果没有,则添加 id ,格式为 step-*
             if (!el.id) {
                 el.id = "step-" + (i + 1);
             }
             // 将当前 step 的相关数据保存到变量 stepsData 中。
             // 在 stepsData 这个对象中,属性名就是 "impress" + el.id ,属性值就是相对应的 translate rotate scale 属性
             stepsData["impress-" + el.id] = step;
         };
         // 定时器,用于改变 hash 值
         var stepEnterTimeout = null;
         /**
          * 自定义 impress:stepenter 事件并触发
          * @param  {[type]} step [description]
          */
         var onStepEnter = function(step) {
             if (lastEntered !== step) {
                 triggerEvent(step, "impress:stepenter");
                 lastEntered = step;
             }
         };
         /**
          * 自定义 impress:stepleave 事件并触发
          * @param  {[type]} step [description]
          */
         var onStepLeave = function(step) {
             if (lastEntered === step) {
                 triggerEvent(step, "impress:stepleave");
                 lastEntered = null;
             }
         };
         /**
          * 切换到下一张幕布的函数
          * @param  el       下一个 step 所在的元素
          * @param  duration 过渡时间
          * @return          [description]
          */
         var goto = function(el, duration) {
             window.scrollTo(0, 0);
             // 获取当前 step 的数据
             var step = stepsData["impress-" + el.id];
             // 根据下一个 step 的数据,计算 canvas 和 root 的目标状态,作出相应调整
             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
             };
             // 检测幕布之间的切换 scale 是变大还是变小,从而定义动画的延迟时间
             var zoomin = target.scale >= currentState.scale;
             duration = toNumber(duration, config.transitionDuration);
             var delay = (duration / 2);
             var targetScale = target.scale * windowScale;
             // 在 root 元素上调整 perspective 和 scale ,让每一张幕布看起来大小都一样
             // root 的调整是动画的一部分(二分之一)
             css(root, {
                 transform: perspective(config.perspective / targetScale) + scale(targetScale),
                 transitionDuration: duration + "ms",
                 transitionDelay: (zoomin ? delay : 0) + "ms"
             });
             // 在 canvas 元素上进行与当前幕布反方向的位移和旋转,保证当前总是正对着我们
             // canvas 的调整是动画的一部分(二份之二)
             css(canvas, {
                 transform: rotate(target.rotate, true) + translate(target.translate),
                 transitionDuration: duration + "ms",
                 transitionDelay: (zoomin ? 0 : delay) + "ms"
             });
             // 相关变量的更新
             currentState = target;
             activeStep = el;
             // 首先清除定时器,在下一张幕布进入的 duration + delay 毫秒之后,自定义一个 impress:stepenter 事件并触发
             // 这个事件触发之后会被 root 接收,用于改变 hash 值
             window.clearTimeout(stepEnterTimeout);
             stepEnterTimeout = window.setTimeout(function() {
                 onStepEnter(activeStep);
             }, duration + delay);
             return el;
         };
         // 定义切换幕布的 api
         var prev = function() {
             var prev = steps.indexOf(activeStep) - 1;
             prev = prev >= 0 ? steps[prev] : steps[steps.length - 1];
             return goto(prev);
         };
         var next = function() {
             var next = steps.indexOf(activeStep) + 1;
             next = next < steps.length ? steps[next] : steps[0];
             return goto(next);
         };
         // impress:init 事件被 root 接收,改变 hash 值
         root.addEventListener("impress:init", function() {
             // Last hash detected
             var lastHash = "";
             // 当 step 进入时,触发 impress:stepenter 事件,这个事件被 root 接收,获取进入的 step 的 id ,从而改变 hash 值为当前 step 的 id
             root.addEventListener("impress:stepenter", function(event) {
                 window.location.hash = lastHash = "#/" + event.target.id;
             }, false);
             // 初始化之后,展示第一张 step
             goto(getElementFromHash() || steps[0], 0);
         }, false);
         // 整个 impress() 函数的返回值,这样 impress().init() 函数才能在外部被调用
         return (roots["impress-root-" + rootId] = {
             init: init,
             goto: goto,
             next: next,
             prev: prev
         });
     }
 })(document, window);
 (function(document, window) {
     "use strict";
     // impress:init 事件触发之后执行
     // impress:init 事件在执行 init() 函数完成初始化之后自定义并且立即触发
     document.addEventListener("impress:init", function(event) {
         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);
         // 通过键盘事件进行 step 之间的切换
         document.addEventListener("keyup", function(event) {
             if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) {
                 return;
             }
             if (event.keyCode === 9 ||
                 (event.keyCode >= 32 && event.keyCode <= 34) ||
                 (event.keyCode >= 37 && event.keyCode <= 40)) {
                 switch (event.keyCode) {
                     case 33: // Page up
                     case 37: // Left
                     case 38: // Up
                         api.prev();
                         break;
                     case 9: // Tab
                     case 32: // Space
                     case 34: // Page down
                     case 39: // Right
                     case 40: // Down
                         api.next();
                         break;
                 }
                 event.preventDefault();
             }
         }, false);
     }, false);
 })(document, window);
 // 调用 impress().init()
 impress().init();
我个人对 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 没有半毛钱关系。
我对 impress.js 源码的理解的更多相关文章
- 深入理解unslider.js源码
		最近用到了一个挺好用的幻灯片插件,叫做unslider.js,就想看看怎么实现幻灯片功能,就看看源码,顺便自己也学习学习.看完之后收获很多,这里和大家分享一下. unslider.js 源码和使用教程 ... 
- underscore.js 源码
		underscore.js 源码 underscore]JavaScript 中如何判断两个元素是否 "相同" Why underscore 最近开始看 underscore.js ... 
- vue.js源码精析
		MVVM大比拼之vue.js源码精析 VUE 源码分析 简介 Vue 是 MVVM 框架中的新贵,如果我没记错的话作者应该毕业不久,现在在google.vue 如作者自己所说,在api设计上受到了很多 ... 
- 从template到DOM(Vue.js源码角度看内部运行机制)
		写在前面 这篇文章算是对最近写的一系列Vue.js源码的文章(https://github.com/answershuto/learnVue)的总结吧,在阅读源码的过程中也确实受益匪浅,希望自己的这些 ... 
- Vue.js源码——事件机制
		写在前面 因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出.文章的原地址:https://github.com/an ... 
- 从Vue.js源码角度再看数据绑定
		写在前面 因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出.文章的原地址:https://github.com/an ... 
- Underscore.js 源码学习笔记(下)
		上接 Underscore.js 源码学习笔记(上) === 756 行开始 函数部分. var executeBound = function(sourceFunc, boundFunc, cont ... 
- Underscore.js 源码学习笔记(上)
		版本 Underscore.js 1.9.1 一共 1693 行.注释我就删了,太长了… 整体是一个 (function() {...}()); 这样的东西,我们应该知道这是一个 IIFE(立即执行 ... 
- 2018-11-23 手工翻译Vue.js源码:尝试重命名标识符与文本
		续前文: 手工翻译Vue.js源码第一步:14个文件重命名 对core/instance/索引中的变量, 方法进行重命名如下(题图): import { 混入初始化 } from './初始化' im ... 
随机推荐
- U3D assetbundle加载与卸载的深入理解
			using UnityEngine; using System.Collections; using System; public class testLoadFromAB : MonoBehavio ... 
- Andorid-15k+的面试题。
			andorid开发也做了3年有余了,也面试很多加企业,借此机会分享一下,我们中遇到过的问题以及解决方案吧,希望能够对正在找工作的andoird程序员有一定的帮助. 特别献上整理过的50道面试题目 1. ... 
- Javascript跨域问题总结
			疯狂的JSONP 关于JSON与JSONP简单总结 window.name实现的跨域数据传输 JavaScript跨域总结与解决办法 flash跨域策略文件crossdomain.xml配置详解 
- 进程&信号&管道实践学习记录
			程序分析 exec1.c & exect2.c & exect3.c 程序代码 (以exect1.c为例,其他两个结构类似) #include <stdio.h> #inc ... 
- 【JVM】模板解释器--字节码的resolve过程
			1.背景 上文探讨了:[JVM]模板解释器--如何根据字节码生成汇编码? 本篇,我们来关注下字节码的resolve过程. 2.问题及准备工作 上文虽然探讨了字节码到汇编码的过程,但是: mov %ra ... 
- android之简易新闻客户端
			将一个新闻信息保存到一个XML文件中,并将放在服务器下.通过手机客户端来从服务器下载该文件并解析显示. news.xml <?xml version="1.0" encodi ... 
- 项目笔记---Windows Service调用Windows API问题
			概要 此文来自于最近一个“诡异”的Windows API调用发现Windows Service在调用某些Windows API的过程中失效,在经过漫长的Baidu,之后终于在StackOverFlow ... 
- WCF入门 (14)
			前言 上周去面试,跪了,这一年没什么长进,还是挺惭愧的. 得到的评语是:想的太多,做的太少. 做了一份面试题,最后一题是数据库的,写个查询.要查出Score有两次及两次以上超过79的Name和他的最高 ... 
- Entity Framework with nolock. 允许脏读
			public static List<T> ToListReadUncommitted<T>(this IQueryable<T> query) { using ( ... 
- 解决 pathForResource 返回 nil的问题
			点击(此处)折叠或打开 NSString* path = [[NSBundle mainBundle] pathForResource:@"sample" ofType:@&quo ... 
