我对 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>
在 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();
我个人对 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 没有半毛钱关系。