This article is part of a series of three:
- Part 1: setting up the app in a local setup and preparing the main elements
- Part 2 (this article): loading images, adding a moves history and an “auto play mode”
- Part 3: serving the app in production mode (with a Golang server) and deploying it using Docker
Last time, we set up the app for development mode, we added our cube and the rotate-on-swipe mechanics. We also introduced a little “switch trick” to get more suitable animations by always forcing the cube to go back to its “front” face after a rotation. Here is a little video showing the end result of Part 1:
Today, we’ll replace our placeholders with actual images, we’ll add a moves history so we can “undo” and “redo” our swipes and we’ll code up a little “auto play mode” so that, if the app is idle for a while, the cube starts moving by itself.
Note: big thanks to Cali Rezo for the images of animals used throughout this tutorial to embellish the Cube App! 😉
Loading up images on the cube
Let’s start easy. Here is a naive (and actually close-to-perfect) idea of how to load our images:
IF swipe THEN direction = getSwipeDirection() SET random image ON FACE MATCHING direction START ROTATE IN direction WHEN END ROTATE SET same image ON "front" FACE SWITCH TO "front" FACE INSTANTLY END IF
Now, there is an issue with this pseudo-code that might not be obvious at first (especially while you’re in dev mode): images might appear too late and the cube might seem blank for a while when we land on a new face. The problem appears on the two lines:
SET random image ON FACE MATCHING direction START ROTATE IN direction
If the image has not yet been “painted” on the cube face, then this face could appear empty at the beginning of the rotation. This is because loading images is not instantaneous. Images are bigger assets, or resources, than plain scripts – they are heavier to load, process and send. The bigger the image, the longer the wait. And if you happen to have some latency on your server (overall every communication), then there might be a moment long enough to be perceptible by a human during which no image has been loaded yet.
While in development on your computer, because your server is running on the same machine as your client, the latency is virtually null. So you probably won’t see any problem. On the other hand, if you test the app on your mobile, there might be enough of a latency for this little blank frame to show.
The solution is to “preload” images: rather than set the image when we get to the new face, we can instead prepare random faces that are still hidden so that when we arrive on them they are already loaded.
Setting the images
images. In our production mode, this variable will be filled by our server upon launch (by reading a specific data folder). For now, we can simply write some image references by hand in our
start() function, something like:
images = [ 'aigle.jpg', 'albatros.jpg', 'babouin.jpg', 'calao.jpg', 'chat.jpg', 'chien.jpg', 'chouette.jpg', 'coccinelle.jpg', 'crevette.jpg', ... ];
This is the list of files I have in my test images folder,
img/, located just next to the
We also need to define an object that matches each side of the cube with the image currently displayed on it –
sides: this will help us remember the various images we picked for our cube faces for the undo/redo feature (see next section).
It is important to note that we need to be able to change the image of a face in two ways:
- for the hidden faces, we can instantly change the image (because we don’t see them so it won’t cause any glitch!)
- for the front face, with our “switch trick”, we need to wait for the rotation animation to finish before changing the image
In other words, whichever function changes the image must be able to handle both the zero- and the 0.65s-timeout cases. To make that easier, we will isolate the image switching process in a util function and call it from a wrapper. This way, we make sure that the process is the same with and without the timeout, and we just let the wrapper care about timeouts.
Thirdly, we want the hidden faces of the cube to be populated with random images from our initial list, to keep the “endless and magical cube” feeling… but we actually need to exclude the images already in use! Otherwise, we might get duplicates or illogical image transitions. This list of images we can pick from will be stored in a second variable,
availableImages, that is the difference between the
images global list and the list of images currently defined in the
- first, a generic
sample()function that picks a random element in an array
- then, a
changeImage()function to set the image on a cube face: it is simply the wrapper around the
_changeImageUtil()method that will actually perform the switch
prepareImages()function that preloads images on the hidden faces of the cube
After all this talking, here is finally the corresponding code!
First step: adding all of our methods:
Second step: cutting down our
rotate() function in two and using our newly defined functions in it:
Third step: using all of this in our
start() function to initialize the process:
Last step: in our CSS file, we should now remove the
background property from our various
cube__face--left… classes since this background color is replaced by an image:
[snippet slug=201128_caliscope-tuto_11 lang=css]
At this point, we have nice images on our cube that are preloaded and all, yay! (I’ve kept the “front”, “left”… labels for now so you can still see the “switch trick” in action, but we will clean this up very soon)
Cleaning up the file
Not much to say, except that the labels on the faces aren’t useful anymore. So, in our
index.html file, we can remove the text inside the
[snippet slug=201128_caliscope-tuto_12 lang=html]
Creating a moves history
If you take a look at the video above, you’ll see that going back and forth between faces re-picks random images every time. Although this works, it makes for a strange feeling when manipulating the cube.
In the Caliscope, there is actually an undo/redo mechanism. Say you’re on a face with a painting A. You then move a face with another painting B. If you “go back” to the first face, you will find the same painting A as before. Then, you can redo your move: if you go in the same direction as you did at first, you won’t get a brand new painting but the painting B you already picked. This helps give users a feeling of “logic” – instead of having a full random cube, you have a magical cube that spawns new paintings infinitely but still has a realistic lifecycle.
The trick is that, of course, if you follow a different path (i.e. if you move in a different direction), you get a new random painting C. Therefore you can have an “endless” (well, in truth, roughly 50-items based) series of paintings.
To do so, we need to keep a history of the moves so we can iterate back and forth through it and replay our latest moves.
Storing the moves
Technically speaking, the moves history is based on stacks – an abstract data type that uses the LIFO (or “Last In First Out”) philosophy. When you register your moves, you stack them on top of each other. Then, when you want to replay the past moves, you gradually pop the items starting from the top of the stack – so the last item that was added is removed first.
There is, however, a small twist: since we want to also have a redo feature, we can’t actually “pop” the moves we check out. Instead, we need to navigate through the stack and mimic the “push-and-pop” stack mechanism.
In our code, this history of moves is a list – each move is stored as an object with 3 relevant pieces of information:
- the direction we swipe in
- the image we have on the face we swipe from
- the image we have on the face we swipe to
We also need to define a constant mapping of “inverse directions”: this is a simple one-to-one correspondence of rotation directions to easily check for reverse moves that require history unstashing.
Adding the “undo” / “redo” system
The entire undo/redo system is added in the
rotate() function and it actually takes less than 30 lines. Yet it requires some changes that are worth detailing. Here is the updated code with a good amount of comments to explain the various important points:
I believe the comments in the code are enough to understand how this code works. The overall idea is that:
- first, we compare the last action with the current one to see if they are reverse to one another (in which case we need to do an undo)
- else if we don’t “undo”, we check if we are at the top of the history stack: if we are, we don’t have any moves to pick from; if we aren’t, i.e. if we have moves “above us” in the stack to look at, and we are going in the same direction as we did at the time these moves were stored, then we should “redo”
- if none applies, we are doing a “normal” move towards a randomly prepared face and we will store this new action in the history for further processing
And with that, we now have an undo/redo mechanism! On the following video, we see that if we go to the right, then back to the left, then back to the right, we only switch between an image A and an image B (you can compare it to the previous video where we had a new image every time):
This also validates that our history has a generic depth, i.e. it is not limited to a given number of move “undo”s. As long as we haven’t reached our initial state, we can go back one, two… n moves in our history!
Adding the “auto play mode”
We are very close to having a complete Cube App. The last feature we can add to reproduce the Caliscope behavior is the “auto play mode”. If you go to the online app and wait for a while, you’ll see that the cube starts to move by itself. More exactly, it starts to move randomly after 7 seconds of inactivity from the user. Then, if you swipe the cube directly, you will return back to “normal mode”.
To do this, we will combine the
setTimeout() function we already know and a similar JS built-in,
setInterval(). While the former lets us wait for a moment before launching a process, the latter will repeat the same process over and over using a given time period (so each process execution is separated by the same amount of time).
First of all, let’s add a little method called
randomRotate() to pick a random swipe direction and simulate a user input (we add a small filter to avoid repeating the same action multiple times, using the
Then, we can actually prepare our
stop()) function that will be called to enter (resp. leave) the “auto play mode”. We have two additional constants, bringing it to a total of 3 timeouts:
ANIM_TIMEOUT: the time it takes to rotate from one cube face to another
PLAY_TIMEOUT: the time between each auto-simulated random rotation
IDLE_TIMEOUT: the time to wait before entering “auto play mode”
These two methods are here to setup and clear timeouts and intervals – this will make sure that you don’t have any remaining processes when you’ve exited “auto play mode”.
Finally, we can update our
start() functions so that after
IDLE_TIMEOUT seconds, we enter “auto play mode”:
We’re almost done: we just need to exit the “auto play mode” when we re-swipe the cube. To do that, we have to check if the
rotate() function is called “by hand” or automatically by the computer to simulate a user input – we’ll add a basic
fromSwipe boolean parameter and use it to cancel the “auto play mode” if need be:
At this point, we have finished the Cube App and we have a result that is similar to the Caliscope (except for the images)!
We could add a few things to match the online app exactly, such as a shadow below the box or a little white fade-in when the app is launched… We should also add a small flag to check whether or not we are currently rotating so that we don’t mistakenly re-swipe and combine multiple cube rotations (this would result in weird visuals).
Note: I won’t cover those in this article. But I’d be glad to see what additions you come up with! 🙂
In the third and last article of this first tutorial, we’ll see how to make this app production-ready using a little Golang server and how to deploy it with Docker.
I hope you’ve enjoyed this post – as usual, don’t hesitate to react either in the comments section or on my LinkedIn.