源码看了两天,删掉了一些优化,和对 ipad 的支持,仅研究了其核心功能的实现,作以下记录。
HTML 结构如下:
<!doctype html> <html lang="zh-cn">
<head>
<meta charset="utf-8" />
<title>impress.js</title>
<link href="css/impress-demo.css" rel="stylesheet" />
</head> <body> <div id="impress"> <div class="step" data-x="1000" data-rotate-y="45" data-scale="3">第一幕</div>
<div class="step" data-z="1000" data-rotate-y="45" data-rotate-z="90" data-scale="2">第二幕</div>
<div class="step" data-x="0" data-z="1000" data-rotate-y="45" data-rotate-z="80" data-scale="1">第三幕</div>
<div id="overview" class="step" data-x="3000" data-y="1500" data-scale="10"></div> </div> <script src="js/impress.js"></script> </body>
</html>
在 HTML 中,每一张显示的幕布都有一个 step 的类,并且所有的 step 类都被包含在一个 id 为 impress 的容器(舞台)中。
而在每一个 step 中,利用 data 自定义每一个 step 的 translate ,rotate ,和 scale 。
最后一个 id 为 overview 的 div ,也同时是一个 step 类,用于在一张幕布上显示所有的演示元素,不是必需的。
impress.js 核心代码
注意,此代码经过我的大量删除,几乎没有经过优化,仅完成核心功能,便于对 impress.js 核心逻辑的理解。
(function(document, window) { /**
* 在需要的时候,为 CSS 属性添加当前浏览器能够识别的前缀
* @param prop 一定要记住,参数是一个字符串,所以传入的 CSS 属性一定要加引号
* @return 返回当前浏览器能够识别的 CSS 属性
*/
var pfx = (function() {
var prefixes = "Moz Webkit O ms".split(" ");
var style = document.createElement("dummy").style;
var memory = {}; return function(prop) {
var uProp = prop.charAt(0).toUpperCase() + prop.slice(1);
var props = (prop + " " + prefixes.join(uProp + " ") + uProp).split(" "); memory[prop] = null;
for (var i in props) {
if (style[props[i]] !== undefined) {
memory[prop] = props[i];
break;
}
}
return memory[prop];
}
})(); /**
* 为指定的元素添加一组 CSS 样式
* @param ele 指定的元素
* @param props 一组 CSS 属性和值,JSON 的形式,属性名和属性值都要加引号
* @return 返回指定的元素
*/
var css = function(ele, props) {
var key, pkey;
for (key in props) {
if (props.hasOwnProperty(key)) {
pkey = pfx(key);
if (pkey !== null) {
ele.style[pkey] = props[key];
}
}
}
return ele;
} /**
* 将传入的参数转换为数值
* @param numeric 要转换为数值的参数
* @param fallback 传入的参数不能转换为数值时返回的值,可以省略,如果省略则返回 0
* @return 返回一个数值或者 0
*/
var toNumber = function(numeric, fallback) {
return isNaN(numeric) ? fallback || 0 : Number(numeric);
} /**
* 设置 3D 转换元素的 translate 值
* @param t 位移值,以对象字面量的形式,属性值不需要带单位
* @return 返回 "translate3d() "
*/
var translate = function(t) {
return "translate3d(" + t.x + "px," + t.y + "px," + t.z + "px) ";
} /**
* 设置 3D 转换元素的 rotate 值
* @param r 旋转值,以对象字面量的形式,属性值不需要带单位
* @return 返回 "rotateX() rotateY() rotateZ() "
*/
var rotate = function(r, revert) {
var rX = " rotateX(" + r.x + "deg) ",
rY = " rotateY(" + r.y + "deg) ",
rZ = " rotateZ(" + r.z + "deg) "; return revert ? rZ + rY + rX : rX + rY + rZ;
}; // 设置 3D 转换元素的 scale 值
var scale = function(s) {
return "scale(" + s + ") ";
} // 设置 3D 转换元素的 perspective 值
var perspective = function(p) {
return "perspective(" + p + "px) ";
} /**
* 计算缩放因子,并限定其最大最小值
* @param config 配置信息
* @return 返回缩放因子
*/
var computeWindowScale = function(config) {
var hScale = window.innerHeight / config.height;
var wScale = window.innerWidth / config.width;
var scale = hScale > wScale ? wScale : hScale;
if (config.maxScale && scale > config.maxScale) {
scale = config.maxScale;
}
if (config.minScale && scale < config.minScale) {
scale = config.minScale;
}
return scale;
} /**
* 自定义事件并立即触发
* @param el 触发事件的元素
* @param eventName 事件名
* @param detail 事件信息
*/
var triggerEvent = function(el, eventName, detail) {
var event = document.createEvent("CustomEvent");
// 事件冒泡,并且可以取消冒泡
event.initCustomEvent(eventName, true, true, detail);
el.dispatchEvent(event);
}; // 通过 hash 值取得元素
var getElementFromHash = function() {
return document.getElementById(window.location.hash.replace(/^#\/?/, ""));
}; // 定义 empty 函数,只是为了书写方便
var empty = function() {
return false;
}; var body = document.body; // 定义一个 defaults 对象,保存着一些默认值
var defaults = {
width: 1024,
height: 768,
maxScale: 1,
minScale: 0,
perspective: 1000,
transitionDuration: 1000
}; // 变量 roots ,保存着 impress 的实例
var roots = {}; var impress = window.impress = function(rootId) {
rootId = rootId || "impress"; // 保存所有 step 的 translate rotate scale 属性
var stepsData = {}; // 当前展示的 step
var activeStep = null; // canvas 的当前状态
var currentState = null; // 包含所有 step 的数组
var steps = null; // 配置信息
var config = null; // 浏览器窗口的缩放因子
var windowScale = null; // Presentation 的根元素
var root = document.getElementById(rootId); // 创建一个 div 元素,保存在变量 canvas 中
// 注意这只是一个 dic ,只是名字好听而已
var canvas = document.createElement("div"); // 初始化状态为 false
var initialized = false; // 这个变量关系到 hash 值的改变,
var lastEntered = null; /**
* 初始化函数
* 引用 impress.js 之后单独调用
*/
var init = function() {
if (initialized) {
return;
} // Presentation 的根元素的 dataset 属性
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
)
}; // 传入配置信息,计算浏览器窗口的缩放因子
windowScale = computeWindowScale(config); // 将所有的 step 都放在 canvas 中,将 canvas 放在根元素下
var stepArr = Array.prototype.slice.call(root.childNodes);
for (var i = 0; i < stepArr.length; i++) {
canvas.appendChild(stepArr[i]);
}
root.appendChild(canvas); // 设置 html body #impress canvas 的初始样式
document.documentElement.style.height = "100%"; 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); // 获取每一个 step ,调用 initStep() 函数初始化它们的样式
steps = Array.prototype.slice.call(root.querySelectorAll(".step"));
for (var i = 0; i < steps.length; i++) {
initStep(steps[i], i);
} // 设置 canvas 的初始状态
currentState = {
translate: {
x: 0,
y: 0,
z: 0
},
rotate: {
x: 0,
y: 0,
z: 0
},
scale: 1
}; // 更新初始化状态为 true
initialized = true; // 自定义事件 impress:init 并触发
triggerEvent(root, "impress:init", {
api: roots["impress-root-" + rootId]
});
}; /**
* 初始化 step 的样式
* @param el 当前 step 元素
* @param i 数字值
*/
var initStep = function(el, i) {
// 获取当前 step 的dataset 属性,保存在变量 data 中
// 根据 data 的属性值,拿到 translate rotate scale 的完整属性,保存在变量 step 中
var data = el.dataset,
step = {
translate: {
x: toNumber(data.x),
y: toNumber(data.y),
z: toNumber(data.z)
},
rotate: {
x: toNumber(data.rotateX),
y: toNumber(data.rotateY),
z: toNumber(data.rotateZ || data.rotate)
},
scale: toNumber(data.scale, 1),
el: el
}; // 根据变量 step 中保存的数据,为当前 step 定义样式
css(el, {
position: "absolute",
transform: "translate(-50%,-50%)" +
translate(step.translate) +
rotate(step.rotate) +
scale(step.scale),
transformStyle: "preserve-3d"
}); // 检测当前 step 是否有 id 属性,如果没有,则添加 id ,格式为 step-*
if (!el.id) {
el.id = "step-" + (i + 1);
} // 将当前 step 的相关数据保存到变量 stepsData 中。
// 在 stepsData 这个对象中,属性名就是 "impress" + el.id ,属性值就是相对应的 translate rotate scale 属性
stepsData["impress-" + el.id] = step;
}; // 定时器,用于改变 hash 值
var stepEnterTimeout = null; /**
* 自定义 impress:stepenter 事件并触发
* @param {[type]} step [description]
*/
var onStepEnter = function(step) {
if (lastEntered !== step) {
triggerEvent(step, "impress:stepenter");
lastEntered = step;
}
}; /**
* 自定义 impress:stepleave 事件并触发
* @param {[type]} step [description]
*/
var onStepLeave = function(step) {
if (lastEntered === step) {
triggerEvent(step, "impress:stepleave");
lastEntered = null;
}
}; /**
* 切换到下一张幕布的函数
* @param el 下一个 step 所在的元素
* @param duration 过渡时间
* @return [description]
*/
var goto = function(el, duration) { window.scrollTo(0, 0); // 获取当前 step 的数据
var step = stepsData["impress-" + el.id]; // 根据下一个 step 的数据,计算 canvas 和 root 的目标状态,作出相应调整
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
}; // 检测幕布之间的切换 scale 是变大还是变小,从而定义动画的延迟时间
var zoomin = target.scale >= currentState.scale; duration = toNumber(duration, config.transitionDuration);
var delay = (duration / 2); var targetScale = target.scale * windowScale; // 在 root 元素上调整 perspective 和 scale ,让每一张幕布看起来大小都一样
// root 的调整是动画的一部分(二分之一)
css(root, {
transform: perspective(config.perspective / targetScale) + scale(targetScale),
transitionDuration: duration + "ms",
transitionDelay: (zoomin ? delay : 0) + "ms"
});
// 在 canvas 元素上进行与当前幕布反方向的位移和旋转,保证当前总是正对着我们
// canvas 的调整是动画的一部分(二份之二)
css(canvas, {
transform: rotate(target.rotate, true) + translate(target.translate),
transitionDuration: duration + "ms",
transitionDelay: (zoomin ? 0 : delay) + "ms"
}); // 相关变量的更新
currentState = target;
activeStep = el; // 首先清除定时器,在下一张幕布进入的 duration + delay 毫秒之后,自定义一个 impress:stepenter 事件并触发
// 这个事件触发之后会被 root 接收,用于改变 hash 值
window.clearTimeout(stepEnterTimeout);
stepEnterTimeout = window.setTimeout(function() {
onStepEnter(activeStep);
}, duration + delay); return el;
}; // 定义切换幕布的 api
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:init 事件被 root 接收,改变 hash 值
root.addEventListener("impress:init", function() { // Last hash detected
var lastHash = ""; // 当 step 进入时,触发 impress:stepenter 事件,这个事件被 root 接收,获取进入的 step 的 id ,从而改变 hash 值为当前 step 的 id
root.addEventListener("impress:stepenter", function(event) {
window.location.hash = lastHash = "#/" + event.target.id;
}, false); // 初始化之后,展示第一张 step
goto(getElementFromHash() || steps[0], 0);
}, false); // 整个 impress() 函数的返回值,这样 impress().init() 函数才能在外部被调用
return (roots["impress-root-" + rootId] = {
init: init,
goto: goto,
next: next,
prev: prev
});
} })(document, window); (function(document, window) {
"use strict"; // impress:init 事件触发之后执行
// impress:init 事件在执行 init() 函数完成初始化之后自定义并且立即触发
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); // 通过键盘事件进行 step 之间的切换
document.addEventListener("keyup", function(event) { if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) {
return;
} if (event.keyCode === 9 ||
(event.keyCode >= 32 && event.keyCode <= 34) ||
(event.keyCode >= 37 && event.keyCode <= 40)) {
switch (event.keyCode) {
case 33: // Page up
case 37: // Left
case 38: // Up
api.prev();
break;
case 9: // Tab
case 32: // Space
case 34: // Page down
case 39: // Right
case 40: // Down
api.next();
break;
} event.preventDefault();
}
}, false); }, false); })(document, window); // 调用 impress().init()
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 没有半毛钱关系。