BitPoet Posted March 25, 2016 Share Posted March 25, 2016 For a little data visualization task at work, I (finally) got to do some tinkering with WebGL / 3D, and as a small Easter egg, I thought I'd share one of the (admittedly a bit crude) side results for anybody also new to and interested in 3D in the web. Before I went into full-blown meshes and textures, I animated DOM elements in 3D and built a little spinner to get a feel for positions, camera and the behaviour of my library of choice, three.js. Don't mind the "Sitemap 3D" title in the screencap, that was just a spur-of-the-moment thing, but what came out was a classic (static) spinner.I really love how you can move regular HTML elements through thee-dimensional space and still use all the regular CSS styling, including colors, transparency, borders and shadows. Step-by-step: Download three.js from GitHub and extract it into a folder named "three.js" in site/templates (it comes with all examples and is a bit big, but we need a few of those). Adapt the template you want to show your 3D spinner in to include jQuery, the three.js main file, CSS3DRenderer and Tween (the latter is used for smooth animations). <script src="<?= $config->urls->templates ?>three.js/build/three.js"></script> <script src="<?= $config->urls->templates ?>three.js/examples/js/renderers/CSS3DRenderer.js"></script> <script src="<?= $config->urls->templates ?>three.js/examples/js/libs/tween.min.js"></script> <?php $jq = $modules->get('JqueryCore'); foreach($config->scripts as $script) { ?> <script src="<?= $script ?>"></script> <?php } ?> Add a div with position: relative and a fixed height (fixed width is optional) in your template and give it an id (in my example I'll use "main"). Paste the following code into a php file in site/templates and include the file in your template before the closing body tag. If you gave your container an id different from "main", adapt the first parameter for the showSpinner call close to the bottom. <div id="spinnertmp"><!-- A temporary container --> <?php // Get our list of items. The first item in the PageArray is shown front center. $mainpages = $pages->find('parent=1')->and($pages->get('/'))->reverse(); $links = array(); $first = true; foreach($mainpages as $pg) { $id = "smitem_{$pg->id}"; $links[] = $id; $cls = ""; if($first) { $cls = " smactive"; $first = false; } // Our spinner items: echo " <div class='smitem$cls' id='$id'> <a href='#' class='smleft'><</a> <a class='smlink' href='$pg->httpUrl' title='$pg->title'>$pg->title</a> <a href='#' class='smright'>></a> </div> "; } ?> </div> <script> function showSpinner(el, prnt) { var camera, scene, renderer; var links = [ <?= implode(",\n\t\t", array_map(function($v) { return "'$v'"; }, $links)) ?> ]; var w, h, wInt, hInt; var objects = []; /** * On window resize, we also recalculate our camera position and set the * size of the render canvas. This last step may not be necessary if you * use a containing element with fixed bounds. */ $(window).on('resize', function(evt) { camera.position.z = w / 1.5 + 250; camera.updateProjectionMatrix(); renderer.setSize($(el).innerWidth(), $(el).innerHeight()); circle(); }); /** * Prev and next link click handlers for our spinner items */ $('.smleft').on('click', function(evt) { evt.preventDefault(); goLeft(); }); $('.smright').on('click', function(evt) { evt.preventDefault(); goRight(); }); // Get the show started, init() also calls render() init(); // Remove the no longer needed container for our spinner items from the DOM $(prnt).remove(); // Start the animation loop animate(); // Move our spinner items into a circle circle(); /** * Rotate the circle left/right by moving the first/last item * to the other end of the item stack and letting circle() * move them into the new positions. */ function goLeft() { objects.unshift(objects.pop()); links.unshift(links.pop()); circle(); } function goRight() { objects.push(objects.shift()); links.push(links.shift()); circle(); } /** * Initial setup */ function init() { $(el).css('padding: 0;'); w = $(el).innerWidth(); h = $(el).innerHeight(); wInt = ( w - 80 - 150 ) / links.length; hInt = ( h - 80 - 50 ) / links.length; camera = new THREE.PerspectiveCamera( 40, w / h, 1, 10000 ); camera.position.z = w / 1.5 + 250; scene = new THREE.Scene(); for(var i = 0, l = links.length; i < l; i++) { var $el = $('#' + links[i]); // create a CSS3DObject from each spinner item element var object = new THREE.CSS3DObject( $el.detach().get(0) ); var curpos = object.position; // for a start, let's align them all in a diagonal row object.position.x = -1 * w + 2 * (i * wInt + 40); object.position.y = -1 * h + 2 * (i * hInt + 40); object.position.z = 50 * i; objects.push( object ); $(el).append($('#' + links[i])); scene.add( object ); } renderer = new THREE.CSS3DRenderer(); renderer.setSize( w, h ); renderer.domElement.style.position = 'absolute'; $(el).append($(renderer.domElement)); render(); } /** * Arrange our items in a circle and float them to the target position */ function circle() { // Get our containing element's coordinate measures w = $(el).innerWidth(); h = $(el).innerHeight(); wInt = ( w - 80 - 150 ) / links.length; hInt = ( h - 80 - 50 ) / links.length; // Make the radius and placement angle dependant on the number of // items and the size of our canvas var l = objects.length; var radius = w / 1.5 - 200; var cX = 0; var cZ = 0; // just basic radians/degree conversion math, nothing to be afraid of var mpi = Math.PI / 180; var startRad = mpi; var incAngle = 360 / l; var incRad = incAngle * mpi; // Let's do some cleanup before the party starts again TWEEN.removeAll(); // Move every item to its new assigned position for(var i = 0; i < l; i++) { // x and z coordinates (left-right and back-front), no y needed as all stay at the same height var xp = cX + Math.sin(startRad) * radius; var zp = cZ + Math.cos(startRad) * radius; // our item's rotation as it revolves around the circle var rotY = i * incAngle * mpi; /** * A tween is a smooth transition over a given time. You pass the start values (must be numbers) * to the constructor and the desired destination values to the to() method. The property names * can in fact be arbitrary but need to be consistent between the constructor and to(). * * The onUpdate handler will be called every time a frame is ready, and the correct transition * values are set to corresponding properties of the "this" object. Assigning the passed properties * to a 3D object is the task of the developer. */ (function(mObj, twNum){ new TWEEN.Tween( {x: mObj.position.x, y: mObj.position.y, z: mObj.position.z, rotY: mObj.rotation.y } ) .to( {x: xp, y: 0, z: zp, rotY: rotY}, 1000 ) .onUpdate(function() { mObj.position.x = this.x; mObj.position.y = this.y; mObj.position.z = this.z; mObj.rotation.y = this.rotY; }) .onComplete(function() { // color only the item currently in the front (first item in our stack) differently $('.smitem').removeClass('smactive'); $('#' + links[0]).addClass('smactive'); }) .start(); // start the transition })(objects[i], i); // increment rotation for the next item startRad += incRad; } } /** * simple but magical, pushes itself onto the stack to be processed the next time * the computer is ready to draw the scene. */ function animate() { requestAnimationFrame(animate); TWEEN.update(); render(); } /** * paint it, dude! */ function render() { renderer.render(scene, camera); } } $(document).ready(function() { showSpinner('#main', '#spinnertmp'); }); </script> Style your container element and spinner items. Here's the CSS I used, but you can go all out there: #spinnertmp { display: none; } #main { height: 400px; position: relative; background-color: rgb(0,20,10); padding: 0; margin: 0; overflow: hidden; } .smitem { position: absolute; width: 200px; height: 50px; padding: 2%; border: 1px solid rgba(127,255,255,0.25); box-shadow: 0px 0px 12px rgba(0,255,255,0.5); text-align: center; left: 20px; top: 20px; background-color: rgba(0,127,127,0.40); display: flex; } .smlink, .smleft, .smright { text-decoration: none; font-size: 1.4em; font-weight: bold; color: rgba(127,255,255,0.75); display: inline-block; } .smlink { flex: 5; } .smlink:hover { color: rgba(255,215,0,0.75); } .smleft, .smright { flex: 1; } .smleft:hover, .smright:hover { color: rgba(255,127,127,0.75); } .smactive { background-color: rgba(0,0,127,0.60); } Load your page and enjoy! 10 Link to comment Share on other sites More sharing options...
SteveB Posted March 25, 2016 Share Posted March 25, 2016 Hah! That's very cool. Though I must say I don't like waiting for stuff to play out. If that was the actual navigation I'd just go away. Better for some kind of slideshow I think. Thanks for the example. Maybe we should have a contest and see what people do with it. 1 Link to comment Share on other sites More sharing options...
BitPoet Posted March 25, 2016 Author Share Posted March 25, 2016 Yepp, that's what I thought too about a navigation, thus the sitemap changed into a slider. Link to comment Share on other sites More sharing options...
Recommended Posts