Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

getBoundingBox improvements #121

Merged
merged 13 commits into from
Oct 19, 2012
Merged
2 changes: 2 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ v0.4.1
* Fix out-of-range alpha values when parsing rgba() strings
* Add additional method signature for new Matrix([a, b, c, d, tx, ty])
* Add Matrix.fromString() method to create new Matrix instance from a string.
* Rename DisplayObject#getComputed to DisplayObject#getBoundingBox and improve
implementation for calculating the bounding box of paths

v0.4.0 / 2012-10-03
-------------------
Expand Down
46 changes: 46 additions & 0 deletions example/library/movies/bounding-box.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
function Tracker(path) {
this.trackee = path;
this.group = new Group().addTo(path.parent);
this.box = new Rect(0, 0, 1000, 1000).addTo(this.group).stroke('#000', 2);
this.vert1 = new Path().addTo(this.group).moveTo(0, 0).lineTo(0, 1000).stroke('#F00', 2);
this.vert2 = new Path().addTo(this.group).moveTo(0, 0).lineTo(0, 1000).stroke('#F00', 2);
this.horz1 = new Path().addTo(this.group).moveTo(0, 0).lineTo(1000, 0).stroke('#00F', 2);
this.horz2 = new Path().addTo(this.group).moveTo(0, 0).lineTo(1000, 0).stroke('#00F', 2);
stage.on('advance', this, this.track);
}

Tracker.prototype = {
track: function() {
var box = this.trackee.getBoundingBox( this.trackee.attr('matrix') );
this.vert1.attr('x', box.left);
this.vert2.attr('x', box.right);
this.horz1.attr('y', box.top);
this.horz2.attr('y', box.bottom);
}
}

var star = new Path.star(100, 100, 50, 5, 3);

star.fill('yellow').stroke('black', 1);
star.addTo(stage);
new Tracker(star);

anim();


