Three.js实现文字雨效果,文字垂直掉落

本文目标是实现如下文字雨垂直降落的效果,如下动图:

代码仓库:

1. 功能概览

  1. 实现文字雨垂直下落,下降速度不同,有远近的效果
  2. 底部有同心圆,同心圆会发生旋转
  3. 插入文字标签,并跟随同心圆一起旋转
  4. 双击暂停动画,再次双击继续开始动画

2. 基础架构搭建

  1. 在vite或者vue项目中开发都可以,我是在vite项目中开发

  2. 必须安装好three.js的npm包

  3. main.js中引入three

js 复制代码
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import GUI from 'three/examples/jsm/libs/lil-gui.module.min.js';
  1. 场景、摄像机、渲染器、轨道控制器
js 复制代码
// 创建场景
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xdec9aa)

// 创建相机
const camera = new THREE.PerspectiveCamera(
  45, // 视角
  window.innerWidth / window.innerHeight, // 宽高比
  0.01, // 近平面
  100 // 远平面
);

// 设置相机位置
camera.position.z = 50;
camera.position.y = 2.5;
camera.position.x = 3;
camera.lookAt(0, 1.2, 0);

// 创建渲染器
const renderer = new THREE.WebGLRenderer({
  antialias: true, // 开启抗锯齿
});
renderer.shadowMap.enabled = true;
renderer.toneMapping = THREE.ReinhardToneMapping
renderer.toneMappingExposure = 1
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 添加轨道控制器,注意轨道控制器不需要加入到场景里面去
const controls = new OrbitControls(camera, renderer.domElement);
// 设置带阻尼的惯性
controls.enableDamping = true;
// 设置阻尼系数
controls.dampingFactor = 0.05;
// 设置旋转速度
// controls.autoRotate = true;
controls.zoom = 20

3. 文字雨效果

  1. 准备数据源,要下降的文字,这个数据一般要从接口拿到
js 复制代码
const groupsTexts = [
    "生而不有",
    "是以圣人处无为之事",
    "难易想成",
    "皆知善之为善",
    "功成而弗居",
    "是以不去",
    "长短相较",
    "行不言之教",
    "天下皆知美之为美",
    "长短相教",
    "万物作焉而不辞",
    "生而不有",
    "是以圣人处无为之事",
    "难易想成",
    "皆知善之为善",
    "功成而弗居",
    "是以不去",
    "长短相较",
    "行不言之教",
    "天下皆知美之为美",
    "长短相教",
    "万物作焉而不辞",
    "生而不有",
    "是以圣人处无为之事",
    "难易想成",
    "皆知善之为善",
    "功成而弗居",
    "是以不去",
    "长短相较",
    "行不言之教",
    "天下皆知美之为美",
    "长短相教",
    "万物作焉而不辞",
    "生而不有",
    "是以圣人处无为之事",
    "难易想成",
    "皆知善之为善",
    "功成而弗居",
    "是以不去",
    "长短相较",
    "行不言之教",
    "天下皆知美之为美",
    "长短相教",
    "万物作焉而不辞",
    "生而不有",
    "是以圣人处无为之事",
    "难易想成",
    "皆知善之为善",
    "功成而弗居",
    "是以不去",
    "长短相较",
    "行不言之教",
    "天下皆知美之为美",
    "长短相教",
    "万物作焉而不辞"
]
  1. createTextSprite创建一个精灵字体
js 复制代码
// 创建文字材质函数,基于canvas画布生成纹理
function createTextSprite(text, color='white', opacity = 1) {
    const canvas = document.createElement('canvas');
    const size = 256;
    canvas.width = size; // 画布宽高
    canvas.height = size;
    const ctx = canvas.getContext('2d'); // 获取画布的2d绘图上下文
    ctx.clearRect(0, 0, size, size); // 清空整个画布区域
    ctx.fillStyle = color; // 绘制文本的颜色
    ctx.globalAlpha = opacity; // 绘制文本的透明度
    ctx.font = 'bold 64px Arial'; // 字体
    ctx.textAlign = 'center'; // 文本水平对其
    ctx.textBaseline = 'middle'; // 文本垂直对其
    ctx.fillText(text, size / 2, size / 2); // 画布中心绘制这个文字
    const texture = new THREE.CanvasTexture(canvas); // 用画布生成THREE.JS纹理,用于后续贴图使用
    texture.minFilter = THREE.LinearFilter; // 设置为线性过滤,避免缩放时产生模糊或者锯齿

    
    const material = new THREE.SpriteMaterial({ // 精灵材质
        map: texture, 
        transparent: true, 
        opacity: opacity, // 控制字体透明度,产生远近的效果
        depthWrite: false, // 关闭深度写入,能够减少透明物体的冲突
        depthTest: true,
    });
    // 用精灵物体更加快捷
    const sprite = new THREE.Sprite(material);
    sprite.scale.set(4, 4, 1);  // 缩放文字大小
    return sprite;
}
  • 借助canvas标签绘制文字,设置文字的大小 颜色 宽高 透明度 对齐效果
  • canvas标签借助CanvasTexture转化为一个贴图texture
  • SpriteMaterial精灵材质中,将texture赋值给map属性,同时注意开启透明transparent,设置透明度;关闭深度写入(这个很关键,不关闭的话,会出现抖动的情况)
  • 最后再创建精灵物体
  1. createTextGroup创建一个字体组合
