@antv/x6
Version:
JavaScript diagramming library that uses SVG and HTML for rendering.
418 lines • 15.5 kB
JavaScript
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
import { Point, Line, Rectangle, Polyline, Ellipse, Path } from '../../geometry';
import { attr } from './attr';
import { sample, toPath, getPointsFromSvgElement } from './path';
import { ensureId, isSVGGraphicsElement, createSvgElement, isHTMLElement, } from './elem';
import { getComputedStyle } from './style';
import { createSVGPoint, createSVGMatrix, decomposeMatrix, transformRectangle, transformStringToMatrix, } from './matrix';
/**
* Returns the bounding box of the element after transformations are
* applied. If `withoutTransformations` is `true`, transformations of
* the element will not be considered when computing the bounding box.
* If `target` is specified, bounding box will be computed relatively
* to the `target` element.
*/
export function bbox(elem, withoutTransformations, target) {
let box;
const ownerSVGElement = elem.ownerSVGElement;
// If the element is not in the live DOM, it does not have a bounding
// box defined and so fall back to 'zero' dimension element.
if (!ownerSVGElement) {
return new Rectangle(0, 0, 0, 0);
}
try {
box = elem.getBBox();
}
catch (e) {
// Fallback for IE.
box = {
x: elem.clientLeft,
y: elem.clientTop,
width: elem.clientWidth,
height: elem.clientHeight,
};
}
if (withoutTransformations) {
return Rectangle.create(box);
}
const matrix = getTransformToElement(elem, target || ownerSVGElement);
return transformRectangle(box, matrix);
}
/**
* Returns the bounding box of the element after transformations are
* applied. Unlike `bbox()`, this function fixes a browser implementation
* bug to return the correct bounding box if this elemenent is a group of
* svg elements (if `options.recursive` is specified).
*/
export function getBBox(elem, options = {}) {
let outputBBox;
const ownerSVGElement = elem.ownerSVGElement;
// If the element is not in the live DOM, it does not have a bounding box
// defined and so fall back to 'zero' dimension element.
// If the element is not an SVGGraphicsElement, we could not measure the
// bounding box either
if (!ownerSVGElement || !isSVGGraphicsElement(elem)) {
if (isHTMLElement(elem)) {
// If the element is a HTMLElement, return the position relative to the body
const { left, top, width, height } = getBoundingOffsetRect(elem);
return new Rectangle(left, top, width, height);
}
return new Rectangle(0, 0, 0, 0);
}
let target = options.target;
const recursive = options.recursive;
if (!recursive) {
try {
outputBBox = elem.getBBox();
}
catch (e) {
outputBBox = {
x: elem.clientLeft,
y: elem.clientTop,
width: elem.clientWidth,
height: elem.clientHeight,
};
}
if (!target) {
return Rectangle.create(outputBBox);
}
// transform like target
const matrix = getTransformToElement(elem, target);
return transformRectangle(outputBBox, matrix);
}
// recursive
{
const children = elem.childNodes;
const n = children.length;
if (n === 0) {
return getBBox(elem, { target });
}
if (!target) {
target = elem; // eslint-disable-line
}
for (let i = 0; i < n; i += 1) {
const child = children[i];
let childBBox;
if (child.childNodes.length === 0) {
childBBox = getBBox(child, { target });
}
else {
// if child is a group element, enter it with a recursive call
childBBox = getBBox(child, { target, recursive: true });
}
if (!outputBBox) {
outputBBox = childBBox;
}
else {
outputBBox = outputBBox.union(childBBox);
}
}
return outputBBox;
}
}
// BBox is calculated by the attribute on the node
export function getBBoxByElementAttr(elem) {
let node = elem;
let tagName = node ? node.tagName.toLowerCase() : '';
// find shape node
while (tagName === 'g') {
node = node.firstElementChild;
tagName = node ? node.tagName.toLowerCase() : '';
}
const attr = (name) => {
const s = node.getAttribute(name);
const v = s ? parseFloat(s) : 0;
return Number.isNaN(v) ? 0 : v;
};
let r;
let bbox;
switch (tagName) {
case 'rect':
bbox = new Rectangle(attr('x'), attr('y'), attr('width'), attr('height'));
break;
case 'circle':
r = attr('r');
bbox = new Rectangle(attr('cx') - r, attr('cy') - r, 2 * r, 2 * r);
break;
default:
break;
}
return bbox;
}
// Matrix is calculated by the transform attribute on the node
export function getMatrixByElementAttr(elem, target) {
let matrix = createSVGMatrix();
if (isSVGGraphicsElement(target) && isSVGGraphicsElement(elem)) {
let node = elem;
const matrixList = [];
while (node && node !== target) {
const transform = node.getAttribute('transform') || null;
const nodeMatrix = transformStringToMatrix(transform);
matrixList.push(nodeMatrix);
node = node.parentNode;
}
matrixList.reverse().forEach((m) => {
matrix = matrix.multiply(m);
});
}
return matrix;
}
/**
* Returns an DOMMatrix that specifies the transformation necessary
* to convert `elem` coordinate system into `target` coordinate system.
*/
export function getTransformToElement(elem, target) {
if (isSVGGraphicsElement(target) && isSVGGraphicsElement(elem)) {
const targetCTM = target.getScreenCTM();
const nodeCTM = elem.getScreenCTM();
if (targetCTM && nodeCTM) {
return targetCTM.inverse().multiply(nodeCTM);
}
}
// Could not get actual transformation matrix
return createSVGMatrix();
}
/**
* Converts a global point with coordinates `x` and `y` into the
* coordinate space of the element.
*/
export function toLocalPoint(elem, x, y) {
const svg = elem instanceof SVGSVGElement
? elem
: elem.ownerSVGElement;
const p = svg.createSVGPoint();
p.x = x;
p.y = y;
try {
const ctm = svg.getScreenCTM();
const globalPoint = p.matrixTransform(ctm.inverse());
const globalToLocalMatrix = getTransformToElement(elem, svg).inverse();
return globalPoint.matrixTransform(globalToLocalMatrix);
}
catch (e) {
return p;
}
}
/**
* Convert the SVGElement to an equivalent geometric shape. The element's
* transformations are not taken into account.
*
* SVGRectElement => Rectangle
*
* SVGLineElement => Line
*
* SVGCircleElement => Ellipse
*
* SVGEllipseElement => Ellipse
*
* SVGPolygonElement => Polyline
*
* SVGPolylineElement => Polyline
*
* SVGPathElement => Path
*
* others => Rectangle
*/
export function toGeometryShape(elem) {
const attr = (name) => {
const s = elem.getAttribute(name);
const v = s ? parseFloat(s) : 0;
return Number.isNaN(v) ? 0 : v;
};
switch (elem instanceof SVGElement && elem.nodeName.toLowerCase()) {
case 'rect':
return new Rectangle(attr('x'), attr('y'), attr('width'), attr('height'));
case 'circle':
return new Ellipse(attr('cx'), attr('cy'), attr('r'), attr('r'));
case 'ellipse':
return new Ellipse(attr('cx'), attr('cy'), attr('rx'), attr('ry'));
case 'polyline': {
const points = getPointsFromSvgElement(elem);
return new Polyline(points);
}
case 'polygon': {
const points = getPointsFromSvgElement(elem);
if (points.length > 1) {
points.push(points[0]);
}
return new Polyline(points);
}
case 'path': {
let d = elem.getAttribute('d');
if (!Path.isValid(d)) {
d = Path.normalize(d);
}
return Path.parse(d);
}
case 'line': {
return new Line(attr('x1'), attr('y1'), attr('x2'), attr('y2'));
}
default:
break;
}
// Anything else is a rectangle
return getBBox(elem);
}
export function getIntersection(elem, ref, target) {
const svg = elem instanceof SVGSVGElement ? elem : elem.ownerSVGElement;
target = target || svg; // eslint-disable-line
const bbox = getBBox(target);
const center = bbox.getCenter();
if (!bbox.intersectsWithLineFromCenterToPoint(ref)) {
return null;
}
let spot = null;
const tagName = elem.tagName.toLowerCase();
// Little speed up optimization for `<rect>` element. We do not do convert
// to path element and sampling but directly calculate the intersection
// through a transformed geometrical rectangle.
if (tagName === 'rect') {
const gRect = new Rectangle(parseFloat(elem.getAttribute('x') || '0'), parseFloat(elem.getAttribute('y') || '0'), parseFloat(elem.getAttribute('width') || '0'), parseFloat(elem.getAttribute('height') || '0'));
// Get the rect transformation matrix with regards to the SVG document.
const rectMatrix = getTransformToElement(elem, target);
const rectMatrixComponents = decomposeMatrix(rectMatrix);
// Rotate the rectangle back so that we can use
// `intersectsWithLineFromCenterToPoint()`.
const reseted = svg.createSVGTransform();
reseted.setRotate(-rectMatrixComponents.rotation, center.x, center.y);
const rect = transformRectangle(gRect, reseted.matrix.multiply(rectMatrix));
spot = Rectangle.create(rect).intersectsWithLineFromCenterToPoint(ref, rectMatrixComponents.rotation);
}
else if (tagName === 'path' ||
tagName === 'polygon' ||
tagName === 'polyline' ||
tagName === 'circle' ||
tagName === 'ellipse') {
const pathNode = tagName === 'path' ? elem : toPath(elem);
const samples = sample(pathNode);
let minDistance = Infinity;
let closestSamples = [];
for (let i = 0, ii = samples.length; i < ii; i += 1) {
const sample = samples[i];
// Convert the sample point in the local coordinate system
// to the global coordinate system.
let gp = createSVGPoint(sample.x, sample.y);
gp = gp.matrixTransform(getTransformToElement(elem, target));
const ggp = Point.create(gp);
const centerDistance = ggp.distance(center);
// Penalize a higher distance to the reference point by 10%.
// This gives better results. This is due to
// inaccuracies introduced by rounding errors and getPointAtLength() returns.
const refDistance = ggp.distance(ref) * 1.1;
const distance = centerDistance + refDistance;
if (distance < minDistance) {
minDistance = distance;
closestSamples = [{ sample, refDistance }];
}
else if (distance < minDistance + 1) {
closestSamples.push({ sample, refDistance });
}
}
closestSamples.sort((a, b) => a.refDistance - b.refDistance);
if (closestSamples[0]) {
spot = Point.create(closestSamples[0].sample);
}
}
return spot;
}
export function animate(elem, options) {
return createAnimation(elem, options, 'animate');
}
export function animateTransform(elem, options) {
return createAnimation(elem, options, 'animateTransform');
}
function createAnimation(elem, options, type) {
// @see
// https://www.w3.org/TR/SVG11/animate.html#AnimateElement
// https://developer.mozilla.org/en-US/docs/Web/API/SVGAnimateElement
// https://developer.mozilla.org/en-US/docs/Web/API/SVGAnimateTransformElement
const animate = createSvgElement(type);
elem.appendChild(animate);
try {
return setupAnimation(animate, options);
}
catch (error) {
// pass
}
return () => { };
}
function setupAnimation(animate, options) {
const { start, complete, repeat } = options, attrs = __rest(options, ["start", "complete", "repeat"]);
attr(animate, attrs);
start && animate.addEventListener('beginEvent', start);
complete && animate.addEventListener('endEvent', complete);
repeat && animate.addEventListener('repeatEvent', repeat);
const ani = animate;
ani.beginElement();
return () => ani.endElement();
}
/**
* Animate the element along the path SVG element (or Vector object).
* `attrs` contain Animation Timing attributes describing the animation.
*/
export function animateAlongPath(elem, options, path) {
const id = ensureId(path);
// https://developer.mozilla.org/en-US/docs/Web/API/SVGAnimationElement
const animate = createSvgElement('animateMotion');
const mpath = createSvgElement('mpath');
attr(mpath, { 'xlink:href': `#${id}` });
animate.appendChild(mpath);
elem.appendChild(animate);
try {
return setupAnimation(animate, options);
}
catch (e) {
// Fallback for IE 9.
if (document.documentElement.getAttribute('smiling') === 'fake') {
// Register the animation. (See `https://answers.launchpad.net/smil/+question/203333`)
const ani = animate;
ani.animators = [];
const win = window;
const animationID = ani.getAttribute('id');
if (animationID) {
win.id2anim[animationID] = ani;
}
const targets = win.getTargets(ani);
for (let i = 0, ii = targets.length; i < ii; i += 1) {
const target = targets[i];
const animator = new win.Animator(ani, target, i);
win.animators.push(animator);
ani.animators[i] = animator;
animator.register();
}
}
}
return () => { };
}
export function getBoundingOffsetRect(elem) {
let left = 0;
let top = 0;
let width = 0;
let height = 0;
if (elem) {
let current = elem;
while (current) {
left += current.offsetLeft;
top += current.offsetTop;
current = current.offsetParent;
if (current) {
left += parseInt(getComputedStyle(current, 'borderLeft'), 10);
top += parseInt(getComputedStyle(current, 'borderTop'), 10);
}
}
width = elem.offsetWidth;
height = elem.offsetHeight;
}
return { left, top, width, height };
}
//# sourceMappingURL=geom.js.map