制作图片倾斜(tilt)效果

原文来自:

http://tympanus.net/codrops/2015/05/28/image-tilt-effect/

所谓的倾斜效果,我也不知如何用语言描述,那就直接看Demo啦,下面我们会对这个效果的实现原理逐步分析:
http://codepen.io/CodingMonkeyzh/pen/jPYNyr

文档结构

对一个图片添加该效果,首先,我们需要一个具有宽高的容器。DOM 结构非常简单。

1
2
3
<div class="container">
<img class="tilt-effect" src="http://placehold.it/500x300" alt="" />
</div>

上面这段结构经过脚本处理之后,会被替换成下面的结构:

1
2
3
4
5
6
7
<div class="container">
<div class="tilt">
<div class="tilt__back" style="background-image: url(http://placehold.it/500x300);"></div>
<div class="tilt__front" style="opacity: 0.7; -webkit-transform: perspective(1000px) translate3d(0px, 0px, 0px) rotate3d(1, 1, 1, 0deg); transform: perspective(1000px) translate3d(0px, 0px, 0px) rotate3d(1, 1, 1, 0deg); background-image: url(http://placehold.it/500x300);"></div>
<div class="tilt__front" style="opacity: 0.7; -webkit-transform: perspective(1000px) translate3d(0px, 0px, 0px) rotate3d(1, 1, 1, 0deg); transform: perspective(1000px) translate3d(0px, 0px, 0px) rotate3d(1, 1, 1, 0deg); background-image: url(http://placehold.it/500x300);"></div>
</div>
</div>

脚本分析

我们利用了filtfx.js这个插件对上面的图片进行处理, 来实现倾斜效果。我在原来的代码中加入了一些注释,来帮助我们理解。下面我们对该插件的核心代码进行分析。

1
2
3
4
5
6
7
function TiltFx(el, options) {
this.el = el;
this.options = extend({}, this.options);
extend(this.options, options);
this._init();
this._initEvents();
}

这是构造函数,如果我们的文档中,加入了上面的插件,那么插件会遍历文档中具有tilt-effetimg元素,来调用构造函数TiltFx()

1
2
3
4
5
6
function init() {
// 遍历所有拥有‘title-effect’类的img元素
[].slice.call(document.querySelectorAll('img.tilt-effect')).forEach(function(img) {
new TiltFx(img, JSON.parse(img.getAttribute('data-tilt-options')));
});
}

TiltFx()具有一个原型属性,两个原型方法。原型属性配置了一些默认的参数用于调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 默认参数
*/
TiltFx.prototype.options = {
extraImgs: 2, // 额外的辅助图片数量
opacity: 0.7,
bgfixed: true, // 底图是否固定
movement: { // 这是一些用于移动的参数
perspective: 1000,
translateX: -10,
translateY: -10,
translateZ: 20,
rotateX: 2,
rotateY: 2,
rotateZ: 0
}
}

第一个原型方法是_init(),用于初始化DOM结点,生成我们的目标DOM结点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
TiltFx.prototype._init = function() {
this.tiltWrapper = document.createElement('div');
this.tiltWrapper.className = 'tilt';

// main image element.
this.tiltImgBack = document.createElement('div');
this.tiltImgBack.className = 'tilt__back';
this.tiltImgBack.style.backgroundImage = 'url(' + this.el.src + ')';
this.tiltWrapper.appendChild(this.tiltImgBack);

// image elements limit.
if (this.options.extraImgs < 1) {
this.options.extraImgs = 1;
} else if (this.options.extraImgs > 5) {
this.options.extraImgs = 5;
}

if (!this.options.movement.perspective) {
this.options.movement.perspective = 0;
}

// add the extra image elements.
this.imgElems = [];
for (var i = 0; i < this.options.extraImgs; ++i) {
var el = document.createElement('div');
el.className = 'tilt__front';
el.style.backgroundImage = 'url(' + this.el.src + ')';
el.style.opacity = this.options.opacity;
this.tiltWrapper.appendChild(el);
this.imgElems.push(el);
}

if (!this.options.bgfixed) {
this.imgElems.push(this.tiltImgBack);
++this.options.extraImgs;
}

// add it to the DOM and remove original img element.
this.el.parentNode.insertBefore(this.tiltWrapper, this.el);
this.el.parentNode.removeChild(this.el);

// tiltWrapper properties: width/height/left/top
this.view = {
width: this.tiltWrapper.offsetWidth,
height: this.tiltWrapper.offsetHeight
};
};

