impress.js 源码解析系列
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了impress.js 源码解析系列相关的知识,希望对你有一定的参考价值。
1 /** 2 * [impress description] 定义 impress 函数 3 * @param {[type]} rootId [description] 4 * @return {[type]} [description] 5 */ 6 var impress = window.impress = function( rootId ) { 7 // 如果浏览器不支持,则退出 8 if ( !impressSupported ) { 9 return { 10 init: empty, 11 goto: empty, 12 prev: empty, 13 next: empty 14 }; 15 } 16 // rootId , 应该是整个画布所在元素的 id ,可以传入参数,如果不传入参数,则默认取得 impress 这个 id 17 // 所以在 html 文件中,画布所在的元素应该设置为 impress 18 rootId = rootId || "impress"; 19 20 // If given root is already initialized just return the API 21 if ( roots[ "impress-root-" + rootId ] ) { 22 return roots[ "impress-root-" + rootId ]; 23 } 24 25 // 变量 stepsData ,保存每一幕的数据 26 var stepsData = {}; 27 28 // 变量 activeStep ,保存当前显示的这一幕的元素 29 var activeStep = null; 30 31 // 变量 currentState ,保存演示的当前位置 (position, rotation and scale) 32 var currentState = null; 33 34 // 变量 steps ,保存 step 组成的数组 35 var steps = null; 36 37 // 变量 config ,初始化配置 38 var config = null; 39 40 // 变量 windowScale ,保存浏览器窗口的缩放因子,初始为 null 41 var windowScale = null; 42 43 // 变量 root ,保存画布的根元素,一般是 id 为 impress 的那个元素 44 var root = byId( rootId ); 45 // 定义 canvas 46 var canvas = document.createElement( "div" ); 47 // 变量 initialized ,初始化为 false 48 var initialized = false; 49 50 // STEP EVENTS 51 // 52 // There are currently two step events triggered by impress.js 53 // `impress:stepenter` is triggered when the step is shown on the 54 // screen (the transition from the previous one is finished) and 55 // `impress:stepleave` is triggered when the step is left (the 56 // transition to next step just starts). 57 58 // 变量 lastEntered ,初始化为 null 59 var lastEntered = null; 60 61 // `onStepEnter` is called whenever the step element is entered 62 // but the event is triggered only if the step is different than 63 // last entered step. 64 65 /** 66 * [onStepEnter description] 给每一幕进入时绑定事件 67 * @param {[type]} step 事件挂载的元素 68 * @return {[type]} 69 */ 70 var onStepEnter = function( step ) { 71 // 由于 lastEntered = null ,当然满足这个条件,所以继续执行 72 if ( lastEntered !== step ) { 73 // 绑定事件,事件名为 impress:stepenter , 挂载在 step 元素上 74 triggerEvent( step, "impress:stepenter" ); 75 // 更新 lastEntered = step 76 lastEntered = step; 77 } 78 }; 79 80 /** 81 * [onStepLeave description] 给每一幕离开时绑定事件,只会在刚进入的这一幕离开时触发 82 * @param {[type]} step 事件挂载的元素 83 * @return {[type]} 84 */ 85 var onStepLeave = function( step ) { 86 // 由于幕布进入时 lastEntered 已经更新,所以满足这个条件,继续执行 87 if ( lastEntered === step ) { 88 // 绑定事件,事件名为 impress:stepleave ,挂载在 step 元素上 89 triggerEvent( step, "impress:stepleave" ); 90 // 更新 lastEntered = null 91 lastEntered = null; 92 } 93 }; 94 95 // `initStep` initializes given step element by reading data from its 96 // data attributes and setting correct styles. 97 /** 98 * [initStep description] 通过元素上的 dataset 属性,定义元素的样式 99 * @param {[type]} el 元素 100 * @param {[type]} idx [description] 101 * @return {[type]} [description] 102 */ 103 var initStep = function( el, idx ) { 104 // 变量 data ,保存元素的 dataset 属性,是一个对象,包含一些名值对 105 var data = el.dataset, 106 // 变量 step ,保存当前幕布的一些样式属性 107 step = { 108 translate: { 109 x: toNumber( data.x ), 110 y: toNumber( data.y ), 111 z: toNumber( data.z ) 112 }, 113 rotate: { 114 x: toNumber( data.rotateX ), 115 y: toNumber( data.rotateY ), 116 z: toNumber( data.rotateZ || data.rotate ) 117 }, 118 scale: toNumber( data.scale, 1 ), 119 el: el 120 }; 121 122 123 // 如果 step 元素没有 id 属性,则为它们添加 id 124 if ( !el.id ) { 125 el.id = "step-" + ( idx + 1 ); 126 } 127 // stepsData 保存着每一幕的数据,找到给定 id 的这一幕,赋值为 step 128 stepsData[ "impress-" + el.id ] = step; 129 130 // 定义 css 样式 131 css( el, { 132 position: "absolute", 133 transform: "translate(-50%,-50%)" + 134 translate( step.translate ) + 135 rotate( step.rotate ) + 136 scale( step.scale ), 137 transformStyle: "preserve-3d" 138 } ); 139 }; 140 141 142 143 144 145 146 /** 147 * [init description] 初始化 148 * @return {[type]} [description] 149 */ 150 var init = function() { 151 // 如果已经初始化,则直接返回 152 if ( initialized ) { return; } 153 154 // 针对移动设备 155 // 作者有提到 ipad ,如果不指定这些属性,显示可能有问题 156 // 如果 DOM 中存在 name="viewport" 的 meta 元素,则取得这个元素,定义它的 content 属性 157 // 如果 DOM 中不存在 这个元素,则创建一个 meta 元素,定义它的 content 属性,定义它的 name 属性,然后把这个元素添加到 head 下 158 var meta = $( "meta[name=‘viewport‘]" ) || document.createElement( "meta" ); 159 meta.content = "width=device-width, minimum-scale=1, maximum-scale=1, user-scalable=no"; 160 if ( meta.parentNode !== document.head ) { 161 meta.name = "viewport"; 162 document.head.appendChild( meta ); 163 } 164 165 // root 元素是 id 为 impress 的元素 166 var rootData = root.dataset; 167 // 定义 config 对象 168 // 这里的 toNumber函数传入了两个参数,其意思是,如果第一个参数能够被转换为数值,则将其转换为数值并返回, 169 // 如果第一个参数不能转换为数值,而返回第二个参数,第二个参数的相关信息已经提前设定好 170 config = { 171 width: toNumber( rootData.width, defaults.width ), 172 height: toNumber( rootData.height, defaults.height ), 173 maxScale: toNumber( rootData.maxScale, defaults.maxScale ), 174 minScale: toNumber( rootData.minScale, defaults.minScale ), 175 perspective: toNumber( rootData.perspective, defaults.perspective ), 176 transitionDuration: toNumber( 177 rootData.transitionDuration, defaults.transitionDuration 178 ) 179 }; 180 181 // 调用 computeWindowScale() 函数计算缩放因子,保存在变量 windowScale 中 182 windowScale = computeWindowScale( config ); 183 184 // 把所有的 step 都放在 canvas 中,把 canvas 放在 impress 元素中 185 arrayify( root.childNodes ).forEach( function( el ) { 186 canvas.appendChild( el ); 187 } ); 188 root.appendChild( canvas ); 189 190 // 定义初始样式 191 document.documentElement.style.height = "100%"; 192 193 css( body, { 194 height: "100%", 195 overflow: "hidden" 196 } ); 197 198 var rootStyles = { 199 position: "absolute", 200 transformOrigin: "top left", 201 transition: "all 0s ease-in-out", 202 transformStyle: "preserve-3d" 203 }; 204 205 css( root, rootStyles ); 206 css( root, { 207 top: "50%", 208 left: "50%", 209 transform: perspective( config.perspective / windowScale ) + scale( windowScale ) 210 } ); 211 css( canvas, rootStyles ); 212 213 body.classList.remove( "impress-disabled" ); 214 body.classList.add( "impress-enabled" ); 215 216 // 取得 impress 下的所有 step ,并初始化 217 steps = $$( ".step", root ); 218 steps.forEach( initStep ); 219 220 // 设置 canvas 的初始状态 221 currentState = { 222 translate: { x: 0, y: 0, z: 0 }, 223 rotate: { x: 0, y: 0, z: 0 }, 224 scale: 1 225 }; 226 227 // 初始化之后,更新 initialized 的值 228 initialized = true; 229 230 // 给 root 添加事件 231 triggerEvent( root, "impress:init", { api: roots[ "impress-root-" + rootId ] } ); 232 }; 233 234 /** 235 * [getStep description] 获取某个 step 236 * @param step 可以是数值,也可以是 step 的 id 237 * @return 如果能够获取某个 step ,则返回这个 step 238 */ 239 var getStep = function( step ) { 240 if ( typeof step === "number" ) { 241 step = step < 0 ? steps[ steps.length + step ] : steps[ step ]; 242 } else if ( typeof step === "string" ) { 243 step = byId( step ); 244 } 245 return ( step && step.id && stepsData[ "impress-" + step.id ] ) ? step : null; 246 }; 247 248 // "impress:stepenter" 事件的超时记录 249 var stepEnterTimeout = null; 250 251 // `goto` API function that moves to step given with `el` parameter 252 // (by index, id or element), with a transition `duration` optionally 253 // given as second parameter. 254 var goto = function( el, duration ) { 255 256 if ( !initialized || !( el = getStep( el ) ) ) { 257 258 // 如果还没有初始化,或者 el 不是一个 step ,则返回 false 259 return false; 260 } 261 262 // Sometimes it‘s possible to trigger focus on first link with some keyboard action. 263 // Browser in such a case tries to scroll the page to make this element visible 264 // (even that body overflow is set to hidden) and it breaks our careful positioning. 265 // 266 // So, as a lousy (and lazy) workaround we will make the page scroll back to the top 267 // whenever slide is selected 268 // 269 // If you are reading this and know any better way to handle it, I‘ll be glad to hear 270 // about it! 271 window.scrollTo( 0, 0 ); 272 273 var step = stepsData[ "impress-" + el.id ]; 274 275 if ( activeStep ) { 276 activeStep.classList.remove( "active" ); 277 body.classList.remove( "impress-on-" + activeStep.id ); 278 } 279 el.classList.add( "active" ); 280 281 body.classList.add( "impress-on-" + el.id ); 282 283 // Compute target state of the canvas based on given step 284 var target = { 285 rotate: { 286 x: -step.rotate.x, 287 y: -step.rotate.y, 288 z: -step.rotate.z 289 }, 290 translate: { 291 x: -step.translate.x, 292 y: -step.translate.y, 293 z: -step.translate.z 294 }, 295 scale: 1 / step.scale 296 }; 297 298 // Check if the transition is zooming in or not. 299 // 300 // This information is used to alter the transition style: 301 // when we are zooming in - we start with move and rotate transition 302 // and the scaling is delayed, but when we are zooming out we start 303 // with scaling down and move and rotation are delayed. 304 var zoomin = target.scale >= currentState.scale; 305 306 duration = toNumber( duration, config.transitionDuration ); 307 var delay = ( duration / 2 ); 308 309 // If the same step is re-selected, force computing window scaling, 310 // because it is likely to be caused by window resize 311 if ( el === activeStep ) { 312 windowScale = computeWindowScale( config ); 313 } 314 315 var targetScale = target.scale * windowScale; 316 317 // Trigger leave of currently active element (if it‘s not the same step again) 318 if ( activeStep && activeStep !== el ) { 319 onStepLeave( activeStep ); 320 } 321 322 // Now we alter transforms of `root` and `canvas` to trigger transitions. 323 // 324 // And here is why there are two elements: `root` and `canvas` - they are 325 // being animated separately: 326 // `root` is used for scaling and `canvas` for translate and rotations. 327 // Transitions on them are triggered with different delays (to make 328 // visually nice and ‘natural‘ looking transitions), so we need to know 329 // that both of them are finished. 330 css( root, { 331 332 // To keep the perspective look similar for different scales 333 // we need to ‘scale‘ the perspective, too 334 transform: perspective( config.perspective / targetScale ) + scale( targetScale ), 335 transitionDuration: duration + "ms", 336 transitionDelay: ( zoomin ? delay : 0 ) + "ms" 337 } ); 338 339 css( canvas, { 340 transform: rotate( target.rotate, true ) + translate( target.translate ), 341 transitionDuration: duration + "ms", 342 transitionDelay: ( zoomin ? 0 : delay ) + "ms" 343 } ); 344 345 // Here is a tricky part... 346 // 347 // If there is no change in scale or no change in rotation and translation, it means 348 // there was actually no delay - because there was no transition on `root` or `canvas` 349 // elements. We want to trigger `impress:stepenter` event in the correct moment, so 350 // here we compare the current and target values to check if delay should be taken into 351 // account. 352 // 353 // I know that this `if` statement looks scary, but it‘s pretty simple when you know 354 // what is going on 355 // - it‘s simply comparing all the values. 356 if ( currentState.scale === target.scale || 357 ( currentState.rotate.x === target.rotate.x && 358 currentState.rotate.y === target.rotate.y && 359 currentState.rotate.z === target.rotate.z && 360 currentState.translate.x === target.translate.x && 361 currentState.translate.y === target.translate.y && 362 currentState.translate.z === target.translate.z ) ) { 363 delay = 0; 364 } 365 366 // Store current state 367 currentState = target; 368 activeStep = el; 369 370 // And here is where we trigger `impress:stepenter` event. 371 // We simply set up a timeout to fire it taking transition duration 372 // (and possible delay) into account. 373 // 374 // I really wanted to make it in more elegant way. The `transitionend` event seemed to 375 // be the best way to do it, but the fact that I‘m using transitions on two separate 376 // elements and that the `transitionend` event is only triggered when there was a 377 // transition (change in the values) caused some bugs and made the code really 378 // complicated, cause I had to handle all the conditions separately. And it still 379 // needed a `setTimeout` fallback for the situations when there is no transition at 380 // all. 381 // So I decided that I‘d rather make the code simpler than use shiny new 382 // `transitionend`. 383 // 384 // If you want learn something interesting and see how it was done with `transitionend` 385 // go back to 386 // version 0.5.2 of impress.js: 387 // http://github.com/bartaz/impress.js/blob/0.5.2/js/impress.js 388 window.clearTimeout( stepEnterTimeout ); 389 stepEnterTimeout = window.setTimeout( function() { 390 onStepEnter( activeStep ); 391 }, duration + delay ); 392 393 return el; 394 }; 395 396 // `prev` API function goes to previous step (in document order) 397 var prev = function() { 398 var prev = steps.indexOf( activeStep ) - 1; 399 prev = prev >= 0 ? steps[ prev ] : steps[ steps.length - 1 ]; 400 401 return goto( prev ); 402 }; 403 404 // `next` API function goes to next step (in document order) 405 var next = function() { 406 var next = steps.indexOf( activeStep ) + 1; 407 next = next < steps.length ? steps[ next ] : steps[ 0 ]; 408 409 return goto( next ); 410 }; 411 412 // Adding some useful classes to step elements. 413 // 414 // All the steps that have not been shown yet are given `future` class. 415 // When the step is entered the `future` class is removed and the `present` 416 // class is given. When the step is left `present` class is replaced with 417 // `past` class. 418 // 419 // So every step element is always in one of three possible states: 420 // `future`, `present` and `past`. 421 // 422 // There classes can be used in CSS to style different types of steps. 423 // For example the `present` class can be used to trigger some custom 424 // animations when step is shown. 425 root.addEventListener( "impress:init", function() { 426 427 // STEP CLASSES 428 steps.forEach( function( step ) { 429 step.classList.add( "future" ); 430 } ); 431 432 root.addEventListener( "impress:stepenter", function( event ) { 433 event.target.classList.remove( "past" ); 434 event.target.classList.remove( "future" ); 435 event.target.classList.add( "present" ); 436 }, false ); 437 438 root.addEventListener( "impress:stepleave", function( event ) { 439 event.target.classList.remove( "present" ); 440 event.target.classList.add( "past" ); 441 }, false ); 442 443 }, false ); 444 445 // Adding hash change support. 446 root.addEventListener( "impress:init", function() { 447 448 // Last hash detected 449 var lastHash = ""; 450 451 // `#/step-id` is used instead of `#step-id` to prevent default browser 452 // scrolling to element in hash. 453 // 454 // And it has to be set after animation finishes, because in Chrome it 455 // makes transtion laggy. 456 // BUG: http://code.google.com/p/chromium/issues/detail?id=62820 457 root.addEventListener( "impress:stepenter", function( event ) { 458 window.location.hash = lastHash = "#/" + event.target.id; 459 }, false ); 460 461 window.addEventListener( "hashchange", function() { 462 463 // When the step is entered hash in the location is updated 464 // (just few lines above from here), so the hash change is 465 // triggered and we would call `goto` again on the same element. 466 // 467 // To avoid this we store last entered hash and compare. 468 if ( window.location.hash !== lastHash ) { 469 goto( getElementFromHash() ); 470 } 471 }, false ); 472 473 // START 474 // by selecting step defined in url or first step of the presentation 475 goto( getElementFromHash() || steps[ 0 ], 0 ); 476 }, false ); 477 478 body.classList.add( "impress-disabled" ); 479 480 // Store and return API for given impress.js root element 481 return ( roots[ "impress-root-" + rootId ] = { 482 init: init, 483 goto: goto, 484 next: next, 485 prev: prev 486 } ); 487 488 }; 489 490 impress.supported = impressSupported;
以上是关于impress.js 源码解析系列的主要内容,如果未能解决你的问题,请参考以下文章
canal 源码解析系列-CanalInstance模块解析
ReactiveSwift源码解析 SignalProducerProtocol延展中的StartLift系列方法的代码实现