2011-05-16 08:33:15 -04:00
|
|
|
/*
|
|
|
|
* Paper.js
|
2011-06-30 06:01:51 -04:00
|
|
|
*
|
2011-05-16 08:33:15 -04:00
|
|
|
* This file is part of Paper.js, a JavaScript Vector Graphics Library,
|
|
|
|
* based on Scriptographer.org and designed to be largely API compatible.
|
|
|
|
* http://paperjs.org/
|
|
|
|
* http://scriptographer.org/
|
2011-06-30 06:01:51 -04:00
|
|
|
*
|
2011-05-16 08:33:15 -04:00
|
|
|
* Copyright (c) 2011, Juerg Lehni & Jonathan Puckey
|
|
|
|
* http://lehni.org/ & http://jonathanpuckey.com/
|
2011-06-30 06:01:51 -04:00
|
|
|
*
|
2011-07-01 06:17:45 -04:00
|
|
|
* Distributed under the MIT license. See LICENSE file for details.
|
|
|
|
*
|
2011-05-16 08:33:15 -04:00
|
|
|
* All rights reserved.
|
|
|
|
*/
|
|
|
|
|
2011-06-22 18:56:05 -04:00
|
|
|
/**
|
|
|
|
* @name View
|
2011-06-27 09:13:24 -04:00
|
|
|
*
|
|
|
|
* @class The View object wraps a canvas element and handles drawing and user
|
2011-06-28 03:20:42 -04:00
|
|
|
* interaction through mouse and keyboard for it. It offer means to scroll the
|
2011-06-27 09:13:24 -04:00
|
|
|
* view, find the currently visible bounds in project coordinates, or the
|
2011-06-28 05:35:08 -04:00
|
|
|
* center, both useful for constructing artwork that should appear centered on
|
2011-06-27 09:13:24 -04:00
|
|
|
* screen.
|
2011-06-22 18:56:05 -04:00
|
|
|
*/
|
2011-08-02 05:08:08 -04:00
|
|
|
var View = this.View = PaperScopeItem.extend(/** @lends View# */{
|
|
|
|
_list: 'views',
|
|
|
|
_reference: 'view',
|
|
|
|
|
2011-05-23 14:10:25 -04:00
|
|
|
/**
|
|
|
|
* Creates a view object
|
2011-08-01 11:21:00 -04:00
|
|
|
* @param {HTMLCanvasElement|String} canvas The canvas object that this
|
|
|
|
* view should wrap, or the String id that represents it
|
2011-05-23 14:10:25 -04:00
|
|
|
*/
|
2011-05-16 08:33:15 -04:00
|
|
|
initialize: function(canvas) {
|
2011-08-02 05:08:08 -04:00
|
|
|
this.base();
|
2011-05-16 08:33:15 -04:00
|
|
|
// Handle canvas argument
|
|
|
|
var size;
|
2011-08-02 05:02:04 -04:00
|
|
|
|
|
|
|
/*#*/ 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) {
|
2011-08-02 04:49:40 -04:00
|
|
|
if (typeof canvas === 'string')
|
|
|
|
canvas = document.getElementById(canvas);
|
|
|
|
if (canvas instanceof HTMLCanvasElement) {
|
2011-05-16 08:33:15 -04:00
|
|
|
this._canvas = canvas;
|
2011-06-30 11:02:49 -04:00
|
|
|
// If the canvas has the resize attribute, resize the it to fill the
|
2011-05-16 08:33:15 -04:00
|
|
|
// window and resize it again whenever the user resizes the window.
|
2011-07-06 15:19:38 -04:00
|
|
|
if (PaperScript.hasAttribute(canvas, 'resize')) {
|
2011-06-26 04:06:19 -04:00
|
|
|
// Subtract canvas' viewport offset from the total size, to
|
|
|
|
// stretch it in
|
2011-08-02 04:49:40 -04:00
|
|
|
var offset = DomElement.getOffset(canvas, true),
|
2011-06-26 04:06:19 -04:00
|
|
|
that = this;
|
2011-08-02 04:49:40 -04:00
|
|
|
size = DomElement.getViewportBounds(canvas)
|
|
|
|
.getSize().subtract(offset);
|
2011-05-16 08:33:15 -04:00
|
|
|
canvas.width = size.width;
|
|
|
|
canvas.height = size.height;
|
|
|
|
DomEvent.add(window, {
|
|
|
|
resize: function(event) {
|
2011-06-26 04:06:19 -04:00
|
|
|
// Only update canvas offset if it's not invisible, as
|
2011-06-13 19:20:58 -04:00
|
|
|
// otherwise the offset would be wrong.
|
|
|
|
if (!DomElement.isInvisible(canvas))
|
2011-08-02 04:49:40 -04:00
|
|
|
offset = DomElement.getOffset(canvas, true);
|
2011-05-16 08:33:15 -04:00
|
|
|
// Set the size now, which internally calls onResize
|
2011-07-01 06:50:11 -04:00
|
|
|
// and redraws the view
|
2011-08-02 04:49:40 -04:00
|
|
|
that.setViewSize(DomElement.getViewportBounds(canvas)
|
|
|
|
.getSize().subtract(offset));
|
2011-05-16 08:33:15 -04:00
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
2011-06-13 19:20:58 -04:00
|
|
|
size = DomElement.isInvisible(canvas)
|
|
|
|
? Size.create(parseInt(canvas.getAttribute('width')),
|
|
|
|
parseInt(canvas.getAttribute('height')))
|
|
|
|
: DomElement.getSize(canvas);
|
2011-05-16 08:33:15 -04:00
|
|
|
}
|
|
|
|
// TODO: Test this on IE:
|
2011-07-06 15:19:38 -04:00
|
|
|
if (PaperScript.hasAttribute(canvas, 'stats')) {
|
2011-05-16 08:33:15 -04:00
|
|
|
this._stats = new Stats();
|
|
|
|
// Align top-left to the canvas
|
|
|
|
var element = this._stats.domElement,
|
2011-06-26 04:06:19 -04:00
|
|
|
style = element.style,
|
|
|
|
offset = DomElement.getOffset(canvas);
|
2011-05-16 08:33:15 -04:00
|
|
|
style.position = 'absolute';
|
|
|
|
style.left = offset.x + 'px';
|
|
|
|
style.top = offset.y + 'px';
|
|
|
|
document.body.appendChild(element);
|
|
|
|
}
|
|
|
|
} 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);
|
|
|
|
}
|
2011-06-22 03:10:17 -04:00
|
|
|
// Generate an id for this view / canvas if it does not have one
|
|
|
|
this._id = this._canvas.getAttribute('id');
|
|
|
|
if (this._id == null)
|
|
|
|
this._canvas.setAttribute('id', this._id = 'canvas-' + View._id++);
|
2011-08-02 05:02:04 -04:00
|
|
|
/*#*/ } // options.browser
|
|
|
|
|
2011-06-22 03:10:17 -04:00
|
|
|
// Link this id to our view
|
|
|
|
View._views[this._id] = this;
|
2011-06-22 02:56:16 -04:00
|
|
|
this._viewSize = LinkedSize.create(this, 'setViewSize',
|
|
|
|
size.width, size.height);
|
2011-05-16 08:33:15 -04:00
|
|
|
this._context = this._canvas.getContext('2d');
|
|
|
|
this._matrix = new Matrix();
|
|
|
|
this._zoom = 1;
|
2011-08-02 05:02:04 -04:00
|
|
|
|
|
|
|
/*#*/ if (options.browser) {
|
2011-05-16 08:33:15 -04:00
|
|
|
this._events = this._createEvents();
|
|
|
|
DomEvent.add(this._canvas, this._events);
|
|
|
|
// Make sure the first view is focused for keyboard input straight away
|
2011-06-22 03:29:53 -04:00
|
|
|
if (!View._focused)
|
|
|
|
View._focused = this;
|
2011-08-02 05:02:04 -04:00
|
|
|
/*#*/ } // options.browser
|
|
|
|
|
2011-06-19 18:14:36 -04:00
|
|
|
// 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;
|
2011-05-16 08:33:15 -04:00
|
|
|
},
|
|
|
|
|
2011-08-02 05:08:08 -04:00
|
|
|
/**
|
|
|
|
* Makes this view the active one, meaning {@link PaperScope#view} will
|
|
|
|
* point to it.
|
|
|
|
*
|
|
|
|
* @name View#activate
|
|
|
|
* @function
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Removes thsi view from the {@link PaperScope#views} list and frees the
|
|
|
|
* associated canvas.
|
|
|
|
*/
|
|
|
|
remove: function() {
|
|
|
|
if (!this.base())
|
|
|
|
return false;
|
|
|
|
// Clear focus if removed view had it
|
|
|
|
if (View._focused == this)
|
|
|
|
View._focused = null;
|
|
|
|
delete View._views[this._id];
|
|
|
|
// Uninstall event handlers again for this view.
|
|
|
|
DomEvent.remove(this._canvas, this._events);
|
|
|
|
// Clearing _onFrame makes the frame handler stop automatically.
|
|
|
|
this._canvas = this._events = this._onFrame = null;
|
|
|
|
return true;
|
|
|
|
},
|
|
|
|
|
2011-08-22 05:13:42 -04:00
|
|
|
_transform: function(matrix, flags) {
|
|
|
|
this._matrix.preConcatenate(matrix);
|
|
|
|
// Force recalculation of these values next time they are requested.
|
|
|
|
this._bounds = null;
|
|
|
|
this._inverse = null;
|
|
|
|
},
|
|
|
|
|
2011-06-27 09:13:24 -04:00
|
|
|
/**
|
2011-06-27 09:27:32 -04:00
|
|
|
* The underlying native canvas element.
|
2011-06-27 09:13:24 -04:00
|
|
|
*
|
2011-06-27 09:27:32 -04:00
|
|
|
* @type HTMLCanvasElement
|
2011-06-27 09:13:24 -04:00
|
|
|
* @bean
|
|
|
|
*/
|
2011-06-04 10:15:35 -04:00
|
|
|
getCanvas: function() {
|
|
|
|
return this._canvas;
|
|
|
|
},
|
|
|
|
|
2011-05-23 14:10:25 -04:00
|
|
|
/**
|
2011-06-22 02:56:16 -04:00
|
|
|
* The size of the view canvas. Changing the view's size will resize it's
|
2011-06-27 09:13:24 -04:00
|
|
|
* underlying canvas.
|
2011-06-30 06:01:51 -04:00
|
|
|
*
|
2011-06-22 02:56:16 -04:00
|
|
|
* @type Size
|
2011-05-23 14:10:25 -04:00
|
|
|
* @bean
|
|
|
|
*/
|
2011-06-22 02:56:16 -04:00
|
|
|
getViewSize: function() {
|
|
|
|
return this._viewSize;
|
2011-05-16 08:33:15 -04:00
|
|
|
},
|
|
|
|
|
2011-06-22 02:56:16 -04:00
|
|
|
setViewSize: function(size) {
|
|
|
|
size = Size.read(arguments);
|
|
|
|
var delta = size.subtract(this._viewSize);
|
2011-07-01 06:49:27 -04:00
|
|
|
if (delta.isZero())
|
|
|
|
return;
|
2011-05-16 08:33:15 -04:00
|
|
|
this._canvas.width = size.width;
|
|
|
|
this._canvas.height = size.height;
|
2011-07-13 08:10:26 -04:00
|
|
|
// Update _viewSize but don't notify of change.
|
|
|
|
this._viewSize.set(size.width, size.height, true);
|
|
|
|
// Force recalculation
|
|
|
|
this._bounds = null;
|
|
|
|
this._redrawNeeded = true;
|
2011-05-16 08:33:15 -04:00
|
|
|
// Call onResize handler on any size change
|
|
|
|
if (this.onResize) {
|
|
|
|
this.onResize({
|
|
|
|
size: size,
|
|
|
|
delta: delta
|
|
|
|
});
|
|
|
|
}
|
2011-07-01 06:50:11 -04:00
|
|
|
if (this._onFrameCallback) {
|
|
|
|
// If there's a _onFrameCallback, call it staight away,
|
|
|
|
// but without requesting another animation frame.
|
|
|
|
this._onFrameCallback(0, true);
|
|
|
|
} else {
|
|
|
|
// Otherwise simply redraw the view now
|
|
|
|
this.draw(true);
|
|
|
|
}
|
2011-05-16 08:33:15 -04:00
|
|
|
},
|
|
|
|
|
2011-05-23 14:10:25 -04:00
|
|
|
/**
|
2011-06-27 09:13:24 -04:00
|
|
|
* The bounds of the currently visible area in project coordinates.
|
2011-06-30 06:01:51 -04:00
|
|
|
*
|
2011-06-10 08:13:55 -04:00
|
|
|
* @type Rectangle
|
2011-05-23 14:10:25 -04:00
|
|
|
* @bean
|
|
|
|
*/
|
2011-05-16 08:33:15 -04:00
|
|
|
getBounds: function() {
|
|
|
|
if (!this._bounds)
|
2011-08-22 05:12:40 -04:00
|
|
|
this._bounds = this._getInverse()._transformBounds(
|
2011-06-22 02:56:16 -04:00
|
|
|
new Rectangle(new Point(), this._viewSize));
|
2011-05-16 08:33:15 -04:00
|
|
|
return this._bounds;
|
|
|
|
},
|
|
|
|
|
2011-05-23 14:10:25 -04:00
|
|
|
/**
|
2011-06-27 09:13:24 -04:00
|
|
|
* The size of the visible area in project coordinates.
|
|
|
|
*
|
2011-05-23 14:10:25 -04:00
|
|
|
* @type Size
|
|
|
|
* @bean
|
|
|
|
*/
|
2011-05-16 08:33:15 -04:00
|
|
|
getSize: function() {
|
|
|
|
return this.getBounds().getSize();
|
|
|
|
},
|
|
|
|
|
2011-05-23 14:10:25 -04:00
|
|
|
/**
|
2011-06-27 09:13:24 -04:00
|
|
|
* The center of the visible area in project coordinates.
|
|
|
|
*
|
2011-05-23 14:10:25 -04:00
|
|
|
* @type Point
|
|
|
|
* @bean
|
|
|
|
*/
|
2011-05-16 08:33:15 -04:00
|
|
|
getCenter: function() {
|
|
|
|
return this.getBounds().getCenter();
|
|
|
|
},
|
|
|
|
|
|
|
|
setCenter: function(center) {
|
|
|
|
this.scrollBy(Point.read(arguments).subtract(this.getCenter()));
|
|
|
|
},
|
|
|
|
|
2011-05-23 14:10:25 -04:00
|
|
|
/**
|
2011-06-27 09:13:24 -04:00
|
|
|
* The zoom factor by which the project coordinates are magnified.
|
2011-06-30 06:01:51 -04:00
|
|
|
*
|
2011-05-27 14:15:15 -04:00
|
|
|
* @type Number
|
2011-05-23 14:10:25 -04:00
|
|
|
* @bean
|
|
|
|
*/
|
2011-05-16 08:33:15 -04:00
|
|
|
getZoom: function() {
|
|
|
|
return this._zoom;
|
|
|
|
},
|
|
|
|
|
|
|
|
setZoom: function(zoom) {
|
|
|
|
// TODO: Clamp the view between 1/32 and 64, just like Illustrator?
|
2011-05-17 08:43:10 -04:00
|
|
|
this._transform(new Matrix().scale(zoom / this._zoom, this.getCenter()));
|
2011-05-16 08:33:15 -04:00
|
|
|
this._zoom = zoom;
|
|
|
|
},
|
|
|
|
|
2011-06-20 17:51:39 -04:00
|
|
|
/**
|
|
|
|
* Checks whether the view is currently visible within the current browser
|
|
|
|
* viewport.
|
2011-06-30 06:01:51 -04:00
|
|
|
*
|
2011-06-20 17:51:39 -04:00
|
|
|
* @return {Boolean} Whether the view is visible.
|
|
|
|
*/
|
|
|
|
isVisible: function() {
|
|
|
|
// TODO: Take bounds into account if it's not the full canvas?
|
|
|
|
return DomElement.isVisible(this._canvas);
|
|
|
|
},
|
|
|
|
|
2011-05-23 14:10:25 -04:00
|
|
|
/**
|
2011-06-27 09:13:24 -04:00
|
|
|
* Scrolls the view by the given vector.
|
|
|
|
*
|
2011-05-23 14:10:25 -04:00
|
|
|
* @param {Point} point
|
|
|
|
*/
|
2011-05-16 08:33:15 -04:00
|
|
|
scrollBy: function(point) {
|
2011-05-17 08:43:10 -04:00
|
|
|
this._transform(new Matrix().translate(Point.read(arguments).negate()));
|
2011-05-16 08:33:15 -04:00
|
|
|
},
|
|
|
|
|
2011-08-02 05:08:08 -04:00
|
|
|
/**
|
|
|
|
* Draws the view.
|
|
|
|
*
|
|
|
|
* @name View#draw
|
|
|
|
* @function
|
|
|
|
*/
|
2011-06-19 18:03:18 -04:00
|
|
|
draw: function(checkRedraw) {
|
|
|
|
if (checkRedraw && !this._redrawNeeded)
|
|
|
|
return false;
|
2011-05-16 08:33:15 -04:00
|
|
|
if (this._stats)
|
|
|
|
this._stats.update();
|
|
|
|
// 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
|
2011-06-19 18:03:18 -04:00
|
|
|
var ctx = this._context,
|
2011-06-22 02:56:16 -04:00
|
|
|
size = this._viewSize;
|
|
|
|
ctx.clearRect(0, 0, size._width + 1, size._height + 1);
|
2011-05-17 08:42:20 -04:00
|
|
|
|
|
|
|
ctx.save();
|
|
|
|
this._matrix.applyToContext(ctx);
|
2011-05-17 08:25:46 -04:00
|
|
|
// Just draw the active project for now
|
2011-05-17 08:42:20 -04:00
|
|
|
this._scope.project.draw(ctx);
|
|
|
|
ctx.restore();
|
2011-06-19 18:14:36 -04:00
|
|
|
if (this._redrawNeeded) {
|
|
|
|
this._redrawNeeded = false;
|
|
|
|
// Update _redrawNotified in PaperScope as soon as a view was drawn
|
|
|
|
this._scope._redrawNotified = false;
|
|
|
|
}
|
2011-06-19 18:03:18 -04:00
|
|
|
return true;
|
2011-05-16 08:33:15 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
// TODO: getInvalidBounds
|
|
|
|
// TODO: invalidate(rect)
|
|
|
|
// TODO: style: artwork / preview / raster / opaque / ink
|
|
|
|
// TODO: getShowGrid
|
|
|
|
// TODO: getMousePoint
|
2011-06-27 09:15:29 -04:00
|
|
|
// TODO: projectToView(rect)
|
2011-05-17 08:42:45 -04:00
|
|
|
|
2011-06-27 09:15:29 -04:00
|
|
|
projectToView: function(point) {
|
2011-05-16 08:33:15 -04:00
|
|
|
return this._matrix._transformPoint(Point.read(arguments));
|
|
|
|
},
|
|
|
|
|
2011-06-27 09:15:29 -04:00
|
|
|
viewToProject: function(point) {
|
2011-05-16 08:33:15 -04:00
|
|
|
return this._getInverse()._transformPoint(Point.read(arguments));
|
|
|
|
},
|
|
|
|
|
2011-05-17 08:43:45 -04:00
|
|
|
_getInverse: function() {
|
|
|
|
if (!this._inverse)
|
|
|
|
this._inverse = this._matrix.createInverse();
|
|
|
|
return this._inverse;
|
|
|
|
},
|
|
|
|
|
2011-05-16 08:33:15 -04:00
|
|
|
/**
|
2011-05-31 08:24:13 -04:00
|
|
|
* {@grouptitle Event Handlers}
|
2011-05-23 14:10:25 -04:00
|
|
|
* Handler function to be called on each frame of an animation.
|
2011-05-31 08:24:13 -04:00
|
|
|
* The function receives an event object which contains information about
|
|
|
|
* the frame event:
|
2011-06-30 06:01:51 -04:00
|
|
|
*
|
2011-06-02 08:17:02 -04:00
|
|
|
* <b>{@code event.count}</b>: the number of times the frame event was fired.
|
|
|
|
* <b>{@code event.time}</b>: the total amount of time passed since the first frame
|
2011-05-31 08:24:13 -04:00
|
|
|
* event in seconds.
|
2011-06-02 08:17:02 -04:00
|
|
|
* <b>{@code event.delta}</b>: the time passed in seconds since the last frame
|
2011-05-31 08:24:13 -04:00
|
|
|
* event.
|
2011-06-30 06:01:51 -04:00
|
|
|
*
|
2011-06-10 08:21:27 -04:00
|
|
|
* @example {@paperscript}
|
2011-05-31 08:24:13 -04:00
|
|
|
* // Creating an animation:
|
2011-06-30 06:01:51 -04:00
|
|
|
*
|
2011-06-10 08:21:27 -04:00
|
|
|
* // Create a rectangle shaped path with its top left point at:
|
|
|
|
* // {x: 50, y: 25} and a size of {width: 50, height: 50}
|
|
|
|
* var path = new Path.Rectangle(new Point(50, 25), new Size(50, 50));
|
2011-05-31 08:24:13 -04:00
|
|
|
* path.fillColor = 'black';
|
2011-06-30 06:01:51 -04:00
|
|
|
*
|
2011-05-31 08:24:13 -04:00
|
|
|
* function onFrame(event) {
|
2011-06-10 08:21:27 -04:00
|
|
|
* // Every frame, rotate the path by 3 degrees:
|
|
|
|
* path.rotate(3);
|
2011-05-31 08:24:13 -04:00
|
|
|
* }
|
2011-06-30 06:01:51 -04:00
|
|
|
*
|
2011-06-27 09:34:32 -04:00
|
|
|
* @type Function
|
2011-05-23 14:10:25 -04:00
|
|
|
* @bean
|
2011-05-16 08:33:15 -04:00
|
|
|
*/
|
|
|
|
getOnFrame: function() {
|
|
|
|
return this._onFrame;
|
|
|
|
},
|
|
|
|
|
|
|
|
setOnFrame: function(onFrame) {
|
|
|
|
this._onFrame = onFrame;
|
|
|
|
if (!onFrame) {
|
|
|
|
delete this._onFrameCallback;
|
|
|
|
return;
|
|
|
|
}
|
2011-08-02 05:02:04 -04:00
|
|
|
/*#*/ if (options.browser) {
|
2011-05-16 08:33:15 -04:00
|
|
|
var that = this,
|
|
|
|
requested = false,
|
|
|
|
before,
|
|
|
|
time = 0,
|
|
|
|
count = 0;
|
|
|
|
this._onFrameCallback = function(param, dontRequest) {
|
|
|
|
requested = false;
|
|
|
|
if (!that._onFrame)
|
|
|
|
return;
|
|
|
|
// Set the global paper object to the current scope
|
|
|
|
paper = that._scope;
|
|
|
|
// Request next frame already
|
|
|
|
requested = true;
|
|
|
|
if (!dontRequest) {
|
|
|
|
DomEvent.requestAnimationFrame(that._onFrameCallback,
|
|
|
|
that._canvas);
|
|
|
|
}
|
|
|
|
var now = Date.now() / 1000,
|
|
|
|
delta = before ? now - before : 0;
|
2011-07-07 08:10:02 -04:00
|
|
|
// Use Base.merge to convert into a Base object, for #toString()
|
|
|
|
that._onFrame(Base.merge({
|
2011-05-16 08:33:15 -04:00
|
|
|
delta: delta, // Time elapsed since last redraw in seconds
|
|
|
|
time: time += delta, // Time since first call of frame() in seconds
|
|
|
|
count: count++
|
2011-07-07 08:10:02 -04:00
|
|
|
}));
|
2011-05-16 08:33:15 -04:00
|
|
|
before = now;
|
|
|
|
// Automatically draw view on each frame.
|
2011-06-19 18:05:39 -04:00
|
|
|
that.draw(true);
|
2011-05-16 08:33:15 -04:00
|
|
|
};
|
|
|
|
// Call the onFrame handler straight away, initializing the sequence
|
|
|
|
// of onFrame calls.
|
|
|
|
if (!requested)
|
|
|
|
this._onFrameCallback();
|
2011-08-02 05:02:04 -04:00
|
|
|
/*#*/ } // options.browser
|
|
|
|
},
|
|
|
|
|
|
|
|
|
2011-05-16 08:33:15 -04:00
|
|
|
|
2011-05-31 08:24:13 -04:00
|
|
|
/**
|
|
|
|
* Handler function that is called whenever a view is resized.
|
2011-06-30 06:01:51 -04:00
|
|
|
*
|
2011-05-31 08:24:13 -04:00
|
|
|
* @example
|
|
|
|
* // Repositioning items when a view is resized:
|
2011-06-30 06:01:51 -04:00
|
|
|
*
|
2011-05-31 08:24:13 -04:00
|
|
|
* // Create a circle shaped path in the center of the view:
|
|
|
|
* var path = new Path.Circle(view.bounds.center, 30);
|
|
|
|
* path.fillColor = 'red';
|
2011-06-30 06:01:51 -04:00
|
|
|
*
|
2011-05-31 08:24:13 -04:00
|
|
|
* function onResize(event) {
|
|
|
|
* // Whenever the view is resized, move the path to its center:
|
|
|
|
* path.position = view.center;
|
|
|
|
* }
|
2011-06-30 06:01:51 -04:00
|
|
|
*
|
2011-06-27 09:34:32 -04:00
|
|
|
* @type Function
|
2011-05-31 08:24:13 -04:00
|
|
|
*/
|
2011-06-20 19:38:11 -04:00
|
|
|
onResize: null
|
2011-08-02 05:02:04 -04:00
|
|
|
}, {
|
|
|
|
statics: {
|
|
|
|
_views: {},
|
|
|
|
_id: 0
|
|
|
|
}
|
2011-08-22 05:05:59 -04:00
|
|
|
}, new function() {
|
|
|
|
// Injection scope for special code on browser (mouse events)
|
|
|
|
// and server (rendering)
|
2011-08-02 05:02:04 -04:00
|
|
|
/*#*/ if (options.browser) {
|
2011-06-20 19:42:39 -04:00
|
|
|
var tool,
|
2011-06-20 19:38:11 -04:00
|
|
|
timer,
|
|
|
|
curPoint,
|
2011-06-22 03:29:53 -04:00
|
|
|
tempFocus,
|
2011-06-20 19:38:11 -04:00
|
|
|
dragging = false;
|
|
|
|
|
2011-06-27 09:15:29 -04:00
|
|
|
function viewToProject(view, event) {
|
|
|
|
return view.viewToProject(DomEvent.getOffset(event, view._canvas));
|
2011-06-20 19:38:11 -04:00
|
|
|
}
|
2011-05-16 08:33:15 -04:00
|
|
|
|
2011-06-22 03:29:53 -04:00
|
|
|
function updateFocus() {
|
|
|
|
if (!View._focused || !View._focused.isVisible()) {
|
|
|
|
// Find the first visible view in all scopes
|
|
|
|
PaperScope.each(function(scope) {
|
|
|
|
for (var i = 0, l = scope.views.length; i < l; i++) {
|
|
|
|
var view = scope.views[i];
|
|
|
|
if (view.isVisible()) {
|
|
|
|
View._focused = tempFocus = view;
|
|
|
|
throw Base.stop;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-06-20 19:38:11 -04:00
|
|
|
function mousemove(event) {
|
2011-06-22 03:29:53 -04:00
|
|
|
var view;
|
|
|
|
if (!dragging) {
|
|
|
|
// See if we can get the view from the current event target, and
|
|
|
|
// handle the mouse move over it.
|
|
|
|
view = View._views[DomEvent.getTarget(event).getAttribute('id')];
|
|
|
|
if (view) {
|
|
|
|
// Temporarily focus this view without making it sticky, so
|
|
|
|
// Key events are handled too during the mouse over
|
|
|
|
View._focused = tempFocus = view;
|
|
|
|
} else if (tempFocus && tempFocus == View._focused) {
|
|
|
|
// Clear temporary focus again and update it.
|
|
|
|
View._focused = null;
|
|
|
|
updateFocus();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!(view = view || View._focused) || !(tool = view._scope.tool))
|
2011-06-20 19:38:11 -04:00
|
|
|
return;
|
2011-06-27 09:15:29 -04:00
|
|
|
var point = event && viewToProject(view, event);
|
2011-06-20 19:38:11 -04:00
|
|
|
var onlyMove = !!(!tool.onMouseDrag && tool.onMouseMove);
|
|
|
|
if (dragging && !onlyMove) {
|
|
|
|
curPoint = point || curPoint;
|
2011-06-22 03:09:22 -04:00
|
|
|
if (curPoint && tool.onHandleEvent('mousedrag', curPoint, event)) {
|
2011-06-20 19:38:11 -04:00
|
|
|
view.draw(true);
|
2011-06-22 03:09:22 -04:00
|
|
|
DomEvent.stop(event);
|
|
|
|
}
|
2011-06-20 19:38:11 -04:00
|
|
|
// PORT: If there is only an onMouseMove handler, also call it when
|
|
|
|
// the user is dragging:
|
|
|
|
} else if ((!dragging || onlyMove)
|
|
|
|
&& tool.onHandleEvent('mousemove', point, event)) {
|
|
|
|
view.draw(true);
|
2011-06-22 03:09:22 -04:00
|
|
|
DomEvent.stop(event);
|
2011-05-16 08:33:15 -04:00
|
|
|
}
|
2011-06-20 19:38:11 -04:00
|
|
|
}
|
2011-05-16 08:33:15 -04:00
|
|
|
|
2011-06-20 19:38:11 -04:00
|
|
|
function mouseup(event) {
|
2011-06-22 03:29:53 -04:00
|
|
|
var view = View._focused;
|
2011-06-20 19:38:11 -04:00
|
|
|
if (!view || !dragging)
|
|
|
|
return;
|
|
|
|
dragging = false;
|
|
|
|
curPoint = null;
|
|
|
|
if (tool) {
|
|
|
|
if (timer != null)
|
|
|
|
timer = clearInterval(timer);
|
2011-06-27 09:15:29 -04:00
|
|
|
if (tool.onHandleEvent('mouseup', viewToProject(view, event), event)) {
|
2011-06-20 19:38:11 -04:00
|
|
|
view.draw(true);
|
2011-06-22 03:09:22 -04:00
|
|
|
DomEvent.stop(event);
|
|
|
|
}
|
2011-05-16 08:33:15 -04:00
|
|
|
}
|
2011-06-20 19:38:11 -04:00
|
|
|
}
|
2011-05-16 08:33:15 -04:00
|
|
|
|
2011-06-20 19:38:11 -04:00
|
|
|
function selectstart(event) {
|
|
|
|
// Only stop this even if we're dragging already, since otherwise no
|
|
|
|
// text whatsoever can be selected on the page.
|
|
|
|
if (dragging)
|
|
|
|
DomEvent.stop(event);
|
|
|
|
}
|
2011-05-16 08:33:15 -04:00
|
|
|
|
2011-06-20 19:38:11 -04:00
|
|
|
// mousemove and mouseup events need to be installed on document, not the
|
|
|
|
// view canvas, since we want to catch the end of drag events even outside
|
|
|
|
// our view. Only the mousedown events are installed on the view, as handled
|
|
|
|
// by _createEvents below.
|
|
|
|
|
|
|
|
DomEvent.add(document, {
|
|
|
|
mousemove: mousemove,
|
|
|
|
mouseup: mouseup,
|
|
|
|
touchmove: mousemove,
|
|
|
|
touchend: mouseup,
|
2011-06-21 16:49:36 -04:00
|
|
|
selectstart: selectstart,
|
|
|
|
scroll: updateFocus
|
2011-06-20 19:38:11 -04:00
|
|
|
});
|
|
|
|
|
2011-06-22 03:10:54 -04:00
|
|
|
DomEvent.add(window, {
|
|
|
|
load: updateFocus
|
|
|
|
});
|
|
|
|
|
2011-06-20 19:38:11 -04:00
|
|
|
return {
|
|
|
|
_createEvents: function() {
|
2011-06-20 19:42:39 -04:00
|
|
|
var view = this;
|
2011-06-20 19:38:11 -04:00
|
|
|
|
|
|
|
function mousedown(event) {
|
|
|
|
// Tell the Key class which view should receive keyboard input.
|
2011-06-22 03:29:53 -04:00
|
|
|
View._focused = view;
|
2011-06-20 19:38:11 -04:00
|
|
|
if (!(tool = view._scope.tool))
|
|
|
|
return;
|
2011-06-27 09:15:29 -04:00
|
|
|
curPoint = viewToProject(view, event);
|
2011-06-20 19:38:11 -04:00
|
|
|
if (tool.onHandleEvent('mousedown', curPoint, event))
|
|
|
|
view.draw(true);
|
2011-05-16 08:33:15 -04:00
|
|
|
if (tool.eventInterval != null)
|
2011-06-20 19:38:11 -04:00
|
|
|
timer = setInterval(mousemove, tool.eventInterval);
|
|
|
|
dragging = true;
|
2011-05-16 08:33:15 -04:00
|
|
|
}
|
|
|
|
|
2011-06-20 19:38:11 -04:00
|
|
|
return {
|
|
|
|
mousedown: mousedown,
|
|
|
|
touchstart: mousedown,
|
|
|
|
selectstart: selectstart
|
|
|
|
};
|
2011-06-21 16:49:36 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
statics: {
|
2011-06-22 03:10:17 -04:00
|
|
|
|
2011-06-21 16:49:36 -04:00
|
|
|
/**
|
|
|
|
* Loops through all scopes and their views and sets the focus on
|
|
|
|
* the first active one.
|
|
|
|
*/
|
|
|
|
updateFocus: updateFocus
|
2011-06-20 19:38:11 -04:00
|
|
|
}
|
|
|
|
};
|
2011-08-22 05:05:59 -04:00
|
|
|
/*#*/ } else if (options.server) {
|
2011-08-21 10:38:06 -04:00
|
|
|
var path = require('path');
|
2011-08-18 05:11:24 -04:00
|
|
|
// 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 {
|
2011-08-20 09:51:54 -04:00
|
|
|
// DOCS: View#exportFrames(param);
|
2011-08-18 05:11:24 -04:00
|
|
|
exportFrames: function(param) {
|
|
|
|
param = Base.merge({
|
|
|
|
fps: 30,
|
|
|
|
prefix: 'frame-',
|
|
|
|
amount: 1
|
|
|
|
}, param);
|
2011-08-20 09:46:06 -04:00
|
|
|
if (!param.directory) {
|
2011-08-18 05:11:24 -04:00
|
|
|
throw new Error('Missing param.directory');
|
2011-08-20 09:46:06 -04:00
|
|
|
}
|
2011-08-18 05:11:24 -04:00
|
|
|
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',
|
2011-08-20 09:46:06 -04:00
|
|
|
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();
|
2011-08-18 05:11:24 -04:00
|
|
|
}
|
2011-08-20 09:46:06 -04:00
|
|
|
}
|
|
|
|
});
|
2011-08-18 05:11:24 -04:00
|
|
|
if (view.onFrame) {
|
|
|
|
view.onFrame({
|
|
|
|
delta: frameDuration,
|
|
|
|
time: frameDuration * count,
|
|
|
|
count: count
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
2011-08-20 09:51:54 -04:00
|
|
|
// DOCS: View#exportImage(uri, callback);
|
2011-08-20 09:46:06 -04:00
|
|
|
exportImage: function(uri, callback) {
|
2011-08-18 05:11:24 -04:00
|
|
|
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);
|
2011-08-20 09:46:06 -04:00
|
|
|
if (callback) {
|
|
|
|
out.on('close', callback);
|
2011-08-18 05:11:24 -04:00
|
|
|
}
|
|
|
|
return out;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
/*#*/ } // options.server
|
2011-05-16 08:33:15 -04:00
|
|
|
});
|