Rubik's

Rubik's Cube ® used by permission of Seven Towns Limited. | Published in 2012

Rubik's cube

Tutorial: Rubik's cube with HTML5 (CSS3 + JavaScript)

: Motivation : Perspective3D : Modeling the cube : The movements : Rotation : JavaScript cross-platform :

Introduction

This webpage was built entirely using only HTML5, basically CSS3 and JavaScript (with the power of YUI Library). With this project, I wanted to show using a real and interesting example, how we can combine the power of CSS3 3D transforms, with some of the new features and API's that HTML5 provides.

You can find the code under my Github user. Feel free to fork, pull, comment or whatever other things you like!

Motivation

This part is non-tecnical stuff, so you can skip it if you like ;)

I've decided to create this webpage after my girlfriend's sister challenged me to solve a Sudoku Rubik's cube. I knew more less how to solve the original one, so I thought that would be an easy task... Far from being truth! After trying a couple of hours, I realize that I was missing an important part: What exactly was happening in every move, or with another words, what was the algorithm like behind that puzzle.

After some research I find out, that most of the websites were really old (with a really ugly UI), and the instructions to solve the cube weren't intuitive at all. So I give up trying to understand those complicated notations, and instead I started to watch some videos to see real examples on how to solve it instead.

Then, I realized that it would be awesome to create a 3D Rubik's cube, where I can go step by step seeing which is the movement to do, and what exactly was going on in every step. And of course, why not doing it in HTML5!!

Step 1: Perpective 3D

The first thing I needed to do is to create the cube model in 3D. I had a couple of choices there using some HTML5 features: WebGL, Canvas... But there was already many HTML5 examples using those technologies. So then I realized that CSS3 and 3D Transforms, could be an awesome way to solve my problem, and more than that, I could create something new (almost no good examples taking fully advantage with the 3D in CSS).

So after checking some docs here and there, to get my mind fresh with the 3D perspectives and transformation usages, I started to mock up the cube. An important note here before start, is that not all browsers have support for 3D transformations yet. So temporarily, I focused in get it working on Webkit browsers (since I wanted to get the Rubik working on iOS devices), but soon will be compatible in almost all new browsers.

The first thing we need to do is tell the browser we want to render 3D elements, and second we want to specify how, or from where those elements will be represented (the perspective):

        
    #cube-viewport{
        -webkit-perspective: 800;
        -webkit-perspective-origin: 50% 200px;    
    }

    #cube{
        position: relative;
        height: 400px;
        width: 400px;
        -webkit-transform-style: preserve-3d;
    }
    

You can think about perspective like if we had a camera, from where we want to record our elements. Note also that #cube will be a child of #cube-viewport

Step 2: Modeling the cube

Now, let's start drawing our cube!

Our rubik is made of little cubes called "cubies". Every face of the cube contains nine cubies, and those cubies will change their position regarding some move, but we will go there later.

Every cubie will have a css class, which will map where on the space each cubie will be positioned.

/* utl matched the cubie on the up face, on the top left */ .utl {-webkit-transform:rotateX(90deg) translate3d(50px,-100px,0)} /* utl matched the cubie on the up layer on the top center */ .ucl {-webkit-transform:rotateX(90deg) translate3d(50px,0,0)} /* utl matched the cubie on the up layer on the center right */ .ucr {-webkit-transform:rotateX(90deg) translate3d(250px,0,0)} /*back face top left*/ .btl {-webkit-transform:rotateX(-180deg) translate3d(50px,-250px,150px)} ...

If you do the maths, I had to create 6 * 9 class in total and position each of them in the proper 3d coordinates, which was a pain in the ass, but at least I just had to do it once :). Regarding CSS style, I could also use matrix3d property which allows me to do the same thing, but more compressed and pretty. Also, for simplicity I used fixed dimensions, but another possibility (more flexible) would be create the styles on the fly via JavaScript.

Moreover, in order to rotate the cube we will use some dynamic CSS styling via JavaScript, just adding the rotations degrees of X and Y:

        
        cube.style.webkitTransform = 
        "rotateX(" + x + "deg) rotateY(" + y + "deg)";
    

It is important also to mention that the coordinates are symmetrical to a centered plane, if not, when we rotate the cube, the center will be unaligned (you can see how it looks in example1).

#Example1 This is what I get complete after the first steps:

I encourage you to check the source code of #example1 since is really clean and understandable.

Step3: The movements

Here it's where the fun begins! :)

