UNPKG

@sky-foundry/two.js

Version:

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

942 lines (693 loc) 24 kB
(function(Two) { // Localize variables var mod = Two.Utils.mod, toFixed = Two.Utils.toFixed; var _ = Two.Utils; var svg = { version: 1.1, ns: 'http://www.w3.org/2000/svg', xlink: 'http://www.w3.org/1999/xlink', alignments: { left: 'start', center: 'middle', right: 'end' }, /** * Create an svg namespaced element. */ createElement: function(name, attrs) { var tag = name; var elem = document.createElementNS(svg.ns, tag); if (tag === 'svg') { attrs = _.defaults(attrs || {}, { version: svg.version }); } if (!_.isEmpty(attrs)) { svg.setAttributes(elem, attrs); } return elem; }, /** * Add attributes from an svg element. */ setAttributes: function(elem, attrs) { var keys = Object.keys(attrs); for (var i = 0; i < keys.length; i++) { if (/href/.test(keys[i])) { elem.setAttributeNS(svg.xlink, keys[i], attrs[keys[i]]); } else { elem.setAttribute(keys[i], attrs[keys[i]]); } } 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 string = ''; 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; var rx, ry, xAxisRotation, largeArcFlag, sweepFlag; // 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.arc: rx = b.rx; ry = b.ry; xAxisRotation = b.xAxisRotation; largeArcFlag = b.largeArcFlag; sweepFlag = b.sweepFlag; command = Two.Commands.arc + ' ' + rx + ' ' + ry + ' ' + xAxisRotation + ' ' + largeArcFlag + ' ' + sweepFlag + ' ' + x + ' ' + y; break; case Two.Commands.curve: ar = (a.controls && a.controls.right) || Two.Vector.zero; bl = (b.controls && b.controls.left) || Two.Vector.zero; 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; } if (b.command !== Two.Commands.close) { command += ' Z'; } } string += command + ' '; } return string; }, getClip: function(shape) { var clip = shape._renderer.clip; if (!clip) { var 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(object) { var elem = object._renderer.elem; if (!elem) { return; } var tag = elem.nodeName; if (!tag || /(radial|linear)gradient/i.test(tag) || object._clip) { return; } this.elem.appendChild(elem); }, removeChild: function(object) { var elem = object._renderer.elem; if (!elem || elem.parentNode != this.elem) { return; } var tag = elem.nodeName; if (!tag) { return; } // Defer subtractions while clipping. if (object._clip) { return; } this.elem.removeChild(elem); }, orderChild: function(object) { this.elem.appendChild(object._renderer.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 i = 0; i < this.children.length; i++) { var child = this.children[i]; svg[child._renderer.type].render.call(child, domElement); } if (this._flagOpacity) { this._renderer.elem.setAttribute('opacity', this._opacity); } if (this._flagClassName) { this._renderer.elem.setAttribute('class', this._className); } if (this._flagAdditions) { this.additions.forEach(svg.group.appendChild, context); } if (this._flagSubtractions) { this.subtractions.forEach(svg.group.removeChild, context); } if (this._flagOrder) { this.children.forEach(svg.group.orderChild, 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(); } }, path: { 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._renderer.vertices, this._closed); changed.d = vertices; } if (this._fill && this._fill._renderer) { this._fill._update(); svg[this._fill._renderer.type].render.call(this._fill, domElement, true); } if (this._flagFill) { changed.fill = this._fill && this._fill.id ? 'url(#' + this._fill.id + ')' : this._fill; } if (this._stroke && this._stroke._renderer) { this._stroke._update(); svg[this._stroke._renderer.type].render.call(this._stroke, domElement, true); } if (this._flagStroke) { changed.stroke = this._stroke && this._stroke.id ? 'url(#' + this._stroke.id + ')' : 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._flagClassName) { changed['class'] = this._className; } 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 (this.dashes && this.dashes.length > 0) { changed['stroke-dasharray'] = this.dashes.join(' '); } // 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) { var clip = svg.getClip(this); var 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(); } }, text: { render: function(domElement) { this._update(); var changed = {}; var flagMatrix = this._matrix.manual || this._flagMatrix; if (flagMatrix) { changed.transform = 'matrix(' + this._matrix.toString() + ')'; } if (this._flagFamily) { changed['font-family'] = this._family; } if (this._flagSize) { changed['font-size'] = this._size; } if (this._flagLeading) { changed['line-height'] = this._leading; } if (this._flagAlignment) { changed['text-anchor'] = svg.alignments[this._alignment] || this._alignment; } if (this._flagBaseline) { changed['alignment-baseline'] = changed['dominant-baseline'] = this._baseline; } if (this._flagStyle) { changed['font-style'] = this._style; } if (this._flagWeight) { changed['font-weight'] = this._weight; } if (this._flagDecoration) { changed['text-decoration'] = this._decoration; } if (this._fill && this._fill._renderer) { this._fill._update(); svg[this._fill._renderer.type].render.call(this._fill, domElement, true); } if (this._flagFill) { changed.fill = this._fill && this._fill.id ? 'url(#' + this._fill.id + ')' : this._fill; } if (this._stroke && this._stroke._renderer) { this._stroke._update(); svg[this._stroke._renderer.type].render.call(this._stroke, domElement, true); } if (this._flagStroke) { changed.stroke = this._stroke && this._stroke.id ? 'url(#' + this._stroke.id + ')' : this._stroke; } if (this._flagLinewidth) { changed['stroke-width'] = this._linewidth; } if (this._flagOpacity) { changed.opacity = this._opacity; } if (this._flagClassName) { changed['class'] = this._className; } if (this._flagVisible) { changed.visibility = this._visible ? 'visible' : 'hidden'; } if (this.dashes && this.dashes.length > 0) { changed['stroke-dasharray'] = this.dashes.join(' '); } if (!this._renderer.elem) { changed.id = this.id; this._renderer.elem = svg.createElement('text', changed); domElement.defs.appendChild(this._renderer.elem); } else { svg.setAttributes(this._renderer.elem, changed); } if (this._flagClip) { var clip = svg.getClip(this); var 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._flagValue) { this._renderer.elem.textContent = this._value; } return this.flagReset(); } }, 'linear-gradient': { render: function(domElement, silent) { if (!silent) { this._update(); } var changed = {}; if (this._flagEndPoints) { changed.x1 = this.left._x; changed.y1 = this.left._y; changed.x2 = this.right._x; changed.y2 = this.right._y; } if (this._flagSpread) { changed.spreadMethod = this._spread; } // If there is no attached DOM element yet, // create it with all necessary attributes. if (!this._renderer.elem) { changed.id = this.id; changed.gradientUnits = 'userSpaceOnUse'; this._renderer.elem = svg.createElement('linearGradient', changed); domElement.defs.appendChild(this._renderer.elem); // Otherwise apply all pending attributes } else { svg.setAttributes(this._renderer.elem, changed); } if (this._flagStops) { var lengthChanged = this._renderer.elem.childNodes.length !== this.stops.length; if (lengthChanged) { this._renderer.elem.childNodes.length = 0; } for (var i = 0; i < this.stops.length; i++) { var stop = this.stops[i]; var attrs = {}; if (stop._flagOffset) { attrs.offset = 100 * stop._offset + '%'; } if (stop._flagColor) { attrs['stop-color'] = stop._color; } if (stop._flagOpacity) { attrs['stop-opacity'] = stop._opacity; } if (!stop._renderer.elem) { stop._renderer.elem = svg.createElement('stop', attrs); } else { svg.setAttributes(stop._renderer.elem, attrs); } if (lengthChanged) { this._renderer.elem.appendChild(stop._renderer.elem); } stop.flagReset(); } } return this.flagReset(); } }, 'radial-gradient': { render: function(domElement, silent) { if (!silent) { this._update(); } var changed = {}; if (this._flagCenter) { changed.cx = this.center._x; changed.cy = this.center._y; } if (this._flagFocal) { changed.fx = this.focal._x; changed.fy = this.focal._y; } if (this._flagRadius) { changed.r = this._radius; } if (this._flagSpread) { changed.spreadMethod = this._spread; } // If there is no attached DOM element yet, // create it with all necessary attributes. if (!this._renderer.elem) { changed.id = this.id; changed.gradientUnits = 'userSpaceOnUse'; this._renderer.elem = svg.createElement('radialGradient', changed); domElement.defs.appendChild(this._renderer.elem); // Otherwise apply all pending attributes } else { svg.setAttributes(this._renderer.elem, changed); } if (this._flagStops) { var lengthChanged = this._renderer.elem.childNodes.length !== this.stops.length; if (lengthChanged) { this._renderer.elem.childNodes.length = 0; } for (var i = 0; i < this.stops.length; i++) { var stop = this.stops[i]; var attrs = {}; if (stop._flagOffset) { attrs.offset = 100 * stop._offset + '%'; } if (stop._flagColor) { attrs['stop-color'] = stop._color; } if (stop._flagOpacity) { attrs['stop-opacity'] = stop._opacity; } if (!stop._renderer.elem) { stop._renderer.elem = svg.createElement('stop', attrs); } else { svg.setAttributes(stop._renderer.elem, attrs); } if (lengthChanged) { this._renderer.elem.appendChild(stop._renderer.elem); } stop.flagReset(); } } return this.flagReset(); } }, texture: { render: function(domElement, silent) { if (!silent) { this._update(); } var changed = {}; var styles = { x: 0, y: 0 }; var image = this.image; if (this._flagLoaded && this.loaded) { switch (image.nodeName.toLowerCase()) { case 'canvas': styles.href = styles['xlink:href'] = image.toDataURL('image/png'); break; case 'img': case 'image': styles.href = styles['xlink:href'] = this.src; break; } } if (this._flagOffset || this._flagLoaded || this._flagScale) { changed.x = this._offset.x; changed.y = this._offset.y; if (image) { changed.x -= image.width / 2; changed.y -= image.height / 2; if (this._scale instanceof Two.Vector) { changed.x *= this._scale.x; changed.y *= this._scale.y; } else { changed.x *= this._scale; changed.y *= this._scale; } } if (changed.x > 0) { changed.x *= - 1; } if (changed.y > 0) { changed.y *= - 1; } } if (this._flagScale || this._flagLoaded || this._flagRepeat) { changed.width = 0; changed.height = 0; if (image) { styles.width = changed.width = image.width; styles.height = changed.height = image.height; // TODO: Hack / Bandaid switch (this._repeat) { case 'no-repeat': changed.width += 1; changed.height += 1; break; } if (this._scale instanceof Two.Vector) { changed.width *= this._scale.x; changed.height *= this._scale.y; } else { changed.width *= this._scale; changed.height *= this._scale; } } } if (this._flagScale || this._flagLoaded) { if (!this._renderer.image) { this._renderer.image = svg.createElement('image', styles); } else if (!_.isEmpty(styles)) { svg.setAttributes(this._renderer.image, styles); } } if (!this._renderer.elem) { changed.id = this.id; changed.patternUnits = 'userSpaceOnUse'; this._renderer.elem = svg.createElement('pattern', changed); domElement.defs.appendChild(this._renderer.elem); } else if (!_.isEmpty(changed)) { svg.setAttributes(this._renderer.elem, changed); } if (this._renderer.elem && this._renderer.image && !this._renderer.appended) { this._renderer.elem.appendChild(this._renderer.image); this._renderer.appended = true; } 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); this.domElement.defs = this.defs; this.domElement.style.overflow = 'hidden'; }; _.extend(Renderer, { Utils: svg }); _.extend(Renderer.prototype, Two.Utils.Events, { constructor: Renderer, setSize: function(width, height) { this.width = width; this.height = height; svg.setAttributes(this.domElement, { width: width, height: height }); return this.trigger(Two.Events.resize, width, height); }, render: function() { svg.group.render.call(this.scene, this.domElement); return this; } }); })((typeof global !== 'undefined' ? global : (this || window)).Two);