IBookAuthor: Javascript Calculator

From dftwiki
Jump to: navigation, search

--D. Thiebaut 17:44, 27 January 2013 (EST)


              


The Javascript Calculator

JavascriptCalculatorSimulator.png

You should first read the excellent article by Simon Southwell[1].

Southwell's article is a step by step description of the features of this simulator, showing how to "cut" the photo of a calculator into several areas corresponding to the keys and to the digits, and how to simulate the behavior of a real calculator when the user presses the keys on the screen. On a computer without a touch-screen display the user must use the mouse to click the keys. On an iPad or tablet, the user simply presses the keys on the screen. It's very cool when you install it and it first works!

The javascript code provided by Southwell is available here. A multi-part PDF copy of the article is available at the bottom of this page.

Creating the Widget

Creating the widget simply requires copying the skeleton widget presented in this collection of tutorials, adding a new directory in it called images to hold all the png files representing the various parts of the calculator (keys, display digits), and updating the Info.plist file with the name of the package (although not really necessary).

File Hierarchy

The list below shows the complete hierarchy of files for the widget. calculator.wdgt is the name of the directory. calc.html is the main html file containing the javascript code for the calculator. Default.png is a single resolution image of the calculator (which will appear in the place holder for the widget in the iBook). Default@2x.png is the double resolution image of the same calculator, for Retina displays. images is the directory containing all the different parts of the calculator. Apple recommends avoiding the following names[2]: Resources/, Support Files/Resources/, or Contents/Resources/. Info.plist is the required file describing the bundle using Apple's description language.

calculator.wdgt
calculator.wdgt/calc.html
calculator.wdgt/Default.png
calculator.wdgt/Default@2x.png
calculator.wdgt/images
calculator.wdgt/images/display.jpg
calculator.wdgt/images/display_left.jpg
calculator.wdgt/images/display_right.jpg
calculator.wdgt/images/index.html
calculator.wdgt/images/keys.jpg
calculator.wdgt/images/lcd0.jpg
calculator.wdgt/images/lcd0dot.jpg
calculator.wdgt/images/lcd1.jpg
calculator.wdgt/images/lcd1dot.jpg
calculator.wdgt/images/lcd2.jpg
calculator.wdgt/images/lcd2dot.jpg
calculator.wdgt/images/lcd3.jpg
calculator.wdgt/images/lcd3dot.jpg
calculator.wdgt/images/lcd4.jpg
calculator.wdgt/images/lcd4dot.jpg
calculator.wdgt/images/lcd5.jpg
calculator.wdgt/images/lcd5dot.jpg
calculator.wdgt/images/lcd6.jpg
calculator.wdgt/images/lcd6dot.jpg
calculator.wdgt/images/lcd7.jpg
calculator.wdgt/images/lcd7dot.jpg
calculator.wdgt/images/lcd8.jpg
calculator.wdgt/images/lcd8dot.jpg
calculator.wdgt/images/lcd9.jpg
calculator.wdgt/images/lcd9dot.jpg
calculator.wdgt/images/lcde.jpg
calculator.wdgt/images/lcdminus.jpg
calculator.wdgt/images/lcdoff.jpg
calculator.wdgt/images/top.jpg
calculator.wdgt/Info.plist

Sources

Info.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>CFBundleDevelopmentRegion</key>
	<string>English</string>
	<key>CFBundleDisplayName</key>
	<string>skeleton2</string>
	<key>CFBundleIdentifier</key>
	<string>com.thiebaut.widget.Untitled</string>
	<key>CFBundleName</key>
	<string>skeleton2</string>
	<key>CFBundleShortVersionString</key>
	<string>1.0</string>
	<key>CFBundleVersion</key>
	<string>1.0</string>
	<key>CloseBoxInsetX</key>
	<integer>15</integer>
	<key>CloseBoxInsetY</key>
	<integer>15</integer>
	<key>Height</key>
	<integer>380</integer>
	<key>MainHTML</key>
	<string>calc.html</string>
	<key>Width</key>
	<integer>280</integer>
</dict>
</plist>


calc.html


<html>
<head>
  <title>Calculator Example</title>
</head>
<body topmargin=0 leftmargin=0 marginheight=0 marginwidth=0 onkeypress="javascript:keyboard(event)">

<table align=center border=0 cellpadding=0 cellspacing=0>
    <tr> <td colspan=10><img src="images/top.jpg"></td> </tr>
    <tr> 
      <td><img name="dl" src="images/display_left.jpg"></td>
      <td><img name="d7" src="images/lcdoff.jpg"></td>
      <td><img name="d6" src="images/lcdoff.jpg"></td>
      <td><img name="d5" src="images/lcdoff.jpg"></td>
      <td><img name="d4" src="images/lcdoff.jpg"></td>
      <td><img name="d3" src="images/lcdoff.jpg"></td>
      <td><img name="d2" src="images/lcdoff.jpg"></td>
      <td><img name="d1" src="images/lcdoff.jpg"></td>
      <td><img name="d0" src="images/lcd0dot.jpg"></td>
      <td><img name="dr" src="images/display_right.jpg"></td>
    </tr>
    <tr><td colspan=10><img src="images/keys.jpg" border=0 usemap="#keymap"></td></tr>