Let's define first the possible movements we can do with our cube. There are couple nomenclatures for the moves, I'm going to use one based on the types of moves and faces (better for future algorithms), but we will try to map also with the traditional one (For example: LM maps with just L move).

So there are three possible movements, and each of those apply also in three slices on the cube, you can check the example2 to make sure you understand the moves:

If you check the source code of #example2 (the cube next to this text), you will see that at the end of the day, each cubie belongs to three different movements.

For example: The cubie "F1", will have to rotate when we move the cube on: LM, UE and FS, no matter which direction (clockwise or anticlockwise)

We conclude that every cubie "belongs" to three totally different moves, which means that it could be translated along three different planes, so we cannot create any kind of nesting or hierarchy in the DOM. I mean, imagine we want to rotate in FS (front face) to the right, and after that DE (down face) to the right as well. F1 should first rotate to be in the Down face and the rotate again.

How are we going to manage that? We will see it in the next chapter! :)

Step4: Rotation

As I showed you before, every cube can be repositioned using three different moves, that's what it makes our job a bit more complicated. Why exactly?

Imaging that we want to do two rotations: First rotate the left side (ML) clockwise and second rotate the down side (DE) clockwise as well. Let's just focus to what happen in a single cubie, for example F1 which is on the front face, top left (ftl).

After executing the first move (ML), the cubie "F1" changed from the front face, top left position (ftl), to the down face, top left (dtl). The mapping is in this case: (ML-right: "ftl" => "dtl").

After executing the second move (DE) the cubie F1 which was in dtl, it went to the down face on the bottom left position (dbl). The mapping rule here: (DE-right: "dtl" => "dbl").

Don't worry if you get lost with all this names, and movements, you can check exactly what I meant in the #example3, but first, let's keep going with the rotation.

Now, how to know which cubies are involved in a particular move and how to move them if there is no common parent for all of them? Easy! Create a container plane dynamically which will contain all cubies involved in the move being triggered. We have every cubie identified with the movements/planes to which that cubie belongs.

     
        // Creating a move here, 
        // but we should receive it from somewhere else...
        var move = {
            face: 'F',
            plane: 'S',
            rotation: 'right'
        };
        //got the cubies with that move (FS)
        list = Y.all('.' + move.face + move.plane);
    

Once we get all the cubies that belongs to that particular move we will rotate the new populated plane according to the given move. I did that creating a css class selectors which will tell the plane how to rotate, also we provide the movement speed and ease function:

/* PLANES & ROTATIONS */ #plane{ -webkit-transform-style: preserve-3d; } /* Set speed and move function */ .moving { -webkit-transition:all .2s linear; } /* PLANE M */ /* Transform the origin to get the point of rotation centered */ .M-left { -webkit-transform-origin: 0px 200px; -webkit-transform:rotateX(90deg); } .M-right { -webkit-transform-origin: 0px 200px; -webkit-transform:rotateX(-90deg) } /* PLANE E */ .E-right {-webkit-transform:rotateY(90deg)} .E-left {-webkit-transform:rotateY(-90deg)} /* PLANE S */ /* Transform the origin to get the point of rotation centered */ .S-left { -webkit-transform-origin: 200px 200px; -webkit-transform:rotateZ(-90deg); } .S-right { -webkit-transform-origin: 200px 200px; -webkit-transform:rotateZ(90deg); }

Once the the move transition is over, we have to reorganize the cubies. The reason of this is because as we saw on the example2 the cubie itself has the class which indicates it's position, and now, it's position has change. Going to the previous example move: Cubie F1 has the class "ftl" when it starts, but after applying the move "ML-right" (left side right), it should be in the position "dtl". How to do this nicely? Well I didnt find any, I had to hardcode the mapping between the cubies position and the moves (I would be happy to hear more solutions to this! :P ).

