最近在學習 three.js在拿example中的項目練手,用了一成天的時間模仿了一個炫酷的元素週期表,在原有的基礎上進行了一些改變。下面我會逐步講解這個項目,算是加深理解,讓你們提提意見。
由於我未搭建我的服務器。截幾張圖給你們看看效果我作的效果(大部分是和原來的同樣)。可能一部分人已經見過這個經典動畫了。(這裏是原項目地址:threejs.org/examples/cs…)css
除了優化了原來的HELIX和GRID形式的的排版以外,我用另一種方式也建立了兩種自定義的排版方式。等會分享給你們。html
下面是GitHub倉庫地址,文件很簡單,就一個HTML文件。想本身手動實現或者拿去用的能夠看一下。喜歡的給顆星星,不勝感激(請忽略代碼中的註釋哈哈)。前端
下面開始分析這個小項目git
技術棧
實現原理
話很少說,直接上代碼。
HTML結構github
<div id="container">
<!-- 選中菜單結構 start-->
<div id="menu">
<button id="table">TABLE</button>
<button id="sphere">SPHERE</button>
<button id="sphere2">SPHERE2</button>
<button id="plane">PLANE</button>
<button id="helix">HELIX</button>
<button id="grid">GRID</button>
</div>
<!-- end -->
</div>複製代碼
HTML部分很是簡單僅僅是一個包含六個控制轉換的按鈕的選擇欄,下面看看他們的樣式算法
#menu {
position: absolute;
z-index: 100;
width: 100%;
bottom: 50px;
text-align: center;
font-size: 32px
}
button {
border: none;
background-color: transparent;
color: rgba( 127, 255, 255, 0.75 );
padding: 12px 24px;
cursor: pointer;
outline: 1px solid rgba( 127, 255, 255, 0.75 );
}
button:hover {
background-color: rgba( 127, 255, 255, 0.5 )
}
button:active {
background-color: rgba( 127, 255, 255, 0.75 )
}複製代碼
首先將選擇欄絕對定位到窗口底部50px處,這裏注意z-index: 100,將其層級設置爲最高能夠防止hover,click事件被其它元素攔截。而後清除button默認樣式,並給它增長了:hover和:active僞類,使交互更生動。canvas
效果以下:數組
而後是118個DOM元素的結構和樣式,由於他們是在JavaScript代碼中動態建立了,這裏我單獨寫了一個元素的結構。bash
<div class="element">
<div class="number">1</div>
<div class="symbol">H</div>
<div class="detail">Hydrogen<br>1.00794</div>
</div>複製代碼
CSS樣式
.element {
width: 120px;
height: 160px;
cursor: default;
text-align: center;
border: 1px solid rgba( 127, 255, 255, 0.25 );
box-shadow: 0 0 12px rgba( 0, 255, 255, 0.5 );
}
.element:hover{
border: 1px solid rgba( 127, 255, 255, 0.75 );
box-shadow: 0 0 12px rgba( 0, 255, 255, 0.75 );
}
.element .number {
position: absolute;
top: 20px;
right: 20px;
font-size: 12px;
color: rgba( 127, 255, 255, 0.75 );
}
.element .symbol {
position: absolute;
top: 40px;
left: 0px;
right: 0;
font-size: 60px;
font-weight: bold;
color: rgba( 255, 255, 255, 0.75 );
text-shadow: 0 0 10px rgba( 0, 255, 255, 0.95 );
}
.element .detail {
position: absolute;
left: 0;
right: 0;
bottom: 15px;
font-size: 12px;
color: rgba( 127, 255, 255, 0.75 );
}複製代碼
注意box-shadow和text-shadow。下面是效果圖
經過box-shadow和text-shadow使DOM元素產生了立體感。
JavaScript部分首先定義了118個元素的數據儲存結構,這裏使用的是數組(因外數量較多,我只拿過來前二十五個,github代碼中有完整數據)
const table = [
"H", "Hydrogen", "1.00794", 1, 1,
"He", "Helium", "4.002602", 18, 1,
"Li", "Lithium", "6.941", 1, 2,
"Be", "Beryllium", "9.012182", 2, 2,
"B", "Boron", "10.811", 13, 2,
"C", "Carbon", "12.0107", 14, 2,
"N", "Nitrogen", "14.0067", 15, 2,
"O", "Oxygen", "15.9994", 16, 2,
"F", "Fluorine", "18.9984032", 17, 2,
"Ne", "Neon", "20.1797", 18, 2,
"Na", "Sodium", "22.98976...", 1, 3,
"Mg", "Magnesium", "24.305", 2, 3,
"Al", "Aluminium", "26.9815386", 13, 3,
"Si", "Silicon", "28.0855", 14, 3,
"P", "Phosphorus", "30.973762", 15, 3,
"S", "Sulfur", "32.065", 16, 3,
"Cl", "Chlorine", "35.453", 17, 3,
"Ar", "Argon", "39.948", 18, 3,
"K", "Potassium", "39.948", 1, 4,
"Ca", "Calcium", "40.078", 2, 4,
"Sc", "Scandium", "44.955912", 3, 4,
"Ti", "Titanium", "47.867", 4, 4,
"V", "Vanadium", "50.9415", 5, 4,
"Cr", "Chromium", "51.9961", 6, 4,
"Mn", "Manganese", "54.938045", 7, 4
]複製代碼
先來分析一下這個數據結構
"H", "Hydrogen", "1.00794", 1, 1,複製代碼
一共118個元素,每一個元素在table數組定義了五條數據分別是符號(symbol),英文全稱,質量(detail),元素在表格排版中所在的列(column)和行(row)這兩個數據在建立表格盤版的時我會說明使用方法。
let scene, camera, renderer, controls;
const objects = [];
const targets = {
grid: [],
helix: [],
table: [],
sphere: []
};複製代碼
這裏定義了一些全局變量。scene,camera,renderer是three.js的環境對象,相機及渲染器。controls是three.js提供控制庫,用於與用戶交互,很簡單。objects用於存儲118個DOM元素。targets對象包含四個數組類型的屬性值,用來保存存有不一樣排版目標位置的Object3D子對象。
元素的建立以及動畫的控制由init函數執行,下面主要的篇幅用於將它
function init() {
const felidView = 40;
const width = window.innerWidth;
const height = window.innerHeight;
const aspect = width / height;
const nearPlane = 1;
const farPlane = 10000;
const WebGLoutput = document.getElementById('container');
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera( felidView, aspect, nearPlane, farPlane );
camera.position.z = 3000;
renderer = new THREE.CSS3DRenderer();
renderer.setSize( width, height );
renderer.domElement.style.position = 'absolute';
WebGLoutput.appendChild( renderer.domElement );
複製代碼
(可能個人代碼縮進比較奇怪,我主要是爲了趣味性哈哈)這段代碼建立了three.js的三個基本組件,場景,相機(perspectiveCamera),渲染器。這裏須要注意的是,這裏的far-clipping-plane設置 的值比較大,本身作的話能夠設置小一些,下降性能損耗。注意這裏採用的是CSS3D渲染器。
透視相機的視錐圖
平面之間的部分被稱爲視錐,簡單點來講就是相機的拍攝區域。圖上的fov(視場)是相機的第一個參數,決定了相機拍攝範圍的大小,相似於人眼的橫向視域(大於180deg了吧)。aspect參數控制相機投影平面的寬高比(通常是canvas的寬高比)這個主要是爲了防止圖片變形,由於投影平面上的圖像最終會經過canvas顯示。注意使用CSS3D渲染器時,顯示視口是div元素。
let i = 0;
let len = table.length;
for ( ; i < len; i += 5 ) {
const element = document.createElement('div');
element.className = 'element';
element.style.backgroundColor = `rgba( 0, 127, 127, ${ Math.random() * 0.5 + 0.25 } )`;
const number = document.createElement('div');
number.className = 'number';number.textContent = i / 5 + 1;
element.appendChild( number );
const symbol = document.createElement('div');
symbol.className = 'symbol';
symbol.textContent = table[ i ];
element.appendChild( symbol );
const detail = document.createElement('div');
detail.className = 'detail';
detail.innerHTML = `${ table[ i + 1 ] }<br/>${ table[ i + 2 ] }`;
element.appendChild( detail );
const object = new THREE.CSS3DObject( element );
object.position.x = Math.random() * 4000 - 2000;
object.position.y = Math.random() * 4000 - 2000;
object.position.z = Math.random() * 4000 - 2000;
scene.add( object );
objects.push( object );
}複製代碼
這段代碼建立了顯示週期表元素的HTML結構,並將每個DOM元素使用THREE.CSS3DObject類包裝成3D對象。而後隨機分配對象的位置在( -2000, 2000 )這個區間內。最後把對象添加場景中,並放入objects數組中保存,爲在後面的動畫作準備。
上面的已經完成了118元素的建立到隨機分配位置顯示的部分。下面開始建立集中排版須要的數據。
table排版
function createTableVertices() {
let i = 0;
for ( ; i < len; i += 5 ) {
const object = new THREE.Object3D();
// [ clumn 18 ]
object.position.x = table[ i + 3 ] * 140 - 1260;
object.position.y = -table[ i + 4 ] * 180 + 1000;
object.position.z = 0;
targets.table.push( object );
}
}複製代碼
這個排版比較簡單,使用table數組中每一個元素的第四個數據(column)和第五個數據(row)直接就能夠的到每一個元素對應的table排版的位置信息,而後將它們賦值給對應的object.position屬性中保存(這個不必定非要這樣,只要是THREE.Vector3類型的數據就能夠)。最後將對象保存到對應的數組中,以便在動畫中使用。
shpere排版
const objLength = objects.length;
function createSphereVertices() {
let i = 0;
const vector = new THREE.Vector3();
for ( ; i < objLength; ++i ) {
let phi = Math.acos( -1 + ( 2 * i ) / objLength );
let theta = Math.sqrt( objLength * Math.PI ) * phi;
const object = new THREE.Object3D();
object.position.x = 800 * Math.cos( theta ) * Math.sin( phi );
object.position.y = 800 * Math.sin( theta ) * Math.sin( phi );
object.position.z = -800 * Math.cos( phi );
// rotation object
vector.copy( object.position ).multiplyScalar( 2 );
object.lookAt( vector );
targets.sphere.push( object );
}
}複製代碼
說實話這段代碼理解的不是很到位總感受原做者的算法複雜化了,代碼貼出來請大佬分析一下。後面我本身用別的方法實現了一種‘圓’不是很好看,可是很好理解。我先說一下vector這個變量的做用,它用來做爲'目標位置',使用object.lookAt( vector )
這個方法讓這個位置的對象看向vector這一點所在的方向,在three.js的內部會將object旋轉以‘看向vector’。將獲得旋轉的值並保存在object對象的rotation屬性中,在動畫中將元素對象的rotation屬性過渡爲對應的值,使其旋轉。
helix排版
function createHelixVertices() {
let i = 0;
const vector = new THREE.Vector3();
for ( ; i < objLength; ++i ) {
let phi = i * 0.213 + Math.PI;
const object = new THREE.Object3D();
object.position.x = 800 * Math.sin( phi );
object.position.y = -( i * 8 ) + 450;
object.position.z = 800 * Math.cos( phi + Math.PI );
object.scale.set( 1.1, 1.1, 1.1 );
vector.x = object.position.x * 2;
vector.y = object.position.y;
vector.z = object.position.z * 2;
object.lookAt( vector );
targets.helix.push( object );
}
}複製代碼
這個排版很好理解,首先看一下Y軸採起的是在Y方向上逐個降低的算法。若是X,Z軸不作處理那就是延Y軸的排成一排。而後我講一下這個0.213是怎麼取的
由於總共118個元素,若是想讓這些元素排列成圓的用上圖的的兩種函數就能夠,我使用的是正弦函數,有圖能夠看出使118個元素排成四個圓只須要給每個元素一個對應的角度,再經過Math.sin( angle )或Math.cos( angle )計算後,獲得四組週期性的值,元素就會呈圓形排列。經過計算公式4 * Math.PI * 2 / 118得出0.213,這樣每個元素在週期表中的位置(這裏是從0開始。)乘以0.213,獲得與其對應的角度。使用這個角度經過正玄餘玄函數獲得在圓中的位置。
grid排版
function createGridVertices() {
let i = 0;
for ( ; i < objLength; ++i ) {
const object = new THREE.Object3D();
object.position.x = 360 * ( i % 5) - 800;
object.position.y = -360 * ( ( i / 5 >> 0 ) % 5 ) + 700;
object.position.z = -700 * ( i / 25 >> 0 );
targets.grid.push( object );
}
}複製代碼
網格佈局使用的主要是分組的思想,這是個5 * 5的網格。在X軸上的佈局採用求餘可使元素分爲五列,在Y軸上先除以5而後取整(這裏我喜歡使用>>位操做符,和Math.floor一個效果)。這樣作是爲元素分行,而後求餘分列。當一個平面內5 * 5排滿後,在Z軸上判斷元素屬於哪一面。
上面四種佈局是原來的經典佈局,原做者使用的是將每一個元素將要太低的位置保存起來。還有兩種佈局是我經過這種思想延伸的,比較偷懶,也很簡單。先看一下是如何使用tween動畫庫來完成元素位置的過渡。
const gridBtn = document.getElementById('grid');
const tableBtn = document.getElementById('table');
const helixBtn = document.getElementById('helix');
const sphereBtn = document.getElementById('sphere');
gridBtn.addEventListener( 'click', function() { transform( targets.grid, 2000 )}, false );
tableBtn.addEventListener( 'click', function() { transform( targets.table, 2000 ) }, false );
helixBtn.addEventListener( 'click', function() { transform( targets.helix, 2000 ) }, false );
sphereBtn.addEventListener( 'click', function() { transform( targets.sphere, 2000 ) }, false );複製代碼
function transform( targets, duration ) {
TWEEN.removeAll();
for ( let i = 0; i < objLength; ++i ) {
let object = objects[ i ];
let 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();
}
// 這個補間用來在位置與旋轉補間同步執行,經過onUpdate在每次更新數據後渲染scene和camera
new TWEEN.Tween( {} )
.to( {}, duration * 2 )
.onUpdate( render )
.start();
}複製代碼
從事件綁定的回調能夠看出,觸發不一樣的排版時,咱們傳入對應的數據。而後將數據取出經過tween.js過渡這些數據產生動畫。這裏有tween.js使用的詳細介紹github.com/tweenjs/twe…
循環以外的的這個‘補間’是用來在動畫過渡期間執行渲染頁面函數的。以下
function render() {
renderer.render( scene, camera );
}複製代碼
onWindowResize函數用於縮放頁面時更新相機參數,場景大小以及從新渲染畫面
animation經過requestAnimationFrame這個動畫神器刷新‘全部補間數據’,更新trackball控制器
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
render();
}
function animation() {
TWEEN.update();
controls.update();
requestAnimationFrame( animation );
}複製代碼
最後說一下我拓展的兩種‘投機取巧的排版’
const sphere2Btn = document.getElementById('sphere2');
sphere2Btn.addEventListener( 'click', function() { transformSphere2( 2000 ) }, false );
function transformSphere2(duration) {
TWEEN.removeAll();
const sphereGeom = new THREE.SphereGeometry( 800, 12, 11 );
const vertices = sphereGeom.vertices;
const vector = new THREE.Vector3();
for ( let i = 0; i < objLength; ++i ) {
const target = new THREE.Object3D();
target.position.copy(vertices[i]);
vector.copy( target.position ).multiplyScalar( 2 );
target.lookAt( vector );
let object = objects[ i ];
new TWEEN.Tween( object.position )
.to( vertices[i],
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();
}複製代碼
整個動畫的原理: 爲每一個元素建立一個目標位置,這些位置組合產生的排版就是元素最終的排版,經過‘補間’過渡位置的轉換。因此我直接使用three.js內置的幾何體,使用它的vertices屬性中的位置做爲目標位置(有一點限制,vertices中頂點(位置)的數目最好接近118)。這樣經過內置的幾何體咱們能夠不進行數學計算,直接建立一些有意思的排版。
寫到這裏講的也差很少了,我是一個剛入門前端的菜鳥,歡迎你們的指點和批評!喜歡的同窗能夠給個贊哦!