02 September, 2013

Moving things with Clojurescript and your phone accelerometer


Until recently I had quite naively assumed that smartphone browsers were not be able to access the phone's accelerometer and gyroscope. Well, turns out that you can. You can even get the phone's GPS coordinates(with the user's permission, of course).

In this post, we'll be using a phone accelerometer to control a blob inside a HTML canvas.

motion_blob.png

Initial setup

Before doing anything else, start a Lein project:

lein new motion-cljs

And setup the project.clj as follows:

(defproject motion-cljs "0.1.0-SNAPSHOT"
  :description "Moving things with Clojurescript"
  :dependencies [[org.clojure/clojure "1.5.1"]
                 [prismatic/dommy "0.1.1"]]
  :plugins [[lein-cljsbuild "0.3.2"]]
  :cljsbuild {:builds
              [{:source-paths ["src/accelerometer"]
                :compiler {:output-to "resources/public/accelerometer.js"
                           :optimizations :simple
                           :pretty-print true}}
               {:source-paths ["src/motion"]
                :compiler {:output-to "resources/public/motion.js"
                           :optimizations :simple
                           :pretty-print true}}]})

As you can see, we have two source paths. We'll be using the first one, accelerometer in the next section to make sure we're able to read the accelerometer data. After that we'll get things moving.

Accessing accelerometer data

First of all, here's how to use Clojurescript to display the accelerometer data in a web page:

(ns accelerometer
  (:use-macros [dommy.macros :only [sel1]])
  (:require [dommy.core]))

(.addEventListener js/window "devicemotion"
                    (fn [event]
                      (let [a (.-accelerationIncludingGravity event)]
                        (dommy/set-text! (sel1 ".acc-x")
                                         (.-x a))
                        (dommy/set-text! (sel1 ".acc-y")
                                         (.-y a))
                        (dommy/set-text! (sel1 ".acc-z")
                                         (.-z a)))))

Here, we add a listener for the devicemotion event. The event will also contain a .-acceleration property(that is, acceleration excluding the effect of gravity). But we'll use the one including gravity in this post.

Compile the Clojurescript file using lein cljsbuild once, then load the compiled Javascript file in a HTML file containing this:

Acc-x: <span class="acc-x"></span></br>
Acc-y: <span class="acc-y"></span></br>
Acc-z: <span class="acc-z"></span></br>

Serve the directory containing the HTML file using:

cd resources/public
python -m SimpleHTTPServer

Then, find your device's local IP address:

hostname -i
# should give you something like 192.168.1.15

So now if you visit http://192.168.1.15:8000/accelerometer.html on your mobile device, you should see the accelerometer data.

Motion

Now let's get to actually moving things. We'll use a canvas element of width and height both 300 pixels.

(def width 300)
(def height 300

We'll store the x and y coordinates of our blob in an atom,

(def state (atom {:x (/ width 2) 
                  :y (/ height 2)}))

Our draw function will draw our blob into the x and y coordinates:

(defn draw
  []
  (let [canvas (.getElementById js/document "canvas")
        ctx (.getContext canvas "2d")
        [r g b] [200 0 0]
        x (:x @state)
        y (:y @state)]
    (.clearRect ctx 0 0 width height)
    (set! (.-fillStyle ctx)
          (str "rgb(" r "," g "," b ")"))
    (.fillRect ctx x y 50 50)))

.clearRect above clears the canvas and .fillRect draws a square with length 50px. The .-fillStyle property gives us a red square.

Let's also show the x and y coordinates:

(defn show-coords
  []
  (let [x (:x @state)
        y (:y @state)]
    (dommy/set-text! (sel1 ".x")
                     (:x @state))
    (dommy/set-text! (sel1 ".y")
                     (:y @state))))

As before let's add our event listener,

(.addEventListener js/window "devicemotion"
                    (fn [event]
                      (let [a (.-accelerationIncludingGravity event)]
                        ;;(.log js/console a)
                        (dommy/set-text! (sel1 ".acc-x")
                                         (.-x a))
                        (dommy/set-text! (sel1 ".acc-y")
                                         (.-y a))
                        (swap! state update-in [:x] + (.-x a))
                        (swap! state update-in [:y] - (.-y a)))))

The interesting thing above are the two swap! blocks. They add the magnitudes of acceleration in the x and y coordinates respectively. This isn't a very good physical model, but this one works fine too so we'll let it pass, okay?

We'll have the canvas re-drawn every 100 milliseconds:

(defn ^:export init
  []
  (.setInterval js/window draw 100)
  (.setInterval js/window show-coords 100))

Finally, put this into an HTML file:

<html>
  <head>
    <style type="text/css">
      canvas { border: 1px solid black; }
    </style>
    <meta name="viewport" content="width=device-width, initial-scale=1">
  </head>
  <body>

    Acc-x: <span class="acc-x"></span></br>
    Acc-y: <span class="acc-y"></span></br>

    x:  <span class="x"></span></br>
    y:  <span class="y"></span></br>

    <script type="text/javascript" src="motion.js"></script>

    <canvas id="canvas" width="300" height="300">
      Your browser does not support HTML5 Canvas.
    </canvas>

    <script>
      motion.init();
    </script>
  </body>
</html>

As before, open this on your mobile device and you should see a canvas with a square which moves around when you tilt your phone.