I’ve been studying
three.jsIn practice, I spent a whole day imitating a cool periodic table with some changes on the original basis. Now I’m going to explain this project step by step, just to deepen your understanding and let you give your comments.

Because I don’t have a personal server. I want to show you some pictures of what I did (mostly the same as the original). Maybe some of you have already seen this classic animation. (Here is the original project address:Threejs.org/examples/cs…)







In addition to optimizing the original HELIX and GRID layouts, I also created two custom layouts in another way. I’ll share with you later.

Here is the GitHub repository address, the file is very simple, just an HTML file. You can take a look if you want to implement it manually. A star if you like, thank you very much.

Github.com/yjhtry/proj…

Let’s start with this little project

Technology stack

  1. HTML, CSS3, Javascript
  2. three.js, tween.js
  3. Trigonometric functions

Realize the principle of

  1. Using the CSS3DRenderer provided by three.js, layered 3D transformations are applied to DOM elements via CSS3 transformation properties. You can wrap the DOM element and manipulate it in the same way you manipulate the Mesh object in Three.js. Essentially, the 3D animation properties of CSS3 are used. This project creates an animation by manipulating the position and rotation properties of the transformed DOM element
  2. Use the lightweight animation library tween’ tween’ to control the transition between the position and rotation properties of the DOM element.
  3. Determine the position and rotation values for each DOM element in a different layout and store them in the position property of a child object of THREE.Object3D (or an imaginary array, as I’ll explain later). We then use ‘tween’ to transition the DOM element’s position and rotation to the corresponding property values.

Without further ado, get right to the code.

HTML structure

<div id="container"> <! Select menu structure 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>Copy the code

The HTML section is very simple. It’s just a selection bar with six buttons that control the transformation. Here’s how they look

        #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: active Rgba (127, 255, 255, 0.75)}Copy the code

First, place the selection bar absolutely 50px at the bottom of the window. Note that z-Index: 100 is set to the highest level to prevent the hover and click events from being intercepted by other elements. I then cleaned up the Button default style and added: hover and: active pseudoclasses to make the interaction more lively.

The effect is as follows:



Then there’s the structure and style of the 118 DOM elements, and since they’re created dynamically in JavaScript code, I’ve written the structure of a single element here.

<div class="element">
    <div class="number">1</div>		
    <div class="symbol">H</div>
    <div class="detail"> Hydrogen, < br > 1.00794 < / div > < / div >Copy the code

CSS styles

.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); }Copy the code

Notice box-shadow and text-shadow. Below are the renderings



Box-shadow and text-shadow give DOM elements a three-dimensional feel.

JavaScript part


First of all, 118 elements of data storage structure are defined, here using array (due to the large number of external, I only take the first 25, github code has the complete data).

    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, and 3,"Si"."Silicon"."28.0855", 14, and 3,"P"."Phosphorus"."30.973762", 15, 3,
			"S"."Sulfur"."32.065", 16, 3,
			"Cl"."Chlorine"."35.453", 17, and 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)Copy the code

Let’s analyze the data structure first

"H"."Hydrogen"."1.00794", 1, 1,Copy the code

There are 118 elements in total, and each element defines five data points in the table array, namely symbol, detail, column and row. I will explain how to use these two data points when creating the table plate.

		let scene, camera, renderer, controls;
		const objects = [];
		const targets = { 
			grid: [],
			helix: [], 
			table: [], 
			sphere: [] 
		};Copy the code

Some global variables are defined here. Scene, camera, renderer are three. Js environment objects, camera, and renderer. Controls is a library of controls provided by three.js to interact with the user, very simple. Objects is used to store 118 DOM elements. The Targets object contains four array type property values that are used to hold Object3D child objects that hold different typesetting target positions.

The creation of elements and control of animations are performed by the init function, which is mostly used in the following sections

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 );

 Copy the code

PerspectiveCamera creates the three basic components of Three.js: the scene, the camera, and the renderer. It should be noted that the value of far-clipping-plane set here is relatively large. If you do it yourself, you can set it smaller to reduce performance loss. Note that the CSS3D renderer is used here.

A cone view of a perspective camera




The area between the planes is called the cone, which is simply the area of the camera. Fov (field of view) is the first parameter of the camera and determines the size of the camera’s field of view, similar to the horizontal field of view of the human eye. The aspect parameter controls the aspect ratio of the camera’s projection plane (usually the canvas’s aspect ratio). This is mainly used to prevent image distortion, since the image on the projection plane will eventually be displayed on the canvas. Note that when using the CSS3D renderer, the display viewport is a div element.

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 );

		}Copy the code

