MorphEngine v1.1 JavaScript API:
Concepts
ND1 supports user functions and full-scale extensions written in JavaScript.
You can write JavaScript user functions in many ways:
-using pure JavaScript, using no API or framework whatsoever (see the User Functions tutorial)
-using the client-side framework that you know from web-browsers (see the User Functions tutorial)
-against ND’s MorphEngine JavaScript API present in ND1 (and you can use that API on varying levels of abstraction. From a one-call API, to a fine-grained one.)
You can write extensions in JavaScript
-using your favorite JavaScript framework
(these
jquery1.3.2
mootools1.2.2
prototype1.6.0.2
mochikit 1.4.2
canvasgraph 0.7
are all present, and others can be permanently pulled in via Asset Localization)
-using, or not using, the MorphEngine API (but probably tying into it to some degree to communicate results or access user data)
There’s a mechanism for you to inject your JavaScript code when ND1 initializes. You can plug into ND1 on many levels. You can add functionality, add to existing functionality, overwrite existing functionality. There’re no limits. You can take over, and present the user with something completely different than ND1 (not that we would recommend that...).
Getting to user data, adding a permanent function (that lives outside a user folder), extending an built-in function to support a brand-new data type, writing a new chart grapher that uses ND1 statistical data, etc. are all very easily accomplished.
This document discusses the MorphEngine on its various levels so you know where to plug in, and how.
If you’re interested in your own data types, see Custom Data Types, a section of the reference that can be read independently of this one.
Overview
(c) 2010 Naive Design. All rights reserved.
The calculator object
Here’s the simplified layout of calculator:
var calculator = {
version: 1.0,
stack: [],
lastArgs: [],
...
vars: {
"version": '"1.1"',
...
},
functions: {
"+":function(x, y) { return x+y; },
...
string: {
"fromString": function(x) { ... },
"toString": function(x) { ... },
"+": function(x, y) { ... },
...
},
binary: {
"+": function(x, y) { ... },
...
},
complex: {
"+": function(c1, c2) { ... },
...
},
vector: {
"fromString": function(x) { ... },
"toString": function(arr) { ... },
"+": function(x, y) {
x = this.fromString(x);
y = this.fromString(y);
if (x.length != y.length)
throw Error("incompatible dimensions");
for (var i=0; i<x.length; i++)
if (typeof x[i] == "number" && typeof y[i] == "number")
x[i] = x[i] + y[i];
else
x[i] = calculator.calc(x[i], y[i], "+");
return this.toString(x);
},
...
},
matrix: {
...
},
"@eval": function(x) {
}
}
...
// call a function/operator that returns one result and takes a variable # of args
calc: function() { ... },
// eval a mathematical expression, which can contain user vars and functions, complex, vector, matrix, etc math
eval: function(expression) { ... },
}
};
User data is stored in a sqlite database and managed with CoreDate in the app. A mechanism synchronizes this data with the JavaScript VM.
In JavaScript, then, accessing user data is an absolute breeze:
Just pick it from the calculator.vars object. There’s a straight mapping to the data as presented in the UI.
An example will suffice to explain this: imagine the user has a statistics datum “∑DAT” with a matrix as its contents in the “My Vars” folder.
This data can be accessed via calculator.vars[“My Vars”][“∑DAT”]. Simple as that.
Before we look at the internals of user functions, let’s discuss the possible scenarios surrounding them.
Two Objects
The display object handles stack and graphics display. (ND1 is a mixed Cocoa/JavaScript app. But everything the calculator displays is in JavaScript.)
In graphics mode, a HTML5 canvas object is employed. You can easily gain access to it and do with it whatever you want. You could run your own HTML5 game, for example, independent of the calculator. (This is what “scribble” does.) Normally, of course, you’d want to tie into user data and/or the calculation abilities in ND1. See the chart graphing example below.
The calculator object provides access to data and math. Accessing user data–current “folder” and beyond–is incredibly easy. Accessing the math in ND1 is on 3 levels with varying granularity, and easy too.
The calculator object also represents the framework into which you can plug in functions and custom data types.
When you write a user function, you can stay entirely in the pure JavaScript world and be literally unconcerned with the environment you’re in. MorphEngine figures how many arguments the function takes and whether it returns a value or not. And does the right things when it comes to evaluating an expression, moving data around, and managing the stack.
Example:
Name: mySine
Definition: function(x) { return Math.sin(x); }
This function simply takes an input value and returns one. It will function in the calculator just fine for real-valued inputs. It will be a first class citizen in every way, and will work in direct stack manipulation, expressions, RPL programs, through its name “mySine”, and thanks to MorphEngine managing it.
If you want your function to work with complex numbers, vectors, matrices, strings, though, you have to account for these types and do the appropriate thing. Since there’re no provisions in JS for complex numbers, for example, you’d have to know how complex numbers are internally represented and how to operate on them.
This is where the API comes in. Rather than you re-inventing the wheel, you use the functions that are already there. See scenario #3.
There’s one more concern: user functions “live” in the current folder. This is a desired property (and there’s mechanism for users to refer to functions (and data) across folders) but maybe you want your specific function to work from everywhere, just like a built-in function. For that, too, you need to inject a line of API code. (We’re coming to that.)
User function scenario #1: written in pure JavaScript
You might be writing a user function, just like in scenario #1 and also use some client-side API. You are inside a web view and the normal client-side JavaScript world is there for you. Better yet, you can load a JS framework, and use it like you normally would.
Example:
Name: mySine2
Definition: function(x) { var ret = Math.sin(x); alert(“I’m going to return “ + ret); return ret; }
While this would cause consternation to users of your function, you can pop up a dialog with alert(), whenever your function is being called. (You could legitimately use this for debugging.)
We’ll get to using other frameworks in a moment.
User function scenario #2: using some client-side API
Now let’s consider the case where you want to extend your function to work with a different type than Number. Let’s say mySine should work with complex numbers, too.
Here’s the function extended to use MorphEngine API calls which help with complex numbers.
Example:
Name: mySine3
Definition: function(c) { return (typeof x == “number”) ? Math.sin(x) : calculator.functions.complex["/"](this["-"](calculator.functions.complex(calculator.functions.complex["*"]("(0,1)", c)), this.exp(this["*"]("(0,-1)", c))), "(0,2)"); },
Complex number functions are located in calculator.functions.complex and here, we just leveraged a few of them.
You had to find the data type of your input, and call the right code.
We can do better (cleaner) than that, though.
User function scenario #3: using MorphEngine API
Here’s the full code for the line chart function (line∑), from the Stat menu, written as an injection:
calculator.functions[“stat_draw_lineChart”] = function() { var data = calculator.functions.stat_data(); var cols = calculator.functions["stat_get_cols"](); display.showGraphics(true); calculator.drawLineChart(data, cols[0]-1, cols[1]-1); };
calculator.functions.drawLineChart: function(data, xcol, ycol) {
var graph = new CanvasGraph("canvas");
graph.setDataset("data", data);
graph.padding = {top: 2, left: 14, bottom: 8, right: 2};
graph.fontSize = 10;
graph.labelWidth = 40;
graph.xOriginIsZero = false;
graph.drawLinePlot({"data": Color.blueColor() });
};
It is broken into two functions to clearly have “glue” code separated from the context-free implementation.
The first function provides using MorphEngine code to obtain the current statistics data (contents of ∑DAT, if available, as a JavaScript array), and the current pair of associated columns; it turns on graphics mode, and calls the second function, which is just normal code using CanvasGraph, having no connection to MorphEngine.
Charting example:
It’s time to pick up our thread now, and come to where, exactly, your functions are placed within calculator.
When using the database UI, they’re placed into the object at calculator.functions[folderName].
So, if there’s a JavaScript function in the My Vars folder named “helloWorld”, it would go into calculator.functions[“My Vars”][“helloWorld”].
Your functions will be callable from within the current folder. For a user to call your function from a different folder, they will have to employ the “.” operator. For example: Funcs.mySine on the edit line or in a RPL program, would locate mySine in the Funcs folder. (One caveat: Your folder name cannot contains spaces.)
Your function will be called for any kind of object(s). That is, if your function declares arguments, it will be given them, off the stack, no matter what kind of objects these may be. It’s your function’s responsibility to execute the appropriate code and/or raise an exception. You can call on MorphEngine calls, documented in the functions list, to help you with non-real types.
If you cannot handle a given kind of object, you can throw an Error, which will be presented to the user as an error dialog. For example: function(x) { if (typeof x != “number”) throw Error(“bad argument”); ... }
There’s a second option available to you for placement of functions. You can place your function in the same place where built-in functions go and thereby gain two benefits:
-your function will be become callable from any folder without dot syntax
-your function will be called type-appropriately
The last benefit comes from you placing your function(s) in the type-appropriate location(s). If your function supports multiple types, you define multiple instances of it (one per type) in multiple locations within calculator.functions.
Back to our example, you’d place the real-valued implementation of mySine into calculator.functions, and you’d place the complex-valued implementation of mySine into calculator.functions.complex.
Your implementation now living side-by-side with built-in functions implies that calls to them get simplified.
Here’d be the complex-valued implementation of mySine:
function(c) { return this["/"](this["-"](this.exp(this["*"]("(0,1)", c)), this.exp(this["*"]("(0,-1)", c))), "(0,2)"); },
(this is now calculator.functions.complex)
If the user attempts to invoke your function on an unsupported type of input, the framework will produce an error for you. So, you don’t have to worry about this anymore.
You change the placement of your functions, from the default, via JavaScript Injection.
Placement of your function
See Settings how to enable JavaScript Injection.
Once enabled, JS Injection allows you to specify, on the Definition page, arbitrary JavaScript that will be run whenever the calculator object initializes. (That happens whenever you switch to the calculator page.)
Among the things you can do, is change the placement of your user functions.
For example, to move your implementation of mySine into the place for built-in functions, you’d do
calculator.functions.complex.mySine = calculator.functions[“My Vars”][“mySine”];
Or, you simply define your entire function here. There’s no need to take the detour via the database.
calculator.functions.complex.mySine = function(c) { return this["/"](this["-"](this.exp(this["*"]("(0,1)", c)), this.exp(this["*"]("(0,-1)", c))), "(0,2)"); },
You can write all your JavaScript code in this one place and have a customized version of ND1, whose database has not been touched.
As part of your injection you can also load the JS framework of your choice. Simply do
<script language="JavaScript" type="text/javascript" src="myJSFramework.js"></script>
where myJSFramework is any of jquery, mtools, prototype. (Version numbers on these, as per first paragraph.)
You can bring in your own framework, or other JS code. Either by
-specifying a remote URL (it should get cached)
or, if you want to be sure it’s available locally (cached) and when no network should be required
-list it as an asset (an item with a URL as definition) in a database folder and use Sharing | Download Assets to download it (see My Skin as an example of this mechanism); then, refer to it just like a provided framework
JavaScript Injection
Apart from adding your own functions, appropriate for each object type, into MorphEngine, the placement options allow you to extend built-in functions. The following extension scenarios emerge:
You can
-add object support for an existing function
There’s a real-valued version of the hyperbolic cosecant (“csch”), but no complex one in MorphEngine 1.0.
You could provide one by injecting a function
calculator.functions.complex.csch = function(x) { .... };
The framework will recognize the availability of the complex-valued function, and the object type would simply start to work (instead of throwing a user error, as now).
-override a built-in function
You can replace a built-in function with yours.
For example,
calculator.functions.sin = function(x) { .... /* your implementation */ };
- extend a built-in function
The factorial function (“factorial”) does not support fractional input in MorphEngine 1.0 (it only works with integers).
If you had a gamma function, you could inject this:
calculator.functions.gamma = function(x) { .... /* your implementation */ };
calculator.functions.factorial_int = calculator.functions.factorial;
calculator.functions.factorial = function(x) { return (this.int(x) == x) this.factorial_int(x) : this.gamma(x)); };
Extending built-in functions
If you put access to user data, usage of arbitrary JS API, and placement into built-in locations together, you can add very powerful new capabilities, that are indistinguishable from built-in ones.
Consider the following example.
Putting placement, data access, and arbitrary code together
The Charting example uses the display object to switch the calculator into graphics mode (via showGraphics(true)).
Doing so replaces the stack with an HTML5 canvas object, which is available through its id “canvas”.
In the example, a CanvasGraph object is constructed with this id.
You can you anything you want with the canvas object. You can add your own even listeners and take care of user interaction.
On a higher level than JavaScript (and web view), two built-in handlers are active: double-tap will switch to fullscreen mode, and pressing the Space or Backspace keys will cancel graphics mode and return to stack display.
When the switch to fullscreen mode occurs, a window.resize event occurs which has the default implementation of calling the function that called display.setGraphics(true). This is designed to trigger an automatic redraw. Make sure your calling function is re-entrant in this sense, or overwrite window.onresize.
The display object has more functions available that are documented elsewhere.
Most notably, it serves to provide the formatting for display of stack items, which you can also customize.
The display object
One of the most powerful concepts in ND1 is the ability to have any kind of object on the stack. This permits you to extend MorphEngine with object types.
A user function
function() { return { “a”: 1, “b”: 2 }; }
would place a custom object with fields “a” and “b” on the stack, for example.
Another user function
function(x) { return alert(“a is: “ + x.a); }
could pick it up and operate on it appropriately.
This permits very powerful usage scenarios. There’s no size limitation on stack objects.
By default, unknown objects are represented as [Object object] on the stack.
You can change this and provide any kind of HTML presentation by extending display.stringForItem(item) using the “extend a built-in function” technique from above.
Following the example,
display.stringForMyObject = function(x) { .... /* your implementation, returning HTML for x */ };
display.stringForBuiltInItem = display.stringForItem;
display.stringForItem = function(x) { return (typeof x == “object” && a in x && b in x) this.stringForMyObject(x) : this.stringForBuiltInItem(x); };
Arbitrary stack objects
Besides of using individual math functions, you can use the calculator object to do higher-level computations:
Use calculator.calc execute any operator on any kinds of input objects.
As shown in the code for calculator.functions.vectors[“+”] above, calculator.calc(arg1, arg2, “+”) will add any two objects that can be added in MorphEngine. The function is a var-arg function: it will take as many arguments, as there’re required for a given operator, with the operator, in string form, always coming last.
User functions, injected or not, are also accessible through this mechanism.
Use calculator.eval to evaluate any mathematical expression MorphEngine can compute.
For example, you could do calculator.eval(“sin(x)+5!+2^3+det([[‘4-y’,2],[2,3]])”).
An expression on this highest level can refer to built-in and user functions and variables and literal syntax.
Using calculator to do math
More details and examples are forthcoming (watch the forums), along with a list of all API calls in calculator and display.
In the meantime, we hope you have sufficient information to extend ND1 for your purposes.
If you’re stuck, need more information, or would like to communicate about your ideas, don’t hesitate to contact us at the support email address.
Where to go for more information?