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.
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().
¶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)
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:
- Initiell tilstand
- 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.
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.