1417 lines
32 KiB
JavaScript
1417 lines
32 KiB
JavaScript
/**
|
||
* {}
|
||
* @author yutent<yutent.io@gmail.com>
|
||
* @date 2024/03/08 10:59:09
|
||
*/
|
||
|
||
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)
|
||
}
|
||
|
||
/*\
|
||
**
|
||
* Wraps a DOM element specified by CSS selector as @SnapElement
|
||
- query (string) CSS selector of the element
|
||
= (SnapElement) the current element
|
||
\*/
|
||
export function 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
|
||
\*/
|
||
export function selectAll(query = '') {
|
||
let nodelist = doc.querySelectorAll(query),
|
||
set = []
|
||
for (let i = 0; i < nodelist.length; i++) {
|
||
set.push(wrap(nodelist[i]))
|
||
}
|
||
return set
|
||
}
|
||
|
||
export class Fragment {
|
||
constructor(frag) {
|
||
this.node = frag
|
||
}
|
||
}
|
||
|
||
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 = '<svg>' + 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)
|
||
}
|
||
|
||
/*\
|
||
**
|
||
* 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
|
||
}
|
||
|
||
/*
|
||
**
|
||
* 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 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)) ||
|
||
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)) || 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 <use> element.
|
||
- id (string) @optional id of element to link
|
||
* or
|
||
- id (SnapElement) @optional element to link
|
||
**
|
||
= (object) the `use` element
|
||
**
|
||
\*/
|
||
use(id) {
|
||
if (id != null) {
|
||
if (id instanceof SnapElement) {
|
||
if (!id.attr('id')) {
|
||
id.attr({ id: uuid(id.type + 'S') })
|
||
}
|
||
id = id.attr('id')
|
||
}
|
||
if (String(id).charAt() == '#') {
|
||
id = id.substring(1)
|
||
}
|
||
return this.el('use', { 'xlink:href': '#' + id })
|
||
} else {
|
||
return SnapElement.prototype.use.call(this)
|
||
}
|
||
}
|
||
/*\
|
||
**
|
||
* Creates a <symbol> element.
|
||
- vbx (number) @optional viewbox X
|
||
- vby (number) @optional viewbox Y
|
||
- vbw (number) @optional viewbox width
|
||
- vbh (number) @optional viewbox height
|
||
= (object) the `symbol` element
|
||
**
|
||
\*/
|
||
symbol(vx, vy, vw, vh) {
|
||
let attr = {}
|
||
if (vx != null && vy != null && vw != null && vh != null) {
|
||
attr.viewBox = [vx, vy, vw, vh]
|
||
}
|
||
|
||
return this.el('symbol', attr)
|
||
}
|
||
/*\
|
||
**
|
||
* Draws a text string
|
||
**
|
||
- x (number) x coordinate position
|
||
- y (number) y coordinate position
|
||
- text (string|array) The text string to draw or array of strings to nest within separate `<tspan>` elements
|
||
= (object) the `text` element
|
||
**
|
||
> Usage
|
||
| let t1 = paper.text(50, 50, "Snap");
|
||
| let t2 = paper.text(50, 50, ["S","n","a","p"]);
|
||
| // Text path usage
|
||
| t1.attr({textpath: "M10,10L100,100"});
|
||
| // or
|
||
| let pth = paper.path("M10,10L100,100");
|
||
| t1.attr({textpath: pth});
|
||
\*/
|
||
text(x, y, text) {
|
||
let attr = {}
|
||
if (is(x, 'object')) {
|
||
attr = x
|
||
} else if (x != null) {
|
||
attr = {
|
||
x: x,
|
||
y: y,
|
||
text: text || ''
|
||
}
|
||
}
|
||
return this.el('text', attr)
|
||
}
|
||
/*\
|
||
**
|
||
* Draws a line
|
||
**
|
||
- x1 (number) x coordinate position of the start
|
||
- y1 (number) y coordinate position of the start
|
||
- x2 (number) x coordinate position of the end
|
||
- y2 (number) y coordinate position of the end
|
||
= (object) the `line` element
|
||
**
|
||
> Usage
|
||
| let t1 = paper.line(50, 50, 100, 100);
|
||
\*/
|
||
line(x1, y1, x2, y2) {
|
||
let attr = {}
|
||
if (is(x1, 'object')) {
|
||
attr = x1
|
||
} else if (x1 != null) {
|
||
attr = {
|
||
x1: x1,
|
||
x2: x2,
|
||
y1: y1,
|
||
y2: y2
|
||
}
|
||
}
|
||
return this.el('line', attr)
|
||
}
|
||
/*\
|
||
**
|
||
* Draws a polyline
|
||
**
|
||
- points (array) array of points
|
||
* or
|
||
- varargs (…) points
|
||
= (object) the `polyline` element
|
||
**
|
||
> Usage
|
||
| let p1 = paper.polyline([10, 10, 100, 100]);
|
||
| let p2 = paper.polyline(10, 10, 100, 100);
|
||
\*/
|
||
polyline(points) {
|
||
if (arguments.length > 1) {
|
||
points = Array.from(arguments)
|
||
}
|
||
let attr = {}
|
||
if (is(points, 'object') && !is(points, 'array')) {
|
||
attr = points
|
||
} else if (points != null) {
|
||
attr = { points: points }
|
||
}
|
||
return this.el('polyline', attr)
|
||
}
|
||
/*\
|
||
**
|
||
* Draws a polygon. See @Paper.polyline
|
||
\*/
|
||
polygon(points) {
|
||
if (arguments.length > 1) {
|
||
points = Array.prototype.slice.call(arguments, 0)
|
||
}
|
||
let attr = {}
|
||
if (is(points, 'object') && !is(points, 'array')) {
|
||
attr = points
|
||
} else if (points != null) {
|
||
attr = { points: points }
|
||
}
|
||
return this.el('polygon', attr)
|
||
}
|
||
|
||
/*
|
||
**
|
||
* Creates an element on paper with a given name and no attributes
|
||
**
|
||
- name (string) tag name
|
||
- attr (object) attributes
|
||
= (SnapElement) the current element
|
||
> Usage
|
||
| let c = paper.circle(10, 10, 10); // is the same as...
|
||
| let c = paper.el("circle").attr({
|
||
| cx: 10,
|
||
| cy: 10,
|
||
| r: 10
|
||
| });
|
||
| // and the same as
|
||
| let c = paper.el("circle", {
|
||
| cx: 10,
|
||
| cy: 10,
|
||
| r: 10
|
||
| });
|
||
*/
|
||
el(name, attr) {
|
||
let el = make(name, this.node)
|
||
attr && el.attr(attr)
|
||
return el
|
||
}
|
||
|
||
/*
|
||
*
|
||
* Draws a rectangle
|
||
**
|
||
- x (number) x coordinate of the top left corner
|
||
- y (number) y coordinate of the top left corner
|
||
- width (number) width
|
||
- height (number) height
|
||
- rx (number) #optional horizontal radius for rounded corners, default is 0
|
||
- ry (number) #optional vertical radius for rounded corners, default is rx or 0
|
||
= (object) the `rect` element
|
||
**
|
||
> Usage
|
||
| // regular rectangle
|
||
| let c = paper.rect(10, 10, 50, 50);
|
||
| // rectangle with rounded corners
|
||
| let c = paper.rect(40, 40, 50, 50, 10);
|
||
*/
|
||
rect(x, y, w, h, rx, ry) {
|
||
let attr
|
||
if (ry == null) {
|
||
ry = rx
|
||
}
|
||
if (is(x, 'object') && x == '[object Object]') {
|
||
attr = x
|
||
} else if (x != null) {
|
||
attr = {
|
||
x: x,
|
||
y: y,
|
||
width: w,
|
||
height: h
|
||
}
|
||
if (rx != null) {
|
||
attr.rx = rx
|
||
attr.ry = ry
|
||
}
|
||
}
|
||
return this.el('rect', attr)
|
||
}
|
||
|
||
/*\
|
||
**
|
||
* Draws a circle
|
||
**
|
||
- x (number) x coordinate of the centre
|
||
- y (number) y coordinate of the centre
|
||
- r (number) radius
|
||
= (object) the `circle` element
|
||
**
|
||
> Usage
|
||
| let c = paper.circle(50, 50, 40);
|
||
\*/
|
||
circle(cx, cy, r) {
|
||
let attr
|
||
if (is(cx, 'object') && cx == '[object Object]') {
|
||
attr = cx
|
||
} else if (cx != null) {
|
||
attr = {
|
||
cx: cx,
|
||
cy: cy,
|
||
r: r
|
||
}
|
||
}
|
||
return this.el('circle', attr)
|
||
}
|
||
|
||
/*\
|
||
**
|
||
* Places an image on the surface
|
||
**
|
||
- src (string) URI of the source image
|
||
- x (number) x offset position
|
||
- y (number) y offset position
|
||
- width (number) width of the image
|
||
- height (number) height of the image
|
||
= (object) the `image` element
|
||
* or
|
||
= (object) Snap element object with type `image`
|
||
**
|
||
> Usage
|
||
| let c = paper.image("apple.png", 10, 10, 80, 80);
|
||
\*/
|
||
image(src, x, y, width, height) {
|
||
let el = this.el('image')
|
||
if (is(src, 'object') && 'src' in src) {
|
||
el.attr(src)
|
||
} else if (src != null) {
|
||
let set = {
|
||
'xlink:href': src,
|
||
preserveAspectRatio: 'none'
|
||
}
|
||
if (x != null && y != null) {
|
||
set.x = x
|
||
set.y = y
|
||
}
|
||
if (width != null && height != null) {
|
||
set.width = width
|
||
set.height = height
|
||
} else {
|
||
preload(src, function () {
|
||
$(el.node, {
|
||
width: this.offsetWidth,
|
||
height: this.offsetHeight
|
||
})
|
||
})
|
||
}
|
||
$(el.node, set)
|
||
}
|
||
return el
|
||
}
|
||
|
||
/*\
|
||
**
|
||
* Draws an ellipse
|
||
**
|
||
- x (number) x coordinate of the centre
|
||
- y (number) y coordinate of the centre
|
||
- rx (number) horizontal radius
|
||
- ry (number) vertical radius
|
||
= (object) the `ellipse` element
|
||
**
|
||
> Usage
|
||
| let c = paper.ellipse(50, 50, 40, 20);
|
||
\*/
|
||
ellipse(cx, cy, rx, ry) {
|
||
let attr
|
||
if (is(cx, 'object') && cx == '[object Object]') {
|
||
attr = cx
|
||
} else if (cx != null) {
|
||
attr = {
|
||
cx: cx,
|
||
cy: cy,
|
||
rx: rx,
|
||
ry: ry
|
||
}
|
||
}
|
||
return this.el('ellipse', attr)
|
||
}
|
||
|
||
path(d) {
|
||
let attr
|
||
if (is(d, 'object') && !is(d, 'array')) {
|
||
attr = d
|
||
} else if (d) {
|
||
attr = { d: d }
|
||
}
|
||
return this.el('path', attr)
|
||
}
|
||
|
||
group(first) {
|
||
let attr,
|
||
el = this.el('g')
|
||
if (arguments.length == 1 && first && !first.type) {
|
||
el.attr(first)
|
||
} else if (arguments.length) {
|
||
el.add(Array.from(arguments))
|
||
}
|
||
return el
|
||
}
|
||
|
||
/*
|
||
**
|
||
* Creates a `<filter>` element
|
||
**
|
||
- filstr (string) SVG fragment of filter provided as a string
|
||
= (object) @SnapElement
|
||
* Note: It is recommended to use filters embedded into the page inside an empty SVG element.
|
||
> Usage
|
||
| let f = paper.filter('<feGaussianBlur stdDeviation="2"/>'),
|
||
| c = paper.circle(10, 10, 10).attr({
|
||
| filter: f
|
||
| });
|
||
*/
|
||
filter(filstr) {
|
||
let paper = this
|
||
if (paper.type !== 'svg') {
|
||
paper = paper.paper
|
||
}
|
||
let f = Snap.parse(String(filstr)),
|
||
id = uuid((this.type || '') + 'S'),
|
||
width = paper.node.offsetWidth,
|
||
height = paper.node.offsetHeight,
|
||
filter = $('filter')
|
||
$(filter, {
|
||
id: id,
|
||
filterUnits: 'userSpaceOnUse'
|
||
})
|
||
filter.appendChild(f.node)
|
||
paper.defs.appendChild(filter)
|
||
return new SnapElement(filter)
|
||
}
|
||
|
||
toString() {
|
||
let doc = this.node.ownerDocument,
|
||
f = doc.createDocumentFragment(),
|
||
d = doc.createElement('div'),
|
||
svg = this.node.cloneNode(true),
|
||
res
|
||
f.appendChild(d)
|
||
d.appendChild(svg)
|
||
$(svg, { xmlns: 'http://www.w3.org/2000/svg' })
|
||
res = d.innerHTML
|
||
return res
|
||
}
|
||
|
||
/*\
|
||
|
||
**
|
||
* Removes all child nodes of the paper, except <defs>.
|
||
*/
|
||
clear() {
|
||
let node = this.node.firstChild,
|
||
next
|
||
while (node) {
|
||
next = node.nextSibling
|
||
if (node.tagName != 'defs') {
|
||
node.parentNode.removeChild(node)
|
||
} else {
|
||
this.clear.call({ node: node })
|
||
}
|
||
node = next
|
||
}
|
||
}
|
||
}
|
||
|
||
export class SnapElement {
|
||
constructor(el) {
|
||
if (el.snap in hub) {
|
||
return hub[el.snap]
|
||
}
|
||
let svg
|
||
try {
|
||
svg = el.ownerSVGElement
|
||
} catch (e) {}
|
||
/*\
|
||
**
|
||
* 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
|
||
| let c = paper.circle(10, 10, 10);
|
||
| c.node.onclick = function () {
|
||
| c.attr("fill", "red");
|
||
| };
|
||
\*/
|
||
this.node = el
|
||
if (svg) {
|
||
this.paper = new Paper(svg)
|
||
}
|
||
/*\
|
||
* SnapElement.type
|
||
[ property (string) ]
|
||
**
|
||
* SVG tag name of the given element.
|
||
\*/
|
||
this.type = el.tagName || el.nodeName
|
||
let id = (this.id = uuid((this.type || '') + 'S'))
|
||
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 }) {
|
||
extend(this, Paper.prototype)
|
||
}
|
||
}
|
||
|
||
/*
|
||
**
|
||
* 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
|
||
= (SnapElement) 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"`.
|
||
*/
|
||
attr(params, value) {
|
||
let node = this.node
|
||
|
||
if (!params) {
|
||
if (node.nodeType != 1) {
|
||
return {
|
||
text: node.nodeValue
|
||
}
|
||
}
|
||
let attr = node.attributes,
|
||
out = {}
|
||
for (let i = 0, ii = attr.length; i < ii; i++) {
|
||
out[attr[i].nodeName] = attr[i].nodeValue
|
||
}
|
||
|
||
return out
|
||
}
|
||
if (is(params, 'string')) {
|
||
if (value !== void 0) {
|
||
params = { [params]: value }
|
||
} else {
|
||
return eve('snap.util.getattr.' + params, this).firstDefined()
|
||
}
|
||
}
|
||
for (let att in params) {
|
||
eve('snap.util.attr.' + att, this, params[att])
|
||
}
|
||
return this
|
||
}
|
||
|
||
/*
|
||
**
|
||
* Returns array of all the children of the element.
|
||
= (array) array of Elements
|
||
*/
|
||
children() {
|
||
let out = [],
|
||
ch = this.node.childNodes
|
||
for (let i = 0, ii = ch.length; i < ii; i++) {
|
||
out[i] = new Snap(ch[i])
|
||
}
|
||
return out
|
||
}
|
||
|
||
/*\
|
||
**
|
||
* 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 }
|
||
\*/
|
||
toJSON() {
|
||
let out = []
|
||
jsonFiller([this], out)
|
||
return out[0]
|
||
}
|
||
|
||
equal(name, b) {
|
||
return eve('snap.util.equal', this, name, b).firstDefined()
|
||
}
|
||
|
||
addClass(value) {
|
||
this.node.classList.add(value)
|
||
return this
|
||
}
|
||
|
||
removeClass(value) {
|
||
this.node.classList.remove(value)
|
||
return this
|
||
}
|
||
|
||
hasClass(value) {
|
||
return this.node.classList.contains(value)
|
||
}
|
||
|
||
toggleClass(value, flag) {
|
||
this.node.classList.toggle(value, flag)
|
||
return this
|
||
}
|
||
|
||
getAlign(way = 'center') {
|
||
let bx = this.paper.getBBox()
|
||
let bb = this.getBBox()
|
||
let out = {
|
||
toString() {
|
||
return 'T' + this.dx + ',' + this.dy
|
||
}
|
||
}
|
||
|
||
switch (way.toLowerCase()) {
|
||
case 'top':
|
||
out.dx = 0
|
||
out.dy = bx.y - bb.y
|
||
break
|
||
case 'bottom':
|
||
out.dx = 0
|
||
out.dy = bx.y2 - bb.y2
|
||
break
|
||
case 'middle':
|
||
out.dx = 0
|
||
out.dy = bx.cy - bb.cy
|
||
break
|
||
case 'left':
|
||
out.dx = bx.x - bb.x
|
||
out.dy = 0
|
||
break
|
||
case 'right':
|
||
out.dx = bx.x2 - bb.x2
|
||
out.dy = 0
|
||
break
|
||
case 'center':
|
||
out.dx = bx.cx - bb.cx
|
||
out.dy = 0
|
||
break
|
||
}
|
||
return out
|
||
}
|
||
|
||
align(way) {
|
||
return this.transform('' + this.getAlign(way))
|
||
}
|
||
}
|
||
|
||
// default
|
||
eve.on('snap.util.getattr', function () {
|
||
let key = eve.nt().split('.').at(-1)
|
||
let css = key.replace(/[A-Z]/g, function (letter) {
|
||
return '-' + letter.toLowerCase()
|
||
})
|
||
if (CSS_ATTR[css]) {
|
||
return this.node.ownerDocument.defaultView
|
||
.getComputedStyle(this.node, null)
|
||
.getPropertyValue(css)
|
||
} else {
|
||
return $(this.node, key)
|
||
}
|
||
})
|
||
|
||
eve.on('snap.util.attr', function (value) {
|
||
let key = eve.nt().split('.').at(-1)
|
||
let attr = { [key]: value }
|
||
|
||
let css = key.replace(/[A-Z]/g, function (letter) {
|
||
return '-' + letter.toLowerCase()
|
||
})
|
||
if (CSS_ATTR[css]) {
|
||
this.node.style[css] = value == null ? E : value
|
||
} else {
|
||
$(this.node, attr)
|
||
}
|
||
})
|