</table>

<map name="keymap">
  <area shape=rect coords=" 16, 39, 46, 68" href="javascript:key_pressed('o')" title="o"></area>
  <area shape=rect coords=" 55, 39, 85, 68" href="javascript:key_pressed('c')" title="c"></area>
  <area shape=rect coords=" 94, 39,124, 68" href="javascript:key_pressed('r')" title="r"></area>
  <area shape=rect coords="133, 39,163, 68" href="javascript:key_pressed('m')" title="m"></area>
  <area shape=rect coords="169, 39,201, 68" href="javascript:key_pressed('M')" title="M"></area>

  <area shape=rect coords=" 16, 74, 46,100" href="javascript:key_pressed('7')" title="7"></area>
  <area shape=rect coords=" 55, 74, 85,100" href="javascript:key_pressed('8')" title="8"></area>
  <area shape=rect coords=" 94, 74,124,100" href="javascript:key_pressed('9')" title="9"></area>
  <area shape=rect coords="133, 74,163,100" href="javascript:key_pressed('%')" title="%"></area>
  <area shape=rect coords="169, 74,201,100" href="javascript:key_pressed('s')" title="s"></area>

  <area shape=rect coords=" 16,112, 46,138" href="javascript:key_pressed('4')" title="4"></area>
  <area shape=rect coords=" 55,112, 85,138" href="javascript:key_pressed('5')" title="5"></area>
  <area shape=rect coords=" 94,112,124,138" href="javascript:key_pressed('6')" title="6"></area>
  <area shape=rect coords="133,112,163,138" href="javascript:key_pressed('*')" title="*"></area>
  <area shape=rect coords="169,112,201,138" href="javascript:key_pressed('/')" title="/"></area>

  <area shape=rect coords=" 16,145, 46,171" href="javascript:key_pressed('1')" title="1"></area>
  <area shape=rect coords=" 55,145, 85,171" href="javascript:key_pressed('2')" title="2"></area>
  <area shape=rect coords=" 94,145,124,171" href="javascript:key_pressed('3')" title="3"></area>
  <area shape=rect coords="169,145,201,171" href="javascript:key_pressed('-')" title="-"></area>

  <area shape=rect coords=" 16,181, 46,207" href="javascript:key_pressed('0')" title="0"></area>
  <area shape=rect coords=" 55,181, 85,207" href="javascript:key_pressed('.')" title="."></area>
  <area shape=rect coords=" 94,181,124,207" href="javascript:key_pressed('i')" title="i"></area>
  <area shape=rect coords="133,145,163,207" href="javascript:key_pressed('+')" title="+"></area>
  <area shape=rect coords="169,181,201,207" href="javascript:key_pressed('=')" title="="></area>

</map>

<script type=text/javascript>
var is_netscape = (navigator.appName=="Netscape") ? 1 : 0;

lcd0 = new Image(20, 45); lcd0.src="images/lcd0.jpg";
lcd1 = new Image(20, 45); lcd1.src="images/lcd1.jpg";
lcd2 = new Image(20, 45); lcd2.src="images/lcd2.jpg";
lcd3 = new Image(20, 45); lcd3.src="images/lcd3.jpg";
lcd4 = new Image(20, 45); lcd4.src="images/lcd4.jpg";
lcd5 = new Image(20, 45); lcd5.src="images/lcd5.jpg";
lcd6 = new Image(20, 45); lcd6.src="images/lcd6.jpg";
lcd7 = new Image(20, 45); lcd7.src="images/lcd7.jpg";
lcd8 = new Image(20, 45); lcd8.src="images/lcd8.jpg";
lcd9 = new Image(20, 45); lcd9.src="images/lcd9.jpg";

lcd0dot = new Image(20, 45); lcd0dot.src="images/lcd0dot.jpg";
lcd1dot = new Image(20, 45); lcd1dot.src="images/lcd1dot.jpg";
lcd2dot = new Image(20, 15); lcd2dot.src="images/lcd2dot.jpg";
lcd3dot = new Image(20, 45); lcd3dot.src="images/lcd3dot.jpg";
lcd4dot = new Image(20, 45); lcd4dot.src="images/lcd4dot.jpg";
lcd5dot = new Image(20, 45); lcd5dot.src="images/lcd5dot.jpg";
lcd6dot = new Image(20, 45); lcd6dot.src="images/lcd6dot.jpg";
lcd7dot = new Image(20, 45); lcd7dot.src="images/lcd7dot.jpg";
lcd8dot = new Image(20, 45); lcd8dot.src="images/lcd8dot.jpg";
lcd9dot = new Image(20, 45); lcd9dot.src="images/lcd9dot.jpg";

