paper.js/src/ui/View.js

425 lines
11 KiB
JavaScript
Raw Normal View History

/*
* Paper.js
*
* 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/
*
* Distributed under the MIT license. See LICENSE file for details.
*
* Copyright (c) 2011, Juerg Lehni & Jonathan Puckey
* http://lehni.org/ & http://jonathanpuckey.com/
*
* All rights reserved.
*/
2011-05-17 08:29:07 -04:00
var View = this.View = Base.extend({
2011-05-23 14:10:25 -04:00
/** @lends View# */
// DOCS: View: there is alot left to document
// TODO: Add bounds parameter that defines position within canvas?
// Find a good name for these bounds, since #bounds is already the artboard
// bounds of the visible area.
2011-05-23 14:10:25 -04:00
/**
* Creates a view object
* @param {Canvas} canvas
* @constructs View
*
* @class The View object..
*/
initialize: function(canvas) {
// Associate this view with the active paper scope.
this._scope = paper;
// Push it onto project.views and set index:
this._index = this._scope.views.push(this) - 1;
// Handle canvas argument
var size;
if (canvas && canvas instanceof HTMLCanvasElement) {
this._canvas = canvas;
var offset = DomElement.getOffset(canvas);
// If the canvas has the resize attribute, resize the it to fill the
// window and resize it again whenever the user resizes the window.
if (canvas.attributes.resize) {
size = DomElement.getWindowSize().subtract(offset);
canvas.width = size.width;
canvas.height = size.height;
var that = this;
DomEvent.add(window, {
resize: function(event) {
// Only get canvas offset if it's not invisible, as
// otherwise the offset would be wrong.
if (!DomElement.isInvisible(canvas))
offset = DomElement.getOffset(canvas);
// Set the size now, which internally calls onResize
that.setViewSize(
DomElement.getWindowSize().subtract(offset));
// If there's a _onFrameCallback, call it staight away,
// but without requesting another animation frame.
if (that._onFrameCallback) {
that._onFrameCallback(0, true);
} else {
that.draw();
}
}
});
} else {
size = DomElement.isInvisible(canvas)
? Size.create(parseInt(canvas.getAttribute('width')),
parseInt(canvas.getAttribute('height')))
: DomElement.getSize(canvas);
}
// TODO: Test this on IE:
if (canvas.attributes.stats) {
this._stats = new Stats();
// Align top-left to the canvas
var element = this._stats.domElement,
style = element.style;
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);
}
this._viewBounds = LinkedRectangle.create(this, 'setViewBounds',
0, 0, size.width, size.height);
this._context = this._canvas.getContext('2d');
this._matrix = new Matrix();
this._zoom = 1;
this._events = this._createEvents();
DomEvent.add(this._canvas, this._events);
// Make sure the first view is focused for keyboard input straight away
2011-05-17 08:29:07 -04:00
if (!View.focused)
View.focused = this;
},
2011-06-04 10:15:35 -04:00
getCanvas: function() {
return this._canvas;
},
2011-05-23 14:10:25 -04:00
/**
* The bounds of the view, i.e. the bounds of the part of the project which
* is visible in the window.
*
* @type Rectangle
* @bean
*/
getViewBounds: function() {
return this._viewBounds;
},
setViewBounds: function(bounds) {
bounds = Rectangle.read(arguments);
var size = bounds.getSize(),
delta = size.subtract(this._viewBounds.getSize());
// TODO: Take into acount bounds.x/y and decide on what grounds to
// change canvas size. Also, if x/y is not 0, do we need to add that
// to transform, or is that up to the user?
this._canvas.width = size.width;
this._canvas.height = size.height;
// Call onResize handler on any size change
if (this.onResize) {
this.onResize({
size: size,
delta: delta
});
}
// Force recalculation
this._bounds = null;
},
2011-05-23 14:10:25 -04:00
/**
* @type Size
* @bean
*/
getViewSize: function() {
return this._viewBounds.getSize();
},
setViewSize: function(size) {
this._viewBounds.setSize.apply(this._viewBounds, arguments);
},
2011-05-23 14:10:25 -04:00
/**
* @type Rectangle
2011-05-23 14:10:25 -04:00
* @bean
*/
getBounds: function() {
if (!this._bounds)
this._bounds = this._matrix._transformBounds(this._viewBounds);
return this._bounds;
},
2011-05-23 14:10:25 -04:00
/**
* @type Size
* @bean
*/
getSize: function() {
return this.getBounds().getSize();
},
2011-05-23 14:10:25 -04:00
/**
* @type Point
* @bean
*/
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
/**
* @type Number
2011-05-23 14:10:25 -04:00
* @bean
*/
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()));
this._zoom = zoom;
},
2011-05-23 14:10:25 -04:00
/**
* @param {Point} point
*/
scrollBy: function(point) {
2011-05-17 08:43:10 -04:00
this._transform(new Matrix().translate(Point.read(arguments).negate()));
},
2011-05-17 08:43:45 -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;
},
draw: function() {
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-05-17 08:42:20 -04:00
var ctx =this._context,
bounds = this._viewBounds;
ctx.clearRect(bounds._x, bounds._y,
// TODO: +1... what if we have multiple views in one canvas?
bounds._width + 1, bounds._height + 1);
2011-05-17 08:42:20 -04:00
ctx.save();
this._matrix.applyToContext(ctx);
// Just draw the active project for now
2011-05-17 08:42:20 -04:00
this._scope.project.draw(ctx);
ctx.restore();
},
activate: function() {
this._scope.view = this;
},
remove: function() {
var res = Base.splice(this._scope.views, null, this._index, 1);
// Uninstall event handlers again for this view.
DomEvent.remove(this._canvas, this._events);
// Clearing _onFrame makes the frame handler stop automatically.
this._scope = this._canvas = this._events = this._onFrame = null;
return !!res.length;
},
// TODO: getInvalidBounds
// TODO: invalidate(rect)
// TODO: style: artwork / preview / raster / opaque / ink
// TODO: getShowGrid
// TODO: getMousePoint
// TODO: artworkToView(rect)
// TODO: Consider naming these projectToView, viewToProject
artworkToView: function(point) {
return this._matrix._transformPoint(Point.read(arguments));
},
viewToArtwork: function(point) {
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;
},
/**
* {@grouptitle Event Handlers}
2011-05-23 14:10:25 -04:00
* Handler function to be called on each frame of an animation.
* The function receives an event object which contains information about
* the frame event:
*
* <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
* event in seconds.
* <b>{@code event.delta}</b>: the time passed in seconds since the last frame
* event.
*
* @example {@paperscript}
* // Creating an animation:
*
* // 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));
* path.fillColor = 'black';
*
* function onFrame(event) {
* // Every frame, rotate the path by 3 degrees:
* path.rotate(3);
* }
2011-05-23 14:10:25 -04:00
*
* @type function
* @bean
*/
getOnFrame: function() {
return this._onFrame;
},
setOnFrame: function(onFrame) {
this._onFrame = onFrame;
if (!onFrame) {
delete this._onFrameCallback;
return;
}
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;
that._onFrame({
delta: delta, // Time elapsed since last redraw in seconds
time: time += delta, // Time since first call of frame() in seconds
count: count++
});
before = now;
// Automatically draw view on each frame.
that.draw();
};
// Call the onFrame handler straight away, initializing the sequence
// of onFrame calls.
if (!requested)
this._onFrameCallback();
},
/**
* Handler function that is called whenever a view is resized.
*
* @example
* // Repositioning items when a view is resized:
*
* // Create a circle shaped path in the center of the view:
* var path = new Path.Circle(view.bounds.center, 30);
* path.fillColor = 'red';
*
* function onResize(event) {
* // Whenever the view is resized, move the path to its center:
* path.position = view.center;
* }
*
* @type function
*/
onResize: null,
_createEvents: function() {
var that = this,
tool,
timer,
curPoint,
dragging = false;
function viewToArtwork(event) {
return that.viewToArtwork(DomEvent.getOffset(event));
}
function mousedown(event) {
// Tell the Key class which view should receive keyboard input.
2011-05-17 08:29:07 -04:00
View.focused = that;
if (!(tool = that._scope.tool))
return;
curPoint = viewToArtwork(event);
tool.onHandleEvent('mousedown', curPoint, event);
if (tool.onMouseDown)
that.draw();
if (tool.eventInterval != null)
timer = setInterval(mousemove, tool.eventInterval);
dragging = true;
}
function mousemove(event) {
if (!(tool = that._scope.tool))
return;
// If the event was triggered by a touch screen device, prevent the
// default behaviour, as it will otherwise scroll the page:
if (event && event.targetTouches)
DomEvent.preventDefault(event);
var point = event && viewToArtwork(event);
var onlyMove = !!(!tool.onMouseDrag && tool.onMouseMove);
if (dragging && !onlyMove) {
curPoint = point || curPoint;
if (curPoint)
tool.onHandleEvent('mousedrag', curPoint, event);
if (tool.onMouseDrag && !tool.onFrame)
that.draw();
// 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);
if (tool.onMouseMove && !tool.onFrame)
that.draw();
}
}
function mouseup(event) {
if (!dragging)
return;
dragging = false;
curPoint = null;
if (tool) {
if (tool.eventInterval != null)
timer = clearInterval(timer);
tool.onHandleEvent('mouseup', viewToArtwork(event), event);
if (tool.onMouseUp)
that.draw();
}
}
return {
mousedown: mousedown,
mousemove: mousemove,
mouseup: mouseup,
touchstart: mousedown,
touchmove: mousemove,
touchend: mouseup
};
}
});