/* * Paper.js - The Swiss Army Knife of Vector Graphics Scripting. * http://paperjs.org/ * * Copyright (c) 2011 - 2013, Juerg Lehni & Jonathan Puckey * http://lehni.org/ & http://jonathanpuckey.com/ * * Distributed under the MIT license. See LICENSE file for details. * * All rights reserved. */ /** * @name CanvasView * @class * @private */ var CanvasView = View.extend(/** @lends CanvasView# */{ _class: 'CanvasView', /** * Creates a view object that wraps a canvas element. * * @name CanvasView#initialize * @param {HTMLCanvasElement} canvas the canvas object that this view should * wrap */ /** * Creates a view object that wraps a newly created canvas element. * * @name CanvasView#initialize * @param {Size} size the size of the canvas to be created */ initialize: function CanvasView(canvas) { // Handle canvas argument if (!(canvas instanceof HTMLCanvasElement)) { // See if the arguments describe the view size: var size = Size.read(arguments); if (size.isZero()) throw new Error( 'Cannot create CanvasView with the provided argument: ' + canvas); canvas = CanvasProvider.getCanvas(size); } this._context = canvas.getContext('2d'); // Have Item count installed mouse events. this._eventCounters = {}; this._ratio = 1; /*#*/ if (__options.environment == 'browser') { if (PaperScope.getAttribute(canvas, 'hidpi') !== 'off') { // Hi-DPI Canvas support based on: // http://www.html5rocks.com/en/tutorials/canvas/hidpi/ var deviceRatio = window.devicePixelRatio || 1, backingStoreRatio = DomElement.getPrefixValue(this._context, 'backingStorePixelRatio') || 1; this._ratio = deviceRatio / backingStoreRatio; } /*#*/ } // __options.environment == 'browser' View.call(this, canvas); }, _setViewSize: function(size) { var width = size.width, height = size.height, ratio = this._ratio, element = this._element, style = element.style; // Upscale the canvas if the two ratios don't match. element.width = width * ratio; element.height = height * ratio; if (ratio !== 1) { style.width = width + 'px'; style.height = height + 'px'; // Now scale the context to counter the fact that we've manually // scaled our canvas element. this._context.scale(ratio, ratio); } }, /** * Draws the view. * * @name View#draw * @function */ draw: function(checkRedraw) { if (checkRedraw && !this._project._needsRedraw) return false; // Initial tests conclude that clearing the canvas using clearRect // is always faster than setting canvas.width = canvas.width // http://jsperf.com/clearrect-vs-setting-width/7 var ctx = this._context, size = this._viewSize; ctx.clearRect(0, 0, size.width + 1, size.height + 1); this._project.draw(ctx, this._matrix, this._ratio); this._project._needsRedraw = false; return true; } }, new function() { // Item based mouse handling: var downPoint, lastPoint, overPoint, downItem, lastItem, overItem, hasDrag, dblClick, clickTime; // Returns true if event was stopped, false otherwise, whether handler was // called or not! function callEvent(view, type, event, point, target, lastPoint) { var item = target, mouseEvent; function call(obj) { if (obj.responds(type)) { // Only produce the event object if we really need it, and then // reuse it if we're bubbling. if (!mouseEvent) { mouseEvent = new MouseEvent(type, event, point, target, // Calculate delta if lastPoint was passed lastPoint ? point.subtract(lastPoint) : null); } if (obj.fire(type, mouseEvent) && mouseEvent.isStopped) { // Call preventDefault() on native event if mouse event was // handled here. event.preventDefault(); return true; } } } // Bubble up the DOM and find a parent that responds to this event. while (item) { if (call(item)) return true; item = item.getParent(); } // Also call event handler on view, if installed. if (call(view)) return true; return false; } return { /** * Returns true if event was stopped, false otherwise, whether handler * was called or not! */ _handleEvent: function(type, point, event) { // Drop out if we don't have any event handlers for this type if (!this._eventCounters[type]) return; // Run the hit-test first var project = this._project, hit = project.hitTest(point, { tolerance: project.options.hitTolerance || 0, fill: true, stroke: true }), item = hit && hit.item, stopped = false; // Now handle the mouse events switch (type) { case 'mousedown': stopped = callEvent(this, type, event, point, item); // See if we're clicking again on the same item, within the // double-click time. Firefox uses 300ms as the max time // difference: dblClick = lastItem == item && (Date.now() - clickTime < 300); downItem = lastItem = item; downPoint = lastPoint = overPoint = point; hasDrag = downItem && downItem.responds('mousedrag'); break; case 'mouseup': // stopping mousup events does not prevent mousedrag / mousemove // hanlding here, but it does click / doubleclick stopped = callEvent(this, type, event, point, item, downPoint); if (hasDrag) { // If the point has changed since the last mousedrag event, // send another one if (lastPoint && !lastPoint.equals(point)) callEvent(this, 'mousedrag', event, point, downItem, lastPoint); // If we end up over another item, send it a mousemove event // now. Use point as overPoint, so delta is (0, 0) since // this will be the first mousemove event for this item. if (item !== downItem) { overPoint = point; callEvent(this, 'mousemove', event, point, item, overPoint); } } if (!stopped && item && item === downItem) { clickTime = Date.now(); callEvent(this, dblClick && downItem.responds('doubleclick') ? 'doubleclick' : 'click', event, downPoint, item); dblClick = false; } downItem = null; hasDrag = false; break; case 'mousemove': // Allow both mousedrag and mousemove events to stop mousemove // events from reaching tools. if (hasDrag) stopped = callEvent(this, 'mousedrag', event, point, downItem, lastPoint); // TODO: Consider implementing this again? "If we have a // mousedrag event, do not send mousemove events to any // item while we're dragging." // For now, we let other items receive mousemove events even // during a drag event. // If we change the overItem, reset overPoint to point so // delta is (0, 0) if (!stopped) { if (item !== overItem) overPoint = point; stopped = callEvent(this, type, event, point, item, overPoint); } lastPoint = overPoint = point; if (item !== overItem) { callEvent(this, 'mouseleave', event, point, overItem); overItem = item; callEvent(this, 'mouseenter', event, point, item); } break; } return stopped; } }; }); /*#*/ if (__options.environment == 'node') { // Node.js based image exporting code. CanvasView.inject(new function() { // 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; } var fs = require('fs'); return { // DOCS: CanvasView#exportFrames(param); exportFrames: function(param) { param = new Base({ 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, startTime = Date.now(), lastTime = startTime; // Start exporting frames by exporting the first frame: exportFrame(param); function exportFrame(param) { var filename = param.prefix + toPaddedString(count, 6) + '.png', path = param.directory + '/' + filename; var out = view.exportImage(path, 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(); } } }); // Use new Base() to convert into a Base object, for #toString() view.fire('frame', new Base({ delta: frameDuration, time: frameDuration * count, count: count })); count++; } }, // DOCS: View#exportImage(path, callback); exportImage: function(path, callback) { this.draw(); var out = fs.createWriteStream(path), stream = this._element.createPNGStream(); // Pipe the png stream to the write stream: stream.pipe(out); if (callback) { out.on('close', callback); } return out; } }; }); /*#*/ } // __options.environment == 'node'