Hi,大家好,今天又要给大家分享前端干货了,这次带来的是一个很炫酷的图片切换动画,基于three.js动画库,图片切换时出现撕裂粉碎的粒子化特效。它有几个特点:
基于three.js动画库,可以很方便地自定义各种动画。 使用非常简单,只需要在页面中定义一个指定id的 div
容器即可。事实上底层是通过Canvas绘制动画,因此绘制非常灵活。
源码下载
关注我的公众号「三世码农」,发送「3014」获取源码下载链接。
↓↓↓
先来看看整体的效果图吧!
效果预览
看完动画预览效果,你是不是非常想了解如此复杂的图片切换动画是如何实现的呢?下面我们继续看代码吧~
如果你对实现过程没啥兴趣,也可以直接在文末获取源码下载链接。
代码实现
HTML代码
前面我们也说过,HTML代码非常简单,仅仅需要在页面上定义一个div
即可,id为three-container
:
<div id="three-container"></div>
然后需要引入three.js动画库以及其他相关的库文件:
<script src='three.min.js'></script>
<script src='TweenMax.min.js'></script>
<script src='bas.js'></script>
<script src='OrbitControls-2.js'></script>
TweenMax
是GreenSock 动画平台中的核心动画工具,可用来构建补间动画(tween)。
three.bas
全称“THREE Buffer Animation System”,是three.js的扩展,它主要提供了一个GPU缓冲池,解决了在动画对象数量很多的情况下,对象数据从CPU发送到GPU时产生的性能瓶颈。
OrbitControls.js
是一个相当神奇的轨道控制器控件,用它可以实现场景用鼠标交互,让场景动起来,控制场景的旋转、平移,缩放。
JavaScript代码
首先我们构建一个图片切换类Slide
,该类主要定义一次图片切换所需要的全部属性和方法:
function Slide(width, height, animationPhase) {
var plane = new THREE.PlaneGeometry(width, height, width * 2, height * 2);
THREE.BAS.Utils.separateFaces(plane);
var geometry = new SlideGeometry(plane);
geometry.bufferUVs();
var aAnimation = geometry.createAttribute('aAnimation', 2);
var aStartPosition = geometry.createAttribute('aStartPosition', 3);
var aControl0 = geometry.createAttribute('aControl0', 3);
var aControl1 = geometry.createAttribute('aControl1', 3);
var aEndPosition = geometry.createAttribute('aEndPosition', 3);
var i, i2, i3, i4, v;
var minDuration = 0.8;
var maxDuration = 1.2;
var maxDelayX = 0.9;
var maxDelayY = 0.125;
var stretch = 0.11;
this.totalDuration = maxDuration + maxDelayX + maxDelayY + stretch;
var startPosition = new THREE.Vector3();
var control0 = new THREE.Vector3();
var control1 = new THREE.Vector3();
var endPosition = new THREE.Vector3();
var tempPoint = new THREE.Vector3();
function getControlPoint0(centroid) {
var signY = Math.sign(centroid.y);
tempPoint.x = THREE.Math.randFloat(0.1, 0.3) * 50;
tempPoint.y = signY * THREE.Math.randFloat(0.1, 0.3) * 70;
tempPoint.z = THREE.Math.randFloatSpread(20);
return tempPoint;
}
function getControlPoint1(centroid) {
var signY = Math.sign(centroid.y);
tempPoint.x = THREE.Math.randFloat(0.3, 0.6) * 50;
tempPoint.y = -signY * THREE.Math.randFloat(0.3, 0.6) * 70;
tempPoint.z = THREE.Math.randFloatSpread(20);
return tempPoint;
}
for (i = 0, i2 = 0, i3 = 0, i4 = 0; i < geometry.faceCount; i++, i2 += 6, i3 += 9, i4 += 12) {
var face = plane.faces[i];
var centroid = THREE.BAS.Utils.computeCentroid(plane, face);
// animation
var duration = THREE.Math.randFloat(minDuration, maxDuration);
var delayX = THREE.Math.mapLinear(centroid.x, -width * 0.5, width * 0.5, 0.0, maxDelayX);
var delayY;
if (animationPhase === 'in') {
delayY = THREE.Math.mapLinear(Math.abs(centroid.y), 0, height * 0.5, 0.0, maxDelayY)
}
else {
delayY = THREE.Math.mapLinear(Math.abs(centroid.y), 0, height * 0.5, maxDelayY, 0.0)
}
for (v = 0; v < 6; v += 2) {
aAnimation.array[i2 + v] = delayX + delayY + (Math.random() * stretch * duration);
aAnimation.array[i2 + v + 1] = duration;
}
// positions
endPosition.copy(centroid);
startPosition.copy(centroid);
if (animationPhase === 'in') {
control0.copy(centroid).sub(getControlPoint0(centroid));
control1.copy(centroid).sub(getControlPoint1(centroid));
}
else { // out
control0.copy(centroid).add(getControlPoint0(centroid));
control1.copy(centroid).add(getControlPoint1(centroid));
}
for (v = 0; v < 9; v += 3) {
aStartPosition.array[i3 + v] = startPosition.x;
aStartPosition.array[i3 + v + 1] = startPosition.y;
aStartPosition.array[i3 + v + 2] = startPosition.z;
aControl0.array[i3 + v] = control0.x;
aControl0.array[i3 + v + 1] = control0.y;
aControl0.array[i3 + v + 2] = control0.z;
aControl1.array[i3 + v] = control1.x;
aControl1.array[i3 + v + 1] = control1.y;
aControl1.array[i3 + v + 2] = control1.z;
aEndPosition.array[i3 + v] = endPosition.x;
aEndPosition.array[i3 + v + 1] = endPosition.y;
aEndPosition.array[i3 + v + 2] = endPosition.z;
}
}
var material = new THREE.BAS.BasicAnimationMaterial(
{
shading: THREE.FlatShading,
side: THREE.DoubleSide,
uniforms: {
uTime: {type: 'f', value: 0}
},
shaderFunctions: [
THREE.BAS.ShaderChunk['cubic_bezier'],
//THREE.BAS.ShaderChunk[(animationPhase === 'in' ? 'ease_out_cubic' : 'ease_in_cubic')],
THREE.BAS.ShaderChunk['ease_in_out_cubic'],
THREE.BAS.ShaderChunk['quaternion_rotation']
],
shaderParameters: [
'uniform float uTime;',
'attribute vec2 aAnimation;',
'attribute vec3 aStartPosition;',
'attribute vec3 aControl0;',
'attribute vec3 aControl1;',
'attribute vec3 aEndPosition;',
],
shaderVertexInit: [
'float tDelay = aAnimation.x;',
'float tDuration = aAnimation.y;',
'float tTime = clamp(uTime - tDelay, 0.0, tDuration);',
'float tProgress = ease(tTime, 0.0, 1.0, tDuration);'
//'float tProgress = tTime / tDuration;'
],
shaderTransformPosition: [
(animationPhase === 'in' ? 'transformed *= tProgress;' : 'transformed *= 1.0 - tProgress;'),
'transformed += cubicBezier(aStartPosition, aControl0, aControl1, aEndPosition, tProgress);'
]
},
{
map: new THREE.Texture(),
}
);
THREE.Mesh.call(this, geometry, material);
this.frustumCulled = false;
}
接下来是定义图片切换时的粉碎粒子图形效果几何模型,主要是将粒子顶点添加到bas缓冲池中:
function SlideGeometry(model) {
THREE.BAS.ModelBufferGeometry.call(this, model);
}
SlideGeometry.prototype = Object.create(THREE.BAS.ModelBufferGeometry.prototype);
SlideGeometry.prototype.constructor = SlideGeometry;
SlideGeometry.prototype.bufferPositions = function () {
var positionBuffer = this.createAttribute('position', 3).array;
for (var i = 0; i < this.faceCount; i++) {
var face = this.modelGeometry.faces[i];
var centroid = THREE.BAS.Utils.computeCentroid(this.modelGeometry, face);
var a = this.modelGeometry.vertices[face.a];
var b = this.modelGeometry.vertices[face.b];
var c = this.modelGeometry.vertices[face.c];
positionBuffer[face.a * 3] = a.x - centroid.x;
positionBuffer[face.a * 3 + 1] = a.y - centroid.y;
positionBuffer[face.a * 3 + 2] = a.z - centroid.z;
positionBuffer[face.b * 3] = b.x - centroid.x;
positionBuffer[face.b * 3 + 1] = b.y - centroid.y;
positionBuffer[face.b * 3 + 2] = b.z - centroid.z;
positionBuffer[face.c * 3] = c.x - centroid.x;
positionBuffer[face.c * 3 + 1] = c.y - centroid.y;
positionBuffer[face.c * 3 + 2] = c.z - centroid.z;
}
};
一切就绪后,最后就是初始化组装动画的过程了:
function init() {
var root = new THREERoot({
createCameraControls: !true,
antialias: (window.devicePixelRatio === 1),
fov: 80
});
root.renderer.setClearColor(0x000000, 0);
root.renderer.setPixelRatio(window.devicePixelRatio || 1);
root.camera.position.set(0, 0, 60);
var width = 100;
var height = 60;
var slide = new Slide(width, height, 'out');
var l1 = new THREE.ImageLoader();
l1.setCrossOrigin('Anonymous');
l1.load('images/winter.jpg', function(img) {
slide.setImage(img);
})
root.scene.add(slide);
var slide2 = new Slide(width, height, 'in');
var l2 = new THREE.ImageLoader();
l2.setCrossOrigin('Anonymous');
l2.load('images/spring.jpg', function(img) {
slide2.setImage(img);
})
root.scene.add(slide2);
var tl = new TimelineMax({repeat:-1, repeatDelay:1.0, yoyo: true});
tl.add(slide.transition(), 0);
tl.add(slide2.transition(), 0);
createTweenScrubber(tl);
window.addEventListener('keyup', function(e) {
if (e.keyCode === 80) {
tl.paused(!tl.paused());
}
});
}
从上面的代码可以看到,我们创建了2个Slide
实例,也就是为两张图片创建了切换动画。
同时使用TimelineMax为这两个Slide创建了补间动画:
var tl = new TimelineMax({repeat:-1, repeatDelay:1.0, yoyo: true});
tl.add(slide.transition(), 0);
tl.add(slide2.transition(), 0);
createTweenScrubber(tl);
repeat: -1
表示无限循环;repeatDelay: 1.0
表示补间动画时间为1秒,你可以修改这个值来改变图片切换的速度。yoyo: true
表示重复的动画将往返进行。
最后,注册了一个键盘事件,当我们按下p
键后,可以暂停/播放该动画,p
键对应的键值码是80
:
window.addEventListener('keyup', function(e) {
if (e.keyCode === 80) {
tl.paused(!tl.paused());
}
});
到这里为止,我们对这个图片切换动画的实现过程就全部讲完了。
文章最后也将全部源码分享给大家。
源码下载
关注我的公众号「三世码农」,发送「3014」获取源码下载链接。
↓↓↓