Canvas og tiltrekningskraft

2015-03-14

Spennende effekt med enkel model

Jeg leste nettop en fantastisk interessant bok som handler om hvordan man kan lage digitale verdener basert på matematiske prinsipper, Nature of code. Den inspirerte meg til å visualisere et enkelt system med ClojureScript og Canvas.

Her er resultatet; trykk rundt omkring for å flytte den store sirkelen.

Bevegelsene er basert på en enkel modell hvor de små sirklene blir tiltrukket av den store sirkelen, la oss kalle den tiltrekkeren heretter.

Det er flere måter å programmere denne oppførselen på. Et verktøy som passer perfekt til jobben er lineær algebra, som handler om geometriske vektorer og matriseregning.

Kort fortalt så er en vektor et element som har både lengde og retning. En vektor tegnes som regel som en pil.

Hvordan er det implementert?

Først og fremst så må hvert element vi skal tegne ha en posisjon: et x- og et y-koordinat (vi holder oss i 2 dimensjoner, men prinsippene er de samme for 3 dimensjoner).

x,y = [20, 30] x,y = [50, 10]

En måte å representere posisjon på er som en vektor. Det kan være litt forvirrende å tenke på ettersom en posisjon ikke har en retning, men dette vil gjøre det lettere for oss når vi skal beregne nye posisjoner senere. I koden ser det slik ut:

;; x = 2 og y = 3
[2 3]

Dette er altså en vektor i dobbel forstand; både en geometrisk vektor og en Clojure vektor-datastruktur.

Både de små sirklene og tiltrekkeren har en posisjon. Hver av de små sirklene har en hastighet i en gitt retning, representert som en vektor. I tillegg har de en akselerasjon i retning av tiltrekkeren. Akselerasjonen er også en vektor.

v v a a

Hvis vi skriver ut tilstanden til modellen på et vilkårlig tidspunkt i simulasjonen, så ser det slikt ut:

@model
=>
{:attractor {:pos [225 225]},
 :balls ({:pos [195.95010685999443 169.67465656341605],
          :velocity [0.29351709042530344 1.3612519797676996],
          :acceleration [0.009194083400046356 0.01776144223966601]}
         {:pos [212.66042238266442 213.40288831234903],
          :velocity [0.19838276960866433 1.9185125529771718],
          :acceleration [0.013601877039455555 0.014662501185116123]}

         ;; Osv....

Simulasjonen foregår på følgende måte:

  1. Endre akselerasjonen til hver sirkel i retning av tiltrekkeren
  2. Endre farten til hver sirkel basert på den nye akselerasjonen
  3. Endre posisjonen til hver sirkel basert på farten til sirkelen

I kildekoden ser det slik ut:

(defn update-model [model]
  "Updates the model"
  (-> model
      attract-circles
      accelerate-circles
      move-circles))

Essensen i dette er koden for å beregne akselerasjonen for hver sirkel.

;; Konstant kraft som tiltrekkeren akselererer sirklene mot seg med
(def attractor-acceleration 0.02)

(defn calculate-attraction-force [ball attractor]
  (let [force-direction (v/vsub (:pos attractor) (:pos ball))       ;; a)
        normalized (v/vnormalize force-direction)                   ;; b)
        with-strength (v/vmult normalized attractor-acceleration)]  ;; c)
    with-strength))

Dette er vel og bra, men der er et problem. Hvis man ikke passer på farten til hver enkelt sirkel, så kan den vokse seg veldig stor. I simulasjonen så er farten begrenset slik at sirklene raskere skal kunne tilpasse posisjonen sin når tiltrekkeren flytter seg.

Sist så er det viktig å merke seg at vektormatematikken ikke er optimalisert, det er bare en naiv implementasjon av elementær lineær algebra. Hvis du er ute etter å simulere et vesentlig større antall entiteter så er det verdt å se på bedre biblioteker.

Kildekoden til simulasjonen er tilgjengelig på Github.

CVTwitterLinkedinstackoverflowKodemakerFlickr500pxRSS