The idea
At some point in our life we must have had contact with this kind of funny timers. The tomato is my favourite so I decided to replicate it.
After choosing the picture on the left to inspire myself, a few problems and question raised in my mind.
- How would I spread minutes in the 2D plane simulating 3D?
- How would I shift them in a “circular” way?
Spherical coordinate system
To help me accomplish this task, I decided that my timer would have the form of a sphere. Then, I would create “points” to represent minutes (or seconds) so that I could latter move them through rotation. The coordinates of a point along a sphere don’t use the Cartesian Coordinates System, instead, it uses different values from the Polar Coordinate System. The polar coordinate system is extended into 3D with two different coordinate systems, the Cylindrical and Spherical coordinate system.
We’ll be using the second and switch from (x ,y , z) to (r, θ, φ):
Also, we cannot forget about the OpenGL coordinate system, which is the Right Hand System, thus:
Spherical Coordinates for orbit rotations when camera is Y-up. (image source)
- Phi φ (vertical position) angle between -90° and 90° [0 ; π]
- Theta θ (y rotation) angle between 0 and 360° [0 ; 2π]
- Radius r [0; +00[
In our case, the radius will be fixed and φ the same for every minute, leaving only θ to be calculated:
Next we need to transform 3D space information into 2D taking into account rotation in y-axis which will represent the progress between 0 and 1 ([0 ; 2π]):
This piece of code was inspired on Alex Lockwood bees and bombs compose
And the output is:
left: π/2 | right: π/1.7
Camera transformations
First challenge is solved, we have minutes positioned across a circular path, but we’re not quite there yet. By analysing the above images, you can see each minute is facing forward - it’s more noticeable with the bigger strokes -, we need to apply a few transformations in order to rotate them:
To do that we’ll be using android.graphics.Camera
A camera instance can be used to compute 3D transformations and generate a matrix that can be applied, for instance, on a android.graphics.Canvas
.
Simply put, we use the Camera
to rotate a view on any axis.
Since Camera
uses android.graphics.Matrix
we can’t use the compose androidx.compose.ui.graphics.Matrix
operations with it. Are we in trouble? No, we can have access to the native canvas by calling canvas.nativeCanvas
, and this compose canvas
instance is returned by drawIntoCanvas
.
So, to achieve it, we use the following code:
- We get both
Camera
andMatrix
instances; - Apply the desired transformations (we’ll get there soon);
- Compute the
Matrix
corresponding to those transformations; - Move to origin since
Matrix
uses 0,0 as it’s transform point; - Move back to where it belongs (after concatenating);
- Save a copy of the current
Canvas
transform; - Apply the transformations by concatenating the
Matrix
; - Do our drawing logic;
- Restore the previous state.
Regarding the Camera
transformations, let’s dive into it.
First things first, since we are going to simulate a 3D object, we won’t be seeing all the minutes:
visible portion
green and yellow = rotation thresholds | blue = fade threshold
This means that we can discard “invisible” information and create a logic that applies rotation only taking into account x-values from Canvas
[start; center[ and [center; end]. Also, a fade effect using a y-value fixed threshold will be applied. The rotation in the y-axis uses values from 90° to 0°, and 0° to -90°:
And this is the result:
left image uses 80°/-80° | right image uses 90°/-90°
Note: actually the values will be from 80° to 0°, and 0° to -80° and an alpha fade in/out will be applied, this is less “real” but it will improve this little visual glitch on the limits that you can see on the right image.
Using the same logic for numbers we get:
Canvas operations
Before we come back again to adjust our transformations, let’s first design our timer to understand why we’ll be revisiting our beloved Camera
.
First we draw a circle and use a Brush
to apply a linear gradient. This will help us simulate a point of light and achieve a volumetric effect:
left: solid | right: linear gradient
Then, since the timer is composed (pun intended) by one moving part and a fixed one, we simulate it by drawing two arcs:
And we finish with the timer track:
Remember when I said we would be revisiting Camera
transformations? Well, the following animation show us why, and the reason is that we must simulate a curvature effect of the strokes and numbers so we can avoid this:
no curvature
Let’s fix it by applying rotations in the 3 axis:
The order of operations matter
Looks better, but we still have to do something about the limits. Let’s add a translation in the x-axis and y-axis to avoid the number “leaving” the sphere:
The order of operations matter
Done, the only thing missing now is to apply a similar logic for the strokes. In this case lets add a z-axis rotation in addition to the one we have in the y-axis:
And the final result will be:
🎉 All right everyone, that’s a wrap! 🎉
You can find the full code of this project here:
GuilhE/KitchenTimer |
Or you can play with it by downloading it from Google Play. Give it a go, I’ve added a few sounds to make it fun 😊
I hope you find this article useful, thanks for reading.