snapsvg/src/svg.js

1437 lines
33 KiB
JavaScript
Raw Normal View History

2024-03-05 13:02:24 +08:00
// 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'
2024-03-06 18:36:03 +08:00
import {
doc,
win,
CSS_ATTR,
xmlns,
T_COMMAND,
PATH_VALUES
} from './lib/constants.js'
import {
$,
is,
uuid,
clone,
preload,
cacher,
jsonFiller,
extend
} from './utils.js'
let has = 'hasOwnProperty',
2024-03-05 13:02:24 +08:00
math = Math,
abs = math.abs,
E = '',
S = ' ',
objectToString = Object.prototype.toString,
2024-03-06 18:36:03 +08:00
hub = {}
2024-03-05 13:02:24 +08:00
2024-03-06 18:36:03 +08:00
export function make(name, parent) {
let res = $(name)
parent.appendChild(res)
let el = wrap(res)
2024-03-05 13:02:24 +08:00
return el
}
2024-03-06 18:36:03 +08:00
export function wrap(dom) {
if (!dom) {
return dom
2024-03-05 13:02:24 +08:00
}
2024-03-06 18:36:03 +08:00
if (dom instanceof SnapElement || dom instanceof Fragment) {
return dom
}
if (dom.tagName.toLowerCase() === 'svg') {
return new Paper(dom)
2024-03-05 13:02:24 +08:00
}
if (
2024-03-06 18:36:03 +08:00
dom.tagName &&
dom.tagName.toLowerCase() == 'object' &&
dom.type == 'image/svg+xml'
2024-03-05 13:02:24 +08:00
) {
2024-03-06 18:36:03 +08:00
return new Paper(dom.contentDocument.getElementsByTagName('svg')[0])
2024-03-05 13:02:24 +08:00
}
2024-03-06 18:36:03 +08:00
return new SnapElement(dom)
2024-03-05 13:02:24 +08:00
}
2024-03-06 18:36:03 +08:00
export class Fragment {
constructor(frag) {
this.node = frag
2024-03-05 13:02:24 +08:00
}
}
/*\
**
2024-03-06 18:36:03 +08:00
* Creates a drawing surface or wraps existing SVG element.
2024-03-05 13:02:24 +08:00
**
2024-03-06 18:36:03 +08:00
- 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
2024-03-05 13:02:24 +08:00
\*/
2024-03-06 18:36:03 +08:00
export class Snap {
static _ = { $ }
/*\
2024-03-05 13:02:24 +08:00
**
2024-03-06 18:36:03 +08:00
* Parses SVG fragment and converts it into a @Fragment
2024-03-05 13:02:24 +08:00
**
\*/
2024-03-06 18:36:03 +08:00
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)
}
/*\
2024-03-05 13:02:24 +08:00
**
2024-03-06 18:36:03 +08:00
* Creates a DOM fragment from a given list of elements or strings
2024-03-05 13:02:24 +08:00
**
2024-03-06 18:36:03 +08:00
- varargs () SVG string
2024-03-05 13:02:24 +08:00
\*/
2024-03-06 18:36:03 +08:00
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)
}
/*\
2024-03-05 13:02:24 +08:00
**
* Returns closest point to a given one on a given path.
2024-03-06 18:36:03 +08:00
- path (SnapElement) path element
2024-03-05 13:02:24 +08:00
- 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
}
\*/
2024-03-06 18:36:03 +08:00
// 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
2024-03-05 13:02:24 +08:00
2024-03-06 18:36:03 +08:00
// linear scan for coarse approximation
for (
let scan, scanLength = 0, scanDistance;
scanLength <= pathLength;
scanLength += precision
2024-03-05 13:02:24 +08:00
) {
2024-03-06 18:36:03 +08:00
if (
(scanDistance = distance2(
(scan = pathNode.getPointAtLength(scanLength))
)) < bestDistance
) {
best = scan
bestLength = scanLength
bestDistance = scanDistance
}
2024-03-05 13:02:24 +08:00
}
2024-03-06 18:36:03 +08:00
// 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
}
2024-03-05 13:02:24 +08:00
}
2024-03-06 18:36:03 +08:00
best = {
x: best.x,
y: best.y,
length: bestLength,
distance: Math.sqrt(bestDistance)
}
return best
2024-03-05 13:02:24 +08:00
}
2024-03-06 18:36:03 +08:00
/*\
2024-03-05 13:02:24 +08:00
**
* 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
\*/
2024-03-06 18:36:03 +08:00
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
2024-03-05 13:02:24 +08:00
}
}
2024-03-06 18:36:03 +08:00
return value
2024-03-05 13:02:24 +08:00
}
2024-03-06 18:36:03 +08:00
/*\
2024-03-05 13:02:24 +08:00
**
2024-03-06 18:36:03 +08:00
* Wraps a DOM element specified by CSS selector as @SnapElement
- query (string) CSS selector of the element
= (SnapElement) the current element
2024-03-05 13:02:24 +08:00
\*/
2024-03-06 18:36:03 +08:00
static select(query = '') {
query = query.replace(/([^\\]):/g, '$1\\:')
return wrap(doc.querySelector(query))
2024-03-05 13:02:24 +08:00
}
2024-03-06 18:36:03 +08:00
/*\
2024-03-05 13:02:24 +08:00
**
2024-03-06 18:36:03 +08:00
* Wraps DOM elements specified by CSS selector as set or array of @SnapElement
- query (string) CSS selector of the element
= (SnapElement) the current element
2024-03-05 13:02:24 +08:00
\*/
2024-03-06 18:36:03 +08:00
static selectAll(query = '') {
let nodelist = doc.querySelectorAll(query),
set = (Snap.set || Array)()
for (let i = 0; i < nodelist.length; i++) {
set.push(wrap(nodelist[i]))
2024-03-05 13:02:24 +08:00
}
2024-03-06 18:36:03 +08:00
return set
2024-03-05 13:02:24 +08:00
}
2024-03-06 18:36:03 +08:00
/*\
2024-03-05 13:02:24 +08:00
**
2024-03-06 18:36:03 +08:00
* Returns you topmost element under given point.
2024-03-05 13:02:24 +08:00
**
2024-03-06 18:36:03 +08:00
= (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"});
2024-03-05 13:02:24 +08:00
\*/
2024-03-06 18:36:03 +08:00
static getElementByPoint(x, y) {
let paper = this,
svg = paper.canvas,
target = doc.elementFromPoint(x, y)
2024-03-05 13:02:24 +08:00
2024-03-06 18:36:03 +08:00
if (!target) {
return null
}
return wrap(target)
}
2024-03-05 13:02:24 +08:00
2024-03-06 18:36:03 +08:00
constructor(w, h) {
if (w) {
if (w.nodeType) {
return wrap(w)
}
if (is(w, 'array') && Snap.set) {
return Snap.set.apply(Snap, w)
}
if (w instanceof 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)
}
2024-03-05 13:02:24 +08:00
}
// 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
}
2024-03-06 18:36:03 +08:00
let pth = Snap.path(pathString)
2024-03-05 13:02:24 +08:00
if (pth.arr) {
return Snap.path.clone(pth.arr)
}
2024-03-06 18:36:03 +08:00
let paramCounts = {
2024-03-05 13:02:24 +08:00
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) {
2024-03-06 18:36:03 +08:00
String(pathString).replace(pathCommand, function (a, b, c) {
let params = [],
2024-03-05 13:02:24 +08:00
name = b.toLowerCase()
2024-03-06 18:36:03 +08:00
c.replace(PATH_VALUES, function (a, b) {
2024-03-05 13:02:24 +08:00
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
\*/
2024-03-06 18:36:03 +08:00
let parseTransformString = (Snap.parseTransformString = function (TString) {
2024-03-05 13:02:24 +08:00
if (!TString) {
return null
}
2024-03-06 18:36:03 +08:00
let paramCounts = { r: 3, s: 4, t: 2, m: 6 },
2024-03-05 13:02:24 +08:00
data = []
if (is(TString, 'array') && is(TString[0], 'array')) {
// rough assumption
data = Snap.path.clone(TString)
}
if (!data.length) {
2024-03-06 18:36:03 +08:00
String(TString).replace(T_COMMAND, function (a, b, c) {
let params = [],
2024-03-05 13:02:24 +08:00
name = b.toLowerCase()
2024-03-06 18:36:03 +08:00
c.replace(PATH_VALUES, function (a, b) {
2024-03-05 13:02:24 +08:00
b && params.push(+b)
})
data.push([b].concat(params))
})
}
data.toString = Snap.path.toString
return data
})
function svgTransform2string(tstr) {
2024-03-06 18:36:03 +08:00
let res = []
2024-03-05 13:02:24 +08:00
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
2024-03-06 18:36:03 +08:00
2024-03-05 13:02:24 +08:00
function transform2matrix(tstr, bbox) {
2024-03-06 18:36:03 +08:00
let tdata = parseTransformString(tstr),
2024-03-05 13:02:24 +08:00
m = new Snap.Matrix()
if (tdata) {
2024-03-06 18:36:03 +08:00
for (let i = 0, ii = tdata.length; i < ii; i++) {
let t = tdata[i],
2024-03-05 13:02:24 +08:00
tlen = t.length,
2024-03-06 18:36:03 +08:00
command = String(t[0]).toLowerCase(),
2024-03-05 13:02:24 +08:00
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
}
2024-03-06 18:36:03 +08:00
2024-03-05 13:02:24 +08:00
Snap._.transform2matrix = transform2matrix
2024-03-06 18:36:03 +08:00
export function getSomeDefs(el) {
let p =
2024-03-05 13:02:24 +08:00
(el.node.ownerSVGElement && wrap(el.node.ownerSVGElement)) ||
(el.node.parentNode && wrap(el.node.parentNode)) ||
Snap.select('svg') ||
2024-03-06 18:36:03 +08:00
new Snap(0, 0),
2024-03-05 13:02:24 +08:00
pdefs = p.select('defs'),
defs = pdefs == null ? false : pdefs.node
if (!defs) {
defs = make('defs', p.node).node
}
return defs
}
2024-03-06 18:36:03 +08:00
2024-03-05 13:02:24 +08:00
function getSomeSVG(el) {
return (
(el.node.ownerSVGElement && wrap(el.node.ownerSVGElement)) ||
Snap.select('svg')
)
}
2024-03-06 18:36:03 +08:00
export function unit2px(el, name, value) {
let svg = getSomeSVG(el).node,
2024-03-05 13:02:24 +08:00
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)
}
2024-03-06 18:36:03 +08:00
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 its 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)
}
/*\
2024-03-05 13:02:24 +08:00
**
2024-03-06 18:36:03 +08:00
* Creates an element on paper with a given name and no attributes
2024-03-05 13:02:24 +08:00
**
2024-03-06 18:36:03 +08:00
- 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
| });
2024-03-05 13:02:24 +08:00
\*/
2024-03-06 18:36:03 +08:00
el(name, attr) {
let el = make(name, this.node)
attr && el.attr(attr)
return el
2024-03-05 13:02:24 +08:00
}
2024-03-06 18:36:03 +08:00
/*\
*
* 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
2024-03-05 13:02:24 +08:00
}
2024-03-06 18:36:03 +08:00
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
2024-03-05 13:02:24 +08:00
}
}
2024-03-06 18:36:03 +08:00
return this.el('rect', attr)
2024-03-05 13:02:24 +08:00
}
2024-03-06 18:36:03 +08:00
2024-03-05 13:02:24 +08:00
/*\
**
2024-03-06 18:36:03 +08:00
* Draws a circle
**
- x (number) x coordinate of the centre
- y (number) y coordinate of the centre
- r (number) radius
= (object) the `circle` element
**
2024-03-05 13:02:24 +08:00
> Usage
2024-03-06 18:36:03 +08:00
| let c = paper.circle(50, 50, 40);
2024-03-05 13:02:24 +08:00
\*/
2024-03-06 18:36:03 +08:00
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)
2024-03-05 13:02:24 +08:00
}
2024-03-06 18:36:03 +08:00
2024-03-05 13:02:24 +08:00
/*\
**
2024-03-06 18:36:03 +08:00
* 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);
2024-03-05 13:02:24 +08:00
\*/
2024-03-06 18:36:03 +08:00
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
})
})
2024-03-05 13:02:24 +08:00
}
2024-03-06 18:36:03 +08:00
$(el.node, set)
}
return el
2024-03-05 13:02:24 +08:00
}
2024-03-06 18:36:03 +08:00
/*\
2024-03-05 13:02:24 +08:00
**
2024-03-06 18:36:03 +08:00
* 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
2024-03-05 13:02:24 +08:00
**
> Usage
2024-03-06 18:36:03 +08:00
| let c = paper.ellipse(50, 50, 40, 20);
2024-03-05 13:02:24 +08:00
\*/
2024-03-06 18:36:03 +08:00
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
2024-03-05 13:02:24 +08:00
}
}
2024-03-06 18:36:03 +08:00
return this.el('ellipse', attr)
}
path(d) {
let attr
if (is(d, 'object') && !is(d, 'array')) {
attr = d
} else if (d) {
attr = { d: d }
2024-03-05 13:02:24 +08:00
}
2024-03-06 18:36:03 +08:00
return this.el('path', attr)
2024-03-05 13:02:24 +08:00
}
2024-03-06 18:36:03 +08:00
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))
2024-03-05 13:02:24 +08:00
}
2024-03-06 18:36:03 +08:00
return el
2024-03-05 13:02:24 +08:00
}
2024-03-06 18:36:03 +08:00
/*\
**
* 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
2024-03-05 13:02:24 +08:00
}
2024-03-06 18:36:03 +08:00
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)
2024-03-05 13:02:24 +08:00
}
2024-03-06 18:36:03 +08:00
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
2024-03-05 13:02:24 +08:00
}
2024-03-06 18:36:03 +08:00
/*\
**
* 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 })
2024-03-05 13:02:24 +08:00
}
2024-03-06 18:36:03 +08:00
node = next
2024-03-05 13:02:24 +08:00
}
}
}
2024-03-06 18:36:03 +08:00
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: []
2024-03-05 13:02:24 +08:00
}
2024-03-06 18:36:03 +08:00
el.snap = id
hub[id] = this
if (this.type == 'g') {
this.add = add2group
2024-03-05 13:02:24 +08:00
}
2024-03-06 18:36:03 +08:00
if (this.type in { g: 1, mask: 1, pattern: 1, symbol: 1 }) {
extend(this, Paper.prototype)
2024-03-05 13:02:24 +08:00
}
}
2024-03-06 18:36:03 +08:00
/*
**
* 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
2024-03-05 13:02:24 +08:00
}
2024-03-06 18:36:03 +08:00
if (is(params, 'string')) {
if (value !== void 0) {
params = { [params]: value }
} else {
return eve('snap.util.getattr.' + params, this).firstDefined()
}
2024-03-05 13:02:24 +08:00
}
2024-03-06 18:36:03 +08:00
for (let att in params) {
eve('snap.util.attr.' + att, this, params[att])
2024-03-05 13:02:24 +08:00
}
2024-03-06 18:36:03 +08:00
return this
2024-03-05 13:02:24 +08:00
}
2024-03-06 18:36:03 +08:00
/*
**
* 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])
2024-03-05 13:02:24 +08:00
}
2024-03-06 18:36:03 +08:00
return out
2024-03-05 13:02:24 +08:00
}
2024-03-06 18:36:03 +08:00
/*\
2024-03-05 13:02:24 +08:00
**
* 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 }
\*/
2024-03-06 18:36:03 +08:00
toJSON() {
let out = []
jsonFiller([this], out)
return out[0]
}
2024-03-07 14:38:37 +08:00
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))
}
2024-03-05 13:02:24 +08:00
}
2024-03-06 18:36:03 +08:00
2024-03-05 13:02:24 +08:00
// default
eve.on('snap.util.getattr', function () {
2024-03-06 18:36:03 +08:00
let key = eve.nt().split('.').at(-1)
let css = key.replace(/[A-Z]/g, function (letter) {
2024-03-05 13:02:24 +08:00
return '-' + letter.toLowerCase()
})
2024-03-06 18:36:03 +08:00
if (CSS_ATTR[css]) {
2024-03-05 13:02:24 +08:00
return this.node.ownerDocument.defaultView
.getComputedStyle(this.node, null)
.getPropertyValue(css)
} else {
2024-03-06 18:36:03 +08:00
return $(this.node, key)
2024-03-05 13:02:24 +08:00
}
})
eve.on('snap.util.attr', function (value) {
2024-03-06 18:36:03 +08:00
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
2024-03-05 13:02:24 +08:00
} else {
$(this.node, attr)
}
})