Categories
Articles

PhoneGap Cordova Accelerometer HTML5 Canvas Animation XCode Example

This is a template example using the accelerometer with Cordova or Phonegap and animating a sprite on the HTML5 canvas. I was working on a game using the html canvas and the accelerometer. I developed a way to combine both in IOS using Objective C and the UIWebView in the UIKit. That approach limits distribution to the IOS platform but allows you to have all of the native UI items like screen navigation and just use the canvas for animations.


This is a template example using the accelerometer with Cordova or Phonegap and animating a sprite on the HTML5 canvas.blog_image400x545

Learn cocos2D Game Development
PhoneGap Essentials: Building Cross-Platform Mobile Apps

I was working on a game using the html canvas and the accelerometer. I developed a way to combine both in IOS using Objective C and the UIWebView in the UIKit. That approach limits distribution to the IOS platform but allows you to have all of the native UI items like screen navigation and just use the canvas for animations.

Then I became curious about distributing on multiple devices containing an accelerometer since the animation is being done in html canvas. The solution was a hybrid mobile platform like Cordova. UI in Cordova is really up to you although you can find UI libraries written in HTML, JavaScript and JQuery optimized for mobile.

Here are screen shots for the app.

There is only one screen and these are just showing the animated red circle in different positions. The functionality is simple in that the red circle moves in the direction of the accelerometer’s x and y values – well with some adjustments but we will cover them in a bit.

When it reaches the boundaries of the canvas for either direction, it stop moving in that one direction. Thus you can move it to any corner and it will stay fixed and you can move it around the edges of the canvas.

You can also tap the canvas to start and stop the animation.

The Project Files

The project uses Cordova 1.6, XCode 4.3.2 and was tested using iOS 5.1 on a IPhone 4.

I had started the example using Cordova 1.5. Cordova 1.6 was released before completing this article and Cordova 1.6 changed the accelerometer values. The release notes included “Rewrite of accelerometer code and removed DeviceInfo old init approach” and “Added unification of accelerometer values on ios (based on android values)”. Boiling those entries down we get that the values are the same across platform, but if you are using IOS you better divide by 10. I commented in the code in case you are still using 1.5 or earlier.

I added a simple html console area to display debugging messages from the Javascript.

I also stripped out most of the extra Cordova comments that were not relevant to this example.

Download XCode Project Files

Step 1 – Install Cordova for XCode

Instructions for setting up your development environment are located at the PhoneGap site Getting Started with iOS.

Step 2 – Create New Cordova-based Application

Step 3 – Set XCode Project Options


Set the XCode project options as follows:

  • Product name: Here I used CordovaAccelerometerCanvas as the project name. You can use a name of your own choosing.
  • Company identifier: Provide your own reverse domain name.
  • Use Automatic Reference Counting: Uncheck

Step 4 – Choose A File Location

Once you select a file location on your computer you will have a folder structure as follows:

And in XCode you will see a project window as follows:

You may notice the project has a warning. Ignore this for now.

Step 5 – Create and Add the www Folder and Files to the Project

These are the normal setup instructions for a Cordova alias PhoneGap XCode project. You can skip this step if you are used to creating Cordova XCode projects.

The process is creating a www folder by running the app once in the IPhone simulator and then add to the project explorer.

First run the app in the IPhone simulator.

The app runs but complains of the missing index.html file.

A www folder with this file and one other js file were created on your file system as this the app launched in the simulator. Here you see them and you need to drag the www folder into the Project Explorer. Do not drag to or copy to the XCode folders outside of XCode.

Fill out the “Choose options for adding these files” dialog as follows.

  • Destination: Unchecked
  • Folders: “Created folder references for added folders” selected.
  • Add to targets: CordovaAccelerometerCanvas checked.

And here are the final results you see in the Project Explorer window.

Run the app in the IPhone simulator one more time.

This time you will be greeted with an Alert dialog with the “Cordova is working” message.

Step 7a – The Completed index.html File

Here is the full index.html file completed for your copy convenience. Just replace your index.html file with this and you can test on your device.

I removed Cordova code comments and commented code not related to our needs that is included in the index.html file.

