Interactive 3D watch using three.js

Tutorials

The goal of our today’s lesson is the continuation of studying webgl using the three.js library. We create interactive (ticking) three-dimensional classical watches that consists of following major elements: round dial, the clock arrows and moving arrows. Watch model will be located in space, so you will easily be able to rotate it in the scene for viewing the watch from any angle.

Live Demo

HTML

It has become common practice that we begin with the most simple – html markup:

index.html

01 <!DOCTYPE html>
02 <html lang="en" >
03     <head>
04         <meta charset="utf-8" />
05         <meta name="author" content="Script Tutorials" />
06         <title>Interactive 3D watch using three.js | Script Tutorials</title>
07         <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
08         <link href="css/main.css" rel="stylesheet" type="text/css" />
09     </head>
10     <body>
11         <script src="js/three.min.js"></script>
12         <script src="js/THREEx.WindowResize.js"></script>
13         <script src="js/OrbitControls.js"></script>
14         <script src="js/stats.min.js"></script>
15         <script src="js/script.js"></script>
16         <div style="position: absolute; top: 10px; left: 20px; text-align: center;"><a href="https://www.script-tutorials.com/interactive-3d-watch-using-three-js/" target="_blank">"Interactive 3D watch using three.js"</a><br>Drag to spin</div>
17     </body>
18 </html>

There is nothing complicated – we only need to connect all the required libraries.

Javascript

Now we begin to implement our new webgl scene. First, let’s prepare the frame of the scene: add all variables, the scene, camera, renderer, controls and stats object:

js/script.js

01 var watch = {
02     scene: null,
03     camera: null,
04     renderer: null,
05     container: null,
06     controls: null,
07     clock: null,
08     stats: null,
09     arrowHr: null,
10     arrowMin: null,
11     arrowSec: null,
12     timeHr: null,
13     timeMin: null,
14     timeSec: null,
15     init: function() { // initialization
16         // create main scene
17         this.scene = new THREE.Scene();
18         var SCREEN_WIDTH = window.innerWidth,
19             SCREEN_HEIGHT = window.innerHeight;
20         // prepare camera
21         var VIEW_ANGLE = 45, ASPECT = SCREEN_WIDTH / SCREEN_HEIGHT, NEAR = 1, FAR = 5000;
22         this.camera = new THREE.PerspectiveCamera( VIEW_ANGLE, ASPECT, NEAR, FAR);
23         this.scene.add(this.camera);
24         this.camera.position.set(0, 1500, 500);
25         this.camera.lookAt(new THREE.Vector3(0,0,0));
26         // prepare renderer
27         this.renderer = new THREE.WebGLRenderer({antialias:true, alpha: false});
28         this.renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT);
29         this.renderer.setClearColor(0xffffff);
30         this.renderer.shadowMapEnabled = true;
31         this.renderer.shadowMapSoft = true;
32         // prepare container
33         this.container = document.createElement('div');
34         document.body.appendChild(this.container);
35         this.container.appendChild(this.renderer.domElement);
36         // events
37         THREEx.WindowResize(this.renderer, this.camera);
38         // prepare controls (OrbitControls)
39         this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);
40         this.controls.target = new THREE.Vector3(0, 0, 0);
41         // prepare clock
42         this.clock = new THREE.Clock();
43         // prepare stats
44         this.stats = new Stats();
45         this.stats.domElement.style.position = 'absolute';
46         this.stats.domElement.style.bottom = '0px';
47         this.stats.domElement.style.zIndex = 10;
48         this.container.appendChild( this.stats.domElement );
49     }
50 };
51 // Animate the scene
52 function animate() {
53     requestAnimationFrame(animate);
54     render();
55     update();
56 }
57 // Update controls and stats
58 function update() {
59     watch.controls.update(watch.clock.getDelta());
60     watch.stats.update();
61 }
62 // Render the scene
63 function render() {
64     if (watch.renderer) {
65         watch.renderer.render(watch.scene, watch.camera);
66     }
67 }
68 // Initialize lesson on page load
69 function initializeLesson() {
70     watch.init();
71     animate();
72 }
73 if (window.addEventListener)
74     window.addEventListener('load', initializeLesson, false);
75 else if (window.attachEvent)
76     window.attachEvent('onload', initializeLesson);
77 else window.onload = initializeLesson;

Now we begin to create watch elements. first we’ll create the dial and rim shapes. We need to add the following code in the end of the ‘init’ function:

01 // add dial shape
02 var dialMesh = new THREE.Mesh(
03     new THREE.CircleGeometry(500, 50),
04     new THREE.MeshBasicMaterial({ color:0xffffff * Math.random(), side: THREE.DoubleSide })
05 );
06 dialMesh.rotation.x = - Math.PI / 2;
07 dialMesh.position.y = 0;
08 this.scene.add(dialMesh);
09 // add watch rim shape
10 var rimMesh = new THREE.Mesh(
11   new THREE.TorusGeometry(500, 20, 10, 100),
12   new THREE.MeshBasicMaterial({ color:0xffffff * Math.random() })
13 );
14 rimMesh.rotation.x = - Math.PI / 2;
15 this.scene.add(rimMesh);

