超酷的 Three.js 图片撕碎切换动画

职场   2024-09-14 22:04   浙江  

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.10.3) * 50;
    tempPoint.y = signY * THREE.Math.randFloat(0.10.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.30.6) * 50;
    tempPoint.y = -signY * THREE.Math.randFloat(0.30.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.50.0, maxDelayX);
    var delayY;

    if (animationPhase === 'in') {
      delayY = THREE.Math.mapLinear(Math.abs(centroid.y), 0, height * 0.50.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'value0}
      },
      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);'
      ]
    },
    {
      mapnew 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),
    fov80
  });

  root.renderer.setClearColor(0x0000000);
  root.renderer.setPixelRatio(window.devicePixelRatio || 1);
  root.camera.position.set(0060);

  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:-1repeatDelay:1.0yoyotrue});

  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:-1repeatDelay:1.0yoyotrue});

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」获取源码下载链接。

↓↓↓

前端新世界
关注前端技术,分享互联网热点
 最新文章