var CUBIE_MOVEMENTS = { 'LM-left':{ "utl":"btl","ucl":"bcl","ubl":"bbl","ftl":"utl","fcl":"ucl","fbl":"ubl","dtl":"ftl", "dcl":"fcl","dbl":"fbl","btl":"dtl","bcl":"dcl","bbl":"dbl","ltl":"lbl","lcl":"lbc", "lbl":"lbr","ltc":"lcl","lbc":"lcr","ltr":"ltl","lcr":"ltc","lbr":"ltr","lcc":"lcc" }, "LM-right":{ "utl":"ftl","ucl":"fcl","ubl":"fbl","ftl":"dtl","fcl":"dcl","fbl":"dbl","dtl":"btl", "dcl":"bcl","dbl":"bbl","btl":"utl","bcl":"ucl","bbl":"ubl","ltl":"ltr","lcl":"ltc", "lbl":"ltl","ltc":"lcr","lbc":"lcl","ltr":"lbr","lcr":"lbc","lbr":"lbl","lcc":"lcc" }, 'RM-right':{ "utr":"ftr","ucr":"fcr","ubr":"fbr","ftr":"dtr","fcr":"dcr","fbr":"dbr","dtr":"btr", "dcr":"bcr","dbr":"bbr","btr":"utr","bcr":"ucr","bbr":"ubr","rtl":"rbl","rcl":"rbc", "rbl":"rbr","rtc":"rcl","rcc":"rcc","rbc":"rcr","rtr":"rtl","rcr":"rtc","rbr":"rtr" }, 'RM-left':{ "utr":"btr","ucr":"bcr","ubr":"bbr","ftr":"utr","fcr":"ucr","fbr":"ubr","dtr":"ftr", "dcr":"fcr","dbr":"fbr","btr":"dtr","bcr":"dcr","bbr":"dbr","rtl":"rtr","rcl":"rtc", "rbl":"rtl","rtc":"rcr","rbc":"rcl","rtr":"rbr","rcr":"rbc","rbr":"rbl","rcc":"rcc" }, ...

And finally after this painful exercise, and after creating the function to reorganize the cubies based on that mapping, you will get the #example3 up and running:

Depending on your browser and in you hardware acceleration you may get a better transition effect. Some test I did using different computers/browsers show little problems when the transition is going on. If someone has any optimization I would be happy to hear!. Anyway in iOS devices the performance is dame impressive!

Also you may notice that when the transition ends, there is kind of weird reorganization/flush on the cube. That's because as I explained you before, we are doing a reorganization of DOM nodes, where we are grouping in a container some cubies together to do the rotation, and when it finishes, we unpopulate that container and put the nodes back in the original place. When you do this kind of manipulation in the DOM, the browser has to re-render( specifically repaint and reflow ) the content. Again I couldn't polish it more there... :)

Step5: JavaScript cross-platform

As you may know, when you're writing a complex web application, it's convenient to rely on a JS library to abstract all those "little" differences between browsers, and make your life easier. And more than that, in an application where we want to target different devices (tablets, phones, desktop...), the abstraction of specific events and features such as gestures, flicks, rotations... will be more than welcome!

For this application, I used YUI.

One of the first things I did was to abstract the user-interaction/events no matter which device or platform. In order to do that I use a module in YUI called gestures: _bind: function () { this._cube.on('transitionEnd',this._endTransition,this); //gestures abstract click/tap/touch this._container.on('gesturemovestart',this._onTouchCube,{},this); this._container.on('gesturemove',this._onMoveCube,{},this); this._container.on('gesturemoveend',this._onEndCube,{},this); //we use those to handle multitouch this._container.on('gesturestart',this._multiTouchStart,this); this._container.on('gesturechange',this._multiTouchMove,this); this._container.on('gestureend',this._multiTouchEnd,this); //this keeps our app maximizing the screen size Y.one('body').on('gesturemovestart',this._checkScroll,{},this); },

As you saw in some of the previous examples we track the user mouse/finger to move the cube:

_onMoveCube:function (evt) { evt.halt(); var x = this._deltaX = ((evt.clientX - this._startX)/1.2), y = this._deltaY = ((evt.clientY - this._startY)/1.2); ... this._cube.setStyle('webkitTransform','rotateX('+ y + 'deg) rotateY(' + x + 'deg)'); },

The last important piece, is when we receive the "gestureend" we have to map how the user flicked or clicked on the screen, with what movement we should perform on the cube. And the tricky part here, is that basically we have to move from a 2D space to a 3d space...

I'm not going to put the example code here because it takes too much lines (needs some refactoring), but feel free to check out the function " _onEndCube " in the #example3

And last but not least, I added bunch of little nice features to improve the user-experince, like using the accelerometer when the user is in portrait mode, or changing views regarding the orientation.

Done!

I hope you enjoy the tutorial, and that you learn something new!

Acknowledgment

I want to mention and give special thanks to Stephen(@hypernaut), which help me debugging some CSS issues, Andrea, Magda and Lu(@luciamarinof) for the grammatic revision of this article, and all other friends which helped me beta-testing the cube.