失败狮计划再现——threejs实现文字环绕动画

前言

说到环绕动画,18年初的时候我曾写过一版太阳系动画,当然,那时完全是慢慢调试坐标然后用css动画写上去的,后来有同事把这个太阳系动画使用threejs改写了,动画变得更加顺滑流畅了,那时我只是对three有个印象,没想到3年后我也有使用到它的一天。(然鹅也并没有真的派上用场)具体需求也很简单明了:文字围绕着中心的图片进行绕转,但是文字和一个球体的区别还是蛮大的。

先看我实现出来的效果:(本来想实现环球影城那种效果的,自行脑补universal系列电影的开头文字环绕地球,可惜学艺不精的我就只能搞出来这种僵硬的效果了)


文字环绕图片旋转

效果虽一般,但是通过这个过程我对threejs的使用也算是入了点门了,再来看实现过程:

实现思路

按照threejs中文网的说法,一共有五种创建文字的方法:
http://www.webgl3d.cn/threejs/docs/#manual/zh/introduction/Creating-text

  1. DOM + CSS
  2. 将文字绘制到画布中,并将其用作Texture(纹理)
  3. 在你所喜欢的3D软件里创建模型,并导出给three.js
  4. three.js自带的文字几何体
  5. 位图字体

其中第一种只是创建了一个说明性的文字定位到了图上,和three并无关系。第二种没找到相关的例子,我怀疑用第二种想让文字动起来还要用点别的操作。第三种不谈了。第四种和第五种是网上比较推崇的方法,搜到的示例也都是这两种居多,但是我试了一下第四种,生成文字需要一种json文件(有ttf文件转换的方法),而中文文件普遍大于20mb,在项目中使用也是three直接读取json,里面的内容应该是无法压缩了,不说生产环境,我在开发环境读取本地文件都要1秒多,这要是上线了应该10秒起步,而且这个页面还是网站的首页,所以直接pass,第五种方法同理,对中文的支持也不是很好。

