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 } },