I finally found some time to finish my short series of tutorials on CSS 3D transforms. If you haven't checked out part 1 & 2 I recommend you to do so, otherwise you will feel lost pretty soon.
In this tutorial we will learn how to build a 3D packshot in HTML and CSS by applying some CSS 3D-transforms. Then we will add some Javascript to make the object freely rotatable in 3d space. And as we will enhance our Javascript with some touch-interactivity, the packshot will also work nicely in Safari for iOS-platforms like iPhone or iPad. A warning: be prepared that the second part of this tutorial will be quite math-heavy.
Open Example...or direct your mobile-browser to www.eleqtriq.com/wp-content/static/demos/2010/rotation/ (sorry for the mobile-unfriendly URL).
The current State of CSS 3D
Since I published my first tutorial a few months ago, more and more sites showed up that actually use CSS3D transforms. But as we all have learned, support for CSS3D-transforms is still marginal outside the iOS world. So what is the use in learning this technique anyway?
- Chromium has already integrated experimental support or CSS 3D Transforms on Windows (this is great. Now my Windows-visitors are able to enjoy the demos too! If you are on a windows machine, download a copy of the latest Chromium and come back. Welcome on board, folks :) [Update: just found out that Safari 5.0.2 for Windows now supports CSS 3D as well. So Windows users do have more options than Mac-users :)] With Chromium and iOS we do not have a large user-base on the desktop, but we will have a real large chunk of the mobile market that will be able to see CSS 3D.
- Apples iAD-Platform will make heavy use of all the advanced Webkit CSS3-tricks. With a current iPhone-market share of 26% of the smartphone market (October 2010) demand for iAD development is expected to continually increase over the next few months. So if you are in the online marketing ad-creation business, CSS3D will be something you should keep in mind.
Enough said, let's start the tutorial:
1. Building the packshot with HTML and CSS:
To demonstrate CSS 3D on a real world-example we will create a virtual packshot for Spritebaker. We start with a basic HTML-"sceleton":
<!-- We need something to apply some CSS 3D-perspective to it. Therefore we set up a section as our stage. -->
<section id="viewport">
<!-- The next container is our box. It consists of 6 sections for every side: -->
<article id="box">
<section id="front">
<!-- HTML for the front goes here-->
</section>
<section id="top">
<!-- HTML for the top goes here-->
</section>
<section id="bottom">
<!-- HTML for the bottom goes here-->
</section>
<section id="back">
<!-- HTML for the back goes here-->
</section>
<section id="left">
<!-- HTML for the left-side goes here-->
</section;>
<section id="right">
<!-- HTML for the inner-side goes here-->
<!-- Just kidding :)-->
</section>
</article>
</section>
In order to style the content we will use a handful of conventional HTML and CSS-techniques, therefore I will not bore you with too much details, you are a gown-up web-developer, right? Some lame 2005 image-replacement and CSS-sprites to make everything fairly accessible and crawlable (as you know I'm a big proponent of Data-URI-Sprites, but I avoid them in my demos most of the time to keep source-code readable).
2. Applying the CSS 3D Transforms:
More fun to come: let's form a nice 3D-box by rotating and positioning every side. The CSS transforms are straightforward and easy to understand:
section#viewport{
perspective: 700px;
perspective-origin: 50% 50%;
}
article#box{
transform-style: preserve-3d;
}
#box section{
transform-style: flat;
/*browsers are bitches at z-sorting css3d-transforms. Use backface-visibility: hidden as often as you can! */
backface-visibility: hidden;
}
#front, #back{
width:250px;
height: 354px;
transform: translate3d(0px, 0px, 37px);
}
#back{
transform: rotateY(180deg) translate3d(0px, 0px, 37px);
}
#top, #bottom{
width:250px;
height: 74px;
transform: rotateX(90deg) translate3d(0px, 0px, 37px);
}
#bottom{
transform: rotateX(-90deg) translate3d(0px, 0px, 317px);
}
#left, #right{
width: 74px;
height: 354px;
transform: rotateY(-90deg) translate3d(0px, 0px, 37px);
}
#right{
transform: rotateY(90deg) translate3d(0px, 0px, 213px);
}
We add some spice by adding a webkit-box-reflect below and voila, ready is our box! Attentive readers will see that the reflection is erroneous, with the box-shape accurately reflected but not the typography on the box. This is a bug in webkit for MacOS. Applying a 3D-transform to an element causes that its content can't be reflected properly (strange to say this bug doesn't affect Safari for iOS).
3. Making the Box rotatable:
It's starting to get interesting: let's add some interactivity with Javascript so we are able to twist and turn the box with the mouse (side-note: we will avoid any Javascript-library like jQuery or Motools and code directly for Webkit instead, as its our sole target-browser. This way the script stays lean and small, <3kb gzipped without comments, mobile browsers will love you for that).
Some Words about Object-Rotation in 3D-Space:
Rotating and twisting objects in 3D-space might not sound like a big deal: just change the object's angle according to the mouse position on the screen, right? Unfortunately it's not that easy. The movement will feel unnatural and "bumpy" and we will have to deal with the problem of "Gimbal-Lock".
There exists a better technique for achieving a natural and smooth experience for the user. It's called the "Virtual Trackball". The Virtual Trackball is an imaginary sphere around our 3D-object. Every mouse-click on the screen will be mapped onto this virtual sphere and every dragging-operation will cause a rotation of the virtual trackball and the object inside, resulting in a smooth and intuitive rotation. Let's see how to build a virtual trackball for our box:
Mathematics of the Virtual Trackball:
How to construct the virtual trackball:
The center of the box and the Sphere must be at the center of the coordinate-system (y=0, y=0, z=0), which is also the center of the stage. The sole purpose of the virtual trackball is calculating angles in 3d-space. We are not interested in distances, it is enough to know their relation. Therefore we will put the sphere into an easy to calculate coordinate-system where its radius is simply "1". Mouse-coordinates on screen can be translated to this new coordinate system easily by dividing all values by the "untranslated" radius. Choosing the right radius is a matter of instinct. Choose a value that "feels right". I decided to make it half of the shortest side of the viewport. This maybe not o. k. in every situation though, you will always have to test and make your decision accordingly.
2. On mouse down, detect the mouse-coordinates and map them onto the trackball:
we translate screen-coordinates to trackball coordinates:
sphereX*2=mouseX/radius
sphereY*2=mouseY/radius
..and then move [0,0] to the center of the trackball coordinate-system:
x = x - 1;
y = y - 1;
We already know the radius of our sphere (it's 1, remember?) so it's quite easy to determine the z-coordinate of the translated point with the help of some good old trigonometry:
2*Z2=1-x2-y2
Now that we have everything in place, we can start building the rest of the logic. I will give an overview in pseudocode. One more remark: the script makes heavy use of 3d transformation-matrices and rotate3D, but avoids "rotateX, Y, Z". If we would use single-axis-rotations it would be necessary to calculate the rotation in quaternion-space to avoid gimbal-lock. If we use rotate3D or matrix3d, webkit will handle the quaternion stuff for us.
At first-run or at the moment the user releases the mouse (or lifts his hand) we create a new matrix3D from the last axis-vector and angle, store it in a variable and apply it to our box. When the user decides to click and rotate again, we will apply this startmatrix together with rotate3D to our box. This way we prevent our box from flipping back to it's initial, "unrotated" state on every mousedown/touchstart-event.
function init(){
set up the virtual trackball;
search the viewport-html-element for something to rotate;
add mousedown- or touchstart-listener to prepare rotation;
add mouseup- or touchend-listener to finish rotation;
add mousemove- or touchmove-listener to rotate;
calculate initial matrix3d from initial angle and rotation axis;
}
function startrotation(){
track click-position and translate it to trackball;
store resulting 3d-vector in variable "mouseDownVect";
}
function rotate(){
track current mouse-position and translate it to trackball;
store resulting 3d-vector in variable "mouseMoveVect";
find rotation-axis by determining normal on mouseDownVect and mouseMoveVect;
find rotation-angle between mouseMoveVect and mouseDownVect;
apply startmatrix and rotate3d(axis, rotation-angle) to the box;
}
function finishrotation(){
calculate new matrix3d from last angle and rotation axis;
combine the last start-matrix and this new matrx to a combined matrix by multiplication;
make current matrix3d new start-matrix;
}
This is an overview of the actual logic. If you want to dig deeper, you should check out the Javascript, I gave my best to comment every step.
The script (I call it "traqball.js" by the way) is released under MIT- and GPL-license and you are alllowed to use it in your projects as long as you stick to the terms of license. Implementation is easy:
- Create some HTML-element (div, scection, article etc.) as viewport
- Apply some -webkit-perspective to it (values around 600 are o.k.)
- Pass the id of viewport over to the "init"-function along with an initial rotation-axis in vector-form ([a, b,c]) and an initial rotation-angle (in radians) and...
- ...place your rotation-"target" block-element inside the viewport. It must be the first block-child, then the script will find it.
Ok, we're through! Hope you enjoyed this little lesson. If you have used the script in your projects I would be more than glad if you send me a link.
Update updated traqball.js to version2.0. Source now is on Github. Read more here..
[…] Visit tutorial […]
Hi :)
Nice stuff & nice effect! I’m trying to connect your magic 3d box with the gyroscope datas of the pad (x,y,z) instead of finger position to obtain something like like a pseudo cam (the box seems to stay fixed in space keeping its right perspectives when the pad moves) but I cant understand how to do this because the movement is waiting for a touch on screen to be initiated. I suppose that it would be easy to remove the touch events and replace xTouch/yTouch by the gyro datas, but I’m afraid that the object-supposed-to-be-clicked will be hard to define? any idea ?