mirror of
https://github.com/scratchfoundation/paper.js.git
synced 2025-01-07 13:22:07 -05:00
285 lines
8.2 KiB
JavaScript
285 lines
8.2 KiB
JavaScript
/*
|
|
* 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 arguments: '
|
|
+ arguments);
|
|
canvas = CanvasProvider.getCanvas(size);
|
|
}
|
|
var ctx = this._context = canvas.getContext('2d');
|
|
// Have Item count installed mouse events.
|
|
this._eventCounters = {};
|
|
// Hi-DPI Canvas support based on:
|
|
// http://www.html5rocks.com/en/tutorials/canvas/hidpi/
|
|
var ratio = (window.devicePixelRatio || 1) / (DomElement.getPrefixValue(
|
|
ctx, 'backingStorePixelRatio') || 1);
|
|
// Upscale the canvas if the two ratios don't match.
|
|
if (ratio > 1) {
|
|
var width = canvas.clientWidth,
|
|
height = canvas.clientHeight,
|
|
style = canvas.style;
|
|
canvas.width = width * ratio;
|
|
canvas.height = height * ratio;
|
|
style.width = width + 'px';
|
|
style.height = height + 'px';
|
|
// Now scale the context to counter the fact that we've manually
|
|
// scaled our canvas element.
|
|
ctx.scale(ratio, ratio);
|
|
}
|
|
View.call(this, canvas);
|
|
},
|
|
|
|
/**
|
|
* 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._project._needsRedraw = false;
|
|
return true;
|
|
}
|
|
}, new function() { // Item based mouse handling:
|
|
|
|
var downPoint,
|
|
lastPoint,
|
|
overPoint,
|
|
downItem,
|
|
lastItem,
|
|
overItem,
|
|
hasDrag,
|
|
doubleClick,
|
|
clickTime;
|
|
|
|
// Returns false if event was stopped, true otherwise, whether handler was
|
|
// called or not!
|
|
function callEvent(type, event, point, target, lastPoint, bubble) {
|
|
var item = target,
|
|
mouseEvent;
|
|
while (item) {
|
|
if (item.responds(type)) {
|
|
// Create an reuse the event object 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 (item.fire(type, mouseEvent)
|
|
&& (!bubble || mouseEvent._stopped))
|
|
return false;
|
|
}
|
|
item = item.getParent();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function handleEvent(view, type, event, point, lastPoint) {
|
|
if (view._eventCounters[type]) {
|
|
var project = view._project,
|
|
hit = project.hitTest(point, {
|
|
tolerance: project.options.hitTolerance || 0,
|
|
fill: true,
|
|
stroke: true
|
|
}),
|
|
item = hit && hit.item;
|
|
if (item) {
|
|
// If this is a mousemove event and we change the overItem,
|
|
// reset lastPoint to point so delta is (0, 0)
|
|
if (type === 'mousemove' && item != overItem)
|
|
lastPoint = point;
|
|
// If we have a downItem with a mousedrag event, do not send
|
|
// mousemove events to any item while we're dragging.
|
|
// TODO: Do we also need to lock mousenter / mouseleave in the
|
|
// same way?
|
|
if (type !== 'mousemove' || !hasDrag)
|
|
callEvent(type, event, point, item, lastPoint);
|
|
return item;
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
_onMouseDown: function(event, point) {
|
|
var item = handleEvent(this, 'mousedown', event, point);
|
|
// See if we're clicking again on the same item, within the
|
|
// double-click time. Firefox uses 300ms as the max time difference:
|
|
doubleClick = lastItem == item && (Date.now() - clickTime < 300);
|
|
downItem = lastItem = item;
|
|
downPoint = lastPoint = overPoint = point;
|
|
hasDrag = downItem && downItem.responds('mousedrag');
|
|
},
|
|
|
|
_onMouseUp: function(event, point) {
|
|
// TODO: Check
|
|
var item = handleEvent(this, 'mouseup', event, point);
|
|
if (hasDrag) {
|
|
// If the point has changed since the last mousedrag event, send
|
|
// another one
|
|
if (lastPoint && !lastPoint.equals(point))
|
|
callEvent('mousedrag', event, point, downItem, lastPoint);
|
|
// If we had a mousedrag event locking mousemove events and are
|
|
// 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('mousemove', event, point, item, overPoint);
|
|
}
|
|
}
|
|
if (item === downItem) {
|
|
clickTime = Date.now();
|
|
if (!doubleClick
|
|
// callEvent returns false if event is stopped.
|
|
|| callEvent('doubleclick', event, downPoint, item))
|
|
callEvent('click', event, downPoint, item);
|
|
doubleClick = false;
|
|
}
|
|
downItem = null;
|
|
hasDrag = false;
|
|
},
|
|
|
|
_onMouseMove: function(event, point) {
|
|
// Call the mousedrag event first if an item was clicked earlier
|
|
if (downItem)
|
|
callEvent('mousedrag', event, point, downItem, lastPoint);
|
|
var item = handleEvent(this, 'mousemove', event, point, overPoint);
|
|
lastPoint = overPoint = point;
|
|
if (item !== overItem) {
|
|
callEvent('mouseleave', event, point, overItem);
|
|
overItem = item;
|
|
callEvent('mouseenter', event, point, item);
|
|
}
|
|
}
|
|
};
|
|
});
|
|
|
|
/*#*/ 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 = 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,
|
|
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 Base.merge to convert into a Base object, for #toString()
|
|
view.fire('frame', Base.merge({
|
|
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'
|