Last active
December 23, 2015 11:38
-
-
Save neckro/6629280 to your computer and use it in GitHub Desktop.
diy SVG library
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SVG helpers | |
define([], function() { "use strict"; | |
/////////////////////// | |
var Svg = (function(svg) { | |
svg.ns = { | |
svg: 'http://www.w3.org/2000/svg', | |
xlink: 'http://www.w3.org/1999/xlink' | |
}; | |
svg.create = function() { | |
var instance = Object.create(svg.proto); | |
svg.init.apply(instance, arguments); | |
return instance; | |
}; | |
svg.init = function(nodetype, attr) { | |
if (nodetype instanceof Node) { | |
this.node = nodetype; | |
} else if (typeof nodetype === 'string') { | |
this.node = document.createElementNS(svg.ns.svg, nodetype); | |
} | |
this.attr(attr); | |
this.transforms = []; | |
this.children = []; | |
return this; | |
}; | |
svg.proto = {}; | |
svg.proto.attr = function(attr, setting) { | |
var self = this; | |
switch (typeof attr) { | |
case 'object': | |
// passed an object: iterate over properties | |
Object.getOwnPropertyNames(attr).forEach(function(e) { | |
self.attrP(e, attr[e]); | |
}); | |
return this; | |
case 'string': | |
return this.attrP(attr, setting); | |
} | |
}; | |
svg.proto.attrP = function(attr, setting) { | |
var ns; | |
if (setting === null || setting === '' || setting === {}) { | |
// clear attribute | |
this.node.removeAttribute(attr); | |
return this; | |
} | |
switch (typeof setting) { | |
case 'object': | |
// join an array | |
if (setting.join) { | |
return this.attrP(e, setting.join(' ')); | |
} | |
// else assume a property object | |
if (!setting.value) break; | |
if (typeof setting.ns === 'string') { | |
ns = svg.ns[setting.ns] || setting.ns; | |
this.node.setAttributeNS(ns, attr, setting.value); | |
} else { | |
this.node.setAttribute(attr, setting.value); | |
} | |
break; | |
case 'number': | |
// if NaN or Infinity, do nothing | |
if (!isFinite(setting)) break; | |
case 'string': | |
this.node.setAttribute(attr, setting); | |
break; | |
case 'number': | |
this.node.setAttribute(attr, setting); | |
break; | |
case 'undefined': | |
// get property | |
return this.node.getAttribute(attr); | |
} | |
return this; | |
}; | |
svg.proto.append = function() { | |
var i, n; | |
for (i = 0; i < arguments.length; i++) { | |
n = arguments[i]; | |
if (!n) continue; | |
if (n instanceof Node) { | |
this.node.appendChild(n); | |
} else if (n.node instanceof Node) { | |
if (n.parent && n.parent !== this && n.parent.detachChild) { | |
n.parent.detachChild(n); | |
} | |
this.node.appendChild(n.node); | |
n.parent = this; | |
if (!(this.children && this.children.push)) { | |
this.children = []; | |
} | |
this.children.push(n); | |
} | |
} | |
return this; | |
}; | |
svg.proto.clear = function() { | |
this.children.forEach(function(e) { | |
e.remove(); | |
}); | |
this.children = []; | |
return this; | |
}; | |
svg.proto.replace = function() { | |
return this.clear().append.apply(this, arguments); | |
}; | |
svg.proto.createChild = function() { | |
var child = svg.create.apply(this, arguments); | |
this.append(child); | |
return child; | |
}; | |
// only removes the record in `this.children` of `child` | |
svg.proto.detachChild = function(child) { | |
if (child && this.children && this.children.length) { | |
var index = this.children.indexOf(child); | |
if (index > -1) { | |
this.children.splice(index, 1); | |
} | |
} | |
}; | |
// removes the record from `this.parent`, then detach self from DOM | |
svg.proto.detach = function() { | |
if (this.parent && this.parent.detachChild) { | |
this.parent.detachChild(this); | |
return this.remove(); | |
} | |
}; | |
svg.proto.addDef = function() { | |
var i, n = this; | |
if (this.parent && this.parent.node instanceof Node) { | |
n = this.parent; | |
} | |
if (!(n.defs && n.defs.node instanceof Node)) { | |
n.defs = svg.create('defs'); | |
n.append(n.defs); | |
} | |
svg.proto.append.apply(n.defs, arguments); | |
return this; | |
}; | |
svg.proto.addMask = function(maskattr, invert) { | |
var maskID = svg.unique_id(); | |
this.maskDef = svg.create('mask', maskattr).attr({ | |
id: maskID | |
}); | |
this.maskDef.append(svg.create('rect', maskattr).attr({ | |
fill: invert ? 'black' : 'white' | |
})); | |
this.mask = svg.create('g', { | |
fill: invert ? 'white' : 'black' | |
}); | |
this.maskDef.append(this.mask); | |
this.addDef(this.maskDef); | |
this.attr({ | |
mask: 'url(#' + maskID + ')' | |
}); | |
return this; | |
}; | |
// Duplicates via an xlink -- better than clone() | |
svg.proto.duplicate = function() { | |
var e, g; | |
if (!this.duplicate_id) { | |
this.duplicate_id = svg.unique_id(); | |
g = svg.create('g', { | |
id: { | |
//ns: 'xml', | |
value: this.duplicate_id | |
} | |
}); | |
this.addDef(g); | |
g.append(this); | |
} | |
e = svg.create('use', { | |
href: { | |
ns: 'xlink', | |
value: '#' + this.duplicate_id | |
} | |
}); | |
return e; | |
}; | |
// returns a group containing a node and its template, | |
// attached where the node was | |
svg.proto.dupegroup = function() { | |
var parent = this.parent; | |
var g = svg.create('g').append(this); | |
var dupe = this.duplicate(); | |
g.append(dupe); | |
parent.append(g); | |
return g; | |
}; | |
// copy an SVG node -- better to use duplicate() | |
svg.proto.clone = function(deep) { | |
if (deep !== false) deep = true; | |
var instance = Object.create(svg.proto); | |
instance.node = this.node.cloneNode(deep); | |
instance.transforms = Array.prototype.slice.call(this.transforms); | |
return instance; | |
}; | |
svg.proto.remove = function() { | |
if (this.node && this.node.parentNode) { | |
return this.node.parentNode.removeChild(this.node); | |
} | |
}; | |
svg.proto.transform = function() { | |
this.transforms.push(Array.prototype.slice.call(arguments)); | |
return this.transformApply(); | |
}; | |
svg.proto.transformReset = function() { | |
this.transforms = []; | |
return this.transform.apply(this, arguments); | |
}; | |
svg.proto.transformPop = function() { | |
var e = this.transforms.pop(Array.prototype.slice.call(arguments)); | |
this.transformApply(); | |
return e; | |
}; | |
svg.proto.transformApply = function() { | |
var i = this.transforms.length, t = []; | |
while (i--) { | |
t.push(svg.transformString(this.transforms[i])); | |
} | |
this.node.setAttribute('transform', t.join(' ')); | |
return this; | |
}; | |
// 0 = bottom, Infinity or other ridiculous number = top | |
svg.proto.setPosition = function(pos) { | |
if (!this.parent || !this.parent.node) return; | |
var children; | |
children = this.parent.node.childNodes; | |
if (pos > (children.length - 1)) { | |
// to be on top, simply re-append to parent | |
this.parent.append(this); | |
return this; | |
} | |
this.parent.node.insertBefore(children[pos]); | |
return this; | |
}; | |
svg.proto.addClass = function(c) { | |
if ( | |
typeof c === 'string' && | |
c.length > 0 && | |
this.node && | |
this.node.classList && | |
this.node.classList.add | |
) { | |
this.node.classList.add(c); | |
} | |
return this; | |
}; | |
svg.proto.removeClass = function(c) { | |
if (this.node && this.node.classList && this.node.classList.remove) { | |
this.node.classList.remove(c); | |
} | |
return this; | |
}; | |
svg.path = {}; | |
svg.path.create = function(attr) { | |
var instance = Object.create(svg.path.proto); | |
svg.init.call(instance, 'path', attr); | |
instance.paths = []; | |
return instance; | |
}; | |
svg.path.proto = Object.create(svg.proto); | |
svg.path.proto.add = function() { | |
var i; | |
// if first argument is an array, assume all arguments are arrays | |
if (Array.isArray(arguments[0])) { | |
for (i = 0; i < arguments.length; i++) { | |
if (Array.isArray(arguments[i])) { | |
this.paths.push(arguments[i]); | |
} | |
} | |
} else { | |
this.paths.push(Array.prototype.slice.call(arguments)); | |
} | |
return this; | |
}; | |
svg.path.proto.closePath = function() { | |
if (this.paths.length === 0) return this; | |
if (this.paths.length > 0 && | |
this.paths[this.paths.length - 1] && | |
this.paths[this.paths.length - 1][0] === 'Z' | |
) { | |
// path was already closed | |
return this; | |
} | |
this.paths.push(['Z']); | |
return this; | |
}; | |
svg.path.proto.applyPath = function() { | |
var i, ip, p, ps = ''; | |
for (i = 0; i < this.paths.length; i++) { | |
ps += this.paths[i][0]; | |
for (ip = 1; ip < this.paths[i].length; ip++) { | |
p = this.paths[i][ip]; | |
if (Array.isArray(p)) { | |
p = this.paths[i][ip].join(','); | |
} | |
ps += p.toString() + ' '; | |
} | |
if (this.paths[i].length == 1) ps += ' '; | |
} | |
this.attr('d', ps); | |
}; | |
svg.path.proto.ellipse = function(cx, cy, rx, ry, flipped) { | |
return this.closePath().add([ | |
'M', [cx - rx, cy] | |
], [ | |
'A', [rx, ry], | |
0, [0, (flipped ? 1 : 0)], | |
[cx + rx, cy] | |
], [ | |
'A', [rx, ry], | |
0, [0, (flipped ? 1 : 0)], | |
[cx - rx, cy] | |
], [ | |
'Z' | |
]); | |
}; | |
svg.transformString = function(t) { | |
if (!Array.isArray(t)) return ''; | |
return t[0].toString() + '(' + t.slice(1).join(' ') + ')'; | |
}; | |
svg.printCallback = function(string, opt, callback) { | |
opt = opt || {}; | |
string = string.toString(); | |
var letter, group; | |
if (!opt.font || !opt.font.glyphs || !opt.font.face) { | |
// no font available | |
return; | |
} | |
svg.prepareFont(opt); | |
group = svg.create('g'); | |
for (var i = 0, ii = string.length; i < ii; i++) { | |
letter = svg.getLetter(opt, string[i], string[i-1], string[i+1]); | |
if (opt.debug) { | |
letter.path = svg.create('rect', { | |
x: 0, | |
y: -opt.size, | |
width: width, | |
height: opt.size, | |
stroke: 'gray', | |
fill: 'none', | |
opacity: 0.5 | |
}); | |
} | |
if (typeof callback === 'function') { | |
callback(letter); | |
} | |
group.append(letter.path); | |
} | |
return group; | |
}; | |
svg.print = function (string, opt) { | |
var lines = string.toString().split('\n'), | |
letter, linegroup, textgroup, | |
hoffset, last_pad_r, i, | |
ox = opt.origin && opt.origin[0] || 0, | |
oy = opt.origin && opt.origin[1] || 0, | |
line_height = opt.line_height || opt.size; | |
svg.prepareFont(opt); | |
if (opt.valign === 'top') { | |
oy += opt.font.face.ascent * opt.scale; | |
} else if (opt.valign === 'middle' || opt.valign === 'center') { | |
oy += (opt.font.face.ascent * opt.scale) - (lines.length * line_height / 2) + (line_height - opt.size) / 2; | |
} else if (opt.valign === 'bottom') { | |
oy += (opt.font.face.descent * opt.scale) - ((lines.length - 1) * line_height); | |
} | |
var justifiedTransform = function(l) { | |
var transform; | |
if (typeof hoffset === 'undefined') { | |
hoffset = -l.pad_l; | |
} | |
transform = ['translate', ox + hoffset, oy]; | |
hoffset += l.width; | |
last_pad_r = l.pad_r; | |
return l.path.transform.apply(l.path, transform); | |
}; | |
textgroup = svg.create('g'); | |
for (i = 0; i < lines.length; i++) { | |
linegroup = svg.printCallback(lines[i], opt, justifiedTransform); | |
if (opt.halign === 'right') { | |
linegroup.transform('translate', last_pad_r - hoffset, 0); | |
} else if (opt.halign === 'middle' || opt.halign === 'center') { | |
linegroup.transform('translate', last_pad_r - hoffset / 2, 0); | |
} | |
textgroup.append(linegroup); | |
hoffset = undefined; | |
oy += line_height; | |
} | |
return textgroup; | |
}; | |
svg.printCircle = function (string, opt) { | |
opt = opt || {}; | |
var a, group, last_pad_r, | |
angle_total, | |
bounds = [], | |
flipped = opt.flipped || false, | |
ox = (opt.origin && opt.origin[0]) || 0, | |
oy = (opt.origin && opt.origin[1]) || 0; | |
svg.prepareFont(opt); | |
opt.origin = [ox, oy]; | |
/* | |
average the radius with the computed radius at em-width, to partially | |
compensate for the fact that we're rendering along an n-gon and not | |
a circle | |
*/ | |
var radius = +opt.radius || 0; | |
radius = (svg.isoHeight( | |
svg.chord(+svg.getLetter(opt, 'M').width || 0, radius), | |
radius | |
) + radius) / 2; | |
var circleTransform = function(l) { | |
a = svg.chord((+l.width || 0), (+radius || 0)) / 2; | |
if (typeof angle_total === 'undefined') { | |
// Remove left padding, to make the position of the initial | |
// letter consistent regardless of letter_spacing | |
angle_total = (opt.angle || 0) + a - svg.chord((+l.pad_l || 0), (+radius || 0)); | |
} else { | |
angle_total += a; | |
} | |
if (flipped) { | |
l.path.transform('scale', 1, -1); | |
} | |
l.path.transform( | |
'translate', | |
ox - (l.width / 2), | |
oy - radius | |
); | |
l.path.transform( | |
'rotate', | |
angle_total + (flipped ? 180 : 0), | |
ox, oy | |
); | |
if (flipped) { | |
l.path.transform('scale', 1, -1); | |
} | |
angle_total += a; | |
last_pad_r = l.pad_r; | |
return l.path; | |
}; | |
group = svg.printCallback(string, opt, circleTransform); | |
// Remove right padding of final letter | |
angle_total -= svg.chord(+last_pad_r, +radius); | |
opt.group_rotation = 0; | |
if (opt.align === 'right') { | |
opt.group_rotation = -angle_total; | |
} else if (opt.align === 'center' || opt.align === 'middle') { | |
opt.group_rotation = -angle_total / 2; | |
} | |
opt.angle_total = angle_total; | |
return group.transform('rotate', (flipped ? -1 : 1) * opt.group_rotation, ox, oy); | |
}; | |
// return a path that represents the bounds of a printCircle() | |
svg.printCircleBounds = function(opt) { | |
var bounds; | |
var rotmax = opt.angle_total; | |
var radmax = opt.radius + opt.size; | |
var outer_end = svg.dp2c(radmax, opt.angle_total - 90); | |
var inner_end = svg.dp2c(opt.radius, opt.angle_total - 90); | |
var path = 'M0,' + -opt.radius + ' v' + -opt.size; | |
path += ' A' + radmax + ',' + radmax + ' 0 0,1 '; | |
path += outer_end[0] + ',' + outer_end[1]; | |
path += ' L' + inner_end[0] + ',' + inner_end[1]; | |
path += ' A' + opt.radius + ',' + opt.radius + ' 0 0,0 0,' + -opt.radius; | |
path += ' Z'; | |
bounds = svg.create('path', { d: path }); | |
bounds.transform('rotate', opt.group_rotation, 0, 0); | |
bounds.transform('translate', opt.origin[0], opt.origin[1]); | |
return bounds; | |
}; | |
svg.getLetter = function(opt, currLetter, prevLetter, nextLetter) { | |
if (!opt || !opt.font || !opt.font.glyphs) return; | |
var | |
prev = opt.font.glyphs && opt.font.glyphs[prevLetter] || {}, | |
curr = opt.font.glyphs[currLetter] || {}, | |
kern_l = prev.k && prev.k[currLetter] || 0, | |
kern_r = curr.k && nextLetter && curr.k[nextLetter] || 0, | |
pad_l = (kern_l + (opt.padding || 0)) * (opt.scale || 1), | |
pad_r = (kern_r + (opt.padding || 0)) * (opt.scale || 1), | |
width = (curr.w || opt.font.w) * (opt.scale || 1) + pad_l + pad_r; | |
return { | |
path: svg.create('path', { | |
// Use dummy path if it's empty, to avoid Chrome parsing error | |
d: curr.d || 'M0 0H0' | |
}) | |
.transform('scale', opt.scale, opt.scale) | |
.transform('translate', pad_l, 0) | |
.attr(opt.attr || {}) | |
.addClass(opt.class), | |
letter: currLetter, | |
width: width, | |
pad_l: pad_l, | |
pad_r: pad_r | |
}; | |
}; | |
svg.prepareFont = function(opt) { | |
if (typeof opt !== 'object') return; | |
opt.size = opt.size || 16; | |
opt.scale = opt.size / opt.font.face["units-per-em"]; | |
opt.letter_spacing = opt.letter_spacing || 0; | |
opt.attr = opt.attr || {}; | |
// For fonts that don't have width specified, use em-width | |
opt.padding = ((opt.font.w || opt.font.glyphs.M.w) * opt.letter_spacing / 2) || 0; | |
return opt; | |
}; | |
svg.isoHeight = function(base, sides) { | |
return +(Math.sqrt( Math.pow(sides, 2) - ( Math.pow(base, 2) / 4 ) )); | |
}; | |
svg.chord = function(chord, radius) { | |
return (180 * ( | |
Math.acos( | |
( | |
(2 * radius * radius) - | |
(chord * chord) | |
) / ( | |
2 * radius * radius | |
) | |
) | |
) / Math.PI) || 0; | |
}; | |
// conversions | |
svg.dtor = function(d) { | |
return d * Math.PI / 180; | |
}; | |
svg.rtod = function(r) { | |
return r * 180 / Math.PI; | |
}; | |
svg.dsin = function(d) { | |
return Math.sin(svg.dtor(d)); | |
}; | |
svg.dcos = function(d) { | |
return Math.cos(svg.dtor(d)); | |
}; | |
// polar to cartesian | |
svg.p2c = function(r, a) { | |
return [Math.cos(a) * r, Math.sin(a) * r]; | |
}; | |
svg.dp2c = function(r, a) { | |
return svg.p2c(r, svg.dtor(a)); | |
}; | |
// cartesian to polar | |
svg.c2p = function(x, y) { | |
return [Math.sqrt((x * x) + (y * y)), Math.atan2(y, x)]; | |
}; | |
svg.dc2p = function(x, y) { | |
var p = svg.c2p(x, y); | |
return [p[0], svg.rtod(p[1])]; | |
}; | |
svg.unique_id = (function() { | |
var unique_id = (Math.floor(Math.random() * 9000) + 1000) * 10000; | |
return function() { | |
return (unique_id++).toString(); | |
}; | |
})(); | |
return svg; })(Svg || {}); | |
/////////////////////// | |
return Svg; }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment