The NW.js project lets you create apps with full native capabilities in HTML, CSS and JavaScript. Even better, it lets you access all Node.js modules directly from the browser environment. This means your web-platform-based application can now retrieve input from sensors or control hardware devices. This is the perfect environment for physical computing projects!

About NW.js

NW.js (previously know as node-webkit) “lets you call all Node.js modules directly from DOM and enables a new way of writing applications with all Web technologies”. Basically, NW.js lets you create native-looking desktop apps (Mac, Windows & Linux) that can be written entirely using HTML, CSS and JavaScript. Because it is based on Google Chrome’s V8 engine and Node.js, you can use regular browser/DOM JavaScript code enhanced with any Node.js-compatible modules. What is so great about Node.js modules, at least from a physical computing standpoint, is that they provides access to external sensing and actuating devices. For instance, there are modules to talk to MIDI devices, connect to microcontrollers, use all sorts of sensors, control robots and drones, etc. Furthermore, you get full access to the local file system and native windowing without any of the security hassles usually imposed by the browser’s security sandbox. This is pretty amazing stuff.

Coding

For the sake of the demonstration, we will be coding a simple app that draws a big red dot on screen. Moving the mouse’s cursor up shrinks the red dot, moving it down enlarges it. It is a super simple project but it will allow us to go through all the basic steps. First, let’s create a folder for our project. In that folder, we need to create an HTML page that will be the entry point into the app. Let’s create one with a single fullscreen canvas element:

<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title>Using NW.js for Physical Computing Projects</title> <style> html, body { width: 100%; height: 100%; margin: 0; } </style> <script src="script.js"></script> </head> <body> <canvas id="canvas"></canvas> </body> </html> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 < ! DOCTYPE html > < html > < head lang = "en" > < meta charset = "UTF-8" > < title > Using NW . js for Physical Computing Projects < / title > <style> html, body { width : 100% ; height : 100% ; margin : 0 ; } </style> <script src = "script.js" > </script> < / head > < body > < canvas id = "canvas" > < / canvas > < / body > < / html >

Then let’s write some JavaScript code in the linked script.js file to draw our red dot on screen (and control its size):

