From ddec6586539f52e3ced9dbf046854e0ef8f27c4f Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Sat, 30 Jul 2011 02:04:30 +0200 Subject: [PATCH 01/14] Make Paper.js work on Node.js. --- src/paper.js | 11 ++++ src/ui/View.js | 111 ++++++++++++++++++++++++++++++++++++- src/util/CanvasProvider.js | 3 +- 3 files changed, 121 insertions(+), 4 deletions(-) diff --git a/src/paper.js b/src/paper.js index 051d025f..14668b21 100644 --- a/src/paper.js +++ b/src/paper.js @@ -104,8 +104,15 @@ var paper = new function() { /*#*/ if (options.browser) { /*#*/ include('browser/DomElement.js'); /*#*/ include('browser/DomEvent.js'); +/*#*/ } // options.browser + +/*#*/ if (options.server) { + var Canvas = this.Canvas = require('canvas'); +/*#*/ } // options.server /*#*/ include('ui/View.js'); + +/*#*/ if (options.browser) { /*#*/ include('ui/Event.js'); /*#*/ include('ui/KeyEvent.js'); /*#*/ include('ui/Key.js'); @@ -130,3 +137,7 @@ var paper = new function() { return new (PaperScope.inject(this)); /*#*/ } // options.version != 'dev' }; + +/*#*/ if (options.server) { +module.exports = paper; +/*#*/ } // options.server diff --git a/src/ui/View.js b/src/ui/View.js index 8be21d2c..48035c8b 100644 --- a/src/ui/View.js +++ b/src/ui/View.js @@ -35,6 +35,26 @@ var View = this.View = Base.extend(/** @lends View# */{ this._index = this._scope.views.push(this) - 1; // 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 (canvas && canvas instanceof HTMLCanvasElement) { this._canvas = canvas; // If the canvas has the resize attribute, resize the it to fill the @@ -88,6 +108,8 @@ var View = this.View = Base.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', @@ -95,11 +117,15 @@ var View = this.View = Base.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; @@ -338,6 +364,7 @@ var View = this.View = Base.extend(/** @lends View# */{ delete this._onFrameCallback; return; } +/*#*/ if (options.browser) { var that = this, requested = false, before, @@ -371,8 +398,83 @@ var View = this.View = Base.extend(/** @lends View# */{ // of onFrame calls. if (!requested) this._onFrameCallback(); +/*#*/ } // options.browser }, + // TODO: support exporting of jpg + exportFrames: function(param) { +/*#*/ if (options.server) { + param = Base.merge({ + fps: 30, + prefix: 'frame-', + amount: 1 + }, param); + if (!param.directory) + throw new Error('Missing param.directory'); + + var view = this, + count = 0, + fs = require('fs'), + frameDuration = 1 / param.fps, + lastTime = startTime = Date.now(); + + // Start exporting frames by exporting the first frame: + exportFrame(param); + + // 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; + } + + function exportFrame(param) { + count++; + if (view.onFrame) { + var then = new Date(); + view.onFrame({ + delta: frameDuration, + time: frameDuration * count, + count: count + }); + console.log(new Date() - then, ' onFrame'); + } + view.draw(); + var filename = param.prefix + toPaddedString(count, 6) + '.png', + uri = param.directory + '/' + filename, + out = fs.createWriteStream(uri), + stream = view._canvas.createPNGStream(); + // Pipe the png stream to the write stream: + stream.pipe(out); + // When the file has been closed, export the next fame: + out.on('close', function() { + var now = Date.now(); + if (param.onProgress) { + param.onProgress({ + count: count, + amount: param.amount, + percentage: Math.round(count / param.amount * 10000) / 100, + time: now - startTime, + delta: now - lastTime + }); + } + lastTime = now; + if (count < param.amount) { + exportFrame(param); + } else { + // Call onComplete handler when finished: + if (param.onComplete) { + param.onComplete(); + } + } + }); + } +/*#*/ } // options.server + }, + /** * Handler function that is called whenever a view is resized. * @@ -391,7 +493,13 @@ var View = this.View = Base.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, @@ -518,8 +626,6 @@ var View = this.View = Base.extend(/** @lends View# */{ }, statics: { - _views: {}, - _id: 0, /** * Loops through all scopes and their views and sets the focus on @@ -528,4 +634,5 @@ var View = this.View = Base.extend(/** @lends View# */{ updateFocus: updateFocus } }; +/*#*/ } // options.browser }); 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 } }, From 30e2ba5582fc329ee851a14588900bf432151c25 Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Tue, 2 Aug 2011 10:49:40 +0200 Subject: [PATCH 02/14] Update View. --- src/ui/View.js | 126 +++++-------------------------------------------- 1 file changed, 11 insertions(+), 115 deletions(-) diff --git a/src/ui/View.js b/src/ui/View.js index 48035c8b..29c9c573 100644 --- a/src/ui/View.js +++ b/src/ui/View.js @@ -35,36 +35,19 @@ var View = this.View = Base.extend(/** @lends View# */{ this._index = this._scope.views.push(this) - 1; // 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 (canvas && canvas instanceof HTMLCanvasElement) { + if (typeof canvas === 'string') + canvas = document.getElementById(canvas); + if (canvas instanceof HTMLCanvasElement) { this._canvas = canvas; // If the canvas has the resize attribute, resize the it to fill the // window and resize it again whenever the user resizes the window. if (PaperScript.hasAttribute(canvas, 'resize')) { // Subtract canvas' viewport offset from the total size, to // stretch it in - var offset = DomElement.getOffset(canvas, false, true), + var offset = DomElement.getOffset(canvas, true), that = this; - size = DomElement.getViewportSize(canvas).subtract(offset); + size = DomElement.getViewportBounds(canvas) + .getSize().subtract(offset); canvas.width = size.width; canvas.height = size.height; DomEvent.add(window, { @@ -72,11 +55,11 @@ var View = this.View = Base.extend(/** @lends View# */{ // Only update canvas offset if it's not invisible, as // otherwise the offset would be wrong. if (!DomElement.isInvisible(canvas)) - offset = DomElement.getOffset(canvas, false, true); + offset = DomElement.getOffset(canvas, true); // Set the size now, which internally calls onResize // and redraws the view - that.setViewSize(DomElement.getViewportSize(canvas) - .subtract(offset)); + that.setViewSize(DomElement.getViewportBounds(canvas) + .getSize().subtract(offset)); } }); } else { @@ -108,8 +91,6 @@ var View = this.View = Base.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', @@ -117,15 +98,11 @@ var View = this.View = Base.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; @@ -364,7 +341,6 @@ var View = this.View = Base.extend(/** @lends View# */{ delete this._onFrameCallback; return; } -/*#*/ if (options.browser) { var that = this, requested = false, before, @@ -398,83 +374,8 @@ var View = this.View = Base.extend(/** @lends View# */{ // of onFrame calls. if (!requested) this._onFrameCallback(); -/*#*/ } // options.browser }, - // TODO: support exporting of jpg - exportFrames: function(param) { -/*#*/ if (options.server) { - param = Base.merge({ - fps: 30, - prefix: 'frame-', - amount: 1 - }, param); - if (!param.directory) - throw new Error('Missing param.directory'); - - var view = this, - count = 0, - fs = require('fs'), - frameDuration = 1 / param.fps, - lastTime = startTime = Date.now(); - - // Start exporting frames by exporting the first frame: - exportFrame(param); - - // 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; - } - - function exportFrame(param) { - count++; - if (view.onFrame) { - var then = new Date(); - view.onFrame({ - delta: frameDuration, - time: frameDuration * count, - count: count - }); - console.log(new Date() - then, ' onFrame'); - } - view.draw(); - var filename = param.prefix + toPaddedString(count, 6) + '.png', - uri = param.directory + '/' + filename, - out = fs.createWriteStream(uri), - stream = view._canvas.createPNGStream(); - // Pipe the png stream to the write stream: - stream.pipe(out); - // When the file has been closed, export the next fame: - out.on('close', function() { - var now = Date.now(); - if (param.onProgress) { - param.onProgress({ - count: count, - amount: param.amount, - percentage: Math.round(count / param.amount * 10000) / 100, - time: now - startTime, - delta: now - lastTime - }); - } - lastTime = now; - if (count < param.amount) { - exportFrame(param); - } else { - // Call onComplete handler when finished: - if (param.onComplete) { - param.onComplete(); - } - } - }); - } -/*#*/ } // options.server - }, - /** * Handler function that is called whenever a view is resized. * @@ -493,13 +394,7 @@ var View = this.View = Base.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, @@ -626,6 +521,8 @@ var View = this.View = Base.extend(/** @lends View# */{ }, statics: { + _views: {}, + _id: 0, /** * Loops through all scopes and their views and sets the focus on @@ -634,5 +531,4 @@ var View = this.View = Base.extend(/** @lends View# */{ updateFocus: updateFocus } }; -/*#*/ } // options.browser }); From 94a6860f7f2b05d74a3067a48250a993c739a5b4 Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Tue, 2 Aug 2011 11:02:04 +0200 Subject: [PATCH 03/14] Add back Node.js related changes to View. --- src/ui/View.js | 111 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 2 deletions(-) diff --git a/src/ui/View.js b/src/ui/View.js index 29c9c573..60385036 100644 --- a/src/ui/View.js +++ b/src/ui/View.js @@ -35,6 +35,26 @@ var View = this.View = Base.extend(/** @lends View# */{ this._index = this._scope.views.push(this) - 1; // 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) { @@ -91,6 +111,8 @@ var View = this.View = Base.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', @@ -98,11 +120,15 @@ var View = this.View = Base.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; @@ -341,6 +367,7 @@ var View = this.View = Base.extend(/** @lends View# */{ delete this._onFrameCallback; return; } +/*#*/ if (options.browser) { var that = this, requested = false, before, @@ -374,6 +401,81 @@ var View = this.View = Base.extend(/** @lends View# */{ // of onFrame calls. if (!requested) this._onFrameCallback(); +/*#*/ } // options.browser + }, + + // TODO: support exporting of jpg + exportFrames: function(param) { +/*#*/ if (options.server) { + param = Base.merge({ + fps: 30, + prefix: 'frame-', + amount: 1 + }, param); + if (!param.directory) + throw new Error('Missing param.directory'); + + var view = this, + count = 0, + fs = require('fs'), + frameDuration = 1 / param.fps, + lastTime = startTime = Date.now(); + + // Start exporting frames by exporting the first frame: + exportFrame(param); + + // 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; + } + + function exportFrame(param) { + count++; + if (view.onFrame) { + var then = new Date(); + view.onFrame({ + delta: frameDuration, + time: frameDuration * count, + count: count + }); + console.log(new Date() - then, ' onFrame'); + } + view.draw(); + var filename = param.prefix + toPaddedString(count, 6) + '.png', + uri = param.directory + '/' + filename, + out = fs.createWriteStream(uri), + stream = view._canvas.createPNGStream(); + // Pipe the png stream to the write stream: + stream.pipe(out); + // When the file has been closed, export the next fame: + out.on('close', function() { + var now = Date.now(); + if (param.onProgress) { + param.onProgress({ + count: count, + amount: param.amount, + percentage: Math.round(count / param.amount * 10000) / 100, + time: now - startTime, + delta: now - lastTime + }); + } + lastTime = now; + if (count < param.amount) { + exportFrame(param); + } else { + // Call onComplete handler when finished: + if (param.onComplete) { + param.onComplete(); + } + } + }); + } +/*#*/ } // options.server }, /** @@ -394,7 +496,13 @@ var View = this.View = Base.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, @@ -521,8 +629,6 @@ var View = this.View = Base.extend(/** @lends View# */{ }, statics: { - _views: {}, - _id: 0, /** * Loops through all scopes and their views and sets the focus on @@ -531,4 +637,5 @@ var View = this.View = Base.extend(/** @lends View# */{ updateFocus: updateFocus } }; +/*#*/ } // options.browser }); From a9780374f2d3501dc1cb0e85162ccf076c5b0de6 Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Tue, 2 Aug 2011 11:07:59 +0200 Subject: [PATCH 04/14] Remove logging statements. --- src/ui/View.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/ui/View.js b/src/ui/View.js index 60385036..fd8cec86 100644 --- a/src/ui/View.js +++ b/src/ui/View.js @@ -437,13 +437,11 @@ var View = this.View = Base.extend(/** @lends View# */{ function exportFrame(param) { count++; if (view.onFrame) { - var then = new Date(); view.onFrame({ delta: frameDuration, time: frameDuration * count, count: count }); - console.log(new Date() - then, ' onFrame'); } view.draw(); var filename = param.prefix + toPaddedString(count, 6) + '.png', From 8ce7ed578e33f26e9e3bc1d7baa555ee87183a6c Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Wed, 10 Aug 2011 21:02:32 +0200 Subject: [PATCH 05/14] Allow loading of Paper.js on Node.js without the need for compiling. --- src/loadNode.js | 31 +++++++++++++++++++++++++++++++ src/paper.js | 12 +++--------- 2 files changed, 34 insertions(+), 9 deletions(-) create mode 100644 src/loadNode.js diff --git a/src/loadNode.js b/src/loadNode.js new file mode 100644 index 00000000..b4a62513 --- /dev/null +++ b/src/loadNode.js @@ -0,0 +1,31 @@ +var context = require('vm').createContext({ + options: { + server: true, + version: 'dev' + }, + Canvas: require('canvas'), + console: console, + require: require, + include: function(uri) { + var source = require('fs').readFileSync(__dirname + '/' + uri); + // For relative includes, we save the current directory and then add + // the uri directory to __dirname: + var oldDirname = __dirname; + __dirname = __dirname + '/' + uri.replace(/[^/]+$/, ''); + require('vm').runInContext(source, context, uri); + __dirname = oldDirname; + } +}); + +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; + +module.exports = new context.PaperScope(); \ No newline at end of file diff --git a/src/paper.js b/src/paper.js index d2f817ca..aa4669ba 100644 --- a/src/paper.js +++ b/src/paper.js @@ -107,10 +107,6 @@ var paper = new function() { /*#*/ include('browser/DomEvent.js'); /*#*/ } // options.browser -/*#*/ if (options.server) { - var Canvas = this.Canvas = require('canvas'); -/*#*/ } // options.server - /*#*/ include('ui/View.js'); /*#*/ if (options.browser) { @@ -128,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 @@ -140,8 +138,4 @@ var paper = new function() { this.enumerable = true; return new (PaperScope.inject(this)); /*#*/ } // options.version != 'dev' -}; - -/*#*/ if (options.server) { -module.exports = paper; -/*#*/ } // options.server +}; \ No newline at end of file From a9eebaed6437d6a87afccb644509072e07671f9a Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Wed, 10 Aug 2011 21:02:54 +0200 Subject: [PATCH 06/14] Support npm. --- index.js | 1 + package.json | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 index.js create mode 100644 package.json 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/package.json b/package.json new file mode 100644 index 00000000..3ece2a14 --- /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": "./src/loadCanvas.js" +} \ No newline at end of file From 0f7406f7f40a97c4b05b718b521d59817bbe42f5 Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Wed, 10 Aug 2011 21:04:01 +0200 Subject: [PATCH 07/14] Add Node.js example. --- examples/Node.js/AnimatedStar.js | 49 ++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 examples/Node.js/AnimatedStar.js 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); + } +} From 25cb4a7dd16e4dd3235ade582e78dab5afa72883 Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Sat, 13 Aug 2011 18:26:43 +0200 Subject: [PATCH 08/14] Support running of PaperScript .pjs files. --- src/loadNode.js | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/loadNode.js b/src/loadNode.js index b4a62513..fa2671d1 100644 --- a/src/loadNode.js +++ b/src/loadNode.js @@ -1,18 +1,28 @@ -var context = require('vm').createContext({ +var fs = require('fs'), + vm = require('vm'), + path = require('path'); + +// 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 = require('fs').readFileSync(__dirname + '/' + uri); - // For relative includes, we save the current directory and then add - // the uri directory to __dirname: + 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 = __dirname + '/' + uri.replace(/[^/]+$/, ''); - require('vm').runInContext(source, context, uri); + __dirname = path.resolve(__dirname, path.dirname(uri)); + vm.runInContext(source, context, uri); __dirname = oldDirname; } }); @@ -28,4 +38,14 @@ context.Base.each(context, function(val, key) { }); 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 From 88ceffcd87cdb832f840b23c9d265fce9e9020f2 Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Sat, 13 Aug 2011 19:11:21 +0200 Subject: [PATCH 09/14] Add Node.js PaperScript Tadpoles example. --- examples/Node.js/Tadpoles.pjs | 280 +++++++++++++++++++++++++++++ examples/Node.js/exportTadpoles.js | 12 ++ 2 files changed, 292 insertions(+) create mode 100644 examples/Node.js/Tadpoles.pjs create mode 100644 examples/Node.js/exportTadpoles.js 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..074d96eb --- /dev/null +++ b/examples/Node.js/exportTadpoles.js @@ -0,0 +1,12 @@ +require('../../index.js'); +var scope = require('./Tadpoles'); +scope.view.exportFrames({ + amount: 200, + directory: __dirname, + onComplete: function() { + console.log('Done exporting.'); + }, + onProgress: function(event) { + console.log(event.percentage + '% complete, frame took: ' + event.delta); + } +}); \ No newline at end of file From 197c2670d68c24623df5cdcd10d20829629ee154 Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Thu, 18 Aug 2011 11:11:24 +0200 Subject: [PATCH 10/14] Implement View#exportImage(uri, param) --- src/ui/View.js | 156 +++++++++++++++++++++++++++---------------------- 1 file changed, 86 insertions(+), 70 deletions(-) diff --git a/src/ui/View.js b/src/ui/View.js index e02fc213..d62887a4 100644 --- a/src/ui/View.js +++ b/src/ui/View.js @@ -418,77 +418,7 @@ var View = this.View = PaperScopeItem.extend(/** @lends View# */{ /*#*/ } // options.browser }, - // TODO: support exporting of jpg - exportFrames: function(param) { -/*#*/ if (options.server) { - param = Base.merge({ - fps: 30, - prefix: 'frame-', - amount: 1 - }, param); - if (!param.directory) - throw new Error('Missing param.directory'); - var view = this, - count = 0, - fs = require('fs'), - frameDuration = 1 / param.fps, - lastTime = startTime = Date.now(); - - // Start exporting frames by exporting the first frame: - exportFrame(param); - - // 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; - } - - function exportFrame(param) { - count++; - if (view.onFrame) { - view.onFrame({ - delta: frameDuration, - time: frameDuration * count, - count: count - }); - } - view.draw(); - var filename = param.prefix + toPaddedString(count, 6) + '.png', - uri = param.directory + '/' + filename, - out = fs.createWriteStream(uri), - stream = view._canvas.createPNGStream(); - // Pipe the png stream to the write stream: - stream.pipe(out); - // When the file has been closed, export the next fame: - out.on('close', function() { - var now = Date.now(); - if (param.onProgress) { - param.onProgress({ - count: count, - amount: param.amount, - percentage: Math.round(count / param.amount * 10000) / 100, - time: now - startTime, - delta: now - lastTime - }); - } - lastTime = now; - if (count < param.amount) { - exportFrame(param); - } else { - // Call onComplete handler when finished: - if (param.onComplete) { - param.onComplete(); - } - } - }); - } -/*#*/ } // options.server - }, /** * Handler function that is called whenever a view is resized. @@ -650,4 +580,90 @@ var View = this.View = PaperScopeItem.extend(/** @lends View# */{ } }; /*#*/ } // 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 { + 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, + onComplete = 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(); + } + } + }, + out = view.exportImage(uri, onComplete); + if (view.onFrame) { + view.onFrame({ + delta: frameDuration, + time: frameDuration * count, + count: count + }); + } + } + }, + // TODO: support exporting of jpg + exportImage: function(uri, param) { + 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 (param && param.onComplete) { + out.on('close', param.onComplete); + } + if (param && param.onError) { + out.on('error', param.onError); + } + return out; + } + }; +/*#*/ } // options.server }); From 70dc88ca916108e38854a51bfbbbc231c7ea1dee Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Sat, 20 Aug 2011 15:46:06 +0200 Subject: [PATCH 11/14] View: Clean up #exportImage() and #exportFrames(). --- src/ui/View.js | 61 +++++++++++++++++++++++--------------------------- 1 file changed, 28 insertions(+), 33 deletions(-) diff --git a/src/ui/View.js b/src/ui/View.js index d62887a4..2e58bf76 100644 --- a/src/ui/View.js +++ b/src/ui/View.js @@ -600,9 +600,9 @@ var View = this.View = PaperScopeItem.extend(/** @lends View# */{ prefix: 'frame-', amount: 1 }, param); - if (!param.directory) + if (!param.directory) { throw new Error('Missing param.directory'); - + } var view = this, count = 0, frameDuration = 1 / param.fps, @@ -614,31 +614,30 @@ var View = this.View = PaperScopeItem.extend(/** @lends View# */{ function exportFrame(param) { count++; var filename = param.prefix + toPaddedString(count, 6) + '.png', - uri = param.directory + '/' + filename, - onComplete = 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 - }); + 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(); } - lastTime = then; - if (count < param.amount) { - exportFrame(param); - } else { - // Call onComplete handler when finished: - if (param.onComplete) { - param.onComplete(); - } - } - }, - out = view.exportImage(uri, onComplete); + } + }); if (view.onFrame) { view.onFrame({ delta: frameDuration, @@ -648,19 +647,15 @@ var View = this.View = PaperScopeItem.extend(/** @lends View# */{ } } }, - // TODO: support exporting of jpg - exportImage: function(uri, param) { + 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 (param && param.onComplete) { - out.on('close', param.onComplete); - } - if (param && param.onError) { - out.on('error', param.onError); + if (callback) { + out.on('close', callback); } return out; } From 1f9993f894c7133283d4d3e00fbd56ef4fbf89e7 Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Sat, 20 Aug 2011 15:48:57 +0200 Subject: [PATCH 12/14] Move Node.js specific things to dedicated folder. --- src/loadNode.js => node.js/index.js | 3 +++ package.json | 38 ++++++++++++++--------------- 2 files changed, 22 insertions(+), 19 deletions(-) rename src/loadNode.js => node.js/index.js (95%) diff --git a/src/loadNode.js b/node.js/index.js similarity index 95% rename from src/loadNode.js rename to node.js/index.js index fa2671d1..cc4c97db 100644 --- a/src/loadNode.js +++ b/node.js/index.js @@ -2,6 +2,8 @@ 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: { @@ -27,6 +29,7 @@ var context = vm.createContext({ } }); +// Load Paper.js library files: context.include('paper.js'); context.Base.each(context, function(val, key) { diff --git a/package.json b/package.json index 3ece2a14..1e2e4124 100644 --- a/package.json +++ b/package.json @@ -1,20 +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": "./src/loadCanvas.js" +{ + "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 From c67b8acd4ee31cd4f8285cd21185727d191d23ce Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Sat, 20 Aug 2011 15:50:25 +0200 Subject: [PATCH 13/14] Clean up Node.js tadpoles example. --- examples/Node.js/exportTadpoles.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/Node.js/exportTadpoles.js b/examples/Node.js/exportTadpoles.js index 074d96eb..17a43399 100644 --- a/examples/Node.js/exportTadpoles.js +++ b/examples/Node.js/exportTadpoles.js @@ -1,7 +1,7 @@ -require('../../index.js'); -var scope = require('./Tadpoles'); -scope.view.exportFrames({ - amount: 200, +require('../../node.js/'); +var paper = require('./Tadpoles'); +paper.view.exportFrames({ + amount: 400, directory: __dirname, onComplete: function() { console.log('Done exporting.'); @@ -9,4 +9,4 @@ scope.view.exportFrames({ onProgress: function(event) { console.log(event.percentage + '% complete, frame took: ' + event.delta); } -}); \ No newline at end of file +}); From bec8cf967eb95402e074dd21aeae2d7796078e4a Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Sat, 20 Aug 2011 15:51:54 +0200 Subject: [PATCH 14/14] View: add documentation todos. --- src/ui/View.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ui/View.js b/src/ui/View.js index 2e58bf76..06270c9b 100644 --- a/src/ui/View.js +++ b/src/ui/View.js @@ -594,6 +594,7 @@ var View = this.View = PaperScopeItem.extend(/** @lends View# */{ return str; } return { + // DOCS: View#exportFrames(param); exportFrames: function(param) { param = Base.merge({ fps: 30, @@ -647,6 +648,7 @@ var View = this.View = PaperScopeItem.extend(/** @lends View# */{ } } }, + // DOCS: View#exportImage(uri, callback); exportImage: function(uri, callback) { this.draw(); // TODO: is it necessary to resolve the path?