­

那个前端写的页面好酷——大量的粒子(元素)的动效实现

  • 2019 年 12 月 1 日
  • 筆記

如何构建粒子世界

基于THREE.CSS3dObject

CSS3dObject这个对象,可以让我们像操作threejs对象那样来操作div,使用threejs丰富的api来实现css+div的3d效果。实际上最终效果就是把threejs的参数转化为css的matrix。我们看一段简单的代码,这是创建一个div元素,然后使用three的api控制它的位置:

      const element = document.createElement("div");        element.className = "element";        element.style.width = "4px";        element.style.height = "4px";        element.style.borderRadius = "50%";        const object = new THREE.CSS3DObject(element);          // 使用threejs的api        object.position.x = x;        object.position.y = y;        object.position.z = z || 0;  复制代码

当然,这些代码还不足以渲染出来,因为three还是得使用three那套流程:场景、相机、渲染器,将物体加入场景,渲染器render。这样子,才能让3d世界展示眼前

// 场景  const scene = new THREE.Scene();  // 相机  const camera = new THREE.PerspectiveCamera(    40,    window.innerWidth / window.innerHeight,    1,    10000  );  camera.position.z = 2000;  // 渲染器  const renderer = new THREE.CSS3DRenderer();  // 把对象加入场景  scene.add(object);  // 渲染  renderer.render(scene, camera);  document.body.appendChild(renderer.domElement);  复制代码

结果是一个个div:

最终效果

适用场景:量级为几十,炫酷的、具有交互的页面元素。

基于THREE的粒子系统

使用THREE.Points粒子系统实现

// 球坐标下的标准球方程  // 这里是球坐标和直角坐标的转换  // size相当于球坐标的r  const getSphere = (i, count, size) => {    const phi = Math.acos(-1 + (2 * i) / count);    const theta = Math.sqrt(count * Math.PI) * phi;    return {      x: size * Math.cos(theta) * Math.sin(phi),      y: size * Math.sin(theta) * Math.sin(phi),      z: -size * Math.cos(phi)    };  };    const color = new THREE.Color();  // 1000 个点  for (let i = 0; i < 1000; i++) {    // 获取点的坐标    const { x, y, z } = getSphere(i, 1000, 500);    positions.push(x, y, z);    // 设置颜色    color.setRGB(2 * Math.random(), 2 * Math.random(), 2 * Math.random());    colors.push(color.r, color.g, color.b);  }  // 创建缓冲几何体  const geometry = new THREE.BufferGeometry();  // 给几何体加上属性,一些版本的设置属性函数的名称为setAttribute  // positions: [x1, y1, z1, x2, y2, z2, ...]  geometry.addAttribute(    "position",    new THREE.Float32BufferAttribute(positions, 3)  );  // colors: [r1, g1, b1, r2, g2, b2, ...]  geometry.addAttribute("color", new THREE.Float32BufferAttribute(colors, 3));    // 给粒子系统加入PointsMaterial材料  const points = new THREE.Points(    geometry,    new THREE.PointsMaterial({      size: 20, // 粒子大小      vertexColors: THREE.VertexColors // 使用顶点颜色    })  );  scene.add(points);    // 动起来,体验一下立体感  function animate() {    requestAnimationFrame(animate);    render();  }  function render() {    points.rotation.x += 0.0025;    points.rotation.y += 0.005;    renderer.render(scene, camera);  }  复制代码

此时,可以看见由一个个点构成的几何体展示出来了。demo代码在codesandbox的vec.html

适用场景:量级大,无细微交互、丰富的粒子变换场景

基于paint api

这个不多说,之前我另一篇文章已经介绍过

适用场景:chrome浏览器,支持复杂的动画,但只能简单的交互且没有参数输出

tweenjs

tweenjs是一个数据缓动的库,里面有一些内置的缓动函数,通常用于动画。使用方法:

const o = { v: 0 }  new TWEEN.Tween(o)    .delay(10000) // 延迟10s    .to({ v: 1 }, 5000) // 5s内把v从0变成1    .start(); // 执行    复制代码

tween自带缓动效果预览

与tween结合

THREE.CSS3dObject与tween结合

基于CSS3dObject实现的,如何动起来?举个例子,一开始,先把全部点放在原点。然后,把这些点缓动成一个球。缓动成球的方法:生成球的坐标点集合,遍历全部在原点的点集,一个个地添加tween,缓动到最终球的坐标点坐标上:

        const count = 60;  // 先放在原点          Object.assign(object.position, {            x: 0,            y: 0,            z: 0          });    // 缓动到球的每一个点的坐标上          const phi = Math.acos(-1 + (2 * i) / count);          const theta = Math.sqrt(count * Math.PI) * phi;            const SIZE = 800;            new TWEEN.Tween(object.position)            .delay(1500)            .to({              x: SIZE * Math.cos(theta) * Math.sin(phi),              y: SIZE * Math.sin(theta) * Math.sin(phi),              z: -SIZE * Math.cos(phi)            })            .start();  复制代码

此时,效果也可以想象出来,就是像爆炸一样

demo地址:gjtyz.csb.app/sphere.html

粒子系统与tween结合