js 复制代码
function createTextGroup(chars, opacity=1) {
    const group = new THREE.Group();
    for (let i = 0; i < chars.length; i++) {
        const sprite = createTextSprite(chars[i], '#5a3814', opacity);
        // 竖直排列的关键点
        sprite.position.set(0, -i * 1, 0);  // 竖直排列,间隔5单位
        group.add(sprite);
    }
    return group;
}
  • 这个方法的作用是,将教学相长这样的一个组合拆分,每个字单独调用createTextSprite方法,
  • 得到的sprite物体添加到group组合中
  • sprite.position.set(0, -i * 1, 0)这行代码是设置文字垂直排列的关键,教学相长的下面,所以他的y设置的是-i * 1,y轴越往上,值越大
  1. 遍历所有数据,创建文字组
js 复制代码
const allGroups = [];
groupsTexts.forEach((chars, idx) => {
    const group = createTextGroup(chars, Math.random());
    group.position.set(
        (Math.random() - 0.5) * 40,  // x随机散布
        Math.random() * 50 + 20,     // y开始在顶部附近
        (idx % 2) * 2               // z位置稍微有差异,制造远近
    ); // 每个字体组合的下降位置不尽相同
    allGroups.push({
        group,
        speed: Math.random() * Math.random() * 0.1 + 0.1 // 下降的速度
    });
    scene.add(group);
});

animate动画中:

js 复制代码
function animate() {
    requestAnimationFrame(animate);

    // 文字组下落
    allGroups.forEach(item => {
        item.group.position.y -= item.speed; // 下落的关键

        if (item.group.position.y < -3) {  // 下面越界后重置到顶部随机位置
            item.group.position.y = 50 + Math.random() * 20;
            item.group.position.x = (Math.random() - 0.5) * 40;
        }
    });

    renderer.render(scene, camera);
}

animate();

这样以后,能够实现如下效果:

4. 底部旋转同心圆

  1. 要创建的同心圆有5层,
js 复制代码
const circleList = [
    {
        radius: 6, 
        color: '#5a3814'
    },
    {
        radius: 12, 
        color: '#5a3814'
    },
    {
        radius: 18, 
        color: '#5a3814'
    },
    {
        radius: 24, 
        color: '#5a3814'
    },
    {
        radius: 30, 
        color: '#5a3814'
    }
]
let circleMeshs = []
circleList.forEach(item => {
    createCircle(item)
})
  1. createCircle方法,创建单个圆
js 复制代码
function createCircle (item) {
    // 创建渐变画布
    const size = 256;
    const canvas = document.createElement('canvas');
    canvas.width = size;
    canvas.height = size;
    
    const ctx = canvas.getContext('2d');
    // 设置渐变:createRadialGradient(x0, y0, r0, x1, y1, r1)  => (x0, y0)起始圆心坐标,r0是半径;
    // (x1, y1)结束圆圆心坐标,r1是结束圆半径,表示从中心到外围的镜像渐变
    const gradient = ctx.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size/2);
    gradient.addColorStop(0, 'white');
    gradient.addColorStop(1, item.color); // 从外到里渐变,最里面是白色
    // 设置画笔
    ctx.fillStyle = gradient;
    // 填充的范围是整个画布
    ctx.fillRect(0, 0, size, size);

    // 纹理
    const texture = new THREE.CanvasTexture(canvas);
    // 圆形平面
    const geometry = new THREE.CircleGeometry(item.radius, 32); // 半径1,32段细分
    const material = new THREE.MeshBasicMaterial({
        color: 0xe4d6c3, 
        map: texture,
        side: THREE.DoubleSide,
        transparent: true,
        depthWrite: false, //深度写入
        depthTest: true, // 深度检测
        opacity: 0.1
    });
    const circle = new THREE.Mesh(geometry, material);
    circle.rotation.x = Math.PI / 2 // 沿着x轴旋转90°
    circle.position.y = -3
    scene.add(circle);
    // 将同心圆添加到数组中
    circleMeshs.push(circle)
}
  • 圆用的是CircleGeometry
  • 注意,必须关闭deepWrite深度写入,否则可能只能看到最外层的圆
  1. animate动画
diff 复制代码
function animate() {
    requestAnimationFrame(animate);

    // 文字组下落
    allGroups.forEach(item => {
        item.group.position.y -= item.speed;

        if (item.group.position.y < -3) {  // 下面越界后重置到顶部随机位置
            item.group.position.y = 50 + Math.random() * 20;
            item.group.position.x = (Math.random() - 0.5) * 40;
        }
    });

    // 让同心圆转起来
+    circleMeshs.forEach(circle => {
+        circle.rotation.z += 0.01 // 这里我有点困惑,实际上看起来是绕着y轴旋转的,但是设置必须设+置为z才行
+    })

    renderer.render(scene, camera);
}