到此看起来出路都被堵住了,不过老话说得好,山穷水尽疑无路,柳暗花明又一村。我在找资料时找到了一篇文章,介绍了使用three的canvas2drenderer渲染将中文作为贴图显示,并做出的动态弹幕的效果,然后我在源码中并没有发现这个渲染器,正如这篇文章所说的canvas2drenderer已经被废弃了......不过既然是废弃了,应该是有替代品的,果然我发现了一个css2drenderer和css3drenderer。
(见https://github.com/mrdoob/three.js/tree/dev/examples/jsm/renderers
之后最重点的部分来了:在three中文网的示例中我终于找到了一个行星绕转的例子,由此文字公转的公式我算是在这个例子的源码中找到了:x坐标做正弦改变,z坐标做余弦改变。因为行星绕转即使不需要自转用户也看不出太大的区别,但是文字不一样,文字是一个长方形,公转的同时还需要缓慢自转,公转一周之时自转也需要恰好一周。所以需要一个沿y轴的自转角度用来处理自转变化(通过2d渲染器无法实现自转,按照CSS2DRenderer的描述,唯一支持的变换是位移,可能旋转变化不属于位移??),最后我参考了几个3d渲染器的示例来实现自转变化(其实就是改变3d对象的rotation属性,2d对象没有找到这个属性)。

最后需要说明一下这个实现方法算是另辟蹊径吧,因为threejs本身是靠WEBGL作为渲染器的,而我使用的是CSS3DRenderer渲染器,正如官网所说:CSS3DRenderer用于通过CSS3的transform属性, 将层级的3D变换应用到DOM元素上。......然而,这一渲染器也有一些十分重要的限制:1.它不可能使用three.js中的材质系统。2.同时也不可能使用几何体。本质上我用的其实还是css3动画,只不过使用了three提供的一些api方便了实现过程罢了。

代码

首先安装three什么的就不说了,先看引入方式

import * as THREE from 'three';
// 或者 按需引入 --根据vscode插件显示 压缩之后按需引入也只减少了20k的大小,我这还只是引用了核心的功能而已,所以大可整体引入以便后续维护扩展
import { Clock, PerspectiveCamera, Scene } from 'three';

// 关键渲染器的引入
import { CSS3DObject, CSS3DRenderer, CSS3DSprite } from 'three/examples/jsm/renderers/CSS3DRenderer';

创建threejs基本的场景需要“三件套” 渲染器,照相机,场景对象。执行渲染器的render方法就能渲染出three的场景,如果周期改变属性再渲染就能实现动画了。

    <!-- template -->
    <div id="animaBox">
      <!-- 中心的脑部图片 -->
      <img ...>
      <!-- 文字v-for (不需要设置定位位置,由three控制位置) -->
      <div></div>
    </div>
    // data
    box: {
        width: 0,
        height: 0,
        CSS3DRenderer: null,
        scene: null,
        camera: null,
        drgs: [], // 二维数组 文字CSS3DObject
        kx: 0, // 坐标轴运动轨迹基数
        ky: 0,
        kz: 0,
        clock: null, // threejs时间对象 调试用
        frame: 0, // 当前动画帧 --调试用
      },
    // methods 顺次执行即可
    // 设置threejs渲染器
    initRenderer() {
      this.box.width = document.getElementById('animaBox').clientWidth;
      this.box.height = document.getElementById('animaBox').clientHeight;
      const renderer = new CSS3DRenderer();
      renderer.setSize(this.box.width, this.box.height);
      document.getElementById('animaBox').appendChild(renderer.domElement);
      renderer.domElement.style.cssText += ';position:absolute;left:0;top:0;';
      this.box.CSS3DRenderer = renderer;
      this.box.clock = new THREE.Clock();
    },
    // 设置摄像机camera
    initCamera() {
      const camera = new THREE.PerspectiveCamera(45, this.box.width / this.box.height, 0.1, 1000);
      camera.position.z = rem2px(6);
      this.box.camera = camera;
    },
    // 设置场景
    initScene() {
      this.box.scene = new THREE.Scene();
    },
    // 渲染图片
    initImage() {
      const imgEle = document.getElementById('centerImg');
      const imgObj = new CSS3DSprite(imgEle);
      const { x, y, z } = this.imgPosition;
      imgObj.position.set(x, y, z);
      this.box.scene.add(imgObj);
    },
    // 渲染文字(文字分成了两排所以存为二维数组,关心最内部的逻辑就好)
    initWord() {
      const ellipseEles = document.querySelectorAll('.qccenter-text-box'); // 椭圆轨道
      ellipseEles.forEach((ellipse, ind1) => {
        this.box.drgs.push([]); // 创建二维数组
        const textEles = document.querySelectorAll(`.qccenter-text-${ind1}`); // 病种
        textEles.forEach((item, ind2) => {
          const objectCSS = new CSS3DObject(item);
          this.box.scene.add(objectCSS);
          this.box.drgs[ind1][ind2] = objectCSS;
        });
      });
    },
    // 运动 关键方法
    animate() {
      requestAnimationFrame(this.animate);
      const { CSS3DRenderer, drgs, scene, camera, clock } = this.box;
      let { kx, ky, kz, frame } = this.box;
      const { vx, vy, vz } = this.moveVelocity; // 此处3轴方向运动速度差不多是慢慢调出来的
      // frame++; // TODO:调试用
      // let kxold = kx; // TODO:调试用
      // 基数更新
      kx += vx;
      kz += vz;
      ky += vy; // 每帧自转弧度
      this.box = Object.assign({}, this.box, { kx, ky, kz, frame });
      
      // 设置文字位置
      drgs.forEach((CSS3DObjects, ind1) => {
        // drgConfigs为文字配置信息这里主要就用到frame和originalY属性
        this.drgConfigs[ind1].forEach((drg, ind2) => {
          // frame为该段文字起始的动画帧(根据调试出的总动画帧数估算),originalY 为y方向初始高度(只有一行文字为0即可)
          const { frame: frameInner, originalY } = drg;
          const drgObj = CSS3DObjects[ind2];
          const x = kx + vx * frameInner;
          const y = ky + vy * frameInner;
          const z = kz + vz * frameInner;
          drgObj.position.set(Math.sin(x) * rem2px(3), originalY, Math.cos(z) * rem2px(1));
          drgObj.rotation.y = y;
        });
      });

      // 计算公转一周所需时间 --TODO:调试用
      // if (Math.sin(kx) >= 0 && Math.sin(kxold) < 0){
      //   console.log(clock.getElapsedTime());
      //   console.log(frame);
      // }
      
      CSS3DRenderer.render(scene, camera);
    },

说明:

  1. 这里我将three用到的东西全放在一个对象里保存。由于页面内容要实现自适应,所以涉及到尺寸的地方都使用了自己封装的rem2px方法
  2. 中间的图片必须为一个three的图片对象并放入场景,这样才能使图片看起来是“立体的”,否则就真的是一个贴图贴上面了,文字会在图片面前完成公转而不是环绕。
  3. 最初我本来想用椭圆方程来根据不同的x坐标求出不同点的文字的初始z坐标的,但是实现出来的效果更加僵硬了,而且无法计算出不同地方的初始自转角,后来灵光一闪采用动画帧的概念,直接把动画帧加入计算才实现了开头呈现的效果,不过我调试出来的动画帧数还是有点误差的,没办法,还好测试10几轮公转没有出现什么问题。

后记

这个动画的结局当然是被产品经理否决了,最终在我建议下改成了文字块进行小范围平面上下移动——这也是在很多网站中能看到的效果。不过这么折腾了几天我也算对threejs有了一个初步的了解吧。这种东西对我来说很难通过所谓的爱好来驱动学习......另外,上一次在文章中提到失败狮这个词的时候我还是海军提督,没想到3年后的现在却成为魔法师了(~ ̄▽ ̄)~

参考内容

three.js 用中文字作为贴图
three官网案例1
three官网案例2

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,113评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,644评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,340评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,449评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,445评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,166评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,442评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,105评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,601评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,066评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,161评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,792评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,351评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,352评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,584评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,618评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,916评论 2 344

推荐阅读更多精彩内容