ODINODIN

Wave of nostalgia

2020-05-20

frontend

I remember it as if it was yesterday. The day our Commodore was switched out with an Amiga 500. That was the first time I saw a sine wave flow across the screen, as if by magic. How do you actually make that happen?

First, some math

To create a sine wave, we need to go back to middle school. We need to know a bit about trigonometric functions. These are functions that give us the relationship between an angle in a right-angled triangle and the lengths of the sides of the triangle.

Trigonometric functions

When you plot the sine function, you see that it is a wave between -1 and 1. It repeats itself into infinity. In JavaScript the sine function is available through Math.sin().

sine_wave

Place letters on a sine wave

To position a letter on the curve, we calculate a y-position for each letter. To adjust the height of the curve, also known as amplitude, we can see from the graph below that the formula is:

y = amplitude * sin(x)

trigonometriske functions

In code this could be like this:

(defn y-pos [{:keys [angle amplitude y-offset]}]
  (-> (js/Math.sin angle)
    (* amplitude)
    (+ y-offset)))

By adding to the y-offset we move the entire curve up or down. We do this in order to center the curve in the middle of the container we draw it in.

Animation

To create the illusion of motion we need two things:

  1. Initial state
  2. A loop that updates and draw the state

Here is the state we're using:

(def initial-state {:letters (seq "KODEMAKER KODEMAKER KODEMAKER")
                    :tick 0
                    :amplitude 30
                    :angle-speed 0.3
                    :x-speed -10
                    :x-spacing 26
                    :speed (/ 1 8)})

:letters Contains which letters to draw.

:tick This represents where we are in time and is used to place the letters. For every round in our loop this is updated with :speed. That is what makes the elements move.

:angle-speed determine how quickly the angle of the curve changes. That is a simplified way of controlling the period of the wave.

:x-speed affects how fast the letters move along the x-axis. It is independent of :angle-speed. If you set it to 0, the letters will only oscillate up and down in the same place. The reason it is negative is that we want the letters to move from left to right. If it were positive, they would move in the opposite direction.

:x-spacing determines the distance between each letter.

;; All state lives here
(defonce state (atom initial-state))

(defn render-loop []
  (render state)
  (swap! state update :tick #(+ % (:speed @state)))
  (.requestAnimationFrame js/window render-loop))

Letter rotation

For the letters to follow the sine curve, we need to rotate them relative to the sine curve. We can take advantage of the fact that cosine is identical to sine, just phase-shifted by 90 degrees or π/2 radians. A radian is the unit for measuring angles.

trigonometric functions

By rotating each letter with cosine of the same angle, it will follow the sine wave perfectly.

(-> (js/Math.cos (+ tick (* idx angle-speed)))
  (str "rad"))

idx is the index of the letter in the :letters-list.

JavaScript’s trigonometric functions take radians, not degrees. 360 degrees is equivalent to 2π radians. To convert from degrees to radians, the formula is:

Radians = degrees * π / 180

CSS’s rotate function is more service-oriented than JavaScript, as it accepts degrees, radians, or gradians. So, of course, we’ll just use radians everywhere and save ourselves the conversion. After all, degrees are a 4,000-year-old legacy concept from Sumeria.

I hadn’t heard of gradians before yesterday, but it’s an angle measurement where the circle is divided into 400 parts. Not a particularly useful thing to know, but now you know it too!

Colors

To change the color of the text, we first create a list of colors. cycle repeats a list indefinitely, and then we retrieve the correct color based on nth, which takes the color list and an index.

(def colors ["red" "blue" "green"])

(nth (cycle colors) 0) ;; "red"
(nth (cycle colors) 1) ;; "blue"
(nth (cycle colors) 2) ;; "green"
(nth (cycle colors) 3) ;; "red"

Clojure is beautiful.

Canvas vs HTML

I have chosen to use HTML elements instead of drawing on a canvas because this allows CSS to be used for styling the letters. In terms of performance, canvas is a much better choice for this type of drawing, but for the few elements we need to move, the DOM works just fine.

Here is an example of how the blur effect is made with CSS

.effect-blurred {
    color: transparent;
    text-shadow: 0 0 30px #000;
}

Summary

It doesn’t take more than that to create a sine wave. This can be taken much further by, for example, combining multiple sine waves into composite waves that generate an infinity of exciting patterns.

Details for the curious

The example in this post is made with ClojureScript. You may read the source code if you want