Jump to content

Happy Easter Holidays (with fun in 3 dimensions)


BitPoet
 Share

Recommended Posts

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.
 

post-2900-0-44920700-1458927721_thumb.gi

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!
  • Like 10
Link to comment
Share on other sites

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.

  • Like 1
Link to comment
Share on other sites

 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...