lcdoff   = new Image(20, 45); lcdoff.src="images/lcdoff.jpg";
lcdminus = new Image(20, 45); lcdminus.src="images/lcdminus.jpg";
lcde     = new Image(20, 45); lcde.src="images/lcde.jpg";

var x=0;
var y=0;
var m=0;

var disp="";
var is_new_num = true;
var is_decimal = false;
var last_op = "";
var error = false;

// ---------------------------------------------------------

function update_display(dspin) {

    var disp_array = new Array();
    var dot_active = false;
    var dsp=dspin;
    var lcds=0;
    var idx=dsp.length;

    if (error) {
        document.d7.src = lcdoff.src;
        document.d6.src = lcdoff.src;
        document.d5.src = lcdoff.src;
        document.d4.src = lcdoff.src;
        document.d3.src = lcdoff.src;
        document.d2.src = lcdminus.src;
        document.d1.src = lcde.src;
        document.d0.src = lcdminus.src;
        return;
    }

    if (dspin.indexOf('.') == -1) {
        dsp = dspin + '.';
        idx++;
    }

    while (lcds < 8) {
        idx--;
        digit = dsp.charAt(idx);
        if (digit == '.') 
            dot_active = true;
        else if (digit == '-') {
            disp_array[lcds] = lcdminus.src;
            lcds++;
        } else if (digit && "0123456789".indexOf(digit) != -1) {
            if (dot_active) 
                disp_array[lcds] = "images/lcd"+digit+"dot.jpg";
            else
                disp_array[lcds] = "images/lcd"+digit+".jpg";
            dot_active = false;
            lcds++;
        } else {
            disp_array[lcds] = lcdoff.src;
            lcds++;
        }
    }

    document.d7.src = disp_array[7];
    document.d6.src = disp_array[6];
    document.d5.src = disp_array[5];
    document.d4.src = disp_array[4];
    document.d3.src = disp_array[3];
    document.d2.src = disp_array[2];
    document.d1.src = disp_array[1];
    document.d0.src = disp_array[0];
}

// ---------------------------------------------------------

function rnd_to_display(val) {

    var rnd_factor;
    var result = (val < 0) ? -1 * val : val;
   
    rnd_factor = 10000000;
    while (result >= 10) {
        result = result / 10;
        rnd_factor = rnd_factor / 10;
    }
    if (val < 0)
        rnd_factor = rnd_factor / 10;
    result = (Math.round(val * rnd_factor))/rnd_factor;

    return result;
}

// ---------------------------------------------------------


function reduce (op) {

    if (op == '+')
        x = x + y;
    if (op == '-')
        x = x - y;
    if (op == '*')
        x = x * y;
    if (op == '/') {
        if (y == 0) {
            error = true;;
            return;
        }  
        x = x / y;
    }

    x = rnd_to_display(x);

    if (x < -9999999 || x > 99999999)
        error = true;

}

// ---------------------------------------------------------

function keyboard(e) {

    var ky, code;

    code = is_netscape ? e.which : e.keyCode;

    ky = String.fromCharCode(code);

    if (ky == 't')
        test();
    else
        key_pressed(ky);
}

// ---------------------------------------------------------

function key_pressed(key) {

    if (error && key != 'o')
        return;

    if ((key && ".0123456789".indexOf(key)) != -1) {

        if (key == '.') {

            if (is_new_num) {
                disp = "0.";
                is_new_num = false;
            } 
            is_decimal = true;

        } else {
            if (!is_new_num && disp.length == 9)
                return;

            else if (is_decimal) {
                disp = disp+key;
            } else {
                if (is_new_num) 
                    disp = key+'.';
                else 
                    disp = disp.substring(0, disp.length-1)+key+'.';
            }

            y = parseFloat(disp);

            is_new_num = false;
    
        }
    } else if ("*/+-".indexOf(key) != -1) {
        if (last_op != "") {
            reduce(last_op);
            disp = x.toString();
        } else 
           x = y;

        is_new_num = true;
        is_decimal = false;
        last_op = key;

    } else if ("=%".indexOf(key) != -1) {
        if (last_op != "") {
            if (key == '%')
                y = y/100;
            reduce(last_op);
            y = x;
            disp = x.toString();
        }

        is_new_num = true;
        is_decimal = false;
        last_op = "";
        
    } else {
        if (key == 's') {
            if (y < 0)
                error = true;
            else {
                y = rnd_to_display(Math.sqrt(y));
                disp = y.toString();
            }
    
            is_new_num = true;
            is_decimal = false;
            
        } else if (key == 'i') {
            y = -1 * y;
            if (y < 0) disp = "-" + disp;
            else disp = disp.substring(1, disp.length);

        } else if (key == 'c') {
            y = 0;
            disp = "0.";
            is_new_num = true;
            is_decimal = false;
    
        } else if (key == 'o') {
            x = 0;
            y = 0;
            m = 0;
            disp = "0.";
            is_new_num = true;
            is_decimal = false;
            last_op = "";
            error = false;
    
        } else if (key == 'r') {
            y = m;
            disp = y.toString();
    
            is_new_num = true;
            is_decimal = false;
    
        } else if ("mM".indexOf(key) != -1) {
            m = m + y * ((key == 'm') ? 1 : -1);
            is_new_num = true;
            is_decimal = false;
        }
    }

    update_display(disp);
}

