Writing apps for Firefox OS phones

The Package

Another thing you want to do (otherwise, you won't be able to run your app on the simulator or handset) is write the manifest. The manifest is a JSON summary that contains all the details needed for Firefox OS to run the app correctly.

The manifest.webapp file must be in the root of the app. The example in Listing  1 gives you an idea of what the file should contain.

Listing 1

Manifest.webapp for Life

01 {
02   "version": "0.1",
03   "name": "Life",
04   "description": "Conway's Game of Life",
05   "launch_path": "/index.html",
06   "icons": {
07     "60": "/img/LifeIcon60x60.png",
08     "128": "/img/LifeIcon128x128.png"
09   },
10   "developer": {
11     "name": "Paul Brown",
12     "url": "http://www.linux-magazine.com"
13   },
14   "installs_allowed_from": ["*"],
15   "default_locale": "en",
16   "orientation": ["portrait"]
17 }

Most of the fields in the listing are pretty self-explanatory; however, note in the icons section that you will have a 60x60-pixel icon and another 128x128 pixels. The handset uses the 60-pixel icon in its list of apps (Figure 2); the 128-pixel icon comes into play if you want to distribute your app through the Firefox OS marketplace, which I talk about a bit later. A handy online guide shows you how to create a thematically consistent icon [4].

Figure 2: Firefox OS uses 60x60-pixel icons on its desktop.

The orientation field in line 16 allows you to decide how your app will be shown on the device: portrait, landscape, portrait-primary, landscape-primary, portrait-secondary, or landscape-secondary.

For example, if you specify portrait-primary in your manifest file, your app appears in portrait mode with the top of your UI at the top of the device, and the bottom of the UI at the bottom of the device's display. The portrait-secondary option flips the orientation 180 degrees. By using portrait, your app will flip in portrait mode depending on how the device is being held, but it will not rotate 90 degrees when you turn the device on its side.

You can also combine these options. For example,

"orientation": ["portrait", "landscape-secondary"]

allows your app to flip either way in portrait mode (right-side up or upside down) and will display correctly on one of its sides but not the other. The Mozilla Developer Network has a good tutorial on how to write a good manifest [5].

The Software

In my example project, I'll be implementing Conway's Game of Life (GoL) [6], which is probably one of the most popular and simplest life simulators. If you are unfamiliar with GoL, check out the "The Game of Life" box.

The Game of Life

In GoL, you set up a culture of cells in a virtual petri dish and then watch the cells reproduce. The rules are simple: Your Petri dish is a grid of square cells. The cells can be alive (filled in) or dead (empty). When you run the simulation, the program calculates the next generation by deciding which cells stay in the same state, which die, and which reproduce according to the following rules:

  • If a cell has zero or one neighbor, it dies from isolation; with four or more neighbors, it dies from overcrowding.
  • If a cell has two neighbors, it stays in the same state in the next generation (i.e., if it's dead in the current generation, it will be dead in the next generation; if it's alive in the current generation, it will be alive in the following generation).
  • If a cell has three neighbors, it either stays the same (if occupied) or comes to life (if empty).

When all the cells are plotted in the newest generation, it becomes the current generation, and the cycle begins again.

These simple rules can lead to extremely complex configurations that are nearly impossible to foresee, unless you have a large tablet of graph paper and, ironically, no life of your own. Consider the R-pentomino initial state as an example (Figure 3). It is a seed of only five cells that grows to an enormous size; generates shapes called gliders, toads, and loaves; and does not stabilize until generation 1,103.

Figure 3: The pattern known as the "R-pentomino" generates an enormous number of cells.

If you have had any experience with JavaScript in the last few years, you will have witnessed the rise of JavaScript libraries, none of which is more popular than jQuery. jQuery [7] has simplified the creation of dynamic web programming to the point of forcing Flash nearly to extinction (which, by the way, is an excellent thing). jCanvas [8], on the other hand, is an extension of jQuery that makes programming on a canvas object much easier.

In my implementation of the GoL (Listing 2), I will use the HTML5 canvas object as a playing board (line 17). To help, I also will use jQuery and jCanvas (lines 7 and 8). In lines 150 to 159 you can see how easy it is draw a cell on the canvas. Note that because I considered a single pixel too small, I decided to go with a 2x2 box for the cells. Therefore, with a 300x300 canvas, the playing field can hold 150x150 cells.

Listing 2

Life for Firefox OS (index.html)

001 <!DOCTYPE html>
002 <html>
003  <head>
004   <title>Game of Life</title>
005
006   <!-- Include external JavaScript -->
007   <script src="js/jquery-2.0.3.js"></script>
008   <script src="js/jcanvas.min.js"></script>
009
010   <!-- Style Sheets -->
011   <link rel="stylesheet" href="style/buttons.css">
012   <link rel="stylesheet" href="style/status.css">
013
014  </head>
015
016  <body>
017   <canvas width="300" height="300" style="border:1px solid black"></canvas>
018   <button id="operate">Start</button>
019   <button id="reset">Clear</button>
020
021   <!-- Status Bar -->
022   <section role="status" id="cell-counter" hidden>
023    <p>Cells: <strong>0</strong></p>
024   </section>
025
026   <script>
027
028    var CULTURE=[];
029    var BUTTON_TEXT="Start";
030    var RUN;
031
032    /*************
033     *
034     * STATUS BAR
035     *
036     */
037
038    $("canvas").dblclick(function(e)
039    {
040     $("#cell-counter" ).html( "<p>Cells: <strong>"+ CULTURE.length + "</strong></p>" );
041     $("#cell-counter").fadeIn().delay(2000).fadeOut('slow');
042    });
043
044    /*************
045     *
046     * EDIT MODE
047     *
048     */
049
050    // Check position of pointer on canvas
051    $("canvas").click(function(e)
052    {
053     var canvas_pos = $("canvas").position();
054     var cellCoords= [Math.floor((e.pageX-canvas_pos.left)/2), Math.floor((e.pageY-canvas_pos.top)/2)]
055     editCell(cellCoords);
056    });
057
058
059    function editCell(coords)
060    {
061     var stringCoords='"'+ coords[0] + ',' + coords[1] +'"'
062     if($.inArray(stringCoords, CULTURE) === -1)
063     {
064      CULTURE.push(stringCoords);
065      drawPixel(coords);
066     }
067     else
068     {
069      CULTURE.splice(CULTURE.indexOf(stringCoords),1);
070      erasePixel(coords);
071     }
072
073    }
074
075    /*************
076     *
077     * RUN MODE
078     *
079     */
080
081    function mainLoop()
082    {
083     // This is the temporary array where we store
084     // the next culture
085     var nextCulture = [];
086
087     // Here we store the cells we have already examined
088     var cellExamined = [];
089
090     $.each(CULTURE, function(i, point)
091     {
092      var newX= convrt2Coords(point)[0];
093      var newY= convrt2Coords(point)[1];
094
095      // nbx and nby are the cells to examine since
096      // they are neighbouring active cells
097
098      for (nbx=newX-1;nbx<newX+2;nbx++)
099      {
100       for (nby=newY-1; nby<newY+2; nby++)
101       {
102        // We'll only examine them if we haven't done so already
103        // (and they are not off the edge of the board).
104        if ($.inArray('"'+ nbx + ',' + nby +'"', cellExamined)===-1)
105        {
106         cellExamined.push('"'+ nbx + ',' + nby +'"');
107         if(nbx>=0 && nbx<=300 && nby>=0 && nby<=300)
108         {
109          // Check neighbours
110          var neighbours=0;
111          for (ncx=nbx-1; ncx<nbx+2; ncx++)
112          {
113           for (ncy=nby-1; ncy<nby+2; ncy++)
114           {
115            if (!(ncx==nbx && ncy==nby))
116            {
117             if($.inArray('"'+ ncx + ',' + ncy +'"', CULTURE)!==-1) neighbours++;
118            }
119           }
120          }
121
122          switch(neighbours)
123          {
124           case (3):
125            // If no. of neighbours is 3, a cell is born
126            nextCulture.push('"' + nbx + ',' + nby + '"');
127           case (2):
128            // If number of neighbours 2, leave alone
129            if ($.inArray('"' + nbx + ',' + nby + '"', CULTURE)!==-1)
130            {
131             nextCulture.push('"' + nbx + ',' + nby + '"');
132            }
133          }
134         }
135        }
136       }
137      }
138     });
139
140     CULTURE = nextCulture;
141     drawPlayingField();
142    }
143
144    /*************
145     *
146     * MISC STUFF
147     *
148     */
149
150    function drawPixel(coords)
151    {
152     $("canvas").drawRect
153     ({
154      fillStyle:"rgba(0, 0, 0, 1)",
155      x: coords[0]*2, y: coords[1]*2,
156      width: 2,
157      height: 2
158     });
159    }
160
161    function erasePixel(coords)
162    {
163     $("canvas").clearCanvas
164     ({
165      x: coords[0]*2, y: coords[1]*2,
166      width: 2,
167      height: 2
168     });
169    }
170
171    function drawPlayingField()
172    {
173     $("canvas").clearCanvas();
174     //Loop thru coors and draw pixels
175     $.each(CULTURE, function(i, point)
176     {
177      drawPixel(convrt2Coords(point));
178      });
179    }
180
181    function convrt2Coords(coords)
182    {
183     var intCoords=[parseInt(coords.split(",")[0].slice(1)), parseInt(coords.split(",")[1])];
184     return intCoords;
185    }
186
187    /*************
188    *
189    * BUTTONS
190    *
191    */
192
193    $("#operate").click(function()
194    {
195     $(this).toggleClass("active");
196
197     if (BUTTON_TEXT=="Start")
198     {
199      if (CULTURE[0])
200      {
201       BUTTON_TEXT="Stop";
202       $(this).text(BUTTON_TEXT);
203       RUN=setInterval(mainLoop,1);
204      }
205     }
206     else
207     {
208      BUTTON_TEXT="Start";
209      clearInterval(RUN);
210      $(this).text(BUTTON_TEXT);
211     }
212    });
213
214    $("#reset").click(function()
215    {
216     BUTTON_TEXT="Start";
217     clearInterval(RUN);
218     $("canvas").clearCanvas();
219     CULTURE=[];
220    });
221
222   </script>
223  </body>
224 </html>

The code in Listing 2 also uses jQuery extensively to refer to and manipulate some of the elements, such as the status bar (Figure 4), which will slowly appear and then fade out if you tap the playing field twice (the equivalent of a double-click) and show you how many live cells are in the current generation (lines 38-42). The HTML to include the status bar in your app is very simple (lines 22-24), although you will have to modify the status.css file to use the clean, elegant font provided by Mozilla (Listing 3).

Figure 4: The status bar shows the number of living cells in a given generation.

Listing 3

Modified status.css File

01 /* ----------------------------------
02  * Status
03  * ---------------------------------- */
04
05 @font-face
06 {
07   font-family: FiraSans;
08   src: url('/fonts/FiraSans/FiraSans-Regular.ttf');
09 }
10
11 @font-face
12 {
13   font-family: FiraSans;
14   src: url('/fonts/FiraSans/FiraSans-Bold.ttf');
15   font-weight:bold;
16 }
17
18 section[role="status"] {
19   background: rgba(64,64,64,1) url(status/images/ui/pattern.png) repeat left top;
20   overflow: hidden;
21   position: absolute;
22   z-index: 100;
23   left: 0;
24   right: 0;
25   bottom: 0;
26   color: #fff;
27   text-align: left;
28 }
29
30 section[role="status"] p {
31   font-family: FiraSans;
32   font-size: 1.8rem;
33   font-weight: normal;
34   line-height: 2.2rem;
35   margin: 1rem 3rem;
36   padding: 0;
37   text-align: left;
38 }
39
40 section[role="status"] p strong {
41   font-family: FiraSans, bold;
42   color: rgb(9, 149, 176);
43 }

The Start(Stop) and Clear buttons (Figure 5) are included in the user interface in lines 18 and 19 (Listing 2). When you click Start or Stop (lines 193-212), you either start the mainLoop function (which runs the simulation) or stop it.

Figure 5: GoL uses a canvas object for the playing field and implements two buttons.

The Clear button uses a jCanvas function to erase all pixels from the canvas and empties the CULTURE array that holds the coordinates of the live cells in the current generation (lines 214-220).

The biggest chunk of code (lines 81-142) is the mainloop mentioned previously. This section calculates the next generation based on the current generation stored in CULTURE. The most obvious way to do this is to loop over every cell in a 150x150 array and examine each of the eight possible neighbors, changing their states as necessary. However, this approach is a very slow, very inefficient way of doing things.

Instead, the CULTURE array contains the coordinates of only the live cells in the current generation. By examining only live cells and their immediate neighbors, you can skip enormous empty chunks of the playing board. All of the white space above, between, and below the live cells is ignored (lines 90-138).

The jQuery $.each() loops over all the elements in CULTURE and avoids examining a cell twice by storing it in the cellExamined array (line 106) and skipping it if it is encountered again (line 104). The new cell configuration is gradually stored in the nextCulture array.

Note that the cells are stored as strings. Thus, cell (130, 45) is stored as [130,45]. Arrays in JavaScript are flat, so a two-dimensional array like

a = [[130, 45], [20, 130], [45, 10]]

is actually stored like

a = [130, 45, 20, 130, 45, 10]

making searches for duplicate coordinates (i.e., lines 62-71) very difficult. The conversion from string to numbers and back again, which is going on within the loops, certainly has an effect on performance, but the only other way would be to store cells as an array of objects. Unfortunately, looping and searching for duplicates within an array of objects is also difficult, hence the string-based solution.

Finally, nextCulture is dumped into CULTURE, and the new generation is drawn to the playing board (lines 140-141).

The Distribution

Now that your app works, it's time to package it and get it distributed. The packaging process is very straightforward. All you need to do is create a ZIP file that has the manifest.webapp file in its root. In Linux, you would cd into the root of your app and type:

zip -r <appname>.zip *

Now, you have two choices: You can host the app yourself on your own site, which Mozilla is perfectly okay with, or you can upload it to the Firefox OS Marketplace (Figure 6) [9]. To use the Marketplace, you need a Firefox OS developer account (Figure 7) [10]; the submission process, is very straightforward (Figures 8 and 9).

Figure 6: The Firefox OS Marketplace contains officially sanctioned and audited apps.
Figure 7: The marketplace developer hub has a friendly interface for uploading your apps.
Figure 8: Submitting a packaged app to the marketplace is a simple, straightforward, four-step process.
Figure 9: Your app is easily installed once it is approved for the marketplace.

Apart from providing more exposure with the marketplace, the wizard helps you weed out any errors you might have in your manifest; moreover, in the future, you will be able to sell your apps using Mozilla's framework.

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

  • Programming the Cell

    The Cell architecPIture is finding its way into a vast range of computer systems – from huge supercomputers to inauspicious Playstation game consoles. We'll show you around the Cell and take a look at a sample Cell application.

  • Linux News
    • OSI accepting individual memberships.
    • OpenGL 4.3 and ES 3.0.
    • License Protection
    • Raspberry Pi ramps up production.
    • FSFE protects licences when bankruptcy strikes.
    • Gaming passwords secured with My1login.
    • Web Apps new to Ubuntu.
    • LPI Forum
    • RSA Anti Rogue App.
    • US Cell Phone Right-to-Know Act.
  • Go_Mobile_Intro.pdf
  • HTML5 Offline

    An offline cache in your browser and a bit of HTML5 acrobatics combine for interactive web applications that keep working even when the Internet connection breaks down.

  • Review: Biella Coleman's "Coding Freedom"
comments powered by Disqus

Direct Download

Read full article as PDF:

Price $2.95

News

njobs Europe
What:
Where:
Country:
Njobs Netherlands Njobs Deutschland Njobs United Kingdom Njobs Italia Njobs France Njobs Espana Njobs Poland
Njobs Austria Njobs Denmark Njobs Belgium Njobs Czech Republic Njobs Mexico Njobs India Njobs Colombia