一、impress 核心设计
impress 页面间转场的实现是依赖于 CSS3 的 translate3d 属性完成。impress 的水平移动是改变了 translateX 坐标,垂直移动是改变 translateY 坐标,缩放的绚丽效果是改变 translateZ 的坐标实现,而这些转化样式的事件监听是通过 js 来实现的。
二、impress 主要函数
函数名 | 函数作用 |
---|---|
pfx | 给css3属性加上当前浏览器可用的前缀 |
arrayify | 将Array-Like对象转换成Array对象 |
css | 将指定属性应用到指定元素上 |
toNumber | 将参数转换成数字,如果无法转换返回默认值 |
byId | 通过id获取元素 |
$ | 返回满足选择器的第一个元素 |
$$ | 返回满足选择器的所有元素 |
triggerEvent | 在指定元素上触发指定事件 |
translate | 将translate对象转换成css使用的字符串 |
rotate | 将rotate对象转换成css使用的字符串 |
scale | 将scale对象转换成css使用的字符串 |
perspective | 将perspective对象转换成css使用的字符串 |
getElementFromHash | 根据hash来获取元素,hash就是URL中形如#step1 的东西 |
computeWindowScale | 根据当前窗口尺寸计算scale因子,用于放大和缩小 |
empty | 什么用都没有的函数,当浏览器不支持impress的时候会用到,一点用都没有 |
impress | 主函数,构造impress对象,这是一个全局对象 |
onStepEnter | 用于触发impress:stepenter 事件 |
onStepLeave | 用于触发impress:stepleave 事件 |
initStep | 初始化给定step |
init | 主初始化函数 |
getStep | 获取指定step |
goto | 切换到指定step |
prev | 切换到上一个step |
next | 切换到下一个step |
throttle | 可以延后运行某个函数 |
三、事件
事件是 impress.js 运行的基础,分别是
impress:init 是初始化事件
impress:stepenter 是进入下一步事件
impress:stepleave 是离开上一步事件
init
事件只在初始化时候触发,且只被触发一次,impress.js内部有一个initialized
变量,初始化之后这个变量会置 true,从而保证只初始化一次。
impress 切页实际上触发了两个事件:stepleave
和stepenter
,这两个事件中间的就是 css 的动画效果。也就是说,先触发stepleave
事件,然后运行css动画,然后触发stepenter
。这两个事件的作用主要就是设定一些标志位和变量,比如设置当前活跃 step。
四、流程
impress对象暴露了四个API
goto()
init()
next()
prev()
impress.js的运行流程可以分为两大部分——初始化过程以及 step 切换过程,正好对应
init()
和goto()
。就像上面说到的。初始化过程只会被运行一次,而切换过程可能被触发很多次。①初始化过程
初始化过程分为两个阶段,
第一个阶段是运行init()
函数,
第二个阶段是运行绑定到impress:init
上的函数。
这两个阶段之间的连接就是在init()
函数的结尾触发impress:init
事件,这样绑定上去的函数就会触发。
var init = function () {
if (initialized) { return; }
// 首先设定viewport
var meta = $("meta[name='viewport']") || document.createElement("meta");
meta.content = "width=device-width, minimum-scale=1, maximum-scale=1, user-scalable=no";
if (meta.parentNode !== document.head) {
meta.name = 'viewport';
document.head.appendChild(meta);
}
// 初始化config对象
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 )
};
// 计算当前scale
windowScale = computeWindowScale( config );
// 将所有step放到canvas中,再将canvas放到root中。
// 注意这里的canvas和css3中的canvas没关系,这里的canvas只是一个div
arrayify( root.childNodes ).forEach(function ( el ) {
canvas.appendChild( el );
});
root.appendChild(canvas);
// 设置html元素的初始高度
document.documentElement.style.height = "100%";
// 设置body元素的初始属性
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);
// 不能确定impress-disabled类是否存在,所以先remove一下
body.classList.remove("impress-disabled");
body.classList.add("impress-enabled");
// 获取所有step并初始化他们
steps = $$(".step", root);
steps.forEach( initStep );
// 设置canvas的初始状态
currentState = {
translate: { x: 0, y: 0, z: 0 },
rotate: { x: 0, y: 0, z: 0 },
scale: 1
};
initialized = true;
// 触发init事件
triggerEvent(root, "impress:init", { api: roots[ "impress-root-" + rootId ] });
};
第二阶段:运行绑定到impress:init
事件上的函数:
root.addEventListener("impress:init", function(){
// 改变step当前状态
steps.forEach(function (step) {
step.classList.add("future");
});
root.addEventListener("impress:stepenter", function (event) {
event.target.classList.remove("past");
event.target.classList.remove("future");
event.target.classList.add("present");
}, false);
root.addEventListener("impress:stepleave", function (event) {
event.target.classList.remove("present");
event.target.classList.add("past");
}, false);
}, false);
// 处理hash相关操作
root.addEventListener("impress:init", function(){
var lastHash = "";
root.addEventListener("impress:stepenter", function (event) {
window.location.hash = lastHash = "#/" + event.target.id;
}, false);
window.addEventListener("hashchange", function () {
if (window.location.hash !== lastHash) {
goto( getElementFromHash() );
}
}, false);
goto(getElementFromHash() || steps[0], 0);
}, false);
// 绑定键盘事件、触摸事件和点击事件
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);
document.addEventListener("keyup", function ( event ) {
if ( event.keyCode === 9 || ( event.keyCode >= 32 && event.keyCode <= 34 ) || (event.keyCode >= 37 && event.keyCode <= 40) ) {
switch( event.keyCode ) {
case 33: // pg up
case 37: // left
case 38: // up
api.prev();
break;
case 9: // tab
case 32: // space
case 34: // pg down
case 39: // right
case 40: // down
api.next();
break;
}
event.preventDefault();
}
}, false);
// 绑定链接点击事件
document.addEventListener("click", function ( event ) {
var target = event.target;
while ( (target.tagName !== "A") &&
(target !== document.documentElement) ) {
target = target.parentNode;
}
if ( target.tagName === "A" ) {
var href = target.getAttribute("href");
// if it's a link to presentation step, target this step
if ( href && href[0] === '#' ) {
target = document.getElementById( href.slice(1) );
}
}
if ( api.goto(target) ) {
event.stopImmediatePropagation();
event.preventDefault();
}
}, false);
// 绑定对象点击事件
document.addEventListener("click", function ( event ) {
var target = event.target;
while ( !(target.classList.contains("step") && !target.classList.contains("active")) &&
(target !== document.documentElement) ) {
target = target.parentNode;
}
if ( api.goto(target) ) {
event.preventDefault();
}
}, false);
// 绑定触摸事件
document.addEventListener("touchstart", function ( event ) {
if (event.touches.length === 1) {
var x = event.touches[0].clientX,
width = window.innerWidth * 0.3,
result = null;
if ( x < width ) {
result = api.prev();
} else if ( x > window.innerWidth - width ) {
result = api.next();
}
if (result) {
event.preventDefault();
}
}
}, false);
// 绑定页面resize事件
window.addEventListener("resize", throttle(function () {
api.goto( document.querySelector(".step.active"), 500 );
}, 250), false);
}, false);
step切换
var goto = function ( el, duration ) {
if ( !initialized || !(el = getStep(el)) ) {
//如果没初始化或者el不是一个step就返回
return false;
}
// 为了避免载入时候浏览器滚动,手动滚动到0,0
window.scrollTo(0, 0);
var step = stepsData["impress-" + el.id];
// 清理当前活跃step上面的标记
if ( activeStep ) {
activeStep.classList.remove("active");
body.classList.remove("impress-on-" + activeStep.id);
}
// 给el加活跃标记
el.classList.add("active");
body.classList.add("impress-on-" + el.id);
// 计算canvas相对于当前step的变换参数
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
};
// 处理缩放
var zoomin = target.scale >= currentState.scale;
duration = toNumber(duration, config.transitionDuration);
var delay = (duration / 2);
// 如果el就是当前活跃step,重新计算scale
if (el === activeStep) {
windowScale = computeWindowScale(config);
}
var targetScale = target.scale * windowScale;
// 触发stepleave事件
if (activeStep && activeStep !== el) {
onStepLeave(activeStep);
}
// 这里就是最核心的部分,设置css来实现动画效果
// 需要注意的是,动画效果有两类:缩放和移动
// 为了让效果看起来更逼真,这两类动画是分开实现的
// 缩放应用在root上,移动应用在canvas上
// 大家还记得元素的结构吗?root下面是canvas,canvas下面是所有step
// 所以缩放root的时候其实就是缩放canvas
// 至于为什么分开可以更逼真,请看最后一节的代码详解
// 这里是把缩放应用到root上
css(root, {
transform: perspective( config.perspective / targetScale ) + scale( targetScale ),
transitionDuration: duration + "ms",
transitionDelay: (zoomin ? delay : 0) + "ms"
});
// 这里就是把移动应用到canvas上
css(canvas, {
transform: rotate(target.rotate, true) + translate(target.translate),
transitionDuration: duration + "ms",
transitionDelay: (zoomin ? 0 : delay) + "ms"
});
if ( currentState.scale === target.scale ||
(currentState.rotate.x === target.rotate.x && currentState.rotate.y === target.rotate.y &&
currentState.rotate.z === target.rotate.z && currentState.translate.x === target.translate.x &&
currentState.translate.y === target.translate.y && currentState.translate.z === target.translate.z) ) {
delay = 0;
}
// 存储当前状态
currentState = target;
activeStep = el;
// 动画执行完毕后触发stepenter事件
window.clearTimeout(stepEnterTimeout);
stepEnterTimeout = window.setTimeout(function() {
onStepEnter(activeStep);
}, duration + delay);
return el;
};
prev和next函数
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.js 解析