Merge branch 'refs/heads/master' into uglifyjs2

Conflicts:
	build/preprocess.sh
This commit is contained in:
Jürg Lehni 2012-12-21 16:15:00 +01:00
commit 1509a934b0
40 changed files with 662 additions and 544 deletions

View file

@ -157,7 +157,7 @@ function stripComments(str) {
if (quote) {
// When checking for quote escaping, we also need to check that the
// escape sign itself is not escaped, as otherwise '\\' would cause
// the wrong impression of and endlessly open string:
// the wrong impression of an unclosed string:
if (str[i] === quoteSign && (str[i - 1] !== '\\' || str[i - 2] === '\\'))
quote = false;
} else if (blockComment) {

View file

@ -28,7 +28,7 @@
# stripped Formated but without comments
# compressed Uses UglifyJS to reduce file size
VERSION=0.22
VERSION=0.3
DATE=$(git log -1 --pretty=format:%ad)
COMMAND="./prepro.js -d '{ \"version\": $VERSION, \"date\": \"$DATE\" }' -d '$3' -i '$4' $2"

View file

@ -18,6 +18,8 @@
var segments = [new Point(30, 90), new Point(100, 150)];
var path2 = new Path(segments);
path2.strokeColor = 'yellow';
var path3 = new Path();
document.getElementById('svg').appendChild(project.exportSvg());
</script>

View file

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Transform Testing</title>
<link rel="stylesheet" href="../css/style.css">
<script type="text/javascript" src="../../dist/paper.js"></script>
</head>
<body>
<canvas id="canvas" width="500px" height="500px"></canvas>
<script type="text/paperscript" canvas="canvas">
var circle1 = new Path.Circle(new Point(100, 100), 50);
circle1.fillColor = 'red';
var circle2 = new Path.Circle(new Point(200, 100), 50);
circle2.fillColor = 'blue';
var group = new Group(circle1, circle2);
group.translate([100, 100]);
group.scale(0.5);
group.rotate(10);
document.getElementById('svg').appendChild(project.exportSvg());
</script>
<svg id="svg" style="width: 500px; height: 500px"></svg>
</body>
</html>

File diff suppressed because one or more lines are too long

16
lib/acorn-min.js vendored

File diff suppressed because one or more lines are too long

7
lib/bootstrap.js vendored
View file

@ -147,8 +147,11 @@ var Base = new function() { // Bootstrap scope
// with optional arguments and as beans should not declare
// the parameters and use the arguments array internally
// instead.
if (beans && val.length === 0
&& (bean = name.match(/^(get|is)(([A-Z])(.*))$/)))
if (beans && (bean = name.match(/^(get|is)(([A-Z])(.*))$/))
// Reg-exp to detect non-hidden parameters. We only
// produce beans if there are no parameters or only
// hidden ones:
&& !/^function\s*\(.*\b[^_,].*\)/.test(val))
beans.push([ bean[3].toLowerCase() + bean[4], bean[2] ]);
}
// No need to look up getter if this is a function already.

38
lib/esprima-min.js vendored

File diff suppressed because one or more lines are too long

View file

@ -148,13 +148,13 @@ var Point = this.Point = Base.extend(/** @lends Point# */{
if (Array.isArray(arg0)) {
this.x = arg0[0];
this.y = arg0.length > 1 ? arg0[1] : arg0[0];
} else if ('x' in arg0) {
} else if (arg0.x != null) {
this.x = arg0.x;
this.y = arg0.y;
} else if ('width' in arg0) {
} else if (arg0.width != null) {
this.x = arg0.width;
this.y = arg0.height;
} else if ('angle' in arg0) {
} else if (arg0.angle != null) {
this.x = arg0.length;
this.y = 0;
this.setAngle(arg0.angle);
@ -504,9 +504,9 @@ var Point = this.Point = Base.extend(/** @lends Point# */{
* @bean
* @type Number
*/
getAngle: function(/* point */) {
getAngle: function(_point) {
// Hide parameters from Bootstrap so it injects bean too
return this.getAngleInRadians(arguments[0]) * 180 / Math.PI;
return this.getAngleInRadians(_point) * 180 / Math.PI;
},
setAngle: function(angle) {
@ -542,9 +542,9 @@ var Point = this.Point = Base.extend(/** @lends Point# */{
* @bean
* @type Number
*/
getAngleInRadians: function(/* point */) {
getAngleInRadians: function(_point) {
// Hide parameters from Bootstrap so it injects bean too
if (arguments[0] === undefined) {
if (_point === undefined) {
if (this._angle == null)
this._angle = Math.atan2(this.y, this.x);
return this._angle;
@ -559,8 +559,8 @@ var Point = this.Point = Base.extend(/** @lends Point# */{
}
},
getAngleInDegrees: function(/* point */) {
return this.getAngle(arguments[0]);
getAngleInDegrees: function(_point) {
return this.getAngle(_point);
},
/**

View file

@ -71,7 +71,7 @@ var Rectangle = this.Rectangle = Base.extend(/** @lends Rectangle# */{
this.x = this.y = this.width = this.height = 0;
if (this._read)
this._read = arg0 === null ? 1 : 0;
} else if (arguments.length > 1 && !('width' in arg0)) {
} else if (arguments.length > 1 && arg0.width == null) {
// We're checking arg0.width to rule out Rectangles, which are
// handled separately below.
// Read a point argument and look at the next value to see wether
@ -167,11 +167,10 @@ var Rectangle = this.Rectangle = Base.extend(/** @lends Rectangle# */{
* @type Point
* @bean
*/
getPoint: function(/* dontLink */) {
// Pass on the optional argument dontLink which tells LinkedPoint to
getPoint: function(_dontLink) {
// Pass on the optional argument _dontLink which tells LinkedPoint to
// produce a normal point instead. Used internally for speed reasons.
return LinkedPoint.create(this, 'setPoint', this.x, this.y,
arguments[0]);
return LinkedPoint.create(this, 'setPoint', this.x, this.y, _dontLink);
},
setPoint: function(point) {
@ -181,16 +180,16 @@ var Rectangle = this.Rectangle = Base.extend(/** @lends Rectangle# */{
return this;
},
/**
* The size of the rectangle
*
* @type Size
* @bean
*/
getSize: function(/* dontLink */) {
// See Rectangle#getPoint() about arguments[0]
getSize: function(_dontLink) {
return LinkedSize.create(this, 'setSize', this.width, this.height,
arguments[0]);
_dontLink);
},
setSize: function(size) {
@ -308,9 +307,9 @@ var Rectangle = this.Rectangle = Base.extend(/** @lends Rectangle# */{
* @type Point
* @bean
*/
getCenter: function(/* dontLink */) {
getCenter: function(_dontLink) {
return LinkedPoint.create(this, 'setCenter',
this.getCenterX(), this.getCenterY(), arguments[0]);
this.getCenterX(), this.getCenterY(), _dontLink);
},
setCenter: function(point) {
@ -726,9 +725,9 @@ var Rectangle = this.Rectangle = Base.extend(/** @lends Rectangle# */{
setY = 'set' + y,
get = 'get' + part,
set = 'set' + part;
this[get] = function(/* dontLink */) {
this[get] = function(_dontLink) {
return LinkedPoint.create(this, set,
this[getX](), this[getY](), arguments[0]);
this[getX](), this[getY](), _dontLink);
};
this[set] = function(point) {
point = Point.read(arguments);

View file

@ -109,10 +109,10 @@ var Size = this.Size = Base.extend(/** @lends Size# */{
if (Array.isArray(arg0)) {
this.width = arg0[0];
this.height = arg0.length > 1 ? arg0[1] : arg0[0];
} else if ('width' in arg0) {
} else if (arg0.width != null) {
this.width = arg0.width;
this.height = arg0.height;
} else if ('x' in arg0) {
} else if (arg0.x != null) {
this.width = arg0.x;
this.height = arg0.y;
} else {

View file

@ -50,8 +50,10 @@ var Color = this.Color = Base.extend(new function() {
var components = {
gray: ['gray'],
rgb: ['red', 'green', 'blue'],
hsb: ['hue', 'saturation', 'brightness'],
hsl: ['hue', 'saturation', 'lightness']
hsl: ['hue', 'saturation', 'lightness'],
// Define hsb last, so its converting saturation getter overrides the
// one of HSL:
hsb: ['hue', 'saturation', 'brightness']
};
var colorCache = {},
@ -334,6 +336,10 @@ var Color = this.Color = Base.extend(new function() {
}, src);
}
return this.base(src);
},
random: function() {
return new RgbColor(Math.random(), Math.random(), Math.random());
}
}
};
@ -367,7 +373,7 @@ var Color = this.Color = Base.extend(new function() {
* Called by various setters whenever a color value changes
*/
_changed: function() {
this._cssString = null;
this._css = null;
if (this._owner)
this._owner._changed(/*#=*/ Change.STYLE);
},
@ -473,23 +479,30 @@ var Color = this.Color = Base.extend(new function() {
/**
* @return {String} A css string representation of the color.
*/
toCssString: function() {
if (!this._cssString) {
toCss: function(noAlpha) {
var css = this._css;
// Only cache _css value if we're not ommiting alpha, as required
// by SVG export.
if (!css || noAlpha) {
var color = this.convert('rgb'),
alpha = color.getAlpha(),
alpha = noAlpha ? 1 : color.getAlpha(),
components = [
Math.round(color._red * 255),
Math.round(color._green * 255),
Math.round(color._blue * 255),
alpha != null ? alpha : 1
Math.round(color._blue * 255)
];
this._cssString = 'rgba(' + components.join(', ') + ')';
if (alpha < 1)
components.push(alpha);
var css = (components.length == 4 ? 'rgba(' : 'rgb(')
+ components.join(', ') + ')';
if (!noAlpha)
this._css = css;
}
return this._cssString;
return css;
},
getCanvasStyle: function() {
return this.toCssString();
return this.toCss();
}
/**

View file

@ -238,7 +238,7 @@ var GradientColor = this.GradientColor = Color.extend(/** @lends GradientColor#
}
for (var i = 0, l = this.gradient._stops.length; i < l; i++) {
var stop = this.gradient._stops[i];
gradient.addColorStop(stop._rampPoint, stop._color.toCssString());
gradient.addColorStop(stop._rampPoint, stop._color.toCss());
}
return gradient;
},

View file

@ -30,19 +30,22 @@ var GradientStop = this.GradientStop = Base.extend(/** @lends GradientStop# */{
*/
initialize: function(arg0, arg1) {
if (arg0) {
var color, rampPoint;
if (arg1 === undefined && Array.isArray(arg0)) {
// [color, rampPoint]
this.setColor(arg0[0]);
this.setRampPoint(arg0[1]);
} else if (arg0 && arg0.color) {
color = arg0[0];
rampPoint = arg0[1];
} else if (arg0.color) {
// stop
this.setColor(arg0.color);
this.setRampPoint(arg0.rampPoint);
color = arg0.color;
rampPoint = arg0.rampPoint;
} else {
// color [, rampPoint]
this.setColor(arg0);
this.setRampPoint(arg1);
// color, rampPoint
color = arg0;
rampPoint = arg1;
}
this.setColor(color);
this.setRampPoint(rampPoint);
}
},

View file

@ -41,7 +41,7 @@ this.Base = Base.inject(/** @lends Base# */{
toString: function() {
return '{ ' + Base.each(this, function(value, key) {
// Hide internal properties even if they are enumerable
if (key.charAt(0) != '_') {
if (!/^_/.test(key)) {
var type = typeof value;
this.push(key + ': ' + (type === 'number'
? Base.formatFloat(value)
@ -70,7 +70,7 @@ this.Base = Base.inject(/** @lends Base# */{
if (obj1.length !== obj2.length)
return false;
for (var i = 0, l = obj1.length; i < l; i++) {
if (!Base.equals(obj1, obj2))
if (!Base.equals(obj1[i], obj2[i]))
return false;
}
return true;

View file

@ -75,12 +75,6 @@ var Item = this.Item = Base.extend(Callback, /** @lends Item# */{
}
};
var onFrameItems = [];
function onFrame(event) {
for (var i = 0, l = onFrameItems.length; i < l; i++)
onFrameItems[i].fire('frame', event);
}
return Base.each(['onMouseDown', 'onMouseUp', 'onMouseDrag', 'onClick',
'onDoubleClick', 'onMouseMove', 'onMouseEnter', 'onMouseLeave'],
function(name) {
@ -88,14 +82,10 @@ var Item = this.Item = Base.extend(Callback, /** @lends Item# */{
}, {
onFrame: {
install: function() {
if (!onFrameItems.length)
this._project.view.attach('frame', onFrame);
onFrameItems.push(this);
this._project.view._animateItem(this, true);
},
uninstall: function() {
onFrameItems.splice(onFrameItems.indexOf(this), 1);
if (!onFrameItems.length)
this._project.view.detach('frame', onFrame);
this._project.view._animateItem(this, false);
}
},
@ -522,7 +512,7 @@ var Item = this.Item = Base.extend(Callback, /** @lends Item# */{
* // Move the circle 100 points to the right
* circle.position.x += 100;
*/
getPosition: function(/* dontLink */) {
getPosition: function(_dontLink) {
// Cache position value.
// Pass true for dontLink in getCenter(), so receive back a normal point
var pos = this._position
@ -531,7 +521,7 @@ var Item = this.Item = Base.extend(Callback, /** @lends Item# */{
// use them to calculate the difference in #setPosition, as when it is
// modified, it would hold new values already and only then cause the
// calling of #setPosition.
return arguments[0] ? pos
return _dontLink ? pos
: LinkedPoint.create(this, 'setPosition', pos.x, pos.y);
},
@ -557,36 +547,48 @@ var Item = this.Item = Base.extend(Callback, /** @lends Item# */{
// Use Matrix#initialize to easily copy over values.
this._matrix.initialize(matrix);
this._changed(/*#=*/ Change.GEOMETRY);
},
/**
* Specifies wether the item has any content or not. The meaning of what
* content is differs from type to type. For example, a {@link Group} with
* no children, a {@link TextItem} with no text content and a {@link Path}
* with no segments all are considered empty.
*
* @type Boolean
*/
isEmpty: function() {
return true;
}
}, Base.each(['bounds', 'strokeBounds', 'handleBounds', 'roughBounds'],
function(name) {
// Produce getters for bounds properties. These handle caching, matrices
// and redirect the call to the private _getBounds, which can be
// overridden by subclasses, see below.
this['get' + Base.capitalize(name)] = function(/* matrix */) {
var type = this._boundsType,
bounds = this._getCachedBounds(
// Allow subclasses to override _boundsType if they use the same
// calculations for multiple types. The default is name:
typeof type == 'string' ? type : type && type[name] || name,
// Pass on the optional matrix
arguments[0]);
// If we're returning 'bounds', create a LinkedRectangle that uses the
// setBounds() setter to update the Item whenever the bounds are
// changed:
return name == 'bounds' ? LinkedRectangle.create(this, 'setBounds',
bounds.x, bounds.y, bounds.width, bounds.height) : bounds;
};
}, /** @lends Item# */{
}, Base.each(['getBounds', 'getStrokeBounds', 'getHandleBounds', 'getRoughBounds'],
function(name) {
// Produce getters for bounds properties. These handle caching, matrices
// and redirect the call to the private _getBounds, which can be
// overridden by subclasses, see below.
this[name] = function(_matrix) {
var getter = this._boundsGetter,
// Allow subclasses to override _boundsGetter if they use
// the same calculations for multiple type of bounds.
// The default is name:
bounds = this._getCachedBounds(typeof getter == 'string'
? getter : getter && getter[name] || name, _matrix);
// If we're returning 'bounds', create a LinkedRectangle that uses
// the setBounds() setter to update the Item whenever the bounds are
// changed:
return name == 'bounds' ? LinkedRectangle.create(this, 'setBounds',
bounds.x, bounds.y, bounds.width, bounds.height) : bounds;
};
},
/** @lends Item# */{
/**
* Private method that deals with the calling of _getBounds, recursive
* matrix concatenation and handles all the complicated caching mechanisms.
*/
_getCachedBounds: function(type, matrix, cacheItem) {
_getCachedBounds: function(getter, matrix, cacheItem) {
// See if we can cache these bounds. We only cache the bounds
// transformed with the internally stored _matrix, (the default if no
// matrix is passed).
var cache = (!matrix || matrix.equals(this._matrix)) && type;
var cache = (!matrix || matrix.equals(this._matrix)) && getter;
// Set up a boundsCache structure that keeps track of items that keep
// cached bounds that depend on this item. We store this in our parent,
// for multiple reasons:
@ -624,7 +626,7 @@ function(name) {
: identity ? matrix : matrix.clone().concatenate(this._matrix);
// If we're caching bounds on this item, pass it on as cacheItem, so the
// children can setup the _boundsCache structures for it.
var bounds = this._getBounds(type, matrix, cache ? this : cacheItem);
var bounds = this._getBounds(getter, matrix, cache ? this : cacheItem);
// If we can cache the result, update the _bounds cache structure
// before returning
if (cache) {
@ -664,7 +666,7 @@ function(name) {
* Subclasses override it to define calculations for the various required
* bounding types.
*/
_getBounds: function(type, matrix, cacheItem) {
_getBounds: function(getter, matrix, cacheItem) {
// Note: We cannot cache these results here, since we do not get
// _changed() notifications here for changing geometry in children.
// But cacheName is used in sub-classes such as PlacedItem.
@ -680,7 +682,7 @@ function(name) {
for (var i = 0, l = children.length; i < l; i++) {
var child = children[i];
if (child._visible && !child.isEmpty()) {
var rect = child._getCachedBounds(type, matrix, cacheItem);
var rect = child._getCachedBounds(getter, matrix, cacheItem);
x1 = Math.min(rect.x, x1);
y1 = Math.min(rect.y, y1);
x2 = Math.max(rect.x + rect.width, x2);
@ -690,10 +692,6 @@ function(name) {
return Rectangle.create(x1, y1, x2 - x1, y2 - y1);
},
isEmpty: function() {
return true;
},
setBounds: function(rect) {
rect = Rectangle.read(arguments);
var bounds = this.getBounds(),
@ -946,8 +944,10 @@ function(name) {
copy.setStyle(this._style);
// If this item has children, clone and append each of them:
if (this._children) {
// Clone all children and add them to the copy. tell #addChild we're
// cloning, as needed by CompoundPath#insertChild().
for (var i = 0, l = this._children.length; i < l; i++)
copy.addChild(this._children[i].clone());
copy.addChild(this._children[i].clone(), true);
}
// Only copy over these fields if they are actually defined in 'this'
// TODO: Consider moving this to Base once it's useful in more than one
@ -1128,8 +1128,9 @@ function(name) {
*
* @param {Item} item The item to be added as a child
*/
addChild: function(item) {
return this.insertChild(undefined, item);
addChild: function(item, _cloning) {
// Pass on internal _cloning boolean, for CompoundPath#insertChild
return this.insertChild(undefined, item, _cloning);
},
/**
@ -1878,11 +1879,11 @@ function(name) {
var rect = bounds[key];
matrix._transformBounds(rect, rect);
}
// If we have cached 'bounds', update _position again as its
// center. We need to take into account _boundsType here too, in
// case another type is assigned to it, e.g. 'strokeBounds'.
var type = this._boundsType,
rect = bounds[type && type.bounds || 'bounds'];
// If we have cached bounds, update _position again as its
// center. We need to take into account _boundsGetter here too, in
// case another getter is assigned to it, e.g. 'getStrokeBounds'.
var getter = this._boundsGetter,
rect = bounds[getter && getter.getBounds || getter || 'getBounds'];
if (rect)
this._position = rect.getCenter(true);
this._bounds = bounds;
@ -2458,8 +2459,8 @@ function(name) {
// first, since otherwise their stroke is drawn half transparent
// over their fill.
if (item._blendMode !== 'normal' || item._opacity < 1
&& !(item._segments
&& (!item.getFillColor() || !item.getStrokeColor()))) {
&& (item._type !== 'path'
|| item.getFillColor() && item.getStrokeColor())) {
var bounds = item.getStrokeBounds();
if (!bounds.width || !bounds.height)
return;

View file

@ -25,7 +25,7 @@
*/
var PlacedItem = this.PlacedItem = Item.extend(/** @lends PlacedItem# */{
// PlacedItem uses strokeBounds for bounds
_boundsType: { bounds: 'strokeBounds' },
_boundsGetter: { getBounds: 'getStrokeBounds' },
_hitTest: function(point, options, matrix) {
var hitResult = this._symbol._definition._hitTest(point, options, matrix);

View file

@ -94,11 +94,11 @@ var PlacedSymbol = this.PlacedSymbol = PlacedItem.extend(/** @lends PlacedSymbol
return this._clone(new PlacedSymbol(this.symbol, this._matrix.clone()));
},
_getBounds: function(type, matrix) {
_getBounds: function(getter, matrix) {
// Redirect the call to the symbol definition to calculate the bounds
// TODO: Implement bounds caching through passing on of cacheItem, so
// that Symbol#_changed() notification become unnecessary!
return this.symbol._definition._getCachedBounds(type, matrix);
return this.symbol._definition._getCachedBounds(getter, matrix);
},
draw: function(ctx, param) {

View file

@ -25,7 +25,7 @@ var Raster = this.Raster = PlacedItem.extend(/** @lends Raster# */{
_type: 'raster',
// Raster doesn't make the distinction between the different bounds,
// so use the same name for all of them
_boundsType: 'bounds',
_boundsGetter: 'getBounds',
// TODO: Implement url / type, width, height.
// TODO: Have PlacedSymbol & Raster inherit from a shared class?
@ -45,28 +45,19 @@ var Raster = this.Raster = PlacedItem.extend(/** @lends Raster# */{
if (typeof object === 'string') {
var str = object,
that = this;
object = document.getElementById(str);
if (!object) {
// str could be a URL to load the image from?
object = new Image();
object.src = str;
}
// str can be a DOM ID or a URL to load the image from
object = document.getElementById(str) || new Image();
// Trigger the onLoad event on the image once it's loaded
DomEvent.add(object, {
load: function() {
that.setImage(object);
that.fire('load');
if (that._project.view)
that._project.view.draw(true);
}
});
// If the image is already loaded, fire a 'load' event anyway,
// so code does not need to make the distinction, and cachig is
// transparently handled too.
if (object.naturalWidth) {
setTimeout(function() {
that.fire('load');
}, 0);
}
if (!object.src)
object.src = str;
}
/*#*/ } else if (options.server) {
// If we're running on the server and it's a string,
@ -105,13 +96,15 @@ var Raster = this.Raster = PlacedItem.extend(/** @lends Raster# */{
},
setSize: function() {
var size = Size.read(arguments),
var size = Size.read(arguments);
if (!this._size.equals(size)) {
// Get reference to image before changing canvas
image = this.getImage();
// Setting canvas internally sets _size
this.setCanvas(CanvasProvider.getCanvas(size));
// Draw image back onto new canvas
this.getContext(true).drawImage(image, 0, 0, size.width, size.height);
var image = this.getImage();
// Setting canvas internally sets _size
this.setCanvas(CanvasProvider.getCanvas(size));
// Draw image back onto new canvas
this.getContext(true).drawImage(image, 0, 0, size.width, size.height);
}
},
/**
@ -161,13 +154,13 @@ var Raster = this.Raster = PlacedItem.extend(/** @lends Raster# */{
* @type Context
* @bean
*/
getContext: function(/* notifyChange */) {
getContext: function(_notifyChange) {
if (!this._context)
this._context = this.getCanvas().getContext('2d');
// Support a hidden parameter that indicates if the context will be used
// to modify the Raster object. We can notify such changes ahead since
// they are only used afterwards for redrawing.
if (arguments[0])
if (_notifyChange)
this._changed(/*#=*/ Change.PIXELS);
return this._context;
},
@ -411,7 +404,7 @@ var Raster = this.Raster = PlacedItem.extend(/** @lends Raster# */{
this.getContext(true).putImageData(data, point.x, point.y);
},
_getBounds: function(type, matrix) {
_getBounds: function(getter, matrix) {
var rect = new Rectangle(this._size).setCenter(0, 0);
return matrix ? matrix._transformBounds(rect) : rect;
},

View file

@ -51,16 +51,16 @@ var CompoundPath = this.CompoundPath = PathItem.extend(/** @lends CompoundPath#
this.addChildren(Array.isArray(paths) ? paths : arguments);
},
insertChild: function(index, item) {
insertChild: function(index, item, _cloning) {
// Only allow the insertion of paths
if (!(item instanceof Path))
if (item._type !== 'path')
return null;
var res = this.base(index, item);
// All children except for the bottom one (first one in list) are set
// to anti-clockwise orientation, so that they appear as holes, but
// only if their orientation was not already specified before
// (= _clockwise is defined).
if (res && item._clockwise === undefined)
if (!_cloning && res && item._clockwise === undefined)
item.setClockwise(item._index == 0);
return res;
},
@ -87,6 +87,20 @@ var CompoundPath = this.CompoundPath = PathItem.extend(/** @lends CompoundPath#
this._children[i].smooth();
},
/**
* All the curves contained within the compound-path, from all its child
* {@link Path} items.
*
* @type Curve[]
* @bean
*/
getCurves: function() {
var curves = [];
for (var i = 0, l = this._children.length; i < l; i++)
curves = curves.concat(this._children[i].getCurves());
return curves;
},
isEmpty: function() {
return this._children.length == 0;
},

View file

@ -41,30 +41,36 @@ var Curve = this.Curve = Base.extend(/** @lends Curve# */{
this._segment1 = new Segment();
this._segment2 = new Segment();
} else if (count == 1) {
// TODO: If beans are not activated, this won't copy from
// an existing segment. OK?
// Note: This copies from existing segments through bean getters
this._segment1 = new Segment(arg0.segment1);
this._segment2 = new Segment(arg0.segment2);
} else if (count == 2) {
this._segment1 = new Segment(arg0);
this._segment2 = new Segment(arg1);
} else if (count == 4) {
this._segment1 = new Segment(arg0, null, arg1);
this._segment2 = new Segment(arg3, arg2, null);
} else if (count == 8) {
// An array as returned by getValues
var p1 = Point.create(arg0, arg1),
p2 = Point.create(arg6, arg7);
this._segment1 = new Segment(p1, null,
Point.create(arg2, arg3).subtract(p1));
this._segment2 = new Segment(p2,
Point.create(arg4, arg5).subtract(p2), null);
} else {
var point1, handle1, handle2, point2;
if (count == 4) {
point1 = arg0;
handle1 = arg1;
handle2 = arg2;
point2 = arg3;
} else if (count == 8) {
// Convert getValue() array back to points and handles so we
// can create segments for those.
point1 = [arg0, arg1];
point2 = [arg6, arg7];
handle1 = [arg2 - arg0, arg7 - arg1];
handle2 = [arg4 - arg6, arg5 - arg7];
}
this._segment1 = new Segment(point1, null, handle1);
this._segment2 = new Segment(point2, handle2, null);
}
},
_changed: function() {
// Clear cached values.
delete this._length;
delete this._bounds;
},
/**
@ -228,14 +234,12 @@ var Curve = this.Curve = Base.extend(/** @lends Curve# */{
* @type Number
* @bean
*/
getLength: function(/* from, to */) {
// Hide parameters from Bootstrap so it injects bean too
var from = arguments[0],
to = arguments[1],
fullLength = arguments.length == 0 || from == 0 && to == 1;
// Hide parameters from Bootstrap so it injects bean too
getLength: function(_from, _to) {
var fullLength = arguments.length == 0 || _from == 0 && _to == 1;
if (fullLength && this._length != null)
return this._length;
var length = Curve.getLength(this.getValues(), from, to);
var length = Curve.getLength(this.getValues(), _from, _to);
if (fullLength)
this._length = length;
return length;
@ -315,11 +319,11 @@ var Curve = this.Curve = Base.extend(/** @lends Curve# */{
// Solve the y-axis cubic polynominal for point.y and count all
// solutions to the right of point.x as crossings.
var vals = this.getValues(),
num = Curve.solveCubic(vals, 1, point.y, roots),
count = Curve.solveCubic(vals, 1, point.y, roots),
crossings = 0;
for (var i = 0; i < num; i++) {
for (var i = 0; i < count; i++) {
var t = roots[i];
if (t >= 0 && t <= 1 && Curve.evaluate(vals, t, 0).x > point.x) {
if (t >= 0 && t < 1 && Curve.evaluate(vals, t, 0).x > point.x) {
// If we're close to 0 and are not changing y-direction from the
// previous curve, do not count this root, as we're merely
// touching a tip. Passing 1 for Curve.evaluate()'s type means
@ -327,7 +331,7 @@ var Curve = this.Curve = Base.extend(/** @lends Curve# */{
// a change of direction:
if (t < Numerical.TOLERANCE && Curve.evaluate(
this.getPrevious().getValues(), 1, 1).y
* Curve.evaluate(vals, t, 1).y >= 0)
* Curve.evaluate(vals, t, 1).y >= Numerical.TOLERANCE)
continue;
crossings++;
}
@ -551,7 +555,24 @@ var Curve = this.Curve = Base.extend(/** @lends Curve# */{
return Math.max(ux * ux, vx * vx) + Math.max(uy * uy, vy * vy) < 1;
}
}
}, new function() { // Scope for methods that require numerical integration
}, Base.each(['getBounds', 'getStrokeBounds', 'getHandleBounds', 'getRoughBounds'],
function(name) {
this[name] = function() {
if (!this._bounds)
this._bounds = {};
var bounds = this._bounds[name];
if (!bounds) {
// Calculate the curve bounds by passing a segment list for the
// curve to the static Path.get*Boudns methods.
bounds = this._bounds[name] = Path[name](
[this._segment1, this._segment2], false, this._path._style);
}
return bounds.clone();
};
},
/** @lends Curve# */{
}),
new function() { // Scope for methods that require numerical integration
function getLengthIntegrand(v) {
// Calculate the coefficients of a Bezier derivative.

View file

@ -131,20 +131,29 @@ var Path = this.Path = PathItem.extend(/** @lends Path# */{
* @type Curve[]
* @bean
*/
getCurves: function() {
if (!this._curves) {
var segments = this._segments,
length = segments.length;
getCurves: function(_includeFill) {
var curves = this._curves,
segments = this._segments;
if (!curves) {
var length = segments.length;
// Reduce length by one if it's an open path:
if (!this._closed && length > 0)
length--;
this._curves = new Array(length);
this._curves = curves = new Array(length);
for (var i = 0; i < length; i++)
this._curves[i] = Curve.create(this, segments[i],
curves[i] = Curve.create(this, segments[i],
// Use first segment for segment2 of closing curve
segments[i + 1] || segments[0]);
}
return this._curves;
// If we're asked to include the closing curve for fill, even if the
// path is not closed for stroke, create a new uncached array and add
// the closing curve. Used in Path#contains()
if (_includeFill && !this._closed && this._style._fillColor) {
curves = curves.concat([
Curve.create(this, segments[segments.length - 1], segments[0])
]);
}
return curves;
},
/**
@ -588,7 +597,9 @@ var Path = this.Path = PathItem.extend(/** @lends Path# */{
segments[i]._index = i;
// Keep curves in sync
if (curves) {
curves.splice(from, amount);
// If we're removing the last segment of a closing path, remove the
// last curve.
curves.splice(from == curves.length ? from - 1 : from, amount);
// Adjust segments for the curves before and after the removed ones
var curve;
if (curve = curves[from - 1])
@ -832,8 +843,10 @@ var Path = this.Path = PathItem.extend(/** @lends Path# */{
// Only revers the path if its clockwise orientation is not the same
// as what it is now demanded to be.
this.reverse();
this._clockwise = clockwise;
}
// Reverse only flips _clockwise state if it was already set, so let's
// always set this here now.
this._clockwise = clockwise;
},
/**
@ -1259,20 +1272,23 @@ var Path = this.Path = PathItem.extend(/** @lends Path# */{
contains: function(point) {
point = Point.read(arguments);
// If the path is not closed, we should not bail out in case it has a
// fill color!
if (!this._closed && !this._style._fillColor
|| !this.getRoughBounds()._containsPoint(point))
return false;
// Note: This only works correctly with even-odd fill rule, or paths
// that do not overlap with themselves.
// TODO: Find out how to implement the "Point In Polygon" problem for
// non-zero fill rule.
if (!this._closed || !this.getRoughBounds()._containsPoint(point))
return false;
// Use the crossing number algorithm, by counting the crossings of the
// beam in right y-direction with the shape, and see if it's an odd
// number, meaning the starting point is inside the shape.
// http://en.wikipedia.org/wiki/Point_in_polygon
var curves = this.getCurves(),
var curves = this.getCurves(true),
crossings = 0,
// Reuse one array for root-finding, give garbage collector a break
roots = [];
roots = new Array(3);
for (var i = 0, l = curves.length; i < l; i++)
crossings += curves[i].getCrossings(point, roots);
return (crossings & 1) == 1;
@ -1841,19 +1857,36 @@ var Path = this.Path = PathItem.extend(/** @lends Path# */{
this.setClosed(true);
}
};
}, new function() { // A dedicated scope for the tricky bounds calculations
}, { // A dedicated scope for the tricky bounds calculations
// We define all the different getBounds functions as static methods on Path
// and have #_getBounds directly access these. All static bounds functions
// below have the same first four parameters: segments, closed, style,
// matrix, so they can be called from #_getBounds() and also be used in
// Curve. But not all of them use all these parameters, and some define
// additional ones after.
_getBounds: function(getter, matrix) {
// See #draw() for an explanation of why we can access _style
// properties directly here:
return Path[getter](this._segments, this._closed, this._style, matrix);
},
// Mess with indentation in order to get more line-space below...
statics: {
/**
* Returns the bounding rectangle of the item excluding stroke width.
*/
function getBounds(matrix, strokePadding) {
getBounds: function(segments, closed, style, matrix, strokePadding) {
// Code ported and further optimised from:
// http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html
var segments = this._segments,
first = segments[0];
var first = segments[0];
// If there are no segments, return "empty" rectangle, just like groups,
// since #bounds is assumed to never return null.
if (!first)
return null;
return new Rectangle();
var coords = new Array(6),
prevCoords = new Array(6);
prevCoords = new Array(6),
roots = new Array(2);
// Make coordinates for first segment available in prevCoords.
first._transformCoordinates(matrix, prevCoords, false);
var min = prevCoords.slice(0, 2),
@ -1898,115 +1931,89 @@ var Path = this.Path = PathItem.extend(/** @lends Path# */{
// Calculate derivative of our bezier polynomial, divided by 3.
// Dividing by 3 allows for simpler calculations of a, b, c and
// leads to the same quadratic roots below.
// leads to the same quadratic roots.
var a = 3 * (v1 - v2) - v0 + v3,
b = 2 * (v0 + v2) - 4 * v1,
c = v1 - v0;
// Solve for derivative for quadratic roots. Each good root
// (meaning a solution 0 < t < 1) is an extrema in the cubic
// polynomial and thus a potential point defining the bounds
// TODO: Use tolerance here, just like Numerical.solveQuadratic
if (a == 0) {
if (b == 0)
continue;
var t = -c / b;
// Test for good root and add to bounds if good (same below)
count = Numerical.solveQuadratic(a, b, c, roots,
Numerical.TOLERANCE);
for (var j = 0; j < count; j++) {
var t = roots[j];
// Test for good roots and only add to bounds if good.
if (tMin < t && t < tMax)
add(null, t);
continue;
}
var q = b * b - 4 * a * c;
if (q < 0)
continue;
// TODO: Match this with Numerical.solveQuadratic
var sqrt = Math.sqrt(q),
f = -0.5 / a,
t1 = (b - sqrt) * f,
t2 = (b + sqrt) * f;
if (tMin < t1 && t1 < tMax)
add(null, t1);
if (tMin < t2 && t2 < tMax)
add(null, t2);
}
// Swap coordinate buffers
// Swap coordinate buffers.
var tmp = prevCoords;
prevCoords = coords;
coords = tmp;
}
for (var i = 1, l = segments.length; i < l; i++)
processSegment(segments[i]);
if (this._closed)
if (closed)
processSegment(first);
return Rectangle.create(min[0], min[1],
max[0] - min[0], max[1] - min[1]);
}
/**
* Returns the horizontal and vertical padding that a transformed round
* stroke adds to the bounding box, by calculating the dimensions of a
* rotated ellipse.
*/
function getPenPadding(radius, matrix) {
if (!matrix)
return [radius, radius];
// If a matrix is provided, we need to rotate the stroke circle
// and calculate the bounding box of the resulting rotated elipse:
// Get rotated hor and ver vectors, and determine rotation angle
// and elipse values from them:
var mx = matrix.createShiftless(),
hor = mx.transform(Point.create(radius, 0)),
ver = mx.transform(Point.create(0, radius)),
phi = hor.getAngleInRadians(),
a = hor.getLength(),
b = ver.getLength();
// Formula for rotated ellipses:
// x = cx + a*cos(t)*cos(phi) - b*sin(t)*sin(phi)
// y = cy + b*sin(t)*cos(phi) + a*cos(t)*sin(phi)
// Derivates (by Wolfram Alpha):
// derivative of x = cx + a*cos(t)*cos(phi) - b*sin(t)*sin(phi)
// dx/dt = a sin(t) cos(phi) + b cos(t) sin(phi) = 0
// derivative of y = cy + b*sin(t)*cos(phi) + a*cos(t)*sin(phi)
// dy/dt = b cos(t) cos(phi) - a sin(t) sin(phi) = 0
// This can be simplified to:
// tan(t) = -b * tan(phi) / a // x
// tan(t) = b * cot(phi) / a // y
// Solving for t gives:
// t = pi * n - arctan(b * tan(phi) / a) // x
// t = pi * n + arctan(b * cot(phi) / a)
// = pi * n + arctan(b / tan(phi) / a) // y
var sin = Math.sin(phi),
cos = Math.cos(phi),
tan = Math.tan(phi),
tx = -Math.atan(b * tan / a),
ty = Math.atan(b / (tan * a));
// Due to symetry, we don't need to cycle through pi * n solutions:
return [Math.abs(a * Math.cos(tx) * cos - b * Math.sin(tx) * sin),
Math.abs(b * Math.sin(ty) * cos + a * Math.cos(ty) * sin)];
}
return Rectangle.create(min[0], min[1], max[0] - min[0], max[1] - min[1]);
},
/**
* Returns the bounding rectangle of the item including stroke width.
*/
function getStrokeBounds(matrix) {
// See #draw() for an explanation of why we can access _style
// properties directly here:
var style = this._style;
getStrokeBounds: function(segments, closed, style, matrix) {
/**
* Returns the horizontal and vertical padding that a transformed round
* stroke adds to the bounding box, by calculating the dimensions of a
* rotated ellipse.
*/
function getPenPadding(radius, matrix) {
if (!matrix)
return [radius, radius];
// If a matrix is provided, we need to rotate the stroke circle
// and calculate the bounding box of the resulting rotated elipse:
// Get rotated hor and ver vectors, and determine rotation angle
// and elipse values from them:
var mx = matrix.createShiftless(),
hor = mx.transform(Point.create(radius, 0)),
ver = mx.transform(Point.create(0, radius)),
phi = hor.getAngleInRadians(),
a = hor.getLength(),
b = ver.getLength();
// Formula for rotated ellipses:
// x = cx + a*cos(t)*cos(phi) - b*sin(t)*sin(phi)
// y = cy + b*sin(t)*cos(phi) + a*cos(t)*sin(phi)
// Derivates (by Wolfram Alpha):
// derivative of x = cx + a*cos(t)*cos(phi) - b*sin(t)*sin(phi)
// dx/dt = a sin(t) cos(phi) + b cos(t) sin(phi) = 0
// derivative of y = cy + b*sin(t)*cos(phi) + a*cos(t)*sin(phi)
// dy/dt = b cos(t) cos(phi) - a sin(t) sin(phi) = 0
// This can be simplified to:
// tan(t) = -b * tan(phi) / a // x
// tan(t) = b * cot(phi) / a // y
// Solving for t gives:
// t = pi * n - arctan(b * tan(phi) / a) // x
// t = pi * n + arctan(b * cot(phi) / a)
// = pi * n + arctan(b / tan(phi) / a) // y
var sin = Math.sin(phi),
cos = Math.cos(phi),
tan = Math.tan(phi),
tx = -Math.atan(b * tan / a),
ty = Math.atan(b / (tan * a));
// Due to symetry, we don't need to cycle through pi * n solutions:
return [Math.abs(a * Math.cos(tx) * cos - b * Math.sin(tx) * sin),
Math.abs(b * Math.sin(ty) * cos + a * Math.cos(ty) * sin)];
}
// TODO: Find a way to reuse 'bounds' cache instead?
if (!style._strokeColor || !style._strokeWidth)
return getBounds.call(this, matrix);
var width = style._strokeWidth,
radius = width / 2,
return Path.getBounds(segments, closed, style, matrix);
var radius = style._strokeWidth / 2,
padding = getPenPadding(radius, matrix),
bounds = Path.getBounds(segments, closed, style, matrix, padding),
join = style._strokeJoin,
cap = style._strokeCap,
// miter is relative to width. Divide it by 2 since we're
// miter is relative to stroke width. Divide it by 2 since we're
// measuring half the distance below
miter = style._miterLimit * width / 2,
segments = this._segments,
length = segments.length,
bounds = getBounds.call(this, matrix, padding);
miter = style._miterLimit * style._strokeWidth / 2;
// Create a rectangle of padding size, used for union with bounds
// further down
var joinBounds = new Rectangle(new Size(padding).multiply(2));
@ -2076,35 +2083,35 @@ var Path = this.Path = PathItem.extend(/** @lends Path# */{
}
}
for (var i = 1, l = length - (this._closed ? 0 : 1); i < l; i++) {
for (var i = 1, l = segments.length - (closed ? 0 : 1); i < l; i++)
addJoin(segments[i], join);
}
if (this._closed) {
if (closed) {
addJoin(segments[0], join);
} else {
addCap(segments[0], cap, 0);
addCap(segments[length - 1], cap, 1);
addCap(segments[segments.length - 1], cap, 1);
}
return bounds;
}
},
/**
* Returns the bounding rectangle of the item including handles.
*/
function getHandleBounds(matrix, stroke, join) {
getHandleBounds: function(segments, closed, style, matrix, strokePadding,
joinPadding) {
var coords = new Array(6),
x1 = Infinity,
x2 = -x1,
y1 = x1,
y2 = x2;
stroke = stroke / 2 || 0; // Stroke padding
join = join / 2 || 0; // Join padding, for miterLimit
for (var i = 0, l = this._segments.length; i < l; i++) {
var segment = this._segments[i];
strokePadding = strokePadding / 2 || 0;
joinPadding = joinPadding / 2 || 0;
for (var i = 0, l = segments.length; i < l; i++) {
var segment = segments[i];
segment._transformCoordinates(matrix, coords, false);
for (var j = 0; j < 6; j += 2) {
// Use different padding for points or handles
var padding = j == 0 ? join : stroke,
var padding = j == 0 ? joinPadding : strokePadding,
x = coords[j],
y = coords[j + 1],
xn = x - padding,
@ -2118,34 +2125,21 @@ var Path = this.Path = PathItem.extend(/** @lends Path# */{
}
}
return Rectangle.create(x1, y1, x2 - x1, y2 - y1);
}
},
/**
* Returns the rough bounding rectangle of the item that is shure to include
* Returns the rough bounding rectangle of the item that is sure to include
* all of the drawing, including stroke width.
*/
function getRoughBounds(matrix) {
getRoughBounds: function(segments, closed, style, matrix) {
// Delegate to handleBounds, but pass on radius values for stroke and
// joins. Hanlde miter joins specially, by passing the largets radius
// possible.
var style = this._style,
width = style._strokeColor ? style._strokeWidth : 0;
return getHandleBounds.call(this, matrix, width,
var strokeWidth = style._strokeColor ? style._strokeWidth : 0;
return Path.getHandleBounds(segments, closed, style, matrix,
strokeWidth,
style._strokeJoin == 'miter'
? width * style._miterLimit
: width);
? strokeWidth * style._miterLimit
: strokeWidth);
}
var get = {
bounds: getBounds,
strokeBounds: getStrokeBounds,
handleBounds: getHandleBounds,
roughBounds: getRoughBounds
};
return {
_getBounds: function(type, matrix) {
return get[type].call(this, matrix);
}
};
});
}});

View file

@ -58,8 +58,7 @@ var Segment = this.Segment = Base.extend(/** @lends Segment# */{
if (count == 0) {
// Nothing
} else if (count == 1) {
// TODO: If beans are not activated, this won't copy from existing
// segments. OK?
// Note: This copies from existing segments through bean getters
if (arg0.point) {
point = arg0.point;
handleIn = arg0.handleIn;
@ -90,8 +89,10 @@ var Segment = this.Segment = Base.extend(/** @lends Segment# */{
_changed: function(point) {
if (!this._path)
return;
// Delegate changes to affected curves if they exist
var curve = this._path._curves && this.getCurve(), other;
// Delegate changes to affected curves if they exist. Check _curves
// first to make sure we're not creating it by calling this.getCurve().
var curve = this._path._curves && this.getCurve(),
other;
if (curve) {
curve._changed();
// Get the other affected curve, which is the previous one for

View file

@ -52,14 +52,14 @@ var Project = this.Project = PaperScopeItem.extend(/** @lends Project# */{
// Activate straight away by passing true to base(), so paper.project is
// set, as required by Layer and DoumentView constructors.
this.base(true);
this._currentStyle = new PathStyle();
this._selectedItems = {};
this._selectedItemCount = 0;
this.layers = [];
this.symbols = [];
this.activeLayer = new Layer();
if (view)
this.view = view instanceof View ? view : View.create(view);
this._currentStyle = new PathStyle();
this._selectedItems = {};
this._selectedItemCount = 0;
// Change tracking, not in use for now. Activate once required:
// this._changes = [];
// this._changesById = {};
@ -278,29 +278,23 @@ var Project = this.Project = PaperScopeItem.extend(/** @lends Project# */{
// matrices by item id, to speed up drawing by eliminating repeated
// concatenation of parent's matrices through caching.
var matrices = {};
// Descriptionf of the paramters to getGlobalMatrix():
// Description of the paramters to getGlobalMatrix():
// mx is the container for the final concatenated matrix, passed
// to getGlobalMatrix() on the initial call.
// cached defines wether the result of the concatenation should be
// cached, only used for parents of items that this is called for.
function getGlobalMatrix(item, mx, cached) {
var cache = cached && matrices[item._id];
if (cache) {
// Found a cached version, copy over the values and return
mx.concatenate(cache);
return mx;
}
if (item._parent) {
// Get concatenated matrix from all the parents, using
// local caching (passing true for cached):
getGlobalMatrix(item._parent, mx, true);
// No need to concatenate if it's the identity matrix
if (!item._matrix.isIdentity())
mx.concatenate(item._matrix);
} else {
// Simply copy over the item's matrix, since it's the root
mx.initialize(item._matrix);
}
// Found a cached version? Return a clone of it.
if (cache)
return cache.clone();
// Get concatenated matrix from all the parents, using
// local caching (passing true for cached):
if (item._parent)
mx = getGlobalMatrix(item._parent, mx, true);
// No need to concatenate if it's the identity matrix
if (!item._matrix.isIdentity())
mx.concatenate(item._matrix);
// If the result needs to be cached, create a copy since matrix
// might be further modified through recursive calls
if (cached)

View file

@ -48,34 +48,43 @@ new function() {
return segments[index1]._point.getDistance(segments[index2]._point);
}
function getTransform(item) {
// Note, we're taking out the translation part of the matrix and move it
// to x, y attributes, to produce more readable markup, and not have to
// use center points in rotate(). To do so, SVG requries us to inverse
// transform the translation point by the matrix itself, since they are
// provided in local coordinates.
var matrix = item._matrix.createShiftless(),
trans = matrix._inverseTransform(item._matrix.getTranslation()),
attrs = {
x: trans.x,
y: trans.y
};
function getTransform(item, coordinates) {
var matrix = item._matrix,
trans = matrix.getTranslation(),
attrs = {};
if (coordinates) {
// If the item suppports x- and y- coordinates, we're taking out the
// translation part of the matrix and move it to x, y attributes, to
// produce more readable markup, and not have to use center points
// in rotate(). To do so, SVG requries us to inverse transform the
// translation point by the matrix itself, since they are provided
// in local coordinates.
matrix = matrix.createShiftless();
var point = matrix._inverseTransform(trans);
attrs.x = point.x;
attrs.y = point.y;
trans = null;
}
if (matrix.isIdentity())
return attrs;
// See if we can formulate this matrix as simple scale / rotate commands
// Note: getScaling() returns values also when it's not a simple scale,
// but angle is only != null if it is, so check for that.
var transform = [],
angle = matrix.getRotation(),
scale = matrix.getScaling();
var angle = matrix.getRotation(),
parts = [];
if (angle != null) {
transform.push(angle
? 'rotate(' + formatFloat(angle) + ')'
: 'scale(' + formatPoint(scale) +')');
matrix = matrix.clone().scale(1, -1);
if (trans && !trans.isZero())
parts.push('translate(' + formatPoint(trans) + ')');
if (angle)
parts.push('rotate(' + formatFloat(angle) + ')');
var scale = matrix.getScaling();
if (!Numerical.isZero(scale.x - 1) || !Numerical.isZero(scale.y - 1))
parts.push('scale(' + formatPoint(scale) +')');
} else {
transform.push('matrix(' + matrix.getValues().join(',') + ')');
parts.push('matrix(' + matrix.getValues().join(',') + ')');
}
attrs.transform = transform.join(' ');
attrs.transform = parts.join(' ');
return attrs;
}
@ -176,9 +185,11 @@ new function() {
return segments.length === 4 && path._closed
&& isColinear(0, 2) && isColinear(1, 3)
? 'rect'
: segments.length >= 3
? path._closed ? 'polygon' : 'polyline'
: 'line';
: segments.length === 0
? 'empty'
: segments.length >= 3
? path._closed ? 'polygon' : 'polyline'
: 'line';
} else if (path._closed) {
if (segments.length === 8
&& isArc(0) && isArc(2) && isArc(4) && isArc(6)
@ -203,13 +214,16 @@ new function() {
// Override default SVG style on groups, then apply style.
attrs.fill = 'none';
var svg = createElement('g', attrs);
for (var i = 0, l = children.length; i < l; i++)
svg.appendChild(children[i].exportSvg());
for (var i = 0, l = children.length; i < l; i++) {
var child = children[i].exportSvg();
if (child)
svg.appendChild(child);
}
return svg;
}
function exportText(item) {
var attrs = getTransform(item),
var attrs = getTransform(item, true),
style = item._style;
if (style._font != null)
attrs['font-family'] = style._font;
@ -227,6 +241,8 @@ new function() {
angle = determineAngle(path, segments, type, center),
attrs;
switch (type) {
case 'empty':
return null;
case 'path':
attrs = {
d: getPath(path, segments)
@ -341,10 +357,15 @@ new function() {
// (A layer or group which can have style values in SVG).
var value = style[entry.get]();
if (!parentStyle || !Base.equals(parentStyle[entry.get](), value)) {
// Support for css-style rgba() values is not in SVG 1.1, so
// separate the alpha value of colors with alpha into the
// separate fill- / stroke-opacity attribute:
if (entry.type === 'color' && value != null && value.getAlpha() < 1)
attrs[entry.attribute + '-opacity'] = value.getAlpha();
attrs[entry.attribute] = value == null
? 'none'
: entry.type === 'color'
? value.toCssString()
? value.toCss(true) // false for noAlpha, see above
: entry.type === 'array'
? value.join(',')
: entry.type === 'number'
@ -372,8 +393,8 @@ new function() {
* @return {SVGSVGElement} the item converted to an SVG node
*/
exportSvg: function() {
var exporter = exporters[this._type];
var svg = exporter && exporter(this, this._type);
var exporter = exporters[this._type],
svg = exporter && exporter(this, this._type);
return svg && applyStyle(this, svg);
}
});

View file

@ -269,6 +269,19 @@ new function() {
// http://www.w3.org/TR/SVG/pservers.html#RadialGradients
radialgradient: importGradient,
// http://www.w3.org/TR/SVG/struct.html#ImageElement
image: function (svg) {
var raster = new Raster(getValue(svg, 'href'));
raster.attach('load', function() {
var size = getSize(svg, 'width', 'height');
this.setSize(size);
// Since x and y start from the top left of an image, add
// half of its size:
this.translate(getPoint(svg, 'x', 'y').add(size.divide(2)));
});
return raster;
},
// http://www.w3.org/TR/SVG/struct.html#SymbolElement
symbol: function(svg, type) {
return new Symbol(applyAttributes(importGroup(svg, type), svg));
@ -382,13 +395,20 @@ new function() {
break;
// http://www.w3.org/TR/SVG/masking.html#ClipPathProperty
case 'clip-path':
var clipPath = getDefinition(value).clone().reduce();
item = createClipGroup(item, clipPath);
item = createClipGroup(item,
getDefinition(value).clone().reduce());
break;
// http://www.w3.org/TR/SVG/types.html#DataTypeTransformList
case 'gradientTransform':
case 'transform':
applyTransform(item, svg, name);
var transforms = svg[name].baseVal,
matrix = new Matrix();
for (var i = 0, l = transforms.numberOfItems; i < l; i++) {
var mx = transforms.getItem(i).matrix;
matrix.concatenate(
new Matrix(mx.a, mx.b, mx.c, mx.d, mx.e, mx.f));
}
item.transform(matrix);
break;
// http://www.w3.org/TR/SVG/pservers.html#StopOpacityProperty
case 'stop-opacity':
@ -401,6 +421,15 @@ new function() {
item.setOpacity(opacity);
}
break;
// http://www.w3.org/TR/SVG/painting.html#FillOpacityProperty
case 'fill-opacity':
// http://www.w3.org/TR/SVG/painting.html#StrokeOpacityProperty
case 'stroke-opacity':
var color = item[name == 'fill-opacity' ? 'getFillColor'
: 'getStrokeColor']();
if (color)
color.setAlpha(Base.toFloat(value));
break;
case 'visibility':
item.setVisible(value === 'visible');
break;
@ -479,57 +508,11 @@ new function() {
}
}
/**
* Applies the transformations specified on the SVG node to a Paper.js item
*
* @param {SVGSVGElement} svg an SVG node
* @param {Item} item a Paper.js item
*/
function applyTransform(item, svg, name) {
var svgTransform = svg[name],
transforms = svgTransform.baseVal,
matrix = new Matrix();
for (var i = 0, l = transforms.numberOfItems; i < l; i++) {
var transform = transforms.getItem(i);
if (transform.type === /*#=*/ SVGTransform.SVG_TRANSFORM_UNKNOWN)
continue;
// Convert SVG Matrix to Paper Matrix.
// TODO: Should this be moved to our Matrix constructor?
var mx = transform.matrix,
a = mx.a,
b = mx.b,
c = mx.c,
d = mx.d;
switch (transform.type) {
// Compensate for SVG's theta rotation going the opposite direction
case /*#=*/ SVGTransform.SVG_TRANSFORM_MATRIX:
var tmp = b;
b = c;
c = tmp;
break;
case /*#=*/ SVGTransform.SVG_TRANSFORM_SKEWX:
b = c;
c = 0;
break;
case /*#=*/ SVGTransform.SVG_TRANSFORM_SKEWY:
c = b;
b = 0;
break;
case /*#=*/ SVGTransform.SVG_TRANSFORM_ROTATE:
b = -b;
c = -c;
break;
}
matrix.concatenate(new Matrix(a, c, b, d, mx.e, mx.f));
}
item.transform(matrix);
}
function importSvg(svg) {
var type = svg.nodeName.toLowerCase(),
importer = importers[type],
item = importer && importer(svg, type);
return item ? applyAttributes(item, svg) : item;
return item && applyAttributes(item, svg);
}

View file

@ -15,13 +15,12 @@
*/
/**
* To shrink code, we automatically replace the long SVGPathSeg and SVGTransform
* constants with their actual numeric values on preprocessing time, using
* prepro statements.
* To shrink code, we automatically replace the long SVGPathSeg constants with
* their actual numeric values on preprocessing time, using prepro statements.
* To do so, we need their values defined, which happens here.
*/
// http://dxr.mozilla.org/mozilla-central/dom/interfaces/svg/nsIDOMSVGPathSeg.idl.html
// http://dxr.mozilla.org/mozilla-central/dom/interfaces/svg/nsIDOMSVGPathSeg.idl.html
var SVGPathSeg = {
PATHSEG_UNKNOWN: 0,
PATHSEG_CLOSEPATH: 1,
@ -44,14 +43,3 @@ var SVGPathSeg = {
PATHSEG_CURVETO_QUADRATIC_SMOOTH_ABS: 18,
PATHSEG_CURVETO_QUADRATIC_SMOOTH_REL: 19
};
// http://dxr.mozilla.org/mozilla-central/dom/interfaces/svg/nsIDOMSVGTransform.idl.html
var SVGTransform = {
SVG_TRANSFORM_UNKNOWN: 0,
SVG_TRANSFORM_MATRIX: 1,
SVG_TRANSFORM_TRANSLATE: 2,
SVG_TRANSFORM_SCALE: 3,
SVG_TRANSFORM_ROTATE: 4,
SVG_TRANSFORM_SKEWX: 5,
SVG_TRANSFORM_SKEWY: 6
};

View file

@ -96,7 +96,7 @@ var PointText = this.PointText = TextItem.extend(/** @lends PointText# */{
var context = null;
return {
_getBounds: function(type, matrix) {
_getBounds: function(getter, matrix) {
// Create an in-memory canvas on which to do the measuring
if (!context)
context = CanvasProvider.getCanvas(

View file

@ -28,7 +28,7 @@
var TextItem = this.TextItem = Item.extend(/** @lends TextItem# */{
// TextItem doesn't make the distinction between the different bounds,
// so use the same name for all of them
_boundsType: 'bounds',
_boundsGetter: 'getBounds',
initialize: function(pointOrMatrix) {
// Note that internally #characterStyle is the same as #style, but
@ -93,7 +93,7 @@ var TextItem = this.TextItem = Item.extend(/** @lends TextItem# */{
},
isEmpty: function() {
return !!this._content;
return !this._content;
},
/**

View file

@ -325,6 +325,8 @@ var Tool = this.Tool = PaperScopeItem.extend(/** @lends Tool# */{
if (set) {
for (var id in set) {
var item = set[id];
// If we remove this item, we also need to erase it from all
// other sets.
for (var key in sets) {
var other = sets[key];
if (other && other != set && other[item._id])

View file

@ -72,6 +72,7 @@ var CanvasView = View.extend(/** @lends CanvasView# */{
lastPoint,
overPoint,
downItem,
lastItem,
overItem,
hasDrag,
doubleClick,
@ -122,8 +123,8 @@ var CanvasView = View.extend(/** @lends CanvasView# */{
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 = downItem == item && Date.now() - clickTime < 300;
downItem = item;
doubleClick = lastItem == item && (Date.now() - clickTime < 300);
downItem = lastItem = item;
downPoint = lastPoint = overPoint = point;
hasDrag = downItem && downItem.responds('mousedrag');
},

View file

@ -57,10 +57,10 @@ var KeyEvent = this.KeyEvent = Event.extend(/** @lends KeyEvent# */{
* @return {String} A string representation of the key event.
*/
toString: function() {
return '{ type: ' + this.type
+ ', key: ' + this.key
+ ', character: ' + this.character
+ ', modifiers: ' + this.getModifiers()
+ ' }';
return "{ type: '" + this.type
+ "', key: '" + this.key
+ "', character: '" + this.character
+ "', modifiers: " + this.getModifiers()
+ " }";
}
});

View file

@ -67,8 +67,8 @@ var MouseEvent = this.MouseEvent = Event.extend(/** @lends MouseEvent# */{
* @return {String} A string representation of the mouse event.
*/
toString: function() {
return '{ type: ' + this.type
+ ', point: ' + this.point
return "{ type: '" + this.type
+ "', point: " + this.point
+ ', target: ' + this.target
+ (this.delta ? ', delta: ' + this.delta : '')
+ ', modifiers: ' + this.getModifiers()

View file

@ -24,61 +24,6 @@
* screen.
*/
var View = this.View = Base.extend(Callback, /** @lends View# */{
_events: {
onFrame: {
install: function() {
/*#*/ if (options.browser) {
var that = this,
requested = false,
before,
time = 0,
count = 0;
this._onFrameCallback = function(param, dontRequest) {
requested = false;
// See if we need to stop due to a call to uninstall()
if (!that._onFrameCallback)
return;
// Set the global paper object to the current scope
paper = that._scope;
if (!dontRequest) {
// Request next frame already
requested = true;
DomEvent.requestAnimationFrame(that._onFrameCallback,
that._element);
}
var now = Date.now() / 1000,
delta = before ? now - before : 0;
// delta: Time elapsed since last redraw in seconds
// time: Time since first call of frame() in seconds
// Use Base.merge to convert into a Base object,
// for #toString()
that.fire('frame', Base.merge({
delta: delta,
time: time += delta,
count: count++
}));
before = now;
// Update framerate stats
if (that._stats)
that._stats.update();
// Automatically draw view on each frame.
that.draw(true);
};
// Call the onFrame handler straight away, initializing the
// sequence of onFrame calls.
if (!requested)
this._onFrameCallback();
/*#*/ } // options.browser
},
uninstall: function() {
delete this._onFrameCallback;
}
},
onResize: {}
},
initialize: function(element) {
// Store reference to the currently active global paper scope, and the
// active project, which will be represented by this view
@ -157,6 +102,9 @@ var View = this.View = Base.extend(Callback, /** @lends View# */{
// Make sure the first view is focused for keyboard input straight away
if (!View._focused)
View._focused = this;
// Items that need the onFrame handler called on them
this._frameItems = {};
this._frameItemCount = 0;
},
/**
@ -178,18 +126,114 @@ var View = this.View = Base.extend(Callback, /** @lends View# */{
DomEvent.remove(this._element, this._viewHandlers);
DomEvent.remove(window, this._windowHandlers);
this._element = this._project = null;
// Removing all onFrame handlers makes the _onFrameCallback handler stop
// Removing all onFrame handlers makes the onFrame handler stop
// automatically through its uninstall method.
this.detach('frame');
this._frameItems = {};
return true;
},
_events: {
onFrame: {
install: function() {
/*#*/ if (options.browser) {
// Call the onFrame handler straight away and initialize the
// sequence of onFrame calls.
if (!this._requested) {
this._animate = true;
this._handleFrame(true);
}
/*#*/ } // options.browser
},
uninstall: function() {
this._animate = false;
}
},
onResize: {}
},
// These are default values for event related properties on the prototype.
// Writing item._count++ does not change the defaults, it creates / updates
// the property on the instance. Useful!
_animate: false,
_time: 0,
_count: 0,
_handleFrame: function(request) {
this._requested = false;
// See if we need to stop due to a call to uninstall()
if (!this._animate)
return;
// Set the global paper object to the current scope
paper = this._scope;
if (request) {
// Request next frame already
this._requested = true;
var that = this;
DomEvent.requestAnimationFrame(function() {
that._handleFrame(true);
}, this._element);
}
var now = Date.now() / 1000,
delta = this._before ? now - this._before : 0;
this._before = now;
// Use Base.merge to convert into a Base object, for #toString()
this.fire('frame', Base.merge({
// Time elapsed since last redraw in seconds:
delta: delta,
// Time since first call of frame() in seconds:
time: this._time += delta,
count: this._count++
}));
// Update framerate stats
if (this._stats)
this._stats.update();
// Automatically draw view on each frame.
this.draw(true);
},
_animateItem: function(item, animate) {
var items = this._frameItems;
if (animate) {
items[item._id] = {
item: item,
// Additional information for the event callback
time: 0,
count: 0
};
if (++this._frameItemCount == 1)
this.attach('frame', this._handleFrameItems);
} else {
delete items[item._id];
if (--this._frameItemCount == 0) {
// If this is the last one, just stop animating straight away.
this.detach('frame', this._handleFrameItems);
}
}
},
// An empty callback that's only there so _frameItems can be handled
// through the onFrame callback framework that automatically starts and
// stops the animation for us whenever there's one or more frame handlers
_handleFrameItems: function(event) {
for (var i in this._frameItems) {
var entry = this._frameItems[i];
entry.item.fire('frame', Base.merge(event, {
// Time since first call of frame() in seconds:
time: entry.time += event.delta,
count: entry.count++
}));
}
},
_redraw: function() {
this._redrawNeeded = true;
if (this._onFrameCallback) {
// If there's a _onFrameCallback, call it staight away,
// but without requesting another animation frame.
this._onFrameCallback(0, true);
if (this._animate) {
// If we're animating, call _handleFrame staight away, but without
// requesting another animation frame.
this._handleFrame();
} else {
// Otherwise simply redraw the view now
this.draw();

View file

@ -335,3 +335,9 @@ function compareItems(item, item2, checkIdentity) {
}
}
}
// SVG
function createSvg(xml) {
return new DOMParser().parseFromString('<svg xmlns="http://www.w3.org/2000/svg">' + xml + '</svg>', 'application/xml');
}

View file

@ -20,38 +20,38 @@ test('Set named color', function() {
var path = new Path();
path.fillColor = 'red';
compareRgbColors(path.fillColor, new RgbColor(1, 0, 0));
equals(path.fillColor.toCssString(), 'rgba(255, 0, 0, 1)');
equals(path.fillColor.toCss(), 'rgb(255, 0, 0)');
});
test('Set color to hex', function() {
var path = new Path();
path.fillColor = '#ff0000';
compareRgbColors(path.fillColor, new RgbColor(1, 0, 0));
equals(path.fillColor.toCssString(), 'rgba(255, 0, 0, 1)');
equals(path.fillColor.toCss(), 'rgb(255, 0, 0)');
var path = new Path();
path.fillColor = '#f00';
compareRgbColors(path.fillColor, new RgbColor(1, 0, 0));
equals(path.fillColor.toCssString(), 'rgba(255, 0, 0, 1)');
equals(path.fillColor.toCss(), 'rgb(255, 0, 0)');
});
test('Set color to object', function() {
var path = new Path();
path.fillColor = { red: 1, green: 0, blue: 1};
compareRgbColors(path.fillColor, new RgbColor(1, 0, 1));
equals(path.fillColor.toCssString(), 'rgba(255, 0, 255, 1)');
equals(path.fillColor.toCss(), 'rgb(255, 0, 255)');
var path = new Path();
path.fillColor = { gray: 0.2 };
compareRgbColors(path.fillColor, new RgbColor(0.8, 0.8, 0.8));
equals(path.fillColor.toCssString(), 'rgba(204, 204, 204, 1)');
equals(path.fillColor.toCss(), 'rgb(204, 204, 204)');
});
test('Set color to array', function() {
var path = new Path();
path.fillColor = [1, 0, 0];
compareRgbColors(path.fillColor, new RgbColor(1, 0, 0));
equals(path.fillColor.toCssString(), 'rgba(255, 0, 0, 1)');
equals(path.fillColor.toCss(), 'rgb(255, 0, 0)');
});
test('Creating colors', function() {

View file

@ -70,4 +70,23 @@ test('clockwise', function() {
equals(function() {
return path3.clockwise;
}, false);
})
});
test('Cloning with non-standard clockwise settings', function() {
var path1 = new Path.Rectangle([200, 200], [100, 100]);
var path2 = new Path.Rectangle([50, 50], [200, 200]);
var path3 = new Path.Rectangle([0, 0], [400, 400]);
path1.clockwise = false;
path2.clockwise = true;
path3.clockwise = true;
var compound = new CompoundPath(path1, path2, path3);
equals(function() {
return path1.clockwise;
}, false);
equals(function() {
return path2.clockwise;
}, true);
equals(function() {
return path3.clockwise;
}, true);
});

View file

@ -50,4 +50,12 @@ test('group.bounds when group contains empty group', function() {
compareRectangles(group.bounds, { x: 75, y: 75, width: 100, height: 100 }, 'group.bounds without empty group');
group.addChild(new Group());
compareRectangles(group.bounds, { x: 75, y: 75, width: 100, height: 100 }, 'group.bounds with empty group');
});
test('path.bounds when contained in a transformed group', function() {
var path = new Path([10, 10], [60, 60]);
var group = new Group([path]);
compareRectangles(path.bounds, { x: 10, y: 10, width: 50, height: 50 }, 'path.bounds before group translation');
group.translate(100, 100);
compareRectangles(path.bounds, { x: 110, y: 110, width: 50, height: 50 }, 'path.bounds after group translation');
});

View file

@ -68,7 +68,10 @@ test('Curve list after removing a segment - 1', function() {
return path.curves.length;
}, 2, 'After creating a path with three segments, we should have two curves. By accessing path.curves we also make sure the curves are created internally.');
path.segments[1].remove();
equals(function() {
return path.segments[1].remove();
}, true, 'Removing the paths second segment should be succesfull.');
equals(function() {
return path.curves.length;
}, 1, 'After removing the middle segment, we should be left with one curve');
@ -81,7 +84,9 @@ test('Curve list after removing a segment - 2', function() {
return path.curves.length;
}, 2, 'After creating a path with three segments, we should have two curves. By accessing path.curves we also make sure the curves are created internally.');
path.segments[2].remove();
equals(function() {
return path.segments[2].remove();
}, true, 'Removing the paths last segment should be succesfull.');
equals(function() {
return path.curves.length;

View file

@ -18,6 +18,12 @@
module('SvgImport');
test('Import complex CompoundPath and clone', function() {
var svg = createSvg('<path id="path" fill="red" d="M4,14h20v-2H4V14z M15,26h7v-2h-7V26z M15,22h9v-2h-9V22z M15,18h9v-2h-9V18z M4,26h9V16H4V26z M28,10V6H0v22c0,0,0,4,4,4 h25c0,0,3-0.062,3-4V10H28z M4,30c-2,0-2-2-2-2V8h24v20c0,0.921,0.284,1.558,0.676,2H4z"/>;');
var item = paper.project.importSvg(svg.getElementById('path'));
compareItems(item, item.clone());
});
test('make an svg line', function() {
var svgns = 'http://www.w3.org/2000/svg';
var shape = document.createElementNS(svgns, 'line');