function anim() {
star.morphTo(
new Path.star(
Math.random() * 400 + 200,
Math.random() * 400 + 200,
Math.random() * 80 + 20,
0 | Math.random() * 5 + 5,
Math.random() * 3 + .2
).fill('random').stroke('random', 1).attr('rotation', Math.random() * Math.PI*2),
'2s',
{
onEnd: anim,
easing: 'sineInOut'
}
);
}
3 changes: 2 additions & 1 deletion example/library/movies/movie_list.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ movieList = {
'shape-getPointAtLength.js',
'shape-center-of-arc.js',
'shape-fill-rule.js',
'overlapping-paths.js'
'overlapping-paths.js',
'bounding-box.js'
],
'Test': [
'text.js',
Expand Down
47 changes: 25 additions & 22 deletions src/runner/bitmap.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
define([
'./asset_display_object',
'../tools'
], function(AssetDisplayObject, tools) {
'../tools',
'../point'
], function(AssetDisplayObject, tools, Point) {
'use strict';

var data = tools.descriptorData, accessor = tools.descriptorAccessor;
Expand Down Expand Up @@ -140,44 +141,46 @@ define([
/**
* Get computed dimensions of the bitmap
*
* @param {String} key any of 'size', 'width', 'height', 'top', 'right', 'left', 'bottom'
* @returns {Object|Number} For the key 'size' it'll return an object with all
* properties, otherwise it'll return a single number for the key specified.
* @param {Matrix} [transform=null] A transform to apply to all points
* before computation.
* @returns {Object} an object with all box properties
*/
proto.getComputed = function(key) {
proto.getBoundingBox = function(transform) {

var value,
size = key === 'size' && {top: 0, right: 0, bottom: 0, left: 0},
box = {top: 0, right: 0, bottom: 0, left: 0},
naturalWidth = this._attributes._naturalWidth,
naturalHeight = this._attributes._naturalHeight,
attrWidth = this.attr('width'),
attrHeight = this.attr('height'),
naturalRatio = naturalWidth / naturalHeight,

// If one dimensions is not specified, then we use the other dimension
// and the ratio to calculate its size:
// and the ratio to calculate its box:
width = attrWidth || (
attrHeight != null ? naturalRatio * attrHeight : naturalWidth
) || 0,
height = attrHeight || (
attrWidth != null ? attrWidth / naturalRatio : naturalHeight
) || 0;
) || 0,

if (key === 'width' || key === 'right') {
value = width;
} else if (size) {
size.right = size.width = width;
}
if (key === 'height' || key === 'bottom') {
value = height;
} else if (size) {
size.bottom = size.height = height;
}
if (key === 'top' || key === 'left') {
value = 0;
topLeft,
bottomRight,
dimensions;

box.right = box.width = width;
box.bottom = box.height = height;

if (transform) {
topLeft = transform.transformPoint(new Point(0, 0));
bottomRight = transform.transformPoint(new Point(width, height));
box.top = topLeft.y;
box.left = topLeft.x;
box.right = bottomRight.x;
box.bottom = bottomRight.y;
}

return size || value;
return box;
};

return Bitmap;
Expand Down
72 changes: 34 additions & 38 deletions src/runner/display_list.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,53 +229,49 @@ define([
return this;
},

getComputed: function(key) {
/**
* Computes bounding boxes and single data points of a display object.
*
* @param {String} key What to compute. One of "top", "right", "bottom",
* "left", "width" and "height"
* @param {Matrix} [transform=null] A transform to apply to all points
* before computation.
* @return {Object}
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc block still has the key parameter

getBoundingBox: function(transform) {

var children = this.displayList.children;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if there are no children, the reduce call will have no effect. In that case, the method will always return top: 0, right: 0, bottom: 0, left: 0, width: 0, height: 0.

The code for empty objects already lives in DisplayObject.prototype.getBoundingBox().

Adding a guard here should do the trick.

if (!children.length) {
  return DisplayObject.prototype.getBoundingBox.call(this, transform);
}

And a unit test for this special case might be handy

var isOffsetKey =
key === 'top' ||
key === 'right' ||
key === 'bottom' ||
key === 'left';

if (isOffsetKey) {
var attributeName = (key === 'top' || key === 'bottom') ? 'y' : 'x';
var compare = (key === 'top' || key === 'left') ? min : max;

return tools.reduce(children, function(current, child, i) {
var childValue = child.attr(attributeName) + child.getComputed(key);
if (i === 0) {
return childValue;
}
return compare(current, childValue);
}, 0);
} else {
var size = tools.reduce(children, function(size, child, i) {
var childSize = child.getComputed('size');
var childX = child.attr('x');
var childY = child.attr('y');

var isFirst = i === 0;
var size = tools.reduce(children, function(size, child, i) {
var childMatrix = child.attr('matrix').clone();
var childSize = child.getBoundingBox(
transform ? childMatrix.concat(transform) : childMatrix
);
var childX = child.attr('x');
var childY = child.attr('y');
childX = childY = 0;

var childTop = childY + childSize.top;
size.top = isFirst ? childTop : min(size.top, childTop);
var isFirst = i === 0;

var childRight = childX + childSize.right;
size.right = isFirst ? childRight : max(size.right, childRight);
var childTop = childY + childSize.top;
size.top = isFirst ? childTop : min(size.top, childTop);

var childBottom = childY + childSize.bottom;
size.bottom = isFirst ? childBottom : max(size.bottom, childBottom);
var childRight = childX + childSize.right;
size.right = isFirst ? childRight : max(size.right, childRight);

var childLeft = childX + childSize.left;
size.left = isFirst ? childLeft : min(size.left, childLeft);
var childBottom = childY + childSize.bottom;
size.bottom = isFirst ? childBottom : max(size.bottom, childBottom);

return size;
}, {top: 0, right: 0, bottom: 0, left: 0, width: 0, height: 0});
var childLeft = childX + childSize.left;
size.left = isFirst ? childLeft : min(size.left, childLeft);

size.height = size.bottom - size.top;
size.width = size.right - size.left;
return size;
}, {top: 0, right: 0, bottom: 0, left: 0, width: 0, height: 0});

return key === 'size' ? size : size[key];
}
size.height = size.bottom - size.top;
size.width = size.right - size.left;

return size;
},
getIndexOfChild: function(displayObject) {
return this.displayList.children.indexOf(displayObject);
Expand Down
25 changes: 22 additions & 3 deletions src/runner/display_object.js
Original file line number Diff line number Diff line change
Expand Up @@ -577,9 +577,28 @@ define([
return this;
},

getComputed: function(thing) {
var size = {top: 0, right: 0, bottom: 0, left: 0, width: 0, height: 0};
return thing === 'size' ? size : size[thing];
/**
* Computes bounding boxes and single data points of a display object.
*
* @param {Matrix} [transform=null] A transform to apply to all points
* before computation.
* @returns {Object} an object with all box properties
*/
getBoundingBox: function(transform) {
var x = 0, y = 0;
if (transform) {
var transformed = transform.transformPoint({x: 0, y: 0});
x = transformed.x;
y = transformed.y;
}
return {
top: y,
right: x,
bottom: y,
left: x,
width: 0,
height: 0
};
},

/**
Expand Down
45 changes: 45 additions & 0 deletions src/runner/path/curved_path.js
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,51 @@ define([
return curvedSegments;
};

/**
* Calculates the potential bounds of a single cubic bezier curve
* @param {Array} p0 The starting point of the curve in the form [x, y]
* @param {Array} curve A curveTo segment (e.g. `['curveTo',n,n,n,n,n,n]`)
*/
CurvedPath.getPotentialBoundsOfCurve = function(p0x, p0y, cp1x, cp1y, cp2x, cp2y, p1x, p1y) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is potential about the returned bounds?


var p0 = [p0x, p0y];
var p1 = [cp1x, cp1y];
var p2 = [cp2x, cp2y];
var p3 = [p1x, p1y];
var bounds = [[], []];

bounds[0].push(p3[0]);
bounds[1].push(p3[1]);

for (var i = 0; i < 2; ++i) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a URL that points to the origin of this approach and if possible add some comments?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the worst part of the implementation because I am not sure how it works. I ported it from a python script I found here: http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html

I tried understanding the math on the article you linked to but in the end, after struggling, I grabbed this and ported it and it seemed to work.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@padolsey please add his MIT License note to our LICENSE file.

var b = 6 * p0[i] - 12 * p1[i] + 6 * p2[i];
var a = -3 * p0[i] + 9 * p1[i] - 9 * p2[i] + 3 * p3[i];
var c = 3 * p1[i] - 3 * p0[i];
if (a == 0) {
if (b == 0) continue;
var t = -c / b;
if (0 < t && t < 1) bounds[i].push(f(t))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

semicolon and brackets are missing

continue;
}
var b2ac = Math.pow(b, 2) - 4 * c * a;
if (b2ac < 0) continue;
var t1 = (-b + Math.sqrt(b2ac))/(2 * a);
if (0 < t1 && t1 < 1) bounds[i].push(f(t1));
var t2 = (-b - Math.sqrt(b2ac))/(2 * a);
if (0 < t2 && t2 < 1) bounds[i].push(f(t2));
}

// Return bounds in the form `[ xBoundsArray, yBoundsArray ]`
return bounds;

function f(t) {
return Math.pow(1-t, 3) * p0[i]
+ 3 * Math.pow(1-t, 2) * t * p1[i]
+ 3 * (1-t) * Math.pow(t, 2) * p2[i]
+ Math.pow(t, 3) * p3[i];
}
};

CurvedPath.fromArc = function(x1, y1, rx, ry, angle, large, sweep, x2, y2) {

// If the endpoints (x1, y1) and (x2, y2) are identical,
Expand Down