working but incomplete version with new build.
|
@ -0,0 +1,285 @@
|
|||
/* globals jQuery */
|
||||
/**
|
||||
* Browser detection.
|
||||
* @module browser
|
||||
* @license MIT
|
||||
*
|
||||
* @copyright 2010 Jeff Schiller, 2010 Alexis Deveria
|
||||
*/
|
||||
|
||||
// Dependencies:
|
||||
// 1) jQuery (for $.alert())
|
||||
|
||||
import './svgpathseg.js';
|
||||
import {NS} from './namespaces.js';
|
||||
|
||||
const $ = jQuery;
|
||||
|
||||
const supportsSVG_ = (function () {
|
||||
return Boolean(document.createElementNS && document.createElementNS(NS.SVG, 'svg').createSVGRect);
|
||||
}());
|
||||
|
||||
/**
|
||||
* @function module:browser.supportsSvg
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const supportsSvg = () => supportsSVG_;
|
||||
|
||||
const {userAgent} = navigator;
|
||||
const svg = document.createElementNS(NS.SVG, 'svg');
|
||||
|
||||
// Note: Browser sniffing should only be used if no other detection method is possible
|
||||
const isOpera_ = Boolean(window.opera);
|
||||
const isWebkit_ = userAgent.includes('AppleWebKit');
|
||||
const isGecko_ = userAgent.includes('Gecko/');
|
||||
const isIE_ = userAgent.includes('MSIE');
|
||||
const isChrome_ = userAgent.includes('Chrome/');
|
||||
const isWindows_ = userAgent.includes('Windows');
|
||||
const isMac_ = userAgent.includes('Macintosh');
|
||||
const isTouch_ = 'ontouchstart' in window;
|
||||
|
||||
const supportsSelectors_ = (function () {
|
||||
return Boolean(svg.querySelector);
|
||||
}());
|
||||
|
||||
const supportsXpath_ = (function () {
|
||||
return Boolean(document.evaluate);
|
||||
}());
|
||||
|
||||
// segList functions (for FF1.5 and 2.0)
|
||||
const supportsPathReplaceItem_ = (function () {
|
||||
const path = document.createElementNS(NS.SVG, 'path');
|
||||
path.setAttribute('d', 'M0,0 10,10');
|
||||
const seglist = path.pathSegList;
|
||||
const seg = path.createSVGPathSegLinetoAbs(5, 5);
|
||||
try {
|
||||
seglist.replaceItem(seg, 1);
|
||||
return true;
|
||||
} catch (err) {}
|
||||
return false;
|
||||
}());
|
||||
|
||||
const supportsPathInsertItemBefore_ = (function () {
|
||||
const path = document.createElementNS(NS.SVG, 'path');
|
||||
path.setAttribute('d', 'M0,0 10,10');
|
||||
const seglist = path.pathSegList;
|
||||
const seg = path.createSVGPathSegLinetoAbs(5, 5);
|
||||
try {
|
||||
seglist.insertItemBefore(seg, 1);
|
||||
return true;
|
||||
} catch (err) {}
|
||||
return false;
|
||||
}());
|
||||
|
||||
// text character positioning (for IE9 and now Chrome)
|
||||
const supportsGoodTextCharPos_ = (function () {
|
||||
const svgroot = document.createElementNS(NS.SVG, 'svg');
|
||||
const svgcontent = document.createElementNS(NS.SVG, 'svg');
|
||||
document.documentElement.append(svgroot);
|
||||
svgcontent.setAttribute('x', 5);
|
||||
svgroot.append(svgcontent);
|
||||
const text = document.createElementNS(NS.SVG, 'text');
|
||||
text.textContent = 'a';
|
||||
svgcontent.append(text);
|
||||
try { // Chrome now fails here
|
||||
const pos = text.getStartPositionOfChar(0).x;
|
||||
return (pos === 0);
|
||||
} catch (err) {
|
||||
return false;
|
||||
} finally {
|
||||
svgroot.remove();
|
||||
}
|
||||
}());
|
||||
|
||||
const supportsPathBBox_ = (function () {
|
||||
const svgcontent = document.createElementNS(NS.SVG, 'svg');
|
||||
document.documentElement.append(svgcontent);
|
||||
const path = document.createElementNS(NS.SVG, 'path');
|
||||
path.setAttribute('d', 'M0,0 C0,0 10,10 10,0');
|
||||
svgcontent.append(path);
|
||||
const bbox = path.getBBox();
|
||||
svgcontent.remove();
|
||||
return (bbox.height > 4 && bbox.height < 5);
|
||||
}());
|
||||
|
||||
// Support for correct bbox sizing on groups with horizontal/vertical lines
|
||||
const supportsHVLineContainerBBox_ = (function () {
|
||||
const svgcontent = document.createElementNS(NS.SVG, 'svg');
|
||||
document.documentElement.append(svgcontent);
|
||||
const path = document.createElementNS(NS.SVG, 'path');
|
||||
path.setAttribute('d', 'M0,0 10,0');
|
||||
const path2 = document.createElementNS(NS.SVG, 'path');
|
||||
path2.setAttribute('d', 'M5,0 15,0');
|
||||
const g = document.createElementNS(NS.SVG, 'g');
|
||||
g.append(path, path2);
|
||||
svgcontent.append(g);
|
||||
const bbox = g.getBBox();
|
||||
svgcontent.remove();
|
||||
// Webkit gives 0, FF gives 10, Opera (correctly) gives 15
|
||||
return (bbox.width === 15);
|
||||
}());
|
||||
|
||||
const supportsEditableText_ = (function () {
|
||||
// TODO: Find better way to check support for this
|
||||
return isOpera_;
|
||||
}());
|
||||
|
||||
const supportsGoodDecimals_ = (function () {
|
||||
// Correct decimals on clone attributes (Opera < 10.5/win/non-en)
|
||||
const rect = document.createElementNS(NS.SVG, 'rect');
|
||||
rect.setAttribute('x', 0.1);
|
||||
const crect = rect.cloneNode(false);
|
||||
const retValue = (!crect.getAttribute('x').includes(','));
|
||||
if (!retValue) {
|
||||
// Todo: i18nize or remove
|
||||
$.alert(
|
||||
'NOTE: This version of Opera is known to contain bugs in SVG-edit.\n' +
|
||||
'Please upgrade to the <a href="http://opera.com">latest version</a> in which the problems have been fixed.'
|
||||
);
|
||||
}
|
||||
return retValue;
|
||||
}());
|
||||
|
||||
const supportsNonScalingStroke_ = (function () {
|
||||
const rect = document.createElementNS(NS.SVG, 'rect');
|
||||
rect.setAttribute('style', 'vector-effect:non-scaling-stroke');
|
||||
return rect.style.vectorEffect === 'non-scaling-stroke';
|
||||
}());
|
||||
|
||||
let supportsNativeSVGTransformLists_ = (function () {
|
||||
const rect = document.createElementNS(NS.SVG, 'rect');
|
||||
const rxform = rect.transform.baseVal;
|
||||
const t1 = svg.createSVGTransform();
|
||||
rxform.appendItem(t1);
|
||||
const r1 = rxform.getItem(0);
|
||||
const isSVGTransform = (o) => {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/SVGTransform
|
||||
return o && typeof o === 'object' && typeof o.setMatrix === 'function' && 'angle' in o;
|
||||
};
|
||||
return isSVGTransform(r1) && isSVGTransform(t1) &&
|
||||
r1.type === t1.type && r1.angle === t1.angle &&
|
||||
r1.matrix.a === t1.matrix.a &&
|
||||
r1.matrix.b === t1.matrix.b &&
|
||||
r1.matrix.c === t1.matrix.c &&
|
||||
r1.matrix.d === t1.matrix.d &&
|
||||
r1.matrix.e === t1.matrix.e &&
|
||||
r1.matrix.f === t1.matrix.f;
|
||||
}());
|
||||
|
||||
// Public API
|
||||
|
||||
/**
|
||||
* @function module:browser.isOpera
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isOpera = () => isOpera_;
|
||||
/**
|
||||
* @function module:browser.isWebkit
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isWebkit = () => isWebkit_;
|
||||
/**
|
||||
* @function module:browser.isGecko
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isGecko = () => isGecko_;
|
||||
/**
|
||||
* @function module:browser.isIE
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isIE = () => isIE_;
|
||||
/**
|
||||
* @function module:browser.isChrome
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isChrome = () => isChrome_;
|
||||
/**
|
||||
* @function module:browser.isWindows
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isWindows = () => isWindows_;
|
||||
/**
|
||||
* @function module:browser.isMac
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isMac = () => isMac_;
|
||||
/**
|
||||
* @function module:browser.isTouch
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isTouch = () => isTouch_;
|
||||
|
||||
/**
|
||||
* @function module:browser.supportsSelectors
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const supportsSelectors = () => supportsSelectors_;
|
||||
|
||||
/**
|
||||
* @function module:browser.supportsXpath
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const supportsXpath = () => supportsXpath_;
|
||||
|
||||
/**
|
||||
* @function module:browser.supportsPathReplaceItem
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const supportsPathReplaceItem = () => supportsPathReplaceItem_;
|
||||
|
||||
/**
|
||||
* @function module:browser.supportsPathInsertItemBefore
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const supportsPathInsertItemBefore = () => supportsPathInsertItemBefore_;
|
||||
|
||||
/**
|
||||
* @function module:browser.supportsPathBBox
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const supportsPathBBox = () => supportsPathBBox_;
|
||||
|
||||
/**
|
||||
* @function module:browser.supportsHVLineContainerBBox
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const supportsHVLineContainerBBox = () => supportsHVLineContainerBBox_;
|
||||
|
||||
/**
|
||||
* @function module:browser.supportsGoodTextCharPos
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const supportsGoodTextCharPos = () => supportsGoodTextCharPos_;
|
||||
|
||||
/**
|
||||
* @function module:browser.supportsEditableText
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const supportsEditableText = () => supportsEditableText_;
|
||||
|
||||
/**
|
||||
* @function module:browser.supportsGoodDecimals
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const supportsGoodDecimals = () => supportsGoodDecimals_;
|
||||
|
||||
/**
|
||||
* @function module:browser.supportsNonScalingStroke
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const supportsNonScalingStroke = () => supportsNonScalingStroke_;
|
||||
|
||||
/**
|
||||
* @function module:browser.supportsNativeTransformLists
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const supportsNativeTransformLists = () => supportsNativeSVGTransformLists_;
|
||||
|
||||
/**
|
||||
* Set `supportsNativeSVGTransformLists_` to `false` (for unit testing).
|
||||
* @function module:browser.disableSupportsNativeTransformLists
|
||||
* @returns {void}
|
||||
*/
|
||||
export const disableSupportsNativeTransformLists = () => {
|
||||
supportsNativeSVGTransformLists_ = false;
|
||||
};
|
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* A jQuery module to work with SVG attributes.
|
||||
* @module jQueryAttr
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* This fixes `$(...).attr()` to work as expected with SVG elements.
|
||||
* Does not currently use `*AttributeNS()` since we rarely need that.
|
||||
* Adds {@link external:jQuery.fn.attr}.
|
||||
* See {@link https://api.jquery.com/attr/} for basic documentation of `.attr()`.
|
||||
*
|
||||
* Additional functionality:
|
||||
* - When getting attributes, a string that's a number is returned as type number.
|
||||
* - If an array is supplied as the first parameter, multiple values are returned
|
||||
* as an object with values for each given attribute.
|
||||
* @function module:jQueryAttr.jQueryAttr
|
||||
* @param {external:jQuery} $ The jQuery object to which to add the plug-in
|
||||
* @returns {external:jQuery}
|
||||
*/
|
||||
export default function jQueryPluginSVG ($) {
|
||||
const proxied = $.fn.attr,
|
||||
svgns = 'http://www.w3.org/2000/svg';
|
||||
/**
|
||||
* @typedef {PlainObject<string, string|Float>} module:jQueryAttr.Attributes
|
||||
*/
|
||||
/**
|
||||
* @function external:jQuery.fn.attr
|
||||
* @param {string|string[]|PlainObject<string, string>} key
|
||||
* @param {string} value
|
||||
* @returns {external:jQuery|module:jQueryAttr.Attributes}
|
||||
*/
|
||||
$.fn.attr = function (key, value) {
|
||||
const len = this.length;
|
||||
if (!len) { return proxied.call(this, key, value); }
|
||||
for (let i = 0; i < len; ++i) {
|
||||
const elem = this[i];
|
||||
// set/get SVG attribute
|
||||
if (elem.namespaceURI === svgns) {
|
||||
// Setting attribute
|
||||
if (value !== undefined) {
|
||||
elem.setAttribute(key, value);
|
||||
} else if (Array.isArray(key)) {
|
||||
// Getting attributes from array
|
||||
const obj = {};
|
||||
let j = key.length;
|
||||
|
||||
while (j--) {
|
||||
const aname = key[j];
|
||||
let attr = elem.getAttribute(aname);
|
||||
// This returns a number when appropriate
|
||||
if (attr || attr === '0') {
|
||||
attr = isNaN(attr) ? attr : (attr - 0);
|
||||
}
|
||||
obj[aname] = attr;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
if (typeof key === 'object') {
|
||||
// Setting attributes from object
|
||||
for (const [name, val] of Object.entries(key)) {
|
||||
elem.setAttribute(name, val);
|
||||
}
|
||||
// Getting attribute
|
||||
} else {
|
||||
let attr = elem.getAttribute(key);
|
||||
if (attr || attr === '0') {
|
||||
attr = isNaN(attr) ? attr : (attr - 0);
|
||||
}
|
||||
return attr;
|
||||
}
|
||||
} else {
|
||||
return proxied.call(this, key, value);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
};
|
||||
return $;
|
||||
}
|
|
@ -0,0 +1,222 @@
|
|||
/* globals jQuery */
|
||||
/**
|
||||
* Provides tools for the layer concept.
|
||||
* @module layer
|
||||
* @license MIT
|
||||
*
|
||||
* @copyright 2011 Jeff Schiller, 2016 Flint O'Brien
|
||||
*/
|
||||
|
||||
import {NS} from './namespaces.js';
|
||||
import {toXml, walkTree, isNullish} from './utilities.js';
|
||||
|
||||
const $ = jQuery;
|
||||
|
||||
/**
|
||||
* This class encapsulates the concept of a layer in the drawing. It can be constructed with
|
||||
* an existing group element or, with three parameters, will create a new layer group element.
|
||||
*
|
||||
* @example
|
||||
* const l1 = new Layer('name', group); // Use the existing group for this layer.
|
||||
* const l2 = new Layer('name', group, svgElem); // Create a new group and add it to the DOM after group.
|
||||
* const l3 = new Layer('name', null, svgElem); // Create a new group and add it to the DOM as the last layer.
|
||||
* @memberof module:layer
|
||||
*/
|
||||
class Layer {
|
||||
/**
|
||||
* @param {string} name - Layer name
|
||||
* @param {SVGGElement|null} group - An existing SVG group element or null.
|
||||
* If group and no svgElem, use group for this layer.
|
||||
* If group and svgElem, create a new group element and insert it in the DOM after group.
|
||||
* If no group and svgElem, create a new group element and insert it in the DOM as the last layer.
|
||||
* @param {SVGGElement} [svgElem] - The SVG DOM element. If defined, use this to add
|
||||
* a new layer to the document.
|
||||
*/
|
||||
constructor (name, group, svgElem) {
|
||||
this.name_ = name;
|
||||
this.group_ = svgElem ? null : group;
|
||||
|
||||
if (svgElem) {
|
||||
// Create a group element with title and add it to the DOM.
|
||||
const svgdoc = svgElem.ownerDocument;
|
||||
this.group_ = svgdoc.createElementNS(NS.SVG, 'g');
|
||||
const layerTitle = svgdoc.createElementNS(NS.SVG, 'title');
|
||||
layerTitle.textContent = name;
|
||||
this.group_.append(layerTitle);
|
||||
if (group) {
|
||||
$(group).after(this.group_);
|
||||
} else {
|
||||
svgElem.append(this.group_);
|
||||
}
|
||||
}
|
||||
|
||||
addLayerClass(this.group_);
|
||||
walkTree(this.group_, function (e) {
|
||||
e.setAttribute('style', 'pointer-events:inherit');
|
||||
});
|
||||
|
||||
this.group_.setAttribute('style', svgElem ? 'pointer-events:all' : 'pointer-events:none');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the layer's name.
|
||||
* @returns {string} The layer name
|
||||
*/
|
||||
getName () {
|
||||
return this.name_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the group element for this layer.
|
||||
* @returns {SVGGElement} The layer SVG group
|
||||
*/
|
||||
getGroup () {
|
||||
return this.group_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Active this layer so it takes pointer events.
|
||||
* @returns {void}
|
||||
*/
|
||||
activate () {
|
||||
this.group_.setAttribute('style', 'pointer-events:all');
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactive this layer so it does NOT take pointer events.
|
||||
* @returns {void}
|
||||
*/
|
||||
deactivate () {
|
||||
this.group_.setAttribute('style', 'pointer-events:none');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this layer visible or hidden based on 'visible' parameter.
|
||||
* @param {boolean} visible - If true, make visible; otherwise, hide it.
|
||||
* @returns {void}
|
||||
*/
|
||||
setVisible (visible) {
|
||||
const expected = visible === undefined || visible ? 'inline' : 'none';
|
||||
const oldDisplay = this.group_.getAttribute('display');
|
||||
if (oldDisplay !== expected) {
|
||||
this.group_.setAttribute('display', expected);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this layer visible?
|
||||
* @returns {boolean} True if visible.
|
||||
*/
|
||||
isVisible () {
|
||||
return this.group_.getAttribute('display') !== 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get layer opacity.
|
||||
* @returns {Float} Opacity value.
|
||||
*/
|
||||
getOpacity () {
|
||||
const opacity = this.group_.getAttribute('opacity');
|
||||
if (isNullish(opacity)) {
|
||||
return 1;
|
||||
}
|
||||
return Number.parseFloat(opacity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the opacity of this layer. If opacity is not a value between 0.0 and 1.0,
|
||||
* nothing happens.
|
||||
* @param {Float} opacity - A float value in the range 0.0-1.0
|
||||
* @returns {void}
|
||||
*/
|
||||
setOpacity (opacity) {
|
||||
if (typeof opacity === 'number' && opacity >= 0.0 && opacity <= 1.0) {
|
||||
this.group_.setAttribute('opacity', opacity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append children to this layer.
|
||||
* @param {SVGGElement} children - The children to append to this layer.
|
||||
* @returns {void}
|
||||
*/
|
||||
appendChildren (children) {
|
||||
for (const child of children) {
|
||||
this.group_.append(child);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {SVGTitleElement|null}
|
||||
*/
|
||||
getTitleElement () {
|
||||
const len = this.group_.childNodes.length;
|
||||
for (let i = 0; i < len; ++i) {
|
||||
const child = this.group_.childNodes.item(i);
|
||||
if (child && child.tagName === 'title') {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the name of this layer.
|
||||
* @param {string} name - The new name.
|
||||
* @param {module:history.HistoryRecordingService} hrService - History recording service
|
||||
* @returns {string|null} The new name if changed; otherwise, null.
|
||||
*/
|
||||
setName (name, hrService) {
|
||||
const previousName = this.name_;
|
||||
name = toXml(name);
|
||||
// now change the underlying title element contents
|
||||
const title = this.getTitleElement();
|
||||
if (title) {
|
||||
$(title).empty();
|
||||
title.textContent = name;
|
||||
this.name_ = name;
|
||||
if (hrService) {
|
||||
hrService.changeElement(title, {'#text': previousName});
|
||||
}
|
||||
return this.name_;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove this layer's group from the DOM. No more functions on group can be called after this.
|
||||
* @returns {SVGGElement} The layer SVG group that was just removed.
|
||||
*/
|
||||
removeGroup () {
|
||||
const group = this.group_;
|
||||
this.group_.remove();
|
||||
this.group_ = undefined;
|
||||
return group;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @property {string} CLASS_NAME - class attribute assigned to all layer groups.
|
||||
*/
|
||||
Layer.CLASS_NAME = 'layer';
|
||||
|
||||
/**
|
||||
* @property {RegExp} CLASS_REGEX - Used to test presence of class Layer.CLASS_NAME
|
||||
*/
|
||||
Layer.CLASS_REGEX = new RegExp('(\\s|^)' + Layer.CLASS_NAME + '(\\s|$)');
|
||||
|
||||
/**
|
||||
* Add class `Layer.CLASS_NAME` to the element (usually `class='layer'`).
|
||||
*
|
||||
* @param {SVGGElement} elem - The SVG element to update
|
||||
* @returns {void}
|
||||
*/
|
||||
function addLayerClass (elem) {
|
||||
const classes = elem.getAttribute('class');
|
||||
if (isNullish(classes) || !classes.length) {
|
||||
elem.setAttribute('class', Layer.CLASS_NAME);
|
||||
} else if (!Layer.CLASS_REGEX.test(classes)) {
|
||||
elem.setAttribute('class', classes + ' ' + Layer.CLASS_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
export default Layer;
|
|
@ -0,0 +1,222 @@
|
|||
/**
|
||||
* Mathematical utilities.
|
||||
* @module math
|
||||
* @license MIT
|
||||
*
|
||||
* @copyright 2010 Alexis Deveria, 2010 Jeff Schiller
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {PlainObject} module:math.AngleCoord45
|
||||
* @property {Float} x - The angle-snapped x value
|
||||
* @property {Float} y - The angle-snapped y value
|
||||
* @property {Integer} a - The angle at which to snap
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {PlainObject} module:math.XYObject
|
||||
* @property {Float} x
|
||||
* @property {Float} y
|
||||
*/
|
||||
|
||||
import {NS} from './namespaces.js';
|
||||
import {getTransformList} from './svgtransformlist.js';
|
||||
|
||||
// Constants
|
||||
const NEAR_ZERO = 1e-14;
|
||||
|
||||
// Throw away SVGSVGElement used for creating matrices/transforms.
|
||||
const svg = document.createElementNS(NS.SVG, 'svg');
|
||||
|
||||
/**
|
||||
* A (hopefully) quicker function to transform a point by a matrix
|
||||
* (this function avoids any DOM calls and just does the math).
|
||||
* @function module:math.transformPoint
|
||||
* @param {Float} x - Float representing the x coordinate
|
||||
* @param {Float} y - Float representing the y coordinate
|
||||
* @param {SVGMatrix} m - Matrix object to transform the point with
|
||||
* @returns {module:math.XYObject} An x, y object representing the transformed point
|
||||
*/
|
||||
export const transformPoint = function (x, y, m) {
|
||||
return {x: m.a * x + m.c * y + m.e, y: m.b * x + m.d * y + m.f};
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to check if the matrix performs no actual transform
|
||||
* (i.e. exists for identity purposes).
|
||||
* @function module:math.isIdentity
|
||||
* @param {SVGMatrix} m - The matrix object to check
|
||||
* @returns {boolean} Indicates whether or not the matrix is 1,0,0,1,0,0
|
||||
*/
|
||||
export const isIdentity = function (m) {
|
||||
return (m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1 && m.e === 0 && m.f === 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* This function tries to return a `SVGMatrix` that is the multiplication `m1 * m2`.
|
||||
* We also round to zero when it's near zero.
|
||||
* @function module:math.matrixMultiply
|
||||
* @param {...SVGMatrix} args - Matrix objects to multiply
|
||||
* @returns {SVGMatrix} The matrix object resulting from the calculation
|
||||
*/
|
||||
export const matrixMultiply = function (...args) {
|
||||
const m = args.reduceRight((prev, m1) => {
|
||||
return m1.multiply(prev);
|
||||
});
|
||||
|
||||
if (Math.abs(m.a) < NEAR_ZERO) { m.a = 0; }
|
||||
if (Math.abs(m.b) < NEAR_ZERO) { m.b = 0; }
|
||||
if (Math.abs(m.c) < NEAR_ZERO) { m.c = 0; }
|
||||
if (Math.abs(m.d) < NEAR_ZERO) { m.d = 0; }
|
||||
if (Math.abs(m.e) < NEAR_ZERO) { m.e = 0; }
|
||||
if (Math.abs(m.f) < NEAR_ZERO) { m.f = 0; }
|
||||
|
||||
return m;
|
||||
};
|
||||
|
||||
/**
|
||||
* See if the given transformlist includes a non-indentity matrix transform.
|
||||
* @function module:math.hasMatrixTransform
|
||||
* @param {SVGTransformList} [tlist] - The transformlist to check
|
||||
* @returns {boolean} Whether or not a matrix transform was found
|
||||
*/
|
||||
export const hasMatrixTransform = function (tlist) {
|
||||
if (!tlist) { return false; }
|
||||
let num = tlist.numberOfItems;
|
||||
while (num--) {
|
||||
const xform = tlist.getItem(num);
|
||||
if (xform.type === 1 && !isIdentity(xform.matrix)) { return true; }
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {PlainObject} module:math.TransformedBox An object with the following values
|
||||
* @property {module:math.XYObject} tl - The top left coordinate
|
||||
* @property {module:math.XYObject} tr - The top right coordinate
|
||||
* @property {module:math.XYObject} bl - The bottom left coordinate
|
||||
* @property {module:math.XYObject} br - The bottom right coordinate
|
||||
* @property {PlainObject} aabox - Object with the following values:
|
||||
* @property {Float} aabox.x - Float with the axis-aligned x coordinate
|
||||
* @property {Float} aabox.y - Float with the axis-aligned y coordinate
|
||||
* @property {Float} aabox.width - Float with the axis-aligned width coordinate
|
||||
* @property {Float} aabox.height - Float with the axis-aligned height coordinate
|
||||
*/
|
||||
|
||||
/**
|
||||
* Transforms a rectangle based on the given matrix.
|
||||
* @function module:math.transformBox
|
||||
* @param {Float} l - Float with the box's left coordinate
|
||||
* @param {Float} t - Float with the box's top coordinate
|
||||
* @param {Float} w - Float with the box width
|
||||
* @param {Float} h - Float with the box height
|
||||
* @param {SVGMatrix} m - Matrix object to transform the box by
|
||||
* @returns {module:math.TransformedBox}
|
||||
*/
|
||||
export const transformBox = function (l, t, w, h, m) {
|
||||
const tl = transformPoint(l, t, m),
|
||||
tr = transformPoint((l + w), t, m),
|
||||
bl = transformPoint(l, (t + h), m),
|
||||
br = transformPoint((l + w), (t + h), m),
|
||||
|
||||
minx = Math.min(tl.x, tr.x, bl.x, br.x),
|
||||
maxx = Math.max(tl.x, tr.x, bl.x, br.x),
|
||||
miny = Math.min(tl.y, tr.y, bl.y, br.y),
|
||||
maxy = Math.max(tl.y, tr.y, bl.y, br.y);
|
||||
|
||||
return {
|
||||
tl,
|
||||
tr,
|
||||
bl,
|
||||
br,
|
||||
aabox: {
|
||||
x: minx,
|
||||
y: miny,
|
||||
width: (maxx - minx),
|
||||
height: (maxy - miny)
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* This returns a single matrix Transform for a given Transform List
|
||||
* (this is the equivalent of `SVGTransformList.consolidate()` but unlike
|
||||
* that method, this one does not modify the actual `SVGTransformList`).
|
||||
* This function is very liberal with its `min`, `max` arguments.
|
||||
* @function module:math.transformListToTransform
|
||||
* @param {SVGTransformList} tlist - The transformlist object
|
||||
* @param {Integer} [min=0] - Optional integer indicating start transform position
|
||||
* @param {Integer} [max] - Optional integer indicating end transform position;
|
||||
* defaults to one less than the tlist's `numberOfItems`
|
||||
* @returns {SVGTransform} A single matrix transform object
|
||||
*/
|
||||
export const transformListToTransform = function (tlist, min, max) {
|
||||
if (!tlist) {
|
||||
// Or should tlist = null have been prevented before this?
|
||||
return svg.createSVGTransformFromMatrix(svg.createSVGMatrix());
|
||||
}
|
||||
min = min || 0;
|
||||
max = max || (tlist.numberOfItems - 1);
|
||||
min = Number.parseInt(min);
|
||||
max = Number.parseInt(max);
|
||||
if (min > max) { const temp = max; max = min; min = temp; }
|
||||
let m = svg.createSVGMatrix();
|
||||
for (let i = min; i <= max; ++i) {
|
||||
// if our indices are out of range, just use a harmless identity matrix
|
||||
const mtom = (i >= 0 && i < tlist.numberOfItems
|
||||
? tlist.getItem(i).matrix
|
||||
: svg.createSVGMatrix());
|
||||
m = matrixMultiply(m, mtom);
|
||||
}
|
||||
return svg.createSVGTransformFromMatrix(m);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the matrix object for a given element.
|
||||
* @function module:math.getMatrix
|
||||
* @param {Element} elem - The DOM element to check
|
||||
* @returns {SVGMatrix} The matrix object associated with the element's transformlist
|
||||
*/
|
||||
export const getMatrix = function (elem) {
|
||||
const tlist = getTransformList(elem);
|
||||
return transformListToTransform(tlist).matrix;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a 45 degree angle coordinate associated with the two given
|
||||
* coordinates.
|
||||
* @function module:math.snapToAngle
|
||||
* @param {Integer} x1 - First coordinate's x value
|
||||
* @param {Integer} y1 - First coordinate's y value
|
||||
* @param {Integer} x2 - Second coordinate's x value
|
||||
* @param {Integer} y2 - Second coordinate's y value
|
||||
* @returns {module:math.AngleCoord45}
|
||||
*/
|
||||
export const snapToAngle = function (x1, y1, x2, y2) {
|
||||
const snap = Math.PI / 4; // 45 degrees
|
||||
const dx = x2 - x1;
|
||||
const dy = y2 - y1;
|
||||
const angle = Math.atan2(dy, dx);
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
const snapangle = Math.round(angle / snap) * snap;
|
||||
|
||||
return {
|
||||
x: x1 + dist * Math.cos(snapangle),
|
||||
y: y1 + dist * Math.sin(snapangle),
|
||||
a: snapangle
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if two rectangles (BBoxes objects) intersect each other.
|
||||
* @function module:math.rectsIntersect
|
||||
* @param {SVGRect} r1 - The first BBox-like object
|
||||
* @param {SVGRect} r2 - The second BBox-like object
|
||||
* @returns {boolean} True if rectangles intersect
|
||||
*/
|
||||
export const rectsIntersect = function (r1, r2) {
|
||||
return r2.x < (r1.x + r1.width) &&
|
||||
(r2.x + r2.width) > r1.x &&
|
||||
r2.y < (r1.y + r1.height) &&
|
||||
(r2.y + r2.height) > r1.y;
|
||||
};
|
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* Namespaces or tools therefor.
|
||||
* @module namespaces
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* Common namepaces constants in alpha order.
|
||||
* @enum {string}
|
||||
* @type {PlainObject}
|
||||
* @memberof module:namespaces
|
||||
*/
|
||||
export const NS = {
|
||||
HTML: 'http://www.w3.org/1999/xhtml',
|
||||
MATH: 'http://www.w3.org/1998/Math/MathML',
|
||||
SE: 'http://svg-edit.googlecode.com',
|
||||
SVG: 'http://www.w3.org/2000/svg',
|
||||
XLINK: 'http://www.w3.org/1999/xlink',
|
||||
XML: 'http://www.w3.org/XML/1998/namespace',
|
||||
XMLNS: 'http://www.w3.org/2000/xmlns/' // see http://www.w3.org/TR/REC-xml-names/#xmlReserved
|
||||
};
|
||||
|
||||
/**
|
||||
* @function module:namespaces.getReverseNS
|
||||
* @returns {string} The NS with key values switched and lowercase
|
||||
*/
|
||||
export const getReverseNS = function () {
|
||||
const reverseNS = {};
|
||||
Object.entries(NS).forEach(([name, URI]) => {
|
||||
reverseNS[URI] = name.toLowerCase();
|
||||
});
|
||||
return reverseNS;
|
||||
};
|
|
@ -0,0 +1,972 @@
|
|||
/* eslint-disable import/unambiguous, max-len */
|
||||
/* globals SVGPathSeg, SVGPathSegMovetoRel, SVGPathSegMovetoAbs,
|
||||
SVGPathSegMovetoRel, SVGPathSegLinetoRel, SVGPathSegLinetoAbs,
|
||||
SVGPathSegLinetoHorizontalRel, SVGPathSegLinetoHorizontalAbs,
|
||||
SVGPathSegLinetoVerticalRel, SVGPathSegLinetoVerticalAbs,
|
||||
SVGPathSegClosePath, SVGPathSegCurvetoCubicRel,
|
||||
SVGPathSegCurvetoCubicAbs, SVGPathSegCurvetoCubicSmoothRel,
|
||||
SVGPathSegCurvetoCubicSmoothAbs, SVGPathSegCurvetoQuadraticRel,
|
||||
SVGPathSegCurvetoQuadraticAbs, SVGPathSegCurvetoQuadraticSmoothRel,
|
||||
SVGPathSegCurvetoQuadraticSmoothAbs, SVGPathSegArcRel, SVGPathSegArcAbs */
|
||||
/**
|
||||
* SVGPathSeg API polyfill
|
||||
* https://github.com/progers/pathseg
|
||||
*
|
||||
* This is a drop-in replacement for the `SVGPathSeg` and `SVGPathSegList` APIs
|
||||
* that were removed from SVG2 ({@link https://lists.w3.org/Archives/Public/www-svg/2015Jun/0044.html}),
|
||||
* including the latest spec changes which were implemented in Firefox 43 and
|
||||
* Chrome 46.
|
||||
*/
|
||||
/* eslint-disable no-shadow, class-methods-use-this, jsdoc/require-jsdoc */
|
||||
// Linting: We avoid `no-shadow` as ESLint thinks these are still available globals
|
||||
// Linting: We avoid `class-methods-use-this` as this is a polyfill that must
|
||||
// follow the conventions
|
||||
(() => {
|
||||
if (!('SVGPathSeg' in window)) {
|
||||
// Spec: https://www.w3.org/TR/SVG11/single-page.html#paths-InterfaceSVGPathSeg
|
||||
class SVGPathSeg {
|
||||
constructor (type, typeAsLetter, owningPathSegList) {
|
||||
this.pathSegType = type;
|
||||
this.pathSegTypeAsLetter = typeAsLetter;
|
||||
this._owningPathSegList = owningPathSegList;
|
||||
}
|
||||
// Notify owning PathSegList on any changes so they can be synchronized back to the path element.
|
||||
_segmentChanged () {
|
||||
if (this._owningPathSegList) {
|
||||
this._owningPathSegList.segmentChanged(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
SVGPathSeg.prototype.classname = 'SVGPathSeg';
|
||||
|
||||
SVGPathSeg.PATHSEG_UNKNOWN = 0;
|
||||
SVGPathSeg.PATHSEG_CLOSEPATH = 1;
|
||||
SVGPathSeg.PATHSEG_MOVETO_ABS = 2;
|
||||
SVGPathSeg.PATHSEG_MOVETO_REL = 3;
|
||||
SVGPathSeg.PATHSEG_LINETO_ABS = 4;
|
||||
SVGPathSeg.PATHSEG_LINETO_REL = 5;
|
||||
SVGPathSeg.PATHSEG_CURVETO_CUBIC_ABS = 6;
|
||||
SVGPathSeg.PATHSEG_CURVETO_CUBIC_REL = 7;
|
||||
SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_ABS = 8;
|
||||
SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_REL = 9;
|
||||
SVGPathSeg.PATHSEG_ARC_ABS = 10;
|
||||
SVGPathSeg.PATHSEG_ARC_REL = 11;
|
||||
SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_ABS = 12;
|
||||
SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_REL = 13;
|
||||
SVGPathSeg.PATHSEG_LINETO_VERTICAL_ABS = 14;
|
||||
SVGPathSeg.PATHSEG_LINETO_VERTICAL_REL = 15;
|
||||
SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_ABS = 16;
|
||||
SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_REL = 17;
|
||||
SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_ABS = 18;
|
||||
SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_REL = 19;
|
||||
|
||||
class SVGPathSegClosePath extends SVGPathSeg {
|
||||
constructor (owningPathSegList) {
|
||||
super(SVGPathSeg.PATHSEG_CLOSEPATH, 'z', owningPathSegList);
|
||||
}
|
||||
toString () { return '[object SVGPathSegClosePath]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter; }
|
||||
clone () { return new SVGPathSegClosePath(undefined); }
|
||||
}
|
||||
|
||||
class SVGPathSegMovetoAbs extends SVGPathSeg {
|
||||
constructor (owningPathSegList, x, y) {
|
||||
super(SVGPathSeg.PATHSEG_MOVETO_ABS, 'M', owningPathSegList);
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
}
|
||||
toString () { return '[object SVGPathSegMovetoAbs]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x + ' ' + this._y; }
|
||||
clone () { return new SVGPathSegMovetoAbs(undefined, this._x, this._y); }
|
||||
}
|
||||
Object.defineProperties(SVGPathSegMovetoAbs.prototype, {
|
||||
x: {
|
||||
get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true
|
||||
},
|
||||
y: {
|
||||
get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true
|
||||
}
|
||||
});
|
||||
|
||||
class SVGPathSegMovetoRel extends SVGPathSeg {
|
||||
constructor (owningPathSegList, x, y) {
|
||||
super(SVGPathSeg.PATHSEG_MOVETO_REL, 'm', owningPathSegList);
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
}
|
||||
toString () { return '[object SVGPathSegMovetoRel]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x + ' ' + this._y; }
|
||||
clone () { return new SVGPathSegMovetoRel(undefined, this._x, this._y); }
|
||||
}
|
||||
Object.defineProperties(SVGPathSegMovetoRel.prototype, {
|
||||
x: {get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true},
|
||||
y: {get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true}
|
||||
});
|
||||
|
||||
class SVGPathSegLinetoAbs extends SVGPathSeg {
|
||||
constructor (owningPathSegList, x, y) {
|
||||
super(SVGPathSeg.PATHSEG_LINETO_ABS, 'L', owningPathSegList);
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
}
|
||||
toString () { return '[object SVGPathSegLinetoAbs]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x + ' ' + this._y; }
|
||||
clone () { return new SVGPathSegLinetoAbs(undefined, this._x, this._y); }
|
||||
}
|
||||
Object.defineProperties(SVGPathSegLinetoAbs.prototype, {
|
||||
x: {get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true},
|
||||
y: {get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true}
|
||||
});
|
||||
|
||||
class SVGPathSegLinetoRel extends SVGPathSeg {
|
||||
constructor (owningPathSegList, x, y) {
|
||||
super(SVGPathSeg.PATHSEG_LINETO_REL, 'l', owningPathSegList);
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
}
|
||||
toString () { return '[object SVGPathSegLinetoRel]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x + ' ' + this._y; }
|
||||
clone () { return new SVGPathSegLinetoRel(undefined, this._x, this._y); }
|
||||
}
|
||||
Object.defineProperties(SVGPathSegLinetoRel.prototype, {
|
||||
x: {get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true},
|
||||
y: {get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true}
|
||||
});
|
||||
|
||||
class SVGPathSegCurvetoCubicAbs extends SVGPathSeg {
|
||||
constructor (owningPathSegList, x, y, x1, y1, x2, y2) {
|
||||
super(SVGPathSeg.PATHSEG_CURVETO_CUBIC_ABS, 'C', owningPathSegList);
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
this._x1 = x1;
|
||||
this._y1 = y1;
|
||||
this._x2 = x2;
|
||||
this._y2 = y2;
|
||||
}
|
||||
toString () { return '[object SVGPathSegCurvetoCubicAbs]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x1 + ' ' + this._y1 + ' ' + this._x2 + ' ' + this._y2 + ' ' + this._x + ' ' + this._y; }
|
||||
clone () { return new SVGPathSegCurvetoCubicAbs(undefined, this._x, this._y, this._x1, this._y1, this._x2, this._y2); }
|
||||
}
|
||||
Object.defineProperties(SVGPathSegCurvetoCubicAbs.prototype, {
|
||||
x: {get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true},
|
||||
y: {get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true},
|
||||
x1: {get () { return this._x1; }, set (x1) { this._x1 = x1; this._segmentChanged(); }, enumerable: true},
|
||||
y1: {get () { return this._y1; }, set (y1) { this._y1 = y1; this._segmentChanged(); }, enumerable: true},
|
||||
x2: {get () { return this._x2; }, set (x2) { this._x2 = x2; this._segmentChanged(); }, enumerable: true},
|
||||
y2: {get () { return this._y2; }, set (y2) { this._y2 = y2; this._segmentChanged(); }, enumerable: true}
|
||||
});
|
||||
|
||||
class SVGPathSegCurvetoCubicRel extends SVGPathSeg {
|
||||
constructor (owningPathSegList, x, y, x1, y1, x2, y2) {
|
||||
super(SVGPathSeg.PATHSEG_CURVETO_CUBIC_REL, 'c', owningPathSegList);
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
this._x1 = x1;
|
||||
this._y1 = y1;
|
||||
this._x2 = x2;
|
||||
this._y2 = y2;
|
||||
}
|
||||
toString () { return '[object SVGPathSegCurvetoCubicRel]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x1 + ' ' + this._y1 + ' ' + this._x2 + ' ' + this._y2 + ' ' + this._x + ' ' + this._y; }
|
||||
clone () { return new SVGPathSegCurvetoCubicRel(undefined, this._x, this._y, this._x1, this._y1, this._x2, this._y2); }
|
||||
}
|
||||
Object.defineProperties(SVGPathSegCurvetoCubicRel.prototype, {
|
||||
x: {get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true},
|
||||
y: {get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true},
|
||||
x1: {get () { return this._x1; }, set (x1) { this._x1 = x1; this._segmentChanged(); }, enumerable: true},
|
||||
y1: {get () { return this._y1; }, set (y1) { this._y1 = y1; this._segmentChanged(); }, enumerable: true},
|
||||
x2: {get () { return this._x2; }, set (x2) { this._x2 = x2; this._segmentChanged(); }, enumerable: true},
|
||||
y2: {get () { return this._y2; }, set (y2) { this._y2 = y2; this._segmentChanged(); }, enumerable: true}
|
||||
});
|
||||
|
||||
class SVGPathSegCurvetoQuadraticAbs extends SVGPathSeg {
|
||||
constructor (owningPathSegList, x, y, x1, y1) {
|
||||
super(SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_ABS, 'Q', owningPathSegList);
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
this._x1 = x1;
|
||||
this._y1 = y1;
|
||||
}
|
||||
toString () { return '[object SVGPathSegCurvetoQuadraticAbs]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x1 + ' ' + this._y1 + ' ' + this._x + ' ' + this._y; }
|
||||
clone () { return new SVGPathSegCurvetoQuadraticAbs(undefined, this._x, this._y, this._x1, this._y1); }
|
||||
}
|
||||
Object.defineProperties(SVGPathSegCurvetoQuadraticAbs.prototype, {
|
||||
x: {get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true},
|
||||
y: {get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true},
|
||||
x1: {get () { return this._x1; }, set (x1) { this._x1 = x1; this._segmentChanged(); }, enumerable: true},
|
||||
y1: {get () { return this._y1; }, set (y1) { this._y1 = y1; this._segmentChanged(); }, enumerable: true}
|
||||
});
|
||||
|
||||
class SVGPathSegCurvetoQuadraticRel extends SVGPathSeg {
|
||||
constructor (owningPathSegList, x, y, x1, y1) {
|
||||
super(SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_REL, 'q', owningPathSegList);
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
this._x1 = x1;
|
||||
this._y1 = y1;
|
||||
}
|
||||
toString () { return '[object SVGPathSegCurvetoQuadraticRel]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x1 + ' ' + this._y1 + ' ' + this._x + ' ' + this._y; }
|
||||
clone () { return new SVGPathSegCurvetoQuadraticRel(undefined, this._x, this._y, this._x1, this._y1); }
|
||||
}
|
||||
Object.defineProperties(SVGPathSegCurvetoQuadraticRel.prototype, {
|
||||
x: {get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true},
|
||||
y: {get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true},
|
||||
x1: {get () { return this._x1; }, set (x1) { this._x1 = x1; this._segmentChanged(); }, enumerable: true},
|
||||
y1: {get () { return this._y1; }, set (y1) { this._y1 = y1; this._segmentChanged(); }, enumerable: true}
|
||||
});
|
||||
|
||||
class SVGPathSegArcAbs extends SVGPathSeg {
|
||||
constructor (owningPathSegList, x, y, r1, r2, angle, largeArcFlag, sweepFlag) {
|
||||
super(SVGPathSeg.PATHSEG_ARC_ABS, 'A', owningPathSegList);
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
this._r1 = r1;
|
||||
this._r2 = r2;
|
||||
this._angle = angle;
|
||||
this._largeArcFlag = largeArcFlag;
|
||||
this._sweepFlag = sweepFlag;
|
||||
}
|
||||
toString () { return '[object SVGPathSegArcAbs]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._r1 + ' ' + this._r2 + ' ' + this._angle + ' ' + (this._largeArcFlag ? '1' : '0') + ' ' + (this._sweepFlag ? '1' : '0') + ' ' + this._x + ' ' + this._y; }
|
||||
clone () { return new SVGPathSegArcAbs(undefined, this._x, this._y, this._r1, this._r2, this._angle, this._largeArcFlag, this._sweepFlag); }
|
||||
}
|
||||
Object.defineProperties(SVGPathSegArcAbs.prototype, {
|
||||
x: {get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true},
|
||||
y: {get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true},
|
||||
r1: {get () { return this._r1; }, set (r1) { this._r1 = r1; this._segmentChanged(); }, enumerable: true},
|
||||
r2: {get () { return this._r2; }, set (r2) { this._r2 = r2; this._segmentChanged(); }, enumerable: true},
|
||||
angle: {get () { return this._angle; }, set (angle) { this._angle = angle; this._segmentChanged(); }, enumerable: true},
|
||||
largeArcFlag: {get () { return this._largeArcFlag; }, set (largeArcFlag) { this._largeArcFlag = largeArcFlag; this._segmentChanged(); }, enumerable: true},
|
||||
sweepFlag: {get () { return this._sweepFlag; }, set (sweepFlag) { this._sweepFlag = sweepFlag; this._segmentChanged(); }, enumerable: true}
|
||||
});
|
||||
|
||||
class SVGPathSegArcRel extends SVGPathSeg {
|
||||
constructor (owningPathSegList, x, y, r1, r2, angle, largeArcFlag, sweepFlag) {
|
||||
super(SVGPathSeg.PATHSEG_ARC_REL, 'a', owningPathSegList);
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
this._r1 = r1;
|
||||
this._r2 = r2;
|
||||
this._angle = angle;
|
||||
this._largeArcFlag = largeArcFlag;
|
||||
this._sweepFlag = sweepFlag;
|
||||
}
|
||||
toString () { return '[object SVGPathSegArcRel]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._r1 + ' ' + this._r2 + ' ' + this._angle + ' ' + (this._largeArcFlag ? '1' : '0') + ' ' + (this._sweepFlag ? '1' : '0') + ' ' + this._x + ' ' + this._y; }
|
||||
clone () { return new SVGPathSegArcRel(undefined, this._x, this._y, this._r1, this._r2, this._angle, this._largeArcFlag, this._sweepFlag); }
|
||||
}
|
||||
Object.defineProperties(SVGPathSegArcRel.prototype, {
|
||||
x: {get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true},
|
||||
y: {get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true},
|
||||
r1: {get () { return this._r1; }, set (r1) { this._r1 = r1; this._segmentChanged(); }, enumerable: true},
|
||||
r2: {get () { return this._r2; }, set (r2) { this._r2 = r2; this._segmentChanged(); }, enumerable: true},
|
||||
angle: {get () { return this._angle; }, set (angle) { this._angle = angle; this._segmentChanged(); }, enumerable: true},
|
||||
largeArcFlag: {get () { return this._largeArcFlag; }, set (largeArcFlag) { this._largeArcFlag = largeArcFlag; this._segmentChanged(); }, enumerable: true},
|
||||
sweepFlag: {get () { return this._sweepFlag; }, set (sweepFlag) { this._sweepFlag = sweepFlag; this._segmentChanged(); }, enumerable: true}
|
||||
});
|
||||
|
||||
class SVGPathSegLinetoHorizontalAbs extends SVGPathSeg {
|
||||
constructor (owningPathSegList, x) {
|
||||
super(SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_ABS, 'H', owningPathSegList);
|
||||
this._x = x;
|
||||
}
|
||||
toString () { return '[object SVGPathSegLinetoHorizontalAbs]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x; }
|
||||
clone () { return new SVGPathSegLinetoHorizontalAbs(undefined, this._x); }
|
||||
}
|
||||
Object.defineProperty(SVGPathSegLinetoHorizontalAbs.prototype, 'x', {get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true});
|
||||
|
||||
class SVGPathSegLinetoHorizontalRel extends SVGPathSeg {
|
||||
constructor (owningPathSegList, x) {
|
||||
super(SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_REL, 'h', owningPathSegList);
|
||||
this._x = x;
|
||||
}
|
||||
toString () { return '[object SVGPathSegLinetoHorizontalRel]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x; }
|
||||
clone () { return new SVGPathSegLinetoHorizontalRel(undefined, this._x); }
|
||||
}
|
||||
Object.defineProperty(SVGPathSegLinetoHorizontalRel.prototype, 'x', {get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true});
|
||||
|
||||
class SVGPathSegLinetoVerticalAbs extends SVGPathSeg {
|
||||
constructor (owningPathSegList, y) {
|
||||
super(SVGPathSeg.PATHSEG_LINETO_VERTICAL_ABS, 'V', owningPathSegList);
|
||||
this._y = y;
|
||||
}
|
||||
toString () { return '[object SVGPathSegLinetoVerticalAbs]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._y; }
|
||||
clone () { return new SVGPathSegLinetoVerticalAbs(undefined, this._y); }
|
||||
}
|
||||
Object.defineProperty(SVGPathSegLinetoVerticalAbs.prototype, 'y', {get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true});
|
||||
|
||||
class SVGPathSegLinetoVerticalRel extends SVGPathSeg {
|
||||
constructor (owningPathSegList, y) {
|
||||
super(SVGPathSeg.PATHSEG_LINETO_VERTICAL_REL, 'v', owningPathSegList);
|
||||
this._y = y;
|
||||
}
|
||||
toString () { return '[object SVGPathSegLinetoVerticalRel]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._y; }
|
||||
clone () { return new SVGPathSegLinetoVerticalRel(undefined, this._y); }
|
||||
}
|
||||
Object.defineProperty(SVGPathSegLinetoVerticalRel.prototype, 'y', {get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true});
|
||||
|
||||
class SVGPathSegCurvetoCubicSmoothAbs extends SVGPathSeg {
|
||||
constructor (owningPathSegList, x, y, x2, y2) {
|
||||
super(SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_ABS, 'S', owningPathSegList);
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
this._x2 = x2;
|
||||
this._y2 = y2;
|
||||
}
|
||||
toString () { return '[object SVGPathSegCurvetoCubicSmoothAbs]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x2 + ' ' + this._y2 + ' ' + this._x + ' ' + this._y; }
|
||||
clone () { return new SVGPathSegCurvetoCubicSmoothAbs(undefined, this._x, this._y, this._x2, this._y2); }
|
||||
}
|
||||
Object.defineProperties(SVGPathSegCurvetoCubicSmoothAbs.prototype, {
|
||||
x: {get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true},
|
||||
y: {get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true},
|
||||
x2: {get () { return this._x2; }, set (x2) { this._x2 = x2; this._segmentChanged(); }, enumerable: true},
|
||||
y2: {get () { return this._y2; }, set (y2) { this._y2 = y2; this._segmentChanged(); }, enumerable: true}
|
||||
});
|
||||
|
||||
class SVGPathSegCurvetoCubicSmoothRel extends SVGPathSeg {
|
||||
constructor (owningPathSegList, x, y, x2, y2) {
|
||||
super(SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_REL, 's', owningPathSegList);
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
this._x2 = x2;
|
||||
this._y2 = y2;
|
||||
}
|
||||
toString () { return '[object SVGPathSegCurvetoCubicSmoothRel]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x2 + ' ' + this._y2 + ' ' + this._x + ' ' + this._y; }
|
||||
clone () { return new SVGPathSegCurvetoCubicSmoothRel(undefined, this._x, this._y, this._x2, this._y2); }
|
||||
}
|
||||
Object.defineProperties(SVGPathSegCurvetoCubicSmoothRel.prototype, {
|
||||
x: {get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true},
|
||||
y: {get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true},
|
||||
x2: {get () { return this._x2; }, set (x2) { this._x2 = x2; this._segmentChanged(); }, enumerable: true},
|
||||
y2: {get () { return this._y2; }, set (y2) { this._y2 = y2; this._segmentChanged(); }, enumerable: true}
|
||||
});
|
||||
|
||||
class SVGPathSegCurvetoQuadraticSmoothAbs extends SVGPathSeg {
|
||||
constructor (owningPathSegList, x, y) {
|
||||
super(SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_ABS, 'T', owningPathSegList);
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
}
|
||||
toString () { return '[object SVGPathSegCurvetoQuadraticSmoothAbs]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x + ' ' + this._y; }
|
||||
clone () { return new SVGPathSegCurvetoQuadraticSmoothAbs(undefined, this._x, this._y); }
|
||||
}
|
||||
Object.defineProperties(SVGPathSegCurvetoQuadraticSmoothAbs.prototype, {
|
||||
x: {get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true},
|
||||
y: {get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true}
|
||||
});
|
||||
|
||||
class SVGPathSegCurvetoQuadraticSmoothRel extends SVGPathSeg {
|
||||
constructor (owningPathSegList, x, y) {
|
||||
super(SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_REL, 't', owningPathSegList);
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
}
|
||||
toString () { return '[object SVGPathSegCurvetoQuadraticSmoothRel]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x + ' ' + this._y; }
|
||||
clone () { return new SVGPathSegCurvetoQuadraticSmoothRel(undefined, this._x, this._y); }
|
||||
}
|
||||
Object.defineProperties(SVGPathSegCurvetoQuadraticSmoothRel.prototype, {
|
||||
x: {get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true},
|
||||
y: {get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true}
|
||||
});
|
||||
|
||||
// Add createSVGPathSeg* functions to SVGPathElement.
|
||||
// Spec: https://www.w3.org/TR/SVG11/single-page.html#paths-InterfaceSVGPathElement.
|
||||
SVGPathElement.prototype.createSVGPathSegClosePath = function () { return new SVGPathSegClosePath(undefined); };
|
||||
SVGPathElement.prototype.createSVGPathSegMovetoAbs = function (x, y) { return new SVGPathSegMovetoAbs(undefined, x, y); };
|
||||
SVGPathElement.prototype.createSVGPathSegMovetoRel = function (x, y) { return new SVGPathSegMovetoRel(undefined, x, y); };
|
||||
SVGPathElement.prototype.createSVGPathSegLinetoAbs = function (x, y) { return new SVGPathSegLinetoAbs(undefined, x, y); };
|
||||
SVGPathElement.prototype.createSVGPathSegLinetoRel = function (x, y) { return new SVGPathSegLinetoRel(undefined, x, y); };
|
||||
SVGPathElement.prototype.createSVGPathSegCurvetoCubicAbs = function (x, y, x1, y1, x2, y2) { return new SVGPathSegCurvetoCubicAbs(undefined, x, y, x1, y1, x2, y2); };
|
||||
SVGPathElement.prototype.createSVGPathSegCurvetoCubicRel = function (x, y, x1, y1, x2, y2) { return new SVGPathSegCurvetoCubicRel(undefined, x, y, x1, y1, x2, y2); };
|
||||
SVGPathElement.prototype.createSVGPathSegCurvetoQuadraticAbs = function (x, y, x1, y1) { return new SVGPathSegCurvetoQuadraticAbs(undefined, x, y, x1, y1); };
|
||||
SVGPathElement.prototype.createSVGPathSegCurvetoQuadraticRel = function (x, y, x1, y1) { return new SVGPathSegCurvetoQuadraticRel(undefined, x, y, x1, y1); };
|
||||
SVGPathElement.prototype.createSVGPathSegArcAbs = function (x, y, r1, r2, angle, largeArcFlag, sweepFlag) { return new SVGPathSegArcAbs(undefined, x, y, r1, r2, angle, largeArcFlag, sweepFlag); };
|
||||
SVGPathElement.prototype.createSVGPathSegArcRel = function (x, y, r1, r2, angle, largeArcFlag, sweepFlag) { return new SVGPathSegArcRel(undefined, x, y, r1, r2, angle, largeArcFlag, sweepFlag); };
|
||||
SVGPathElement.prototype.createSVGPathSegLinetoHorizontalAbs = function (x) { return new SVGPathSegLinetoHorizontalAbs(undefined, x); };
|
||||
SVGPathElement.prototype.createSVGPathSegLinetoHorizontalRel = function (x) { return new SVGPathSegLinetoHorizontalRel(undefined, x); };
|
||||
SVGPathElement.prototype.createSVGPathSegLinetoVerticalAbs = function (y) { return new SVGPathSegLinetoVerticalAbs(undefined, y); };
|
||||
SVGPathElement.prototype.createSVGPathSegLinetoVerticalRel = function (y) { return new SVGPathSegLinetoVerticalRel(undefined, y); };
|
||||
SVGPathElement.prototype.createSVGPathSegCurvetoCubicSmoothAbs = function (x, y, x2, y2) { return new SVGPathSegCurvetoCubicSmoothAbs(undefined, x, y, x2, y2); };
|
||||
SVGPathElement.prototype.createSVGPathSegCurvetoCubicSmoothRel = function (x, y, x2, y2) { return new SVGPathSegCurvetoCubicSmoothRel(undefined, x, y, x2, y2); };
|
||||
SVGPathElement.prototype.createSVGPathSegCurvetoQuadraticSmoothAbs = function (x, y) { return new SVGPathSegCurvetoQuadraticSmoothAbs(undefined, x, y); };
|
||||
SVGPathElement.prototype.createSVGPathSegCurvetoQuadraticSmoothRel = function (x, y) { return new SVGPathSegCurvetoQuadraticSmoothRel(undefined, x, y); };
|
||||
|
||||
if (!('getPathSegAtLength' in SVGPathElement.prototype)) {
|
||||
// Add getPathSegAtLength to SVGPathElement.
|
||||
// Spec: https://www.w3.org/TR/SVG11/single-page.html#paths-__svg__SVGPathElement__getPathSegAtLength
|
||||
// This polyfill requires SVGPathElement.getTotalLength to implement the distance-along-a-path algorithm.
|
||||
SVGPathElement.prototype.getPathSegAtLength = function (distance) {
|
||||
if (distance === undefined || !isFinite(distance)) {
|
||||
throw new Error('Invalid arguments.');
|
||||
}
|
||||
|
||||
const measurementElement = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
measurementElement.setAttribute('d', this.getAttribute('d'));
|
||||
let lastPathSegment = measurementElement.pathSegList.numberOfItems - 1;
|
||||
|
||||
// If the path is empty, return 0.
|
||||
if (lastPathSegment <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
do {
|
||||
measurementElement.pathSegList.removeItem(lastPathSegment);
|
||||
if (distance > measurementElement.getTotalLength()) {
|
||||
break;
|
||||
}
|
||||
lastPathSegment--;
|
||||
} while (lastPathSegment > 0);
|
||||
return lastPathSegment;
|
||||
};
|
||||
}
|
||||
|
||||
window.SVGPathSeg = SVGPathSeg;
|
||||
window.SVGPathSegClosePath = SVGPathSegClosePath;
|
||||
window.SVGPathSegMovetoAbs = SVGPathSegMovetoAbs;
|
||||
window.SVGPathSegMovetoRel = SVGPathSegMovetoRel;
|
||||
window.SVGPathSegLinetoAbs = SVGPathSegLinetoAbs;
|
||||
window.SVGPathSegLinetoRel = SVGPathSegLinetoRel;
|
||||
window.SVGPathSegCurvetoCubicAbs = SVGPathSegCurvetoCubicAbs;
|
||||
window.SVGPathSegCurvetoCubicRel = SVGPathSegCurvetoCubicRel;
|
||||
window.SVGPathSegCurvetoQuadraticAbs = SVGPathSegCurvetoQuadraticAbs;
|
||||
window.SVGPathSegCurvetoQuadraticRel = SVGPathSegCurvetoQuadraticRel;
|
||||
window.SVGPathSegArcAbs = SVGPathSegArcAbs;
|
||||
window.SVGPathSegArcRel = SVGPathSegArcRel;
|
||||
window.SVGPathSegLinetoHorizontalAbs = SVGPathSegLinetoHorizontalAbs;
|
||||
window.SVGPathSegLinetoHorizontalRel = SVGPathSegLinetoHorizontalRel;
|
||||
window.SVGPathSegLinetoVerticalAbs = SVGPathSegLinetoVerticalAbs;
|
||||
window.SVGPathSegLinetoVerticalRel = SVGPathSegLinetoVerticalRel;
|
||||
window.SVGPathSegCurvetoCubicSmoothAbs = SVGPathSegCurvetoCubicSmoothAbs;
|
||||
window.SVGPathSegCurvetoCubicSmoothRel = SVGPathSegCurvetoCubicSmoothRel;
|
||||
window.SVGPathSegCurvetoQuadraticSmoothAbs = SVGPathSegCurvetoQuadraticSmoothAbs;
|
||||
window.SVGPathSegCurvetoQuadraticSmoothRel = SVGPathSegCurvetoQuadraticSmoothRel;
|
||||
}
|
||||
|
||||
// Checking for SVGPathSegList in window checks for the case of an implementation without the
|
||||
// SVGPathSegList API.
|
||||
// The second check for appendItem is specific to Firefox 59+ which removed only parts of the
|
||||
// SVGPathSegList API (e.g., appendItem). In this case we need to re-implement the entire API
|
||||
// so the polyfill data (i.e., _list) is used throughout.
|
||||
if (!('SVGPathSegList' in window) || !('appendItem' in window.SVGPathSegList.prototype)) {
|
||||
// Spec: https://www.w3.org/TR/SVG11/single-page.html#paths-InterfaceSVGPathSegList
|
||||
class SVGPathSegList {
|
||||
constructor (pathElement) {
|
||||
this._pathElement = pathElement;
|
||||
this._list = this._parsePath(this._pathElement.getAttribute('d'));
|
||||
|
||||
// Use a MutationObserver to catch changes to the path's "d" attribute.
|
||||
this._mutationObserverConfig = {attributes: true, attributeFilter: ['d']};
|
||||
this._pathElementMutationObserver = new MutationObserver(this._updateListFromPathMutations.bind(this));
|
||||
this._pathElementMutationObserver.observe(this._pathElement, this._mutationObserverConfig);
|
||||
}
|
||||
// Process any pending mutations to the path element and update the list as needed.
|
||||
// This should be the first call of all public functions and is needed because
|
||||
// MutationObservers are not synchronous so we can have pending asynchronous mutations.
|
||||
_checkPathSynchronizedToList () {
|
||||
this._updateListFromPathMutations(this._pathElementMutationObserver.takeRecords());
|
||||
}
|
||||
|
||||
_updateListFromPathMutations (mutationRecords) {
|
||||
if (!this._pathElement) {
|
||||
return;
|
||||
}
|
||||
let hasPathMutations = false;
|
||||
mutationRecords.forEach((record) => {
|
||||
if (record.attributeName === 'd') {
|
||||
hasPathMutations = true;
|
||||
}
|
||||
});
|
||||
if (hasPathMutations) {
|
||||
this._list = this._parsePath(this._pathElement.getAttribute('d'));
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize the list and update the path's 'd' attribute.
|
||||
_writeListToPath () {
|
||||
this._pathElementMutationObserver.disconnect();
|
||||
this._pathElement.setAttribute('d', SVGPathSegList._pathSegArrayAsString(this._list));
|
||||
this._pathElementMutationObserver.observe(this._pathElement, this._mutationObserverConfig);
|
||||
}
|
||||
|
||||
// When a path segment changes the list needs to be synchronized back to the path element.
|
||||
segmentChanged (pathSeg) {
|
||||
this._writeListToPath();
|
||||
}
|
||||
|
||||
clear () {
|
||||
this._checkPathSynchronizedToList();
|
||||
|
||||
this._list.forEach((pathSeg) => {
|
||||
pathSeg._owningPathSegList = null;
|
||||
});
|
||||
this._list = [];
|
||||
this._writeListToPath();
|
||||
}
|
||||
|
||||
initialize (newItem) {
|
||||
this._checkPathSynchronizedToList();
|
||||
|
||||
this._list = [newItem];
|
||||
newItem._owningPathSegList = this;
|
||||
this._writeListToPath();
|
||||
return newItem;
|
||||
}
|
||||
|
||||
_checkValidIndex (index) {
|
||||
if (isNaN(index) || index < 0 || index >= this.numberOfItems) {
|
||||
throw new Error('INDEX_SIZE_ERR');
|
||||
}
|
||||
}
|
||||
|
||||
getItem (index) {
|
||||
this._checkPathSynchronizedToList();
|
||||
|
||||
this._checkValidIndex(index);
|
||||
return this._list[index];
|
||||
}
|
||||
|
||||
insertItemBefore (newItem, index) {
|
||||
this._checkPathSynchronizedToList();
|
||||
|
||||
// Spec: If the index is greater than or equal to numberOfItems, then the new item is appended to the end of the list.
|
||||
if (index > this.numberOfItems) {
|
||||
index = this.numberOfItems;
|
||||
}
|
||||
if (newItem._owningPathSegList) {
|
||||
// SVG2 spec says to make a copy.
|
||||
newItem = newItem.clone();
|
||||
}
|
||||
this._list.splice(index, 0, newItem);
|
||||
newItem._owningPathSegList = this;
|
||||
this._writeListToPath();
|
||||
return newItem;
|
||||
}
|
||||
|
||||
replaceItem (newItem, index) {
|
||||
this._checkPathSynchronizedToList();
|
||||
|
||||
if (newItem._owningPathSegList) {
|
||||
// SVG2 spec says to make a copy.
|
||||
newItem = newItem.clone();
|
||||
}
|
||||
this._checkValidIndex(index);
|
||||
this._list[index] = newItem;
|
||||
newItem._owningPathSegList = this;
|
||||
this._writeListToPath();
|
||||
return newItem;
|
||||
}
|
||||
|
||||
removeItem (index) {
|
||||
this._checkPathSynchronizedToList();
|
||||
|
||||
this._checkValidIndex(index);
|
||||
const item = this._list[index];
|
||||
this._list.splice(index, 1);
|
||||
this._writeListToPath();
|
||||
return item;
|
||||
}
|
||||
|
||||
appendItem (newItem) {
|
||||
this._checkPathSynchronizedToList();
|
||||
|
||||
if (newItem._owningPathSegList) {
|
||||
// SVG2 spec says to make a copy.
|
||||
newItem = newItem.clone();
|
||||
}
|
||||
this._list.push(newItem);
|
||||
newItem._owningPathSegList = this;
|
||||
// TODO: Optimize this to just append to the existing attribute.
|
||||
this._writeListToPath();
|
||||
return newItem;
|
||||
}
|
||||
|
||||
// This closely follows SVGPathParser::parsePath from Source/core/svg/SVGPathParser.cpp.
|
||||
_parsePath (string) {
|
||||
if (!string || !string.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const owningPathSegList = this;
|
||||
|
||||
class Builder {
|
||||
constructor () {
|
||||
this.pathSegList = [];
|
||||
}
|
||||
appendSegment (pathSeg) {
|
||||
this.pathSegList.push(pathSeg);
|
||||
}
|
||||
}
|
||||
|
||||
class Source {
|
||||
constructor (string) {
|
||||
this._string = string;
|
||||
this._currentIndex = 0;
|
||||
this._endIndex = this._string.length;
|
||||
this._previousCommand = SVGPathSeg.PATHSEG_UNKNOWN;
|
||||
|
||||
this._skipOptionalSpaces();
|
||||
}
|
||||
_isCurrentSpace () {
|
||||
const character = this._string[this._currentIndex];
|
||||
return character <= ' ' && (character === ' ' || character === '\n' || character === '\t' || character === '\r' || character === '\f');
|
||||
}
|
||||
|
||||
_skipOptionalSpaces () {
|
||||
while (this._currentIndex < this._endIndex && this._isCurrentSpace()) {
|
||||
this._currentIndex++;
|
||||
}
|
||||
return this._currentIndex < this._endIndex;
|
||||
}
|
||||
|
||||
_skipOptionalSpacesOrDelimiter () {
|
||||
if (this._currentIndex < this._endIndex && !this._isCurrentSpace() && this._string.charAt(this._currentIndex) !== ',') {
|
||||
return false;
|
||||
}
|
||||
if (this._skipOptionalSpaces()) {
|
||||
if (this._currentIndex < this._endIndex && this._string.charAt(this._currentIndex) === ',') {
|
||||
this._currentIndex++;
|
||||
this._skipOptionalSpaces();
|
||||
}
|
||||
}
|
||||
return this._currentIndex < this._endIndex;
|
||||
}
|
||||
|
||||
hasMoreData () {
|
||||
return this._currentIndex < this._endIndex;
|
||||
}
|
||||
|
||||
peekSegmentType () {
|
||||
const lookahead = this._string[this._currentIndex];
|
||||
return this._pathSegTypeFromChar(lookahead);
|
||||
}
|
||||
|
||||
_pathSegTypeFromChar (lookahead) {
|
||||
switch (lookahead) {
|
||||
case 'Z':
|
||||
case 'z':
|
||||
return SVGPathSeg.PATHSEG_CLOSEPATH;
|
||||
case 'M':
|
||||
return SVGPathSeg.PATHSEG_MOVETO_ABS;
|
||||
case 'm':
|
||||
return SVGPathSeg.PATHSEG_MOVETO_REL;
|
||||
case 'L':
|
||||
return SVGPathSeg.PATHSEG_LINETO_ABS;
|
||||
case 'l':
|
||||
return SVGPathSeg.PATHSEG_LINETO_REL;
|
||||
case 'C':
|
||||
return SVGPathSeg.PATHSEG_CURVETO_CUBIC_ABS;
|
||||
case 'c':
|
||||
return SVGPathSeg.PATHSEG_CURVETO_CUBIC_REL;
|
||||
case 'Q':
|
||||
return SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_ABS;
|
||||
case 'q':
|
||||
return SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_REL;
|
||||
case 'A':
|
||||
return SVGPathSeg.PATHSEG_ARC_ABS;
|
||||
case 'a':
|
||||
return SVGPathSeg.PATHSEG_ARC_REL;
|
||||
case 'H':
|
||||
return SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_ABS;
|
||||
case 'h':
|
||||
return SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_REL;
|
||||
case 'V':
|
||||
return SVGPathSeg.PATHSEG_LINETO_VERTICAL_ABS;
|
||||
case 'v':
|
||||
return SVGPathSeg.PATHSEG_LINETO_VERTICAL_REL;
|
||||
case 'S':
|
||||
return SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_ABS;
|
||||
case 's':
|
||||
return SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_REL;
|
||||
case 'T':
|
||||
return SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_ABS;
|
||||
case 't':
|
||||
return SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_REL;
|
||||
default:
|
||||
return SVGPathSeg.PATHSEG_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
_nextCommandHelper (lookahead, previousCommand) {
|
||||
// Check for remaining coordinates in the current command.
|
||||
if ((lookahead === '+' || lookahead === '-' || lookahead === '.' || (lookahead >= '0' && lookahead <= '9')) && previousCommand !== SVGPathSeg.PATHSEG_CLOSEPATH) {
|
||||
if (previousCommand === SVGPathSeg.PATHSEG_MOVETO_ABS) {
|
||||
return SVGPathSeg.PATHSEG_LINETO_ABS;
|
||||
}
|
||||
if (previousCommand === SVGPathSeg.PATHSEG_MOVETO_REL) {
|
||||
return SVGPathSeg.PATHSEG_LINETO_REL;
|
||||
}
|
||||
return previousCommand;
|
||||
}
|
||||
return SVGPathSeg.PATHSEG_UNKNOWN;
|
||||
}
|
||||
|
||||
initialCommandIsMoveTo () {
|
||||
// If the path is empty it is still valid, so return true.
|
||||
if (!this.hasMoreData()) {
|
||||
return true;
|
||||
}
|
||||
const command = this.peekSegmentType();
|
||||
// Path must start with moveTo.
|
||||
return command === SVGPathSeg.PATHSEG_MOVETO_ABS || command === SVGPathSeg.PATHSEG_MOVETO_REL;
|
||||
}
|
||||
|
||||
// Parse a number from an SVG path. This very closely follows genericParseNumber(...) from Source/core/svg/SVGParserUtilities.cpp.
|
||||
// Spec: https://www.w3.org/TR/SVG11/single-page.html#paths-PathDataBNF
|
||||
_parseNumber () {
|
||||
let exponent = 0;
|
||||
let integer = 0;
|
||||
let frac = 1;
|
||||
let decimal = 0;
|
||||
let sign = 1;
|
||||
let expsign = 1;
|
||||
|
||||
const startIndex = this._currentIndex;
|
||||
|
||||
this._skipOptionalSpaces();
|
||||
|
||||
// Read the sign.
|
||||
if (this._currentIndex < this._endIndex && this._string.charAt(this._currentIndex) === '+') {
|
||||
this._currentIndex++;
|
||||
} else if (this._currentIndex < this._endIndex && this._string.charAt(this._currentIndex) === '-') {
|
||||
this._currentIndex++;
|
||||
sign = -1;
|
||||
}
|
||||
|
||||
if (this._currentIndex === this._endIndex || ((this._string.charAt(this._currentIndex) < '0' || this._string.charAt(this._currentIndex) > '9') && this._string.charAt(this._currentIndex) !== '.')) {
|
||||
// The first character of a number must be one of [0-9+-.].
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Read the integer part, build right-to-left.
|
||||
const startIntPartIndex = this._currentIndex;
|
||||
while (this._currentIndex < this._endIndex && this._string.charAt(this._currentIndex) >= '0' && this._string.charAt(this._currentIndex) <= '9') {
|
||||
this._currentIndex++; // Advance to first non-digit.
|
||||
}
|
||||
|
||||
if (this._currentIndex !== startIntPartIndex) {
|
||||
let scanIntPartIndex = this._currentIndex - 1;
|
||||
let multiplier = 1;
|
||||
while (scanIntPartIndex >= startIntPartIndex) {
|
||||
integer += multiplier * (this._string.charAt(scanIntPartIndex--) - '0');
|
||||
multiplier *= 10;
|
||||
}
|
||||
}
|
||||
|
||||
// Read the decimals.
|
||||
if (this._currentIndex < this._endIndex && this._string.charAt(this._currentIndex) === '.') {
|
||||
this._currentIndex++;
|
||||
|
||||
// There must be a least one digit following the .
|
||||
if (this._currentIndex >= this._endIndex || this._string.charAt(this._currentIndex) < '0' || this._string.charAt(this._currentIndex) > '9') {
|
||||
return undefined;
|
||||
}
|
||||
while (this._currentIndex < this._endIndex && this._string.charAt(this._currentIndex) >= '0' && this._string.charAt(this._currentIndex) <= '9') {
|
||||
frac *= 10;
|
||||
decimal += (this._string.charAt(this._currentIndex) - '0') / frac;
|
||||
this._currentIndex += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Read the exponent part.
|
||||
if (this._currentIndex !== startIndex && this._currentIndex + 1 < this._endIndex && (this._string.charAt(this._currentIndex) === 'e' || this._string.charAt(this._currentIndex) === 'E') && (this._string.charAt(this._currentIndex + 1) !== 'x' && this._string.charAt(this._currentIndex + 1) !== 'm')) {
|
||||
this._currentIndex++;
|
||||
|
||||
// Read the sign of the exponent.
|
||||
if (this._string.charAt(this._currentIndex) === '+') {
|
||||
this._currentIndex++;
|
||||
} else if (this._string.charAt(this._currentIndex) === '-') {
|
||||
this._currentIndex++;
|
||||
expsign = -1;
|
||||
}
|
||||
|
||||
// There must be an exponent.
|
||||
if (this._currentIndex >= this._endIndex || this._string.charAt(this._currentIndex) < '0' || this._string.charAt(this._currentIndex) > '9') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
while (this._currentIndex < this._endIndex && this._string.charAt(this._currentIndex) >= '0' && this._string.charAt(this._currentIndex) <= '9') {
|
||||
exponent *= 10;
|
||||
exponent += (this._string.charAt(this._currentIndex) - '0');
|
||||
this._currentIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
let number = integer + decimal;
|
||||
number *= sign;
|
||||
|
||||
if (exponent) {
|
||||
number *= 10 ** (expsign * exponent);
|
||||
}
|
||||
|
||||
if (startIndex === this._currentIndex) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this._skipOptionalSpacesOrDelimiter();
|
||||
|
||||
return number;
|
||||
}
|
||||
|
||||
_parseArcFlag () {
|
||||
if (this._currentIndex >= this._endIndex) {
|
||||
return undefined;
|
||||
}
|
||||
let flag = false;
|
||||
const flagChar = this._string.charAt(this._currentIndex++);
|
||||
if (flagChar === '0') {
|
||||
flag = false;
|
||||
} else if (flagChar === '1') {
|
||||
flag = true;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this._skipOptionalSpacesOrDelimiter();
|
||||
return flag;
|
||||
}
|
||||
|
||||
parseSegment () {
|
||||
const lookahead = this._string[this._currentIndex];
|
||||
let command = this._pathSegTypeFromChar(lookahead);
|
||||
if (command === SVGPathSeg.PATHSEG_UNKNOWN) {
|
||||
// Possibly an implicit command. Not allowed if this is the first command.
|
||||
if (this._previousCommand === SVGPathSeg.PATHSEG_UNKNOWN) {
|
||||
return null;
|
||||
}
|
||||
command = this._nextCommandHelper(lookahead, this._previousCommand);
|
||||
if (command === SVGPathSeg.PATHSEG_UNKNOWN) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
this._currentIndex++;
|
||||
}
|
||||
|
||||
this._previousCommand = command;
|
||||
|
||||
switch (command) {
|
||||
case SVGPathSeg.PATHSEG_MOVETO_REL:
|
||||
return new SVGPathSegMovetoRel(owningPathSegList, this._parseNumber(), this._parseNumber());
|
||||
case SVGPathSeg.PATHSEG_MOVETO_ABS:
|
||||
return new SVGPathSegMovetoAbs(owningPathSegList, this._parseNumber(), this._parseNumber());
|
||||
case SVGPathSeg.PATHSEG_LINETO_REL:
|
||||
return new SVGPathSegLinetoRel(owningPathSegList, this._parseNumber(), this._parseNumber());
|
||||
case SVGPathSeg.PATHSEG_LINETO_ABS:
|
||||
return new SVGPathSegLinetoAbs(owningPathSegList, this._parseNumber(), this._parseNumber());
|
||||
case SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_REL:
|
||||
return new SVGPathSegLinetoHorizontalRel(owningPathSegList, this._parseNumber());
|
||||
case SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_ABS:
|
||||
return new SVGPathSegLinetoHorizontalAbs(owningPathSegList, this._parseNumber());
|
||||
case SVGPathSeg.PATHSEG_LINETO_VERTICAL_REL:
|
||||
return new SVGPathSegLinetoVerticalRel(owningPathSegList, this._parseNumber());
|
||||
case SVGPathSeg.PATHSEG_LINETO_VERTICAL_ABS:
|
||||
return new SVGPathSegLinetoVerticalAbs(owningPathSegList, this._parseNumber());
|
||||
case SVGPathSeg.PATHSEG_CLOSEPATH:
|
||||
this._skipOptionalSpaces();
|
||||
return new SVGPathSegClosePath(owningPathSegList);
|
||||
case SVGPathSeg.PATHSEG_CURVETO_CUBIC_REL: {
|
||||
const points = {x1: this._parseNumber(), y1: this._parseNumber(), x2: this._parseNumber(), y2: this._parseNumber(), x: this._parseNumber(), y: this._parseNumber()};
|
||||
return new SVGPathSegCurvetoCubicRel(owningPathSegList, points.x, points.y, points.x1, points.y1, points.x2, points.y2);
|
||||
} case SVGPathSeg.PATHSEG_CURVETO_CUBIC_ABS: {
|
||||
const points = {x1: this._parseNumber(), y1: this._parseNumber(), x2: this._parseNumber(), y2: this._parseNumber(), x: this._parseNumber(), y: this._parseNumber()};
|
||||
return new SVGPathSegCurvetoCubicAbs(owningPathSegList, points.x, points.y, points.x1, points.y1, points.x2, points.y2);
|
||||
} case SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_REL: {
|
||||
const points = {x2: this._parseNumber(), y2: this._parseNumber(), x: this._parseNumber(), y: this._parseNumber()};
|
||||
return new SVGPathSegCurvetoCubicSmoothRel(owningPathSegList, points.x, points.y, points.x2, points.y2);
|
||||
} case SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_ABS: {
|
||||
const points = {x2: this._parseNumber(), y2: this._parseNumber(), x: this._parseNumber(), y: this._parseNumber()};
|
||||
return new SVGPathSegCurvetoCubicSmoothAbs(owningPathSegList, points.x, points.y, points.x2, points.y2);
|
||||
} case SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_REL: {
|
||||
const points = {x1: this._parseNumber(), y1: this._parseNumber(), x: this._parseNumber(), y: this._parseNumber()};
|
||||
return new SVGPathSegCurvetoQuadraticRel(owningPathSegList, points.x, points.y, points.x1, points.y1);
|
||||
} case SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_ABS: {
|
||||
const points = {x1: this._parseNumber(), y1: this._parseNumber(), x: this._parseNumber(), y: this._parseNumber()};
|
||||
return new SVGPathSegCurvetoQuadraticAbs(owningPathSegList, points.x, points.y, points.x1, points.y1);
|
||||
} case SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_REL:
|
||||
return new SVGPathSegCurvetoQuadraticSmoothRel(owningPathSegList, this._parseNumber(), this._parseNumber());
|
||||
case SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_ABS:
|
||||
return new SVGPathSegCurvetoQuadraticSmoothAbs(owningPathSegList, this._parseNumber(), this._parseNumber());
|
||||
case SVGPathSeg.PATHSEG_ARC_REL: {
|
||||
const points = {x1: this._parseNumber(), y1: this._parseNumber(), arcAngle: this._parseNumber(), arcLarge: this._parseArcFlag(), arcSweep: this._parseArcFlag(), x: this._parseNumber(), y: this._parseNumber()};
|
||||
return new SVGPathSegArcRel(owningPathSegList, points.x, points.y, points.x1, points.y1, points.arcAngle, points.arcLarge, points.arcSweep);
|
||||
} case SVGPathSeg.PATHSEG_ARC_ABS: {
|
||||
const points = {x1: this._parseNumber(), y1: this._parseNumber(), arcAngle: this._parseNumber(), arcLarge: this._parseArcFlag(), arcSweep: this._parseArcFlag(), x: this._parseNumber(), y: this._parseNumber()};
|
||||
return new SVGPathSegArcAbs(owningPathSegList, points.x, points.y, points.x1, points.y1, points.arcAngle, points.arcLarge, points.arcSweep);
|
||||
} default:
|
||||
throw new Error('Unknown path seg type.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const builder = new Builder();
|
||||
const source = new Source(string);
|
||||
|
||||
if (!source.initialCommandIsMoveTo()) {
|
||||
return [];
|
||||
}
|
||||
while (source.hasMoreData()) {
|
||||
const pathSeg = source.parseSegment();
|
||||
if (!pathSeg) {
|
||||
return [];
|
||||
}
|
||||
builder.appendSegment(pathSeg);
|
||||
}
|
||||
|
||||
return builder.pathSegList;
|
||||
}
|
||||
|
||||
// STATIC
|
||||
static _pathSegArrayAsString (pathSegArray) {
|
||||
let string = '';
|
||||
let first = true;
|
||||
pathSegArray.forEach((pathSeg) => {
|
||||
if (first) {
|
||||
first = false;
|
||||
string += pathSeg._asPathString();
|
||||
} else {
|
||||
string += ' ' + pathSeg._asPathString();
|
||||
}
|
||||
});
|
||||
return string;
|
||||
}
|
||||
}
|
||||
|
||||
SVGPathSegList.prototype.classname = 'SVGPathSegList';
|
||||
|
||||
Object.defineProperty(SVGPathSegList.prototype, 'numberOfItems', {
|
||||
get () {
|
||||
this._checkPathSynchronizedToList();
|
||||
return this._list.length;
|
||||
},
|
||||
enumerable: true
|
||||
});
|
||||
|
||||
// Add the pathSegList accessors to SVGPathElement.
|
||||
// Spec: https://www.w3.org/TR/SVG11/single-page.html#paths-InterfaceSVGAnimatedPathData
|
||||
Object.defineProperties(SVGPathElement.prototype, {
|
||||
pathSegList: {
|
||||
get () {
|
||||
if (!this._pathSegList) {
|
||||
this._pathSegList = new SVGPathSegList(this);
|
||||
}
|
||||
return this._pathSegList;
|
||||
},
|
||||
enumerable: true
|
||||
},
|
||||
// TODO: The following are not implemented and simply return SVGPathElement.pathSegList.
|
||||
normalizedPathSegList: {get () { return this.pathSegList; }, enumerable: true},
|
||||
animatedPathSegList: {get () { return this.pathSegList; }, enumerable: true},
|
||||
animatedNormalizedPathSegList: {get () { return this.pathSegList; }, enumerable: true}
|
||||
});
|
||||
window.SVGPathSegList = SVGPathSegList;
|
||||
}
|
||||
})();
|
|
@ -0,0 +1,397 @@
|
|||
/**
|
||||
* Partial polyfill of `SVGTransformList`
|
||||
* @module SVGTransformList
|
||||
*
|
||||
* @license MIT
|
||||
*
|
||||
* @copyright 2010 Alexis Deveria, 2010 Jeff Schiller
|
||||
*/
|
||||
|
||||
import {NS} from './namespaces.js';
|
||||
import {supportsNativeTransformLists} from './browser.js';
|
||||
|
||||
const svgroot = document.createElementNS(NS.SVG, 'svg');
|
||||
|
||||
/**
|
||||
* Helper function to convert `SVGTransform` to a string.
|
||||
* @param {SVGTransform} xform
|
||||
* @returns {string}
|
||||
*/
|
||||
function transformToString (xform) {
|
||||
const m = xform.matrix;
|
||||
let text = '';
|
||||
switch (xform.type) {
|
||||
case 1: // MATRIX
|
||||
text = 'matrix(' + [m.a, m.b, m.c, m.d, m.e, m.f].join(',') + ')';
|
||||
break;
|
||||
case 2: // TRANSLATE
|
||||
text = 'translate(' + m.e + ',' + m.f + ')';
|
||||
break;
|
||||
case 3: // SCALE
|
||||
if (m.a === m.d) {
|
||||
text = 'scale(' + m.a + ')';
|
||||
} else {
|
||||
text = 'scale(' + m.a + ',' + m.d + ')';
|
||||
}
|
||||
break;
|
||||
case 4: { // ROTATE
|
||||
let cx = 0;
|
||||
let cy = 0;
|
||||
// this prevents divide by zero
|
||||
if (xform.angle !== 0) {
|
||||
const K = 1 - m.a;
|
||||
cy = (K * m.f + m.b * m.e) / (K * K + m.b * m.b);
|
||||
cx = (m.e - m.b * cy) / K;
|
||||
}
|
||||
text = 'rotate(' + xform.angle + ' ' + cx + ',' + cy + ')';
|
||||
break;
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map of SVGTransformList objects.
|
||||
*/
|
||||
let listMap_ = {};
|
||||
|
||||
/**
|
||||
* @interface module:SVGTransformList.SVGEditTransformList
|
||||
* @property {Integer} numberOfItems unsigned long
|
||||
*/
|
||||
/**
|
||||
* @function module:SVGTransformList.SVGEditTransformList#clear
|
||||
* @returns {void}
|
||||
*/
|
||||
/**
|
||||
* @function module:SVGTransformList.SVGEditTransformList#initialize
|
||||
* @param {SVGTransform} newItem
|
||||
* @returns {SVGTransform}
|
||||
*/
|
||||
/**
|
||||
* DOES NOT THROW DOMException, INDEX_SIZE_ERR.
|
||||
* @function module:SVGTransformList.SVGEditTransformList#getItem
|
||||
* @param {Integer} index unsigned long
|
||||
* @returns {SVGTransform}
|
||||
*/
|
||||
/**
|
||||
* DOES NOT THROW DOMException, INDEX_SIZE_ERR.
|
||||
* @function module:SVGTransformList.SVGEditTransformList#insertItemBefore
|
||||
* @param {SVGTransform} newItem
|
||||
* @param {Integer} index unsigned long
|
||||
* @returns {SVGTransform}
|
||||
*/
|
||||
/**
|
||||
* DOES NOT THROW DOMException, INDEX_SIZE_ERR.
|
||||
* @function module:SVGTransformList.SVGEditTransformList#replaceItem
|
||||
* @param {SVGTransform} newItem
|
||||
* @param {Integer} index unsigned long
|
||||
* @returns {SVGTransform}
|
||||
*/
|
||||
/**
|
||||
* DOES NOT THROW DOMException, INDEX_SIZE_ERR.
|
||||
* @function module:SVGTransformList.SVGEditTransformList#removeItem
|
||||
* @param {Integer} index unsigned long
|
||||
* @returns {SVGTransform}
|
||||
*/
|
||||
/**
|
||||
* @function module:SVGTransformList.SVGEditTransformList#appendItem
|
||||
* @param {SVGTransform} newItem
|
||||
* @returns {SVGTransform}
|
||||
*/
|
||||
/**
|
||||
* NOT IMPLEMENTED.
|
||||
* @ignore
|
||||
* @function module:SVGTransformList.SVGEditTransformList#createSVGTransformFromMatrix
|
||||
* @param {SVGMatrix} matrix
|
||||
* @returns {SVGTransform}
|
||||
*/
|
||||
/**
|
||||
* NOT IMPLEMENTED.
|
||||
* @ignore
|
||||
* @function module:SVGTransformList.SVGEditTransformList#consolidate
|
||||
* @returns {SVGTransform}
|
||||
*/
|
||||
|
||||
/**
|
||||
* SVGTransformList implementation for Webkit.
|
||||
* These methods do not currently raise any exceptions.
|
||||
* These methods also do not check that transforms are being inserted. This is basically
|
||||
* implementing as much of SVGTransformList that we need to get the job done.
|
||||
* @implements {module:SVGTransformList.SVGEditTransformList}
|
||||
*/
|
||||
export class SVGTransformList { // eslint-disable-line no-shadow
|
||||
/**
|
||||
* @param {Element} elem
|
||||
* @returns {SVGTransformList}
|
||||
*/
|
||||
constructor (elem) {
|
||||
this._elem = elem || null;
|
||||
this._xforms = [];
|
||||
// TODO: how do we capture the undo-ability in the changed transform list?
|
||||
this._update = function () {
|
||||
let tstr = '';
|
||||
// /* const concatMatrix = */ svgroot.createSVGMatrix();
|
||||
for (let i = 0; i < this.numberOfItems; ++i) {
|
||||
const xform = this._list.getItem(i);
|
||||
tstr += transformToString(xform) + ' ';
|
||||
}
|
||||
this._elem.setAttribute('transform', tstr);
|
||||
};
|
||||
this._list = this;
|
||||
this._init = function () {
|
||||
// Transform attribute parser
|
||||
let str = this._elem.getAttribute('transform');
|
||||
if (!str) { return; }
|
||||
|
||||
// TODO: Add skew support in future
|
||||
const re = /\s*((scale|matrix|rotate|translate)\s*\(.*?\))\s*,?\s*/;
|
||||
// const re = /\s*(?<xform>(?:scale|matrix|rotate|translate)\s*\(.*?\))\s*,?\s*/;
|
||||
let m = true;
|
||||
while (m) {
|
||||
m = str.match(re);
|
||||
str = str.replace(re, '');
|
||||
if (m && m[1]) {
|
||||
const x = m[1];
|
||||
const bits = x.split(/\s*\(/);
|
||||
const name = bits[0];
|
||||
const valBits = bits[1].match(/\s*(.*?)\s*\)/);
|
||||
valBits[1] = valBits[1].replace(/(\d)-/g, '$1 -');
|
||||
const valArr = valBits[1].split(/[, ]+/);
|
||||
const letters = 'abcdef'.split('');
|
||||
/*
|
||||
if (m && m.groups.xform) {
|
||||
const x = m.groups.xform;
|
||||
const [name, bits] = x.split(/\s*\(/);
|
||||
const valBits = bits.match(/\s*(?<nonWhitespace>.*?)\s*\)/);
|
||||
valBits.groups.nonWhitespace = valBits.groups.nonWhitespace.replace(
|
||||
/(?<digit>\d)-/g, '$<digit> -'
|
||||
);
|
||||
const valArr = valBits.groups.nonWhitespace.split(/[, ]+/);
|
||||
const letters = [...'abcdef'];
|
||||
*/
|
||||
const mtx = svgroot.createSVGMatrix();
|
||||
Object.values(valArr).forEach(function (item, i) {
|
||||
valArr[i] = Number.parseFloat(item);
|
||||
if (name === 'matrix') {
|
||||
mtx[letters[i]] = valArr[i];
|
||||
}
|
||||
});
|
||||
const xform = svgroot.createSVGTransform();
|
||||
const fname = 'set' + name.charAt(0).toUpperCase() + name.slice(1);
|
||||
const values = name === 'matrix' ? [mtx] : valArr;
|
||||
|
||||
if (name === 'scale' && values.length === 1) {
|
||||
values.push(values[0]);
|
||||
} else if (name === 'translate' && values.length === 1) {
|
||||
values.push(0);
|
||||
} else if (name === 'rotate' && values.length === 1) {
|
||||
values.push(0, 0);
|
||||
}
|
||||
xform[fname](...values);
|
||||
this._list.appendItem(xform);
|
||||
}
|
||||
}
|
||||
};
|
||||
this._removeFromOtherLists = function (item) {
|
||||
if (item) {
|
||||
// Check if this transform is already in a transformlist, and
|
||||
// remove it if so.
|
||||
Object.values(listMap_).some((tl) => {
|
||||
for (let i = 0, len = tl._xforms.length; i < len; ++i) {
|
||||
if (tl._xforms[i] === item) {
|
||||
tl.removeItem(i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
this.numberOfItems = 0;
|
||||
}
|
||||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
clear () {
|
||||
this.numberOfItems = 0;
|
||||
this._xforms = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SVGTransform} newItem
|
||||
* @returns {void}
|
||||
*/
|
||||
initialize (newItem) {
|
||||
this.numberOfItems = 1;
|
||||
this._removeFromOtherLists(newItem);
|
||||
this._xforms = [newItem];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Integer} index unsigned long
|
||||
* @throws {Error}
|
||||
* @returns {SVGTransform}
|
||||
*/
|
||||
getItem (index) {
|
||||
if (index < this.numberOfItems && index >= 0) {
|
||||
return this._xforms[index];
|
||||
}
|
||||
const err = new Error('DOMException with code=INDEX_SIZE_ERR');
|
||||
err.code = 1;
|
||||
throw err;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SVGTransform} newItem
|
||||
* @param {Integer} index unsigned long
|
||||
* @returns {SVGTransform}
|
||||
*/
|
||||
insertItemBefore (newItem, index) {
|
||||
let retValue = null;
|
||||
if (index >= 0) {
|
||||
if (index < this.numberOfItems) {
|
||||
this._removeFromOtherLists(newItem);
|
||||
const newxforms = new Array(this.numberOfItems + 1);
|
||||
// TODO: use array copying and slicing
|
||||
let i;
|
||||
for (i = 0; i < index; ++i) {
|
||||
newxforms[i] = this._xforms[i];
|
||||
}
|
||||
newxforms[i] = newItem;
|
||||
for (let j = i + 1; i < this.numberOfItems; ++j, ++i) {
|
||||
newxforms[j] = this._xforms[i];
|
||||
}
|
||||
this.numberOfItems++;
|
||||
this._xforms = newxforms;
|
||||
retValue = newItem;
|
||||
this._list._update();
|
||||
} else {
|
||||
retValue = this._list.appendItem(newItem);
|
||||
}
|
||||
}
|
||||
return retValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SVGTransform} newItem
|
||||
* @param {Integer} index unsigned long
|
||||
* @returns {SVGTransform}
|
||||
*/
|
||||
replaceItem (newItem, index) {
|
||||
let retValue = null;
|
||||
if (index < this.numberOfItems && index >= 0) {
|
||||
this._removeFromOtherLists(newItem);
|
||||
this._xforms[index] = newItem;
|
||||
retValue = newItem;
|
||||
this._list._update();
|
||||
}
|
||||
return retValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Integer} index unsigned long
|
||||
* @throws {Error}
|
||||
* @returns {SVGTransform}
|
||||
*/
|
||||
removeItem (index) {
|
||||
if (index < this.numberOfItems && index >= 0) {
|
||||
const retValue = this._xforms[index];
|
||||
const newxforms = new Array(this.numberOfItems - 1);
|
||||
let i;
|
||||
for (i = 0; i < index; ++i) {
|
||||
newxforms[i] = this._xforms[i];
|
||||
}
|
||||
for (let j = i; j < this.numberOfItems - 1; ++j, ++i) {
|
||||
newxforms[j] = this._xforms[i + 1];
|
||||
}
|
||||
this.numberOfItems--;
|
||||
this._xforms = newxforms;
|
||||
this._list._update();
|
||||
return retValue;
|
||||
}
|
||||
const err = new Error('DOMException with code=INDEX_SIZE_ERR');
|
||||
err.code = 1;
|
||||
throw err;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SVGTransform} newItem
|
||||
* @returns {SVGTransform}
|
||||
*/
|
||||
appendItem (newItem) {
|
||||
this._removeFromOtherLists(newItem);
|
||||
this._xforms.push(newItem);
|
||||
this.numberOfItems++;
|
||||
this._list._update();
|
||||
return newItem;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @function module:SVGTransformList.resetListMap
|
||||
* @returns {void}
|
||||
*/
|
||||
export const resetListMap = function () {
|
||||
listMap_ = {};
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes transforms of the given element from the map.
|
||||
* @function module:SVGTransformList.removeElementFromListMap
|
||||
* @param {Element} elem - a DOM Element
|
||||
* @returns {void}
|
||||
*/
|
||||
export let removeElementFromListMap = function (elem) { // eslint-disable-line import/no-mutable-exports
|
||||
if (elem.id && listMap_[elem.id]) {
|
||||
delete listMap_[elem.id];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an object that behaves like a `SVGTransformList` for the given DOM element.
|
||||
* @function module:SVGTransformList.getTransformList
|
||||
* @param {Element} elem - DOM element to get a transformlist from
|
||||
* @todo The polyfill should have `SVGAnimatedTransformList` and this should use it
|
||||
* @returns {SVGAnimatedTransformList|SVGTransformList}
|
||||
*/
|
||||
export const getTransformList = function (elem) {
|
||||
if (!supportsNativeTransformLists()) {
|
||||
const id = elem.id || 'temp';
|
||||
let t = listMap_[id];
|
||||
if (!t || id === 'temp') {
|
||||
listMap_[id] = new SVGTransformList(elem);
|
||||
listMap_[id]._init();
|
||||
t = listMap_[id];
|
||||
}
|
||||
return t;
|
||||
}
|
||||
if (elem.transform) {
|
||||
return elem.transform.baseVal;
|
||||
}
|
||||
if (elem.gradientTransform) {
|
||||
return elem.gradientTransform.baseVal;
|
||||
}
|
||||
if (elem.patternTransform) {
|
||||
return elem.patternTransform.baseVal;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* @callback module:SVGTransformList.removeElementFromListMap
|
||||
* @param {Element} elem
|
||||
* @returns {void}
|
||||
*/
|
||||
/**
|
||||
* Replace `removeElementFromListMap` for unit-testing.
|
||||
* @function module:SVGTransformList.changeRemoveElementFromListMap
|
||||
* @param {module:SVGTransformList.removeElementFromListMap} cb Passed a single argument `elem`
|
||||
* @returns {void}
|
||||
*/
|
||||
|
||||
export const changeRemoveElementFromListMap = function (cb) { // eslint-disable-line promise/prefer-await-to-callbacks
|
||||
removeElementFromListMap = cb;
|
||||
};
|
|
@ -0,0 +1,313 @@
|
|||
/**
|
||||
* Tools for working with units.
|
||||
* @module units
|
||||
* @license MIT
|
||||
*
|
||||
* @copyright 2010 Alexis Deveria, 2010 Jeff Schiller
|
||||
*/
|
||||
|
||||
import {NS} from './namespaces.js';
|
||||
|
||||
const wAttrs = ['x', 'x1', 'cx', 'rx', 'width'];
|
||||
const hAttrs = ['y', 'y1', 'cy', 'ry', 'height'];
|
||||
const unitAttrs = ['r', 'radius', ...wAttrs, ...hAttrs];
|
||||
// unused
|
||||
/*
|
||||
const unitNumMap = {
|
||||
'%': 2,
|
||||
em: 3,
|
||||
ex: 4,
|
||||
px: 5,
|
||||
cm: 6,
|
||||
mm: 7,
|
||||
in: 8,
|
||||
pt: 9,
|
||||
pc: 10
|
||||
};
|
||||
*/
|
||||
// Container of elements.
|
||||
let elementContainer_;
|
||||
|
||||
// Stores mapping of unit type to user coordinates.
|
||||
let typeMap_ = {};
|
||||
|
||||
/**
|
||||
* @interface module:units.ElementContainer
|
||||
*/
|
||||
/**
|
||||
* @function module:units.ElementContainer#getBaseUnit
|
||||
* @returns {string} The base unit type of the container ('em')
|
||||
*/
|
||||
/**
|
||||
* @function module:units.ElementContainer#getElement
|
||||
* @returns {?Element} An element in the container given an id
|
||||
*/
|
||||
/**
|
||||
* @function module:units.ElementContainer#getHeight
|
||||
* @returns {Float} The container's height
|
||||
*/
|
||||
/**
|
||||
* @function module:units.ElementContainer#getWidth
|
||||
* @returns {Float} The container's width
|
||||
*/
|
||||
/**
|
||||
* @function module:units.ElementContainer#getRoundDigits
|
||||
* @returns {Integer} The number of digits number should be rounded to
|
||||
*/
|
||||
|
||||
/* eslint-disable jsdoc/valid-types */
|
||||
/**
|
||||
* @typedef {PlainObject} module:units.TypeMap
|
||||
* @property {Float} em
|
||||
* @property {Float} ex
|
||||
* @property {Float} in
|
||||
* @property {Float} cm
|
||||
* @property {Float} mm
|
||||
* @property {Float} pt
|
||||
* @property {Float} pc
|
||||
* @property {Integer} px
|
||||
* @property {0} %
|
||||
*/
|
||||
/* eslint-enable jsdoc/valid-types */
|
||||
|
||||
/**
|
||||
* Initializes this module.
|
||||
*
|
||||
* @function module:units.init
|
||||
* @param {module:units.ElementContainer} elementContainer - An object implementing the ElementContainer interface.
|
||||
* @returns {void}
|
||||
*/
|
||||
export const init = function (elementContainer) {
|
||||
elementContainer_ = elementContainer;
|
||||
|
||||
// Get correct em/ex values by creating a temporary SVG.
|
||||
const svg = document.createElementNS(NS.SVG, 'svg');
|
||||
document.body.append(svg);
|
||||
const rect = document.createElementNS(NS.SVG, 'rect');
|
||||
rect.setAttribute('width', '1em');
|
||||
rect.setAttribute('height', '1ex');
|
||||
rect.setAttribute('x', '1in');
|
||||
svg.append(rect);
|
||||
const bb = rect.getBBox();
|
||||
svg.remove();
|
||||
|
||||
const inch = bb.x;
|
||||
typeMap_ = {
|
||||
em: bb.width,
|
||||
ex: bb.height,
|
||||
in: inch,
|
||||
cm: inch / 2.54,
|
||||
mm: inch / 25.4,
|
||||
pt: inch / 72,
|
||||
pc: inch / 6,
|
||||
px: 1,
|
||||
'%': 0
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Group: Unit conversion functions.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @function module:units.getTypeMap
|
||||
* @returns {module:units.TypeMap} The unit object with values for each unit
|
||||
*/
|
||||
export const getTypeMap = function () {
|
||||
return typeMap_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {GenericArray} module:units.CompareNumbers
|
||||
* @property {Integer} length 2
|
||||
* @property {Float} 0
|
||||
* @property {Float} 1
|
||||
*/
|
||||
|
||||
/**
|
||||
* Rounds a given value to a float with number of digits defined in
|
||||
* `round_digits` of `saveOptions`
|
||||
*
|
||||
* @function module:units.shortFloat
|
||||
* @param {string|Float|module:units.CompareNumbers} val - The value (or Array of two numbers) to be rounded
|
||||
* @returns {Float|string} If a string/number was given, returns a Float. If an array, return a string
|
||||
* with comma-separated floats
|
||||
*/
|
||||
export const shortFloat = function (val) {
|
||||
const digits = elementContainer_.getRoundDigits();
|
||||
if (!isNaN(val)) {
|
||||
return Number(Number(val).toFixed(digits));
|
||||
}
|
||||
if (Array.isArray(val)) {
|
||||
return shortFloat(val[0]) + ',' + shortFloat(val[1]);
|
||||
}
|
||||
return Number.parseFloat(val).toFixed(digits) - 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts the number to given unit or baseUnit.
|
||||
* @function module:units.convertUnit
|
||||
* @param {string|Float} val
|
||||
* @param {"em"|"ex"|"in"|"cm"|"mm"|"pt"|"pc"|"px"|"%"} [unit]
|
||||
* @returns {Float}
|
||||
*/
|
||||
export const convertUnit = function (val, unit) {
|
||||
unit = unit || elementContainer_.getBaseUnit();
|
||||
// baseVal.convertToSpecifiedUnits(unitNumMap[unit]);
|
||||
// const val = baseVal.valueInSpecifiedUnits;
|
||||
// baseVal.convertToSpecifiedUnits(1);
|
||||
return shortFloat(val / typeMap_[unit]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets an element's attribute based on the unit in its current value.
|
||||
*
|
||||
* @function module:units.setUnitAttr
|
||||
* @param {Element} elem - DOM element to be changed
|
||||
* @param {string} attr - Name of the attribute associated with the value
|
||||
* @param {string} val - Attribute value to convert
|
||||
* @returns {void}
|
||||
*/
|
||||
export const setUnitAttr = function (elem, attr, val) {
|
||||
// if (!isNaN(val)) {
|
||||
// New value is a number, so check currently used unit
|
||||
// const oldVal = elem.getAttribute(attr);
|
||||
|
||||
// Enable this for alternate mode
|
||||
// if (oldVal !== null && (isNaN(oldVal) || elementContainer_.getBaseUnit() !== 'px')) {
|
||||
// // Old value was a number, so get unit, then convert
|
||||
// let unit;
|
||||
// if (oldVal.substr(-1) === '%') {
|
||||
// const res = getResolution();
|
||||
// unit = '%';
|
||||
// val *= 100;
|
||||
// if (wAttrs.includes(attr)) {
|
||||
// val = val / res.w;
|
||||
// } else if (hAttrs.includes(attr)) {
|
||||
// val = val / res.h;
|
||||
// } else {
|
||||
// return val / Math.sqrt((res.w*res.w) + (res.h*res.h))/Math.sqrt(2);
|
||||
// }
|
||||
// } else {
|
||||
// if (elementContainer_.getBaseUnit() !== 'px') {
|
||||
// unit = elementContainer_.getBaseUnit();
|
||||
// } else {
|
||||
// unit = oldVal.substr(-2);
|
||||
// }
|
||||
// val = val / typeMap_[unit];
|
||||
// }
|
||||
//
|
||||
// val += unit;
|
||||
// }
|
||||
// }
|
||||
elem.setAttribute(attr, val);
|
||||
};
|
||||
|
||||
const attrsToConvert = {
|
||||
line: ['x1', 'x2', 'y1', 'y2'],
|
||||
circle: ['cx', 'cy', 'r'],
|
||||
ellipse: ['cx', 'cy', 'rx', 'ry'],
|
||||
foreignObject: ['x', 'y', 'width', 'height'],
|
||||
rect: ['x', 'y', 'width', 'height'],
|
||||
image: ['x', 'y', 'width', 'height'],
|
||||
use: ['x', 'y', 'width', 'height'],
|
||||
text: ['x', 'y']
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts all applicable attributes to the configured baseUnit.
|
||||
* @function module:units.convertAttrs
|
||||
* @param {Element} element - A DOM element whose attributes should be converted
|
||||
* @returns {void}
|
||||
*/
|
||||
export const convertAttrs = function (element) {
|
||||
const elName = element.tagName;
|
||||
const unit = elementContainer_.getBaseUnit();
|
||||
const attrs = attrsToConvert[elName];
|
||||
if (!attrs) { return; }
|
||||
|
||||
const len = attrs.length;
|
||||
for (let i = 0; i < len; i++) {
|
||||
const attr = attrs[i];
|
||||
const cur = element.getAttribute(attr);
|
||||
if (cur) {
|
||||
if (!isNaN(cur)) {
|
||||
element.setAttribute(attr, (cur / typeMap_[unit]) + unit);
|
||||
}
|
||||
// else {
|
||||
// Convert existing?
|
||||
// }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts given values to numbers. Attributes must be supplied in
|
||||
* case a percentage is given.
|
||||
*
|
||||
* @function module:units.convertToNum
|
||||
* @param {string} attr - Name of the attribute associated with the value
|
||||
* @param {string} val - Attribute value to convert
|
||||
* @returns {Float} The converted number
|
||||
*/
|
||||
export const convertToNum = function (attr, val) {
|
||||
// Return a number if that's what it already is
|
||||
if (!isNaN(val)) { return val - 0; }
|
||||
if (val.substr(-1) === '%') {
|
||||
// Deal with percentage, depends on attribute
|
||||
const num = val.substr(0, val.length - 1) / 100;
|
||||
const width = elementContainer_.getWidth();
|
||||
const height = elementContainer_.getHeight();
|
||||
|
||||
if (wAttrs.includes(attr)) {
|
||||
return num * width;
|
||||
}
|
||||
if (hAttrs.includes(attr)) {
|
||||
return num * height;
|
||||
}
|
||||
return num * Math.sqrt((width * width) + (height * height)) / Math.sqrt(2);
|
||||
}
|
||||
const unit = val.substr(-2);
|
||||
const num = val.substr(0, val.length - 2);
|
||||
// Note that this multiplication turns the string into a number
|
||||
return num * typeMap_[unit];
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if an attribute's value is in a valid format.
|
||||
* @function module:units.isValidUnit
|
||||
* @param {string} attr - The name of the attribute associated with the value
|
||||
* @param {string} val - The attribute value to check
|
||||
* @param {Element} selectedElement
|
||||
* @returns {boolean} Whether the unit is valid
|
||||
*/
|
||||
export const isValidUnit = function (attr, val, selectedElement) {
|
||||
if (unitAttrs.includes(attr)) {
|
||||
// True if it's just a number
|
||||
if (!isNaN(val)) {
|
||||
return true;
|
||||
}
|
||||
// Not a number, check if it has a valid unit
|
||||
val = val.toLowerCase();
|
||||
return Object.keys(typeMap_).some((unit) => {
|
||||
const re = new RegExp('^-?[\\d\\.]+' + unit + '$');
|
||||
return re.test(val);
|
||||
});
|
||||
}
|
||||
if (attr === 'id') {
|
||||
// if we're trying to change the id, make sure it's not already present in the doc
|
||||
// and the id value is valid.
|
||||
|
||||
let result = false;
|
||||
// because getElem() can throw an exception in the case of an invalid id
|
||||
// (according to https://www.w3.org/TR/xml-id/ IDs must be a NCName)
|
||||
// we wrap it in an exception and only return true if the ID was valid and
|
||||
// not already present
|
||||
try {
|
||||
const elem = elementContainer_.getElement(val);
|
||||
result = (!elem || elem === selectedElement);
|
||||
} catch (e) {}
|
||||
return result;
|
||||
}
|
||||
return true;
|
||||
};
|
|
@ -0,0 +1,54 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="chrome=1"/>
|
||||
<link rel="icon" type="image/png" href="images/logo.png"/>
|
||||
<link rel="stylesheet" href="svg-editor.css"/>
|
||||
<title>Browser does not support SVG | SVG-edit</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
p {
|
||||
font-size: 0.8em;
|
||||
font-family: Verdana, Helvetica, Arial;
|
||||
color: #000;
|
||||
padding: 8px;
|
||||
margin: 0;
|
||||
}
|
||||
#logo {
|
||||
float: left;
|
||||
padding: 10px;
|
||||
}
|
||||
#caniuse {
|
||||
position: absolute;
|
||||
top: 7em;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
#caniuse > iframe {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<img id="logo" src="images/logo.png" width="48" height="48" alt="SVG-edit logo" />
|
||||
<p>Sorry, but your browser does not support SVG. Below is a list of
|
||||
alternate browsers and versions that support SVG and SVG-edit
|
||||
(from <a href="https://caniuse.com/#cats=SVG">caniuse.com</a>).
|
||||
</p>
|
||||
<p>Try the latest version of
|
||||
<a href="https://www.getfirefox.com">Firefox</a>,
|
||||
<a href="https://www.google.com/chrome/">Chrome</a>,
|
||||
<a href="https://www.apple.com/safari/">Safari</a>,
|
||||
<a href="https://www.opera.com/download">Opera</a> or
|
||||
<a href="https://support.microsoft.com/en-us/help/17621/internet-explorer-downloads">Internet Explorer</a>.
|
||||
</p>
|
||||
<div id="caniuse">
|
||||
<iframe src="https://caniuse.com/#cats=SVG"></iframe>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Embed API</title>
|
||||
<link rel="icon" type="image/png" href="images/logo.png"/>
|
||||
<script src="jquery.min.js"></script>
|
||||
<script type="module" src="embedapi-dom.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<button id="load">Load example</button>
|
||||
<button id="save">Save data</button>
|
||||
<button id="exportPNG">Export data to PNG</button>
|
||||
<button id="exportPDF">Export data to PDF</button>
|
||||
<br/>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,395 @@
|
|||
/**
|
||||
* Handles underlying communication between the embedding window and the
|
||||
* editor frame.
|
||||
* @module EmbeddedSVGEdit
|
||||
*/
|
||||
|
||||
let cbid = 0;
|
||||
|
||||
/**
|
||||
* @callback module:EmbeddedSVGEdit.CallbackSetter
|
||||
* @param {GenericCallback} newCallback Callback to be stored (signature dependent on function)
|
||||
* @returns {void}
|
||||
*/
|
||||
/**
|
||||
* @callback module:EmbeddedSVGEdit.CallbackSetGetter
|
||||
* @param {...any} args Signature dependent on the function
|
||||
* @returns {module:EmbeddedSVGEdit.CallbackSetter}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} funcName
|
||||
* @returns {module:EmbeddedSVGEdit.CallbackSetGetter}
|
||||
*/
|
||||
function getCallbackSetter (funcName) {
|
||||
return function (...args) {
|
||||
const that = this, // New callback
|
||||
callbackID = this.send(funcName, args, function () { /* */ }); // The callback (currently it's nothing, but will be set later)
|
||||
|
||||
return function (newCallback) {
|
||||
that.callbacks[callbackID] = newCallback; // Set callback
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Having this separate from messageListener allows us to
|
||||
* avoid using JSON parsing (and its limitations) in the case
|
||||
* of same domain control.
|
||||
* @param {module:EmbeddedSVGEdit.EmbeddedSVGEdit} t The `this` value
|
||||
* @param {PlainObject} data
|
||||
* @param {JSON} data.result
|
||||
* @param {string} data.error
|
||||
* @param {Integer} data.id
|
||||
* @returns {void}
|
||||
*/
|
||||
function addCallback (t, {result, error, id: callbackID}) {
|
||||
if (typeof callbackID === 'number' && t.callbacks[callbackID]) {
|
||||
// These should be safe both because we check `cbid` is numeric and
|
||||
// because the calls are from trusted origins
|
||||
if (result) {
|
||||
t.callbacks[callbackID](result); // lgtm [js/unvalidated-dynamic-method-call]
|
||||
} else {
|
||||
t.callbacks[callbackID](error, 'error'); // lgtm [js/unvalidated-dynamic-method-call]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Event} e
|
||||
* @returns {void}
|
||||
*/
|
||||
function messageListener (e) {
|
||||
// We accept and post strings as opposed to objects for the sake of IE9 support; this
|
||||
// will most likely be changed in the future
|
||||
if (!e.data || !['string', 'object'].includes(typeof e.data)) {
|
||||
return;
|
||||
}
|
||||
const {allowedOrigins} = this,
|
||||
data = typeof e.data === 'object' ? e.data : JSON.parse(e.data);
|
||||
if (!data || typeof data !== 'object' || data.namespace !== 'svg-edit' ||
|
||||
e.source !== this.frame.contentWindow ||
|
||||
(!allowedOrigins.includes('*') && !allowedOrigins.includes(e.origin))
|
||||
) {
|
||||
// eslint-disable-next-line no-console -- Info for developers
|
||||
console.error(
|
||||
`The origin ${e.origin} was not whitelisted as an origin from ` +
|
||||
`which responses may be received by this ${window.origin} script.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
addCallback(this, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @callback module:EmbeddedSVGEdit.MessageListener
|
||||
* @param {MessageEvent} e
|
||||
* @returns {void}
|
||||
*/
|
||||
/**
|
||||
* @param {module:EmbeddedSVGEdit.EmbeddedSVGEdit} t The `this` value
|
||||
* @returns {module:EmbeddedSVGEdit.MessageListener} Event listener
|
||||
*/
|
||||
function getMessageListener (t) {
|
||||
return function (e) {
|
||||
messageListener.call(t, e);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Embedded SVG-edit API.
|
||||
* General usage:
|
||||
* - Have an iframe somewhere pointing to a version of svg-edit > r1000.
|
||||
* @example
|
||||
// Initialize the magic with:
|
||||
const svgCanvas = new EmbeddedSVGEdit(window.frames.svgedit);
|
||||
|
||||
// Pass functions in this format:
|
||||
svgCanvas.setSvgString('string');
|
||||
|
||||
// Or if a callback is needed:
|
||||
svgCanvas.setSvgString('string')(function (data, error) {
|
||||
if (error) {
|
||||
// There was an error
|
||||
} else {
|
||||
// Handle data
|
||||
}
|
||||
});
|
||||
|
||||
// Everything is done with the same API as the real svg-edit,
|
||||
// and all documentation is unchanged.
|
||||
|
||||
// However, this file depends on the postMessage API which
|
||||
// can only support JSON-serializable arguments and
|
||||
// return values, so, for example, arguments whose value is
|
||||
// 'undefined', a function, a non-finite number, or a built-in
|
||||
// object like Date(), RegExp(), etc. will most likely not behave
|
||||
// as expected. In such a case one may need to host
|
||||
// the SVG editor on the same domain and reference the
|
||||
// JavaScript methods on the frame itself.
|
||||
|
||||
// The only other difference is when handling returns:
|
||||
// the callback notation is used instead.
|
||||
const blah = new EmbeddedSVGEdit(window.frames.svgedit);
|
||||
blah.clearSelection('woot', 'blah', 1337, [1, 2, 3, 4, 5, 'moo'], -42, {
|
||||
a: 'tree', b: 6, c: 9
|
||||
})(function () { console.log('GET DATA', args); });
|
||||
*
|
||||
* @memberof module:EmbeddedSVGEdit
|
||||
*/
|
||||
class EmbeddedSVGEdit {
|
||||
/**
|
||||
* @param {HTMLIFrameElement} frame
|
||||
* @param {string[]} [allowedOrigins=[]] Array of origins from which incoming
|
||||
* messages will be allowed when same origin is not used; defaults to none.
|
||||
* If supplied, it should probably be the same as svgEditor's allowedOrigins
|
||||
*/
|
||||
constructor (frame, allowedOrigins) {
|
||||
const that = this;
|
||||
this.allowedOrigins = allowedOrigins || [];
|
||||
// Initialize communication
|
||||
this.frame = frame;
|
||||
this.callbacks = {};
|
||||
// List of functions extracted with this:
|
||||
// Run in firebug on http://svg-edit.googlecode.com/svn/trunk/docs/files/svgcanvas-js.html
|
||||
|
||||
// for (const i=0,q=[],f = document.querySelectorAll('div.CFunction h3.CTitle a'); i < f.length; i++) { q.push(f[i].name); }; q
|
||||
// const functions = ['clearSelection', 'addToSelection', 'removeFromSelection', 'open', 'save', 'getSvgString', 'setSvgString',
|
||||
// 'createLayer', 'deleteCurrentLayer', 'setCurrentLayer', 'renameCurrentLayer', 'setCurrentLayerPosition', 'setLayerVisibility',
|
||||
// 'moveSelectedToLayer', 'clear'];
|
||||
|
||||
// Newer, well, it extracts things that aren't documented as well. All functions accessible through the normal thingy can now be accessed though the API
|
||||
// const {svgCanvas} = frame.contentWindow;
|
||||
// const l = [];
|
||||
// for (const i in svgCanvas) { if (typeof svgCanvas[i] === 'function') { l.push(i);} };
|
||||
// alert("['" + l.join("', '") + "']");
|
||||
// Run in svgedit itself
|
||||
const functions = [
|
||||
'addExtension',
|
||||
'addSVGElementFromJson',
|
||||
'addToSelection',
|
||||
'alignSelectedElements',
|
||||
'assignAttributes',
|
||||
'bind',
|
||||
'call',
|
||||
'changeSelectedAttribute',
|
||||
'cleanupElement',
|
||||
'clear',
|
||||
'clearSelection',
|
||||
'clearSvgContentElement',
|
||||
'cloneLayer',
|
||||
'cloneSelectedElements',
|
||||
'convertGradients',
|
||||
'convertToGroup',
|
||||
'convertToNum',
|
||||
'convertToPath',
|
||||
'copySelectedElements',
|
||||
'createLayer',
|
||||
'cutSelectedElements',
|
||||
'cycleElement',
|
||||
'deleteCurrentLayer',
|
||||
'deleteSelectedElements',
|
||||
'embedImage',
|
||||
'exportPDF',
|
||||
'findDefs',
|
||||
'getBBox',
|
||||
'getBlur',
|
||||
'getBold',
|
||||
'getColor',
|
||||
'getContentElem',
|
||||
'getCurrentDrawing',
|
||||
'getDocumentTitle',
|
||||
'getEditorNS',
|
||||
'getElem',
|
||||
'getFillOpacity',
|
||||
'getFontColor',
|
||||
'getFontFamily',
|
||||
'getFontSize',
|
||||
'getHref',
|
||||
'getId',
|
||||
'getIntersectionList',
|
||||
'getItalic',
|
||||
'getMode',
|
||||
'getMouseTarget',
|
||||
'getNextId',
|
||||
'getOffset',
|
||||
'getOpacity',
|
||||
'getPaintOpacity',
|
||||
'getPrivateMethods',
|
||||
'getRefElem',
|
||||
'getResolution',
|
||||
'getRootElem',
|
||||
'getRotationAngle',
|
||||
'getSelectedElems',
|
||||
'getStrokeOpacity',
|
||||
'getStrokeWidth',
|
||||
'getStrokedBBox',
|
||||
'getStyle',
|
||||
'getSvgString',
|
||||
'getText',
|
||||
'getTitle',
|
||||
'getTransformList',
|
||||
'getUIStrings',
|
||||
'getUrlFromAttr',
|
||||
'getVersion',
|
||||
'getVisibleElements',
|
||||
'getVisibleElementsAndBBoxes',
|
||||
'getZoom',
|
||||
'groupSelectedElements',
|
||||
'groupSvgElem',
|
||||
'hasMatrixTransform',
|
||||
'identifyLayers',
|
||||
'importSvgString',
|
||||
'leaveContext',
|
||||
'linkControlPoints',
|
||||
'makeHyperlink',
|
||||
'matrixMultiply',
|
||||
'mergeAllLayers',
|
||||
'mergeLayer',
|
||||
'moveSelectedElements',
|
||||
'moveSelectedToLayer',
|
||||
'moveToBottomSelectedElement',
|
||||
'moveToTopSelectedElement',
|
||||
'moveUpDownSelected',
|
||||
'open',
|
||||
'pasteElements',
|
||||
'prepareSvg',
|
||||
'pushGroupProperties',
|
||||
'randomizeIds',
|
||||
'rasterExport',
|
||||
'ready',
|
||||
'recalculateAllSelectedDimensions',
|
||||
'recalculateDimensions',
|
||||
'remapElement',
|
||||
'removeFromSelection',
|
||||
'removeHyperlink',
|
||||
'removeUnusedDefElems',
|
||||
'renameCurrentLayer',
|
||||
'round',
|
||||
'runExtensions',
|
||||
'sanitizeSvg',
|
||||
'save',
|
||||
'selectAllInCurrentLayer',
|
||||
'selectOnly',
|
||||
'setBBoxZoom',
|
||||
'setBackground',
|
||||
'setBlur',
|
||||
'setBlurNoUndo',
|
||||
'setBlurOffsets',
|
||||
'setBold',
|
||||
'setColor',
|
||||
'setConfig',
|
||||
'setContext',
|
||||
'setCurrentLayer',
|
||||
'setCurrentLayerPosition',
|
||||
'setDocumentTitle',
|
||||
'setFillPaint',
|
||||
'setFontColor',
|
||||
'setFontFamily',
|
||||
'setFontSize',
|
||||
'setGoodImage',
|
||||
'setGradient',
|
||||
'setGroupTitle',
|
||||
'setHref',
|
||||
'setIdPrefix',
|
||||
'setImageURL',
|
||||
'setItalic',
|
||||
'setLayerVisibility',
|
||||
'setLinkURL',
|
||||
'setMode',
|
||||
'setOpacity',
|
||||
'setPaint',
|
||||
'setPaintOpacity',
|
||||
'setRectRadius',
|
||||
'setResolution',
|
||||
'setRotationAngle',
|
||||
'setSegType',
|
||||
'setStrokeAttr',
|
||||
'setStrokePaint',
|
||||
'setStrokeWidth',
|
||||
'setSvgString',
|
||||
'setTextContent',
|
||||
'setUiStrings',
|
||||
'setUseData',
|
||||
'setZoom',
|
||||
'svgCanvasToString',
|
||||
'svgToString',
|
||||
'transformListToTransform',
|
||||
'ungroupSelectedElement',
|
||||
'uniquifyElems',
|
||||
'updateCanvas',
|
||||
'zoomChanged'
|
||||
];
|
||||
|
||||
// TODO: rewrite the following, it's pretty scary.
|
||||
for (const func of functions) {
|
||||
this[func] = getCallbackSetter(func);
|
||||
}
|
||||
|
||||
// Older IE may need a polyfill for addEventListener, but so it would for SVG
|
||||
window.addEventListener('message', getMessageListener(this));
|
||||
window.addEventListener('keydown', (e) => {
|
||||
const {type, key} = e;
|
||||
if (key === 'Backspace') {
|
||||
e.preventDefault();
|
||||
const keyboardEvent = new KeyboardEvent(type, {key});
|
||||
that.frame.contentDocument.dispatchEvent(keyboardEvent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {ArgumentsArray} args Signature dependent on function
|
||||
* @param {GenericCallback} callback (This may be better than a promise in case adding an event.)
|
||||
* @returns {Integer}
|
||||
*/
|
||||
send (name, args, callback) { // eslint-disable-line promise/prefer-await-to-callbacks
|
||||
const that = this;
|
||||
cbid++;
|
||||
|
||||
this.callbacks[cbid] = callback;
|
||||
setTimeout((function (callbackID) {
|
||||
return function () { // Delay for the callback to be set in case its synchronous
|
||||
/*
|
||||
* Todo: Handle non-JSON arguments and return values (undefined,
|
||||
* nonfinite numbers, functions, and built-in objects like Date,
|
||||
* RegExp), etc.? Allow promises instead of callbacks? Review
|
||||
* SVG-Edit functions for whether JSON-able parameters can be
|
||||
* made compatile with all API functionality
|
||||
*/
|
||||
// We accept and post strings for the sake of IE9 support
|
||||
let sameOriginWithGlobal = false;
|
||||
try {
|
||||
sameOriginWithGlobal = window.location.origin === that.frame.contentWindow.location.origin &&
|
||||
that.frame.contentWindow.svgEditor.canvas;
|
||||
} catch (err) {}
|
||||
|
||||
if (sameOriginWithGlobal) {
|
||||
// Although we do not really need this API if we are working same
|
||||
// domain, it could allow us to write in a way that would work
|
||||
// cross-domain as well, assuming we stick to the argument limitations
|
||||
// of the current JSON-based communication API (e.g., not passing
|
||||
// callbacks). We might be able to address these shortcomings; see
|
||||
// the todo elsewhere in this file.
|
||||
const message = {id: callbackID},
|
||||
{svgEditor: {canvas: svgCanvas}} = that.frame.contentWindow;
|
||||
try {
|
||||
message.result = svgCanvas[name](...args);
|
||||
} catch (err) {
|
||||
message.error = err.message;
|
||||
}
|
||||
addCallback(that, message);
|
||||
} else { // Requires the ext-xdomain-messaging.js extension
|
||||
that.frame.contentWindow.postMessage(JSON.stringify({
|
||||
namespace: 'svgCanvas', id: callbackID, name, args
|
||||
}), '*');
|
||||
}
|
||||
};
|
||||
}(cbid)), 0);
|
||||
|
||||
return cbid;
|
||||
}
|
||||
}
|
||||
|
||||
export default EmbeddedSVGEdit;
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.8 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 789 B After Width: | Height: | Size: 789 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 568 B After Width: | Height: | Size: 568 B |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.4 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |