@sky-foundry/two.js
Version:
A renderer agnostic two-dimensional drawing api for the web.
733 lines (548 loc) • 16.8 kB
JavaScript
(function(Two) {
// Constants
var min = Math.min, max = Math.max;
var _ = Two.Utils;
/**
* @class
* @name Two.Group.Children
* @extends Two.Utils.Collection
* @description A children collection which is accesible both by index and by object id
*/
var Children = function() {
Two.Utils.Collection.apply(this, arguments);
Object.defineProperty(this, '_events', {
value : {},
enumerable: false
});
this.ids = {};
this.on(Two.Events.insert, this.attach);
this.on(Two.Events.remove, this.detach);
Children.prototype.attach.apply(this, arguments);
};
Children.prototype = new Two.Utils.Collection();
_.extend(Children.prototype, {
constructor: Children,
attach: function(children) {
for (var i = 0; i < children.length; i++) {
this.ids[children[i].id] = children[i];
}
return this;
},
detach: function(children) {
for (var i = 0; i < children.length; i++) {
delete this.ids[children[i].id];
}
return this;
}
});
/**
* @class
* @name Two.Group
*/
var Group = Two.Group = function(children) {
Two.Shape.call(this, true);
this._renderer.type = 'group';
this.additions = [];
this.subtractions = [];
this.children = _.isArray(children) ? children : arguments;
};
_.extend(Group, {
Children: Children,
InsertChildren: function(children) {
for (var i = 0; i < children.length; i++) {
replaceParent.call(this, children[i], this);
}
},
RemoveChildren: function(children) {
for (var i = 0; i < children.length; i++) {
replaceParent.call(this, children[i]);
}
},
OrderChildren: function(children) {
this._flagOrder = true;
},
Properties: [
'fill',
'stroke',
'linewidth',
'visible',
'cap',
'join',
'miter',
],
MakeObservable: function(object) {
var properties = Two.Group.Properties;
Object.defineProperty(object, 'opacity', {
enumerable: true,
get: function() {
return this._opacity;
},
set: function(v) {
this._flagOpacity = this._opacity !== v;
this._opacity = v;
}
});
Object.defineProperty(object, 'className', {
enumerable: true,
get: function() {
return this._className;
},
set: function(v) {
this._flagClassName = this._className !== v;
this._className = v;
}
});
Object.defineProperty(object, 'beginning', {
enumerable: true,
get: function() {
return this._beginning;
},
set: function(v) {
this._flagBeginning = this._beginning !== v;
this._beginning = v;
}
});
Object.defineProperty(object, 'ending', {
enumerable: true,
get: function() {
return this._ending;
},
set: function(v) {
this._flagEnding = this._ending !== v;
this._ending = v;
}
});
Object.defineProperty(object, 'length', {
enumerable: true,
get: function() {
if (this._flagLength || this._length <= 0) {
this._length = 0;
for (var i = 0; i < this.children.length; i++) {
var child = this.children[i];
this._length += child.length;
}
}
return this._length;
}
});
Two.Shape.MakeObservable(object);
Group.MakeGetterSetters(object, properties);
Object.defineProperty(object, 'children', {
enumerable: true,
get: function() {
return this._children;
},
set: function(children) {
var insertChildren = _.bind(Group.InsertChildren, this);
var removeChildren = _.bind(Group.RemoveChildren, this);
var orderChildren = _.bind(Group.OrderChildren, this);
if (this._children) {
this._children.unbind();
}
this._children = new Children(children);
this._children.bind(Two.Events.insert, insertChildren);
this._children.bind(Two.Events.remove, removeChildren);
this._children.bind(Two.Events.order, orderChildren);
}
});
Object.defineProperty(object, 'mask', {
enumerable: true,
get: function() {
return this._mask;
},
set: function(v) {
this._mask = v;
this._flagMask = true;
if (!v.clip) {
v.clip = true;
}
}
});
},
MakeGetterSetters: function(group, properties) {
if (!_.isArray(properties)) {
properties = [properties];
}
_.each(properties, function(k) {
Group.MakeGetterSetter(group, k);
});
},
MakeGetterSetter: function(group, k) {
var secret = '_' + k;
Object.defineProperty(group, k, {
enumerable: true,
get: function() {
return this[secret];
},
set: function(v) {
this[secret] = v;
_.each(this.children, function(child) { // Trickle down styles
child[k] = v;
});
}
});
}
});
_.extend(Group.prototype, Two.Shape.prototype, {
// Flags
// http://en.wikipedia.org/wiki/Flag
_flagAdditions: false,
_flagSubtractions: false,
_flagOrder: false,
_flagOpacity: true,
_flagClassName: false,
_flagBeginning: false,
_flagEnding: false,
_flagLength: false,
_flagMask: false,
// Underlying Properties
_fill: '#fff',
_stroke: '#000',
_linewidth: 1.0,
_opacity: 1.0,
_className: '',
_visible: true,
_cap: 'round',
_join: 'round',
_miter: 4,
_closed: true,
_curved: false,
_automatic: true,
_beginning: 0,
_ending: 1.0,
_length: 0,
_mask: null,
constructor: Group,
// /**
// * TODO: Group has a gotcha in that it's at the moment required to be bound to
// * an instance of two in order to add elements correctly. This needs to
// * be rethought and fixed.
// */
clone: function(parent) {
var group = new Group();
var children = _.map(this.children, function(child) {
return child.clone();
});
group.add(children);
group.opacity = this.opacity;
if (this.mask) {
group.mask = this.mask;
}
group.translation.copy(this.translation);
group.rotation = this.rotation;
group.scale = this.scale;
group.className = this.className;
if (parent) {
parent.add(group);
}
return group._update();
},
// /**
// * Export the data from the instance of Two.Group into a plain JavaScript
// * object. This also makes all children plain JavaScript objects. Great
// * for turning into JSON and storing in a database.
// */
toObject: function() {
var result = {
children: [],
translation: this.translation.toObject(),
rotation: this.rotation,
scale: this.scale instanceof Two.Vector ? this.scale.toObject() : this.scale,
opacity: this.opacity,
className: this.className,
mask: (this.mask ? this.mask.toObject() : null)
};
_.each(this.children, function(child, i) {
result.children[i] = child.toObject();
}, this);
return result;
},
// /**
// * Anchor all children to the upper left hand corner
// * of the group.
// */
corner: function() {
var rect = this.getBoundingClientRect(true),
corner = { x: rect.left, y: rect.top };
this.children.forEach(function(child) {
child.translation.sub(corner);
});
return this;
},
// /**
// * Anchors all children around the center of the group,
// * effectively placing the shape around the unit circle.
// */
center: function() {
var rect = this.getBoundingClientRect(true);
rect.centroid = {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2
};
this.children.forEach(function(child) {
if (child.isShape) {
child.translation.sub(rect.centroid);
}
});
// this.translation.copy(rect.centroid);
return this;
},
// /**
// * Recursively search for id. Returns the first element found.
// * Returns null if none found.
// */
getById: function (id) {
var search = function (node, id) {
if (node.id === id) {
return node;
} else if (node.children) {
var i = node.children.length;
while (i--) {
var found = search(node.children[i], id);
if (found) return found;
}
}
};
return search(this, id) || null;
},
// /**
// * Recursively search for classes. Returns an array of matching elements.
// * Empty array if none found.
// */
getByClassName: function (cl) {
var found = [];
var search = function (node, cl) {
if (node.classList.indexOf(cl) != -1) {
found.push(node);
} else if (node.children) {
node.children.forEach(function (child) {
search(child, cl);
});
}
return found;
};
return search(this, cl);
},
// /**
// * Recursively search for children of a specific type,
// * e.g. Two.Polygon. Pass a reference to this type as the param.
// * Returns an empty array if none found.
// */
getByType: function(type) {
var found = [];
var search = function (node, type) {
for (var id in node.children) {
if (node.children[id] instanceof type) {
found.push(node.children[id]);
} else if (node.children[id] instanceof Two.Group) {
search(node.children[id], type);
}
}
return found;
};
return search(this, type);
},
// /**
// * Add objects to the group.
// */
add: function(objects) {
// Allow to pass multiple objects either as array or as multiple arguments
// If it's an array also create copy of it in case we're getting passed
// a childrens array directly.
if (!(objects instanceof Array)) {
objects = _.toArray(arguments);
} else {
objects = objects.slice();
}
// Add the objects
for (var i = 0; i < objects.length; i++) {
if (!(objects[i] && objects[i].id)) continue;
this.children.push(objects[i]);
}
return this;
},
// /**
// * Remove objects from the group.
// */
remove: function(objects) {
var l = arguments.length,
grandparent = this.parent;
// Allow to call remove without arguments
// This will detach the object from its own parent.
if (l <= 0 && grandparent) {
grandparent.remove(this);
return this;
}
// Allow to pass multiple objects either as array or as multiple arguments
// If it's an array also create copy of it in case we're getting passed
// a childrens array directly.
if (!(objects instanceof Array)) {
objects = _.toArray(arguments);
} else {
objects = objects.slice();
}
// Remove the objects
for (var i = 0; i < objects.length; i++) {
if (!objects[i] || !(this.children.ids[objects[i].id])) continue;
this.children.splice(_.indexOf(this.children, objects[i]), 1);
}
return this;
},
// /**
// * Return an object with top, left, right, bottom, width, and height
// * parameters of the group.
// */
getBoundingClientRect: function(shallow) {
var rect;
// TODO: Update this to not __always__ update. Just when it needs to.
this._update(true);
// Variables need to be defined here, because of nested nature of groups.
var left = Infinity, right = -Infinity,
top = Infinity, bottom = -Infinity;
var regex = Two.Texture.RegularExpressions.effect;
for (var i = 0; i < this.children.length; i++) {
var child = this.children[i];
if (!child.visible || regex.test(child._renderer.type)) {
continue;
}
rect = child.getBoundingClientRect(shallow);
if (!_.isNumber(rect.top) || !_.isNumber(rect.left) ||
!_.isNumber(rect.right) || !_.isNumber(rect.bottom)) {
continue;
}
top = min(rect.top, top);
left = min(rect.left, left);
right = max(rect.right, right);
bottom = max(rect.bottom, bottom);
}
return {
top: top,
left: left,
right: right,
bottom: bottom,
width: right - left,
height: bottom - top
};
},
// /**
// * Trickle down of noFill
// */
noFill: function() {
this.children.forEach(function(child) {
child.noFill();
});
return this;
},
// /**
// * Trickle down of noStroke
// */
noStroke: function() {
this.children.forEach(function(child) {
child.noStroke();
});
return this;
},
// /**
// * Trickle down subdivide
// */
subdivide: function() {
var args = arguments;
this.children.forEach(function(child) {
child.subdivide.apply(child, args);
});
return this;
},
_update: function() {
if (this._flagBeginning || this._flagEnding) {
var beginning = Math.min(this._beginning, this._ending);
var ending = Math.max(this._beginning, this._ending);
var length = this.length;
var sum = 0;
var bd = beginning * length;
var ed = ending * length;
var distance = (ed - bd);
for (var i = 0; i < this.children.length; i++) {
var child = this.children[i];
var l = child.length;
if (bd > sum + l) {
child.beginning = 1;
child.ending = 1;
} else if (ed < sum) {
child.beginning = 0;
child.ending = 0;
} else if (bd > sum && bd < sum + l) {
child.beginning = (bd - sum) / l;
child.ending = 1;
} else if (ed > sum && ed < sum + l) {
child.beginning = 0;
child.ending = (ed - sum) / l;
} else {
child.beginning = 0;
child.ending = 1;
}
sum += l;
}
}
return Two.Shape.prototype._update.apply(this, arguments);
},
flagReset: function() {
if (this._flagAdditions) {
this.additions.length = 0;
this._flagAdditions = false;
}
if (this._flagSubtractions) {
this.subtractions.length = 0;
this._flagSubtractions = false;
}
this._flagOrder = this._flagMask = this._flagOpacity = this._flagClassName
this._flagBeginning = this._flagEnding = false;
Two.Shape.prototype.flagReset.call(this);
return this;
}
});
Group.MakeObservable(Group.prototype);
// /**
// * Helper function used to sync parent-child relationship within the
// * `Two.Group.children` object.
// *
// * Set the parent of the passed object to another object
// * and updates parent-child relationships
// * Calling with one arguments will simply remove the parenting
// */
function replaceParent(child, newParent) {
var parent = child.parent;
var index;
if (parent === newParent) {
this.additions.push(child);
this._flagAdditions = true;
return;
}
if (parent && parent.children.ids[child.id]) {
index = _.indexOf(parent.children, child);
parent.children.splice(index, 1);
// If we're passing from one parent to another...
index = _.indexOf(parent.additions, child);
if (index >= 0) {
parent.additions.splice(index, 1);
} else {
parent.subtractions.push(child);
parent._flagSubtractions = true;
}
}
if (newParent) {
child.parent = newParent;
this.additions.push(child);
this._flagAdditions = true;
return;
}
// If we're passing from one parent to another...
index = _.indexOf(this.additions, child);
if (index >= 0) {
this.additions.splice(index, 1);
} else {
this.subtractions.push(child);
this._flagSubtractions = true;
}
delete child.parent;
}
})((typeof global !== 'undefined' ? global : (this || window)).Two);