Интерактивные 3D-часы с использованием three.js
Целью сегодняшнего урока является продолжение изучения webgl с использованием библиотеки three.js. Мы создаем интерактивные (тикающие) трехмерные классические часы, которые состоят из следующих основных элементов: круглый циферблат, стрелки часов и стрелки перемещения. Модель часов будет расположена в пространстве, поэтому вы легко сможете повернуть ее в сцене для просмотра часов под любым углом.
Результат нашего урока таков:
Live Demo
HTML
Это стало обычной практикой, что мы начинаем с самой простой — HTML-разметки:
index.html
<!DOCTYPE html> <html lang="en" > <head> <meta charset="utf-8" /> <meta name="author" content="Script Tutorials" /> <title>Interactive 3D watch using three.js | Script Tutorials</title> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <link href="css/main.css" rel="stylesheet" type="text/css" /> </head> <body> <script src="js/three.min.js"></script> <script src="js/THREEx.WindowResize.js"></script> <script src="js/OrbitControls.js"></script> <script src="js/stats.min.js"></script> <script src="js/script.js"></script> <div style="position: absolute; top: 10px; left: 20px; text-align: center;"><a href="http://www.script-tutorials.com/interactive-3d-watch-using-three-js/" target="_blank">"Interactive 3D watch using three.js"</a><br>Drag to spin</div> </body> </html>
В этом нет ничего сложного — нам нужно только подключить все необходимые библиотеки.
Javascript
Теперь мы начинаем реализовывать нашу новую сцену webgl. Сначала давайте подготовим кадр сцены: добавим все переменные, сцену, камеру, средство визуализации, элементы управления и объект статистики:
JS / script.js
var watch = { scene: null, camera: null, renderer: null, container: null, controls: null, clock: null, stats: null, arrowHr: null, arrowMin: null, arrowSec: null, timeHr: null, timeMin: null, timeSec: null, init: function() { // initialization // create main scene this.scene = new THREE.Scene(); var SCREEN_WIDTH = window.innerWidth, SCREEN_HEIGHT = window.innerHeight; // prepare camera var VIEW_ANGLE = 45, ASPECT = SCREEN_WIDTH / SCREEN_HEIGHT, NEAR = 1, FAR = 5000; this.camera = new THREE.PerspectiveCamera( VIEW_ANGLE, ASPECT, NEAR, FAR); this.scene.add(this.camera); this.camera.position.set(0, 1500, 500); this.camera.lookAt(new THREE.Vector3(0,0,0)); // prepare renderer this.renderer = new THREE.WebGLRenderer({antialias:true, alpha: false}); this.renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT); this.renderer.setClearColor(0xffffff); this.renderer.shadowMapEnabled = true; this.renderer.shadowMapSoft = true; // prepare container this.container = document.createElement('div'); document.body.appendChild(this.container); this.container.appendChild(this.renderer.domElement); // events THREEx.WindowResize(this.renderer, this.camera); // prepare controls (OrbitControls) this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement); this.controls.target = new THREE.Vector3(0, 0, 0); // prepare clock this.clock = new THREE.Clock(); // prepare stats this.stats = new Stats(); this.stats.domElement.style.position = 'absolute'; this.stats.domElement.style.bottom = '0px'; this.stats.domElement.style.zIndex = 10; this.container.appendChild( this.stats.domElement ); } }; // Animate the scene function animate() { requestAnimationFrame(animate); render(); update(); } // Update controls and stats function update() { watch.controls.update(watch.clock.getDelta()); watch.stats.update(); } // Render the scene function render() { if (watch.renderer) { watch.renderer.render(watch.scene, watch.camera); } } // Initialize lesson on page load function initializeLesson() { watch.init(); animate(); } if (window.addEventListener) window.addEventListener('load', initializeLesson, false); else if (window.attachEvent) window.attachEvent('onload', initializeLesson); else window.onload = initializeLesson;
Теперь мы начинаем создавать элементы часов. Сначала мы создадим формы циферблата и обода. Нам нужно добавить следующий код в конец функции ‘init’:
// add dial shape var dialMesh = new THREE.Mesh( new THREE.CircleGeometry(500, 50), new THREE.MeshBasicMaterial({ color:0xffffff * Math.random(), side: THREE.DoubleSide }) ); dialMesh.rotation.x = - Math.PI / 2; dialMesh.position.y = 0; this.scene.add(dialMesh); // add watch rim shape var rimMesh = new THREE.Mesh( new THREE.TorusGeometry(500, 20, 10, 100), new THREE.MeshBasicMaterial({ color:0xffffff * Math.random() }) ); rimMesh.rotation.x = - Math.PI / 2; this.scene.add(rimMesh);
Теперь мы создадим делители часов (стрелки). Обратите внимание, что некоторые стрелки немного длиннее (как на реальных часах):
// add watch arrow var iHours = 12; var mergedArrows = new THREE.Geometry(); var extrudeOpts = {amount: 10, steps: 1, bevelSegments: 1, bevelSize: 1, bevelThickness:1}; var handFrom = 400, handTo = 450; for (i = 1; i <= iHours; i++) { // prepare each arrow in a circle var arrowShape = new THREE.Shape(); var from = (i % 3 == 0) ? 350 : handFrom; var a = i * Math.PI / iHours * 2; arrowShape.moveTo(Math.cos(a) * from, Math.sin(a) * from); arrowShape.lineTo(Math.cos(a) * from + 5, Math.sin(a) * from + 5); arrowShape.lineTo(Math.cos(a) * handTo + 5, Math.sin(a) * handTo + 5); arrowShape.lineTo(Math.cos(a) * handTo, Math.sin(a) * handTo); var arrowGeom = new THREE.ExtrudeGeometry(arrowShape, extrudeOpts); THREE.GeometryUtils.merge(mergedArrows, arrowGeom); } var arrowsMesh = new THREE.Mesh(mergedArrows, new THREE.MeshBasicMaterial({ color:0x444444 * Math.random() })); arrowsMesh.rotation.x = - Math.PI / 2; arrowsMesh.position.y = 10; this.scene.add(arrowsMesh);
Все геометрии стрелок (ExtrudeGeometry) были объединены (для повышения производительности) в одну геометрию (mergedArrows) с помощью функции THREE.GeometryUtils.merge. Наконец, мы создадим еще три стрелки для указания секунд, минут и часов:
// add seconds arrow handTo = 350; var arrowSecShape = new THREE.Shape(); arrowSecShape.moveTo(-50, -5); arrowSecShape.lineTo(Math.cos(a) * handTo, Math.sin(a) * handTo); arrowSecShape.lineTo(-50, 5); var arrowSecGeom = new THREE.ExtrudeGeometry(arrowSecShape, extrudeOpts); this.arrowSec = new THREE.Mesh(arrowSecGeom, new THREE.MeshBasicMaterial({ color:0x000000 })); this.arrowSec.rotation.x = - Math.PI / 2; this.arrowSec.position.y = 20; this.scene.add(this.arrowSec); // add minutes arrow var arrowMinShape = new THREE.Shape(); arrowMinShape.moveTo(0, -5); arrowMinShape.lineTo(Math.cos(a) * handTo, Math.sin(a) * handTo - 5); arrowMinShape.lineTo(Math.cos(a) * handTo, Math.sin(a) * handTo + 5); arrowMinShape.lineTo(0, 5); var arrowMinGeom = new THREE.ExtrudeGeometry(arrowMinShape, extrudeOpts); this.arrowMin = new THREE.Mesh(arrowMinGeom, new THREE.MeshBasicMaterial({ color:0x000000 })); this.arrowMin.rotation.x = - Math.PI / 2; this.arrowMin.position.y = 20; this.scene.add(this.arrowMin); // add hours arrow handTo = 300; var arrowHrShape = new THREE.Shape(); arrowHrShape.moveTo(0, -5); arrowHrShape.lineTo(Math.cos(a) * handTo, Math.sin(a) * handTo - 5); arrowHrShape.lineTo(Math.cos(a) * handTo, Math.sin(a) * handTo + 5); arrowHrShape.lineTo(0, 5); var arrowHrGeom = new THREE.ExtrudeGeometry(arrowHrShape, extrudeOpts); this.arrowHr = new THREE.Mesh(arrowHrGeom, new THREE.MeshBasicMaterial({ color:0x000000 })); this.arrowHr.rotation.x = - Math.PI / 2; this.arrowHr.position.y = 20; this.scene.add(this.arrowHr);
Все эти стрелки вытеснены из плоских форм.
Теперь пришло время научить стрелять тикающим. Для этого нам понадобится добавить следующий код в функцию update:
// get current time var date = new Date; watch.timeSec = date.getSeconds(); watch.timeMin = date.getMinutes(); watch.timeHr = date.getHours(); // update watch arrows positions var rotSec = watch.timeSec * 2 * Math.PI / 60 - Math.PI/2; watch.arrowSec.rotation.z = -rotSec; var rotMin = watch.timeMin * 2 * Math.PI / 60 - Math.PI/2; watch.arrowMin.rotation.z = -rotMin; var rotHr = watch.timeHr * 2 * Math.PI / 12 - Math.PI/2; watch.arrowHr.rotation.z = -rotHr;