Rotating 3D Cube with Touch (webkit only)

I've been toying around with this for a little while --- and it's been a lot of fun. It's a pretty straightforward project: make a 3D cube with CSS and make it spin with javascript via touch and mouse interactions. Currently, the interactive part only works in webkit browsers as I'm using the WebkitCSSMatrix object to apply the transformations. In the future, I hope to come back to this and revise it to work non-webkit browsers. But that's a project for another day.

Swipe to rotate

Let's start with making the 3D cube with CSS using 3D transforms (this part should work in any browser that supports CSS 3D transformations):

<div class="cube">
	<div></div>
	<div></div>
	<div></div>
	<div></div>
	<div></div>
	<div></div>
</div>

The HTML really only requires a containing element and 6 children (one for each face of the 3d cube.) We'll apply 3D translation to each face:

.cube { background:#ccc;}
.cube div { 
    position:absolute;
    z-index:1;
    top:0;
    left:0;
    height:300px;
    width:300px;
    background:rgba(0,0,0,0.5);
    -webkit-backface-visibility:show;
    -moz-backface-visibility:show;
    border:1px solid #fff;
}
.cube > div:first-child  {
	-webkit-transform: rotateX(90deg) translateZ(150px);
	-moz-transform: rotateX(90deg) translateZ(150px);
}
.cube > div:nth-child(2) {
	-webkit-transform: translateZ(150px);
	-moz-transform: translateZ(150px);
}
.cube > div:nth-child(3) {
	-webkit-transform: rotateY(90deg) translateZ(150px);
	-moz-transform: rotateY(90deg) translateZ(150px);
}
.cube > div:nth-child(4) {
	-webkit-transform: rotateY(180deg) translateZ(150px);
	-moz-transform: rotateY(180deg) translateZ(150px);
}
.cube > div:nth-child(5) {
	-webkit-transform: rotateY(-90deg) translateZ(150px);
	-moz-transform: rotateY(-90deg) translateZ(150px);
}		
.cube > div:nth-child(6) {
	-webkit-transform: rotateX(-90deg) translateZ(150px);
	-moz-transform: rotateX(-90deg) translateZ(150px);
	background:#ff0000;
}

One thing to note is that the translateZ value is 1/2 of the width (or height) of the face you're transforming (depending on which axis you're rotating) because the default transform-origin is the center. I've given the containing element a background color in the example above so you can see the relationship between it and the translated faces of the cube.

Anyway, at this point our cube is looking pretty flat:

Let's give this thing some dimension:

.cube {
    width:300px;
    height:300px;
    -webkit-transform-style:preserve-3d;
    -moz-transform-style:preserve-3d;
    -webkit-transform:rotateX(10deg) rotateY(45deg);
    -moz-transform:rotateX(10deg) rotateY(45deg);
}

The transform-style:preserve-3d basically means that child elements should maintain their 3d position with respect to their parent, which means that as we apply css transformations to the containing element, its children will transform accordingly.

Okay, now let's make this thing spin. In order to do that, we probably need to add one more element around the cube container. I'll explain why in a bit.

<div class="cube-outer-wrapper">
	<div class="cube">
		<div></div>
		<div></div>
		<div></div>
		<div></div>
		<div></div>
		<div></div>
	</div>
</div>

Next, let's start adding some javascript. The first thing we need to do is normalize some variables.

hasTouch = 'ontouchstart' in window,
// normalize requestAnimationFrame
nextFrame = (function () {
	return window.requestAnimationFrame
        || window.webkitRequestAnimationFrame
	|| window.mozRequestAnimationFrame
	|| function (callback) { return setTimeout(callback, 1); }
})(),
// normalize cancelRequestAnimationFrame
cancelFrame = (function () {
	return window.cancelRequestAnimationFrame
	|| window.cancelWebkitRequestAnimationFrame
	|| window.cancelMozRequestAnimationFrame
	|| clearTimeout
})(),

// normalize touch and mouse events
START_EV = hasTouch ? 'touchstart' : 'mousedown',
MOVE_EV = hasTouch ? 'touchmove' : 'mousemove',
END_EV = hasTouch ? 'touchend' : 'mouseup',
CANCEL_EV = hasTouch ? 'touchcancel' : 'mouseup';

Next, let's stub out an object.


	// Constructor
	Cube = function () {};

	Cube.prototype = {
		// handle bound events
		handleEvent: function () {},

		// rotate the cube
		_rotate: function () {},

		// for animating with momentum
		_animate: function () {},

		// bind events
		_bind: function () {},

		// unbind events
		_unbind: function (eventName, element, bubble) {},

		// called on touchstart / mousedown
		_start: function () {},

		// called on touchmove / mousemove
		_move: function (e) {},

		// called on touchend / mouseup
		_end: function (e) {},

		// called on transition end
		_transitionEnd: function (e) {}
	};

And now we'll start coloring everything in. Let's start with the constructor.