<!DOCTYPE html>
<html>
    <head>
        <title></title>
        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no;" />
        <meta charset="utf-8">
        <script type="text/javascript" charset="utf-8" src="cordova-1.6.0.js"></script>
        <script type="text/javascript">
            var console_log;                // Debugging div on device
            var canvas_ctx;                 // HTML canvas 2d context
            var SPEED = 10;                 // Canvas redraw speed in milliseconds and accelerometer frequency
            var DISTANCE_FACTOR = .1;       // Factor to adjust accelerometer values to screen distance. Cordova 1.6 changed to values to be Android compatible which appeared to be * 10. For Cordova 1.5 make this value 1.
            var ax = 0;                     // Acceleration x axis (Accelerometer value adjusted for direction)
            var ay = 0;                     // Acceleration y axis (Accelerometer value adjusted for direction)
            var x;                          // Circle x position
            var y;                          // Circle y position
            var vx = 0;                     // Velocity x axis
            var vy = 0;                     // Velocity y axis
            var WIDTH = 320;                // Width of canvas
            var HEIGHT = 300;               // Height of canvas
            var RADIUS = 50;                // Width of circle object
            var CIRCLE_COLOR = "#f00";      // Circle color
            var CANVAS_COLOR = "#FAF7F8";   // Color of canvas background
            var watchID;                    // Accelerometer.watchAcceleration return value. 
            var drawID;                     // Draw time interval. 
            var playing = true;             // Boolean if animation is playing.
            /* DOM body onload event callback */
            function onBodyLoad()
            {		
                document.addEventListener("deviceready", onDeviceReady, false);
            }
            /* Cordova has been initialized and is ready to roll */
            function onDeviceReady()
            {
                console.log('console_div');
                console_log = document.getElementById('console_div');
                console_log.innerHTML += "onDeviceReady()<br/>";
                init();
            }
            /* Initialize canvas and animation */
            function init() 
            {
                var canvas = document.getElementById("canvas");
                canvas_ctx = canvas.getContext("2d");
                // Center 
                x = WIDTH / 2 ;
                y = HEIGHT/ 2 ;
                startPlay();
            }
            /* Start watching the accelerometer */
            function startWatch() 
            {
                var options = { frequency: SPEED };
                watchID = navigator.accelerometer.watchAcceleration(onSuccess, onError, options);
            }   
            // Stop watching the accelerometer
            function stopWatch() 
            {
                if (watchID) {
                    navigator.accelerometer.clearWatch(watchID);
                    watchID = null;
                }
            }
            /* Accelerometer data callback */
            function onSuccess( acceleration )
            {
                // Set drawing acceleration values
                ax = acceleration.x * DISTANCE_FACTOR * -1; // -1 to change direction for Cordova 1.6. Removed for Cordova 1.5.
                ay = acceleration.y * DISTANCE_FACTOR ;// Add * -1 for Cordova 1.5;
                // Optional ouput for understanding accelerator values.
                console_log.innerHTML = 
                'Acceleration X: ' + acceleration.x + '<br />' +
                'Acceleration Y: ' + acceleration.y + '<br />' +
                'Acceleration Z: ' + acceleration.z + '<br />' +
                'Timestamp: '      + acceleration.timestamp ;
            }
            /*  Accelerometer error callback */
            function onError()
            {
                alert("Accelerometer Error");
            }
            /* Steps to start animation play */
            function startPlay()
            {
                playing = true;
                vx = 0;
                vy = 0;
                startWatch();
                drawID = setInterval(draw, SPEED);
            }
            /* Steps to stop animation play */
            function stopPlay()
            {
                clearInterval(drawID);
                stopWatch();
                playing = false;
            }
            /* Draw circle */   
            function circle( x, y, r ) 
            {
                canvas_ctx.beginPath();
                canvas_ctx.arc(x, y, r, 0, Math.PI*2, true);
                canvas_ctx.fill();
            }
            /* Draw rectangle */
            function rect( x, y, w, h ) 
            {
                canvas_ctx.beginPath();
                canvas_ctx.rect(x,y,w,h);
                canvas_ctx.closePath();
                canvas_ctx.fill();
            }
            /* Clear canvas */
            function clear() 
            {
                canvas_ctx.clearRect(0, 0, WIDTH, HEIGHT);
            }
            /* Compute drawing metrics and draw frame */    
            function draw() 
            {
                // Increase velocity by acceleration
                vx += ax;
                vy += ay;
                // Update circle drawing position.
                x += vx;
                y += vy;
                /* Boundaries testing */
                // Right boundary
                if ( x + RADIUS > WIDTH  )
                {
                    x = WIDTH - RADIUS ;
                    vx = 0;
                }
                // Left boundary
                if (x - RADIUS  <= 0)
                {
                    x = RADIUS   ;
                    vx = 0;
                }
                // Bottom boundary
                if (y +  RADIUS  > HEIGHT)
                {
                    y = HEIGHT - RADIUS ;
                    vy = 0;
                }
                // Top boundary
                if (y - RADIUS  <= 0)
                {
                    y = RADIUS  ;
                    vy = 0;
                }
                
                // Debugging info.
                //console_log.innerHTML = 
                //'x: ' + x + '<br />' +
                //'y: ' + y + '<br />' +
                //'vx: ' + vx + '<br />' +
                //'vy: ' + vy + '<br />' +
                //'ax: ' + ax + '<br />' +
                //'ay: ' + ay + '<br />' ;    
                
                /* Draw frame */
                // Clear canvas
                clear();
                // Draw canvas background
                canvas_ctx.fillStyle = CANVAS_COLOR;
                rect( 0, 0, WIDTH, HEIGHT );
                /* Draw circle */
                canvas_ctx.fillStyle = CIRCLE_COLOR;
                circle( x, y, RADIUS );
            }
            /* Canvas tag touch end event handler */
            function canvasTouchEnd()
            {
                if (playing)
                {
                    stopPlay();
                }
                else
                {
                    startPlay();
                }
            }
        </script>
    </head>
    <body onload="onBodyLoad()" style = "text-align:center;background-color:#ccc;padding:0px;margin:0px;">
        <div>
            <h1 style = "font-size:20px;margin-bottom:0px;margin-top:0px;padding-top:0px;">Accelerometer + HTML5 Canvas</h1>
            <canvas id="canvas" width="320" height="300" ontouchend = "canvasTouchEnd();" >
                This text is displayed if your browser 
                does not support HTML5 Canvas.
            </canvas>
            <div id = "console_div" style = "position:absolute;text-align:left;margin:2px;border:1px solid black;background-color:#fff;top:330px;left:0px;width:314px;height:118px;overflow:auto;" 
                >
            </div>
        </div>
    </body>
