diff --git a/examples/Node.js/AnimatedStar.js b/examples/Node.js/AnimatedStar.js
new file mode 100644
index 00000000..9aebea2b
--- /dev/null
+++ b/examples/Node.js/AnimatedStar.js
@@ -0,0 +1,49 @@
+var paper = require('paper');
+paper.setup(new paper.Canvas(1024, 768));
+
+var layer = paper.project.activeLayer;
+
+var values = {
+	count: 34,
+	points: 32
+};
+
+initialize();
+
+paper.view.exportFrames({
+	amount: 100,
+	directory: __dirname,
+	onComplete: function() {
+		console.log('Done exporting.');
+	},
+	onProgress: function(event) {
+		console.log(event.percentage + '% complete, frame took: ' + event.delta);
+	}
+});
+
+function initialize() {
+	for (var i = 0; i < values.count; i++) {
+		var offset = new paper.Point(20 + 10 * i, 0);
+		var path = new paper.Path();
+		path.fillColor = i % 2 ? 'red' : 'black';
+		path.closed = true;
+
+		var l = offset.length;
+		for (var j = 0; j < values.points * 2; j++) {
+			offset.angle += 360 / values.points;
+			var vector = offset.normalize(l * (j % 2 ? 0.1 : -0.1));
+			path.add(offset.add(vector));
+		}
+		path.smooth();
+		layer.insertChild(0, path);
+	}
+	layer.fitBounds(paper.view.bounds);
+}
+
+paper.view.onFrame = function(event) {
+	for (var i = 0, l = layer.children.length; i < l; i++) {
+		var item = layer.children[i];
+		var angle = (values.count - i) * Math.sin(event.count / 128) / 10;
+		item.rotate(angle);
+	}
+}
diff --git a/examples/Node.js/Tadpoles.pjs b/examples/Node.js/Tadpoles.pjs
new file mode 100644
index 00000000..a4d04965
--- /dev/null
+++ b/examples/Node.js/Tadpoles.pjs
@@ -0,0 +1,280 @@
+paper.setup(new Canvas(1024, 768));
+
+// Adapted from Flocking Processing example by Daniel Schiffman:
+// http://processing.org/learning/topics/flocking.html
+
+project.currentStyle = {
+	strokeColor: 'white',
+	strokeWidth: 2,
+	strokeCap: 'round'
+};
+
+new Path.Rectangle(view.bounds).fillColor = 'black';
+
+var head = new Path.Oval([0, 0], [13, 8]);
+head.fillColor = 'white';
+head.strokeColor = null;
+var headSymbol = new Symbol(head);
+
+var size = view.size;
+
+var Boid = Base.extend({
+	initialize: function(position, maxSpeed, maxForce) {
+		var strength = Math.random() * 0.5;
+		this.acc = new Point(0, 0);
+		this.vel = Point.random() * 2 - 1;
+		this.loc = position.clone();
+		this.r = 30;
+		this.maxSpeed = maxSpeed + strength;
+		this.maxForce = maxForce + strength;
+		this.head = headSymbol.place();
+		this.path = new Path();
+		this.shortPath = new Path();
+		this.shortPath.strokeWidth = 4;
+		for (var i = 0, l = strength * 10 + 10; i < l; i++) {
+			this.path.add(this.loc);
+			if (i < 3)
+				this.shortPath.add(this.loc);
+		}
+
+		this.firstSegment = this.path.segments[0];
+		this.count = 0;
+		this.lastRot = 0;
+	},
+
+	run: function(boids) {
+		this.lastLoc = this.loc.clone();
+		if (!groupTogether) {
+			this.flock(boids);
+		} else {
+			this.align(boids);
+		}
+		this.borders();
+
+		this.update();
+		this.firstSegment.point = this.loc;
+		var lastPoint = this.firstSegment.point;
+		var lastVector = this.loc - this.lastLoc;
+		var segments = this.path.segments;
+		for (var i = 1, l = segments.length; i < l; i++) {
+			var segment = segments[i];
+			var vector = lastPoint - segment.point;
+			this.count += this.vel.length * 10;
+			var rotLength = Math.sin((this.count + i * 3) / 300);
+			var rotated = lastVector.rotate(90).normalize(rotLength);
+			lastPoint += lastVector.normalize(-5 - this.vel.length / 3);
+			segment.point = lastPoint;
+			segment.point += rotated;
+			lastVector = vector;
+		}
+		this.path.smooth();
+		this.head.position = this.loc;
+		var vector = this.loc - this.lastLoc;
+		var rot = vector.angle;
+		this.head.rotate(rot - this.lastRot);
+		this.lastRot = rot;
+
+		var shortSegments = this.shortPath.segments;
+		for (var i = 0; i < 3; i++)
+			shortSegments[i] = segments[i].clone();
+	},
+
+	// We accumulate a new acceleration each time based on three rules
+	flock: function(boids) {
+		var sep = this.separate(boids) * 3;
+		var ali = this.align(boids);
+		var coh = this.cohesion(boids);
+		this.acc += sep + ali + coh;
+	},
+
+	update: function() {
+		// Update velocity
+		this.vel += this.acc;
+		// Limit speed (vector#limit?)
+		this.vel.length = Math.min(this.maxSpeed, this.vel.length);
+		this.loc += this.vel;
+		// Reset acceleration to 0 each cycle
+		this.acc.length = 0;
+	},
+
+	seek: function(target) {
+		this.acc += this.steer(target, false);
+	},
+
+	arrive: function(target) {
+		this.acc += this.steer(target, true);
+	},
+
+	// A method that calculates a steering vector towards a target
+	// Takes a second argument, if true, it slows down as it approaches
+	// the target
+	steer: function(target, slowdown) {
+		var steer,
+			desired = target - this.loc,
+			d = desired.length;
+		if (d > 0) {
+			// Two options for desired vector magnitude
+			// (1 -- based on distance, 2 -- maxSpeed)
+			if (slowdown && d < 100) {
+				// // This damping is somewhat arbitrary:
+				desired.length = this.maxSpeed * (d / 100);
+			} else {
+				desired.length = this.maxSpeed;
+			}
+			steer = desired - this.vel;
+			steer.length = Math.min(this.maxForce, steer.length);
+		} else {
+			steer = new Point(0, 0);
+		}
+		return steer;
+	},
+
+	borders: function() {
+		var loc = this.loc;
+		var r = this.r;
+		var oldLoc = this.loc.clone();
+		var width = size.width;
+		var height = size.height;
+		if (loc.x < -r) loc.x = width + r;
+		if (loc.y < -r) loc.y = height + r;
+		if (loc.x > width + r) loc.x = -r;
+		if (loc.y > height + r) loc.y = -r;
+		var vector = this.loc - oldLoc;
+		if (!vector.isZero())
+			this.path.position += vector;
+	},
+
+	separate: function(boids) {
+		var desiredSeperation = 60;
+		var steer = new Point(0, 0);
+		var count = 0;
+		// For every boid in the system, check if it's too close
+		for (var i = 0, l = boids.length; i < l; i++) {
+			var other = boids[i];
+			var d = other.loc.getDistance(this.loc);
+			if (d > 0 && d < desiredSeperation) {
+				// Calculate vector pointing away from neighbor
+				var diff = this.loc - other.loc;
+				steer += diff.normalize(1 / d);
+				count++;
+			}
+		}
+		// Average -- divide by how many
+		if (count > 0)
+			steer /= count;
+		if (steer.length > 0) {
+			// Implement Reynolds: Steering = Desired - Velocity
+			steer.length = this.maxSpeed;
+			steer -= this.vel;
+			steer.length = Math.min(steer.length, this.maxForce);
+		}
+		return steer;
+	},
+
+	// Alignment
+	// For every nearby boid in the system, calculate the average velocity
+	align: function(boids) {
+		var neighborDist = 25;
+		var steer = new Point(0, 0);
+		var count = 0;
+		var nearest = 999;
+		var closestPoint;
+		for (var i = 0, l = boids.length; i < l; i++) {
+			var other = boids[i];
+			var d = this.loc.getDistance(other.loc);
+			if (d > 0 && d < nearest) {
+				closestPoint = other.loc;
+				nearest = d;
+			}
+			if (d > 0 && d < neighborDist) {
+				steer += other.vel;
+				count++;
+			}
+		}
+
+		if (count > 0)
+			steer /= count;
+		if (steer.length > 0) {
+			// Implement Reynolds: Steering = Desired - Velocity
+			steer.length = this.maxSpeed;
+			steer -= this.vel;
+			steer.length = Math.min(steer.length, this.maxForce);
+		}
+		return steer;
+	},
+
+	// Cohesion
+	// For the average location (i.e. center) of all nearby boids,
+	// calculate steering vector towards that location
+	cohesion: function(boids) {
+		var neighborDist = 100;
+		var sum = new Point(0, 0);
+		var count = 0;
+		for (var i = 0, l = boids.length; i < l; i++) {
+			var other = boids[i];
+			var d = this.loc.getDistance(other.loc);
+			if (d > 0 && d < neighborDist) {
+				sum += other.loc; // Add location
+				count++;
+			}
+		}
+		if (count > 0) {
+			sum /= count;
+			// Steer towards the location
+			return this.steer(sum, false);
+		}
+		return sum;
+	}
+});
+
+var heartPath = new Path([
+	[[514.6962890625, 624.703125], [7.0966796875, -26.3369140625], [-7.10205078125, -27.0244140625]],
+	[[484.29052734375, 548.6025390625], [13.16845703125, 23.7060546875], [-13.173828125, -23.70703125]],
+	[[407.84619140625, 438.14453125], [37.79296875, 49.935546875], [-27.71630859375, -36.6435546875]],
+	[[356.654296875, 368.400390625], [6.41015625, 9.8505859375], [-10.53759765625, -16.02978515625]],
+	[[333.80712890625, 324.25146484375], [4.69189453125, 13.3994140625], [-4.697265625, -13.39892578125]],
+	[[326.76416015625, 283.53857421875], [0, 13.74267578125], [0, -25.42431640625]],
+	[[352.18798828125, 219.634765625], [-16.95263671875, 17.17822265625], [16.94775390625, -17.1787109375]],
+	[[415.0615234375, 193.8671875], [-24.96826171875, 0], [25.19287109375, 0]],
+	[[480.68310546875, 220.66552734375], [-18.552734375, -17.86572265625], [13.96826171875, 13.28662109375]],
+	[[514.6962890625, 280.10302734375], [-8.70703125, -26.3369140625], [7.55859375, -25.88037109375]],
+	[[546.6484375, 221.0087890625], [-13.7431640625, 13.517578125], [19.0087890625, -18.32177734375]],
+	[[612.61328125, 193.5234375], [-24.9677734375, 0], [24.7373046875, 0]],
+	[[675.486328125, 219.119140625], [-17.177734375, -17.06005859375], [17.1787109375, 17.06591796875]],
+	[[701.2548828125, 280.10302734375], [0, -23.58837890625], [0, 20.61376953125]],
+	[[686.1376953125, 344.52197265625], [10.076171875, -22.33203125], [-10.08203125, 22.33203125]],
+	[[627.73046875, 432.3046875], [28.8603515625, -36.1875], [-37.5673828125, 47.412109375]],
+	[[545.6171875, 549.1171875], [17.1787109375, -30.458984375], [-13.517578125, 24.0498046875]]
+]);
+heartPath.closed = true;
+heartPath.position = view.center;
+heartPath.strokeColor = null;
+heartPath.scale(1.5);
+
+var groupTogether = false;
+var pathLength = heartPath.length;
+var mouseDown = false;
+var boids = [];
+
+// Add the boids:
+for (var i = 0; i < 300; i++) {
+	var position = view.center;
+	boids.push(new Boid(position, 10, 0.05));
+}
+
+function onFrame(event) {
+	for (var i = 0, l = boids.length; i < l; i++) {
+		if (groupTogether) {
+			var length = ((i + event.count / 30) % l) / l * pathLength;
+			var point = heartPath.getPointAt(length);
+			boids[i].arrive(point);
+		}
+		boids[i].run(boids);
+	}
+}
+
+// Reposition the heart path whenever the window is resized:
+function onResize(event) {
+	size = view.size;
+	heartPath.position = view.center;
+}
\ No newline at end of file
diff --git a/examples/Node.js/exportTadpoles.js b/examples/Node.js/exportTadpoles.js
new file mode 100644
index 00000000..17a43399
--- /dev/null
+++ b/examples/Node.js/exportTadpoles.js
@@ -0,0 +1,12 @@
+require('../../node.js/');
+var paper = require('./Tadpoles');
+paper.view.exportFrames({
+	amount: 400,
+	directory: __dirname,
+	onComplete: function() {
+		console.log('Done exporting.');
+	},
+	onProgress: function(event) {
+		console.log(event.percentage + '% complete, frame took: ' + event.delta);
+	}
+});
diff --git a/index.js b/index.js
new file mode 100644
index 00000000..50fd42bf
--- /dev/null
+++ b/index.js
@@ -0,0 +1 @@
+module.exports = require('./src/loadNode.js');
\ No newline at end of file
diff --git a/node.js/index.js b/node.js/index.js
new file mode 100644
index 00000000..cc4c97db
--- /dev/null
+++ b/node.js/index.js
@@ -0,0 +1,54 @@
+var fs = require('fs'),
+	vm = require('vm'),
+	path = require('path');
+
+__dirname = path.resolve(__dirname, '../src/');
+
+// Create the context within which we will run the source files:
+var context = vm.createContext({
+	options: {
+		server: true,
+		version: 'dev'
+	},
+	// Node Canvas library: https://github.com/learnboost/node-canvas
+	Canvas: require('canvas'),
+	// Copy over global variables:
+    console: console,
+	require: require,
+	__dirname: __dirname,
+	__filename: __filename,
+	// Used to load and run source files within the same context:
+	include: function(uri) {
+		var source = fs.readFileSync(path.resolve(__dirname, uri), 'utf8');
+		// For relative includes, we save the current directory and then
+		// add the uri directory to __dirname:
+		var oldDirname = __dirname;
+		__dirname = path.resolve(__dirname, path.dirname(uri));
+		vm.runInContext(source, context, uri);
+		__dirname = oldDirname;
+	}
+});
+
+// Load Paper.js library files:
+context.include('paper.js');
+
+context.Base.each(context, function(val, key) {
+	if (val && val.prototype instanceof context.Base) {
+		val._name = key;
+		// Export all classes through PaperScope:
+		context.PaperScope.prototype[key] = val;
+	}
+});
+context.PaperScope.prototype['Canvas'] = context.Canvas;
+
+require.extensions['.pjs'] = function(module, uri) {
+	var source = context.PaperScript.compile(fs.readFileSync(uri, 'utf8'));
+	var envVars = 'var __dirname = \'' + path.dirname(uri) + '\';' + 
+				 'var __filename = \'' + uri + '\';';
+	vm.runInContext(envVars, context);
+	var scope = new context.PaperScope();
+	context.PaperScript.evaluate(source, scope);
+	module.exports = scope;
+};
+
+module.exports = new context.PaperScope();
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..1e2e4124
--- /dev/null
+++ b/package.json
@@ -0,0 +1,20 @@
+{
+	"name": "paper",
+	"description":  "Vector graphics scripting framework",
+	"version":  "0.2.1",
+	"contributors": [{
+		"name" : "Jürg Lehni",
+		"url" : "http://lehni.org"
+	}, {
+		"name" : "Jonathan Puckey",
+		"url" : "http://jonathanpuckey.com"
+	}],
+	"homepage": "http://paperjs.org",
+	"keywords": ["canvas", "graphic", "graphics", "vector", "paper.js"], 	
+	"repository": "git://github.com/paperjs/paper.js/",
+	"dependencies": {
+		"canvas": "0.7.0"
+	},
+	"engines": { "node": ">= 0.4.0 && < 0.6.0" },
+	"main": "./node.js/index.js"
+}
\ No newline at end of file
diff --git a/src/paper.js b/src/paper.js
index 9065d690..aa4669ba 100644
--- a/src/paper.js
+++ b/src/paper.js
@@ -105,8 +105,11 @@ var paper = new function() {
 /*#*/ if (options.browser) {
 /*#*/ include('browser/DomElement.js');
 /*#*/ include('browser/DomEvent.js');
+/*#*/ } // options.browser
 
 /*#*/ include('ui/View.js');
+
+/*#*/ if (options.browser) {
 /*#*/ include('ui/Event.js');
 /*#*/ include('ui/KeyEvent.js');
 /*#*/ include('ui/Key.js');
@@ -121,7 +124,9 @@ var paper = new function() {
 
 /*#*/ include('core/PaperScript.js');
 
+/*#*/ if (options.browser) {
 /*#*/ include('core/initialize.js');
+/*#*/ } // options.browser
 
 /*#*/ if (options.version != 'dev') {
 // Finally inject the classes set on 'this' into the PaperScope class and create
@@ -133,4 +138,4 @@ var paper = new function() {
 this.enumerable = true;
 return new (PaperScope.inject(this));
 /*#*/ } // options.version != 'dev'
-};
+};
\ No newline at end of file
diff --git a/src/ui/View.js b/src/ui/View.js
index 11d5fad7..06270c9b 100644
--- a/src/ui/View.js
+++ b/src/ui/View.js
@@ -36,6 +36,26 @@ var View = this.View = PaperScopeItem.extend(/** @lends View# */{
 		this.base();
 		// Handle canvas argument
 		var size;
+
+/*#*/ if (options.server) {
+		if (canvas && canvas instanceof Canvas) {
+			this._canvas = canvas;
+			size = Size.create(canvas.width, canvas.height);
+		} else {
+			// 2nd argument onwards could be view size, otherwise use default:
+			size = Size.read(arguments, 1);
+			if (size.isZero())
+				size = new Size(1024, 768);
+			this._canvas = CanvasProvider.getCanvas(size);
+		}
+
+		// Generate an id for this view / canvas if it does not have one
+		this._id = this._canvas.id;
+		if (this._id == null)
+			this._canvas.id = this._id = 'canvas-' + View._id++;
+/*#*/ } // options.server
+
+/*#*/ if (options.browser) {
 		if (typeof canvas === 'string')
 			canvas = document.getElementById(canvas);
 		if (canvas instanceof HTMLCanvasElement) {
@@ -92,6 +112,8 @@ var View = this.View = PaperScopeItem.extend(/** @lends View# */{
 		this._id = this._canvas.getAttribute('id');
 		if (this._id == null)
 			this._canvas.setAttribute('id', this._id = 'canvas-' + View._id++);
+/*#*/ } // options.browser
+
 		// Link this id to our view
 		View._views[this._id] = this;
 		this._viewSize = LinkedSize.create(this, 'setViewSize',
@@ -99,11 +121,15 @@ var View = this.View = PaperScopeItem.extend(/** @lends View# */{
 		this._context = this._canvas.getContext('2d');
 		this._matrix = new Matrix();
 		this._zoom = 1;
+
+/*#*/ if (options.browser) {
 		this._events = this._createEvents();
 		DomEvent.add(this._canvas, this._events);
 		// Make sure the first view is focused for keyboard input straight away
 		if (!View._focused)
 			View._focused = this;
+/*#*/ } // options.browser
+
 		// As soon as a new view is added we need to mark the redraw as not
 		// motified, so the next call loops through all the views again.
 		this._scope._redrawNotified = false;
@@ -355,6 +381,7 @@ var View = this.View = PaperScopeItem.extend(/** @lends View# */{
 			delete this._onFrameCallback;
 			return;
 		}
+/*#*/ if (options.browser) {
 		var that = this,
 			requested = false,
 			before,
@@ -388,8 +415,11 @@ var View = this.View = PaperScopeItem.extend(/** @lends View# */{
 		// of onFrame calls.
 		if (!requested)
 			this._onFrameCallback();
+/*#*/ } // options.browser
 	},
 
+
+
 	/**
 	 * Handler function that is called whenever a view is resized.
 	 *
@@ -408,7 +438,13 @@ var View = this.View = PaperScopeItem.extend(/** @lends View# */{
 	 * @type Function
 	 */
 	onResize: null
+}, {
+	statics: {
+		_views: {},
+		_id: 0
+	}
 }, new function() { // Injection scope for mouse handlers
+/*#*/ if (options.browser) {
 	var tool,
 		timer,
 		curPoint,
@@ -535,8 +571,6 @@ var View = this.View = PaperScopeItem.extend(/** @lends View# */{
 		},
 
 		statics: {
-			_views: {},
-			_id: 0,
 
 			/**
 			 * Loops through all scopes and their views and sets the focus on
@@ -545,4 +579,88 @@ var View = this.View = PaperScopeItem.extend(/** @lends View# */{
 			updateFocus: updateFocus
 		}
 	};
+/*#*/ } // options.browser
+}, new function() {
+/*#*/ if (options.server) {
+	var fs = require('fs'),
+		path = require('path');
+	// Utility function that converts a number to a string with
+	// x amount of padded 0 digits:
+	function toPaddedString(number, length) {
+		var str = number.toString(10);
+		for (var i = 0, l = length - str.length; i < l; i++) {
+			str = '0' + str;
+		}
+		return str;
+	}
+	return {
+		// DOCS: View#exportFrames(param);
+		exportFrames: function(param) {
+			param = Base.merge({
+				fps: 30,
+				prefix: 'frame-',
+				amount: 1
+			}, param);
+			if (!param.directory) {
+				throw new Error('Missing param.directory');
+			}
+			var view = this,
+				count = 0,
+				frameDuration = 1 / param.fps,
+				lastTime = startTime = Date.now();
+
+			// Start exporting frames by exporting the first frame:
+			exportFrame(param);
+
+			function exportFrame(param) {
+				count++;
+				var filename = param.prefix + toPaddedString(count, 6) + '.png',
+					uri = param.directory + '/' + filename;
+				var out = view.exportImage(uri, function() {
+					// When the file has been closed, export the next fame:
+					var then = Date.now();
+					if (param.onProgress) {
+						param.onProgress({
+							count: count,
+							amount: param.amount,
+							percentage: Math.round(count / param.amount
+									* 10000) / 100,
+							time: then - startTime,
+							delta: then - lastTime
+						});
+					}
+					lastTime = then;
+					if (count < param.amount) {
+						exportFrame(param);
+					} else {
+						// Call onComplete handler when finished:
+						if (param.onComplete) {
+							param.onComplete();
+						}
+					}
+				});
+				if (view.onFrame) {
+					view.onFrame({
+						delta: frameDuration,
+						time: frameDuration * count,
+						count: count
+					});
+				}
+			}
+		},
+		// DOCS: View#exportImage(uri, callback);
+		exportImage: function(uri, callback) {
+			this.draw();
+			// TODO: is it necessary to resolve the path?
+			var out = fs.createWriteStream(path.resolve(__dirname, uri)),
+				stream = this._canvas.createPNGStream();
+			// Pipe the png stream to the write stream:
+			stream.pipe(out);
+			if (callback) {
+				out.on('close', callback);
+			}
+			return out;
+		}
+	};
+/*#*/ } // options.server
 });
diff --git a/src/util/CanvasProvider.js b/src/util/CanvasProvider.js
index 57f27630..705901cf 100644
--- a/src/util/CanvasProvider.js
+++ b/src/util/CanvasProvider.js
@@ -43,8 +43,7 @@ var CanvasProvider = {
 			canvas.height = size.height;
 			return canvas;
 /*#*/ } else { // !options.browser
-			// Only rhino-canvas for now:
-			return new Image(size.width, size.height);
+			return new Canvas(size.width, size.height);
 /*#*/ } // !options.browser
 		}
 	},