// Copyright (c) 2013 - 2017 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. import eve from './eve.js' /*\ * 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 \*/ export 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) { try { w = glob.doc.querySelector(String(w)) return wrap(w) } catch (e) { return null } } } 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: window, doc: 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\(([^,]+),([^,]+),([^,]+),([^\)]+)\)/, 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]*)+)/gi, tCommand = /([rstm])[\s,]*((-?\d*\.?\d*(?:e[\-+]?\d+)?[\s]*,?[\s]*)+)/gi, pathValues = /(-?\d*\.?\d*(?:e[\-+]?\d+)?)[\s]*,?[\s]*/gi, 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 = {}, /*\ * Snap.url [ method ] ** * Wraps path into `"url('')"`. - value (string) path = (string) wrapped path \*/ 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. - x (number) value - y (number) value = (number) atan2 in degrees \*/ Snap.atan2 = function (x, y) { return Snap.deg(math.atan2(x, y)) } /*\ * Snap.angle [ method ] ** * Returns an angle between two or three points - 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 - 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 - 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. - 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) * 0.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 *= 0.5 while (precision > 0.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 *= 0.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 < 0.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 < 0.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 && w.tagName.toLowerCase() == '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) }