我用 threeJs 写了一个 3D 的 87键键盘

1,633 阅读2分钟

前言

今天的主角是 THREEJS , three.js 是用 JavaScript 编写的 WebGl 第三方库,是前端实现 3D 可视化比较流行的一个方案. three.js 是一个比较复杂的库,它包含了许多 Api(如比较基本的 创建一个场景、摄像机、渲染器等),以及拓展的一些插件 想在短时间内熟练掌握还是比较困难的,可阅读three.js 中文文档,阅读文档还是比较枯燥和难懂的,在你掌握创建一个场景、相机、渲染器三个基本元素的情况下,建议阅读 three.js 官方提供的案例代码遇到不懂的代码再返回去查看中文文档.案例github地址(tip:examples目录下)

使用 three.js 写一个 3D 87键键盘(最后附带源码地址)

第一步:引入 threeJs 以及相关插件

// 引入 three.js
import * as THREE from '../build/three.module.js';
// TweenJS是使用JavaScript中的一个简单的补间动画库,在此案例中主要是实现 '键' 的动画
import { TWEEN } from '../common/jsm/libs/tween.module.min.js';
// 引入 TrackballControls 他是最常用的控件,可以使用鼠标轻松的移动、平移,缩放场景。
import { TrackballControls } from '../common/jsm/controls/TrackballControls.js';
// 将 DOM 元素转为 3d 元素;
import { CSS3DRenderer, CSS3DObject } from '../common/jsm/renderers/CSS3DRenderer.js';

第二步:声明 threeJs 场景内的基本元素和 87 的键 JSON 数据

// 87 键的 JSON 数据(在此只写了部分数据)
// 键名 键名二 键图标 键位置(x,y) 键宽高(height,width);
const table = [
    "Esc", "", "", 1, 1,160,160,
    "F1", "", "iconfont icon-jingyin", 3, 1,160,160,
    "F2", "", "iconfont icon-jianyinliang", 4, 1,160,160,
    "F3", "", "iconfont icon-jiayinliang", 5, 1,160,160,
    "F4", "", "", 6, 1,160,160,
    "F5", "", "", 7.5, 1,160,160,
    "F6", "", "", 8.5, 1,160,160,
    "F7", "", "", 9.5, 1,160,160,
    "F8", "", "", 10.5, 1,160,160,
    ... // 省略
    'Win','','',2,6,160,160,
    'Alt','','',3,6,160,160,
    '_____________','','',7.54,6,1280,160,
    'Alt','','',12.04,6,160,160,
    'Fn','','',13.04,6,160,160,
    '↖','','',14.04,6,160,160,
    'Ctrl','','',15.04,6,160,160,
    '←','','',16.53,6,160,160,
    '↓','','',17.53,6,160,160,
    '→','','',18.53,6,160,160,
];
// 场景、相机、渲染器、控件对象
let scene,camera,renderer,controls;
// 存放每个键的初始状态
let objects = [];
// 存放每个键的最终状态
const targets = {table:[]};

第三步:定义一个初始化函数并在初始化函数中赋值上述声明的变量

function init(){
    scene =  new THREE.Scene(); // 场景(必要)
    scene.position.y = -400; // 场景 y 轴上的位置
    camera = new THREE.PerspectiveCamera(40,window.innerWidth/window.innerHeight,1,10000); // 相机(必要)
    camera.position.z = 3000; // 相机在 z 轴上的位置

    // 循环 table 创建对应 '键' 的 html 元素;
    // 生成 键 的初始位置 和 最终位置
    for( let i = 0; i<table.length;i+=7){
        // 每个键(宽高背景颜色)
        const element = document.createElement('div');
        element.className = 'element';
        element.style.backgroundColor = 'rgba(0,0,0,'+(Math.random()*0.5+0.25)+')';
        element.style.height = table[i+6] + 'px';
        element.style.width = table[i+5] + 'px';
        element.style.display = 'inline-block';
        element.addEventListener('click',function(){
            clickHandle(table[i])
        })
        // 键的名称
        const symbol = document.createElement('span');
        symbol.className = 'symbol';
        symbol.textContent = table[i];
        element.appendChild(symbol);
        // 键的二级名称
        if(table[i+1]!=''){
            const details = document.createElement('span');
            details.className = 'detail';
            details.innerHTML = table[i+1];
            element.appendChild(details);
        }
        // 键的图标
        if(table[i+2]!=''){
            const iconBox = document.createElement('div');
            const icon = document.createElement('i');
            iconBox.appendChild(icon);
            iconBox.className = 'icon';
            icon.className = table[i+2]
            element.appendChild(iconBox);
        }

        // 初始随机生成键的位置
        const objectcss = new CSS3DObject( element );
        objectcss.position.x = Math.random()*4000 - 2000;
        objectcss.position.y = Math.random()*4000 - 2000;
        objectcss.position.z = Math.random()*4000 - 2000;
        scene.add(objectcss);

        objects.push(objectcss);

        // 键的最后位置
        const object = new THREE.Object3D();
        object.position.x = (table[i+3]*160) - 1330;
        object.position.y = -(table[i+4]*170) + 990;
        targets.table.push(object);
    }
    renderer = new CSS3DRenderer(); // 渲染器(必要)
    renderer.setSize(window.innerWidth,window.innerHeight);// 渲染器大小
    document.getElementById('container').appendChild(renderer.domElement);
    controls = new TrackballControls( camera, renderer.domElement );// 控件作用于这个场景,使场景可控
    controls.minDistance = 500;
    controls.maxDistance = 6000;
    controls.addEventListener('change',render);

    transform( targets.table, 2000 );// 动画
}

第四步:根据 键 随机生成的初始化位置(上述定义的objects) 动画到 键的 最终位置(上述定义的targets.table)

// 动画 使用到了 TWEEN 插件
function transform(targets,duration){
    TWEEN.removeAll();
    // 遍历初始化位置,从初始化位置动画到最终位置
    for(let i = 0;i<objects.length;i++){
        const object = objects[i];
        const target = targets[i];

        new TWEEN.Tween(object.position)
            .to({x:target.position.x,y:target.position.y,z:target.position.z},Math.random()*duration+duration)
            .easing(TWEEN.Easing.Exponential.InOut)
            .start();
        new TWEEN.Tween(object.rotation)
            .to({x:target.rotation.x,y:target.rotation.y,z:target.rotation.z},Math.random()*duration+duration)
            .easing(TWEEN.Easing.Exponential.InOut)
            .start();

        new TWEEN.Tween( this )
            .to( {}, duration * 2 )
            .onUpdate(render)
            .start();
    }
}

第五步:在基本元素都具备的条件下 定义渲染函数并在动画更新的时候调用(重新渲染)

function render() {
    renderer.render( scene, camera );
}

第七步:更新动画

function animate() {
    requestAnimationFrame( animate );
    TWEEN.update();
    controls.update();
}

last效果图

keyboard.jpg

源码

码云地址

  • 注意:源码下载后不能直接在浏览器中预览效果,因为依赖的静态资源会发生跨域问题,最简单的方法是使用 vscode 的 插件(Live Server)来打开html文件,它创建的服务会进行资源托管就不会有跨域的问题啦~

我的其他文章