The Lord of the Rings Quiz

March 4, 2024 | Project

screenshot of lotr quiz landing page

Technology

Why I built this?

I built this as a fun way to to learn React, attempt some creativity, and play around with 3D graphics.

I had the idea when I playing an online quiz that tests how many of the top 200 Harry Potter characters you can name within 18 minutes. I thought it would be fun to make something similar for the Lord of the Rings, but quickly realized I can’t even spell half the names in Lord of the Rings. So I decided to make a multi-choice quiz.

Features

The Precious

The crown jewel of this project is the interactive ring on the landing page. As the ring slowly spins around you can see the fires of Mount Doom reflected on the the metallic surface of the ring. Then, when a user hovers or clicks on the ring, the Elvish script glows brighter and dims again when they stop interacting with it.

I also tried adding some particles/sparkles around the ring, but I ultimately took them out because of poor performance on mobile devices. Additionally, because the page is laid out in two columns with the canvas (where the ring is rendered) on the left and text on the right, the particles created an invisible line when they got too close to the column gap.

I might add them back in the future, but I would need to do some digging on the optimizing particle performance and extend the canvas to fill the view port and then stack and position the text on top of it.

[@portabletext/react] Unknown block type "embeddedVideo", specify a component for it in the `components.types` prop

The reflections of Mount Doom

I created the reflection on the ring using an environment map. I like to think of environment maps as panoramic spheres, similar to the ones your phone creates by stitching together a series of photos into a seamless 360-degree image.

While there are some highly detailed environment maps out there, none of them really reflected the ambiance of Mount Doom. So I actually generated photos using Stable Diffusion and just used the images without combining them since I didn’t need anything really detailed or even physically accurate.

Adding the glow

To make the text glow brighter on touch, I needed a model with an emissive texture which thankfully the one I downloaded did. The emissive texture is simply an image that wraps onto the model and tells the 3D engine which parts of the model should emit light with darker or black areas of the image emitting little to no light.

I initially thought I would have to use a bloom (it adds a sort of blur around the model to give it a glowing effect, check out this example).

However, I found that I could get a satisfactory result without it. I simply ended up bumping up the emissive intensity on hover using a React spring. This increased saturation of the text to give it a subtle glowing effect. Adding bloom would have made the glow extend off the ring with a blur, but I didn’t think adding that overhead was worth it in terms of performance.

Scenery Changes

As the quiz progresses the scenery changes as though making the journey to Mordor. This was achieved by simply applying different CSS classes with slow transitions based on the current question number .

An image comparing the background scenery changes at the beginning and the end of quiz.
Begining of quiz vs the end

Quiz Results

A different quote from the book is displayed depending on how well you did, as well as the the result of each question. I used the canvas-confetti package to create the confetti canons for the perfect score.

[@portabletext/react] Unknown block type "embeddedVideo", specify a component for it in the `components.types` prop

Challenges.

Creating a timer component.

Since this was my first attempt at a React app, even creating something as straightforward as a timer felt like a surprising challenge within the React ecosystem. I wanted to create a timer component because what’s a quiz without some pressure, right? The end goal was to timer component that could be used like this.

Timer.jsx

1<Timer
2  initialTime={30}
3  onEnd={() => {
4    next();
5  }}
6  play={!paused}
7/>
  • Initial time: the amount of time the clock starts with and resets to.
  • on end: a callback function to execute when the timer runs out (in this case advance to the next question)
  • play: a boolean to control whether or not the timer is running. I added this because there is an intro animation, and I don’t want the timer to start until the animation is complete.

Attempt #1.

This was a first attempt. I stored the time remaining on the clock and the timer in a React state variable. I wanted the timer (or more accurately, the interval’s id) in state because I wanted to be able to clear or pause it.

Timer.jsx

1export function Timer({ onEnd, initialTime, start }) {
2  const [time, setTime] = useState(initialTime);
3  const [timer, setTimer] = useState();
4
5  setTimer(
6    setInterval(() => {
7      if (time > 0) {
8        setTime((t) => t - 1);
9      }
10    }, 1000)
11  );
12  
13  return (
14    <span className="timer">
15      0:{time.toString().length === 2 ? time : `0${time}`}
16    </span>
17  );
18}

And before I knew it, I had created an infinite loop!

Image showing React infinite loop error

The problem is the setTimer(). When setTimer() is called the state is being updated. And in React, state changes cause a component to re-render, thus calling setTimer() again and again. So if that’s the case, then why don’t I just get rid of the timer state and just call setInterval() directly?

Attempt #2

Timer.jsx

1import { useState } from "react";
2
3export function Timer({ onEnd, initialTime, play }) {
4  const [time, setTime] = useState(initialTime);
5
6  setInterval(() => {
7    if (time > 0 && play) {
8      setTime((t) => t - 1);
9    }
10
11    if (time === 0) {
12      onEnd();
13    }
14  }, 1000);
15
16  return <p className="timer">{time}</p>;
17}

Voila, there’s no error, but…. it’s not what I expected (nor wanted).