粒子系统使用的是缓冲几何体,我们可以自己给缓冲几何体加上一些自定义属性,然后通过顶点着色器来读取,达到控制顶点属性的效果。

着色器

webgl的着色器的是gpu执行的,所以性能很好,大量的粒子动态渲染都可以不卡。接下来,我们实现一个位置、大小、颜色同时缓动的粒子特效。参考官方文档,我们修改一下代码,得到满足我们需求的顶点着色器代码:

        attribute float size;          attribute vec3 position2;          uniform float lamda; // 自己传入          uniform float size1; // 自己传入          void main() {              vec3 temp;              temp.x = position.x * lamda + position2.x * (1. - lamda);              temp.y = position.y * lamda + position2.y * (1. - lamda);              temp.z = position.z * lamda + position2.z * (1. - lamda);              vec4 mvPosition = modelViewMatrix * vec4( temp, 1.0 );              gl_PointSize = size1;              gl_Position = projectionMatrix * mvPosition;          }  复制代码

改变颜色的是片元着色器:

// 传入color变量          uniform vec3 color;          void main() {              gl_FragColor = vec4(color, 1.0);          }  复制代码

canvas文字转粒子坐标

大概的思路:创建一个canvas,获取ctx,使用ctx写红色字。再遍历getImageData的点,发现是红点,则把红点坐标加入。最终返回结果是Float32Array类型化数组(x1, y1, z1, x2, y2, z2……),因为THREE的几何体addAttribute的时候需要Float32Array对象

    function getTxtData(str) {        const c = document.createElement("canvas");        c.width = innerHeight;        c.height = innerWidth;        const t = c.getContext("2d");        t.fillStyle = "#f00";        t.font = "150px Georgia";        t.fillText(str, 100, 100);        const { data, width, height } = t.getImageData(          0,          0,          innerHeight,          innerWidth        );        const temp = [];        const gap = 3;        for (let w = 0; w < width; w += gap) {          for (let h = 0; h < height; h += gap) {            const index = (h * width + w) * 4;            if (data[index] == 255) {              temp.push(w / 10, -h / 10, 0);            }          }        }        return new Float32Array(temp);      }  复制代码

给缓冲几何体加上属性&加入tween

      const material = new THREE.MeshBasicMaterial({ color: 0xff0000 });        const geometry = new THREE.BufferGeometry();  // 字多的、复杂的,粒子多        const m = getTxtData("I am here");        geometry.addAttribute("position", new THREE.BufferAttribute(m, 3));  // 字少的、简单的,粒子少        const newPositions = getTxtData("lhyt");        const newLen = newPositions.length;        const positionArr = geometry.attributes.position.array;          const positionLen = positionArr.length;        const position2 = new Float32Array(positionLen);        position2.set(newPositions);        // 顶点较少的模型顶点坐标,后半部分从头开始赋值        // 其实也可以隐藏的,不过重复赋值效果好一些        for (let i = newLen, j = 0; i < positionLen; i++, j++) {          position2[i] = newPositions[j];          position2[i + 1] = newPositions[j + 1];          position2[i + 2] = newPositions[j + 2];        }    // position2是第二个状态,粒子多的那个状态        geometry.addAttribute(          "position2",          new THREE.BufferAttribute(position2, 3)        );  // 给tween用的缓动操作对象        const o = {          v: 0,          s: 1,          c: 0x00ffff        };        let uniforms = {          // 顶点颜色          color: {            type: "v3",            value: new THREE.Color(o.c)          },          // 传递lamda、size1值,用于shader计算顶点位置          lamda: {            value: o.v          },          size1: {            value: o.s          }        };        // 着色器材料        const shaderMaterial = new THREE.ShaderMaterial({          uniforms,          vertexShader: `          attribute float size;          attribute vec3 position2;          uniform float lamda; // 自己传入          uniform float size1; // 自己传入          void main() {              vec3 temp;  // lamda为0,取position;lamda为0,取position2,达到状态切换的效果              temp.x = position.x * lamda + position2.x * (1. - lamda);              temp.y = position.y * lamda + position2.y * (1. - lamda);              temp.z = position.z * lamda + position2.z * (1. - lamda);  // emm,因为需要4维矩阵来运算              vec4 mvPosition = modelViewMatrix * vec4( temp, 1.0 );              gl_PointSize = size1;              gl_Position = projectionMatrix * mvPosition;          }          `,          fragmentShader: `          uniform vec3 color;          void main() {              gl_FragColor = vec4(color, 1.0);          }          `,          blending: THREE.AdditiveBlending,          transparent: true        });        // 创建粒子系统        const particleSystem = new THREE.Points(geometry, shaderMaterial);          scene.add(particleSystem);          new TWEEN.Tween(o)          .delay(1000)          .to({ v: 1, s: 10, c: 0x0000ff }, 2000) // 切换的参数最终状态          .onUpdate(() => {  // tween更新的时候,把粒子系统的uniforms里面参数也更新,着色器也会根据参数更新            particleSystem.material.uniforms.lamda.value = o.v;            particleSystem.material.uniforms.size1.value = o.s;            particleSystem.material.uniforms.color.value = new THREE.Color(o.c);          })          .start();  复制代码

后面就是一些渲染、raf方法,具体代码参考codesandbox的textParticle.html部分的代码