This code creates an HTML structure that displays the periodic table elements and wraps each DOM element into a 3D object using the three.css3dobject class. And then I randomly assign the position of the object to be in the interval minus 2000, 2000. Finally, add the object to the scene and save it in the Objects array for later animation.

The above has completed the creation of the 118 element to the randomly assigned location display. Let’s start by creating the data needed for centralized typesetting.

The table layout

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 ); }}Copy the code

Use the fourth column and fifth row of each element in the table array to retrieve the position information of each element in the table, and then assign them to the corresponding Object. Position property. As long as the data is of type three.Vector3). Finally, the object is saved into the corresponding array for use in the animation.

Shpere typesetting

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 );
	    lettheta = 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 ); }}Copy the code

To tell the truth, this code is not very well understood and always feel that the original author’s algorithm is complicated, the code posted for big guy analysis. And then I realized a circle in a different way that wasn’t pretty, but it was easy to understand. LookAt (vector) we use object.lookat (vector) to make the object look in the direction of the vector. Inside three.js we rotate the object to look into the vector. The rotation value is obtained and stored in the object’s rotation property. In the animation, transition the element object’s rotation property to the corresponding value, causing it to rotate.

Helix typesetting

function createHelixVertices() {

        let i = 0;
	const vector = new THREE.Vector3();

	for(; i < objLength; ++i ) {letPhi = 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 ); }}Copy the code

The layout is easy to understand, but first of all, the Y axis takes the algorithm of going down in the Y direction. If you don’t do anything with the X and Z axis it’s just going to line up along the Y axis. And then I’m going to tell you how do we get 0.213





Because there are 118 elements, and if you want to arrange the elements in a circle you can just use the two functions in the figure above, I’m using the sine function, and you can see that to arrange the 118 elements in four circles you just need to assign an Angle to each element, When math.sin (Angle) or math.cos (Angle) is computed, the elements are arranged in a circle, resulting in four sets of periodic values. Calculate 0.213 by calculating formula 4 * math.pi * 2/118, which gives the position of each element on the periodic table (starting at 0 here). Multiply by 0.213 to get the corresponding Angle. Use this Angle to find the position in the circle by using the positive and complementary functions.

The grid layout

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 ); }}Copy the code

The grid layout uses the idea of grouping, which is a 5 by 5 grid. The layout on the X axis uses complement to split elements into five columns, dividing by 5 and rounding off elements on the Y axis (I like to use the >> bit operator here, as well as the math.floor effect). So what you do is you branch the elements, and then you do the mod. When a plane is full of 5 by 5, determine which side of the Z axis the elements belong to.

The above four layouts are the original classic layout, which the original author used to save each element where it was going to be too low. There are two other layouts that I’ve extended with this idea, which are lazy and very simple. Let’s take a look at how we can use the Tween library to transition elements.

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 );Copy the code

function transform( targets, duration ) {

        TWEEN.removeAll();

	for ( let i = 0; i < objLength; ++i ) {

	let object = objects[ i ];
	lettarget = 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(); } // This tween is used to execute synchronously between position and rotation tween, Render scene and camera new tween.tween ({}).to({}, duration * 2).onupdate (render).start(); }Copy the code

As you can see from the event-bound callback, we pass in the corresponding data when different typesetting is triggered. We then take the data out and animate it with tween.js. Here is a detailed introduction to the use of tween.js github.com/tweenjs/twe…

The ‘tween’ outside the loop is used to perform the render page function during the animation transition. The following

function render() {

        renderer.render( scene, camera );

}Copy the code

The onWindowResize function is used to update camera parameters, scene size, and re-render the image while zooming in and out of the page

The animation refreshes all tween data and updates the Trackball controller using the requestAnimationFrame animation artifact

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 );	
}Copy the code

Finally, two types of ‘opportunistic typography’ that I’ve developed

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();

}Copy the code

The principle of the whole animation is to create a target position for each element, and the resulting layout of these positions is the final layout of the element, through the transition of ‘tween’ positions. So I went straight to the three-.js built-in geometry and used the positions in its Vertices properties as the target positions (with a bit of limitation, the number of vertices in vertices should be close to 118). This allows us to create interesting layouts without doing any math with the built-in geometry.

Write here to say also almost, I am a beginner of the front end, welcome everybody to give directions and criticism! Like the students can give a thumbs-up oh!