Cube = function (element) {
	this.container = element;
	this.outerWrapper = this.container.parentNode;

	this._bind(START_EV);

	if (!hasTouch) {
		this._bind('mouseout', this.outerWrapper);
	}
};

Remember above when I was describing the markup and how I said we'd want to add another element around the cube? We're using that element (this.container.parentNode) to bind the touch events to. If we use the cube container (the element we're actually rotating) the active area in the 2D space decreases as the angle of rotation becomes more perpendicular to the plane you're interacting with. Binding the interaction to an element that does not rotate makes the whole thing considerably more fluid.

Next, we know there are some events we need to handle: touchstart, touchmove, touchend, and transitionend -- but let's hold off on talking about what we need to do on transitionEnd for right now.

On touchstart, we'll want to know what time the touch started (important for evaluating the speed of the swipe,) the starting X coordinate, and the starting Y coordinate. We'll also want to reset any currently running rotations and any inline styles applied by running animations.

_start: function (e) {
	var point = hasTouch ? e.changedTouches[0] : e,
	      self = this;

	this.container.style.webkitTransitionDuration = 0;
	this._rotate(0);

	this._unbind('webkitTransitionEnd', this.container);

	this.startTime = e.timeStamp;
	this.startX = point.screenX;
	this.lastX = point.screenX;

	this._bind(MOVE_EV);
	this._bind(END_EV);

	this.animationDuration = 0.01;
},

The method called on move should evaluate the current X position of the touch / mouse event and compare it to the last recorded coordinate, then set the last recorded coordinate to the current value.

_move: function (e) {
	var point = hasTouch ? e.touches[0] : e;
	this._rotate((point.screenX - this.lastX) / 2;
	this.lastX = point.screenX;
	e.preventDefault();
},

The method called on touchend / mouseup should evaluate the speed of the movement and animate the cube accordingly. 300ms, while somewhat arbitrary, felt like a good delimiter between a gentle rotation and one that should apply some momentum. This isn't a perfect way to do this as we should also probably be evaluating acceleration here to distinguish between a somewhat steady pan and one whose speed is increasing significantly. For the sake of simplicity, we'll table that for now...

_end: function (e) {
	var self = this,
	    now = e.timeStamp,
	    speed = now - this.startTime;
	
	if (speed < 300) {
		this._animate((this.lastX - this.startX) * 2, speed);
		this.animationDuration = 0.01;
	}

	self._unbind(MOVE_EV);
	self._unbind(END_EV);
},

Since we've already referenced _rotate() and _animate() in out methods above, let's talk about them next.

Our _rotate() method is pretty straightforward. it takes some value as an argument and rotates the cube by that value (in degrees.) However, because we're applying the rotate using the WebKitCSSMatrix object, this demo only works in webkit (though it would be great if there was a moz equivalent.) Why use WebKitCSSMatrix? It's purely for the sake of simplicity as it makes getting and setting 3D Matrix values really easy.

_rotate: function (y) {
	var currRotation = window.getComputedStyle(this.container).getPropertyValue('-webkit-transform'),
	     m = new WebKitCSSMatrix(currRotation),
	newRotation = m.rotate(0, Math.floor(y), 0).toString();
	this.container.style.webkitTransform = newRotation;
},

The _animate() method does a little bit more. If you recall from above, we only call this when the total duration of swipe is under the set threshold. What we want to do here is call _rotate() periodically, each time reducing the rotation and increasing the duration of the animation to create the effect of deceleration.

_animate: function (x, speed) {
	var target = x || this.targetX,
	     self = this,
	     deceleration = 0.85,
	     speed = speed || self.velocity,
	     duration = 0.01;

	this.targetX = x;
	this.velocity = speed;
	this.speed = this.velocity;
	animate = function (){
		self.container.style.webkitTransition = '-webkit-transform ' + self.animationDuration + 's linear';
		self._bind('webkitTransitionEnd', self.container);

		self._rotate(self.targetX);
		self.targetX = (self.targetX) * (deceleration * deceleration);
		self.animationDuration = (self.animationDuration * (1.5 / deceleration)).toFixed(2);
	};

	if (Math.abs(this.targetX) > 0.2) {
		this.animation = nextFrame(animate);
	} else {
		cancelFrame(this.animation);
	}
},

Remember I said we'd talk about transitionEnd? Now seems like a good time. When we called the local animate function above, we bound transitionEnd event to the container, which simply unbinds itself and calls the _animate method again. if the target rotation is above the threshold, it continues the animation. It's as simple as that.

_transitionEnd: function (e) {
	this._unbind('webkitTransitionEnd', this.container);
	this._animate(this.targetX);
}

Finally, let's wire this thing up.

handleEvent: function (e) {
	var self = this;
	switch (e.type) {
		case START_EV:
			self._start(e);
			break;
		case MOVE_EV:
			self._move(e);
			break;
		case END_EV:
			self._end(e);
			break;
		case 'mouseout':
			self._end(e);
			break;
		case 'webkitTransitionEnd':
			self._transitionEnd(e);
			break;
	}

},

_bind: function (eventName, element, bubble) {
	(element || this.outerWrapper).addEventListener(eventName, this, !!bubble);
},

_unbind: function (eventName, element, bubble) {
	(element || this.outerWrapper).removeEventListener(eventName, this, !!bubble);
},

So, that's it... now I just need to find practical application for this.

Comments

crimsonchilla
Watch for chewing, especially around items such as electric cords. Try to reward your animal whenever they respond or pay heed to your voice when you utter out their name. Children will love that Toffee the pony loves to be fussed over and pampered.
kanye west
Hey are using Wordpress for your blog platform? I'm new to the blog world but I'm trying to get started and create my own. Do you need any coding expertise to make your own blog? Any help would be really appreciated!
Metal Slug gratis
Remarkable! Its genuinely awesome paragraph, I have got much clear idea about from this article.
Slots Pharaoh s...
Amazing blog! Is your theme custom made or did you download it from somewhere? A design like yours with a feew simple tweeks would really make my blog stand out. Please let me know where you got your design. Appreciate it
Swamp Attack Cheats
It's not my first time to pay a visit this website, i am browsing this wweb site dailly and get good data from here everyday.
Armand
I am regular visitor, how are you everybody? This post posted at this web page is in fact good.
Vernell
Heya! I'm at work browsing your blog from my new apple iphone! Just wanted to say I love reading your blog and look forward tto all your posts! Keep up the fantastic work!
Dream League So...
I like thee valuable info you provide on your articles. I'llbookmark your weblog and test once more here regularly. I'm somewhat certain I'll learn plenty of new stuff right here! Good lujck for the following!
clash of lords ...
I just could not depart your web site prior to suggesting that I actually loved the standard info an individual supply in your guests? Is going to be again incessantly to investigate cross-check new posts
Real Racing 3 H...
Everything is very open witgh a really clear explanation of the challenges. It was truly informative. Your website is useful. Maany thanks for sharing!
Dream League So...
Hey There. I found our blog using msn. This is a really well written article. I'll make sure to bookmark it and return to read more of your useful information. Thanks for the post. I will certainly return.
Metal Slug Defe...
What's up, for all time i used to check weblog posts here earlpy in the dawn, becauise i love to gain knowledge of mor and more.
moviestarplanet hack
From hair oil to cricket, wherever Amitabh Bachchan has his name hooked up in just one way or perhaps the other he assures achievement. They have a distinct search in conditions of style and shades, such as Camo types. It is no secret that a boost in confidence and having a positive self-image can contribute to a woman's over all well being but the majority of women do not have cosmetic surgery for anyone other than themselves.
fifa 15 coin ge...
Then, have students find five other countries and choose a different colored star to label. Basic Soccer Rules - 17 Rules. - An ascending or a rising channel makes consecutive higher highs and higher lows.
Eminem
I am not sure where you are getting your information, but good topic. I needs to spend some time learning more or understanding more. Thanks for magnificent information I was looking for this information for my mission.
Senaida
The Dell Thunder is packed with a 4. They are fun, very competitive and who knows you may find yourself with a few nice prizes, on top of the bragging rights of being a FIFA League champion. Spread buyers of the group index will be concerned that the side have only once before made the finals, in 1982.
Eminem lyrics
It's awesome for me to have a website, which is useful in support of my experience. thanks admin
not fake
While loans from non-traditional lenders are widely accessible and have easy approvals, you will find few major criteria to satisfy not fake therefore, if you've 800 credit scores and 50% equity in your house, you may obtain a better rate and closing costs than someone with 630 credit scores with below 20% equity within their house.
Maria
Howdy would you mind letting me know which web host you're using? I've loaded your blog in 3 completely different web browsers and I must say this blog loads a lot faster then most. Can you suggest a good hosting provider at a honest price? Thank you, I appreciate it!
hack tool download
You can also use a melee weapon to get the achievement by getting a score of 40 or 50. 99Nightcrawler #7, $3. It looks like she'll get a bit of justice after all.
my website
You need to know the way to apply and understand how to qualify my website the immediate effect is likewise to raise the price tag on every new home in arizona by $25, 000 to $50,000.
Tech recruiters
It's the best time to make some plans for the future and it's time to be happy. I've read this post and if I could I desire to suggest you some interesting things or advice. Perhaps you can write next articles referring to this article. I wish to read more things about it!
Madonna
WOW just what I was looking for. Came here by searching for gold watches
application process
Very soon this website will be famous amid all blog people, due to it's pleasant content
cialis
Hmm is anyone else having problems with the images on this blog loading? I'm trying to find out if its a problem on my end or if it's the blog. Any responses would be greatly appreciated.

Add new comment