animate();

这样以后的效果:

5. 文字标签

想要创建这样的文字标签,并且跟随同心圆一起移动:

  1. 创建标签的方法,注意标签有背景色,还有文字
js 复制代码
function createLabel (text) {
    // 用canvas写文字
    const width = 128
    const height = 128
    const canvas = document.createElement('canvas')
    canvas.width = width
    canvas.height = height

    const ctx = canvas.getContext('2d')
    const gradient = ctx.createRadialGradient(width / 2, height / 2, 0, width / 2, height / 2, width / 2);
    gradient.addColorStop(0, '#ffffff'); // 边缘白色
    gradient.addColorStop(1, 'pink'); // 中心深棕色
    ctx.fillStyle = gradient // 画笔设置为渐变
    ctx.fillRect(0, 0, width, height) // 修改画布颜色

    ctx.font = '24px Arial'
    ctx.fillStyle = '#ae7c47' // 修改画笔的颜色来绘制文字
    ctx.textAlign = 'center'
    ctx.textBaseline = 'middle'
    ctx.fillText(text, width / 2, height / 2) // 填充文字

    // canvas当做纹理贴图
    const texture = new THREE.CanvasTexture(canvas) 
    texture.minFilter = THREE.LinearFilter // 防止模糊

    // 平面几何体,稍宽,适合文字
    const geometry = new THREE.PlaneGeometry(3.5, 1.75); // 可以调整比例,使更扁宽贴圆

    const material = new THREE.MeshBasicMaterial({
        map: texture,
        transparent: true,
        side: THREE.DoubleSide,
        depthWrite: false,
        depthTest: true,
    });
    const mesh = new THREE.Mesh(geometry, material);
    return mesh
}
  • 上面的步骤和之前的类似,唯一要注意的是canvas画布的背景和文字颜色不同,所以要记得修改画笔的颜色后再去绘制
js 复制代码
const labelGroup = new THREE.Group()
scene.add(labelGroup)
const radius4 = circleList[circleList.length - 2].radius
const radius5 = circleList[circleList.length - 1].radius 
// 让标签的位置处于第四和第五个圆之间
const labelRadius = (radius4 + radius5) / 2
// 标签内容
const labels = ['AG致用', '二元辩证', '原点规律', '历史背景', '成长案例'];
const labelCount = labels.length
for (let i = 0; i < labels.length; i++) {
    const angle = (i / labelCount) * Math.PI * 2
    const labelMesh = createLabel(labels[i])
    // 位置坐标转化
    labelMesh.position.set(
        labelRadius * Math.cos(angle),
        -3,
        labelRadius * Math.sin(angle),
    )
    labelMesh.lookAt(0, 0, 0)
    labelMesh.rotateY(Math.PI)
    labelGroup.add(labelMesh)
}

上面的坐标转化见下图:

这行代码是让标签均匀分布在圆上:

js 复制代码
const angle = (i / labelCount) * Math.PI * 2

最终在animate函数中更新标签的位置

diff 复制代码
function animate() {
    if (!isAnimate) return
    requestAnimationFrame(animate);

    // 文字组下落
    allGroups.forEach(item => {
        item.group.position.y -= item.speed;

        if (item.group.position.y < -3) {  // 下面越界后重置到顶部随机位置
            item.group.position.y = 50 + Math.random() * 20;
            item.group.position.x = (Math.random() - 0.5) * 40;
        }
    });

    // 让同心圆转起来
    circleMeshs.forEach(circle => {
        circle.rotation.z += 0.01
    })

    // 让标签转起来
+    labelGroup.rotation.y += 0.01

    renderer.render(scene, camera);
}
相关推荐
魔云连洲3 小时前
详细解释浏览器是如何渲染页面的?
前端·css·浏览器渲染
Kx…………3 小时前
Day2—3:前端项目uniapp壁纸实战
前端·css·学习·uni-app·html
培根芝士5 小时前
Electron打包支持多语言
前端·javascript·electron
mr_cmx5 小时前
Nodejs数据库单一连接模式和连接池模式的概述及写法
前端·数据库·node.js
东部欧安时6 小时前
研一自救指南 - 07. CSS面向面试学习
前端·css
涵信6 小时前
第十二节:原理深挖-React Fiber架构核心思想
前端·react.js·架构
ohMyGod_1236 小时前
React-useRef
前端·javascript·react.js
每一天,每一步6 小时前
AI语音助手 React 组件使用js-audio-recorder实现,将获取到的语音转成base64发送给后端,后端接口返回文本内容
前端·javascript·react.js
上趣工作室6 小时前
vue3专题1------父组件中更改子组件的属性
前端·javascript·vue.js
冯诺一没有曼6 小时前
无线网络入侵检测系统实战 | 基于React+Python的可视化安全平台开发详解
前端·安全·react.js