Last modified: 2021.08.24
Before starting, it might be a good idea to take a look at the code for the spinning hexagon in its entirety before taking a more in-depth look at it piece-by-piece. To see a complete listing, click here. The line numbers referenced on this page will correspond to the line numbers in the PDF. There are issues with this code, and a fair amount of them will be talked about here.
[Line] HTML ------------ [1] <canvas id="illusion" width="256" height="256" [2] style = "border: 4px solid #0000FF;"> [3] Your browser does not support canvas. [4] </canvas>
The above code goes into the HTML portion of the webpage. Before starting to work with a canvas, there first needs to be a canvas to work with. This will make a canvas that's 256 pixels width, 256 pixels high, and has a border around it that's 4 pixels wide and dark blue. This canvas will be called "illusion" because that's what I decided to call it. If, for some reason, the browser being used doesn't support canvas, the "Your browser does not support canvas." message will appear.
With the HTML setup out of the way, it's time to move on and get to the JavaScript that will make this hexagon happen. Instead of putting it in a separate JavaScript file and having the HTML page get the code from there, I just integrated the code into the HTML file using the <script> tags. This would be a bad idea for projects that require lots of code or code spread over multiple files, but it's fine here since there isn't a lot of code here.
[Line] JavaScript ------------ [03] var canvas = document.getElementById("illusion"); [04] var context = canvas.getContext("2d"); [05] var rotationStep = 0;
Starting off with some setup. Line [03] will get the canvas from the HTML page and make using it possible with the "canvas" object. Line [04] will get a 2D drawing context for the canvas and call it "context". This is what provides the functions for drawing lines, rectangles, and more to the canvas. The actual name of the library that makes this possible is "CanvasRenderingContext2D". As catchy as that may be, I'll just call it "the Context" from this point on. Once that is done, the next line will create a variable called "rotationStep" and set its value to 0. This will be used to keep track of what step/frame of the animation the program is on.
[Line] JavaScript ------------ [07] context.fillStyle = "#000088"; [08] context.fillRect(0, 112, 32, 32); [09] context.fillRect(224, 112, 32, 32);
Line [07] sets the "fillStyle" value of the Context, which sets the way that the Context will fill a solid object. In this case, it sets the fill style to a solid color, that being the dark blue seen on the side rectangles in the finished product. Speaking of, those rectangles are drawn by the next two lines of code.
The "fillRect" function will draw a filled rectangle based on four given numbers. The first two numbers are the x and y coordinates of the starting point of the rectangle. The third number is how wide to draw the rectangle, while the fourth number is how high to draw it. Looking at line [08], the starting point of the rectangle is at (0, 112), which will be 112 pixels down and along the left edge of the canvas. It will have a width of 32 pixels and a height of 32 pixels. Since both of those numbers are positive, the width will go 32 pixels to the right and the height will go 32 pixels down. This will draw the rectangle seen on the left side of the canvas.
Next, it's time to start writing the function that will draw the lines that will make the hexagon illusion and update the current frame. This will be in a function called "revolveIllusion" because I'm stupid and forgot to double check the difference between rotations and revolutions. This hexagon is *rotating* since its movement is around its own axis and not another object.
[Line] JavaScript ------------ [16] // Clear current lines [17] context.clearRect(32, 0, 192, 255); [18] // Line drawing settings shared by both sets [19] context.strokeStyle = "#0000FF"; [20] context.lineJoin = "round"; [21] context.lineCap = "round";
The first part of the animated line drawing function is just some setup. Line [17] will draw a rectangle that clears out the pixels that were already there, making them transparent. The four numbers given to the "clearRect" function are exactly the same types of numbers that would be given to "fillRect".
"strokeStyle" is similar in function to "fillStyle", but instead of being for filling objects, it's for drawing lines or outlined objects. The value that it's being set to will tell it to make lines solid blue.
The "lineJoin" property determines how to handle drawing the points where line segments are joined together. By setting it to "round", the Context will make those points rounded. "lineCap" is very similar, but it's for the ends of lines. This also gets set to be rounded.
[Line] JavaScript ------------ [22] // Lines that start at the top [23] context.beginPath(); [24] var topY = 48 + (rotationStep * 2) + (rotationStep / 2); [25] context.lineWidth = 5 - parseInt((40 - Math.abs(topY - 128)) / 10);
Despite only being a few lines, there are many important things happening here. Line [23] tells the Context that a new line is being setup. The functions that actually start making use of this don't start until line [26]. Putting that function call there doesn't present any issues, but it also looks a little off. I put it there to show that this was where the code that determines how this line will be drawn starts. At least, I think that's what happened. I wrote this code back in April, and now I'm trying to explain it in August. It's been a minute, so I don't remember exactly what I was thinking.
Line [24] determines the Y position of the top line. In the concept portion of this explanation, I said that JavaScript vars store floating point numbers (basically, numbers with a decimal place in them) and that I didn't know that when I was originally writing this code. This line and the one after it are where that's obvious. Basically, this multiplies the current animation frame by 2.5 and adds it to the starting Y position to figure out where the Y coordinate of the current frame should be.
Line [25] determines how wide the line should be based on the current frame, then sets that as the value used to determine how wide a line should be. That "parseInt" will convert a floating point value to an integer value, but again, that was totally unnecessary. Anyway, the first part of this equation determines how far away the line is from the center by subtracting 128 pixels from the result of the code on line [24], then getting the absolute value of that. Okay, it's confession time. The rest of the numbers there are the result of guesswork and trial-and-error to just figure out what looked right. I probably could have figured out something better if I had spent a little more time on it, and I'm not sure why I didn't. The idea was to scale that absolute value to something that could reasonably be used as an offset for a starting width, then... well, use that to offset the starting width.
[Line] JavaScript ------------ [26] context.moveTo(32, 128); [27] context.lineTo(72, topY); [28] context.lineTo(184, topY); [29] context.lineTo(224, 128); [30] context.stroke();
These lines define the points where the line will be drawn, then they draw the line. A good way of thinking about how these functions work is that the "moveTo"s and the "lineTo"s tell the Context how to draw the line, then the "stroke" function makes it actually draw the line. The "moveTo" function is used here to start plotting out the line segments that will make up the top line. The given X and Y coordinates will start the line inside of the rectangle on the left. Line [27] will tell the Context to move from there to the specified coordinates, creating the leftmost line segment of the top line in the process. Line [28] will complete the middle segment and line [29] will complete the rightmost line segment, thus completing the line. Next, the bottom line will need to be drawn. There's nothing new to explain there, so I'll skip it and talk about the code that determines the next animation frame.
As discussed previously, this animation will be 64 steps (or frames) long. The frames will start at 0 and go until 63, since computers like to start counting from zero and I wrote my code around this being the case. When it tries to go past frame 63, it should loop back to frame 0. Otherwise, it should go to the next frame in the animation. Before showing how I have it written, I'll show code that demonstrates how it should be written in almost all cases. There's a few slight variations to how this can be done, but I'll go with the one that most closely resembles how I have it working.
JavaScript ------------ rotationStep++; // Shorthand for "rotationStep = rotationStep + 1;". if (rotationStep > 63) { rotationStep = 0; }
The first line will increase the value of rotationStep by one without making any kind of check to make sure it's in the correct range. After that, the "if" statement checks to see if rotationStep is greater than 63, and if it is, it will run the code that's in the curly braces. That code sets rotationStep to 0. This will achieve the desired result.
So if that's the way it should be done, then what did I do?
[Line] JavaScript ------------ [41] rotationStep = (rotationStep + 1) & 63;
There are a few things to understand here. Firstly, a little bit about Boolean logic and the "and" operator. In Boolean logic, "and" will evaluate two or more Boolean states (basically, values that are either true or false) and results in a true value if all of the evaluated states are true. Otherwise, it will return false. JavaScript, in addition to many other programming languages, have two different types of "and". The logical "and" is the more common of the two. This is used to evaluate Boolean values as described above. A logical "and" in JavaScript is "&&".
The code I have at line [41] is using the other kind, which is the bitwise "and". Numbers in a computer are almost always stored as binary values. Binary values are comprised entirely of zeros and ones. Bitwise "and" will take two numbers, treat the 0's as false and the 1's as true, then evaluate each corresponding digit place in the two numbers with an "and", with the result becoming the value in the resulting number. Here's an example:
Decimal | Binary 5 | 0101 & 3 | & 0011 ----------------- 1 | 0001
The important part to look at is the binary representations of the numbers. The only 1 in the result is in the same digit place where both of the input numbers had a 1 (which happens to be in the 1's place!). For reference, I'll list some numbers near 0 and 63 to better illustrate how the bitwise "and" is getting used.
Decimal | Binary 0 | 0000 0000 1 | 0000 0001 2 | 0000 0010 3 | 0000 0011 ... 61 | 0011 1101 62 | 0011 1110 63 | 0011 1111 64 | 0100 0000
While not every number between 0 and 63 is listed here, hopefully this is enough to show what's happening. Looking back at line [41], the two numbers in the bitwise "and" operation were the next animation frame (rotationStep + 1) and 63. Looking at the table, 63 has all 1's for the first six digit places. All of the positive integers before 63 are comprised of some combination that fits in six binary digits. 64 has a 1 in the 64's place and a 0 everywhere else.
This means that (64 & 63 = 0) and any positive integer less than or equal to 63 will stay the same when that operation is applied (X & 63 = X; where 0 <= X <= 63). This will give the same result as that previous code written with the "if" statement, except it will likely run faster because it doesn't need to evaluate a condition and branch accordingly. This only works because the numbers chosen worked out in a way that made this possible. This is NOT a possible solution for most situations like these.
The target demographic I have in mind while writing this is beginner programmers or people who are interested in programming but don't really plan on making a habit or career of it. In either case, neither of these groups of people really need to worry about any of the bitwise operations, including bitwise "and". This is mostly good to know for writing very efficient code or for something like assembly language programming, where operations like this are far more common (especially in older processors). From what I can tell, most programming positions will be for writing higher level code with more concern for getting code written fast as opposed to getting it written well, making this kind of lower-level thinking uncommon in most code written today.
At this point, all of the code that's going to make this animation work has been written. Now all that's left is to write a little bit of code that will actually get the hexagon to spin when the page loads.
[Line] JavaScript ------------ [11] function startRevolvingIllusion() [12] { setInterval(revolveIllusion, 30); } ------ [75] window.addEventListener("load", startRevolvingIllusion, false);
A lot of the heavy lifting of getting the animation to play in a web browser is being done by functions that have already been written that are made readily available by JavaScript. Line [12] uses the "setInterval" function to continually run the function that draws the animated lines after a given number of milliseconds. I just kept changing the number of milliseconds between each of the function calls until it looked right, and ended up on 30. This means there will be a 30ms delay between each of the 64 frames, meaning that one complete loop through the animation (which will appear to be half a rotation) will take 1.92 seconds (3.84 seconds for a complete rotation).
Line [75] will create an event listener that will run the startRevolvingIllusion function (I'm still stupid because that should be rotating) when the window loads. Basically, when the page is done loading, it will run the function that actually updates the animation, which will get the "hexagon" to "spin".
Return to main page
Previous page: Concepts Behind the Spinning Hexagon
Materials Referenced:
: EventTarget.addEventListener(). MDN Web Docs, Mozilla and individual contributors. Last modified 2021/08/22.
: CanvasRenderingContext2D. MDN Web Docs, Mozilla and individual contributors. Last modified 2021/08/23.
: Window setInterval() Method. W3Schools, Refsnes Data. Accessed 2021/08/23.
Link to EventTarget.addEventListener() documentation
Link to CanvasRenderingContext2D documentation
Link to Window setInterval() documentation