Drawing on browser canvas with Lua

As I was working on my turtle library that runs on a desktop, I thought it would be useful to have a way to execute the same Lua code in a browser (as a way to demonstrate/share the result). There are several options to get Lua to run in a browser, but most of them can be placed in one of two categories: re-writing Lua VM in JavaScript (like emscripten) and translating Lua into JavaScript (like lua.js). There is also an option of writing a Lua VM in some other language, but this would require a plugin to be installed, with all associated hassles. Lua VM approach provides an option of full compatibility (as you would be running a regular Lua VM, just implemented in JavaScript), but tends to be slow. Translation to Lua gives you more speed (as the browser executes the JavaScript code of your application rather than VM) at the cost of not having Lua features that don't exist in JavaScript (like non-string keys in tables, coroutines, and some other things). As I was looking for a way to run simple scripts that work with graphics, the speed was more important than a full set of Lua features, so I decided to try lua.js.

I wanted to be able to run a simple Lua script that I directly mapped from a JavaScript sample:

 local canvas = document:getElementById("canvas");
 local ctx = canvas:getContext("2d");

 ctx.fillStyle = "rgb(200,0,0)";
 ctx:fillRect(10, 10, 55, 50);

 ctx.fillStyle = "rgba(0, 0, 200, 0.5)";
 ctx:fillRect(30, 30, 55, 50);

It took only 5 minutes to get a simple script to execute (as in get-to-the-first-line), but I immediately run into a problem not knowing how to get document.getElementById("canvas") working correctly. The problem was that lua.js compiled this to a code that was trying to find document in its global table, where it obviously didn't exist. This was easy to fix: I added lua_tableset(lua_script, "document", document); to my javascript code to set the document key to reference the real document table.

The next issue was that the code was trying (reasonably enough) to get getElementById from document using Lua methods, and it wasn't working as it was searching in pseudo tables that lua.js setup, rather than in the actual document table. To fix this, I added a check to lua_rawget method to work with "native" values:

function lua_rawget(table, key) {
 if (typeof table == "object" && table[key]) {
   return table[key];
 }

The next problem was more interesting. The code correctly found the right method to execute, but failed to execute it with infamous "Could not convert JavaScript argument" nsresult: "0x80570009 (NS_ERROR_XPC_BAD_CONVERT_JS)" error. The interesting part was that after func = document.getElementById, document.getElementById("canvas") was working, while func("canvas") triggered that error. Even func.apply(null, ["canvas"]), which was the code that lua.js generated, produced the same result. It took a bit of googling and reading to realize what was going on. It turned out that document.getElementById call was passing document as the scope for the function, which made it all work. After I did the same thing with func.apply(document, ["canvas"]), this call started to work.

The next thing it failed on was the second call, canvas:getContext("2d");. The error was different this time -- "Illegal operation on WrappedNative prototype object" nsresult: "0x8057000c (NS_ERROR_XPC_BAD_OP_ON_WN_PROTO)" -- but now I was well equipped to deal with it. In this case it expected a different scope (canvas element, rather than document), so I did a proper fix to provide that context. I changed lua_rawcall to take on scope parameter, and changed lua_mcall to pass that parameter when it detected a "native" call:

function lua_rawcall(func, args, scope) {
 try {
   return func.apply(scope, args);
 } catch (e) {
function lua_mcall(obj, methodname, args) {
 if (typeof obj == "object" && typeof obj[methodname] == "function") {
   //  this is a JavaScript call, like document:getElementById
   return [lua_rawcall(obj[methodname], args, obj)];
 }
 else {
   return lua_call(lua_tableget(obj, methodname), [obj].concat(args));
 }
}

The next issue was to get ctx.fillStyle = "rgb(200,0,0)" to work as lua.js was trying to set ctx.fillStyle in Lua tables, whereas I needed it to be set in JavaScript. I modified lua_rawset function in the same way I modified lua_rawget:

function lua_rawset(table, key, value) {
 if (typeof table == "object" && table[key]) {
   table[key] = value;
   return;
 }

The last issue was that all Lua calls return a table (to allow multiple parameters to be returned), but native JS calls expect simple result; I modified the returned result in lua_mcall to be an array return [...]; and things start to fly.

One last tweak I did was that instead of putting lua_tableset(lua_script, "document", document); in the JavaScript code manually, I added this assignment to the code that lua2js and lua+parser.js generate (at the end of case 1: output):

      "G.str['document'] = document;\n" +
      "G.str['window'] = window;\n" +

You can play with the result here: drawing on browser canvas with Lua demo. Kudos to Maximilian Herkender for doing all the work that enabled this.

You should get a copy of my slick ZeroBrane Studio IDE and follow me on twitter here.

2 Comments

You should try you test with LUAJIT over at http://luajit.org/ It runs much faster than the interpreted lua, at least I'm 80% sure...

Note: You May Have To Compile LUAJIT from source, I know I had to.

Joseph, thanks for the note; compiling with LuaJIT won't work in this case as the compilation happens in the browser and the code is compiled to JavaScript.

Leave a comment

what will you say?
(required)
(required)

About

I am Paul Kulchenko.
I live in Kirkland, WA with my wife and three kids.
I do consulting as a software developer.
I study robotics and artificial intelligence.
I write books and open-source software.
I teach introductory computer science.
I develop a slick Lua IDE and debugger.

Recommended

Close