首页 > 代码库 > 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;
View Code

 

impress.js 源码解析系列(2)