首页 > 代码库 > impress.js 源码解析系列(2)
impress.js 源码解析系列(2)
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 =http://www.mamicode.com/ {}; 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 =http://www.mamicode.com/ 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: el120 };121 122 123 // 如果 step 元素没有 id 属性,则为它们添加 id124 if ( !el.id ) {125 el.id = "step-" + ( idx + 1 );126 }127 // stepsData 保存着每一幕的数据,找到给定 id 的这一幕,赋值为 step128 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 =http://www.mamicode.com/ 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.transitionDuration178 )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: 1225 };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] 获取某个 step236 * @param step 可以是数值,也可以是 step 的 id237 * @return 如果能够获取某个 step ,则返回这个 step238 */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` parameter252 // (by index, id or element), with a transition `duration` optionally253 // given as second parameter.254 var goto = function( el, duration ) {255 256 if ( !initialized || !( el = getStep( el ) ) ) {257 258 // 如果还没有初始化,或者 el 不是一个 step ,则返回 false259 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 visible264 // (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 top267 // whenever slide is selected268 //269 // If you are reading this and know any better way to handle it, I‘ll be glad to hear270 // 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 step284 var target = {285 rotate: {286 x: -step.rotate.x,287 y: -step.rotate.y,288 z: -step.rotate.z289 },290 translate: {291 x: -step.translate.x,292 y: -step.translate.y,293 z: -step.translate.z294 },295 scale: 1 / step.scale296 };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 transition302 // and the scaling is delayed, but when we are zooming out we start303 // 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 resize311 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 are325 // 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 make328 // visually nice and ‘natural‘ looking transitions), so we need to know329 // that both of them are finished.330 css( root, {331 332 // To keep the perspective look similar for different scales333 // we need to ‘scale‘ the perspective, too334 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 means348 // 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, so350 // here we compare the current and target values to check if delay should be taken into351 // account.352 //353 // I know that this `if` statement looks scary, but it‘s pretty simple when you know354 // what is going on355 // - 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 state367 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 duration372 // (and possible delay) into account.373 //374 // I really wanted to make it in more elegant way. The `transitionend` event seemed to375 // be the best way to do it, but the fact that I‘m using transitions on two separate376 // elements and that the `transitionend` event is only triggered when there was a377 // transition (change in the values) caused some bugs and made the code really378 // complicated, cause I had to handle all the conditions separately. And it still379 // needed a `setTimeout` fallback for the situations when there is no transition at380 // all.381 // So I decided that I‘d rather make the code simpler than use shiny new382 // `transitionend`.383 //384 // If you want learn something interesting and see how it was done with `transitionend`385 // go back to386 // version 0.5.2 of impress.js:387 // http://github.com/bartaz/impress.js/blob/0.5.2/js/impress.js388 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 with417 // `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 custom424 // animations when step is shown.425 root.addEventListener( "impress:init", function() {426 427 // STEP CLASSES428 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 detected449 var lastHash = "";450 451 // `#/step-id` is used instead of `#step-id` to prevent default browser452 // scrolling to element in hash.453 //454 // And it has to be set after animation finishes, because in Chrome it455 // makes transtion laggy.456 // BUG: http://code.google.com/p/chromium/issues/detail?id=62820457 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 updated464 // (just few lines above from here), so the hash change is465 // 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 // START474 // by selecting step defined in url or first step of the presentation475 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 element481 return ( roots[ "impress-root-" + rootId ] = {482 init: init,483 goto: goto,484 next: next,485 prev: prev486 } );487 488 };489 490 impress.supported = impressSupported;
impress.js 源码解析系列(2)
声明:以上内容来自用户投稿及互联网公开渠道收集整理发布,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任,若内容有误或涉及侵权可进行投诉: 投诉/举报 工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。