Introduction

In the previous post, I wrote about how I finally created, after one failed attempt, a good audio engine for my homepage. While crafting the audio solutions, I started also thinking about adding one last aspect to the homepage: some kind of visual component to accompany the audio solution.

Firstly, I thought of also using user input to change the background, for example, hue and saturation. Or perhaps pre-render a simple 3D animation and use it as the background of the webpage. But, to accompany the audio solution, I thought an interactive 3D background would be more fitting.

In this post, I will focus on how I managed to create an interactive background for my homepage. This is going to be a shorter post, I promise.

Interactive 3D background

The Idea

I did not want to do anything fancy that would cost me a lot of time. So, the animation I planned on implementing was a tunnel of rings of cubes passing through the viewport. The cubes would be rotating within themselves and rotating in their plane of depth. When the user clicks the animation, the cubes would change colors and start rotating in the opposite direction.

Choosing a framework

So here I am, once again, evaluating my options to generate a live 3D animation. WebGL was the first thing that came to my mind, I’d already used it in my University Computer Vision course. I also remembered how bare-metal it was, and that it required writing sharers in GLSL even for simple tasks. Given the time already spent on this “super”-project (website, audio,…), I strayed from low-level graphical frameworks. SDL was another option but would require messing around with manual compilation to WebAssembly (through Emscripten). I was getting discouraged and close to giving up when I figured I could simply use a game engine to pull the heavyweight for me. Not too long ago, I participated in the 2022 Global Game Jam with some friends, and we used the Godot engine to create our game. Godot is a fairly new awesome community-driven open-source game engine in rapid development. Godot has already an HTML5 exporting option (using WebAssembly), and I was interested in getting more experience with Godot, so I choose it as the live renderer.

Implementation in Godot

The basic scene setup for the planned animation is a camera and a spotlight aimed forward. To implement this animation, I first created a script that generates a ring of cubes (Ring script). This script creates 10 white cubes at scene initiation, equally spaced in a circumference with 7 units of radius. For each animation frame, these cubes are rotated in their axis at a fixed value (different for each axis, and constrained by the fame delta time). Then, I created a script that instantiates multiple rings to generate the tunnel (Tunnel script). This script creates 10 rings, with 6 units of spacing. While the animation is running, this script translates and rotates the rings. The further the ring is from the camera, the higher the rotation speed. When a ring reaches the back of the camera, it resets to the back of the farthest ring. This gives the illusion of an infinite tunnel. Alone, this would generate the wanted animation, however, there would be no interaction. To implement interaction, I added a mouse click event listener that inverts the angular velocity of the rotation. I also created another script called Supervisor. The first function of this script is to create a fade-in effect on the animation, by slowly adding power to the spotlight (which starts with no power). Then, it listens for click events and when it catches one, changes the color of the spotlight, which then affects the color of the cubes. After implementing this, we get:

The final step I took was to reduce the render resolution from 1200x600 to 200x200. I did this because I like the low-res aesthetics (looks like an old game) and to improve the performance of the renderer. This is how it looks:

Final steps

Packing up the solution

As mentioned before, Godot allows you to export the project to HTML5 through WebAssembly. After exporting the “game”, we get multiple files:

  1. 3d.audio.worklet.js: Useless to us, since the project has no audio
  2. 3d.html: The game user interface (canvas,…);
  3. 3d.js: Setup logic and glue logic with the wasm part;
  4. 3d.pck: Game file resources;
  5. 3d.png: Game icon, also useless to us;
  6. 3d.wasm: The game itself.

I didn’t want to use an HTML iframe to embed this animation in the background of my homepage, because I suspected it would bring problems with the interaction and sizing of the viewport. So, I needed to figure out how the game engine generated the output to modify it to my needs.

This part was harder than understanding what Faust did, this engine is much more complex. The first thing I did was analyze the 3d.html file. After a quick look, I suspected that most of the code inside it was useless to me (seems to be minor error handling and progress bar creation). The bare minimum HTML body needed to run the game is the canvas, including the 3d.js, GODOT_CONFIG, the engine class instantiation and call to startGame:

