snapsvg/src/svg.js

1417 lines
32 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* {}
* @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)
}
})
JavaScript 100%