Jeg husker det som om det var i går. Dagen da Commodore ble byttet ut med Amiga 500. Det var første gang jeg så en sinusbølge flyte over skjermen. Hvordan lager man egentlig det?

Først, litt matte

For å lage en sinuskurve så må vi en tur tilbake til ungdomskolen. Vi trenger å vite litt om trigonometriske funksjoner. Det er funksjoner som gir oss forholdet mellom en vinkel i rettvinklet trekant og lengden på sidene i trekanten.

trigonometriske funksjoner

Hvis du plotter sinusfunksjonen så ser du at den bølger seg mellom -1 og 1. Den gjentar seg selv i det uendelige. I JavaScript så har vi sinusfunksjonen via Math.sin().

sinuskurve

Plassere bokstaver på en sinuskurve

For å plassere en bokstav på kurven så beregner vi en y-posisjon per bokstav. For å justere høyden på kurven, også kjent som amplituden, så ser vi av grafen under at formelen er:

y = amplitude * sin(x)

trigonometriske funksjoner

I koden blir det slik:

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

Ved å plusse på y-offset så flytter vi hele kurven opp eller ned. Det gjør vi for å kunne sentrere kurven i midten av boksen den tegnes i.

Animasjon

For å sette det hele i bevegelse så trenger vi to ting:

  1. Initiell tilstand
  2. En loop som oppdaterer og tegner tilstanden

Tilstanden vi bruker er:

(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 inneholder bokstavene som skal tegnes.

:tick Denne representerer hvor vi er i tid og blir brukt for å plassere bokstavene. For hver runde i loopen vår så oppdateres denne med :speed. Det er dette som gjør at elementene flytter på seg.

:angle-speed bestemmer hvor raskt vinkelen på kurven endrer seg. Det er en forenklet måte å bestemme perioden i bølgen på.

:x-speed påvirker hvor raskt bokstavene flyttes langs x-aksen. Den er uavhengig av :angle-speed. Om du setter den til 0 så vil bokstavene bare oscillere opp og ned på samme sted. Grunnen til at den er negativ er at vi ønsker at bokstavene skal gå fra venstre mot høyre. Om den er positiv så går de den andre veien.

:x-spacing angir hvor stor avstand det er mellom hver bokstav.

;; All tilstand bor her
(defonce state (atom initial-state))

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

Rotere bokstavene

For at bokstavene skal følge sinuskurven så må vi rotere de relativt til sinuskurven. Da kan vi utnytte at cosinus er helt lik som sinus, bare faseforskjøvet med 90 grader eller π/2 radianer. En radian er SI-systemet sin enhet for å måle vinkler.

trigonometriske funksjoner

Ved å rotere hver bokstav med cosinus til samme vinkel så vil den følge sinusbølgen.

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

idx i dette tilfellet er indeksen til bokstaven i :letters-listen.

JavaScript sine trigonometriske funksjoner tar radianer, ikke grader. 360 grader er det samme som 2π radianer. For å gjøre om fra grader til radianer så blir formelen derfor:

Radianer = grader * π / 180

CSS sin rotate-funksjon er mer service-innstilt enn JavaScript, siden den tar grader, radianer eller gradianer. Så da bruker vi selvsagt bare radianer over hele fjøla og sparer oss konvertering. Grader er tross alt et 4000 år gammelt legacy-konsept fra Sumeria. Gradianer hadde jeg ikke hørt om før i går, men det er et vinkelmål hvor sirkelen er delt inn i 400 deler. En ganske unyttig ting å vite, men nå vet du det også.

Fargelegging

For å endre farge på teksten så lager vi oss først en liste med farger. cycle repeterer en liste i det uendelige, og så henter vi ut riktig farge basert på nth som tar fargelisten og en indeks.

(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 er nydelig.

Canvas vs HTML

Jeg har valgt å bruke HTML-elementer istedenfor å tegne til et canvas fordi da kan man bruke CSS til å style bokstavene. Når det gjelder ytelse så er canvas et mye bedre valg til denne type tegning, men for de få elementene vi skal flytte på så funker DOM’en greit.

Her er et eksempel på hvordan blur-effekten er laget med CSS.

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

Oppsummert

Mer skal det ikke til for å lage en sinuskurve. Dette kan man ta mye lenger ved å f.eks kombinere flere sinusbølger til sammensatte bølger som genererer en uendelighet av spennende mønster.

For de ekstra interesserte

Eksemplet i denne posten er laget med ClojureScript. Du kan lese kildekoden om du vil.