</html>

The remaining part of this article will explain the parts of the index.html file by topic.

Step 7a – The index.html body Section

Key in this section is the canvas tag on line 189. I am using the ontouchend event versus the onclick event to prevent a canvas flicker when the screen is touched. The action is to start and stop the animation.

Line 193 is my own html debugging console for simple examples. You should see the accelerometer values displayed in here when testing on a device.

 <body onload="onBodyLoad()" style = "text-align:center;background-color:#ccc;padding:0px;margin:0px;">
        <div>
            <h1 style = "font-size:20px;margin-bottom:0px;margin-top:0px;padding-top:0px;">Accelerometer + HTML5 Canvas</h1>
            <canvas id="canvas" width="320" height="300" ontouchend = "canvasTouchEnd();" >
                This text is displayed if your browser 
                does not support HTML5 Canvas.
            </canvas>
            <div id = "console_div" style = "position:absolute;text-align:left;margin:2px;border:1px solid black;background-color:#fff;top:330px;left:0px;width:314px;height:118px;overflow:auto;" 
                >
            </div>
        </div>
    </body>

[ad name=”Google Adsense”]
Step 7b – The index.html Accelerometer Code

Cordova provides an api for the accelerometer. We use a good portion of it with this example.

The variables on lines 11 to 14 are helpers for the animation. We do not need them for raw access to the accelerometer. You can see their explanations and we will look at them applied further along.

            var SPEED = 10;                 // Canvas redraw speed in milliseconds and accelerometer frequency
            var DISTANCE_FACTOR = .1;       // Factor to adjust accelerometer values to screen distance. Cordova 1.6 changed to values to be Android compatible which appeared to be * 10. For Cordova 1.5 make this value 1.
            var ax = 0;                     // Acceleration x axis (Accelerometer value adjusted for direction)
            var ay = 0;                     // Acceleration y axis (Accelerometer value adjusted for direction)