另外一个原型方式是用于监听鼠标事件之类的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
TiltFx.prototype._initEvents = function() {
var self = this,
moveOpts = self.options.movement;

// mousemove event..
this.tiltWrapper.addEventListener('mousemove', function(ev) {
requestAnimationFrame(function() {
// mouse position relative to the document.
var mousepos = getMousePos(ev),
// document scrolls.
docScrolls = {
left: document.body.scrollLeft + document.documentElement.scrollLeft,
top: document.body.scrollTop + document.documentElement.scrollTop
},
bounds = self.tiltWrapper.getBoundingClientRect(),
// mouse position relative to the main element (tiltWrapper).
relmousepos = {
x: mousepos.x - bounds.left - docScrolls.left,
y: mousepos.y - bounds.top - docScrolls.top
};

// configure the movement for each image element.
for (var i = 0, len = self.imgElems.length; i < len; ++i) {
var el = self.imgElems[i],
rotX = moveOpts.rotateX ? 2 * ((i + 1) * moveOpts.rotateX / self.options.extraImgs) / self.view.height * relmousepos.y - ((i + 1) * moveOpts.rotateX / self.options.extraImgs) : 0,
rotY = moveOpts.rotateY ? 2 * ((i + 1) * moveOpts.rotateY / self.options.extraImgs) / self.view.width * relmousepos.x - ((i + 1) * moveOpts.rotateY / self.options.extraImgs) : 0,
rotZ = moveOpts.rotateZ ? 2 * ((i + 1) * moveOpts.rotateZ / self.options.extraImgs) / self.view.width * relmousepos.x - ((i + 1) * moveOpts.rotateZ / self.options.extraImgs) : 0,
transX = moveOpts.translateX ? 2 * ((i + 1) * moveOpts.translateX / self.options.extraImgs) / self.view.width * relmousepos.x - ((i + 1) * moveOpts.translateX / self.options.extraImgs) : 0,
transY = moveOpts.translateY ? 2 * ((i + 1) * moveOpts.translateY / self.options.extraImgs) / self.view.height * relmousepos.y - ((i + 1) * moveOpts.translateY / self.options.extraImgs) : 0,
transZ = moveOpts.translateZ ? 2 * ((i + 1) * moveOpts.translateZ / self.options.extraImgs) / self.view.height * relmousepos.y - ((i + 1) * moveOpts.translateZ / self.options.extraImgs) : 0;

el.style.WebkitTransform = 'perspective(' + moveOpts.perspective + 'px) translate3d(' + transX + 'px,' + transY + 'px,' + transZ + 'px) rotate3d(1,0,0,' + rotX + 'deg) rotate3d(0,1,0,' + rotY + 'deg) rotate3d(0,0,1,' + rotZ + 'deg)';
el.style.transform = 'perspective(' + moveOpts.perspective + 'px) translate3d(' + transX + 'px,' + transY + 'px,' + transZ + 'px) rotate3d(1,0,0,' + rotX + 'deg) rotate3d(0,1,0,' + rotY + 'deg) rotate3d(0,0,1,' + rotZ + 'deg)';
}
});
});

// reset all when mouse leaves the main wrapper.
this.tiltWrapper.addEventListener('mouseleave', function(ev) {
setTimeout(function() {
for (var i = 0, len = self.imgElems.length; i < len; ++i) {
var el = self.imgElems[i];
el.style.WebkitTransform = 'perspective(' + moveOpts.perspective + 'px) translate3d(0,0,0) rotate3d(1,1,1,0deg)';
el.style.transform = 'perspective(' + moveOpts.perspective + 'px) translate3d(0,0,0) rotate3d(1,1,1,0deg)';
}
}, 60);

});

// window resize
window.addEventListener('resize', throttle(function(ev) {
// recalculate tiltWrapper properties: width/height/left/top
self.view = {
width: self.tiltWrapper.offsetWidth,
height: self.tiltWrapper.offsetHeight
};
}, 50));
};

我们可以看到,监听mousemove的事件处理函数中的计算比较复杂,关键的部分就是在这里:

1
2
3
4
5
6
7
8
9
10
var el = self.imgElems[i],
rotX = moveOpts.rotateX ? 2 * ((i + 1) * moveOpts.rotateX / self.options.extraImgs) / self.view.height * relmousepos.y - ((i + 1) * moveOpts.rotateX / self.options.extraImgs) : 0,
rotY = moveOpts.rotateY ? 2 * ((i + 1) * moveOpts.rotateY / self.options.extraImgs) / self.view.width * relmousepos.x - ((i + 1) * moveOpts.rotateY / self.options.extraImgs) : 0,
rotZ = moveOpts.rotateZ ? 2 * ((i + 1) * moveOpts.rotateZ / self.options.extraImgs) / self.view.width * relmousepos.x - ((i + 1) * moveOpts.rotateZ / self.options.extraImgs) : 0,
transX = moveOpts.translateX ? 2 * ((i + 1) * moveOpts.translateX / self.options.extraImgs) / self.view.width * relmousepos.x - ((i + 1) * moveOpts.translateX / self.options.extraImgs) : 0,
transY = moveOpts.translateY ? 2 * ((i + 1) * moveOpts.translateY / self.options.extraImgs) / self.view.height * relmousepos.y - ((i + 1) * moveOpts.translateY / self.options.extraImgs) : 0,
transZ = moveOpts.translateZ ? 2 * ((i + 1) * moveOpts.translateZ / self.options.extraImgs) / self.view.height * relmousepos.y - ((i + 1) * moveOpts.translateZ / self.options.extraImgs) : 0;

el.style.WebkitTransform = 'perspective(' + moveOpts.perspective + 'px) translate3d(' + transX + 'px,' + transY + 'px,' + transZ + 'px) rotate3d(1,0,0,' + rotX + 'deg) rotate3d(0,1,0,' + rotY + 'deg) rotate3d(0,0,1,' + rotZ + 'deg)';
el.style.transform = 'perspective(' + moveOpts.perspective + 'px) translate3d(' + transX + 'px,' + transY + 'px,' + transZ + 'px) rotate3d(1,0,0,' + rotX + 'deg) rotate3d(0,1,0,' + rotY + 'deg) rotate3d(0,0,1,' + rotZ + 'deg)';

这里我们根据鼠标的位置,计算出了各个图层对应的偏移量和旋转角度,然后对它们进行变换即可。
最后mouseleave之后,我们再把个个图层恢复到初始位置就行了。

Demo:http://codepen.io/CodingMonkeyzh/pen/jPYNyr