UNPKG

@sky-foundry/two.js

Version:

A renderer agnostic two-dimensional drawing api for the web.

733 lines (548 loc) 16.8 kB
(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);