Preface (before the Trigonometry)
I love solving CSS problems ♥️ .
It’s like a guilty pleasure of mine, to work on them late at night 😏 after work like a hobby, outside of all the other backend or DevOps work that I do.
These problems appeal to me because there are many different ways to solve each of these problems. Not to mention that there is a wide range of designs for websites serving different features, leading to a wide array of solutions to be discovered out there.
But vanilla CSS is kind of meh…
CSS is restricted to rules provided by the style sheet language. This also meant that we could not apply programming principles when designing styling solutions, which rely on constructs like functions and variables.
One such pattern is code reusability, a core programming concept in identifying similar patterns in code for repeated use. This principle helps in saving the costs of reimplementation. It also increases the cohesiveness of code while reducing the coupling effect. These benefits were simply not achievable with a style sheet language.
What’s the solution to that?
It’s been hinted above — use the SCSS syntax of Sass (Syntactically Awesome Style Sheets)! 🙌 SCSS allows us to elevate the use of CSS , allowing us to practise principles like code reuse as we discussed earlier.
Another advantage is that it implements style inheritance better than vanilla CSS, through centralising the declaration of class names with `@extend`. Through declaring class names in lesser parts (preferably one) of the code, we achieve an overall cleaner code base for maintainability.
The best part, is that SCSS is a preprocessor language. It compiles to regular CSS at build time, and does not incur any dependency issues with our production environments.
What the CSS? Now what do I do? How is this related to Trigonometry?
Let’s talk about an interesting styling + animation problem that I “crammed” in one of those late nights.
The premise was as follows — can we add a loading animation to one of our applications?
Approach #1: Load a gif
Madness! Preposterous! You should not do this unless you have no choice. Every page needs this loader. Imagine the costs for hosting multiple GIFs like this, and driving users away with higher data consumed per page.
Also, GIFs doesn’t scale elegantly to larger sizes, ew… 😞
Approach #2: Use Lottiefiles and load with JSON
A minified bundle for the library is slightly under 60kB, while the JSON file is under 2kB. The combined cost is definitely smaller than most GIFs out there, and other animations in the page can use the same library without downloading it again, making it an attractive option as compared to GIFs.
Approach #3: Try to draw manually with CSS (and fail ☹ )
In attempting to recreate the animation in CSS, we should first break down the problem to simpler problems to solve.
First, we can observe that there are four hexagons within the animation, albeit with different sizes. Once we’ve figured out a way to draw a hexagon, it should be a matter of adjusting the size attributes three more times for the remaining hexagons.
Second, the animation consists of rotation of the hexagons in two different directions. Since we broke down the problem of drawing the hexagons individually above, it stands that we can apply the same logic in adding the animation attributes to each hexagon for the desired effect. The animation can be created by applying a change in rotation value with the transform property, occuring with a looping @keyframes configuration.
Third, we have to identify the methodology to draw a hexagon. There are different approaches, but my preferred approach was to use a single div to draw the hexagon. This is done by drawing only the top and bottom border of the div, and then rotating the
:before pseudo element by 120deg, and the
:after pseudo element by 240deg, whilst running the same border logic on both psuedo elements.
But we end up with the situation above — how long should each side of a hexagon be? Each side should be a function of the distance from the center of the hexagon (including border thickness). We could solve it, using the brute force approach of guessing, but it’s not a scalable method for drawing multiple hexagons of different sizes here (and also for whenever we need to amend the sizes).
Approach #4: Revise on math, and then implement trigonometry 🌚
To find the length of the outer side of a hexagon, we can refer to the following graphic:
From the above, what we want to solve is the value of O, since 2 * O is our required solution for drawing the side of a hexagon. The only known inputs here are A, which is half the cross section width (or size of the hexagon), and a constant unit for adjacent angle at 30º.
We can refer to the following trigonometry formulas for formulating a function to compute O .
tan(30º) = O / A , O = tan(30º) * A
cos(30º) = A / H
sin(30º) = O / H , O = sin(30º) * H
From the above, implementing a function for
tan makes the most sense, since the inputs of A and the adjacent angle are both available. The returned value of O from the
tan function can be multiplied by 2 and returned as the final solution as the length of a side for the hexagon.
How do we implement
tan? We can take reference from the following formulas:
tan(x) = sin(x) / cos(x)
sin(x) = cos(x - 90º)
So, as it turns out, we should still implement
sin first, and use it to implement
cos, followed by the
The hardest step
sin would mean we need to understand how to compute the value. Fortunately, the logic to compute the value can be explained with the following Taylor Series expansion formula:
We can also observe that, to complete the implementation, we have to implement a
factorial function. The implementation was simple, using a for-loop to compute the value. An optimization was also added in to prevent re-computation from previous
Once we have implemented our
factorial function, we look to use it to implement the
sin function. Using a for loop, we can emulate the formula’s process for each iteration of new inputs.
While the higher bound of the summation element of the
sin formula tends to infinity, we can settle on realistic grounds as long as the approximation was sufficiently accurate. For us, we felt that the 10ᵗʰ iteration was more than sufficient since it would cause a deviation of less than 10⁻¹⁹ % as an approximated figure.
Following the implementation of the
sin function, we move on to
tan, which are quite easy to implement with code reuse.
Now that we have some implementation in place, how do we verify that our trigonometric functions are working properly?
Unit tests for SCSS?
As a software engineer, the answer is clear to me from my other works — writing unit tests, and maintaining them under regression testing. However, the idea seems whacky for CSS, since UI and UX are something that should be directly experienced and visualised.
That said, in solving this trigonometry problem, we’ve created logic that is decoupled from UI and UX. That could potentially result in bugs or the logic breaking in the future whenever it is updated. Unit testing seems to be the way to go in resolving this problem.
For SCSS, I managed to find a library called true, by oddbird. It is able to be integrated with test runners such as mocha and jest, making it a good candidate for applications already using these tools.
When setting up this library, other than installing the node module, we will also need to create a shim. The shim should contain two things — the source of all your test files for SCSS, and relaying the test runner’s utility (like jest) to this library. An example for this shim file is linked here.
A simple test case for
factorial, can be seen as follows:
Once all the test cases have been implemented, we can run the test runner using its cli utility — we should get the following output:
Anyway, for this implementation, I am using jest as the test runner — my project documenting the implementation is as shown here.
With confidence that our trigonometric functions work, we can implement it for our original solution. This solves the problem of incorrect length for the hexagon, without using the brute force approach.
What did I learn?
This taught me a lesson, in that beyond a styling language, SCSS opens the floodgates to the many different things that can be achieved. This is done by elevating CSS to something closer to a programming language.
By treating SCSS with the same mindset as building logic in applications, we can harness core software engineering principles. This allows us to automate computation for our solution using trigonometry, with the core
sin functions key to implementing
tan functions through code reuse.
We also harnessed the ability to check our logic for functions and mixins in SCSS. This is in line with proper quality control processes in engineering — building unit tests to ensure functionality, new or otherwise, conforms to the intended specifications.