window.onload = function() { // Retrieve canvas element and set it to occupy the whole window var canvas = document.getElementById("canvas"); canvas.width = window.innerWidth; canvas.height = window.innerHeight; // Retrieve the 2d context which we will use to draw our big red dot var context = canvas.getContext("2d"); context.fillStyle = "red"; // The zoom factor which will be affected by the mouse position var zoom = 1; // Going up zooms out, going down zooms in canvas.addEventListener('mousemove', function(e) { zoom = e.clientY / canvas.height; }); // Request an initial update of the canvas requestAnimationFrame(update); // Draw the red dot with a size proportional to the zoom factor function update() { context.clearRect(0, 0, canvas.width, canvas.height); context.beginPath(); context.arc( canvas.width/2, canvas.height/2, canvas.height * zoom, 0, 2 * Math.PI ); context.fill(); requestAnimationFrame(update); } }; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 window . onload = function ( ) { // Retrieve canvas element and set it to occupy the whole window var canvas = document . getElementById ( "canvas" ) ; canvas . width = window . innerWidth ; canvas . height = window . innerHeight ; // Retrieve the 2d context which we will use to draw our big red dot var context = canvas . getContext ( "2d" ) ; context . fillStyle = "red" ; // The zoom factor which will be affected by the mouse position var zoom = 1 ; // Going up zooms out, going down zooms in canvas . addEventListener ( 'mousemove' , function ( e ) { zoom = e . clientY / canvas . height ; } ) ; // Request an initial update of the canvas requestAnimationFrame ( update ) ; // Draw the red dot with a size proportional to the zoom factor function update ( ) { context . clearRect ( 0 , 0 , canvas . width , canvas . height ) ; context . beginPath ( ) ; context . arc ( canvas . width / 2 , canvas . height / 2 , canvas . height * zoom , 0 , 2 * Math . PI ) ; context . fill ( ) ; requestAnimationFrame ( update ) ; } } ;

Testing

Once our page and script are ready, we must add a package.json manifest file to configure how NW.js will display our app:

{ "main": "index.html", "name": "nw-demo", "description": "NW.js Demo App Description", "version": "0.1.0", "window": { "title": "NW.js Demo App", "icon": "link.png", "frame": false, "width": 800, "height": 500, "position": "mouse", "min_width": 400, "min_height": 200, "max_width": 800, "max_height": 600, "fullscreen": false }, "webkit": { "plugin": true } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 { "main" : "index.html" , "name" : "nw-demo" , "description" : "NW.js Demo App Description" , "version" : "0.1.0" , "window" : { "title" : "NW.js Demo App" , "icon" : "link.png" , "frame" : false , "width" : 800 , "height" : 500 , "position" : "mouse" , "min_width" : 400 , "min_height" : 200 , "max_width" : 800 , "max_height" : 600 , "fullscreen" : false } , "webkit" : { "plugin" : true } }

The options listed above are pretty self-explanatory. There are many more if you need specialized behaviour but only the first two ( main and name) are needed. Obviously, the “main” directive must point to your primary HTML page. If you want a fullscreen window, as is often the case with physical computing projects, simply set "fullscreen": true in the “window” section. You also have the option to enter kiosk mode through the package.json file with "kiosk": true.

Once configuration is done, we are left with a project folder containing three files:

index.html

script.js

package.json

To be able to easily launch our application we are only missing one thing: the nw executable files. Download them for your platform. Once downloaded, you need to copy the necessary files within your project folder. On Mac OS X, simply copy the nwjs.app file to the project folder. On Linux and Windows, copy all files extracted from the downloaded package to the project folder. Once this is done, if you are on Mac or Windows, you simply need to double-click the executable ( nwjs.app or nw.exe) to start your application. If you are on Linux, run nw from the command line. That’s it!

There are more details on how to manually run and package apps for distribution on the GitHub wiki.

Debugging

To debug the application, you can open Chromium’s Developer Tools by clicking on the icon in the top right of the window (version 0.12 and below) or pressing the F12 key on Windows/Linux or CMD-ALT-I on Mac (version 0.13 and above): Alternatively, it is possible to programmatically open the developer tools. Here’s how to do it in version 0.12 an below:

require('nw.gui').Window.get().showDevTools() 1 require ( 'nw.gui' ) . Window . get ( ) . showDevTools ( )

For version 0.13 an above:

nw.Window.get().showDevTools() 1 nw . Window . get ( ) . showDevTools ( )

Windows & Screens

NW.js will let you create native chrome or chromeless-windows very easily. NW.js’ window object extends EventEmitter. This means you can use Window.on() to listen for native events. Here are a few things you can do:

// Get the active window (version 0.12) var gui = require('nw.gui'); var w = gui.Window.get(); // Get the active window (version 0.13) //var w = nw.Window.get() // Listen to the minimize event w.on('minimize', function() { console.log('Window has been minimized!'); }); // Minimize the window w.minimize(); // Create a new window and listen for the focus event (version 0.12) var w2 = gui.Window.open('http://tangiblejs.com', {width: 500, height: 400}); // Create a new window and listen for the focus event (version 0.13) //var w2 = nw.Window.open('http://tangiblsjs.com', {width: 500, height: 400}); w2.on('focus', function() { console.log('Focus on new window!'); }); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // Get the active window (version 0.12) var gui = require ( 'nw.gui' ) ; var w = gui . Window . get ( ) ; // Get the active window (version 0.13) //var w = nw.Window.get() // Listen to the minimize event w . on ( 'minimize' , function ( ) { console . log ( 'Window has been minimized!' ) ; } ) ; // Minimize the window w . minimize ( ) ; // Create a new window and listen for the focus event (version 0.12) var w2 = gui . Window . open ( 'http://tangiblejs.com' , { width : 500 , height : 400 } ) ; // Create a new window and listen for the focus event (version 0.13) //var w2 = nw.Window.open('http://tangiblsjs.com', {width: 500, height: 400}); w2 . on ( 'focus' , function ( ) { console . log ( 'Focus on new window!' ) ; } ) ;

The Window.open() method accepts, as a second parameter, an object containing configuration options. Any option that can be used in the manifest file’s ( package.json) window property can be used here. You can get all details about using windows here. Something that’s very common in physical computing is to have multiple video outputs. The Screen API provides information about all available screens. This makes it possible to position windows to match the actual video output configuration. The Screen object is a singleton that needs to be initialized. Once initialized, you can retrieve an array of all screens through its screens property:

// version 0.12 var gui = require("nw.gui"); gui.Screen.Init(); // version 0.13 nw.Screen.Init(); var screens = gui.Screen.screens; // version 0.12 var screens = nw.Screen.screens; // version 0.13 1 2 3 4 5 6 7 8 9 // version 0.12 var gui = require ( "nw.gui" ) ; gui . Screen . Init ( ) ; // version 0.13 nw . Screen . Init ( ) ; var screens = gui . Screen . screens ; // version 0.12 var screens = nw . Screen . screens ; // version 0.13

Each screen in the array has the following structure and information:

screen { id : int, bounds : { x : int, y : int, width : int, height : int }, work_area : { x : int, y : int, width : int, height : int }, scaleFactor : float, isBuiltIn : bool, rotation : int, touchSupport : int } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 screen { id : int , bounds : { x : int , y : int , width : int , height : int } , work_area : { x : int , y : int , width : int , height : int } , scaleFactor : float , isBuiltIn : bool , rotation : int , touchSupport : int }

The bounds represent the physical screen resolution. The positions do not necessarily start at 0 and can be negative depending on the screen arrangement. The Screen object also extends EventEmitter which means it dispatches events ( displayAdded, displayRemoved, displayBoundsChanged ) that can be listened to with the gui.Screen.on() method.

And more…

All of that is already pretty amazing and we have not even mentioned the built-in or third-party Node modules. Just to give you an idea, the built-in modules cover areas such as:

filesystem access

sockets

process management

cryptography

etc.

But, perhaps, even more exciting are the countless third-party modules. There are so many that it makes your head spin. Here are a few examples of particular interest to those who foray into the physical computing world:

cylon : a framework to interact with robots, drones, and various devices such as the LeapMotion controller, Skynet communication network, Tessel microcontroller, Nest thermostat, Neurosky biosensor, Pebble watch, etc;

: a framework to interact with robots, drones, and various devices such as the LeapMotion controller, Skynet communication network, Tessel microcontroller, Nest thermostat, Neurosky biosensor, Pebble watch, etc; opencv : a set of bindings to the very capable open source computer vision library;

: a set of bindings to the very capable open source computer vision library; serialport : a library to work with the serial port;

: a library to work with the serial port; johnny-five : a hardware programming framework that can be used with Arduino, Electric Imp, Beagle Bone, Intel Galileo & Edison, Linino One, Pinoccio, Raspberry Pi, Spark Core, TI Launchpad and more;

: a hardware programming framework that can be used with Arduino, Electric Imp, Beagle Bone, Intel Galileo & Edison, Linino One, Pinoccio, Raspberry Pi, Spark Core, TI Launchpad and more; webmidi : an easier way to use the Web MIDI API to send and receive MIDI commands;

: an easier way to use the Web MIDI API to send and receive MIDI commands; tuio : support for the TUIO protocol particularly used with multitouch and tactile interaction devices;

: support for the TUIO protocol particularly used with multitouch and tactile interaction devices; osc : support for the Open Sound Control protocol very common in musical software and instruments;

: support for the Open Sound Control protocol very common in musical software and instruments; phidgets : support for Phidgets interface boards which can be used to connect various sensors and actuators.

… and about a gazillion more!

Conclusion

As you can probably tell by now, most of the fun with NW.js comes from the fact that it lets you combine the power of browser libraries and Node.js modules. This means you could, for instance, gather sensor input with the phidgets Node module and dynamically generate 3D objects with Three.js. Or, you could gather geographical data through the Google Maps API and fly an ARDrone quadcopter with cylon.js. Crazy stuff…

If you are curious to see know how to do the same with Electron, Esdras Lopez ported the code in this tutorial to Electron. Check it out on GitHub.

Downloads

If you want, you can download the full source code (for NW.js version 0.12) of the above example as a zip package.