
UPDATE 07/20/11: Updated the demo to take better advantage of built-in Paper.js features.
UPDATE 07/27/11: Compacted the demo even further utilizing the active layer object.
Recently, we’ve been exploring various vector graphics libraries in an order to craft some more compelling data visualizations. I’ll admit, we weren’t too enthusiastic about the prospect of manipulating SVG or learning some strange custom syntax. Fortunately for us, Paper.js was released a few weeks back.
Paper.js is a brand new open source vector graphics scripting framework with two important distinctions. First, it’s actually nothing new. Paper.js is entirely based off Scriptographer, the ten year old scripting framework for Adobe Illustrator, complete with its own mature and battle-tested display hierarchy and Object Model. This is welcome news for any developer familiar with Illustrator or – more importantly – Flash. Second, it uses the Canvas element as opposed to SVG. Besides the speed boost this grants, it also means we can take advantage of the rasterization and bitmap manipulation features of Canvas.
We’ve put together a simple example that showcases some of the structure and features of Paper.js including debugging guidelines, smoothing, and path manipulation. Additionally, it demonstrates simple interactions and event handling.
Click to view our tutorial example.
Paperscript (the Paper.js extension of Javascript) is an important differentiation from other vector graphic libraries in that it provides a sand-boxed scope for Paper.js scripts. It treats objects such as Point and Size like native data structures with operator overloaders in full effect. For example, you can add two points by simply performing: point1 + point2. Contrast this to a similar framework like Raphael, which requires the cumbersome manipulation of SVG strings, and you can start seeing the benefits of paperscript.
The first step in our demo is to declare our paperscript and tie a canvas instance to its scope. We’ll also declare some built-in handlers for resize, keyboard, and mouse movement events. The handlers are automatically bound once declared – no need to attach them to an element.
<canvas id="canvas" resize keepalive="true"></canvas> <script type="text/paperscript" canvas="canvas"> function onResize() { // } function onMouseMove(event) { // } function onKeyUp(e) { // } </script>
Let’s construct a simple fake terrain effect with depth being simulated by brightness. In this case we’re building a series of paths stacked vertically with an equal number of segments across each path resulting in a square grid (gridSize x gridSize). First we need to declare our grid dimensions.
var gridSize = 20;
Path is the core visual building block in Paper.js that is used to construct other vector graphics objects in its display hierarchy. We’ll be using them directly to build our grid. Every row in our grid represents a new Path object with columns being represented by that object’s segments. We’ll instantiate each path on every loop iteration. Next we’ll use the Path method add() to add the segment points.
for (var j = 0; j <= gridSize; j++) { var path = new Path(); for (var i = 0; i <= gridSize; i++) { path.add(new Segment()); } }
Let’s change some of the rendering properties of each Path so we can visually differentiate them. As we loop over each row, we assign a stroke color to the path which becomes brighter as increments get higher. Paper.js has built in color objects for handling RGB, HSB, HSL, and Grayscale. We’ll use HSLColor to display a range of greens. The stroke width can be set globally and inherited by every newly instantiated path. Paper.js has a base Project object which acts as our “document”. We set the stroke width of our projects’ current style to a value proportional to the canvas dimensions.
project.currentStyle.strokeWidth = (view.size.height + view.size.width) / 0.8 / gridSize; for (var j = 0; j <= gridSize; j++) { var path = new Path(); var brightness = j / gridSize * 0.608; path.strokeColor = new HSLColor(155, 1, brightness); for (var i = 0; i <= gridSize; i++) { path.add(new Segment()); } }
It’s important to note that once a visual object is created, it will be rendered as part of the display hierarchy. There is no manual draw() or render() method to call. Paper.js has several structures for managing the display hierarchy including Project, Symbol, and Layer objects. In our example, when we want to iterate over each segment, we reference the child nodes (the path objects) of our project’s active layer.
var layer = project.activeLayer;
Now we can start doing the fun stuff – segment manipulation. Inside the onMouseMove event handler, we construct our reference loop. Here we use a nested for loop for simplicity. It should be noted that this is a very inefficient method for performance. Structures such as linked lists would be more appropriate if we were dealing with a large number of segments.
function onMouseMove(event) { for (var j = 0; j <= gridSize; j++) { var path = layer.children[j]; for (var i = 0; i <= gridSize; i++) { var segment = path.segments[i]; } } }
Next, we move the segments’ point. For our demo, we apply some force vectors to each grid point. These vectors are generated by offsetting the segment coordinates (point) by a normalized vector (delta) pointing to our mouse position (event.point). Recall that paperscript has operator overloaders. This means that we can simply subtract the mouse point object from the grid point (event.point – point). Additionally, points have a built-in normalize method, which means we can write the entire vector operation in one line ((event.point – o).normalize()). The normals are amplified by force (force) to give us a smoother bump effect and then applied to the grid point. Finally, we use the built-in smooth() method on each path so we don’t have to worry about bezier handle adjustments.
function onMouseMove(event) { mousePoint = event.point; for (var j = 0; j <= gridSize; j++) { var path = layer.children[j]; for (var i = 0; i <= gridSize; i++) { var segment = path.segments[i]; var point = view.size * 1.2 * (new Point(i, j) / gridSize); var delta = (event.point - point).normalize(); var force = ((view.size.height + view.size.width) / 2) * 0.2; segment.point = point + delta * [force / -2, force]; } path.smooth(); } }
The last interaction we want to apply is a toggle for the debugger guidelines. These guidelines visually look like a selected path in Illustrator, complete with bezier handles (non-interactive at the moment). The fullySelected property on our project’s active layer will trigger guidelines an all its children – another example of the robust display hierarchy in Paper.js. Now we can see how the path handles change in real-time.
function onKeyUp(event) { layer.fullySelected = !layer.selected; }
The finished demo can be viewed here. Make sure to view the source for better documentation and the complete flow. We took this demo a little bit farther and tied it to the Audio Data API (currently only available in FireFox 4+) and created an audio visualizer written purely in Paper.js.
We’re excited about the possibilities Paper.js creates – particularly when applied to data visualizations. Support and development of the library seems to be healthy, and the roadmap includes some very interesting future features. Paper.js + Node.js, anybody?