cupiditatea
Version:
A two-dimensional drawing api meant for modern browsers.
734 lines (538 loc) • 16.1 kB
JavaScript
(function(Two) {
/**
* Constants
*/
var min = Math.min, max = Math.max, round = Math.round,
getComputedMatrix = Two.Utils.getComputedMatrix;
var commands = {};
_.each(Two.Commands, function(v, k) {
commands[k] = new RegExp(v);
});
var Polygon = Two.Polygon = function(vertices, closed, curved, manual) {
Two.Shape.call(this);
this._renderer.type = 'polygon';
this._closed = !!closed;
this._curved = !!curved;
this.beginning = 0;
this.ending = 1;
// Style properties
this.fill = '#fff';
this.stroke = '#000';
this.linewidth = 1.0;
this.opacity = 1.0;
this.visible = true;
this.cap = 'butt'; // Default of Adobe Illustrator
this.join = 'miter'; // Default of Adobe Illustrator
this.miter = 4; // Default of Adobe Illustrator
this._vertices = [];
this.vertices = vertices;
// Determines whether or not two.js should calculate curves, lines, and
// commands automatically for you or to let the developer manipulate them
// for themselves.
this.automatic = !manual;
};
_.extend(Polygon, {
Properties: [
'fill',
'stroke',
'linewidth',
'opacity',
'visible',
'cap',
'join',
'miter',
'closed',
'curved',
'automatic',
'beginning',
'ending'
],
FlagVertices: function() {
this._flagVertices = true;
this._flagLength = true;
},
MakeObservable: function(object) {
Two.Shape.MakeObservable(object);
// Only the first 8 properties are flagged like this. The subsequent
// properties behave differently and need to be hand written.
_.each(Polygon.Properties.slice(0, 8), function(property) {
var secret = '_' + property;
var flag = '_flag' + property.charAt(0).toUpperCase() + property.slice(1);
Object.defineProperty(object, property, {
get: function() {
return this[secret];
},
set: function(v) {
this[secret] = v;
this[flag] = true;
}
});
});
Object.defineProperty(object, 'length', {
get: function() {
if (this._flagLength) {
this._updateLength();
}
return this._length;
}
});
Object.defineProperty(object, 'closed', {
get: function() {
return this._closed;
},
set: function(v) {
this._closed = !!v;
this._flagVertices = true;
}
});
Object.defineProperty(object, 'curved', {
get: function() {
return this._curved;
},
set: function(v) {
this._curved = !!v;
this._flagVertices = true;
}
});
Object.defineProperty(object, 'automatic', {
get: function() {
return this._automatic;
},
set: function(v) {
if (v === this._automatic) {
return;
}
this._automatic = !!v;
var method = this._automatic ? 'ignore' : 'listen';
_.each(this.vertices, function(v) {
v[method]();
});
}
});
Object.defineProperty(object, 'beginning', {
get: function() {
return this._beginning;
},
set: function(v) {
this._beginning = min(max(v, 0.0), this._ending);
this._flagVertices = true;
}
});
Object.defineProperty(object, 'ending', {
get: function() {
return this._ending;
},
set: function(v) {
this._ending = min(max(v, this._beginning), 1.0);
this._flagVertices = true;
}
});
Object.defineProperty(object, 'vertices', {
get: function() {
return this._collection;
},
set: function(vertices) {
var updateVertices = _.bind(Polygon.FlagVertices, this);
var bindVerts = _.bind(function(items) {
// This function is called a lot
// when importing a large SVG
var i = items.length;
while(i--) {
items[i].bind(Two.Events.change, updateVertices);
}
updateVertices();
}, this);
var unbindVerts = _.bind(function(items) {
_.each(items, function(v) {
v.unbind(Two.Events.change, updateVertices);
}, this);
updateVertices();
}, this);
// Remove previous listeners
if (this._collection) {
this._collection.unbind();
}
// Create new Collection with copy of vertices
this._collection = new Two.Utils.Collection(vertices.slice(0));
// Listen for Collection changes and bind / unbind
this._collection.bind(Two.Events.insert, bindVerts);
this._collection.bind(Two.Events.remove, unbindVerts);
// Bind Initial Vertices
bindVerts(this._collection);
}
});
Object.defineProperty(object, 'clip', {
get: function() {
return this._clip;
},
set: function(v) {
this._clip = v;
this._flagClip = true;
}
});
}
});
_.extend(Polygon.prototype, Two.Shape.prototype, {
// Flags
// http://en.wikipedia.org/wiki/Flag
_flagVertices: true,
_flagLength: true,
_flagFill: true,
_flagStroke: true,
_flagLinewidth: true,
_flagOpacity: true,
_flagVisible: true,
_flagCap: true,
_flagJoin: true,
_flagMiter: true,
_flagClip: false,
// Underlying Properties
_length: 0,
_fill: '#fff',
_stroke: '#000',
_linewidth: 1.0,
_opacity: 1.0,
_visible: true,
_cap: 'round',
_join: 'round',
_miter: 4,
_closed: true,
_curved: false,
_automatic: true,
_beginning: 0,
_ending: 1.0,
_clip: false,
clone: function(parent) {
parent = parent || this.parent;
var points = _.map(this.vertices, function(v) {
return v.clone();
});
var clone = new Polygon(points, this.closed, this.curved, !this.automatic);
_.each(Two.Shape.Properties, function(k) {
clone[k] = this[k];
}, this);
clone.translation.copy(this.translation);
clone.rotation = this.rotation;
clone.scale = this.scale;
parent.add(clone);
return clone;
},
toObject: function() {
var result = {
vertices: _.map(this.vertices, function(v) {
return v.toObject();
})
};
_.each(Two.Shape.Properties, function(k) {
result[k] = this[k];
}, this);
result.translation = this.translation.toObject;
result.rotation = this.rotation;
result.scale = this.scale;
return result;
},
noFill: function() {
this.fill = 'transparent';
return this;
},
noStroke: function() {
this.stroke = 'transparent';
return this;
},
/**
* Orient the vertices of the shape to the upper lefthand
* corner of the polygon.
*/
corner: function() {
var rect = this.getBoundingClientRect(true);
rect.centroid = {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2
};
_.each(this.vertices, function(v) {
v.addSelf(rect.centroid);
});
return this;
},
/**
* Orient the vertices of the shape to the center of the
* polygon.
*/
center: function() {
var rect = this.getBoundingClientRect(true);
rect.centroid = {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2
};
_.each(this.vertices, function(v) {
v.subSelf(rect.centroid);
});
// this.translation.addSelf(rect.centroid);
return this;
},
/**
* Remove self from the scene / parent.
*/
remove: function() {
if (!this.parent) {
return this;
}
this.parent.remove(this);
return this;
},
/**
* Return an object with top, left, right, bottom, width, and height
* parameters of the group.
*/
getBoundingClientRect: function(shallow) {
// TODO: Update this to not __always__ update. Just when it needs to.
this._update(true);
var matrix = !!shallow ? this._matrix : getComputedMatrix(this);
var border = this.linewidth / 2, x, y;
var left = Infinity, right = -Infinity,
top = Infinity, bottom = -Infinity;
_.each(this._vertices, function(v) {
x = v.x;
y = v.y;
v = matrix.multiply(x, y , 1);
top = min(v.y - border, top);
left = min(v.x - border, left);
right = max(v.x + border, right);
bottom = max(v.y + border, bottom);
});
return {
top: top,
left: left,
right: right,
bottom: bottom,
width: right - left,
height: bottom - top
};
},
/**
* Given a float `t` from 0 to 1, return a point or assign a passed `obj`'s
* coordinates to that percentage on this Two.Polygon's curve.
*/
getPointAt: function(t, obj) {
var x, x1, x2, x3, x4, y, y1, y2, y3, y4, left, right;
var target = this.length * Math.min(Math.max(t, 0), 1);
var length = this.vertices.length;
var last = length - 1;
var a = null;
var b = null;
for (var i = 0, l = this._lengths.length, sum = 0; i < l; i++) {
if (sum + this._lengths[i] > target) {
a = this.vertices[this.closed ? Two.Utils.mod(i, length) : i];
b = this.vertices[Math.min(Math.max(i - 1, 0), last)];
target -= sum;
t = target / this._lengths[i];
break;
}
sum += this._lengths[i];
}
if (_.isNull(a) || _.isNull(b)) {
return null;
}
right = b.controls && b.controls.right;
left = a.controls && a.controls.left;
x1 = b.x;
y1 = b.y;
x2 = (right || b).x;
y2 = (right || b).y;
x3 = (left || a).x;
y3 = (left || a).y;
x4 = a.x;
y4 = a.y;
if (right && b._relative) {
x2 += b.x;
y2 += b.y;
}
if (left && a._relative) {
x3 += a.x;
y3 += a.y;
}
x = Two.Utils.getPointOnCubicBezier(t, x1, x2, x3, x4);
y = Two.Utils.getPointOnCubicBezier(t, y1, y2, y3, y4);
if (_.isObject(obj)) {
obj.x = x;
obj.y = y;
return obj;
}
return new Two.Vector(x, y);
},
/**
* Based on closed / curved and sorting of vertices plot where all points
* should be and where the respective handles should be too.
*/
plot: function() {
if (this.curved) {
Two.Utils.getCurveFromPoints(this._vertices, this.closed);
return this;
}
for (var i = 0; i < this._vertices.length; i++) {
this._vertices[i]._command = i === 0 ? Two.Commands.move : Two.Commands.line;
}
return this;
},
subdivide: function(limit) {
//TODO: DRYness (function below)
this._update();
var last = this.vertices.length - 1;
var b = this.vertices[last];
var closed = this._closed || this.vertices[last]._command === Two.Commands.close;
var points = [];
_.each(this.vertices, function(a, i) {
if (i <= 0 && !closed) {
b = a;
return;
}
if (a.command === Two.Commands.move) {
points.push(new Two.Anchor(b.x, b.y));
if (i > 0) {
points[points.length - 1].command = Two.Commands.line;
}
b = a;
return;
}
var verts = getSubdivisions(a, b, limit);
points = points.concat(verts);
// Assign commands to all the verts
_.each(verts, function(v, i) {
if (i <= 0 && b.command === Two.Commands.move) {
v.command = Two.Commands.move;
} else {
v.command = Two.Commands.line;
}
});
if (i >= last) {
// TODO: Add check if the two vectors in question are the same values.
if (this._closed && this._automatic) {
b = a;
verts = getSubdivisions(a, b, limit);
points = points.concat(verts);
// Assign commands to all the verts
_.each(verts, function(v, i) {
if (i <= 0 && b.command === Two.Commands.move) {
v.command = Two.Commands.move;
} else {
v.command = Two.Commands.line;
}
});
} else if (closed) {
points.push(new Two.Anchor(a.x, a.y));
}
points[points.length - 1].command = closed ? Two.Commands.close : Two.Commands.line;
}
b = a;
}, this);
this._automatic = false;
this._curved = false;
this.vertices = points;
return this;
},
_updateLength: function(limit) {
//TODO: DRYness (function above)
this._update();
var last = this.vertices.length - 1;
var b = this.vertices[last];
var closed = this._closed || this.vertices[last]._command === Two.Commands.close;
var sum = 0;
if (_.isUndefined(this._lengths)) {
this._lengths = [];
}
_.each(this.vertices, function(a, i) {
if ((i <= 0 && !closed) || a.command === Two.Commands.move) {
b = a;
this._lengths[i] = 0;
return;
}
this._lengths[i] = getCurveLength(a, b, limit);
sum += this._lengths[i];
if (i >= last && closed) {
b = a;
this._lengths[i + 1] = getCurveLength(a, b, limit);
sum += this._lengths[i + 1];
}
b = a;
}, this);
this._length = sum;
return this;
},
_update: function() {
if (this._flagVertices) {
var l = this.vertices.length;
var last = l - 1, v;
var ia = round((this._beginning) * last);
var ib = round((this._ending) * last);
this._vertices.length = 0;
for (var i = ia; i < ib + 1; i++) {
v = this.vertices[i];
this._vertices.push(v);
}
if (this._automatic) {
this.plot();
}
}
Two.Shape.prototype._update.call(this);
return this;
},
flagReset: function() {
this._flagVertices = this._flagFill = this._flagStroke =
this._flagLinewidth = this._flagOpacity = this._flagVisible =
this._flagCap = this._flagJoin = this._flagMiter =
this._flagClip = false;
Two.Shape.prototype.flagReset.call(this);
return this;
}
});
Polygon.MakeObservable(Polygon.prototype);
/**
* Utility functions
*/
function getCurveLength(a, b, limit) {
// TODO: DRYness
var x1, x2, x3, x4, y1, y2, y3, y4;
var right = b.controls && b.controls.right;
var left = a.controls && a.controls.left;
x1 = b.x;
y1 = b.y;
x2 = (right || b).x;
y2 = (right || b).y;
x3 = (left || a).x;
y3 = (left || a).y;
x4 = a.x;
y4 = a.y;
if (right && b._relative) {
x2 += b.x;
y2 += b.y;
}
if (left && a._relative) {
x3 += a.x;
y3 += a.y;
}
return Two.Utils.getCurveLength(x1, y1, x2, y2, x3, y3, x4, y4, limit);
}
function getSubdivisions(a, b, limit) {
// TODO: DRYness
var x1, x2, x3, x4, y1, y2, y3, y4;
var right = b.controls && b.controls.right;
var left = a.controls && a.controls.left;
x1 = b.x;
y1 = b.y;
x2 = (right || b).x;
y2 = (right || b).y;
x3 = (left || a).x;
y3 = (left || a).y;
x4 = a.x;
y4 = a.y;
if (right && b._relative) {
x2 += b.x;
y2 += b.y;
}
if (left && a._relative) {
x3 += a.x;
y3 += a.y;
}
return Two.Utils.subdivide(x1, y1, x2, y2, x3, y3, x4, y4, limit);
}
})(Two);