// Copyright (c) 2013 - 2015 Adobe Systems Incorporated. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. var Snap = (function(root) { Snap.version = "0.4.1"; /*\ * Snap [ method ] ** * Creates a drawing surface or wraps existing SVG element. ** - width (number|string) width of surface - height (number|string) height of surface * or - DOM (SVGElement) element to be wrapped into Snap structure * or - array (array) array of elements (will return set of elements) * or - query (string) CSS query selector = (object) @Element \*/ function Snap(w, h) { if (w) { if (w.nodeType) { return wrap(w); } if (is(w, "array") && Snap.set) { return Snap.set.apply(Snap, w); } if (w instanceof Element) { return w; } if (h == null) { w = glob.doc.querySelector(String(w)); return wrap(w); } } w = w == null ? "100%" : w; h = h == null ? "100%" : h; return new Paper(w, h); } Snap.toString = function () { return "Snap v" + this.version; }; Snap._ = {}; var glob = { win: root.window, doc: root.window.document }; Snap._.glob = glob; var has = "hasOwnProperty", Str = String, toFloat = parseFloat, toInt = parseInt, math = Math, mmax = math.max, mmin = math.min, abs = math.abs, pow = math.pow, PI = math.PI, round = math.round, E = "", S = " ", objectToString = Object.prototype.toString, ISURL = /^url\(['"]?([^\)]+?)['"]?\)$/i, colourRegExp = /^\s*((#[a-f\d]{6})|(#[a-f\d]{3})|rgba?\(\s*([\d\.]+%?\s*,\s*[\d\.]+%?\s*,\s*[\d\.]+%?(?:\s*,\s*[\d\.]+%?)?)\s*\)|hsba?\(\s*([\d\.]+(?:deg|\xb0|%)?\s*,\s*[\d\.]+%?\s*,\s*[\d\.]+(?:%?\s*,\s*[\d\.]+)?%?)\s*\)|hsla?\(\s*([\d\.]+(?:deg|\xb0|%)?\s*,\s*[\d\.]+%?\s*,\s*[\d\.]+(?:%?\s*,\s*[\d\.]+)?%?)\s*\))\s*$/i, bezierrg = /^(?:cubic-)?bezier\(([^,]+),([^,]+),([^,]+),([^\)]+)\)/, reURLValue = /^url\(#?([^)]+)\)$/, separator = Snap._.separator = /[,\s]+/, whitespace = /[\s]/g, commaSpaces = /[\s]*,[\s]*/, hsrg = {hs: 1, rg: 1}, pathCommand = /([a-z])[\s,]*((-?\d*\.?\d*(?:e[\-+]?\d+)?[\s]*,?[\s]*)+)/ig, tCommand = /([rstm])[\s,]*((-?\d*\.?\d*(?:e[\-+]?\d+)?[\s]*,?[\s]*)+)/ig, pathValues = /(-?\d*\.?\d*(?:e[\-+]?\\d+)?)[\s]*,?[\s]*/ig, idgen = 0, idprefix = "S" + (+new Date).toString(36), ID = function (el) { return (el && el.type ? el.type : E) + idprefix + (idgen++).toString(36); }, xlink = "http://www.w3.org/1999/xlink", xmlns = "http://www.w3.org/2000/svg", hub = {}, URL = Snap.url = function (url) { return "url('#" + url + "')"; }; function $(el, attr) { if (attr) { if (el == "#text") { el = glob.doc.createTextNode(attr.text || attr["#text"] || ""); } if (el == "#comment") { el = glob.doc.createComment(attr.text || attr["#text"] || ""); } if (typeof el == "string") { el = $(el); } if (typeof attr == "string") { if (el.nodeType == 1) { if (attr.substring(0, 6) == "xlink:") { return el.getAttributeNS(xlink, attr.substring(6)); } if (attr.substring(0, 4) == "xml:") { return el.getAttributeNS(xmlns, attr.substring(4)); } return el.getAttribute(attr); } else if (attr == "text") { return el.nodeValue; } else { return null; } } if (el.nodeType == 1) { for (var key in attr) if (attr[has](key)) { var val = Str(attr[key]); if (val) { if (key.substring(0, 6) == "xlink:") { el.setAttributeNS(xlink, key.substring(6), val); } else if (key.substring(0, 4) == "xml:") { el.setAttributeNS(xmlns, key.substring(4), val); } else { el.setAttribute(key, val); } } else { el.removeAttribute(key); } } } else if ("text" in attr) { el.nodeValue = attr.text; } } else { el = glob.doc.createElementNS(xmlns, el); } return el; } Snap._.$ = $; Snap._.id = ID; function getAttrs(el) { var attrs = el.attributes, name, out = {}; for (var i = 0; i < attrs.length; i++) { if (attrs[i].namespaceURI == xlink) { name = "xlink:"; } else { name = ""; } name += attrs[i].name; out[name] = attrs[i].textContent; } return out; } function is(o, type) { type = Str.prototype.toLowerCase.call(type); if (type == "finite") { return isFinite(o); } if (type == "array" && (o instanceof Array || Array.isArray && Array.isArray(o))) { return true; } return (type == "null" && o === null) || (type == typeof o && o !== null) || (type == "object" && o === Object(o)) || objectToString.call(o).slice(8, -1).toLowerCase() == type; } /*\ * Snap.format [ method ] ** * Replaces construction of type `{}` to the corresponding argument ** - token (string) string to format - json (object) object which properties are used as a replacement = (string) formatted string > Usage | // this draws a rectangular shape equivalent to "M10,20h40v50h-40z" | paper.path(Snap.format("M{x},{y}h{dim.width}v{dim.height}h{dim['negative width']}z", { | x: 10, | y: 20, | dim: { | width: 40, | height: 50, | "negative width": -40 | } | })); \*/ Snap.format = (function () { var tokenRegex = /\{([^\}]+)\}/g, objNotationRegex = /(?:(?:^|\.)(.+?)(?=\[|\.|$|\()|\[('|")(.+?)\2\])(\(\))?/g, // matches .xxxxx or ["xxxxx"] to run over object properties replacer = function (all, key, obj) { var res = obj; key.replace(objNotationRegex, function (all, name, quote, quotedName, isFunc) { name = name || quotedName; if (res) { if (name in res) { res = res[name]; } typeof res == "function" && isFunc && (res = res()); } }); res = (res == null || res == obj ? all : res) + ""; return res; }; return function (str, obj) { return Str(str).replace(tokenRegex, function (all, key) { return replacer(all, key, obj); }); }; })(); function clone(obj) { if (typeof obj == "function" || Object(obj) !== obj) { return obj; } var res = new obj.constructor; for (var key in obj) if (obj[has](key)) { res[key] = clone(obj[key]); } return res; } Snap._.clone = clone; function repush(array, item) { for (var i = 0, ii = array.length; i < ii; i++) if (array[i] === item) { return array.push(array.splice(i, 1)[0]); } } function cacher(f, scope, postprocessor) { function newf() { var arg = Array.prototype.slice.call(arguments, 0), args = arg.join("\u2400"), cache = newf.cache = newf.cache || {}, count = newf.count = newf.count || []; if (cache[has](args)) { repush(count, args); return postprocessor ? postprocessor(cache[args]) : cache[args]; } count.length >= 1e3 && delete cache[count.shift()]; count.push(args); cache[args] = f.apply(scope, arg); return postprocessor ? postprocessor(cache[args]) : cache[args]; } return newf; } Snap._.cacher = cacher; function angle(x1, y1, x2, y2, x3, y3) { if (x3 == null) { var x = x1 - x2, y = y1 - y2; if (!x && !y) { return 0; } return (180 + math.atan2(-y, -x) * 180 / PI + 360) % 360; } else { return angle(x1, y1, x3, y3) - angle(x2, y2, x3, y3); } } function rad(deg) { return deg % 360 * PI / 180; } function deg(rad) { return rad * 180 / PI % 360; } function x_y() { return this.x + S + this.y; } function x_y_w_h() { return this.x + S + this.y + S + this.width + " \xd7 " + this.height; } /*\ * Snap.rad [ method ] ** * Transform angle to radians - deg (number) angle in degrees = (number) angle in radians \*/ Snap.rad = rad; /*\ * Snap.deg [ method ] ** * Transform angle to degrees - rad (number) angle in radians = (number) angle in degrees \*/ Snap.deg = deg; /*\ * Snap.sin [ method ] ** * Equivalent to `Math.sin()` only works with degrees, not radians. - angle (number) angle in degrees = (number) sin \*/ Snap.sin = function (angle) { return math.sin(Snap.rad(angle)); }; /*\ * Snap.tan [ method ] ** * Equivalent to `Math.tan()` only works with degrees, not radians. - angle (number) angle in degrees = (number) tan \*/ Snap.tan = function (angle) { return math.tan(Snap.rad(angle)); }; /*\ * Snap.cos [ method ] ** * Equivalent to `Math.cos()` only works with degrees, not radians. - angle (number) angle in degrees = (number) cos \*/ Snap.cos = function (angle) { return math.cos(Snap.rad(angle)); }; /*\ * Snap.asin [ method ] ** * Equivalent to `Math.asin()` only works with degrees, not radians. - num (number) value = (number) asin in degrees \*/ Snap.asin = function (num) { return Snap.deg(math.asin(num)); }; /*\ * Snap.acos [ method ] ** * Equivalent to `Math.acos()` only works with degrees, not radians. - num (number) value = (number) acos in degrees \*/ Snap.acos = function (num) { return Snap.deg(math.acos(num)); }; /*\ * Snap.atan [ method ] ** * Equivalent to `Math.atan()` only works with degrees, not radians. - num (number) value = (number) atan in degrees \*/ Snap.atan = function (num) { return Snap.deg(math.atan(num)); }; /*\ * Snap.atan2 [ method ] ** * Equivalent to `Math.atan2()` only works with degrees, not radians. - num (number) value = (number) atan2 in degrees \*/ Snap.atan2 = function (num) { return Snap.deg(math.atan2(num)); }; /*\ * Snap.angle [ method ] ** * Returns an angle between two or three points > Parameters - x1 (number) x coord of first point - y1 (number) y coord of first point - x2 (number) x coord of second point - y2 (number) y coord of second point - x3 (number) #optional x coord of third point - y3 (number) #optional y coord of third point = (number) angle in degrees \*/ Snap.angle = angle; /*\ * Snap.len [ method ] ** * Returns distance between two points > Parameters - x1 (number) x coord of first point - y1 (number) y coord of first point - x2 (number) x coord of second point - y2 (number) y coord of second point = (number) distance \*/ Snap.len = function (x1, y1, x2, y2) { return Math.sqrt(Snap.len2(x1, y1, x2, y2)); }; /*\ * Snap.len2 [ method ] ** * Returns squared distance between two points > Parameters - x1 (number) x coord of first point - y1 (number) y coord of first point - x2 (number) x coord of second point - y2 (number) y coord of second point = (number) distance \*/ Snap.len2 = function (x1, y1, x2, y2) { return (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2); }; /*\ * Snap.closestPoint [ method ] ** * Returns closest point to a given one on a given path. > Parameters - path (Element) path element - x (number) x coord of a point - y (number) y coord of a point = (object) in format { x (number) x coord of the point on the path y (number) y coord of the point on the path length (number) length of the path to the point distance (number) distance from the given point to the path } \*/ // Copied from http://bl.ocks.org/mbostock/8027637 Snap.closestPoint = function (path, x, y) { function distance2(p) { var dx = p.x - x, dy = p.y - y; return dx * dx + dy * dy; } var pathNode = path.node, pathLength = pathNode.getTotalLength(), precision = pathLength / pathNode.pathSegList.numberOfItems * .125, best, bestLength, bestDistance = Infinity; // linear scan for coarse approximation for (var scan, scanLength = 0, scanDistance; scanLength <= pathLength; scanLength += precision) { if ((scanDistance = distance2(scan = pathNode.getPointAtLength(scanLength))) < bestDistance) { best = scan, bestLength = scanLength, bestDistance = scanDistance; } } // binary search for precise estimate precision *= .5; while (precision > .5) { var before, after, beforeLength, afterLength, beforeDistance, afterDistance; if ((beforeLength = bestLength - precision) >= 0 && (beforeDistance = distance2(before = pathNode.getPointAtLength(beforeLength))) < bestDistance) { best = before, bestLength = beforeLength, bestDistance = beforeDistance; } else if ((afterLength = bestLength + precision) <= pathLength && (afterDistance = distance2(after = pathNode.getPointAtLength(afterLength))) < bestDistance) { best = after, bestLength = afterLength, bestDistance = afterDistance; } else { precision *= .5; } } best = { x: best.x, y: best.y, length: bestLength, distance: Math.sqrt(bestDistance) }; return best; } /*\ * Snap.is [ method ] ** * Handy replacement for the `typeof` operator - o (…) any object or primitive - type (string) name of the type, e.g., `string`, `function`, `number`, etc. = (boolean) `true` if given value is of given type \*/ Snap.is = is; /*\ * Snap.snapTo [ method ] ** * Snaps given value to given grid - values (array|number) given array of values or step of the grid - value (number) value to adjust - tolerance (number) #optional maximum distance to the target value that would trigger the snap. Default is `10`. = (number) adjusted value \*/ Snap.snapTo = function (values, value, tolerance) { tolerance = is(tolerance, "finite") ? tolerance : 10; if (is(values, "array")) { var i = values.length; while (i--) if (abs(values[i] - value) <= tolerance) { return values[i]; } } else { values = +values; var rem = value % values; if (rem < tolerance) { return value - rem; } if (rem > values - tolerance) { return value - rem + values; } } return value; }; // Colour /*\ * Snap.getRGB [ method ] ** * Parses color string as RGB object - color (string) color string in one of the following formats: # * Note that `%` can be used any time: `rgb(20%, 255, 50%)`. = (object) RGB object in the following format: o { o r (number) red, o g (number) green, o b (number) blue, o hex (string) color in HTML/CSS format: #••••••, o error (boolean) true if string can't be parsed o } \*/ Snap.getRGB = cacher(function (colour) { if (!colour || !!((colour = Str(colour)).indexOf("-") + 1)) { return {r: -1, g: -1, b: -1, hex: "none", error: 1, toString: rgbtoString}; } if (colour == "none") { return {r: -1, g: -1, b: -1, hex: "none", toString: rgbtoString}; } !(hsrg[has](colour.toLowerCase().substring(0, 2)) || colour.charAt() == "#") && (colour = toHex(colour)); if (!colour) { return {r: -1, g: -1, b: -1, hex: "none", error: 1, toString: rgbtoString}; } var res, red, green, blue, opacity, t, values, rgb = colour.match(colourRegExp); if (rgb) { if (rgb[2]) { blue = toInt(rgb[2].substring(5), 16); green = toInt(rgb[2].substring(3, 5), 16); red = toInt(rgb[2].substring(1, 3), 16); } if (rgb[3]) { blue = toInt((t = rgb[3].charAt(3)) + t, 16); green = toInt((t = rgb[3].charAt(2)) + t, 16); red = toInt((t = rgb[3].charAt(1)) + t, 16); } if (rgb[4]) { values = rgb[4].split(commaSpaces); red = toFloat(values[0]); values[0].slice(-1) == "%" && (red *= 2.55); green = toFloat(values[1]); values[1].slice(-1) == "%" && (green *= 2.55); blue = toFloat(values[2]); values[2].slice(-1) == "%" && (blue *= 2.55); rgb[1].toLowerCase().slice(0, 4) == "rgba" && (opacity = toFloat(values[3])); values[3] && values[3].slice(-1) == "%" && (opacity /= 100); } if (rgb[5]) { values = rgb[5].split(commaSpaces); red = toFloat(values[0]); values[0].slice(-1) == "%" && (red /= 100); green = toFloat(values[1]); values[1].slice(-1) == "%" && (green /= 100); blue = toFloat(values[2]); values[2].slice(-1) == "%" && (blue /= 100); (values[0].slice(-3) == "deg" || values[0].slice(-1) == "\xb0") && (red /= 360); rgb[1].toLowerCase().slice(0, 4) == "hsba" && (opacity = toFloat(values[3])); values[3] && values[3].slice(-1) == "%" && (opacity /= 100); return Snap.hsb2rgb(red, green, blue, opacity); } if (rgb[6]) { values = rgb[6].split(commaSpaces); red = toFloat(values[0]); values[0].slice(-1) == "%" && (red /= 100); green = toFloat(values[1]); values[1].slice(-1) == "%" && (green /= 100); blue = toFloat(values[2]); values[2].slice(-1) == "%" && (blue /= 100); (values[0].slice(-3) == "deg" || values[0].slice(-1) == "\xb0") && (red /= 360); rgb[1].toLowerCase().slice(0, 4) == "hsla" && (opacity = toFloat(values[3])); values[3] && values[3].slice(-1) == "%" && (opacity /= 100); return Snap.hsl2rgb(red, green, blue, opacity); } red = mmin(math.round(red), 255); green = mmin(math.round(green), 255); blue = mmin(math.round(blue), 255); opacity = mmin(mmax(opacity, 0), 1); rgb = {r: red, g: green, b: blue, toString: rgbtoString}; rgb.hex = "#" + (16777216 | blue | (green << 8) | (red << 16)).toString(16).slice(1); rgb.opacity = is(opacity, "finite") ? opacity : 1; return rgb; } return {r: -1, g: -1, b: -1, hex: "none", error: 1, toString: rgbtoString}; }, Snap); /*\ * Snap.hsb [ method ] ** * Converts HSB values to a hex representation of the color - h (number) hue - s (number) saturation - b (number) value or brightness = (string) hex representation of the color \*/ Snap.hsb = cacher(function (h, s, b) { return Snap.hsb2rgb(h, s, b).hex; }); /*\ * Snap.hsl [ method ] ** * Converts HSL values to a hex representation of the color - h (number) hue - s (number) saturation - l (number) luminosity = (string) hex representation of the color \*/ Snap.hsl = cacher(function (h, s, l) { return Snap.hsl2rgb(h, s, l).hex; }); /*\ * Snap.rgb [ method ] ** * Converts RGB values to a hex representation of the color - r (number) red - g (number) green - b (number) blue = (string) hex representation of the color \*/ Snap.rgb = cacher(function (r, g, b, o) { if (is(o, "finite")) { var round = math.round; return "rgba(" + [round(r), round(g), round(b), +o.toFixed(2)] + ")"; } return "#" + (16777216 | b | (g << 8) | (r << 16)).toString(16).slice(1); }); var toHex = function (color) { var i = glob.doc.getElementsByTagName("head")[0] || glob.doc.getElementsByTagName("svg")[0], red = "rgb(255, 0, 0)"; toHex = cacher(function (color) { if (color.toLowerCase() == "red") { return red; } i.style.color = red; i.style.color = color; var out = glob.doc.defaultView.getComputedStyle(i, E).getPropertyValue("color"); return out == red ? null : out; }); return toHex(color); }, hsbtoString = function () { return "hsb(" + [this.h, this.s, this.b] + ")"; }, hsltoString = function () { return "hsl(" + [this.h, this.s, this.l] + ")"; }, rgbtoString = function () { return this.opacity == 1 || this.opacity == null ? this.hex : "rgba(" + [this.r, this.g, this.b, this.opacity] + ")"; }, prepareRGB = function (r, g, b) { if (g == null && is(r, "object") && "r" in r && "g" in r && "b" in r) { b = r.b; g = r.g; r = r.r; } if (g == null && is(r, string)) { var clr = Snap.getRGB(r); r = clr.r; g = clr.g; b = clr.b; } if (r > 1 || g > 1 || b > 1) { r /= 255; g /= 255; b /= 255; } return [r, g, b]; }, packageRGB = function (r, g, b, o) { r = math.round(r * 255); g = math.round(g * 255); b = math.round(b * 255); var rgb = { r: r, g: g, b: b, opacity: is(o, "finite") ? o : 1, hex: Snap.rgb(r, g, b), toString: rgbtoString }; is(o, "finite") && (rgb.opacity = o); return rgb; }; /*\ * Snap.color [ method ] ** * Parses the color string and returns an object featuring the color's component values - clr (string) color string in one of the supported formats (see @Snap.getRGB) = (object) Combined RGB/HSB object in the following format: o { o r (number) red, o g (number) green, o b (number) blue, o hex (string) color in HTML/CSS format: #••••••, o error (boolean) `true` if string can't be parsed, o h (number) hue, o s (number) saturation, o v (number) value (brightness), o l (number) lightness o } \*/ Snap.color = function (clr) { var rgb; if (is(clr, "object") && "h" in clr && "s" in clr && "b" in clr) { rgb = Snap.hsb2rgb(clr); clr.r = rgb.r; clr.g = rgb.g; clr.b = rgb.b; clr.opacity = 1; clr.hex = rgb.hex; } else if (is(clr, "object") && "h" in clr && "s" in clr && "l" in clr) { rgb = Snap.hsl2rgb(clr); clr.r = rgb.r; clr.g = rgb.g; clr.b = rgb.b; clr.opacity = 1; clr.hex = rgb.hex; } else { if (is(clr, "string")) { clr = Snap.getRGB(clr); } if (is(clr, "object") && "r" in clr && "g" in clr && "b" in clr && !("error" in clr)) { rgb = Snap.rgb2hsl(clr); clr.h = rgb.h; clr.s = rgb.s; clr.l = rgb.l; rgb = Snap.rgb2hsb(clr); clr.v = rgb.b; } else { clr = {hex: "none"}; clr.r = clr.g = clr.b = clr.h = clr.s = clr.v = clr.l = -1; clr.error = 1; } } clr.toString = rgbtoString; return clr; }; /*\ * Snap.hsb2rgb [ method ] ** * Converts HSB values to an RGB object - h (number) hue - s (number) saturation - v (number) value or brightness = (object) RGB object in the following format: o { o r (number) red, o g (number) green, o b (number) blue, o hex (string) color in HTML/CSS format: #•••••• o } \*/ Snap.hsb2rgb = function (h, s, v, o) { if (is(h, "object") && "h" in h && "s" in h && "b" in h) { v = h.b; s = h.s; o = h.o; h = h.h; } h *= 360; var R, G, B, X, C; h = (h % 360) / 60; C = v * s; X = C * (1 - abs(h % 2 - 1)); R = G = B = v - C; h = ~~h; R += [C, X, 0, 0, X, C][h]; G += [X, C, C, X, 0, 0][h]; B += [0, 0, X, C, C, X][h]; return packageRGB(R, G, B, o); }; /*\ * Snap.hsl2rgb [ method ] ** * Converts HSL values to an RGB object - h (number) hue - s (number) saturation - l (number) luminosity = (object) RGB object in the following format: o { o r (number) red, o g (number) green, o b (number) blue, o hex (string) color in HTML/CSS format: #•••••• o } \*/ Snap.hsl2rgb = function (h, s, l, o) { if (is(h, "object") && "h" in h && "s" in h && "l" in h) { l = h.l; s = h.s; h = h.h; } if (h > 1 || s > 1 || l > 1) { h /= 360; s /= 100; l /= 100; } h *= 360; var R, G, B, X, C; h = (h % 360) / 60; C = 2 * s * (l < .5 ? l : 1 - l); X = C * (1 - abs(h % 2 - 1)); R = G = B = l - C / 2; h = ~~h; R += [C, X, 0, 0, X, C][h]; G += [X, C, C, X, 0, 0][h]; B += [0, 0, X, C, C, X][h]; return packageRGB(R, G, B, o); }; /*\ * Snap.rgb2hsb [ method ] ** * Converts RGB values to an HSB object - r (number) red - g (number) green - b (number) blue = (object) HSB object in the following format: o { o h (number) hue, o s (number) saturation, o b (number) brightness o } \*/ Snap.rgb2hsb = function (r, g, b) { b = prepareRGB(r, g, b); r = b[0]; g = b[1]; b = b[2]; var H, S, V, C; V = mmax(r, g, b); C = V - mmin(r, g, b); H = (C == 0 ? null : V == r ? (g - b) / C : V == g ? (b - r) / C + 2 : (r - g) / C + 4 ); H = ((H + 360) % 6) * 60 / 360; S = C == 0 ? 0 : C / V; return {h: H, s: S, b: V, toString: hsbtoString}; }; /*\ * Snap.rgb2hsl [ method ] ** * Converts RGB values to an HSL object - r (number) red - g (number) green - b (number) blue = (object) HSL object in the following format: o { o h (number) hue, o s (number) saturation, o l (number) luminosity o } \*/ Snap.rgb2hsl = function (r, g, b) { b = prepareRGB(r, g, b); r = b[0]; g = b[1]; b = b[2]; var H, S, L, M, m, C; M = mmax(r, g, b); m = mmin(r, g, b); C = M - m; H = (C == 0 ? null : M == r ? (g - b) / C : M == g ? (b - r) / C + 2 : (r - g) / C + 4); H = ((H + 360) % 6) * 60 / 360; L = (M + m) / 2; S = (C == 0 ? 0 : L < .5 ? C / (2 * L) : C / (2 - 2 * L)); return {h: H, s: S, l: L, toString: hsltoString}; }; // Transformations /*\ * Snap.parsePathString [ method ] ** * Utility method ** * Parses given path string into an array of arrays of path segments - pathString (string|array) path string or array of segments (in the last case it is returned straight away) = (array) array of segments \*/ Snap.parsePathString = function (pathString) { if (!pathString) { return null; } var pth = Snap.path(pathString); if (pth.arr) { return Snap.path.clone(pth.arr); } var paramCounts = {a: 7, c: 6, o: 2, h: 1, l: 2, m: 2, r: 4, q: 4, s: 4, t: 2, v: 1, u: 3, z: 0}, data = []; if (is(pathString, "array") && is(pathString[0], "array")) { // rough assumption data = Snap.path.clone(pathString); } if (!data.length) { Str(pathString).replace(pathCommand, function (a, b, c) { var params = [], name = b.toLowerCase(); c.replace(pathValues, function (a, b) { b && params.push(+b); }); if (name == "m" && params.length > 2) { data.push([b].concat(params.splice(0, 2))); name = "l"; b = b == "m" ? "l" : "L"; } if (name == "o" && params.length == 1) { data.push([b, params[0]]); } if (name == "r") { data.push([b].concat(params)); } else while (params.length >= paramCounts[name]) { data.push([b].concat(params.splice(0, paramCounts[name]))); if (!paramCounts[name]) { break; } } }); } data.toString = Snap.path.toString; pth.arr = Snap.path.clone(data); return data; }; /*\ * Snap.parseTransformString [ method ] ** * Utility method ** * Parses given transform string into an array of transformations - TString (string|array) transform string or array of transformations (in the last case it is returned straight away) = (array) array of transformations \*/ var parseTransformString = Snap.parseTransformString = function (TString) { if (!TString) { return null; } var paramCounts = {r: 3, s: 4, t: 2, m: 6}, data = []; if (is(TString, "array") && is(TString[0], "array")) { // rough assumption data = Snap.path.clone(TString); } if (!data.length) { Str(TString).replace(tCommand, function (a, b, c) { var params = [], name = b.toLowerCase(); c.replace(pathValues, function (a, b) { b && params.push(+b); }); data.push([b].concat(params)); }); } data.toString = Snap.path.toString; return data; }; function svgTransform2string(tstr) { var res = []; tstr = tstr.replace(/(?:^|\s)(\w+)\(([^)]+)\)/g, function (all, name, params) { params = params.split(/\s*,\s*|\s+/); if (name == "rotate" && params.length == 1) { params.push(0, 0); } if (name == "scale") { if (params.length > 2) { params = params.slice(0, 2); } else if (params.length == 2) { params.push(0, 0); } if (params.length == 1) { params.push(params[0], 0, 0); } } if (name == "skewX") { res.push(["m", 1, 0, math.tan(rad(params[0])), 1, 0, 0]); } else if (name == "skewY") { res.push(["m", 1, math.tan(rad(params[0])), 0, 1, 0, 0]); } else { res.push([name.charAt(0)].concat(params)); } return all; }); return res; } Snap._.svgTransform2string = svgTransform2string; Snap._.rgTransform = /^[a-z][\s]*-?\.?\d/i; function transform2matrix(tstr, bbox) { var tdata = parseTransformString(tstr), m = new Snap.Matrix; if (tdata) { for (var i = 0, ii = tdata.length; i < ii; i++) { var t = tdata[i], tlen = t.length, command = Str(t[0]).toLowerCase(), absolute = t[0] != command, inver = absolute ? m.invert() : 0, x1, y1, x2, y2, bb; if (command == "t" && tlen == 2){ m.translate(t[1], 0); } else if (command == "t" && tlen == 3) { if (absolute) { x1 = inver.x(0, 0); y1 = inver.y(0, 0); x2 = inver.x(t[1], t[2]); y2 = inver.y(t[1], t[2]); m.translate(x2 - x1, y2 - y1); } else { m.translate(t[1], t[2]); } } else if (command == "r") { if (tlen == 2) { bb = bb || bbox; m.rotate(t[1], bb.x + bb.width / 2, bb.y + bb.height / 2); } else if (tlen == 4) { if (absolute) { x2 = inver.x(t[2], t[3]); y2 = inver.y(t[2], t[3]); m.rotate(t[1], x2, y2); } else { m.rotate(t[1], t[2], t[3]); } } } else if (command == "s") { if (tlen == 2 || tlen == 3) { bb = bb || bbox; m.scale(t[1], t[tlen - 1], bb.x + bb.width / 2, bb.y + bb.height / 2); } else if (tlen == 4) { if (absolute) { x2 = inver.x(t[2], t[3]); y2 = inver.y(t[2], t[3]); m.scale(t[1], t[1], x2, y2); } else { m.scale(t[1], t[1], t[2], t[3]); } } else if (tlen == 5) { if (absolute) { x2 = inver.x(t[3], t[4]); y2 = inver.y(t[3], t[4]); m.scale(t[1], t[2], x2, y2); } else { m.scale(t[1], t[2], t[3], t[4]); } } } else if (command == "m" && tlen == 7) { m.add(t[1], t[2], t[3], t[4], t[5], t[6]); } } } return m; } Snap._.transform2matrix = transform2matrix; Snap._unit2px = unit2px; var contains = glob.doc.contains || glob.doc.compareDocumentPosition ? function (a, b) { var adown = a.nodeType == 9 ? a.documentElement : a, bup = b && b.parentNode; return a == bup || !!(bup && bup.nodeType == 1 && ( adown.contains ? adown.contains(bup) : a.compareDocumentPosition && a.compareDocumentPosition(bup) & 16 )); } : function (a, b) { if (b) { while (b) { b = b.parentNode; if (b == a) { return true; } } } return false; }; function getSomeDefs(el) { var p = (el.node.ownerSVGElement && wrap(el.node.ownerSVGElement)) || (el.node.parentNode && wrap(el.node.parentNode)) || Snap.select("svg") || Snap(0, 0), pdefs = p.select("defs"), defs = pdefs == null ? false : pdefs.node; if (!defs) { defs = make("defs", p.node).node; } return defs; } function getSomeSVG(el) { return el.node.ownerSVGElement && wrap(el.node.ownerSVGElement) || Snap.select("svg"); } Snap._.getSomeDefs = getSomeDefs; Snap._.getSomeSVG = getSomeSVG; function unit2px(el, name, value) { var svg = getSomeSVG(el).node, out = {}, mgr = svg.querySelector(".svg---mgr"); if (!mgr) { mgr = $("rect"); $(mgr, {x: -9e9, y: -9e9, width: 10, height: 10, "class": "svg---mgr", fill: "none"}); svg.appendChild(mgr); } function getW(val) { if (val == null) { return E; } if (val == +val) { return val; } $(mgr, {width: val}); try { return mgr.getBBox().width; } catch (e) { return 0; } } function getH(val) { if (val == null) { return E; } if (val == +val) { return val; } $(mgr, {height: val}); try { return mgr.getBBox().height; } catch (e) { return 0; } } function set(nam, f) { if (name == null) { out[nam] = f(el.attr(nam) || 0); } else if (nam == name) { out = f(value == null ? el.attr(nam) || 0 : value); } } switch (el.type) { case "rect": set("rx", getW); set("ry", getH); case "image": set("width", getW); set("height", getH); case "text": set("x", getW); set("y", getH); break; case "circle": set("cx", getW); set("cy", getH); set("r", getW); break; case "ellipse": set("cx", getW); set("cy", getH); set("rx", getW); set("ry", getH); break; case "line": set("x1", getW); set("x2", getW); set("y1", getH); set("y2", getH); break; case "marker": set("refX", getW); set("markerWidth", getW); set("refY", getH); set("markerHeight", getH); break; case "radialGradient": set("fx", getW); set("fy", getH); break; case "tspan": set("dx", getW); set("dy", getH); break; default: set(name, getW); } svg.removeChild(mgr); return out; } /*\ * Snap.select [ method ] ** * Wraps a DOM element specified by CSS selector as @Element - query (string) CSS selector of the element = (Element) the current element \*/ Snap.select = function (query) { query = Str(query).replace(/([^\\]):/g, "$1\\:"); return wrap(glob.doc.querySelector(query)); }; /*\ * Snap.selectAll [ method ] ** * Wraps DOM elements specified by CSS selector as set or array of @Element - query (string) CSS selector of the element = (Element) the current element \*/ Snap.selectAll = function (query) { var nodelist = glob.doc.querySelectorAll(query), set = (Snap.set || Array)(); for (var i = 0; i < nodelist.length; i++) { set.push(wrap(nodelist[i])); } return set; }; function add2group(list) { if (!is(list, "array")) { list = Array.prototype.slice.call(arguments, 0); } var i = 0, j = 0, node = this.node; while (this[i]) delete this[i++]; for (i = 0; i < list.length; i++) { if (list[i].type == "set") { list[i].forEach(function (el) { node.appendChild(el.node); }); } else { node.appendChild(list[i].node); } } var children = node.childNodes; for (i = 0; i < children.length; i++) { this[j++] = wrap(children[i]); } return this; } // Hub garbage collector every 10s setInterval(function () { for (var key in hub) if (hub[has](key)) { var el = hub[key], node = el.node; if (el.type != "svg" && !node.ownerSVGElement || el.type == "svg" && (!node.parentNode || "ownerSVGElement" in node.parentNode && !node.ownerSVGElement)) { delete hub[key]; } } }, 1e4); function Element(el) { if (el.snap in hub) { return hub[el.snap]; } var svg; try { svg = el.ownerSVGElement; } catch(e) {} /*\ * Element.node [ property (object) ] ** * Gives you a reference to the DOM object, so you can assign event handlers or just mess around. > Usage | // draw a circle at coordinate 10,10 with radius of 10 | var c = paper.circle(10, 10, 10); | c.node.onclick = function () { | c.attr("fill", "red"); | }; \*/ this.node = el; if (svg) { this.paper = new Paper(svg); } /*\ * Element.type [ property (string) ] ** * SVG tag name of the given element. \*/ this.type = el.tagName || el.nodeName; var id = this.id = ID(this); this.anims = {}; this._ = { transform: [] }; el.snap = id; hub[id] = this; if (this.type == "g") { this.add = add2group; } if (this.type in {g: 1, mask: 1, pattern: 1, symbol: 1}) { for (var method in Paper.prototype) if (Paper.prototype[has](method)) { this[method] = Paper.prototype[method]; } } } /*\ * Element.attr [ method ] ** * Gets or sets given attributes of the element. ** - params (object) contains key-value pairs of attributes you want to set * or - param (string) name of the attribute = (Element) the current element * or = (string) value of attribute > Usage | el.attr({ | fill: "#fc0", | stroke: "#000", | strokeWidth: 2, // CamelCase... | "fill-opacity": 0.5, // or dash-separated names | width: "*=2" // prefixed values | }); | console.log(el.attr("fill")); // #fc0 * Prefixed values in format `"+=10"` supported. All four operations * (`+`, `-`, `*` and `/`) could be used. Optionally you can use units for `+` * and `-`: `"+=2em"`. \*/ Element.prototype.attr = function (params, value) { var el = this, node = el.node; if (!params) { if (node.nodeType != 1) { return { text: node.nodeValue }; } var attr = node.attributes, out = {}; for (var i = 0, ii = attr.length; i < ii; i++) { out[attr[i].nodeName] = attr[i].nodeValue; } return out; } if (is(params, "string")) { if (arguments.length > 1) { var json = {}; json[params] = value; params = json; } else { return eve("snap.util.getattr." + params, el).firstDefined(); } } for (var att in params) { if (params[has](att)) { eve("snap.util.attr." + att, el, params[att]); } } return el; }; /*\ * Snap.parse [ method ] ** * Parses SVG fragment and converts it into a @Fragment ** - svg (string) SVG string = (Fragment) the @Fragment \*/ Snap.parse = function (svg) { var f = glob.doc.createDocumentFragment(), full = true, div = glob.doc.createElement("div"); svg = Str(svg); if (!svg.match(/^\s*<\s*svg(?:\s|>)/)) { svg = "" + svg + ""; full = false; } div.innerHTML = svg; svg = div.getElementsByTagName("svg")[0]; if (svg) { if (full) { f = svg; } else { while (svg.firstChild) { f.appendChild(svg.firstChild); } } } return new Fragment(f); }; function Fragment(frag) { this.node = frag; } /*\ * Snap.fragment [ method ] ** * Creates a DOM fragment from a given list of elements or strings ** - varargs (…) SVG string = (Fragment) the @Fragment \*/ Snap.fragment = function () { var args = Array.prototype.slice.call(arguments, 0), f = glob.doc.createDocumentFragment(); for (var i = 0, ii = args.length; i < ii; i++) { var item = args[i]; if (item.node && item.node.nodeType) { f.appendChild(item.node); } if (item.nodeType) { f.appendChild(item); } if (typeof item == "string") { f.appendChild(Snap.parse(item).node); } } return new Fragment(f); }; function make(name, parent) { var res = $(name); parent.appendChild(res); var el = wrap(res); return el; } function Paper(w, h) { var res, desc, defs, proto = Paper.prototype; if (w && w.tagName == "svg") { if (w.snap in hub) { return hub[w.snap]; } var doc = w.ownerDocument; res = new Element(w); desc = w.getElementsByTagName("desc")[0]; defs = w.getElementsByTagName("defs")[0]; if (!desc) { desc = $("desc"); desc.appendChild(doc.createTextNode("Created with Snap")); res.node.appendChild(desc); } if (!defs) { defs = $("defs"); res.node.appendChild(defs); } res.defs = defs; for (var key in proto) if (proto[has](key)) { res[key] = proto[key]; } res.paper = res.root = res; } else { res = make("svg", glob.doc.body); $(res.node, { height: h, version: 1.1, width: w, xmlns: xmlns }); } return res; } function wrap(dom) { if (!dom) { return dom; } if (dom instanceof Element || dom instanceof Fragment) { return dom; } if (dom.tagName && dom.tagName.toLowerCase() == "svg") { return new Paper(dom); } if (dom.tagName && dom.tagName.toLowerCase() == "object" && dom.type == "image/svg+xml") { return new Paper(dom.contentDocument.getElementsByTagName("svg")[0]); } return new Element(dom); } Snap._.make = make; Snap._.wrap = wrap; /*\ * Paper.el [ method ] ** * Creates an element on paper with a given name and no attributes ** - name (string) tag name - attr (object) attributes = (Element) the current element > Usage | var c = paper.circle(10, 10, 10); // is the same as... | var c = paper.el("circle").attr({ | cx: 10, | cy: 10, | r: 10 | }); | // and the same as | var c = paper.el("circle", { | cx: 10, | cy: 10, | r: 10 | }); \*/ Paper.prototype.el = function (name, attr) { var el = make(name, this.node); attr && el.attr(attr); return el; }; /*\ * Element.children [ method ] ** * Returns array of all the children of the element. = (array) array of Elements \*/ Element.prototype.children = function () { var out = [], ch = this.node.childNodes; for (var i = 0, ii = ch.length; i < ii; i++) { out[i] = Snap(ch[i]); } return out; }; function jsonFiller(root, o) { for (var i = 0, ii = root.length; i < ii; i++) { var item = { type: root[i].type, attr: root[i].attr() }, children = root[i].children(); o.push(item); if (children.length) { jsonFiller(children, item.childNodes = []); } } } /*\ * Element.toJSON [ method ] ** * Returns object representation of the given element and all its children. = (object) in format o { o type (string) this.type, o attr (object) attributes map, o childNodes (array) optional array of children in the same format o } \*/ Element.prototype.toJSON = function () { var out = []; jsonFiller([this], out); return out[0]; }; // default eve.on("snap.util.getattr", function () { var att = eve.nt(); att = att.substring(att.lastIndexOf(".") + 1); var css = att.replace(/[A-Z]/g, function (letter) { return "-" + letter.toLowerCase(); }); if (cssAttr[has](css)) { return this.node.ownerDocument.defaultView.getComputedStyle(this.node, null).getPropertyValue(css); } else { return $(this.node, att); } }); var cssAttr = { "alignment-baseline": 0, "baseline-shift": 0, "clip": 0, "clip-path": 0, "clip-rule": 0, "color": 0, "color-interpolation": 0, "color-interpolation-filters": 0, "color-profile": 0, "color-rendering": 0, "cursor": 0, "direction": 0, "display": 0, "dominant-baseline": 0, "enable-background": 0, "fill": 0, "fill-opacity": 0, "fill-rule": 0, "filter": 0, "flood-color": 0, "flood-opacity": 0, "font": 0, "font-family": 0, "font-size": 0, "font-size-adjust": 0, "font-stretch": 0, "font-style": 0, "font-variant": 0, "font-weight": 0, "glyph-orientation-horizontal": 0, "glyph-orientation-vertical": 0, "image-rendering": 0, "kerning": 0, "letter-spacing": 0, "lighting-color": 0, "marker": 0, "marker-end": 0, "marker-mid": 0, "marker-start": 0, "mask": 0, "opacity": 0, "overflow": 0, "pointer-events": 0, "shape-rendering": 0, "stop-color": 0, "stop-opacity": 0, "stroke": 0, "stroke-dasharray": 0, "stroke-dashoffset": 0, "stroke-linecap": 0, "stroke-linejoin": 0, "stroke-miterlimit": 0, "stroke-opacity": 0, "stroke-width": 0, "text-anchor": 0, "text-decoration": 0, "text-rendering": 0, "unicode-bidi": 0, "visibility": 0, "word-spacing": 0, "writing-mode": 0 }; eve.on("snap.util.attr", function (value) { var att = eve.nt(), attr = {}; att = att.substring(att.lastIndexOf(".") + 1); attr[att] = value; var style = att.replace(/-(\w)/gi, function (all, letter) { return letter.toUpperCase(); }), css = att.replace(/[A-Z]/g, function (letter) { return "-" + letter.toLowerCase(); }); if (cssAttr[has](css)) { this.node.style[style] = value == null ? E : value; } else { $(this.node, attr); } }); (function (proto) {}(Paper.prototype)); // simple ajax /*\ * Snap.ajax [ method ] ** * Simple implementation of Ajax ** - url (string) URL - postData (object|string) data for post request - callback (function) callback - scope (object) #optional scope of callback * or - url (string) URL - callback (function) callback - scope (object) #optional scope of callback = (XMLHttpRequest) the XMLHttpRequest object, just in case \*/ Snap.ajax = function (url, postData, callback, scope){ var req = new XMLHttpRequest, id = ID(); if (req) { if (is(postData, "function")) { scope = callback; callback = postData; postData = null; } else if (is(postData, "object")) { var pd = []; for (var key in postData) if (postData.hasOwnProperty(key)) { pd.push(encodeURIComponent(key) + "=" + encodeURIComponent(postData[key])); } postData = pd.join("&"); } req.open((postData ? "POST" : "GET"), url, true); if (postData) { req.setRequestHeader("X-Requested-With", "XMLHttpRequest"); req.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); } if (callback) { eve.once("snap.ajax." + id + ".0", callback); eve.once("snap.ajax." + id + ".200", callback); eve.once("snap.ajax." + id + ".304", callback); } req.onreadystatechange = function() { if (req.readyState != 4) return; eve("snap.ajax." + id + "." + req.status, scope, req); }; if (req.readyState == 4) { return req; } req.send(postData); return req; } }; /*\ * Snap.load [ method ] ** * Loads external SVG file as a @Fragment (see @Snap.ajax for more advanced AJAX) ** - url (string) URL - callback (function) callback - scope (object) #optional scope of callback \*/ Snap.load = function (url, callback, scope) { Snap.ajax(url, function (req) { var f = Snap.parse(req.responseText); scope ? callback.call(scope, f) : callback(f); }); }; var getOffset = function (elem) { var box = elem.getBoundingClientRect(), doc = elem.ownerDocument, body = doc.body, docElem = doc.documentElement, clientTop = docElem.clientTop || body.clientTop || 0, clientLeft = docElem.clientLeft || body.clientLeft || 0, top = box.top + (g.win.pageYOffset || docElem.scrollTop || body.scrollTop ) - clientTop, left = box.left + (g.win.pageXOffset || docElem.scrollLeft || body.scrollLeft) - clientLeft; return { y: top, x: left }; }; /*\ * Snap.getElementByPoint [ method ] ** * Returns you topmost element under given point. ** = (object) Snap element object - x (number) x coordinate from the top left corner of the window - y (number) y coordinate from the top left corner of the window > Usage | Snap.getElementByPoint(mouseX, mouseY).attr({stroke: "#f00"}); \*/ Snap.getElementByPoint = function (x, y) { var paper = this, svg = paper.canvas, target = glob.doc.elementFromPoint(x, y); if (glob.win.opera && target.tagName == "svg") { var so = getOffset(target), sr = target.createSVGRect(); sr.x = x - so.x; sr.y = y - so.y; sr.width = sr.height = 1; var hits = target.getIntersectionList(sr, null); if (hits.length) { target = hits[hits.length - 1]; } } if (!target) { return null; } return wrap(target); }; /*\ * Snap.plugin [ method ] ** * Let you write plugins. You pass in a function with five arguments, like this: | Snap.plugin(function (Snap, Element, Paper, global, Fragment) { | Snap.newmethod = function () {}; | Element.prototype.newmethod = function () {}; | Paper.prototype.newmethod = function () {}; | }); * Inside the function you have access to all main objects (and their * prototypes). This allow you to extend anything you want. ** - f (function) your plugin body \*/ Snap.plugin = function (f) { f(Snap, Element, Paper, glob, Fragment); }; glob.win.Snap = Snap; return Snap; }(window || this));