Line 24 has an id for the accelerometer watch event. This can be used to clear the watch event when you do not need the accelerometer anymore.

            var watchID;                    // Accelerometer.watchAcceleration return value. 

I created the startWatch function for this example to call when we need to start collecting accelerometer data.

The accelerometer works by calling your own event handler functions using the watchAcceleration method you see on line 54.
The watchAcceleration take a reference to your success and error handler as the first two arguments.

It also takes an options object for the third argument. Currently the only option object key is frequency measured in milliseconds. I am using the SPEED variable set to 10 milliseconds for the frequency.

The watchAcceleration returns an id so we can refer to it later.

            /* Start watching the accelerometer */
            function startWatch() 
            {
                var options = { frequency: SPEED };
                watchID = navigator.accelerometer.watchAcceleration(onSuccess, onError, options);
            }   

The stopWatch function is also created for the example to stop requesting accelerometer data anywhere in the code. Line 60 uses the clearWatch method to stop watching the accelerometer. The watchID variable from line 54 identifies the watch activity to end.

To prevent throwing errors, logic is included to test for the watchID.

            // Stop watching the accelerometer
            function stopWatch() 
            {
                if (watchID) {
                    navigator.accelerometer.clearWatch(watchID);
                    watchID = null;
                }
            }

The onSuccess function on line 64 is called from the watchAcceleration on line 54.

The onSuccess function passes an acceleration object. The acceleration object currently has four values and they are all displayed in our html console on line 71.

The other code in the onSuccess method prepares variables for the animation.

The ax and ay variables on line 68 and 69 are accelerometer x and y values respectively. They represent acceleration in our animation.

For the x-axis value we need to reverse the value sign so the tilt of the device represents the direction of the x-axis animation. Interesting this is for Cordova 1.6. In Cordova 1.5 this is not needed.

For the y-axis value we need to reverse the value sign if you are using Cordova 1.5. Again to represent the direction by the tilt of the device.

Finally the x and y acceleration values were adjusted with Cordova 1.6. As I had mentioned I started the example in Cordova 1.5 and was able to use the raw values. But with Cordova 1.6 the values are 10x so I added the DISTANCE_FACTOR variable set to .1 to compensate. Perhaps I should have called it a “VERSION_FACTOR”.

I had no need for z and timestamp properties in this example, but they are displayed for interest. You may get updates from the device due to your frequency of request, but the device has not sensed new values, so you can use the timestamp property in case you are using the accelerometer updates for a cpu intensive function that without a change is unnecessary.

            /* Accelerometer data callback */
            function onSuccess( acceleration )
            {
                // Set drawing acceleration values
                ax = acceleration.x * DISTANCE_FACTOR * -1; // -1 to change direction for Cordova 1.6. Removed for Cordova 1.5.
                ay = acceleration.y * DISTANCE_FACTOR ;// Add * -1 for Cordova 1.5;
                // Optional ouput for understanding accelerator values.
                console_log.innerHTML = 
                'Acceleration X: ' + acceleration.x + '<br />' +
                'Acceleration Y: ' + acceleration.y + '<br />' +
                'Acceleration Z: ' + acceleration.z + '<br />' +
                'Timestamp: '      + acceleration.timestamp ;
            }

This final code snippet is simply the error handler for watching the accelerometer. Apparently there is no information provided for the error, so you will need to improvise.


            /*  Accelerometer error callback */
            function onError()
            {
                alert("Accelerometer Error");
            }

Step 7c – User Start and Stop Interaction

Touching the canvas starts and stops the animation. This starts on line 189 with the canvas tag ontouchend event calling the canvasTouchEnd() function on line 172.

           <canvas id="canvas" width="320" height="300" ontouchend = "canvasTouchEnd();" >
                This text is displayed if your browser 
                does not support HTML5 Canvas.
            </canvas>

There is the playing variable set on line 26 that retains the state of animation playing.

            var playing = true;             // Boolean if animation is playing.

