// 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'
import {
doc,
win,
CSS_ATTR,
xmlns,
T_COMMAND,
PATH_VALUES
} from './lib/constants.js'
import { Matrix } from './matrix.js'
import {
$,
is,
uuid,
clone,
preload,
cacher,
jsonFiller,
extend
} from './utils.js'
let has = 'hasOwnProperty',
math = Math,
abs = math.abs,
E = '',
S = ' ',
objectToString = Object.prototype.toString,
hub = {}
export function make(name, parent) {
let res = $(name)
parent.appendChild(res)
let el = wrap(res)
return el
}
export function wrap(dom) {
if (!dom) {
return dom
}
if (dom instanceof SnapElement || dom instanceof Fragment) {
return dom
}
if (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 SnapElement(dom)
}
export class Fragment {
constructor(frag) {
this.node = frag
}
}
/*\
**
* 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) @SnapElement
\*/
export class Snap {
static _ = { $ }
/*\
**
* Parses SVG fragment and converts it into a @Fragment
**
\*/
static parse(svg) {
let f = doc.createDocumentFragment(),
full = true,
div = doc.createElement('div')
svg = String(svg)
if (!svg.match(/^\s*<\s*svg(?:\s|>)/)) {
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)
}
/*\
**
* Creates a DOM fragment from a given list of elements or strings
**
- varargs (…) SVG string
\*/
static fragment(...args) {
let f = doc.createDocumentFragment()
for (let i = 0, ii = args.length; i < ii; i++) {
let 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)
}
/*\
**
* Returns closest point to a given one on a given path.
- path (SnapElement) 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
static closestPoint(path, x, y) {
function distance2(p) {
let dx = p.x - x,
dy = p.y - y
return dx * dx + dy * dy
}
let pathNode = path.node,
pathLength = pathNode.getTotalLength(),
precision = (pathLength / pathNode.pathSegList.numberOfItems) * 0.125,
best,
bestLength,
bestDistance = Infinity
// linear scan for coarse approximation
for (
let 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) {
let 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
}
/*\
**
* 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
\*/
static snapTo(values, value, tolerance) {
tolerance = is(tolerance, 'finite') ? tolerance : 10
if (is(values, 'array')) {
let i = values.length
while (i--)
if (abs(values[i] - value) <= tolerance) {
return values[i]
}
} else {
values = +values
let rem = value % values
if (rem < tolerance) {
return value - rem
}
if (rem > values - tolerance) {
return value - rem + values
}
}
return value
}
/*\
**
* Wraps a DOM element specified by CSS selector as @SnapElement
- query (string) CSS selector of the element
= (SnapElement) the current element
\*/
static select(query = '') {
query = query.replace(/([^\\]):/g, '$1\\:')
return wrap(doc.querySelector(query))
}
/*\
**
* Wraps DOM elements specified by CSS selector as set or array of @SnapElement
- query (string) CSS selector of the element
= (SnapElement) the current element
\*/
static selectAll(query = '') {
let nodelist = doc.querySelectorAll(query),
set = []
for (let i = 0; i < nodelist.length; i++) {
set.push(wrap(nodelist[i]))
}
return set
}
/*\
**
* 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"});
\*/
static getElementByPoint(x, y) {
let paper = this,
svg = paper.canvas,
target = doc.elementFromPoint(x, y)
if (!target) {
return null
}
return wrap(target)
}
constructor(w, h) {
if (w) {
if (w.nodeType) {
return wrap(w)
}
if (w instanceof SnapElement) {
return w
}
if (h == null) {
try {
w = 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)
}
}
// Transformations
/*\
**
* 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
}
let pth = Snap.path(pathString)
if (pth.arr) {
return Snap.path.clone(pth.arr)
}
let 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) {
String(pathString).replace(pathCommand, function (a, b, c) {
let params = [],
name = b.toLowerCase()
c.replace(PATH_VALUES, 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
}
/*\
**
* 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
\*/
let parseTransformString = (Snap.parseTransformString = function (TString) {
if (!TString) {
return null
}
let 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) {
String(TString).replace(T_COMMAND, function (a, b, c) {
let params = [],
name = b.toLowerCase()
c.replace(PATH_VALUES, function (a, b) {
b && params.push(+b)
})
data.push([b].concat(params))
})
}
data.toString = Snap.path.toString
return data
})
function svgTransform2string(tstr) {
let 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) {
let tdata = parseTransformString(tstr),
m = new Matrix()
if (tdata) {
for (let i = 0, ii = tdata.length; i < ii; i++) {
let t = tdata[i],
tlen = t.length,
command = String(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
export function getSomeDefs(el) {
let p =
(el.node.ownerSVGElement && wrap(el.node.ownerSVGElement)) ||
(el.node.parentNode && wrap(el.node.parentNode)) ||
Snap.select('svg') ||
new 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')
)
}
export function unit2px(el, name, value) {
let 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
}
function add2group(list) {
if (!is(list, 'array')) {
list = Array.prototype.slice.call(arguments, 0)
}
let 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)
}
}
let 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 (let key in hub)
if (hub[has](key)) {
let 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)
export class Paper {
constructor(w, h) {
let res, desc, defs
if (w && w.tagName && w.tagName.toLowerCase() == 'svg') {
if (w.snap in hub) {
return hub[w.snap]
}
let doc = w.ownerDocument
res = new SnapElement(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
extend(res, Paper.prototype)
res.paper = res.root = res
} else {
res = make('svg', doc.body)
$(res.node, {
width: w,
height: h,
version: 1.1,
xmlns
})
}
return res
}
/*\
**
* Creates a nested SVG element.
- x (number) @optional X of the element
- y (number) @optional Y of the element
- width (number) @optional width of the element
- height (number) @optional height of the element
- vbx (number) @optional viewbox X
- vby (number) @optional viewbox Y
- vbw (number) @optional viewbox width
- vbh (number) @optional viewbox height
**
= (object) the `svg` element
**
\*/
svg(x, y, width, height, vbx, vby, vbw, vbh) {
let attrs = {}
if (is(x, 'object') && y == null) {
attrs = x
} else {
if (x != null) {
attrs.x = x
}
if (y != null) {
attrs.y = y
}
if (width != null) {
attrs.width = width
}
if (height != null) {
attrs.height = height
}
if (vbx != null && vby != null && vbw != null && vbh != null) {
attrs.viewBox = [vbx, vby, vbw, vbh]
}
}
return this.el('svg', attrs)
}
mask(first) {
let attr,
el = this.el('mask')
if (arguments.length == 1 && first && !first.type) {
el.attr(first)
} else if (arguments.length) {
el.add(Array.from(arguments))
}
return el
}
/*\
**
* Equivalent in behaviour to @Paper.g, except it’s a pattern.
- x (number) @optional X of the element
- y (number) @optional Y of the element
- width (number) @optional width of the element
- height (number) @optional height of the element
- vbx (number) @optional viewbox X
- vby (number) @optional viewbox Y
- vbw (number) @optional viewbox width
- vbh (number) @optional viewbox height
**
= (object) the `pattern` element
**
\*/
ptrn(x, y, width, height, vx, vy, vw, vh) {
if (is(x, 'object')) {
let attr = x
} else {
attr = { patternUnits: 'userSpaceOnUse' }
if (x) {
attr.x = x
}
if (y) {
attr.y = y
}
if (width != null) {
attr.width = width
}
if (height != null) {
attr.height = height
}
if (vx != null && vy != null && vw != null && vh != null) {
attr.viewBox = [vx, vy, vw, vh]
} else {
attr.viewBox = [x || 0, y || 0, width || 0, height || 0]
}
}
return this.el('pattern', attr)
}
/*\
**
* Creates a