UNPKG

snapsvg

Version:
797 lines (794 loc) 25.2 kB
// Copyright (c) 2013 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. Snap.plugin(function (Snap, Element, Paper, glob, Fragment) { var elproto = Element.prototype, is = Snap.is, Str = String, unit2px = Snap._unit2px, $ = Snap._.$, make = Snap._.make, getSomeDefs = Snap._.getSomeDefs, has = "hasOwnProperty", wrap = Snap._.wrap; /*\ * Element.getBBox [ method ] ** * Returns the bounding box descriptor for the given element ** = (object) bounding box descriptor: o { o cx: (number) x of the center, o cy: (number) x of the center, o h: (number) height, o height: (number) height, o path: (string) path command for the box, o r0: (number) radius of a circle that fully encloses the box, o r1: (number) radius of the smallest circle that can be enclosed, o r2: (number) radius of the largest circle that can be enclosed, o vb: (string) box as a viewbox command, o w: (number) width, o width: (number) width, o x2: (number) x of the right side, o x: (number) x of the left side, o y2: (number) y of the bottom edge, o y: (number) y of the top edge o } \*/ elproto.getBBox = function (isWithoutTransform) { if (this.type == "tspan") { return Snap._.box(this.node.getClientRects().item(0)); } if (!Snap.Matrix || !Snap.path) { return this.node.getBBox(); } var el = this, m = new Snap.Matrix; if (el.removed) { return Snap._.box(); } while (el.type == "use") { if (!isWithoutTransform) { m = m.add(el.transform().localMatrix.translate(el.attr("x") || 0, el.attr("y") || 0)); } if (el.original) { el = el.original; } else { var href = el.attr("xlink:href"); el = el.original = el.node.ownerDocument.getElementById(href.substring(href.indexOf("#") + 1)); } } var _ = el._, pathfinder = Snap.path.get[el.type] || Snap.path.get.deflt; try { if (isWithoutTransform) { _.bboxwt = pathfinder ? Snap.path.getBBox(el.realPath = pathfinder(el)) : Snap._.box(el.node.getBBox()); return Snap._.box(_.bboxwt); } else { el.realPath = pathfinder(el); el.matrix = el.transform().localMatrix; _.bbox = Snap.path.getBBox(Snap.path.map(el.realPath, m.add(el.matrix))); return Snap._.box(_.bbox); } } catch (e) { // Firefox doesn’t give you bbox of hidden element return Snap._.box(); } }; var propString = function () { return this.string; }; function extractTransform(el, tstr) { if (tstr == null) { var doReturn = true; if (el.type == "linearGradient" || el.type == "radialGradient") { tstr = el.node.getAttribute("gradientTransform"); } else if (el.type == "pattern") { tstr = el.node.getAttribute("patternTransform"); } else { tstr = el.node.getAttribute("transform"); } if (!tstr) { return new Snap.Matrix; } tstr = Snap._.svgTransform2string(tstr); } else { if (!Snap._.rgTransform.test(tstr)) { tstr = Snap._.svgTransform2string(tstr); } else { tstr = Str(tstr).replace(/\.{3}|\u2026/g, el._.transform || ""); } if (is(tstr, "array")) { tstr = Snap.path ? Snap.path.toString.call(tstr) : Str(tstr); } el._.transform = tstr; } var m = Snap._.transform2matrix(tstr, el.getBBox(1)); if (doReturn) { return m; } else { el.matrix = m; } } /*\ * Element.transform [ method ] ** * Gets or sets transformation of the element ** - tstr (string) transform string in Snap or SVG format = (Element) the current element * or = (object) transformation descriptor: o { o string (string) transform string, o globalMatrix (Matrix) matrix of all transformations applied to element or its parents, o localMatrix (Matrix) matrix of transformations applied only to the element, o diffMatrix (Matrix) matrix of difference between global and local transformations, o global (string) global transformation as string, o local (string) local transformation as string, o toString (function) returns `string` property o } \*/ elproto.transform = function (tstr) { var _ = this._; if (tstr == null) { var papa = this, global = new Snap.Matrix(this.node.getCTM()), local = extractTransform(this), ms = [local], m = new Snap.Matrix, i, localString = local.toTransformString(), string = Str(local) == Str(this.matrix) ? Str(_.transform) : localString; while (papa.type != "svg" && (papa = papa.parent())) { ms.push(extractTransform(papa)); } i = ms.length; while (i--) { m.add(ms[i]); } return { string: string, globalMatrix: global, totalMatrix: m, localMatrix: local, diffMatrix: global.clone().add(local.invert()), global: global.toTransformString(), total: m.toTransformString(), local: localString, toString: propString }; } if (tstr instanceof Snap.Matrix) { this.matrix = tstr; this._.transform = tstr.toTransformString(); } else { extractTransform(this, tstr); } if (this.node) { if (this.type == "linearGradient" || this.type == "radialGradient") { $(this.node, {gradientTransform: this.matrix}); } else if (this.type == "pattern") { $(this.node, {patternTransform: this.matrix}); } else { $(this.node, {transform: this.matrix}); } } return this; }; /*\ * Element.parent [ method ] ** * Returns the element's parent ** = (Element) the parent element \*/ elproto.parent = function () { return wrap(this.node.parentNode); }; /*\ * Element.append [ method ] ** * Appends the given element to current one ** - el (Element|Set) element to append = (Element) the parent element \*/ /*\ * Element.add [ method ] ** * See @Element.append \*/ elproto.append = elproto.add = function (el) { if (el) { if (el.type == "set") { var it = this; el.forEach(function (el) { it.add(el); }); return this; } el = wrap(el); this.node.appendChild(el.node); el.paper = this.paper; } return this; }; /*\ * Element.appendTo [ method ] ** * Appends the current element to the given one ** - el (Element) parent element to append to = (Element) the child element \*/ elproto.appendTo = function (el) { if (el) { el = wrap(el); el.append(this); } return this; }; /*\ * Element.prepend [ method ] ** * Prepends the given element to the current one ** - el (Element) element to prepend = (Element) the parent element \*/ elproto.prepend = function (el) { if (el) { if (el.type == "set") { var it = this, first; el.forEach(function (el) { if (first) { first.after(el); } else { it.prepend(el); } first = el; }); return this; } el = wrap(el); var parent = el.parent(); this.node.insertBefore(el.node, this.node.firstChild); this.add && this.add(); el.paper = this.paper; this.parent() && this.parent().add(); parent && parent.add(); } return this; }; /*\ * Element.prependTo [ method ] ** * Prepends the current element to the given one ** - el (Element) parent element to prepend to = (Element) the child element \*/ elproto.prependTo = function (el) { el = wrap(el); el.prepend(this); return this; }; /*\ * Element.before [ method ] ** * Inserts given element before the current one ** - el (Element) element to insert = (Element) the parent element \*/ elproto.before = function (el) { if (el.type == "set") { var it = this; el.forEach(function (el) { var parent = el.parent(); it.node.parentNode.insertBefore(el.node, it.node); parent && parent.add(); }); this.parent().add(); return this; } el = wrap(el); var parent = el.parent(); this.node.parentNode.insertBefore(el.node, this.node); this.parent() && this.parent().add(); parent && parent.add(); el.paper = this.paper; return this; }; /*\ * Element.after [ method ] ** * Inserts given element after the current one ** - el (Element) element to insert = (Element) the parent element \*/ elproto.after = function (el) { el = wrap(el); var parent = el.parent(); if (this.node.nextSibling) { this.node.parentNode.insertBefore(el.node, this.node.nextSibling); } else { this.node.parentNode.appendChild(el.node); } this.parent() && this.parent().add(); parent && parent.add(); el.paper = this.paper; return this; }; /*\ * Element.insertBefore [ method ] ** * Inserts the element after the given one ** - el (Element) element next to whom insert to = (Element) the parent element \*/ elproto.insertBefore = function (el) { el = wrap(el); var parent = this.parent(); el.node.parentNode.insertBefore(this.node, el.node); this.paper = el.paper; parent && parent.add(); el.parent() && el.parent().add(); return this; }; /*\ * Element.insertAfter [ method ] ** * Inserts the element after the given one ** - el (Element) element next to whom insert to = (Element) the parent element \*/ elproto.insertAfter = function (el) { el = wrap(el); var parent = this.parent(); el.node.parentNode.insertBefore(this.node, el.node.nextSibling); this.paper = el.paper; parent && parent.add(); el.parent() && el.parent().add(); return this; }; /*\ * Element.remove [ method ] ** * Removes element from the DOM = (Element) the detached element \*/ elproto.remove = function () { var parent = this.parent(); this.node.parentNode && this.node.parentNode.removeChild(this.node); delete this.paper; this.removed = true; parent && parent.add(); return this; }; /*\ * Element.select [ method ] ** * Gathers the nested @Element matching the given set of CSS selectors ** - query (string) CSS selector = (Element) result of query selection \*/ elproto.select = function (query) { return wrap(this.node.querySelector(query)); }; /*\ * Element.selectAll [ method ] ** * Gathers nested @Element objects matching the given set of CSS selectors ** - query (string) CSS selector = (Set|array) result of query selection \*/ elproto.selectAll = function (query) { var nodelist = this.node.querySelectorAll(query), set = (Snap.set || Array)(); for (var i = 0; i < nodelist.length; i++) { set.push(wrap(nodelist[i])); } return set; }; /*\ * Element.asPX [ method ] ** * Returns given attribute of the element as a `px` value (not %, em, etc.) ** - attr (string) attribute name - value (string) #optional attribute value = (Element) result of query selection \*/ elproto.asPX = function (attr, value) { if (value == null) { value = this.attr(attr); } return +unit2px(this, attr, value); }; // SIERRA Element.use(): I suggest adding a note about how to access the original element the returned <use> instantiates. It's a part of SVG with which ordinary web developers may be least familiar. /*\ * Element.use [ method ] ** * Creates a `<use>` element linked to the current element ** = (Element) the `<use>` element \*/ elproto.use = function () { var use, id = this.node.id; if (!id) { id = this.id; $(this.node, { id: id }); } if (this.type == "linearGradient" || this.type == "radialGradient" || this.type == "pattern") { use = make(this.type, this.node.parentNode); } else { use = make("use", this.node.parentNode); } $(use.node, { "xlink:href": "#" + id }); use.original = this; return use; }; function fixids(el) { var els = el.selectAll("*"), it, url = /^\s*url\(("|'|)(.*)\1\)\s*$/, ids = [], uses = {}; function urltest(it, name) { var val = $(it.node, name); val = val && val.match(url); val = val && val[2]; if (val && val.charAt() == "#") { val = val.substring(1); } else { return; } if (val) { uses[val] = (uses[val] || []).concat(function (id) { var attr = {}; attr[name] = Snap.url(id); $(it.node, attr); }); } } function linktest(it) { var val = $(it.node, "xlink:href"); if (val && val.charAt() == "#") { val = val.substring(1); } else { return; } if (val) { uses[val] = (uses[val] || []).concat(function (id) { it.attr("xlink:href", "#" + id); }); } } for (var i = 0, ii = els.length; i < ii; i++) { it = els[i]; urltest(it, "fill"); urltest(it, "stroke"); urltest(it, "filter"); urltest(it, "mask"); urltest(it, "clip-path"); linktest(it); var oldid = $(it.node, "id"); if (oldid) { $(it.node, {id: it.id}); ids.push({ old: oldid, id: it.id }); } } for (i = 0, ii = ids.length; i < ii; i++) { var fs = uses[ids[i].old]; if (fs) { for (var j = 0, jj = fs.length; j < jj; j++) { fs[j](ids[i].id); } } } } /*\ * Element.clone [ method ] ** * Creates a clone of the element and inserts it after the element ** = (Element) the clone \*/ elproto.clone = function () { var clone = wrap(this.node.cloneNode(true)); if ($(clone.node, "id")) { $(clone.node, {id: clone.id}); } fixids(clone); clone.insertAfter(this); return clone; }; /*\ * Element.toDefs [ method ] ** * Moves element to the shared `<defs>` area ** = (Element) the element \*/ elproto.toDefs = function () { var defs = getSomeDefs(this); defs.appendChild(this.node); return this; }; /*\ * Element.toPattern [ method ] ** * Creates a `<pattern>` element from the current element ** * To create a pattern you have to specify the pattern rect: - x (string|number) - y (string|number) - width (string|number) - height (string|number) = (Element) the `<pattern>` element * You can use pattern later on as an argument for `fill` attribute: | var p = paper.path("M10-5-10,15M15,0,0,15M0-5-20,15").attr({ | fill: "none", | stroke: "#bada55", | strokeWidth: 5 | }).pattern(0, 0, 10, 10), | c = paper.circle(200, 200, 100); | c.attr({ | fill: p | }); \*/ elproto.pattern = elproto.toPattern = function (x, y, width, height) { var p = make("pattern", getSomeDefs(this)); if (x == null) { x = this.getBBox(); } if (is(x, "object") && "x" in x) { y = x.y; width = x.width; height = x.height; x = x.x; } $(p.node, { x: x, y: y, width: width, height: height, patternUnits: "userSpaceOnUse", id: p.id, viewBox: [x, y, width, height].join(" ") }); p.node.appendChild(this.node); return p; }; // SIERRA Element.marker(): clarify what a reference point is. E.g., helps you offset the object from its edge such as when centering it over a path. // SIERRA Element.marker(): I suggest the method should accept default reference point values. Perhaps centered with (refX = width/2) and (refY = height/2)? Also, couldn't it assume the element's current _width_ and _height_? And please specify what _x_ and _y_ mean: offsets? If so, from where? Couldn't they also be assigned default values? /*\ * Element.marker [ method ] ** * Creates a `<marker>` element from the current element ** * To create a marker you have to specify the bounding rect and reference point: - x (number) - y (number) - width (number) - height (number) - refX (number) - refY (number) = (Element) the `<marker>` element * You can specify the marker later as an argument for `marker-start`, `marker-end`, `marker-mid`, and `marker` attributes. The `marker` attribute places the marker at every point along the path, and `marker-mid` places them at every point except the start and end. \*/ // TODO add usage for markers elproto.marker = function (x, y, width, height, refX, refY) { var p = make("marker", getSomeDefs(this)); if (x == null) { x = this.getBBox(); } if (is(x, "object") && "x" in x) { y = x.y; width = x.width; height = x.height; refX = x.refX || x.cx; refY = x.refY || x.cy; x = x.x; } $(p.node, { viewBox: [x, y, width, height].join(" "), markerWidth: width, markerHeight: height, orient: "auto", refX: refX || 0, refY: refY || 0, id: p.id }); p.node.appendChild(this.node); return p; }; var eldata = {}; /*\ * Element.data [ method ] ** * Adds or retrieves given value associated with given key. (Don’t confuse * with `data-` attributes) * * See also @Element.removeData - key (string) key to store data - value (any) #optional value to store = (object) @Element * or, if value is not specified: = (any) value > Usage | for (var i = 0, i < 5, i++) { | paper.circle(10 + 15 * i, 10, 10) | .attr({fill: "#000"}) | .data("i", i) | .click(function () { | alert(this.data("i")); | }); | } \*/ elproto.data = function (key, value) { var data = eldata[this.id] = eldata[this.id] || {}; if (arguments.length == 0){ eve("snap.data.get." + this.id, this, data, null); return data; } if (arguments.length == 1) { if (Snap.is(key, "object")) { for (var i in key) if (key[has](i)) { this.data(i, key[i]); } return this; } eve("snap.data.get." + this.id, this, data[key], key); return data[key]; } data[key] = value; eve("snap.data.set." + this.id, this, value, key); return this; }; /*\ * Element.removeData [ method ] ** * Removes value associated with an element by given key. * If key is not provided, removes all the data of the element. - key (string) #optional key = (object) @Element \*/ elproto.removeData = function (key) { if (key == null) { eldata[this.id] = {}; } else { eldata[this.id] && delete eldata[this.id][key]; } return this; }; /*\ * Element.outerSVG [ method ] ** * Returns SVG code for the element, equivalent to HTML's `outerHTML`. * * See also @Element.innerSVG = (string) SVG code for the element \*/ /*\ * Element.toString [ method ] ** * See @Element.outerSVG \*/ elproto.outerSVG = elproto.toString = toString(1); /*\ * Element.innerSVG [ method ] ** * Returns SVG code for the element's contents, equivalent to HTML's `innerHTML` = (string) SVG code for the element \*/ elproto.innerSVG = toString(); function toString(type) { return function () { var res = type ? "<" + this.type : "", attr = this.node.attributes, chld = this.node.childNodes; if (type) { for (var i = 0, ii = attr.length; i < ii; i++) { res += " " + attr[i].name + '="' + attr[i].value.replace(/"/g, '\\"') + '"'; } } if (chld.length) { type && (res += ">"); for (i = 0, ii = chld.length; i < ii; i++) { if (chld[i].nodeType == 3) { res += chld[i].nodeValue; } else if (chld[i].nodeType == 1) { res += wrap(chld[i]).toString(); } } type && (res += "</" + this.type + ">"); } else { type && (res += "/>"); } return res; }; } elproto.toDataURL = function () { if (window && window.btoa) { var bb = this.getBBox(), svg = Snap.format('<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{width}" height="{height}" viewBox="{x} {y} {width} {height}">{contents}</svg>', { x: +bb.x.toFixed(3), y: +bb.y.toFixed(3), width: +bb.width.toFixed(3), height: +bb.height.toFixed(3), contents: this.outerSVG() }); return "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(svg))); } }; /*\ * Fragment.select [ method ] ** * See @Element.select \*/ Fragment.prototype.select = elproto.select; /*\ * Fragment.selectAll [ method ] ** * See @Element.selectAll \*/ Fragment.prototype.selectAll = elproto.selectAll; });