<canvas id='canvas'></canvas>
<script type='text/javascript' src='3d.js'></script>
<script type='text/javascript'>
    const GODOT_CONFIG = {"args":[],"canvasResizePolicy":2,"executable":"3d","experimentalVK":false,"fileSizes":{"3d.pck":7984,"3d.wasm":17860295},"focusCanvas":true,"gdnativeLibs":[]};
    var engine = new Engine(GODOT_CONFIG);
    engine.startGame();
</script>

Since I wanted to inject the minimum amount of lines possible (to keep the Hugo partial template clean), I created a new file called top.js to serve as the top-level script. This script deals with canvas instantiation and script loading. The 3d.html file stopped serving a purpose, so I deleted it. This leaves us with 4 files: top.js, 3d.js, 3d.pck and 3d.wasm. Let’s integrate with Hugo and see what we get.

Integration with Hugo

To integrate the animation renderer with the homepage, I used the same approach as the way I integrated the Faust audio engine. Within the partial layout of the homepage, I just imported the top-level renderer script (top.js).

This mostly worked, but there was a collection of problems to be fixed. Firstly, I noticed that the Godot engine would override the title and favicon of the homepage with the ones from the game project. Of course, I didn’t want that, so I started digging in the >14k lines 😱 of the 3d.js code (after beautifying the code). To fix the page title, I looked for any reference to document.title, and changed that variable’s name. To fix the favicon, I overwritten the _godot_js_display_window_icon_set function with an empty function.

I also found that the game engine was stealing the keyboard and sometimes the mouse focus (especially on mobile), and not alloying other sources to handle the events. This was critical in a mobile browser, where the user was not able to click any of the buttons/hyperlinks of the homepage. I fixed this by removing all the calls to the function preventDefault of the events. On the flip side, I expanded the event handling of the engine to catch clicks outside the canvas by changing the GodotEventListeners target from canvas to window.

Most of the issues were fixed and the user experience was pretty much as good as it can get, however, something was still bugging me: canvas scaling/expansion. While playing within the Godot engine, I noticed that when expanding the game window horizontally, the game’s viewport (or field-of-view) would expand to fit the new window. But, when expanding vertically, the game would not expand the render in that dimension. In the following image, I changed the background color of the viewport to white to reflect better the problem:

This issue is also present in the HMTL5 export. I tried to manually change the size of the canvas by altering the HTML, but the game script was somehow preventing me, by defaulting to its resolution. Essentially I wanted the game to keep its 1:1 aspect ratio while expanding to the highest dimension. After a bit of digging within 3d.js, I was able to fix the code to set the canva’s width to my desired value (essentially, width = width > height ? width : height;). Now, the canvas expands as I want, but it is not kept centered on the screen. To fix this, I created (inside the top.js file) a MutationObserver that, when the screen dimensions change, triggers a function that fixes the horizontal offset of the canvas (through the CSS style properties).

After that, I added some custom glue logic (in the homepage partial layout file) to force the theme to be dark and to remove the theme toggle button. I also needed to set the header and footer CSS position to relative for them to stay on top of the canvas and set the scrollbar width to zero.

That was pretty much it! All these preparation steps were neatly packed into a bash script, to aid in debugging and standardizing the process.

Final remarks

The journey is complete. The homepage is as interesting as I want it to be. It took a lot of time, but it is finally over. Was it worth it? Maybe. Hopefully, the page is an attention grabber and will stand out from the sea of similar-looking homepages.

I don’t have my next post planned, so only time will tell what I will be writing about next. Until then, thanks for reading!

TL;DR

To accompany the audio solution, I wanted to create a visual one. Since I did not want to spend too much time on this, I ended up creating an interactive animation using a game engine called Godot. The animation is essentially a tunnel of rings of cubes going at the camera. After exporting it to HTMl5 (WebAssembly), I fixed the problems I found, as I integrated the program with the homepage.