cupiditatea
Version:
A two-dimensional drawing api meant for modern browsers.
490 lines (354 loc) • 11.7 kB
JavaScript
(function(Two) {
// Localize variables
var mod = Two.Utils.mod, toFixed = Two.Utils.toFixed;
var svg = {
version: 1.1,
ns: 'http://www.w3.org/2000/svg',
xlink: 'http://www.w3.org/1999/xlink',
/**
* Create an svg namespaced element.
*/
createElement: function(name, attrs) {
var tag = name;
var elem = document.createElementNS(this.ns, tag);
if (tag === 'svg') {
attrs = _.defaults(attrs || {}, {
version: this.version
});
}
if (_.isObject(attrs)) {
svg.setAttributes(elem, attrs);
}
return elem;
},
/**
* Add attributes from an svg element.
*/
setAttributes: function(elem, attrs) {
for (var key in attrs) {
elem.setAttribute(key, attrs[key]);
}
return this;
},
/**
* Remove attributes from an svg element.
*/
removeAttributes: function(elem, attrs) {
for (var key in attrs) {
elem.removeAttribute(key);
}
return this;
},
/**
* Turn a set of vertices into a string for the d property of a path
* element. It is imperative that the string collation is as fast as
* possible, because this call will be happening multiple times a
* second.
*/
toString: function(points, closed) {
var l = points.length,
last = l - 1,
d, // The elusive last Two.Commands.move point
ret = '';
for (var i = 0; i < l; i++) {
var b = points[i];
var command;
var prev = closed ? mod(i - 1, l) : Math.max(i - 1, 0);
var next = closed ? mod(i + 1, l) : Math.min(i + 1, last);
var a = points[prev];
var c = points[next];
var vx, vy, ux, uy, ar, bl, br, cl;
// Access x and y directly,
// bypassing the getter
var x = toFixed(b._x);
var y = toFixed(b._y);
switch (b._command) {
case Two.Commands.close:
command = Two.Commands.close;
break;
case Two.Commands.curve:
ar = (a.controls && a.controls.right) || a;
bl = (b.controls && b.controls.left) || b;
if (a._relative) {
vx = toFixed((ar.x + a.x));
vy = toFixed((ar.y + a.y));
} else {
vx = toFixed(ar.x);
vy = toFixed(ar.y);
}
if (b._relative) {
ux = toFixed((bl.x + b.x));
uy = toFixed((bl.y + b.y));
} else {
ux = toFixed(bl.x);
uy = toFixed(bl.y);
}
command = ((i === 0) ? Two.Commands.move : Two.Commands.curve) +
' ' + vx + ' ' + vy + ' ' + ux + ' ' + uy + ' ' + x + ' ' + y;
break;
case Two.Commands.move:
d = b;
command = Two.Commands.move + ' ' + x + ' ' + y;
break;
default:
command = b._command + ' ' + x + ' ' + y;
}
// Add a final point and close it off
if (i >= last && closed) {
if (b._command === Two.Commands.curve) {
// Make sure we close to the most previous Two.Commands.move
c = d;
br = (b.controls && b.controls.right) || b;
cl = (c.controls && c.controls.left) || c;
if (b._relative) {
vx = toFixed((br.x + b.x));
vy = toFixed((br.y + b.y));
} else {
vx = toFixed(br.x);
vy = toFixed(br.y);
}
if (c._relative) {
ux = toFixed((cl.x + c.x));
uy = toFixed((cl.y + c.y));
} else {
ux = toFixed(cl.x);
uy = toFixed(cl.y);
}
x = toFixed(c.x);
y = toFixed(c.y);
command +=
' C ' + vx + ' ' + vy + ' ' + ux + ' ' + uy + ' ' + x + ' ' + y;
}
command += ' Z';
}
ret += command + ' ';
}
return ret;
},
getClip: function(shape) {
clip = shape._renderer.clip;
if (!clip) {
root = shape;
while (root.parent) {
root = root.parent;
}
clip = shape._renderer.clip = svg.createElement('clipPath');
root.defs.appendChild(clip);
}
return clip;
},
group: {
// TODO: Can speed up.
// TODO: How does this effect a f
appendChild: function(id) {
var elem = this.domElement.querySelector('#' + id);
if (!elem) {
return;
}
var tag = elem.nodeName;
if (!tag) {
return;
}
var tagName = tag.replace(/svg\:/ig, '').toLowerCase();
// Defer additions while clipping
if (/clippath/.test(tagName)) {
return;
}
this.elem.appendChild(elem);
},
// TODO: Can speed up.
removeChild: function(id) {
var elem = this.domElement.querySelector('#' + id);
if (!elem) {
return;
}
var tag = elem.nodeName;
if (!tag) {
return;
}
var tagName = tag.replace(/svg\:/ig, '').toLowerCase();
// Defer subtractions while clipping
if (/clippath/.test(tagName)) {
return;
}
this.elem.removeChild(elem);
},
renderChild: function(child) {
svg[child._renderer.type].render.call(child, this);
},
render: function(domElement) {
this._update();
// Shortcut for hidden objects.
// Doesn't reset the flags, so changes are stored and
// applied once the object is visible again
if (this._opacity === 0 && !this._flagOpacity) {
return this;
}
if (!this._renderer.elem) {
this._renderer.elem = svg.createElement('g', {
id: this.id
});
domElement.appendChild(this._renderer.elem);
}
// _Update styles for the <g>
var flagMatrix = this._matrix.manual || this._flagMatrix;
var context = {
domElement: domElement,
elem: this._renderer.elem
};
if (flagMatrix) {
this._renderer.elem.setAttribute('transform', 'matrix(' + this._matrix.toString() + ')');
}
for (var id in this.children) {
var child = this.children[id];
svg[child._renderer.type].render.call(child, domElement);
}
if (this._flagOpacity) {
this._renderer.elem.setAttribute('opacity', this._opacity);
}
if (this._flagAdditions) {
_.each(this.additions, svg.group.appendChild, context);
}
if (this._flagSubtractions) {
_.each(this.subtractions, svg.group.removeChild, context);
}
/**
* Commented two-way functionality of clips / masks with groups and
* polygons. Uncomment when this bug is fixed:
* https://code.google.com/p/chromium/issues/detail?id=370951
*/
// if (this._flagClip) {
// clip = svg.getClip(this);
// elem = this._renderer.elem;
// if (this._clip) {
// elem.removeAttribute('id');
// clip.setAttribute('id', this.id);
// clip.appendChild(elem);
// } else {
// clip.removeAttribute('id');
// elem.setAttribute('id', this.id);
// this.parent._renderer.elem.appendChild(elem); // TODO: should be insertBefore
// }
// }
if (this._flagMask) {
if (this._mask) {
this._renderer.elem.setAttribute('clip-path', 'url(#' + this._mask.id + ')');
} else {
this._renderer.elem.removeAttribute('clip-path');
}
}
return this.flagReset();
}
},
polygon: {
render: function(domElement) {
this._update();
// Shortcut for hidden objects.
// Doesn't reset the flags, so changes are stored and
// applied once the object is visible again
if (this._opacity === 0 && !this._flagOpacity) {
return this;
}
// Collect any attribute that needs to be changed here
var changed = {};
var flagMatrix = this._matrix.manual || this._flagMatrix;
if (flagMatrix) {
changed.transform = 'matrix(' + this._matrix.toString() + ')';
}
if (this._flagVertices) {
var vertices = svg.toString(this._vertices, this._closed);
changed.d = vertices;
}
if (this._flagFill) {
changed.fill = this._fill;
}
if (this._flagStroke) {
changed.stroke = this._stroke;
}
if (this._flagLinewidth) {
changed['stroke-width'] = this._linewidth;
}
if (this._flagOpacity) {
changed['stroke-opacity'] = this._opacity;
changed['fill-opacity'] = this._opacity;
}
if (this._flagVisible) {
changed.visibility = this._visible ? 'visible' : 'hidden';
}
if (this._flagCap) {
changed['stroke-linecap'] = this._cap;
}
if (this._flagJoin) {
changed['stroke-linejoin'] = this._join;
}
if (this._flagMiter) {
changed['stroke-miterlimit'] = this.miter;
}
// If there is no attached DOM element yet,
// create it with all necessary attributes.
if (!this._renderer.elem) {
changed.id = this.id;
this._renderer.elem = svg.createElement('path', changed);
domElement.appendChild(this._renderer.elem);
// Otherwise apply all pending attributes
} else {
svg.setAttributes(this._renderer.elem, changed);
}
if (this._flagClip) {
clip = svg.getClip(this);
elem = this._renderer.elem;
if (this._clip) {
elem.removeAttribute('id');
clip.setAttribute('id', this.id);
clip.appendChild(elem);
} else {
clip.removeAttribute('id');
elem.setAttribute('id', this.id);
this.parent._renderer.elem.appendChild(elem); // TODO: should be insertBefore
}
}
/**
* Commented two-way functionality of clips / masks with groups and
* polygons. Uncomment when this bug is fixed:
* https://code.google.com/p/chromium/issues/detail?id=370951
*/
// if (this._flagMask) {
// if (this._mask) {
// elem.setAttribute('clip-path', 'url(#' + this._mask.id + ')');
// } else {
// elem.removeAttribute('clip-path');
// }
// }
return this.flagReset();
}
}
};
/**
* @class
*/
var Renderer = Two[Two.Types.svg] = function(params) {
this.domElement = params.domElement || svg.createElement('svg');
this.scene = new Two.Group();
this.scene.parent = this;
this.defs = svg.createElement('defs');
this.domElement.appendChild(this.defs);
};
_.extend(Renderer, {
Utils: svg
});
_.extend(Renderer.prototype, Backbone.Events, {
setSize: function(width, height) {
this.width = width;
this.height = height;
svg.setAttributes(this.domElement, {
width: width,
height: height
});
return this;
},
render: function() {
svg.group.render.call(this.scene, this.domElement);
return this;
}
});
})(Two);