// ---------------------------------------------------------

testdata = "o:0.@" +                                     // initialise
"15.3 + 1.89 =          : 17.19 @" +                     // Add
"15.3 - 1.89 =          : 13.41 @" +                     // Subtract
"15.3 * 1.89 =          : 28.917 @" +                    // Multiply
"15.3 / 1.89 =          : 8.0952381 @" +                 // Divide
"15.3 + 1.89 i =        : 13.41 @" +                     // change sign
"15.3 s + 1.89 =        : 5.8015214 @" +                 // square root
"15.3 * 1.89 %          : 0.28917 @" +                   // percent
"22.5 c 15.3 + 1.89 =   : 17.19 @" +                     // clear entry
"1.89 m 15.3 + r =      : 17.19 @" +                     // M+ and recall
"3.78 M 15.3 - r =      : 17.19 @" +                     // M- and recall
"55 + 99 =              : 154. @" +                      // Trailing decimal
"o:0.";                                                  // last line

// ---------------------------------------------------------
function test() {

    var answer = false;                 // State indicating reading expected answer
    var testnum = 0;                    // test number
    var failures = new Array();         // List of test failures
    var fidx = 0;                       // Failure index
    var testchar = "";                  // Current input character

    if (!confirm("Run the self tests?\n(may take some seconds)"))
        return;

    // Scan through all test data ...
    for (tidx = 0; tidx < testdata.length; tidx = tidx + 1) {

        // get next input character
        testchar = testdata.charAt(tidx);

        // Non white-space input
        if (testchar != ' ') {
            // If answer delimiter, clear expected answer string and set state
            if (testchar == ':') {
                answer = true;
                expected = "";
            }
            // If test delimiter, check answer
            else if (testchar == '@') {
                // Clear state
                answer = false;

                // Get calculator's result
                result = disp;

                // Check result matches expected, and log if not
                if (parseFloat(expected) != parseFloat(result)) 
                    failures[fidx++] = testnum;
 
                testnum++;
            }
            // Whilst in answer state, append input to expected variable
            else if (answer == true)
                expected = expected + testchar;
            // Input key sequences
            else {
                // Abort if an invalid character found in the test data
                if ("0123456789.+-*/=%simMroc".indexOf(testchar) == -1) {
                    alert("Invalid input character ("+testchar+") in test sequence\n"+
                          "at line "+testnum);
                    return;
                }

                key_pressed(testchar);
            }
        }
    }

    // Clear testchar and reuse for failure message
    testchar = "";

    // Add all failure test numbers to string
    for (tidx = 0; tidx < fidx; tidx++) 
        testchar = testchar + " " + failures[tidx];

    // Display test results
    if (fidx > 0)
        alert (fidx+" failures at lines:"+testchar);
    else
        alert ("All "+ (testnum-1) + " tests pass");

}
</script>

</body>
</html>

Installation

  • Make sure the widget files are stored in a directory ending in wdgt. Here we used calculator.wdgt.
  • In iBook Author, insert a widget, and pick HTML.
  • Select the new widget and position it where you want in your iBook page.
  • In the Inspector window, choose the folder calculator.wdgt and the Default image (Default.png) should appear in the widget.
  • Connect your iPad to the Mac on which you are creating your iBook and preview the iBook. Verify that the widget works.


IPadScreenCaptureCalculatorSimulator.png



IPadScreenCaptureCalculatorSimulatorRunning.png


Misc

PDF Copies of Southwell's article can be found here: Introduction, Part 1, Part 2, Part 3, Part 4, Part 5, and Appendix.


References

  1. How to Write a Calculator Simulator, Simon Southwell, http://www.anita-simulators.org.uk/calc/calc_example/article2_front.htm, published March 2004, captured Jan. 2013.
  2. iBooks Author: About HTML widget creation, Apple, http://support.apple.com/kb/HT5068, Oct. 2012, captured Jan 2013.