Skip to content

Instantly share code, notes, and snippets.

@neckro
Last active December 23, 2015 11:38
Show Gist options
  • Save neckro/6629280 to your computer and use it in GitHub Desktop.
Save neckro/6629280 to your computer and use it in GitHub Desktop.
diy SVG library
// 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