The canvasTouchEnd function uses the playing boolean variable to determine starting or stopping animation and calls function to handle changing the animation playing state.

            /* Canvas tag touch end event handler */
            function canvasTouchEnd()
            {
                if (playing)
                {
                    stopPlay();
                }
                else
                {
                    startPlay();
                }
            }

The startPlay function first sets the playing state to true.

To start playing the velocity variables are set to zero. This prevents any timing issues that may cause the animation move in a direction not indicative of the tilt of the device when the animation is restarted.

Receiving events from the accelerometer is started with the call to the startWatch function we reviewed earlier.

Handing the canvas redraw is done on line 89 with a JavaScript timer. The timing of the of the canvas redraw is the same as the updates from the accelerometer. You could consider a different design where the accelerometer updates perform the canvas redraw. However the approach we are using allows animations to occur that not dependent on the accelerometer changes should you need to add them.

            /* Steps to start animation play */
            function startPlay()
            {
                playing = true;
                vx = 0;
                vy = 0;
                startWatch();
                drawID = setInterval(draw, SPEED);
            }

Stopping animation is done by clearing the timer interval, stopping the accelerometer watching and setting the playing state to false.

            /* Steps to stop animation play */
            function stopPlay()
            {
                clearInterval(drawID);
                stopWatch();
                playing = false;
            }

Step 7d – The index.html Canvas Animation

The canvas animation does not require Cordova. It is just an interesting way to demonstrate using the accelerometer. All the animation in the canvas could be done in any HTML5 web browser.

First is the canvas tag which has a predefined width and height. We could create the canvas tag dynamically in JavaScript, but I left it out to keep code simpler.

           <canvas id="canvas" width="320" height="300" ontouchend = "canvasTouchEnd();" >
                This text is displayed if your browser 
                does not support HTML5 Canvas.
            </canvas>

Next there are some variables that impact the canvas animation work.

Line 10 has a variable to reference the canvas 2d context to allow drawing. This will refer back to the canvas tag.

The SPEED variable on line 11 is for the JavaScript timer that will call the draw function. The draw function does all the work on the canvas.

Line 13 and 14 are acceleration values that are updated using the accelerometer.

The x and y on lines 14 and 15 are the position of the circle sprite we are animating.

The velocity variables are initialized to zero on lines 16 and 17.

Height and width of the canvas are repeated here for computing the boundaries for our animated circle sprite.

The diameter of the circle sprite is configurable on line 21

Adding some color on lines 22 and 23 for the circle sprite and background respectively.

Then we have on line 25 the JavaScript timer interval id for redrawing the canvas.

            var console_log;                // Debugging div on device
            var canvas_ctx;                 // HTML canvas 2d context
            var SPEED = 10;                 // Canvas redraw speed in milliseconds and accelerometer frequency
            var DISTANCE_FACTOR = .1;       // Factor to adjust accelerometer values to screen distance. Cordova 1.6 changed to values to be Android compatible which appeared to be * 10. For Cordova 1.5 make this value 1.
            var ax = 0;                     // Acceleration x axis (Accelerometer value adjusted for direction)
            var ay = 0;                     // Acceleration y axis (Accelerometer value adjusted for direction)
            var x;                          // Circle x position
            var y;                          // Circle y position
            var vx = 0;                     // Velocity x axis
            var vy = 0;                     // Velocity y axis
            var WIDTH = 320;                // Width of canvas
            var HEIGHT = 300;               // Height of canvas
            var RADIUS = 50;                // Width of circle object
            var CIRCLE_COLOR = "#f00";      // Circle color
            var CANVAS_COLOR = "#FAF7F8";   // Color of canvas background
            var watchID;                    // Accelerometer.watchAcceleration return value. 
            var drawID;                     // Draw time interval. 
            var playing = true;             // Boolean if animation is playing.

The init() function is called when the app boots up and here you see on line 43 and 44 the standard way to get the drawing context to the canvas tag.

Also the starting position of the circle sprite is placed at the center of the screen.

            /* Initialize canvas and animation */
            function init() 
            {
                var canvas = document.getElementById("canvas");
                canvas_ctx = canvas.getContext("2d");
                // Center 
                x = WIDTH / 2 ;
                y = HEIGHT/ 2 ;
                startPlay();
            }