Now we will create a watch dividers (arrows). Please note, that some arrows are a bit longer (as at real watches):

01 // add watch arrow
02 var iHours = 12;
03 var mergedArrows = new THREE.Geometry();
04 var extrudeOpts = {amount: 10, steps: 1, bevelSegments: 1, bevelSize: 1, bevelThickness:1};
05 var handFrom = 400, handTo = 450;
06 for (i = 1; i <= iHours; i++) {
07   // prepare each arrow in a circle
08   var arrowShape = new THREE.Shape();
09   var from = (i % 3 == 0) ? 350 : handFrom;
10   var a = i * Math.PI / iHours * 2;
11   arrowShape.moveTo(Math.cos(a) * from, Math.sin(a) * from);
12   arrowShape.lineTo(Math.cos(a) * from + 5, Math.sin(a) * from + 5);
13   arrowShape.lineTo(Math.cos(a) * handTo + 5, Math.sin(a) * handTo + 5);
14   arrowShape.lineTo(Math.cos(a) * handTo, Math.sin(a) * handTo);
15   var arrowGeom = new THREE.ExtrudeGeometry(arrowShape, extrudeOpts);
16   THREE.GeometryUtils.merge(mergedArrows, arrowGeom);
17 }
18 var arrowsMesh = new THREE.Mesh(mergedArrows, new THREE.MeshBasicMaterial({ color:0x444444 * Math.random() }));
19 arrowsMesh.rotation.x = - Math.PI / 2;
20 arrowsMesh.position.y = 10;
21 this.scene.add(arrowsMesh);

All arrow geometries (ExtrudeGeometry) were merged (for better performance) into a single geometry (mergedArrows) using the ‘THREE.GeometryUtils.merge’ function. Finally, we will create three more arrows to point seconds, minutes and hours:

01 // add seconds arrow
02 handTo = 350;
03 var arrowSecShape = new THREE.Shape();
04 arrowSecShape.moveTo(-50, -5);
05 arrowSecShape.lineTo(Math.cos(a) * handTo, Math.sin(a) * handTo);
06 arrowSecShape.lineTo(-50, 5);
07 var arrowSecGeom = new THREE.ExtrudeGeometry(arrowSecShape, extrudeOpts);
08 this.arrowSec = new THREE.Mesh(arrowSecGeom, new THREE.MeshBasicMaterial({ color:0x000000 }));
09 this.arrowSec.rotation.x = - Math.PI / 2;
10 this.arrowSec.position.y = 20;
11 this.scene.add(this.arrowSec);
12 // add minutes arrow
13 var arrowMinShape = new THREE.Shape();
14 arrowMinShape.moveTo(0, -5);
15 arrowMinShape.lineTo(Math.cos(a) * handTo, Math.sin(a) * handTo - 5);
16 arrowMinShape.lineTo(Math.cos(a) * handTo, Math.sin(a) * handTo + 5);
17 arrowMinShape.lineTo(0, 5);
18 var arrowMinGeom = new THREE.ExtrudeGeometry(arrowMinShape, extrudeOpts);
19 this.arrowMin = new THREE.Mesh(arrowMinGeom, new THREE.MeshBasicMaterial({ color:0x000000 }));
20 this.arrowMin.rotation.x = - Math.PI / 2;
21 this.arrowMin.position.y = 20;
22 this.scene.add(this.arrowMin);
23 // add hours arrow
24 handTo = 300;
25 var arrowHrShape = new THREE.Shape();
26 arrowHrShape.moveTo(0, -5);
27 arrowHrShape.lineTo(Math.cos(a) * handTo, Math.sin(a) * handTo - 5);
28 arrowHrShape.lineTo(Math.cos(a) * handTo, Math.sin(a) * handTo + 5);
29 arrowHrShape.lineTo(0, 5);
30 var arrowHrGeom = new THREE.ExtrudeGeometry(arrowHrShape, extrudeOpts);
31 this.arrowHr = new THREE.Mesh(arrowHrGeom, new THREE.MeshBasicMaterial({ color:0x000000 }));
32 this.arrowHr.rotation.x = - Math.PI / 2;
33 this.arrowHr.position.y = 20;
34 this.scene.add(this.arrowHr);

All these arrows are extruded from flat shapes.

Now, the time has come to teach arrows ticking. For this we will need to add the following code to the ‘update’ function:

01 // get current time
02 var date = new Date;
03 watch.timeSec = date.getSeconds();
04 watch.timeMin = date.getMinutes();
05 watch.timeHr = date.getHours();
06 // update watch arrows positions
07 var rotSec = watch.timeSec * 2 * Math.PI / 60 - Math.PI/2;
08 watch.arrowSec.rotation.z = -rotSec;
09 var rotMin = watch.timeMin * 2 * Math.PI / 60 - Math.PI/2;
10 watch.arrowMin.rotation.z = -rotMin;
11 var rotHr = watch.timeHr * 2 * Math.PI / 12 - Math.PI/2;
12 watch.arrowHr.rotation.z = -rotHr;

Live Demo

[sociallocker]

download in package

[/sociallocker]


Conclusion

Stay tuned for new tutorials and you will always find something new and interesting for yourself.

Rate article