There are some utility functions for drawing shapes in the html canvas. First we have the circle method which demonstrates how to draw a circle given x, y and radius values.

Discussing the values for drawing on the canvas are beyond the scope of this article but I strongly suggest you get a copy of Foundation HTML5 Animation with JavaScript.

The values for the canvas 2d context arc method are centerX, centerY, radius, startingAngle in radians, endingAngle in radians and the boolean antiClockwise.

The startingAngle and endingAngle have radian values to draw a complete circle.

Note you will need to compensate for x and y because the values for x and y reference the center. You will see this offset in the boundary testing code.

            /* Draw circle */   
            function circle( x, y, r ) 
            {
                canvas_ctx.beginPath();
                canvas_ctx.arc(x, y, r, 0, Math.PI*2, true);
                canvas_ctx.fill();
            }

Next is the rect function on line 106 to draw a rectangle. The canvas 2d context rect method is straightforward drawing from top left corner of the rectangle.

            /* Draw rectangle */
            function rect( x, y, w, h ) 
            {
                canvas_ctx.beginPath();
                canvas_ctx.rect(x,y,w,h);
                canvas_ctx.closePath();
                canvas_ctx.fill();
            }

In drawing for animation, you will need to clear the canvas, so this clear function allows that to happen. The one line is clearRect method of the canvas 2d context. The clearRect method clears a rectangular area and here we have the entire canvas covered.


            /* Clear canvas */
            function clear() 
            {
                canvas_ctx.clearRect(0, 0, WIDTH, HEIGHT);
            }

The drawing position computations and actual drawing are both done in the draw function. Some animation applications you may want to split the drawing from the computations, but in this case putting them together meets our needs.

The velocity in the x and y directions are computed on lines 122 and 123. Simply they are increased or decreased by the change in acceleration values from the accelerometer. Acceleration values contain direction as well as acceleration.

Then we recompute the circle sprite position on lines 125 and 126.

Lines 127 – 151 are computing the boundaries for the canvas in relation to the proposed position of the center point, the x and y values, of the circle sprite. If any boundary is reached, then the velocity for that direction is set to zero and the position value is computed to keep the circle sprite in view at the edge.

I left the debugging information lines in but commented. If you use them, then comment lines 71 – 75.

Final work is to draw and this starts on line 164 where the canvas is cleared of all drawings.

Then lines 166 and 167 get our canvas background drawn.

The circle sprite is drawn on lines 169 and 170.

            /* Compute drawing metrics and draw frame */    
            function draw() 
            {
                // Increase velocity by acceleration
                vx += ax;
                vy += ay;
                // Update circle drawing position.
                x += vx;
                y += vy;
                /* Boundaries testing */
                // Right boundary
                if ( x + RADIUS > WIDTH  )
                {
                    x = WIDTH - RADIUS ;
                    vx = 0;
                }
                // Left boundary
                if (x - RADIUS  <= 0)
                {
                    x = RADIUS   ;
                    vx = 0;
                }
                // Bottom boundary
                if (y +  RADIUS  > HEIGHT)
                {
                    y = HEIGHT - RADIUS ;
                    vy = 0;
                }
                // Top boundary
                if (y - RADIUS  <= 0)
                {
                    y = RADIUS  ;
                    vy = 0;
                }
                
                // Debugging info.
                //console_log.innerHTML = 
                //'x: ' + x + '<br />' +
                //'y: ' + y + '<br />' +
                //'vx: ' + vx + '<br />' +
                //'vy: ' + vy + '<br />' +
                //'ax: ' + ax + '<br />' +
                //'ay: ' + ay + '<br />' ;    
                
                /* Draw frame */
                // Clear canvas
                clear();
                // Draw canvas background
                canvas_ctx.fillStyle = CANVAS_COLOR;
                rect( 0, 0, WIDTH, HEIGHT );
                /* Draw circle */
                canvas_ctx.fillStyle = CIRCLE_COLOR;
                circle( x, y, RADIUS );
            }

Good luck!

[ad name=”Google Adsense”]