2025-04-16 02:11:26 +01:00

7075 lines
174 KiB
JavaScript

/*!
* @svgdotjs/svg.js - A lightweight library for manipulating and animating SVG.
* @version 3.2.0
* https://svgjs.dev/
*
* @copyright Wout Fierens <wout@mick-wout.com>
* @license MIT
*
* BUILT: Mon Jun 12 2023 10:34:51 GMT+0200 (Central European Summer Time)
*/;
const methods$1 = {};
const names = [];
function registerMethods(name, m) {
if (Array.isArray(name)) {
for (const _name of name) {
registerMethods(_name, m);
}
return;
}
if (typeof name === 'object') {
for (const _name in name) {
registerMethods(_name, name[_name]);
}
return;
}
addMethodNames(Object.getOwnPropertyNames(m));
methods$1[name] = Object.assign(methods$1[name] || {}, m);
}
function getMethodsFor(name) {
return methods$1[name] || {};
}
function getMethodNames() {
return [...new Set(names)];
}
function addMethodNames(_names) {
names.push(..._names);
}
// Map function
function map(array, block) {
let i;
const il = array.length;
const result = [];
for (i = 0; i < il; i++) {
result.push(block(array[i]));
}
return result;
} // Filter function
function filter(array, block) {
let i;
const il = array.length;
const result = [];
for (i = 0; i < il; i++) {
if (block(array[i])) {
result.push(array[i]);
}
}
return result;
} // Degrees to radians
function radians(d) {
return d % 360 * Math.PI / 180;
} // Radians to degrees
function degrees(r) {
return r * 180 / Math.PI % 360;
} // Convert dash-separated-string to camelCase
function camelCase(s) {
return s.toLowerCase().replace(/-(.)/g, function (m, g) {
return g.toUpperCase();
});
} // Convert camel cased string to dash separated
function unCamelCase(s) {
return s.replace(/([A-Z])/g, function (m, g) {
return '-' + g.toLowerCase();
});
} // Capitalize first letter of a string
function capitalize(s) {
return s.charAt(0).toUpperCase() + s.slice(1);
} // Calculate proportional width and height values when necessary
function proportionalSize(element, width, height, box) {
if (width == null || height == null) {
box = box || element.bbox();
if (width == null) {
width = box.width / box.height * height;
} else if (height == null) {
height = box.height / box.width * width;
}
}
return {
width: width,
height: height
};
}
/**
* This function adds support for string origins.
* It searches for an origin in o.origin o.ox and o.originX.
* This way, origin: {x: 'center', y: 50} can be passed as well as ox: 'center', oy: 50
**/
function getOrigin(o, element) {
const origin = o.origin; // First check if origin is in ox or originX
let ox = o.ox != null ? o.ox : o.originX != null ? o.originX : 'center';
let oy = o.oy != null ? o.oy : o.originY != null ? o.originY : 'center'; // Then check if origin was used and overwrite in that case
if (origin != null) {
[ox, oy] = Array.isArray(origin) ? origin : typeof origin === 'object' ? [origin.x, origin.y] : [origin, origin];
} // Make sure to only call bbox when actually needed
const condX = typeof ox === 'string';
const condY = typeof oy === 'string';
if (condX || condY) {
const {
height,
width,
x,
y
} = element.bbox(); // And only overwrite if string was passed for this specific axis
if (condX) {
ox = ox.includes('left') ? x : ox.includes('right') ? x + width : x + width / 2;
}
if (condY) {
oy = oy.includes('top') ? y : oy.includes('bottom') ? y + height : y + height / 2;
}
} // Return the origin as it is if it wasn't a string
return [ox, oy];
}
var utils = {
__proto__: null,
map: map,
filter: filter,
radians: radians,
degrees: degrees,
camelCase: camelCase,
unCamelCase: unCamelCase,
capitalize: capitalize,
proportionalSize: proportionalSize,
getOrigin: getOrigin
};
// Default namespaces
const svg = 'http://www.w3.org/2000/svg';
const html = 'http://www.w3.org/1999/xhtml';
const xmlns = 'http://www.w3.org/2000/xmlns/';
const xlink = 'http://www.w3.org/1999/xlink';
const svgjs = 'http://svgjs.dev/svgjs';
var namespaces = {
__proto__: null,
svg: svg,
html: html,
xmlns: xmlns,
xlink: xlink,
svgjs: svgjs
};
const globals = {
window: typeof window === 'undefined' ? null : window,
document: typeof document === 'undefined' ? null : document
};
function registerWindow(win = null, doc = null) {
globals.window = win;
globals.document = doc;
}
const save = {};
function saveWindow() {
save.window = globals.window;
save.document = globals.document;
}
function restoreWindow() {
globals.window = save.window;
globals.document = save.document;
}
function withWindow(win, fn) {
saveWindow();
registerWindow(win, win.document);
fn(win, win.document);
restoreWindow();
}
function getWindow() {
return globals.window;
}
class Base {// constructor (node/*, {extensions = []} */) {
// // this.tags = []
// //
// // for (let extension of extensions) {
// // extension.setup.call(this, node)
// // this.tags.push(extension.name)
// // }
// }
}
const elements = {};
const root = '___SYMBOL___ROOT___'; // Method for element creation
function create(name, ns = svg) {
// create element
return globals.document.createElementNS(ns, name);
}
function makeInstance(element, isHTML = false) {
if (element instanceof Base) return element;
if (typeof element === 'object') {
return adopter(element);
}
if (element == null) {
return new elements[root]();
}
if (typeof element === 'string' && element.charAt(0) !== '<') {
return adopter(globals.document.querySelector(element));
} // Make sure, that HTML elements are created with the correct namespace
const wrapper = isHTML ? globals.document.createElement('div') : create('svg');
wrapper.innerHTML = element; // We can use firstChild here because we know,
// that the first char is < and thus an element
element = adopter(wrapper.firstChild); // make sure, that element doesn't have its wrapper attached
wrapper.removeChild(wrapper.firstChild);
return element;
}
function nodeOrNew(name, node) {
return node && node.ownerDocument && node instanceof node.ownerDocument.defaultView.Node ? node : create(name);
} // Adopt existing svg elements
function adopt(node) {
// check for presence of node
if (!node) return null; // make sure a node isn't already adopted
if (node.instance instanceof Base) return node.instance;
if (node.nodeName === '#document-fragment') {
return new elements.Fragment(node);
} // initialize variables
let className = capitalize(node.nodeName || 'Dom'); // Make sure that gradients are adopted correctly
if (className === 'LinearGradient' || className === 'RadialGradient') {
className = 'Gradient'; // Fallback to Dom if element is not known
} else if (!elements[className]) {
className = 'Dom';
}
return new elements[className](node);
}
let adopter = adopt;
function mockAdopt(mock = adopt) {
adopter = mock;
}
function register(element, name = element.name, asRoot = false) {
elements[name] = element;
if (asRoot) elements[root] = element;
addMethodNames(Object.getOwnPropertyNames(element.prototype));
return element;
}
function getClass(name) {
return elements[name];
} // Element id sequence
let did = 1000; // Get next named element id
function eid(name) {
return 'Svgjs' + capitalize(name) + did++;
} // Deep new id assignment
function assignNewId(node) {
// do the same for SVG child nodes as well
for (let i = node.children.length - 1; i >= 0; i--) {
assignNewId(node.children[i]);
}
if (node.id) {
node.id = eid(node.nodeName);
return node;
}
return node;
} // Method for extending objects
function extend(modules, methods) {
let key, i;
modules = Array.isArray(modules) ? modules : [modules];
for (i = modules.length - 1; i >= 0; i--) {
for (key in methods) {
modules[i].prototype[key] = methods[key];
}
}
}
function wrapWithAttrCheck(fn) {
return function (...args) {
const o = args[args.length - 1];
if (o && o.constructor === Object && !(o instanceof Array)) {
return fn.apply(this, args.slice(0, -1)).attr(o);
} else {
return fn.apply(this, args);
}
};
}
function siblings() {
return this.parent().children();
} // Get the current position siblings
function position() {
return this.parent().index(this);
} // Get the next element (will return null if there is none)
function next() {
return this.siblings()[this.position() + 1];
} // Get the next element (will return null if there is none)
function prev() {
return this.siblings()[this.position() - 1];
} // Send given element one step forward
function forward() {
const i = this.position();
const p = this.parent(); // move node one step forward
p.add(this.remove(), i + 1);
return this;
} // Send given element one step backward
function backward() {
const i = this.position();
const p = this.parent();
p.add(this.remove(), i ? i - 1 : 0);
return this;
} // Send given element all the way to the front
function front() {
const p = this.parent(); // Move node forward
p.add(this.remove());
return this;
} // Send given element all the way to the back
function back() {
const p = this.parent(); // Move node back
p.add(this.remove(), 0);
return this;
} // Inserts a given element before the targeted element
function before(element) {
element = makeInstance(element);
element.remove();
const i = this.position();
this.parent().add(element, i);
return this;
} // Inserts a given element after the targeted element
function after(element) {
element = makeInstance(element);
element.remove();
const i = this.position();
this.parent().add(element, i + 1);
return this;
}
function insertBefore(element) {
element = makeInstance(element);
element.before(this);
return this;
}
function insertAfter(element) {
element = makeInstance(element);
element.after(this);
return this;
}
registerMethods('Dom', {
siblings,
position,
next,
prev,
forward,
backward,
front,
back,
before,
after,
insertBefore,
insertAfter
});
// Parse unit value
const numberAndUnit = /^([+-]?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?)([a-z%]*)$/i; // Parse hex value
const hex = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i; // Parse rgb value
const rgb = /rgb\((\d+),(\d+),(\d+)\)/; // Parse reference id
const reference = /(#[a-z_][a-z0-9\-_]*)/i; // splits a transformation chain
const transforms = /\)\s*,?\s*/; // Whitespace
const whitespace = /\s/g; // Test hex value
const isHex = /^#[a-f0-9]{3}$|^#[a-f0-9]{6}$/i; // Test rgb value
const isRgb = /^rgb\(/; // Test for blank string
const isBlank = /^(\s+)?$/; // Test for numeric string
const isNumber = /^[+-]?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i; // Test for image url
const isImage = /\.(jpg|jpeg|png|gif|svg)(\?[^=]+.*)?/i; // split at whitespace and comma
const delimiter = /[\s,]+/; // Test for path letter
const isPathLetter = /[MLHVCSQTAZ]/i;
var regex = {
__proto__: null,
numberAndUnit: numberAndUnit,
hex: hex,
rgb: rgb,
reference: reference,
transforms: transforms,
whitespace: whitespace,
isHex: isHex,
isRgb: isRgb,
isBlank: isBlank,
isNumber: isNumber,
isImage: isImage,
delimiter: delimiter,
isPathLetter: isPathLetter
};
function classes() {
const attr = this.attr('class');
return attr == null ? [] : attr.trim().split(delimiter);
} // Return true if class exists on the node, false otherwise
function hasClass(name) {
return this.classes().indexOf(name) !== -1;
} // Add class to the node
function addClass(name) {
if (!this.hasClass(name)) {
const array = this.classes();
array.push(name);
this.attr('class', array.join(' '));
}
return this;
} // Remove class from the node
function removeClass(name) {
if (this.hasClass(name)) {
this.attr('class', this.classes().filter(function (c) {
return c !== name;
}).join(' '));
}
return this;
} // Toggle the presence of a class on the node
function toggleClass(name) {
return this.hasClass(name) ? this.removeClass(name) : this.addClass(name);
}
registerMethods('Dom', {
classes,
hasClass,
addClass,
removeClass,
toggleClass
});
function css(style, val) {
const ret = {};
if (arguments.length === 0) {
// get full style as object
this.node.style.cssText.split(/\s*;\s*/).filter(function (el) {
return !!el.length;
}).forEach(function (el) {
const t = el.split(/\s*:\s*/);
ret[t[0]] = t[1];
});
return ret;
}
if (arguments.length < 2) {
// get style properties as array
if (Array.isArray(style)) {
for (const name of style) {
const cased = camelCase(name);
ret[name] = this.node.style[cased];
}
return ret;
} // get style for property
if (typeof style === 'string') {
return this.node.style[camelCase(style)];
} // set styles in object
if (typeof style === 'object') {
for (const name in style) {
// set empty string if null/undefined/'' was given
this.node.style[camelCase(name)] = style[name] == null || isBlank.test(style[name]) ? '' : style[name];
}
}
} // set style for property
if (arguments.length === 2) {
this.node.style[camelCase(style)] = val == null || isBlank.test(val) ? '' : val;
}
return this;
} // Show element
function show() {
return this.css('display', '');
} // Hide element
function hide() {
return this.css('display', 'none');
} // Is element visible?
function visible() {
return this.css('display') !== 'none';
}
registerMethods('Dom', {
css,
show,
hide,
visible
});
function data(a, v, r) {
if (a == null) {
// get an object of attributes
return this.data(map(filter(this.node.attributes, el => el.nodeName.indexOf('data-') === 0), el => el.nodeName.slice(5)));
} else if (a instanceof Array) {
const data = {};
for (const key of a) {
data[key] = this.data(key);
}
return data;
} else if (typeof a === 'object') {
for (v in a) {
this.data(v, a[v]);
}
} else if (arguments.length < 2) {
try {
return JSON.parse(this.attr('data-' + a));
} catch (e) {
return this.attr('data-' + a);
}
} else {
this.attr('data-' + a, v === null ? null : r === true || typeof v === 'string' || typeof v === 'number' ? v : JSON.stringify(v));
}
return this;
}
registerMethods('Dom', {
data
});
function remember(k, v) {
// remember every item in an object individually
if (typeof arguments[0] === 'object') {
for (const key in k) {
this.remember(key, k[key]);
}
} else if (arguments.length === 1) {
// retrieve memory
return this.memory()[k];
} else {
// store memory
this.memory()[k] = v;
}
return this;
} // Erase a given memory
function forget() {
if (arguments.length === 0) {
this._memory = {};
} else {
for (let i = arguments.length - 1; i >= 0; i--) {
delete this.memory()[arguments[i]];
}
}
return this;
} // This triggers creation of a new hidden class which is not performant
// However, this function is not rarely used so it will not happen frequently
// Return local memory object
function memory() {
return this._memory = this._memory || {};
}
registerMethods('Dom', {
remember,
forget,
memory
});
function sixDigitHex(hex) {
return hex.length === 4 ? ['#', hex.substring(1, 2), hex.substring(1, 2), hex.substring(2, 3), hex.substring(2, 3), hex.substring(3, 4), hex.substring(3, 4)].join('') : hex;
}
function componentHex(component) {
const integer = Math.round(component);
const bounded = Math.max(0, Math.min(255, integer));
const hex = bounded.toString(16);
return hex.length === 1 ? '0' + hex : hex;
}
function is(object, space) {
for (let i = space.length; i--;) {
if (object[space[i]] == null) {
return false;
}
}
return true;
}
function getParameters(a, b) {
const params = is(a, 'rgb') ? {
_a: a.r,
_b: a.g,
_c: a.b,
_d: 0,
space: 'rgb'
} : is(a, 'xyz') ? {
_a: a.x,
_b: a.y,
_c: a.z,
_d: 0,
space: 'xyz'
} : is(a, 'hsl') ? {
_a: a.h,
_b: a.s,
_c: a.l,
_d: 0,
space: 'hsl'
} : is(a, 'lab') ? {
_a: a.l,
_b: a.a,
_c: a.b,
_d: 0,
space: 'lab'
} : is(a, 'lch') ? {
_a: a.l,
_b: a.c,
_c: a.h,
_d: 0,
space: 'lch'
} : is(a, 'cmyk') ? {
_a: a.c,
_b: a.m,
_c: a.y,
_d: a.k,
space: 'cmyk'
} : {
_a: 0,
_b: 0,
_c: 0,
space: 'rgb'
};
params.space = b || params.space;
return params;
}
function cieSpace(space) {
if (space === 'lab' || space === 'xyz' || space === 'lch') {
return true;
} else {
return false;
}
}
function hueToRgb(p, q, t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
}
class Color {
constructor(...inputs) {
this.init(...inputs);
} // Test if given value is a color
static isColor(color) {
return color && (color instanceof Color || this.isRgb(color) || this.test(color));
} // Test if given value is an rgb object
static isRgb(color) {
return color && typeof color.r === 'number' && typeof color.g === 'number' && typeof color.b === 'number';
}
/*
Generating random colors
*/
static random(mode = 'vibrant', t, u) {
// Get the math modules
const {
random,
round,
sin,
PI: pi
} = Math; // Run the correct generator
if (mode === 'vibrant') {
const l = (81 - 57) * random() + 57;
const c = (83 - 45) * random() + 45;
const h = 360 * random();
const color = new Color(l, c, h, 'lch');
return color;
} else if (mode === 'sine') {
t = t == null ? random() : t;
const r = round(80 * sin(2 * pi * t / 0.5 + 0.01) + 150);
const g = round(50 * sin(2 * pi * t / 0.5 + 4.6) + 200);
const b = round(100 * sin(2 * pi * t / 0.5 + 2.3) + 150);
const color = new Color(r, g, b);
return color;
} else if (mode === 'pastel') {
const l = (94 - 86) * random() + 86;
const c = (26 - 9) * random() + 9;
const h = 360 * random();
const color = new Color(l, c, h, 'lch');
return color;
} else if (mode === 'dark') {
const l = 10 + 10 * random();
const c = (125 - 75) * random() + 86;
const h = 360 * random();
const color = new Color(l, c, h, 'lch');
return color;
} else if (mode === 'rgb') {
const r = 255 * random();
const g = 255 * random();
const b = 255 * random();
const color = new Color(r, g, b);
return color;
} else if (mode === 'lab') {
const l = 100 * random();
const a = 256 * random() - 128;
const b = 256 * random() - 128;
const color = new Color(l, a, b, 'lab');
return color;
} else if (mode === 'grey') {
const grey = 255 * random();
const color = new Color(grey, grey, grey);
return color;
} else {
throw new Error('Unsupported random color mode');
}
} // Test if given value is a color string
static test(color) {
return typeof color === 'string' && (isHex.test(color) || isRgb.test(color));
}
cmyk() {
// Get the rgb values for the current color
const {
_a,
_b,
_c
} = this.rgb();
const [r, g, b] = [_a, _b, _c].map(v => v / 255); // Get the cmyk values in an unbounded format
const k = Math.min(1 - r, 1 - g, 1 - b);
if (k === 1) {
// Catch the black case
return new Color(0, 0, 0, 1, 'cmyk');
}
const c = (1 - r - k) / (1 - k);
const m = (1 - g - k) / (1 - k);
const y = (1 - b - k) / (1 - k); // Construct the new color
const color = new Color(c, m, y, k, 'cmyk');
return color;
}
hsl() {
// Get the rgb values
const {
_a,
_b,
_c
} = this.rgb();
const [r, g, b] = [_a, _b, _c].map(v => v / 255); // Find the maximum and minimum values to get the lightness
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const l = (max + min) / 2; // If the r, g, v values are identical then we are grey
const isGrey = max === min; // Calculate the hue and saturation
const delta = max - min;
const s = isGrey ? 0 : l > 0.5 ? delta / (2 - max - min) : delta / (max + min);
const h = isGrey ? 0 : max === r ? ((g - b) / delta + (g < b ? 6 : 0)) / 6 : max === g ? ((b - r) / delta + 2) / 6 : max === b ? ((r - g) / delta + 4) / 6 : 0; // Construct and return the new color
const color = new Color(360 * h, 100 * s, 100 * l, 'hsl');
return color;
}
init(a = 0, b = 0, c = 0, d = 0, space = 'rgb') {
// This catches the case when a falsy value is passed like ''
a = !a ? 0 : a; // Reset all values in case the init function is rerun with new color space
if (this.space) {
for (const component in this.space) {
delete this[this.space[component]];
}
}
if (typeof a === 'number') {
// Allow for the case that we don't need d...
space = typeof d === 'string' ? d : space;
d = typeof d === 'string' ? 0 : d; // Assign the values straight to the color
Object.assign(this, {
_a: a,
_b: b,
_c: c,
_d: d,
space
}); // If the user gave us an array, make the color from it
} else if (a instanceof Array) {
this.space = b || (typeof a[3] === 'string' ? a[3] : a[4]) || 'rgb';
Object.assign(this, {
_a: a[0],
_b: a[1],
_c: a[2],
_d: a[3] || 0
});
} else if (a instanceof Object) {
// Set the object up and assign its values directly
const values = getParameters(a, b);
Object.assign(this, values);
} else if (typeof a === 'string') {
if (isRgb.test(a)) {
const noWhitespace = a.replace(whitespace, '');
const [_a, _b, _c] = rgb.exec(noWhitespace).slice(1, 4).map(v => parseInt(v));
Object.assign(this, {
_a,
_b,
_c,
_d: 0,
space: 'rgb'
});
} else if (isHex.test(a)) {
const hexParse = v => parseInt(v, 16);
const [, _a, _b, _c] = hex.exec(sixDigitHex(a)).map(hexParse);
Object.assign(this, {
_a,
_b,
_c,
_d: 0,
space: 'rgb'
});
} else throw Error('Unsupported string format, can\'t construct Color');
} // Now add the components as a convenience
const {
_a,
_b,
_c,
_d
} = this;
const components = this.space === 'rgb' ? {
r: _a,
g: _b,
b: _c
} : this.space === 'xyz' ? {
x: _a,
y: _b,
z: _c
} : this.space === 'hsl' ? {
h: _a,
s: _b,
l: _c
} : this.space === 'lab' ? {
l: _a,
a: _b,
b: _c
} : this.space === 'lch' ? {
l: _a,
c: _b,
h: _c
} : this.space === 'cmyk' ? {
c: _a,
m: _b,
y: _c,
k: _d
} : {};
Object.assign(this, components);
}
lab() {
// Get the xyz color
const {
x,
y,
z
} = this.xyz(); // Get the lab components
const l = 116 * y - 16;
const a = 500 * (x - y);
const b = 200 * (y - z); // Construct and return a new color
const color = new Color(l, a, b, 'lab');
return color;
}
lch() {
// Get the lab color directly
const {
l,
a,
b
} = this.lab(); // Get the chromaticity and the hue using polar coordinates
const c = Math.sqrt(a ** 2 + b ** 2);
let h = 180 * Math.atan2(b, a) / Math.PI;
if (h < 0) {
h *= -1;
h = 360 - h;
} // Make a new color and return it
const color = new Color(l, c, h, 'lch');
return color;
}
/*
Conversion Methods
*/
rgb() {
if (this.space === 'rgb') {
return this;
} else if (cieSpace(this.space)) {
// Convert to the xyz color space
let {
x,
y,
z
} = this;
if (this.space === 'lab' || this.space === 'lch') {
// Get the values in the lab space
let {
l,
a,
b
} = this;
if (this.space === 'lch') {
const {
c,
h
} = this;
const dToR = Math.PI / 180;
a = c * Math.cos(dToR * h);
b = c * Math.sin(dToR * h);
} // Undo the nonlinear function
const yL = (l + 16) / 116;
const xL = a / 500 + yL;
const zL = yL - b / 200; // Get the xyz values
const ct = 16 / 116;
const mx = 0.008856;
const nm = 7.787;
x = 0.95047 * (xL ** 3 > mx ? xL ** 3 : (xL - ct) / nm);
y = 1.00000 * (yL ** 3 > mx ? yL ** 3 : (yL - ct) / nm);
z = 1.08883 * (zL ** 3 > mx ? zL ** 3 : (zL - ct) / nm);
} // Convert xyz to unbounded rgb values
const rU = x * 3.2406 + y * -1.5372 + z * -0.4986;
const gU = x * -0.9689 + y * 1.8758 + z * 0.0415;
const bU = x * 0.0557 + y * -0.2040 + z * 1.0570; // Convert the values to true rgb values
const pow = Math.pow;
const bd = 0.0031308;
const r = rU > bd ? 1.055 * pow(rU, 1 / 2.4) - 0.055 : 12.92 * rU;
const g = gU > bd ? 1.055 * pow(gU, 1 / 2.4) - 0.055 : 12.92 * gU;
const b = bU > bd ? 1.055 * pow(bU, 1 / 2.4) - 0.055 : 12.92 * bU; // Make and return the color
const color = new Color(255 * r, 255 * g, 255 * b);
return color;
} else if (this.space === 'hsl') {
// https://bgrins.github.io/TinyColor/docs/tinycolor.html
// Get the current hsl values
let {
h,
s,
l
} = this;
h /= 360;
s /= 100;
l /= 100; // If we are grey, then just make the color directly
if (s === 0) {
l *= 255;
const color = new Color(l, l, l);
return color;
} // TODO I have no idea what this does :D If you figure it out, tell me!
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q; // Get the rgb values
const r = 255 * hueToRgb(p, q, h + 1 / 3);
const g = 255 * hueToRgb(p, q, h);
const b = 255 * hueToRgb(p, q, h - 1 / 3); // Make a new color
const color = new Color(r, g, b);
return color;
} else if (this.space === 'cmyk') {
// https://gist.github.com/felipesabino/5066336
// Get the normalised cmyk values
const {
c,
m,
y,
k
} = this; // Get the rgb values
const r = 255 * (1 - Math.min(1, c * (1 - k) + k));
const g = 255 * (1 - Math.min(1, m * (1 - k) + k));
const b = 255 * (1 - Math.min(1, y * (1 - k) + k)); // Form the color and return it
const color = new Color(r, g, b);
return color;
} else {
return this;
}
}
toArray() {
const {
_a,
_b,
_c,
_d,
space
} = this;
return [_a, _b, _c, _d, space];
}
toHex() {
const [r, g, b] = this._clamped().map(componentHex);
return `#${r}${g}${b}`;
}
toRgb() {
const [rV, gV, bV] = this._clamped();
const string = `rgb(${rV},${gV},${bV})`;
return string;
}
toString() {
return this.toHex();
}
xyz() {
// Normalise the red, green and blue values
const {
_a: r255,
_b: g255,
_c: b255
} = this.rgb();
const [r, g, b] = [r255, g255, b255].map(v => v / 255); // Convert to the lab rgb space
const rL = r > 0.04045 ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92;
const gL = g > 0.04045 ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92;
const bL = b > 0.04045 ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92; // Convert to the xyz color space without bounding the values
const xU = (rL * 0.4124 + gL * 0.3576 + bL * 0.1805) / 0.95047;
const yU = (rL * 0.2126 + gL * 0.7152 + bL * 0.0722) / 1.00000;
const zU = (rL * 0.0193 + gL * 0.1192 + bL * 0.9505) / 1.08883; // Get the proper xyz values by applying the bounding
const x = xU > 0.008856 ? Math.pow(xU, 1 / 3) : 7.787 * xU + 16 / 116;
const y = yU > 0.008856 ? Math.pow(yU, 1 / 3) : 7.787 * yU + 16 / 116;
const z = zU > 0.008856 ? Math.pow(zU, 1 / 3) : 7.787 * zU + 16 / 116; // Make and return the color
const color = new Color(x, y, z, 'xyz');
return color;
}
/*
Input and Output methods
*/
_clamped() {
const {
_a,
_b,
_c
} = this.rgb();
const {
max,
min,
round
} = Math;
const format = v => max(0, min(round(v), 255));
return [_a, _b, _c].map(format);
}
/*
Constructing colors
*/
}
class Point {
// Initialize
constructor(...args) {
this.init(...args);
} // Clone point
clone() {
return new Point(this);
}
init(x, y) {
const base = {
x: 0,
y: 0
}; // ensure source as object
const source = Array.isArray(x) ? {
x: x[0],
y: x[1]
} : typeof x === 'object' ? {
x: x.x,
y: x.y
} : {
x: x,
y: y
}; // merge source
this.x = source.x == null ? base.x : source.x;
this.y = source.y == null ? base.y : source.y;
return this;
}
toArray() {
return [this.x, this.y];
}
transform(m) {
return this.clone().transformO(m);
} // Transform point with matrix
transformO(m) {
if (!Matrix.isMatrixLike(m)) {
m = new Matrix(m);
}
const {
x,
y
} = this; // Perform the matrix multiplication
this.x = m.a * x + m.c * y + m.e;
this.y = m.b * x + m.d * y + m.f;
return this;
}
}
function point(x, y) {
return new Point(x, y).transformO(this.screenCTM().inverseO());
}
function closeEnough(a, b, threshold) {
return Math.abs(b - a) < (threshold || 1e-6);
}
class Matrix {
constructor(...args) {
this.init(...args);
}
static formatTransforms(o) {
// Get all of the parameters required to form the matrix
const flipBoth = o.flip === 'both' || o.flip === true;
const flipX = o.flip && (flipBoth || o.flip === 'x') ? -1 : 1;
const flipY = o.flip && (flipBoth || o.flip === 'y') ? -1 : 1;
const skewX = o.skew && o.skew.length ? o.skew[0] : isFinite(o.skew) ? o.skew : isFinite(o.skewX) ? o.skewX : 0;
const skewY = o.skew && o.skew.length ? o.skew[1] : isFinite(o.skew) ? o.skew : isFinite(o.skewY) ? o.skewY : 0;
const scaleX = o.scale && o.scale.length ? o.scale[0] * flipX : isFinite(o.scale) ? o.scale * flipX : isFinite(o.scaleX) ? o.scaleX * flipX : flipX;
const scaleY = o.scale && o.scale.length ? o.scale[1] * flipY : isFinite(o.scale) ? o.scale * flipY : isFinite(o.scaleY) ? o.scaleY * flipY : flipY;
const shear = o.shear || 0;
const theta = o.rotate || o.theta || 0;
const origin = new Point(o.origin || o.around || o.ox || o.originX, o.oy || o.originY);
const ox = origin.x;
const oy = origin.y; // We need Point to be invalid if nothing was passed because we cannot default to 0 here. That is why NaN
const position = new Point(o.position || o.px || o.positionX || NaN, o.py || o.positionY || NaN);
const px = position.x;
const py = position.y;
const translate = new Point(o.translate || o.tx || o.translateX, o.ty || o.translateY);
const tx = translate.x;
const ty = translate.y;
const relative = new Point(o.relative || o.rx || o.relativeX, o.ry || o.relativeY);
const rx = relative.x;
const ry = relative.y; // Populate all of the values
return {
scaleX,
scaleY,
skewX,
skewY,
shear,
theta,
rx,
ry,
tx,
ty,
ox,
oy,
px,
py
};
}
static fromArray(a) {
return {
a: a[0],
b: a[1],
c: a[2],
d: a[3],
e: a[4],
f: a[5]
};
}
static isMatrixLike(o) {
return o.a != null || o.b != null || o.c != null || o.d != null || o.e != null || o.f != null;
} // left matrix, right matrix, target matrix which is overwritten
static matrixMultiply(l, r, o) {
// Work out the product directly
const a = l.a * r.a + l.c * r.b;
const b = l.b * r.a + l.d * r.b;
const c = l.a * r.c + l.c * r.d;
const d = l.b * r.c + l.d * r.d;
const e = l.e + l.a * r.e + l.c * r.f;
const f = l.f + l.b * r.e + l.d * r.f; // make sure to use local variables because l/r and o could be the same
o.a = a;
o.b = b;
o.c = c;
o.d = d;
o.e = e;
o.f = f;
return o;
}
around(cx, cy, matrix) {
return this.clone().aroundO(cx, cy, matrix);
} // Transform around a center point
aroundO(cx, cy, matrix) {
const dx = cx || 0;
const dy = cy || 0;
return this.translateO(-dx, -dy).lmultiplyO(matrix).translateO(dx, dy);
} // Clones this matrix
clone() {
return new Matrix(this);
} // Decomposes this matrix into its affine parameters
decompose(cx = 0, cy = 0) {
// Get the parameters from the matrix
const a = this.a;
const b = this.b;
const c = this.c;
const d = this.d;
const e = this.e;
const f = this.f; // Figure out if the winding direction is clockwise or counterclockwise
const determinant = a * d - b * c;
const ccw = determinant > 0 ? 1 : -1; // Since we only shear in x, we can use the x basis to get the x scale
// and the rotation of the resulting matrix
const sx = ccw * Math.sqrt(a * a + b * b);
const thetaRad = Math.atan2(ccw * b, ccw * a);
const theta = 180 / Math.PI * thetaRad;
const ct = Math.cos(thetaRad);
const st = Math.sin(thetaRad); // We can then solve the y basis vector simultaneously to get the other
// two affine parameters directly from these parameters
const lam = (a * c + b * d) / determinant;
const sy = c * sx / (lam * a - b) || d * sx / (lam * b + a); // Use the translations
const tx = e - cx + cx * ct * sx + cy * (lam * ct * sx - st * sy);
const ty = f - cy + cx * st * sx + cy * (lam * st * sx + ct * sy); // Construct the decomposition and return it
return {
// Return the affine parameters
scaleX: sx,
scaleY: sy,
shear: lam,
rotate: theta,
translateX: tx,
translateY: ty,
originX: cx,
originY: cy,
// Return the matrix parameters
a: this.a,
b: this.b,
c: this.c,
d: this.d,
e: this.e,
f: this.f
};
} // Check if two matrices are equal
equals(other) {
if (other === this) return true;
const comp = new Matrix(other);
return closeEnough(this.a, comp.a) && closeEnough(this.b, comp.b) && closeEnough(this.c, comp.c) && closeEnough(this.d, comp.d) && closeEnough(this.e, comp.e) && closeEnough(this.f, comp.f);
} // Flip matrix on x or y, at a given offset
flip(axis, around) {
return this.clone().flipO(axis, around);
}
flipO(axis, around) {
return axis === 'x' ? this.scaleO(-1, 1, around, 0) : axis === 'y' ? this.scaleO(1, -1, 0, around) : this.scaleO(-1, -1, axis, around || axis); // Define an x, y flip point
} // Initialize
init(source) {
const base = Matrix.fromArray([1, 0, 0, 1, 0, 0]); // ensure source as object
source = source instanceof Element ? source.matrixify() : typeof source === 'string' ? Matrix.fromArray(source.split(delimiter).map(parseFloat)) : Array.isArray(source) ? Matrix.fromArray(source) : typeof source === 'object' && Matrix.isMatrixLike(source) ? source : typeof source === 'object' ? new Matrix().transform(source) : arguments.length === 6 ? Matrix.fromArray([].slice.call(arguments)) : base; // Merge the source matrix with the base matrix
this.a = source.a != null ? source.a : base.a;
this.b = source.b != null ? source.b : base.b;
this.c = source.c != null ? source.c : base.c;
this.d = source.d != null ? source.d : base.d;
this.e = source.e != null ? source.e : base.e;
this.f = source.f != null ? source.f : base.f;
return this;
}
inverse() {
return this.clone().inverseO();
} // Inverses matrix
inverseO() {
// Get the current parameters out of the matrix
const a = this.a;
const b = this.b;
const c = this.c;
const d = this.d;
const e = this.e;
const f = this.f; // Invert the 2x2 matrix in the top left
const det = a * d - b * c;
if (!det) throw new Error('Cannot invert ' + this); // Calculate the top 2x2 matrix
const na = d / det;
const nb = -b / det;
const nc = -c / det;
const nd = a / det; // Apply the inverted matrix to the top right
const ne = -(na * e + nc * f);
const nf = -(nb * e + nd * f); // Construct the inverted matrix
this.a = na;
this.b = nb;
this.c = nc;
this.d = nd;
this.e = ne;
this.f = nf;
return this;
}
lmultiply(matrix) {
return this.clone().lmultiplyO(matrix);
}
lmultiplyO(matrix) {
const r = this;
const l = matrix instanceof Matrix ? matrix : new Matrix(matrix);
return Matrix.matrixMultiply(l, r, this);
} // Left multiplies by the given matrix
multiply(matrix) {
return this.clone().multiplyO(matrix);
}
multiplyO(matrix) {
// Get the matrices
const l = this;
const r = matrix instanceof Matrix ? matrix : new Matrix(matrix);
return Matrix.matrixMultiply(l, r, this);
} // Rotate matrix
rotate(r, cx, cy) {
return this.clone().rotateO(r, cx, cy);
}
rotateO(r, cx = 0, cy = 0) {
// Convert degrees to radians
r = radians(r);
const cos = Math.cos(r);
const sin = Math.sin(r);
const {
a,
b,
c,
d,
e,
f
} = this;
this.a = a * cos - b * sin;
this.b = b * cos + a * sin;
this.c = c * cos - d * sin;
this.d = d * cos + c * sin;
this.e = e * cos - f * sin + cy * sin - cx * cos + cx;
this.f = f * cos + e * sin - cx * sin - cy * cos + cy;
return this;
} // Scale matrix
scale(x, y, cx, cy) {
return this.clone().scaleO(...arguments);
}
scaleO(x, y = x, cx = 0, cy = 0) {
// Support uniform scaling
if (arguments.length === 3) {
cy = cx;
cx = y;
y = x;
}
const {
a,
b,
c,
d,
e,
f
} = this;
this.a = a * x;
this.b = b * y;
this.c = c * x;
this.d = d * y;
this.e = e * x - cx * x + cx;
this.f = f * y - cy * y + cy;
return this;
} // Shear matrix
shear(a, cx, cy) {
return this.clone().shearO(a, cx, cy);
}
shearO(lx, cx = 0, cy = 0) {
const {
a,
b,
c,
d,
e,
f
} = this;
this.a = a + b * lx;
this.c = c + d * lx;
this.e = e + f * lx - cy * lx;
return this;
} // Skew Matrix
skew(x, y, cx, cy) {
return this.clone().skewO(...arguments);
}
skewO(x, y = x, cx = 0, cy = 0) {
// support uniformal skew
if (arguments.length === 3) {
cy = cx;
cx = y;
y = x;
} // Convert degrees to radians
x = radians(x);
y = radians(y);
const lx = Math.tan(x);
const ly = Math.tan(y);
const {
a,
b,
c,
d,
e,
f
} = this;
this.a = a + b * lx;
this.b = b + a * ly;
this.c = c + d * lx;
this.d = d + c * ly;
this.e = e + f * lx - cy * lx;
this.f = f + e * ly - cx * ly;
return this;
} // SkewX
skewX(x, cx, cy) {
return this.skew(x, 0, cx, cy);
} // SkewY
skewY(y, cx, cy) {
return this.skew(0, y, cx, cy);
}
toArray() {
return [this.a, this.b, this.c, this.d, this.e, this.f];
} // Convert matrix to string
toString() {
return 'matrix(' + this.a + ',' + this.b + ',' + this.c + ',' + this.d + ',' + this.e + ',' + this.f + ')';
} // Transform a matrix into another matrix by manipulating the space
transform(o) {
// Check if o is a matrix and then left multiply it directly
if (Matrix.isMatrixLike(o)) {
const matrix = new Matrix(o);
return matrix.multiplyO(this);
} // Get the proposed transformations and the current transformations
const t = Matrix.formatTransforms(o);
const current = this;
const {
x: ox,
y: oy
} = new Point(t.ox, t.oy).transform(current); // Construct the resulting matrix
const transformer = new Matrix().translateO(t.rx, t.ry).lmultiplyO(current).translateO(-ox, -oy).scaleO(t.scaleX, t.scaleY).skewO(t.skewX, t.skewY).shearO(t.shear).rotateO(t.theta).translateO(ox, oy); // If we want the origin at a particular place, we force it there
if (isFinite(t.px) || isFinite(t.py)) {
const origin = new Point(ox, oy).transform(transformer); // TODO: Replace t.px with isFinite(t.px)
// Doesn't work because t.px is also 0 if it wasn't passed
const dx = isFinite(t.px) ? t.px - origin.x : 0;
const dy = isFinite(t.py) ? t.py - origin.y : 0;
transformer.translateO(dx, dy);
} // Translate now after positioning
transformer.translateO(t.tx, t.ty);
return transformer;
} // Translate matrix
translate(x, y) {
return this.clone().translateO(x, y);
}
translateO(x, y) {
this.e += x || 0;
this.f += y || 0;
return this;
}
valueOf() {
return {
a: this.a,
b: this.b,
c: this.c,
d: this.d,
e: this.e,
f: this.f
};
}
}
function ctm() {
return new Matrix(this.node.getCTM());
}
function screenCTM() {
/* https://bugzilla.mozilla.org/show_bug.cgi?id=1344537
This is needed because FF does not return the transformation matrix
for the inner coordinate system when getScreenCTM() is called on nested svgs.
However all other Browsers do that */
if (typeof this.isRoot === 'function' && !this.isRoot()) {
const rect = this.rect(1, 1);
const m = rect.node.getScreenCTM();
rect.remove();
return new Matrix(m);
}
return new Matrix(this.node.getScreenCTM());
}
register(Matrix, 'Matrix');
function parser() {
// Reuse cached element if possible
if (!parser.nodes) {
const svg = makeInstance().size(2, 0);
svg.node.style.cssText = ['opacity: 0', 'position: absolute', 'left: -100%', 'top: -100%', 'overflow: hidden'].join(';');
svg.attr('focusable', 'false');
svg.attr('aria-hidden', 'true');
const path = svg.path().node;
parser.nodes = {
svg,
path
};
}
if (!parser.nodes.svg.node.parentNode) {
const b = globals.document.body || globals.document.documentElement;
parser.nodes.svg.addTo(b);
}
return parser.nodes;
}
function isNulledBox(box) {
return !box.width && !box.height && !box.x && !box.y;
}
function domContains(node) {
return node === globals.document || (globals.document.documentElement.contains || function (node) {
// This is IE - it does not support contains() for top-level SVGs
while (node.parentNode) {
node = node.parentNode;
}
return node === globals.document;
}).call(globals.document.documentElement, node);
}
class Box {
constructor(...args) {
this.init(...args);
}
addOffset() {
// offset by window scroll position, because getBoundingClientRect changes when window is scrolled
this.x += globals.window.pageXOffset;
this.y += globals.window.pageYOffset;
return new Box(this);
}
init(source) {
const base = [0, 0, 0, 0];
source = typeof source === 'string' ? source.split(delimiter).map(parseFloat) : Array.isArray(source) ? source : typeof source === 'object' ? [source.left != null ? source.left : source.x, source.top != null ? source.top : source.y, source.width, source.height] : arguments.length === 4 ? [].slice.call(arguments) : base;
this.x = source[0] || 0;
this.y = source[1] || 0;
this.width = this.w = source[2] || 0;
this.height = this.h = source[3] || 0; // Add more bounding box properties
this.x2 = this.x + this.w;
this.y2 = this.y + this.h;
this.cx = this.x + this.w / 2;
this.cy = this.y + this.h / 2;
return this;
}
isNulled() {
return isNulledBox(this);
} // Merge rect box with another, return a new instance
merge(box) {
const x = Math.min(this.x, box.x);
const y = Math.min(this.y, box.y);
const width = Math.max(this.x + this.width, box.x + box.width) - x;
const height = Math.max(this.y + this.height, box.y + box.height) - y;
return new Box(x, y, width, height);
}
toArray() {
return [this.x, this.y, this.width, this.height];
}
toString() {
return this.x + ' ' + this.y + ' ' + this.width + ' ' + this.height;
}
transform(m) {
if (!(m instanceof Matrix)) {
m = new Matrix(m);
}
let xMin = Infinity;
let xMax = -Infinity;
let yMin = Infinity;
let yMax = -Infinity;
const pts = [new Point(this.x, this.y), new Point(this.x2, this.y), new Point(this.x, this.y2), new Point(this.x2, this.y2)];
pts.forEach(function (p) {
p = p.transform(m);
xMin = Math.min(xMin, p.x);
xMax = Math.max(xMax, p.x);
yMin = Math.min(yMin, p.y);
yMax = Math.max(yMax, p.y);
});
return new Box(xMin, yMin, xMax - xMin, yMax - yMin);
}
}
function getBox(el, getBBoxFn, retry) {
let box;
try {
// Try to get the box with the provided function
box = getBBoxFn(el.node); // If the box is worthless and not even in the dom, retry
// by throwing an error here...
if (isNulledBox(box) && !domContains(el.node)) {
throw new Error('Element not in the dom');
}
} catch (e) {
// ... and calling the retry handler here
box = retry(el);
}
return box;
}
function bbox() {
// Function to get bbox is getBBox()
const getBBox = node => node.getBBox(); // Take all measures so that a stupid browser renders the element
// so we can get the bbox from it when we try again
const retry = el => {
try {
const clone = el.clone().addTo(parser().svg).show();
const box = clone.node.getBBox();
clone.remove();
return box;
} catch (e) {
// We give up...
throw new Error(`Getting bbox of element "${el.node.nodeName}" is not possible: ${e.toString()}`);
}
};
const box = getBox(this, getBBox, retry);
const bbox = new Box(box);
return bbox;
}
function rbox(el) {
const getRBox = node => node.getBoundingClientRect();
const retry = el => {
// There is no point in trying tricks here because if we insert the element into the dom ourselves
// it obviously will be at the wrong position
throw new Error(`Getting rbox of element "${el.node.nodeName}" is not possible`);
};
const box = getBox(this, getRBox, retry);
const rbox = new Box(box); // If an element was passed, we want the bbox in the coordinate system of that element
if (el) {
return rbox.transform(el.screenCTM().inverseO());
} // Else we want it in absolute screen coordinates
// Therefore we need to add the scrollOffset
return rbox.addOffset();
} // Checks whether the given point is inside the bounding box
function inside(x, y) {
const box = this.bbox();
return x > box.x && y > box.y && x < box.x + box.width && y < box.y + box.height;
}
registerMethods({
viewbox: {
viewbox(x, y, width, height) {
// act as getter
if (x == null) return new Box(this.attr('viewBox')); // act as setter
return this.attr('viewBox', new Box(x, y, width, height));
},
zoom(level, point) {
// Its best to rely on the attributes here and here is why:
// clientXYZ: Doesn't work on non-root svgs because they dont have a CSSBox (silly!)
// getBoundingClientRect: Doesn't work because Chrome just ignores width and height of nested svgs completely
// that means, their clientRect is always as big as the content.
// Furthermore this size is incorrect if the element is further transformed by its parents
// computedStyle: Only returns meaningful values if css was used with px. We dont go this route here!
// getBBox: returns the bounding box of its content - that doesn't help!
let {
width,
height
} = this.attr(['width', 'height']); // Width and height is a string when a number with a unit is present which we can't use
// So we try clientXYZ
if (!width && !height || typeof width === 'string' || typeof height === 'string') {
width = this.node.clientWidth;
height = this.node.clientHeight;
} // Giving up...
if (!width || !height) {
throw new Error('Impossible to get absolute width and height. Please provide an absolute width and height attribute on the zooming element');
}
const v = this.viewbox();
const zoomX = width / v.width;
const zoomY = height / v.height;
const zoom = Math.min(zoomX, zoomY);
if (level == null) {
return zoom;
}
let zoomAmount = zoom / level; // Set the zoomAmount to the highest value which is safe to process and recover from
// The * 100 is a bit of wiggle room for the matrix transformation
if (zoomAmount === Infinity) zoomAmount = Number.MAX_SAFE_INTEGER / 100;
point = point || new Point(width / 2 / zoomX + v.x, height / 2 / zoomY + v.y);
const box = new Box(v).transform(new Matrix({
scale: zoomAmount,
origin: point
}));
return this.viewbox(box);
}
}
});
register(Box, 'Box');
class List extends Array {
constructor(arr = [], ...args) {
super(arr, ...args);
if (typeof arr === 'number') return this;
this.length = 0;
this.push(...arr);
}
}
extend([List], {
each(fnOrMethodName, ...args) {
if (typeof fnOrMethodName === 'function') {
return this.map((el, i, arr) => {
return fnOrMethodName.call(el, el, i, arr);
});
} else {
return this.map(el => {
return el[fnOrMethodName](...args);
});
}
},
toArray() {
return Array.prototype.concat.apply([], this);
}
});
const reserved = ['toArray', 'constructor', 'each'];
List.extend = function (methods) {
methods = methods.reduce((obj, name) => {
// Don't overwrite own methods
if (reserved.includes(name)) return obj; // Don't add private methods
if (name[0] === '_') return obj; // Relay every call to each()
obj[name] = function (...attrs) {
return this.each(name, ...attrs);
};
return obj;
}, {});
extend([List], methods);
};
function baseFind(query, parent) {
return new List(map((parent || globals.document).querySelectorAll(query), function (node) {
return adopt(node);
}));
} // Scoped find method
function find(query) {
return baseFind(query, this.node);
}
function findOne(query) {
return adopt(this.node.querySelector(query));
}
let listenerId = 0;
const windowEvents = {};
function getEvents(instance) {
let n = instance.getEventHolder(); // We dont want to save events in global space
if (n === globals.window) n = windowEvents;
if (!n.events) n.events = {};
return n.events;
}
function getEventTarget(instance) {
return instance.getEventTarget();
}
function clearEvents(instance) {
let n = instance.getEventHolder();
if (n === globals.window) n = windowEvents;
if (n.events) n.events = {};
} // Add event binder in the SVG namespace
function on(node, events, listener, binding, options) {
const l = listener.bind(binding || node);
const instance = makeInstance(node);
const bag = getEvents(instance);
const n = getEventTarget(instance); // events can be an array of events or a string of events
events = Array.isArray(events) ? events : events.split(delimiter); // add id to listener
if (!listener._svgjsListenerId) {
listener._svgjsListenerId = ++listenerId;
}
events.forEach(function (event) {
const ev = event.split('.')[0];
const ns = event.split('.')[1] || '*'; // ensure valid object
bag[ev] = bag[ev] || {};
bag[ev][ns] = bag[ev][ns] || {}; // reference listener
bag[ev][ns][listener._svgjsListenerId] = l; // add listener
n.addEventListener(ev, l, options || false);
});
} // Add event unbinder in the SVG namespace
function off(node, events, listener, options) {
const instance = makeInstance(node);
const bag = getEvents(instance);
const n = getEventTarget(instance); // listener can be a function or a number
if (typeof listener === 'function') {
listener = listener._svgjsListenerId;
if (!listener) return;
} // events can be an array of events or a string or undefined
events = Array.isArray(events) ? events : (events || '').split(delimiter);
events.forEach(function (event) {
const ev = event && event.split('.')[0];
const ns = event && event.split('.')[1];
let namespace, l;
if (listener) {
// remove listener reference
if (bag[ev] && bag[ev][ns || '*']) {
// removeListener
n.removeEventListener(ev, bag[ev][ns || '*'][listener], options || false);
delete bag[ev][ns || '*'][listener];
}
} else if (ev && ns) {
// remove all listeners for a namespaced event
if (bag[ev] && bag[ev][ns]) {
for (l in bag[ev][ns]) {
off(n, [ev, ns].join('.'), l);
}
delete bag[ev][ns];
}
} else if (ns) {
// remove all listeners for a specific namespace
for (event in bag) {
for (namespace in bag[event]) {
if (ns === namespace) {
off(n, [event, ns].join('.'));
}
}
}
} else if (ev) {
// remove all listeners for the event
if (bag[ev]) {
for (namespace in bag[ev]) {
off(n, [ev, namespace].join('.'));
}
delete bag[ev];
}
} else {
// remove all listeners on a given node
for (event in bag) {
off(n, event);
}
clearEvents(instance);
}
});
}
function dispatch(node, event, data, options) {
const n = getEventTarget(node); // Dispatch event
if (event instanceof globals.window.Event) {
n.dispatchEvent(event);
} else {
event = new globals.window.CustomEvent(event, {
detail: data,
cancelable: true,
...options
});
n.dispatchEvent(event);
}
return event;
}
class EventTarget extends Base {
addEventListener() {}
dispatch(event, data, options) {
return dispatch(this, event, data, options);
}
dispatchEvent(event) {
const bag = this.getEventHolder().events;
if (!bag) return true;
const events = bag[event.type];
for (const i in events) {
for (const j in events[i]) {
events[i][j](event);
}
}
return !event.defaultPrevented;
} // Fire given event
fire(event, data, options) {
this.dispatch(event, data, options);
return this;
}
getEventHolder() {
return this;
}
getEventTarget() {
return this;
} // Unbind event from listener
off(event, listener, options) {
off(this, event, listener, options);
return this;
} // Bind given event to listener
on(event, listener, binding, options) {
on(this, event, listener, binding, options);
return this;
}
removeEventListener() {}
}
register(EventTarget, 'EventTarget');
function noop() {} // Default animation values
const timeline = {
duration: 400,
ease: '>',
delay: 0
}; // Default attribute values
const attrs = {
// fill and stroke
'fill-opacity': 1,
'stroke-opacity': 1,
'stroke-width': 0,
'stroke-linejoin': 'miter',
'stroke-linecap': 'butt',
fill: '#000000',
stroke: '#000000',
opacity: 1,
// position
x: 0,
y: 0,
cx: 0,
cy: 0,
// size
width: 0,
height: 0,
// radius
r: 0,
rx: 0,
ry: 0,
// gradient
offset: 0,
'stop-opacity': 1,
'stop-color': '#000000',
// text
'text-anchor': 'start'
};
var defaults = {
__proto__: null,
noop: noop,
timeline: timeline,
attrs: attrs
};
class SVGArray extends Array {
constructor(...args) {
super(...args);
this.init(...args);
}
clone() {
return new this.constructor(this);
}
init(arr) {
// This catches the case, that native map tries to create an array with new Array(1)
if (typeof arr === 'number') return this;
this.length = 0;
this.push(...this.parse(arr));
return this;
} // Parse whitespace separated string
parse(array = []) {
// If already is an array, no need to parse it
if (array instanceof Array) return array;
return array.trim().split(delimiter).map(parseFloat);
}
toArray() {
return Array.prototype.concat.apply([], this);
}
toSet() {
return new Set(this);
}
toString() {
return this.join(' ');
} // Flattens the array if needed
valueOf() {
const ret = [];
ret.push(...this);
return ret;
}
}
class SVGNumber {
// Initialize
constructor(...args) {
this.init(...args);
}
convert(unit) {
return new SVGNumber(this.value, unit);
} // Divide number
divide(number) {
number = new SVGNumber(number);
return new SVGNumber(this / number, this.unit || number.unit);
}
init(value, unit) {
unit = Array.isArray(value) ? value[1] : unit;
value = Array.isArray(value) ? value[0] : value; // initialize defaults
this.value = 0;
this.unit = unit || ''; // parse value
if (typeof value === 'number') {
// ensure a valid numeric value
this.value = isNaN(value) ? 0 : !isFinite(value) ? value < 0 ? -3.4e+38 : +3.4e+38 : value;
} else if (typeof value === 'string') {
unit = value.match(numberAndUnit);
if (unit) {
// make value numeric
this.value = parseFloat(unit[1]); // normalize
if (unit[5] === '%') {
this.value /= 100;
} else if (unit[5] === 's') {
this.value *= 1000;
} // store unit
this.unit = unit[5];
}
} else {
if (value instanceof SVGNumber) {
this.value = value.valueOf();
this.unit = value.unit;
}
}
return this;
} // Subtract number
minus(number) {
number = new SVGNumber(number);
return new SVGNumber(this - number, this.unit || number.unit);
} // Add number
plus(number) {
number = new SVGNumber(number);
return new SVGNumber(this + number, this.unit || number.unit);
} // Multiply number
times(number) {
number = new SVGNumber(number);
return new SVGNumber(this * number, this.unit || number.unit);
}
toArray() {
return [this.value, this.unit];
}
toJSON() {
return this.toString();
}
toString() {
return (this.unit === '%' ? ~~(this.value * 1e8) / 1e6 : this.unit === 's' ? this.value / 1e3 : this.value) + this.unit;
}
valueOf() {
return this.value;
}
}
const hooks = [];
function registerAttrHook(fn) {
hooks.push(fn);
} // Set svg element attribute
function attr(attr, val, ns) {
// act as full getter
if (attr == null) {
// get an object of attributes
attr = {};
val = this.node.attributes;
for (const node of val) {
attr[node.nodeName] = isNumber.test(node.nodeValue) ? parseFloat(node.nodeValue) : node.nodeValue;
}
return attr;
} else if (attr instanceof Array) {
// loop through array and get all values
return attr.reduce((last, curr) => {
last[curr] = this.attr(curr);
return last;
}, {});
} else if (typeof attr === 'object' && attr.constructor === Object) {
// apply every attribute individually if an object is passed
for (val in attr) this.attr(val, attr[val]);
} else if (val === null) {
// remove value
this.node.removeAttribute(attr);
} else if (val == null) {
// act as a getter if the first and only argument is not an object
val = this.node.getAttribute(attr);
return val == null ? attrs[attr] : isNumber.test(val) ? parseFloat(val) : val;
} else {
// Loop through hooks and execute them to convert value
val = hooks.reduce((_val, hook) => {
return hook(attr, _val, this);
}, val); // ensure correct numeric values (also accepts NaN and Infinity)
if (typeof val === 'number') {
val = new SVGNumber(val);
} else if (Color.isColor(val)) {
// ensure full hex color
val = new Color(val);
} else if (val.constructor === Array) {
// Check for plain arrays and parse array values
val = new SVGArray(val);
} // if the passed attribute is leading...
if (attr === 'leading') {
// ... call the leading method instead
if (this.leading) {
this.leading(val);
}
} else {
// set given attribute on node
typeof ns === 'string' ? this.node.setAttributeNS(ns, attr, val.toString()) : this.node.setAttribute(attr, val.toString());
} // rebuild if required
if (this.rebuild && (attr === 'font-size' || attr === 'x')) {
this.rebuild();
}
}
return this;
}
class Dom extends EventTarget {
constructor(node, attrs) {
super();
this.node = node;
this.type = node.nodeName;
if (attrs && node !== attrs) {
this.attr(attrs);
}
} // Add given element at a position
add(element, i) {
element = makeInstance(element); // If non-root svg nodes are added we have to remove their namespaces
if (element.removeNamespace && this.node instanceof globals.window.SVGElement) {
element.removeNamespace();
}
if (i == null) {
this.node.appendChild(element.node);
} else if (element.node !== this.node.childNodes[i]) {
this.node.insertBefore(element.node, this.node.childNodes[i]);
}
return this;
} // Add element to given container and return self
addTo(parent, i) {
return makeInstance(parent).put(this, i);
} // Returns all child elements
children() {
return new List(map(this.node.children, function (node) {
return adopt(node);
}));
} // Remove all elements in this container
clear() {
// remove children
while (this.node.hasChildNodes()) {
this.node.removeChild(this.node.lastChild);
}
return this;
} // Clone element
clone(deep = true, assignNewIds = true) {
// write dom data to the dom so the clone can pickup the data
this.writeDataToDom(); // clone element
let nodeClone = this.node.cloneNode(deep);
if (assignNewIds) {
// assign new id
nodeClone = assignNewId(nodeClone);
}
return new this.constructor(nodeClone);
} // Iterates over all children and invokes a given block
each(block, deep) {
const children = this.children();
let i, il;
for (i = 0, il = children.length; i < il; i++) {
block.apply(children[i], [i, children]);
if (deep) {
children[i].each(block, deep);
}
}
return this;
}
element(nodeName, attrs) {
return this.put(new Dom(create(nodeName), attrs));
} // Get first child
first() {
return adopt(this.node.firstChild);
} // Get a element at the given index
get(i) {
return adopt(this.node.childNodes[i]);
}
getEventHolder() {
return this.node;
}
getEventTarget() {
return this.node;
} // Checks if the given element is a child
has(element) {
return this.index(element) >= 0;
}
html(htmlOrFn, outerHTML) {
return this.xml(htmlOrFn, outerHTML, html);
} // Get / set id
id(id) {
// generate new id if no id set
if (typeof id === 'undefined' && !this.node.id) {
this.node.id = eid(this.type);
} // don't set directly with this.node.id to make `null` work correctly
return this.attr('id', id);
} // Gets index of given element
index(element) {
return [].slice.call(this.node.childNodes).indexOf(element.node);
} // Get the last child
last() {
return adopt(this.node.lastChild);
} // matches the element vs a css selector
matches(selector) {
const el = this.node;
const matcher = el.matches || el.matchesSelector || el.msMatchesSelector || el.mozMatchesSelector || el.webkitMatchesSelector || el.oMatchesSelector || null;
return matcher && matcher.call(el, selector);
} // Returns the parent element instance
parent(type) {
let parent = this; // check for parent
if (!parent.node.parentNode) return null; // get parent element
parent = adopt(parent.node.parentNode);
if (!type) return parent; // loop through ancestors if type is given
do {
if (typeof type === 'string' ? parent.matches(type) : parent instanceof type) return parent;
} while (parent = adopt(parent.node.parentNode));
return parent;
} // Basically does the same as `add()` but returns the added element instead
put(element, i) {
element = makeInstance(element);
this.add(element, i);
return element;
} // Add element to given container and return container
putIn(parent, i) {
return makeInstance(parent).add(this, i);
} // Remove element
remove() {
if (this.parent()) {
this.parent().removeElement(this);
}
return this;
} // Remove a given child
removeElement(element) {
this.node.removeChild(element.node);
return this;
} // Replace this with element
replace(element) {
element = makeInstance(element);
if (this.node.parentNode) {
this.node.parentNode.replaceChild(element.node, this.node);
}
return element;
}
round(precision = 2, map = null) {
const factor = 10 ** precision;
const attrs = this.attr(map);
for (const i in attrs) {
if (typeof attrs[i] === 'number') {
attrs[i] = Math.round(attrs[i] * factor) / factor;
}
}
this.attr(attrs);
return this;
} // Import / Export raw svg
svg(svgOrFn, outerSVG) {
return this.xml(svgOrFn, outerSVG, svg);
} // Return id on string conversion
toString() {
return this.id();
}
words(text) {
// This is faster than removing all children and adding a new one
this.node.textContent = text;
return this;
}
wrap(node) {
const parent = this.parent();
if (!parent) {
return this.addTo(node);
}
const position = parent.index(this);
return parent.put(node, position).put(this);
} // write svgjs data to the dom
writeDataToDom() {
// dump variables recursively
this.each(function () {
this.writeDataToDom();
});
return this;
} // Import / Export raw svg
xml(xmlOrFn, outerXML, ns) {
if (typeof xmlOrFn === 'boolean') {
ns = outerXML;
outerXML = xmlOrFn;
xmlOrFn = null;
} // act as getter if no svg string is given
if (xmlOrFn == null || typeof xmlOrFn === 'function') {
// The default for exports is, that the outerNode is included
outerXML = outerXML == null ? true : outerXML; // write svgjs data to the dom
this.writeDataToDom();
let current = this; // An export modifier was passed
if (xmlOrFn != null) {
current = adopt(current.node.cloneNode(true)); // If the user wants outerHTML we need to process this node, too
if (outerXML) {
const result = xmlOrFn(current);
current = result || current; // The user does not want this node? Well, then he gets nothing
if (result === false) return '';
} // Deep loop through all children and apply modifier
current.each(function () {
const result = xmlOrFn(this);
const _this = result || this; // If modifier returns false, discard node
if (result === false) {
this.remove(); // If modifier returns new node, use it
} else if (result && this !== _this) {
this.replace(_this);
}
}, true);
} // Return outer or inner content
return outerXML ? current.node.outerHTML : current.node.innerHTML;
} // Act as setter if we got a string
// The default for import is, that the current node is not replaced
outerXML = outerXML == null ? false : outerXML; // Create temporary holder
const well = create('wrapper', ns);
const fragment = globals.document.createDocumentFragment(); // Dump raw svg
well.innerHTML = xmlOrFn; // Transplant nodes into the fragment
for (let len = well.children.length; len--;) {
fragment.appendChild(well.firstElementChild);
}
const parent = this.parent(); // Add the whole fragment at once
return outerXML ? this.replace(fragment) && parent : this.add(fragment);
}
}
extend(Dom, {
attr,
find,
findOne
});
register(Dom, 'Dom');
class Element extends Dom {
constructor(node, attrs) {
super(node, attrs); // initialize data object
this.dom = {}; // create circular reference
this.node.instance = this;
if (node.hasAttribute('svgjs:data')) {
// pull svgjs data from the dom (getAttributeNS doesn't work in html5)
this.setData(JSON.parse(node.getAttribute('svgjs:data')) || {});
}
} // Move element by its center
center(x, y) {
return this.cx(x).cy(y);
} // Move by center over x-axis
cx(x) {
return x == null ? this.x() + this.width() / 2 : this.x(x - this.width() / 2);
} // Move by center over y-axis
cy(y) {
return y == null ? this.y() + this.height() / 2 : this.y(y - this.height() / 2);
} // Get defs
defs() {
const root = this.root();
return root && root.defs();
} // Relative move over x and y axes
dmove(x, y) {
return this.dx(x).dy(y);
} // Relative move over x axis
dx(x = 0) {
return this.x(new SVGNumber(x).plus(this.x()));
} // Relative move over y axis
dy(y = 0) {
return this.y(new SVGNumber(y).plus(this.y()));
}
getEventHolder() {
return this;
} // Set height of element
height(height) {
return this.attr('height', height);
} // Move element to given x and y values
move(x, y) {
return this.x(x).y(y);
} // return array of all ancestors of given type up to the root svg
parents(until = this.root()) {
const isSelector = typeof until === 'string';
if (!isSelector) {
until = makeInstance(until);
}
const parents = new List();
let parent = this;
while ((parent = parent.parent()) && parent.node !== globals.document && parent.nodeName !== '#document-fragment') {
parents.push(parent);
if (!isSelector && parent.node === until.node) {
break;
}
if (isSelector && parent.matches(until)) {
break;
}
if (parent.node === this.root().node) {
// We worked our way to the root and didn't match `until`
return null;
}
}
return parents;
} // Get referenced element form attribute value
reference(attr) {
attr = this.attr(attr);
if (!attr) return null;
const m = (attr + '').match(reference);
return m ? makeInstance(m[1]) : null;
} // Get parent document
root() {
const p = this.parent(getClass(root));
return p && p.root();
} // set given data to the elements data property
setData(o) {
this.dom = o;
return this;
} // Set element size to given width and height
size(width, height) {
const p = proportionalSize(this, width, height);
return this.width(new SVGNumber(p.width)).height(new SVGNumber(p.height));
} // Set width of element
width(width) {
return this.attr('width', width);
} // write svgjs data to the dom
writeDataToDom() {
// remove previously set data
this.node.removeAttribute('svgjs:data');
if (Object.keys(this.dom).length) {
this.node.setAttribute('svgjs:data', JSON.stringify(this.dom)); // see #428
}
return super.writeDataToDom();
} // Move over x-axis
x(x) {
return this.attr('x', x);
} // Move over y-axis
y(y) {
return this.attr('y', y);
}
}
extend(Element, {
bbox,
rbox,
inside,
point,
ctm,
screenCTM
});
register(Element, 'Element');
const sugar = {
stroke: ['color', 'width', 'opacity', 'linecap', 'linejoin', 'miterlimit', 'dasharray', 'dashoffset'],
fill: ['color', 'opacity', 'rule'],
prefix: function (t, a) {
return a === 'color' ? t : t + '-' + a;
}
} // Add sugar for fill and stroke
;
['fill', 'stroke'].forEach(function (m) {
const extension = {};
let i;
extension[m] = function (o) {
if (typeof o === 'undefined') {
return this.attr(m);
}
if (typeof o === 'string' || o instanceof Color || Color.isRgb(o) || o instanceof Element) {
this.attr(m, o);
} else {
// set all attributes from sugar.fill and sugar.stroke list
for (i = sugar[m].length - 1; i >= 0; i--) {
if (o[sugar[m][i]] != null) {
this.attr(sugar.prefix(m, sugar[m][i]), o[sugar[m][i]]);
}
}
}
return this;
};
registerMethods(['Element', 'Runner'], extension);
});
registerMethods(['Element', 'Runner'], {
// Let the user set the matrix directly
matrix: function (mat, b, c, d, e, f) {
// Act as a getter
if (mat == null) {
return new Matrix(this);
} // Act as a setter, the user can pass a matrix or a set of numbers
return this.attr('transform', new Matrix(mat, b, c, d, e, f));
},
// Map rotation to transform
rotate: function (angle, cx, cy) {
return this.transform({
rotate: angle,
ox: cx,
oy: cy
}, true);
},
// Map skew to transform
skew: function (x, y, cx, cy) {
return arguments.length === 1 || arguments.length === 3 ? this.transform({
skew: x,
ox: y,
oy: cx
}, true) : this.transform({
skew: [x, y],
ox: cx,
oy: cy
}, true);
},
shear: function (lam, cx, cy) {
return this.transform({
shear: lam,
ox: cx,
oy: cy
}, true);
},
// Map scale to transform
scale: function (x, y, cx, cy) {
return arguments.length === 1 || arguments.length === 3 ? this.transform({
scale: x,
ox: y,
oy: cx
}, true) : this.transform({
scale: [x, y],
ox: cx,
oy: cy
}, true);
},
// Map translate to transform
translate: function (x, y) {
return this.transform({
translate: [x, y]
}, true);
},
// Map relative translations to transform
relative: function (x, y) {
return this.transform({
relative: [x, y]
}, true);
},
// Map flip to transform
flip: function (direction = 'both', origin = 'center') {
if ('xybothtrue'.indexOf(direction) === -1) {
origin = direction;
direction = 'both';
}
return this.transform({
flip: direction,
origin: origin
}, true);
},
// Opacity
opacity: function (value) {
return this.attr('opacity', value);
}
});
registerMethods('radius', {
// Add x and y radius
radius: function (x, y = x) {
const type = (this._element || this).type;
return type === 'radialGradient' ? this.attr('r', new SVGNumber(x)) : this.rx(x).ry(y);
}
});
registerMethods('Path', {
// Get path length
length: function () {
return this.node.getTotalLength();
},
// Get point at length
pointAt: function (length) {
return new Point(this.node.getPointAtLength(length));
}
});
registerMethods(['Element', 'Runner'], {
// Set font
font: function (a, v) {
if (typeof a === 'object') {
for (v in a) this.font(v, a[v]);
return this;
}
return a === 'leading' ? this.leading(v) : a === 'anchor' ? this.attr('text-anchor', v) : a === 'size' || a === 'family' || a === 'weight' || a === 'stretch' || a === 'variant' || a === 'style' ? this.attr('font-' + a, v) : this.attr(a, v);
}
}); // Add events to elements
const methods = ['click', 'dblclick', 'mousedown', 'mouseup', 'mouseover', 'mouseout', 'mousemove', 'mouseenter', 'mouseleave', 'touchstart', 'touchmove', 'touchleave', 'touchend', 'touchcancel'].reduce(function (last, event) {
// add event to Element
const fn = function (f) {
if (f === null) {
this.off(event);
} else {
this.on(event, f);
}
return this;
};
last[event] = fn;
return last;
}, {});
registerMethods('Element', methods);
function untransform() {
return this.attr('transform', null);
} // merge the whole transformation chain into one matrix and returns it
function matrixify() {
const matrix = (this.attr('transform') || '' // split transformations
).split(transforms).slice(0, -1).map(function (str) {
// generate key => value pairs
const kv = str.trim().split('(');
return [kv[0], kv[1].split(delimiter).map(function (str) {
return parseFloat(str);
})];
}).reverse() // merge every transformation into one matrix
.reduce(function (matrix, transform) {
if (transform[0] === 'matrix') {
return matrix.lmultiply(Matrix.fromArray(transform[1]));
}
return matrix[transform[0]].apply(matrix, transform[1]);
}, new Matrix());
return matrix;
} // add an element to another parent without changing the visual representation on the screen
function toParent(parent, i) {
if (this === parent) return this;
const ctm = this.screenCTM();
const pCtm = parent.screenCTM().inverse();
this.addTo(parent, i).untransform().transform(pCtm.multiply(ctm));
return this;
} // same as above with parent equals root-svg
function toRoot(i) {
return this.toParent(this.root(), i);
} // Add transformations
function transform(o, relative) {
// Act as a getter if no object was passed
if (o == null || typeof o === 'string') {
const decomposed = new Matrix(this).decompose();
return o == null ? decomposed : decomposed[o];
}
if (!Matrix.isMatrixLike(o)) {
// Set the origin according to the defined transform
o = { ...o,
origin: getOrigin(o, this)
};
} // The user can pass a boolean, an Element or an Matrix or nothing
const cleanRelative = relative === true ? this : relative || false;
const result = new Matrix(cleanRelative).transform(o);
return this.attr('transform', result);
}
registerMethods('Element', {
untransform,
matrixify,
toParent,
toRoot,
transform
});
class Container extends Element {
flatten(parent = this, index) {
this.each(function () {
if (this instanceof Container) {
return this.flatten().ungroup();
}
});
return this;
}
ungroup(parent = this.parent(), index = parent.index(this)) {
// when parent != this, we want append all elements to the end
index = index === -1 ? parent.children().length : index;
this.each(function (i, children) {
// reverse each
return children[children.length - i - 1].toParent(parent, index);
});
return this.remove();
}
}
register(Container, 'Container');
class Defs extends Container {
constructor(node, attrs = node) {
super(nodeOrNew('defs', node), attrs);
}
flatten() {
return this;
}
ungroup() {
return this;
}
}
register(Defs, 'Defs');
class Shape extends Element {}
register(Shape, 'Shape');
function rx(rx) {
return this.attr('rx', rx);
} // Radius y value
function ry(ry) {
return this.attr('ry', ry);
} // Move over x-axis
function x$3(x) {
return x == null ? this.cx() - this.rx() : this.cx(x + this.rx());
} // Move over y-axis
function y$3(y) {
return y == null ? this.cy() - this.ry() : this.cy(y + this.ry());
} // Move by center over x-axis
function cx$1(x) {
return this.attr('cx', x);
} // Move by center over y-axis
function cy$1(y) {
return this.attr('cy', y);
} // Set width of element
function width$2(width) {
return width == null ? this.rx() * 2 : this.rx(new SVGNumber(width).divide(2));
} // Set height of element
function height$2(height) {
return height == null ? this.ry() * 2 : this.ry(new SVGNumber(height).divide(2));
}
var circled = {
__proto__: null,
rx: rx,
ry: ry,
x: x$3,
y: y$3,
cx: cx$1,
cy: cy$1,
width: width$2,
height: height$2
};
class Ellipse extends Shape {
constructor(node, attrs = node) {
super(nodeOrNew('ellipse', node), attrs);
}
size(width, height) {
const p = proportionalSize(this, width, height);
return this.rx(new SVGNumber(p.width).divide(2)).ry(new SVGNumber(p.height).divide(2));
}
}
extend(Ellipse, circled);
registerMethods('Container', {
// Create an ellipse
ellipse: wrapWithAttrCheck(function (width = 0, height = width) {
return this.put(new Ellipse()).size(width, height).move(0, 0);
})
});
register(Ellipse, 'Ellipse');
class Fragment extends Dom {
constructor(node = globals.document.createDocumentFragment()) {
super(node);
} // Import / Export raw xml
xml(xmlOrFn, outerXML, ns) {
if (typeof xmlOrFn === 'boolean') {
ns = outerXML;
outerXML = xmlOrFn;
xmlOrFn = null;
} // because this is a fragment we have to put all elements into a wrapper first
// before we can get the innerXML from it
if (xmlOrFn == null || typeof xmlOrFn === 'function') {
const wrapper = new Dom(create('wrapper', ns));
wrapper.add(this.node.cloneNode(true));
return wrapper.xml(false, ns);
} // Act as setter if we got a string
return super.xml(xmlOrFn, false, ns);
}
}
register(Fragment, 'Fragment');
function from(x, y) {
return (this._element || this).type === 'radialGradient' ? this.attr({
fx: new SVGNumber(x),
fy: new SVGNumber(y)
}) : this.attr({
x1: new SVGNumber(x),
y1: new SVGNumber(y)
});
}
function to(x, y) {
return (this._element || this).type === 'radialGradient' ? this.attr({
cx: new SVGNumber(x),
cy: new SVGNumber(y)
}) : this.attr({
x2: new SVGNumber(x),
y2: new SVGNumber(y)
});
}
var gradiented = {
__proto__: null,
from: from,
to: to
};
class Gradient extends Container {
constructor(type, attrs) {
super(nodeOrNew(type + 'Gradient', typeof type === 'string' ? null : type), attrs);
} // custom attr to handle transform
attr(a, b, c) {
if (a === 'transform') a = 'gradientTransform';
return super.attr(a, b, c);
}
bbox() {
return new Box();
}
targets() {
return baseFind('svg [fill*=' + this.id() + ']');
} // Alias string conversion to fill
toString() {
return this.url();
} // Update gradient
update(block) {
// remove all stops
this.clear(); // invoke passed block
if (typeof block === 'function') {
block.call(this, this);
}
return this;
} // Return the fill id
url() {
return 'url(#' + this.id() + ')';
}
}
extend(Gradient, gradiented);
registerMethods({
Container: {
// Create gradient element in defs
gradient(...args) {
return this.defs().gradient(...args);
}
},
// define gradient
Defs: {
gradient: wrapWithAttrCheck(function (type, block) {
return this.put(new Gradient(type)).update(block);
})
}
});
register(Gradient, 'Gradient');
class Pattern extends Container {
// Initialize node
constructor(node, attrs = node) {
super(nodeOrNew('pattern', node), attrs);
} // custom attr to handle transform
attr(a, b, c) {
if (a === 'transform') a = 'patternTransform';
return super.attr(a, b, c);
}
bbox() {
return new Box();
}
targets() {
return baseFind('svg [fill*=' + this.id() + ']');
} // Alias string conversion to fill
toString() {
return this.url();
} // Update pattern by rebuilding
update(block) {
// remove content
this.clear(); // invoke passed block
if (typeof block === 'function') {
block.call(this, this);
}
return this;
} // Return the fill id
url() {
return 'url(#' + this.id() + ')';
}
}
registerMethods({
Container: {
// Create pattern element in defs
pattern(...args) {
return this.defs().pattern(...args);
}
},
Defs: {
pattern: wrapWithAttrCheck(function (width, height, block) {
return this.put(new Pattern()).update(block).attr({
x: 0,
y: 0,
width: width,
height: height,
patternUnits: 'userSpaceOnUse'
});
})
}
});
register(Pattern, 'Pattern');
class Image extends Shape {
constructor(node, attrs = node) {
super(nodeOrNew('image', node), attrs);
} // (re)load image
load(url, callback) {
if (!url) return this;
const img = new globals.window.Image();
on(img, 'load', function (e) {
const p = this.parent(Pattern); // ensure image size
if (this.width() === 0 && this.height() === 0) {
this.size(img.width, img.height);
}
if (p instanceof Pattern) {
// ensure pattern size if not set
if (p.width() === 0 && p.height() === 0) {
p.size(this.width(), this.height());
}
}
if (typeof callback === 'function') {
callback.call(this, e);
}
}, this);
on(img, 'load error', function () {
// dont forget to unbind memory leaking events
off(img);
});
return this.attr('href', img.src = url, xlink);
}
}
registerAttrHook(function (attr, val, _this) {
// convert image fill and stroke to patterns
if (attr === 'fill' || attr === 'stroke') {
if (isImage.test(val)) {
val = _this.root().defs().image(val);
}
}
if (val instanceof Image) {
val = _this.root().defs().pattern(0, 0, pattern => {
pattern.add(val);
});
}
return val;
});
registerMethods({
Container: {
// create image element, load image and set its size
image: wrapWithAttrCheck(function (source, callback) {
return this.put(new Image()).size(0, 0).load(source, callback);
})
}
});
register(Image, 'Image');
class PointArray extends SVGArray {
// Get bounding box of points
bbox() {
let maxX = -Infinity;
let maxY = -Infinity;
let minX = Infinity;
let minY = Infinity;
this.forEach(function (el) {
maxX = Math.max(el[0], maxX);
maxY = Math.max(el[1], maxY);
minX = Math.min(el[0], minX);
minY = Math.min(el[1], minY);
});
return new Box(minX, minY, maxX - minX, maxY - minY);
} // Move point string
move(x, y) {
const box = this.bbox(); // get relative offset
x -= box.x;
y -= box.y; // move every point
if (!isNaN(x) && !isNaN(y)) {
for (let i = this.length - 1; i >= 0; i--) {
this[i] = [this[i][0] + x, this[i][1] + y];
}
}
return this;
} // Parse point string and flat array
parse(array = [0, 0]) {
const points = []; // if it is an array, we flatten it and therefore clone it to 1 depths
if (array instanceof Array) {
array = Array.prototype.concat.apply([], array);
} else {
// Else, it is considered as a string
// parse points
array = array.trim().split(delimiter).map(parseFloat);
} // validate points - https://svgwg.org/svg2-draft/shapes.html#DataTypePoints
// Odd number of coordinates is an error. In such cases, drop the last odd coordinate.
if (array.length % 2 !== 0) array.pop(); // wrap points in two-tuples
for (let i = 0, len = array.length; i < len; i = i + 2) {
points.push([array[i], array[i + 1]]);
}
return points;
} // Resize poly string
size(width, height) {
let i;
const box = this.bbox(); // recalculate position of all points according to new size
for (i = this.length - 1; i >= 0; i--) {
if (box.width) this[i][0] = (this[i][0] - box.x) * width / box.width + box.x;
if (box.height) this[i][1] = (this[i][1] - box.y) * height / box.height + box.y;
}
return this;
} // Convert array to line object
toLine() {
return {
x1: this[0][0],
y1: this[0][1],
x2: this[1][0],
y2: this[1][1]
};
} // Convert array to string
toString() {
const array = []; // convert to a poly point string
for (let i = 0, il = this.length; i < il; i++) {
array.push(this[i].join(','));
}
return array.join(' ');
}
transform(m) {
return this.clone().transformO(m);
} // transform points with matrix (similar to Point.transform)
transformO(m) {
if (!Matrix.isMatrixLike(m)) {
m = new Matrix(m);
}
for (let i = this.length; i--;) {
// Perform the matrix multiplication
const [x, y] = this[i];
this[i][0] = m.a * x + m.c * y + m.e;
this[i][1] = m.b * x + m.d * y + m.f;
}
return this;
}
}
const MorphArray = PointArray; // Move by left top corner over x-axis
function x$2(x) {
return x == null ? this.bbox().x : this.move(x, this.bbox().y);
} // Move by left top corner over y-axis
function y$2(y) {
return y == null ? this.bbox().y : this.move(this.bbox().x, y);
} // Set width of element
function width$1(width) {
const b = this.bbox();
return width == null ? b.width : this.size(width, b.height);
} // Set height of element
function height$1(height) {
const b = this.bbox();
return height == null ? b.height : this.size(b.width, height);
}
var pointed = {
__proto__: null,
MorphArray: MorphArray,
x: x$2,
y: y$2,
width: width$1,
height: height$1
};
class Line extends Shape {
// Initialize node
constructor(node, attrs = node) {
super(nodeOrNew('line', node), attrs);
} // Get array
array() {
return new PointArray([[this.attr('x1'), this.attr('y1')], [this.attr('x2'), this.attr('y2')]]);
} // Move by left top corner
move(x, y) {
return this.attr(this.array().move(x, y).toLine());
} // Overwrite native plot() method
plot(x1, y1, x2, y2) {
if (x1 == null) {
return this.array();
} else if (typeof y1 !== 'undefined') {
x1 = {
x1,
y1,
x2,
y2
};
} else {
x1 = new PointArray(x1).toLine();
}
return this.attr(x1);
} // Set element size to given width and height
size(width, height) {
const p = proportionalSize(this, width, height);
return this.attr(this.array().size(p.width, p.height).toLine());
}
}
extend(Line, pointed);
registerMethods({
Container: {
// Create a line element
line: wrapWithAttrCheck(function (...args) {
// make sure plot is called as a setter
// x1 is not necessarily a number, it can also be an array, a string and a PointArray
return Line.prototype.plot.apply(this.put(new Line()), args[0] != null ? args : [0, 0, 0, 0]);
})
}
});
register(Line, 'Line');
class Marker extends Container {
// Initialize node
constructor(node, attrs = node) {
super(nodeOrNew('marker', node), attrs);
} // Set height of element
height(height) {
return this.attr('markerHeight', height);
}
orient(orient) {
return this.attr('orient', orient);
} // Set marker refX and refY
ref(x, y) {
return this.attr('refX', x).attr('refY', y);
} // Return the fill id
toString() {
return 'url(#' + this.id() + ')';
} // Update marker
update(block) {
// remove all content
this.clear(); // invoke passed block
if (typeof block === 'function') {
block.call(this, this);
}
return this;
} // Set width of element
width(width) {
return this.attr('markerWidth', width);
}
}
registerMethods({
Container: {
marker(...args) {
// Create marker element in defs
return this.defs().marker(...args);
}
},
Defs: {
// Create marker
marker: wrapWithAttrCheck(function (width, height, block) {
// Set default viewbox to match the width and height, set ref to cx and cy and set orient to auto
return this.put(new Marker()).size(width, height).ref(width / 2, height / 2).viewbox(0, 0, width, height).attr('orient', 'auto').update(block);
})
},
marker: {
// Create and attach markers
marker(marker, width, height, block) {
let attr = ['marker']; // Build attribute name
if (marker !== 'all') attr.push(marker);
attr = attr.join('-'); // Set marker attribute
marker = arguments[1] instanceof Marker ? arguments[1] : this.defs().marker(width, height, block);
return this.attr(attr, marker);
}
}
});
register(Marker, 'Marker');
/***
Base Class
==========
The base stepper class that will be
***/
function makeSetterGetter(k, f) {
return function (v) {
if (v == null) return this[k];
this[k] = v;
if (f) f.call(this);
return this;
};
}
const easing = {
'-': function (pos) {
return pos;
},
'<>': function (pos) {
return -Math.cos(pos * Math.PI) / 2 + 0.5;
},
'>': function (pos) {
return Math.sin(pos * Math.PI / 2);
},
'<': function (pos) {
return -Math.cos(pos * Math.PI / 2) + 1;
},
bezier: function (x1, y1, x2, y2) {
// see https://www.w3.org/TR/css-easing-1/#cubic-bezier-algo
return function (t) {
if (t < 0) {
if (x1 > 0) {
return y1 / x1 * t;
} else if (x2 > 0) {
return y2 / x2 * t;
} else {
return 0;
}
} else if (t > 1) {
if (x2 < 1) {
return (1 - y2) / (1 - x2) * t + (y2 - x2) / (1 - x2);
} else if (x1 < 1) {
return (1 - y1) / (1 - x1) * t + (y1 - x1) / (1 - x1);
} else {
return 1;
}
} else {
return 3 * t * (1 - t) ** 2 * y1 + 3 * t ** 2 * (1 - t) * y2 + t ** 3;
}
};
},
// see https://www.w3.org/TR/css-easing-1/#step-timing-function-algo
steps: function (steps, stepPosition = 'end') {
// deal with "jump-" prefix
stepPosition = stepPosition.split('-').reverse()[0];
let jumps = steps;
if (stepPosition === 'none') {
--jumps;
} else if (stepPosition === 'both') {
++jumps;
} // The beforeFlag is essentially useless
return (t, beforeFlag = false) => {
// Step is called currentStep in referenced url
let step = Math.floor(t * steps);
const jumping = t * step % 1 === 0;
if (stepPosition === 'start' || stepPosition === 'both') {
++step;
}
if (beforeFlag && jumping) {
--step;
}
if (t >= 0 && step < 0) {
step = 0;
}
if (t <= 1 && step > jumps) {
step = jumps;
}
return step / jumps;
};
}
};
class Stepper {
done() {
return false;
}
}
/***
Easing Functions
================
***/
class Ease extends Stepper {
constructor(fn = timeline.ease) {
super();
this.ease = easing[fn] || fn;
}
step(from, to, pos) {
if (typeof from !== 'number') {
return pos < 1 ? from : to;
}
return from + (to - from) * this.ease(pos);
}
}
/***
Controller Types
================
***/
class Controller extends Stepper {
constructor(fn) {
super();
this.stepper = fn;
}
done(c) {
return c.done;
}
step(current, target, dt, c) {
return this.stepper(current, target, dt, c);
}
}
function recalculate() {
// Apply the default parameters
const duration = (this._duration || 500) / 1000;
const overshoot = this._overshoot || 0; // Calculate the PID natural response
const eps = 1e-10;
const pi = Math.PI;
const os = Math.log(overshoot / 100 + eps);
const zeta = -os / Math.sqrt(pi * pi + os * os);
const wn = 3.9 / (zeta * duration); // Calculate the Spring values
this.d = 2 * zeta * wn;
this.k = wn * wn;
}
class Spring extends Controller {
constructor(duration = 500, overshoot = 0) {
super();
this.duration(duration).overshoot(overshoot);
}
step(current, target, dt, c) {
if (typeof current === 'string') return current;
c.done = dt === Infinity;
if (dt === Infinity) return target;
if (dt === 0) return current;
if (dt > 100) dt = 16;
dt /= 1000; // Get the previous velocity
const velocity = c.velocity || 0; // Apply the control to get the new position and store it
const acceleration = -this.d * velocity - this.k * (current - target);
const newPosition = current + velocity * dt + acceleration * dt * dt / 2; // Store the velocity
c.velocity = velocity + acceleration * dt; // Figure out if we have converged, and if so, pass the value
c.done = Math.abs(target - newPosition) + Math.abs(velocity) < 0.002;
return c.done ? target : newPosition;
}
}
extend(Spring, {
duration: makeSetterGetter('_duration', recalculate),
overshoot: makeSetterGetter('_overshoot', recalculate)
});
class PID extends Controller {
constructor(p = 0.1, i = 0.01, d = 0, windup = 1000) {
super();
this.p(p).i(i).d(d).windup(windup);
}
step(current, target, dt, c) {
if (typeof current === 'string') return current;
c.done = dt === Infinity;
if (dt === Infinity) return target;
if (dt === 0) return current;
const p = target - current;
let i = (c.integral || 0) + p * dt;
const d = (p - (c.error || 0)) / dt;
const windup = this._windup; // antiwindup
if (windup !== false) {
i = Math.max(-windup, Math.min(i, windup));
}
c.error = p;
c.integral = i;
c.done = Math.abs(p) < 0.001;
return c.done ? target : current + (this.P * p + this.I * i + this.D * d);
}
}
extend(PID, {
windup: makeSetterGetter('_windup'),
p: makeSetterGetter('P'),
i: makeSetterGetter('I'),
d: makeSetterGetter('D')
});
const segmentParameters = {
M: 2,
L: 2,
H: 1,
V: 1,
C: 6,
S: 4,
Q: 4,
T: 2,
A: 7,
Z: 0
};
const pathHandlers = {
M: function (c, p, p0) {
p.x = p0.x = c[0];
p.y = p0.y = c[1];
return ['M', p.x, p.y];
},
L: function (c, p) {
p.x = c[0];
p.y = c[1];
return ['L', c[0], c[1]];
},
H: function (c, p) {
p.x = c[0];
return ['H', c[0]];
},
V: function (c, p) {
p.y = c[0];
return ['V', c[0]];
},
C: function (c, p) {
p.x = c[4];
p.y = c[5];
return ['C', c[0], c[1], c[2], c[3], c[4], c[5]];
},
S: function (c, p) {
p.x = c[2];
p.y = c[3];
return ['S', c[0], c[1], c[2], c[3]];
},
Q: function (c, p) {
p.x = c[2];
p.y = c[3];
return ['Q', c[0], c[1], c[2], c[3]];
},
T: function (c, p) {
p.x = c[0];
p.y = c[1];
return ['T', c[0], c[1]];
},
Z: function (c, p, p0) {
p.x = p0.x;
p.y = p0.y;
return ['Z'];
},
A: function (c, p) {
p.x = c[5];
p.y = c[6];
return ['A', c[0], c[1], c[2], c[3], c[4], c[5], c[6]];
}
};
const mlhvqtcsaz = 'mlhvqtcsaz'.split('');
for (let i = 0, il = mlhvqtcsaz.length; i < il; ++i) {
pathHandlers[mlhvqtcsaz[i]] = function (i) {
return function (c, p, p0) {
if (i === 'H') c[0] = c[0] + p.x;else if (i === 'V') c[0] = c[0] + p.y;else if (i === 'A') {
c[5] = c[5] + p.x;
c[6] = c[6] + p.y;
} else {
for (let j = 0, jl = c.length; j < jl; ++j) {
c[j] = c[j] + (j % 2 ? p.y : p.x);
}
}
return pathHandlers[i](c, p, p0);
};
}(mlhvqtcsaz[i].toUpperCase());
}
function makeAbsolut(parser) {
const command = parser.segment[0];
return pathHandlers[command](parser.segment.slice(1), parser.p, parser.p0);
}
function segmentComplete(parser) {
return parser.segment.length && parser.segment.length - 1 === segmentParameters[parser.segment[0].toUpperCase()];
}
function startNewSegment(parser, token) {
parser.inNumber && finalizeNumber(parser, false);
const pathLetter = isPathLetter.test(token);
if (pathLetter) {
parser.segment = [token];
} else {
const lastCommand = parser.lastCommand;
const small = lastCommand.toLowerCase();
const isSmall = lastCommand === small;
parser.segment = [small === 'm' ? isSmall ? 'l' : 'L' : lastCommand];
}
parser.inSegment = true;
parser.lastCommand = parser.segment[0];
return pathLetter;
}
function finalizeNumber(parser, inNumber) {
if (!parser.inNumber) throw new Error('Parser Error');
parser.number && parser.segment.push(parseFloat(parser.number));
parser.inNumber = inNumber;
parser.number = '';
parser.pointSeen = false;
parser.hasExponent = false;
if (segmentComplete(parser)) {
finalizeSegment(parser);
}
}
function finalizeSegment(parser) {
parser.inSegment = false;
if (parser.absolute) {
parser.segment = makeAbsolut(parser);
}
parser.segments.push(parser.segment);
}
function isArcFlag(parser) {
if (!parser.segment.length) return false;
const isArc = parser.segment[0].toUpperCase() === 'A';
const length = parser.segment.length;
return isArc && (length === 4 || length === 5);
}
function isExponential(parser) {
return parser.lastToken.toUpperCase() === 'E';
}
function pathParser(d, toAbsolute = true) {
let index = 0;
let token = '';
const parser = {
segment: [],
inNumber: false,
number: '',
lastToken: '',
inSegment: false,
segments: [],
pointSeen: false,
hasExponent: false,
absolute: toAbsolute,
p0: new Point(),
p: new Point()
};
while (parser.lastToken = token, token = d.charAt(index++)) {
if (!parser.inSegment) {
if (startNewSegment(parser, token)) {
continue;
}
}
if (token === '.') {
if (parser.pointSeen || parser.hasExponent) {
finalizeNumber(parser, false);
--index;
continue;
}
parser.inNumber = true;
parser.pointSeen = true;
parser.number += token;
continue;
}
if (!isNaN(parseInt(token))) {
if (parser.number === '0' || isArcFlag(parser)) {
parser.inNumber = true;
parser.number = token;
finalizeNumber(parser, true);
continue;
}
parser.inNumber = true;
parser.number += token;
continue;
}
if (token === ' ' || token === ',') {
if (parser.inNumber) {
finalizeNumber(parser, false);
}
continue;
}
if (token === '-') {
if (parser.inNumber && !isExponential(parser)) {
finalizeNumber(parser, false);
--index;
continue;
}
parser.number += token;
parser.inNumber = true;
continue;
}
if (token.toUpperCase() === 'E') {
parser.number += token;
parser.hasExponent = true;
continue;
}
if (isPathLetter.test(token)) {
if (parser.inNumber) {
finalizeNumber(parser, false);
} else if (!segmentComplete(parser)) {
throw new Error('parser Error');
} else {
finalizeSegment(parser);
}
--index;
}
}
if (parser.inNumber) {
finalizeNumber(parser, false);
}
if (parser.inSegment && segmentComplete(parser)) {
finalizeSegment(parser);
}
return parser.segments;
}
function arrayToString(a) {
let s = '';
for (let i = 0, il = a.length; i < il; i++) {
s += a[i][0];
if (a[i][1] != null) {
s += a[i][1];
if (a[i][2] != null) {
s += ' ';
s += a[i][2];
if (a[i][3] != null) {
s += ' ';
s += a[i][3];
s += ' ';
s += a[i][4];
if (a[i][5] != null) {
s += ' ';
s += a[i][5];
s += ' ';
s += a[i][6];
if (a[i][7] != null) {
s += ' ';
s += a[i][7];
}
}
}
}
}
}
return s + ' ';
}
class PathArray extends SVGArray {
// Get bounding box of path
bbox() {
parser().path.setAttribute('d', this.toString());
return new Box(parser.nodes.path.getBBox());
} // Move path string
move(x, y) {
// get bounding box of current situation
const box = this.bbox(); // get relative offset
x -= box.x;
y -= box.y;
if (!isNaN(x) && !isNaN(y)) {
// move every point
for (let l, i = this.length - 1; i >= 0; i--) {
l = this[i][0];
if (l === 'M' || l === 'L' || l === 'T') {
this[i][1] += x;
this[i][2] += y;
} else if (l === 'H') {
this[i][1] += x;
} else if (l === 'V') {
this[i][1] += y;
} else if (l === 'C' || l === 'S' || l === 'Q') {
this[i][1] += x;
this[i][2] += y;
this[i][3] += x;
this[i][4] += y;
if (l === 'C') {
this[i][5] += x;
this[i][6] += y;
}
} else if (l === 'A') {
this[i][6] += x;
this[i][7] += y;
}
}
}
return this;
} // Absolutize and parse path to array
parse(d = 'M0 0') {
if (Array.isArray(d)) {
d = Array.prototype.concat.apply([], d).toString();
}
return pathParser(d);
} // Resize path string
size(width, height) {
// get bounding box of current situation
const box = this.bbox();
let i, l; // If the box width or height is 0 then we ignore
// transformations on the respective axis
box.width = box.width === 0 ? 1 : box.width;
box.height = box.height === 0 ? 1 : box.height; // recalculate position of all points according to new size
for (i = this.length - 1; i >= 0; i--) {
l = this[i][0];
if (l === 'M' || l === 'L' || l === 'T') {
this[i][1] = (this[i][1] - box.x) * width / box.width + box.x;
this[i][2] = (this[i][2] - box.y) * height / box.height + box.y;
} else if (l === 'H') {
this[i][1] = (this[i][1] - box.x) * width / box.width + box.x;
} else if (l === 'V') {
this[i][1] = (this[i][1] - box.y) * height / box.height + box.y;
} else if (l === 'C' || l === 'S' || l === 'Q') {
this[i][1] = (this[i][1] - box.x) * width / box.width + box.x;
this[i][2] = (this[i][2] - box.y) * height / box.height + box.y;
this[i][3] = (this[i][3] - box.x) * width / box.width + box.x;
this[i][4] = (this[i][4] - box.y) * height / box.height + box.y;
if (l === 'C') {
this[i][5] = (this[i][5] - box.x) * width / box.width + box.x;
this[i][6] = (this[i][6] - box.y) * height / box.height + box.y;
}
} else if (l === 'A') {
// resize radii
this[i][1] = this[i][1] * width / box.width;
this[i][2] = this[i][2] * height / box.height; // move position values
this[i][6] = (this[i][6] - box.x) * width / box.width + box.x;
this[i][7] = (this[i][7] - box.y) * height / box.height + box.y;
}
}
return this;
} // Convert array to string
toString() {
return arrayToString(this);
}
}
const getClassForType = value => {
const type = typeof value;
if (type === 'number') {
return SVGNumber;
} else if (type === 'string') {
if (Color.isColor(value)) {
return Color;
} else if (delimiter.test(value)) {
return isPathLetter.test(value) ? PathArray : SVGArray;
} else if (numberAndUnit.test(value)) {
return SVGNumber;
} else {
return NonMorphable;
}
} else if (morphableTypes.indexOf(value.constructor) > -1) {
return value.constructor;
} else if (Array.isArray(value)) {
return SVGArray;
} else if (type === 'object') {
return ObjectBag;
} else {
return NonMorphable;
}
};
class Morphable {
constructor(stepper) {
this._stepper = stepper || new Ease('-');
this._from = null;
this._to = null;
this._type = null;
this._context = null;
this._morphObj = null;
}
at(pos) {
return this._morphObj.morph(this._from, this._to, pos, this._stepper, this._context);
}
done() {
const complete = this._context.map(this._stepper.done).reduce(function (last, curr) {
return last && curr;
}, true);
return complete;
}
from(val) {
if (val == null) {
return this._from;
}
this._from = this._set(val);
return this;
}
stepper(stepper) {
if (stepper == null) return this._stepper;
this._stepper = stepper;
return this;
}
to(val) {
if (val == null) {
return this._to;
}
this._to = this._set(val);
return this;
}
type(type) {
// getter
if (type == null) {
return this._type;
} // setter
this._type = type;
return this;
}
_set(value) {
if (!this._type) {
this.type(getClassForType(value));
}
let result = new this._type(value);
if (this._type === Color) {
result = this._to ? result[this._to[4]]() : this._from ? result[this._from[4]]() : result;
}
if (this._type === ObjectBag) {
result = this._to ? result.align(this._to) : this._from ? result.align(this._from) : result;
}
result = result.toConsumable();
this._morphObj = this._morphObj || new this._type();
this._context = this._context || Array.apply(null, Array(result.length)).map(Object).map(function (o) {
o.done = true;
return o;
});
return result;
}
}
class NonMorphable {
constructor(...args) {
this.init(...args);
}
init(val) {
val = Array.isArray(val) ? val[0] : val;
this.value = val;
return this;
}
toArray() {
return [this.value];
}
valueOf() {
return this.value;
}
}
class TransformBag {
constructor(...args) {
this.init(...args);
}
init(obj) {
if (Array.isArray(obj)) {
obj = {
scaleX: obj[0],
scaleY: obj[1],
shear: obj[2],
rotate: obj[3],
translateX: obj[4],
translateY: obj[5],
originX: obj[6],
originY: obj[7]
};
}
Object.assign(this, TransformBag.defaults, obj);
return this;
}
toArray() {
const v = this;
return [v.scaleX, v.scaleY, v.shear, v.rotate, v.translateX, v.translateY, v.originX, v.originY];
}
}
TransformBag.defaults = {
scaleX: 1,
scaleY: 1,
shear: 0,
rotate: 0,
translateX: 0,
translateY: 0,
originX: 0,
originY: 0
};
const sortByKey = (a, b) => {
return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0;
};
class ObjectBag {
constructor(...args) {
this.init(...args);
}
align(other) {
const values = this.values;
for (let i = 0, il = values.length; i < il; ++i) {
// If the type is the same we only need to check if the color is in the correct format
if (values[i + 1] === other[i + 1]) {
if (values[i + 1] === Color && other[i + 7] !== values[i + 7]) {
const space = other[i + 7];
const color = new Color(this.values.splice(i + 3, 5))[space]().toArray();
this.values.splice(i + 3, 0, ...color);
}
i += values[i + 2] + 2;
continue;
}
if (!other[i + 1]) {
return this;
} // The types differ, so we overwrite the new type with the old one
// And initialize it with the types default (e.g. black for color or 0 for number)
const defaultObject = new other[i + 1]().toArray(); // Than we fix the values array
const toDelete = values[i + 2] + 3;
values.splice(i, toDelete, other[i], other[i + 1], other[i + 2], ...defaultObject);
i += values[i + 2] + 2;
}
return this;
}
init(objOrArr) {
this.values = [];
if (Array.isArray(objOrArr)) {
this.values = objOrArr.slice();
return;
}
objOrArr = objOrArr || {};
const entries = [];
for (const i in objOrArr) {
const Type = getClassForType(objOrArr[i]);
const val = new Type(objOrArr[i]).toArray();
entries.push([i, Type, val.length, ...val]);
}
entries.sort(sortByKey);
this.values = entries.reduce((last, curr) => last.concat(curr), []);
return this;
}
toArray() {
return this.values;
}
valueOf() {
const obj = {};
const arr = this.values; // for (var i = 0, len = arr.length; i < len; i += 2) {
while (arr.length) {
const key = arr.shift();
const Type = arr.shift();
const num = arr.shift();
const values = arr.splice(0, num);
obj[key] = new Type(values); // .valueOf()
}
return obj;
}
}
const morphableTypes = [NonMorphable, TransformBag, ObjectBag];
function registerMorphableType(type = []) {
morphableTypes.push(...[].concat(type));
}
function makeMorphable() {
extend(morphableTypes, {
to(val) {
return new Morphable().type(this.constructor).from(this.toArray()) // this.valueOf())
.to(val);
},
fromArray(arr) {
this.init(arr);
return this;
},
toConsumable() {
return this.toArray();
},
morph(from, to, pos, stepper, context) {
const mapper = function (i, index) {
return stepper.step(i, to[index], pos, context[index], context);
};
return this.fromArray(from.map(mapper));
}
});
}
class Path extends Shape {
// Initialize node
constructor(node, attrs = node) {
super(nodeOrNew('path', node), attrs);
} // Get array
array() {
return this._array || (this._array = new PathArray(this.attr('d')));
} // Clear array cache
clear() {
delete this._array;
return this;
} // Set height of element
height(height) {
return height == null ? this.bbox().height : this.size(this.bbox().width, height);
} // Move by left top corner
move(x, y) {
return this.attr('d', this.array().move(x, y));
} // Plot new path
plot(d) {
return d == null ? this.array() : this.clear().attr('d', typeof d === 'string' ? d : this._array = new PathArray(d));
} // Set element size to given width and height
size(width, height) {
const p = proportionalSize(this, width, height);
return this.attr('d', this.array().size(p.width, p.height));
} // Set width of element
width(width) {
return width == null ? this.bbox().width : this.size(width, this.bbox().height);
} // Move by left top corner over x-axis
x(x) {
return x == null ? this.bbox().x : this.move(x, this.bbox().y);
} // Move by left top corner over y-axis
y(y) {
return y == null ? this.bbox().y : this.move(this.bbox().x, y);
}
} // Define morphable array
Path.prototype.MorphArray = PathArray; // Add parent method
registerMethods({
Container: {
// Create a wrapped path element
path: wrapWithAttrCheck(function (d) {
// make sure plot is called as a setter
return this.put(new Path()).plot(d || new PathArray());
})
}
});
register(Path, 'Path');
function array() {
return this._array || (this._array = new PointArray(this.attr('points')));
} // Clear array cache
function clear() {
delete this._array;
return this;
} // Move by left top corner
function move$2(x, y) {
return this.attr('points', this.array().move(x, y));
} // Plot new path
function plot(p) {
return p == null ? this.array() : this.clear().attr('points', typeof p === 'string' ? p : this._array = new PointArray(p));
} // Set element size to given width and height
function size$1(width, height) {
const p = proportionalSize(this, width, height);
return this.attr('points', this.array().size(p.width, p.height));
}
var poly = {
__proto__: null,
array: array,
clear: clear,
move: move$2,
plot: plot,
size: size$1
};
class Polygon extends Shape {
// Initialize node
constructor(node, attrs = node) {
super(nodeOrNew('polygon', node), attrs);
}
}
registerMethods({
Container: {
// Create a wrapped polygon element
polygon: wrapWithAttrCheck(function (p) {
// make sure plot is called as a setter
return this.put(new Polygon()).plot(p || new PointArray());
})
}
});
extend(Polygon, pointed);
extend(Polygon, poly);
register(Polygon, 'Polygon');
class Polyline extends Shape {
// Initialize node
constructor(node, attrs = node) {
super(nodeOrNew('polyline', node), attrs);
}
}
registerMethods({
Container: {
// Create a wrapped polygon element
polyline: wrapWithAttrCheck(function (p) {
// make sure plot is called as a setter
return this.put(new Polyline()).plot(p || new PointArray());
})
}
});
extend(Polyline, pointed);
extend(Polyline, poly);
register(Polyline, 'Polyline');
class Rect extends Shape {
// Initialize node
constructor(node, attrs = node) {
super(nodeOrNew('rect', node), attrs);
}
}
extend(Rect, {
rx,
ry
});
registerMethods({
Container: {
// Create a rect element
rect: wrapWithAttrCheck(function (width, height) {
return this.put(new Rect()).size(width, height);
})
}
});
register(Rect, 'Rect');
class Queue {
constructor() {
this._first = null;
this._last = null;
} // Shows us the first item in the list
first() {
return this._first && this._first.value;
} // Shows us the last item in the list
last() {
return this._last && this._last.value;
}
push(value) {
// An item stores an id and the provided value
const item = typeof value.next !== 'undefined' ? value : {
value: value,
next: null,
prev: null
}; // Deal with the queue being empty or populated
if (this._last) {
item.prev = this._last;
this._last.next = item;
this._last = item;
} else {
this._last = item;
this._first = item;
} // Return the current item
return item;
} // Removes the item that was returned from the push
remove(item) {
// Relink the previous item
if (item.prev) item.prev.next = item.next;
if (item.next) item.next.prev = item.prev;
if (item === this._last) this._last = item.prev;
if (item === this._first) this._first = item.next; // Invalidate item
item.prev = null;
item.next = null;
}
shift() {
// Check if we have a value
const remove = this._first;
if (!remove) return null; // If we do, remove it and relink things
this._first = remove.next;
if (this._first) this._first.prev = null;
this._last = this._first ? this._last : null;
return remove.value;
}
}
const Animator = {
nextDraw: null,
frames: new Queue(),
timeouts: new Queue(),
immediates: new Queue(),
timer: () => globals.window.performance || globals.window.Date,
transforms: [],
frame(fn) {
// Store the node
const node = Animator.frames.push({
run: fn
}); // Request an animation frame if we don't have one
if (Animator.nextDraw === null) {
Animator.nextDraw = globals.window.requestAnimationFrame(Animator._draw);
} // Return the node so we can remove it easily
return node;
},
timeout(fn, delay) {
delay = delay || 0; // Work out when the event should fire
const time = Animator.timer().now() + delay; // Add the timeout to the end of the queue
const node = Animator.timeouts.push({
run: fn,
time: time
}); // Request another animation frame if we need one
if (Animator.nextDraw === null) {
Animator.nextDraw = globals.window.requestAnimationFrame(Animator._draw);
}
return node;
},
immediate(fn) {
// Add the immediate fn to the end of the queue
const node = Animator.immediates.push(fn); // Request another animation frame if we need one
if (Animator.nextDraw === null) {
Animator.nextDraw = globals.window.requestAnimationFrame(Animator._draw);
}
return node;
},
cancelFrame(node) {
node != null && Animator.frames.remove(node);
},
clearTimeout(node) {
node != null && Animator.timeouts.remove(node);
},
cancelImmediate(node) {
node != null && Animator.immediates.remove(node);
},
_draw(now) {
// Run all the timeouts we can run, if they are not ready yet, add them
// to the end of the queue immediately! (bad timeouts!!! [sarcasm])
let nextTimeout = null;
const lastTimeout = Animator.timeouts.last();
while (nextTimeout = Animator.timeouts.shift()) {
// Run the timeout if its time, or push it to the end
if (now >= nextTimeout.time) {
nextTimeout.run();
} else {
Animator.timeouts.push(nextTimeout);
} // If we hit the last item, we should stop shifting out more items
if (nextTimeout === lastTimeout) break;
} // Run all of the animation frames
let nextFrame = null;
const lastFrame = Animator.frames.last();
while (nextFrame !== lastFrame && (nextFrame = Animator.frames.shift())) {
nextFrame.run(now);
}
let nextImmediate = null;
while (nextImmediate = Animator.immediates.shift()) {
nextImmediate();
} // If we have remaining timeouts or frames, draw until we don't anymore
Animator.nextDraw = Animator.timeouts.first() || Animator.frames.first() ? globals.window.requestAnimationFrame(Animator._draw) : null;
}
};
const makeSchedule = function (runnerInfo) {
const start = runnerInfo.start;
const duration = runnerInfo.runner.duration();
const end = start + duration;
return {
start: start,
duration: duration,
end: end,
runner: runnerInfo.runner
};
};
const defaultSource = function () {
const w = globals.window;
return (w.performance || w.Date).now();
};
class Timeline extends EventTarget {
// Construct a new timeline on the given element
constructor(timeSource = defaultSource) {
super();
this._timeSource = timeSource; // Store the timing variables
this._startTime = 0;
this._speed = 1.0; // Determines how long a runner is hold in memory. Can be a dt or true/false
this._persist = 0; // Keep track of the running animations and their starting parameters
this._nextFrame = null;
this._paused = true;
this._runners = [];
this._runnerIds = [];
this._lastRunnerId = -1;
this._time = 0;
this._lastSourceTime = 0;
this._lastStepTime = 0; // Make sure that step is always called in class context
this._step = this._stepFn.bind(this, false);
this._stepImmediate = this._stepFn.bind(this, true);
}
active() {
return !!this._nextFrame;
}
finish() {
// Go to end and pause
this.time(this.getEndTimeOfTimeline() + 1);
return this.pause();
} // Calculates the end of the timeline
getEndTime() {
const lastRunnerInfo = this.getLastRunnerInfo();
const lastDuration = lastRunnerInfo ? lastRunnerInfo.runner.duration() : 0;
const lastStartTime = lastRunnerInfo ? lastRunnerInfo.start : this._time;
return lastStartTime + lastDuration;
}
getEndTimeOfTimeline() {
const endTimes = this._runners.map(i => i.start + i.runner.duration());
return Math.max(0, ...endTimes);
}
getLastRunnerInfo() {
return this.getRunnerInfoById(this._lastRunnerId);
}
getRunnerInfoById(id) {
return this._runners[this._runnerIds.indexOf(id)] || null;
}
pause() {
this._paused = true;
return this._continue();
}
persist(dtOrForever) {
if (dtOrForever == null) return this._persist;
this._persist = dtOrForever;
return this;
}
play() {
// Now make sure we are not paused and continue the animation
this._paused = false;
return this.updateTime()._continue();
}
reverse(yes) {
const currentSpeed = this.speed();
if (yes == null) return this.speed(-currentSpeed);
const positive = Math.abs(currentSpeed);
return this.speed(yes ? -positive : positive);
} // schedules a runner on the timeline
schedule(runner, delay, when) {
if (runner == null) {
return this._runners.map(makeSchedule);
} // The start time for the next animation can either be given explicitly,
// derived from the current timeline time or it can be relative to the
// last start time to chain animations directly
let absoluteStartTime = 0;
const endTime = this.getEndTime();
delay = delay || 0; // Work out when to start the animation
if (when == null || when === 'last' || when === 'after') {
// Take the last time and increment
absoluteStartTime = endTime;
} else if (when === 'absolute' || when === 'start') {
absoluteStartTime = delay;
delay = 0;
} else if (when === 'now') {
absoluteStartTime = this._time;
} else if (when === 'relative') {
const runnerInfo = this.getRunnerInfoById(runner.id);
if (runnerInfo) {
absoluteStartTime = runnerInfo.start + delay;
delay = 0;
}
} else if (when === 'with-last') {
const lastRunnerInfo = this.getLastRunnerInfo();
const lastStartTime = lastRunnerInfo ? lastRunnerInfo.start : this._time;
absoluteStartTime = lastStartTime;
} else {
throw new Error('Invalid value for the "when" parameter');
} // Manage runner
runner.unschedule();
runner.timeline(this);
const persist = runner.persist();
const runnerInfo = {
persist: persist === null ? this._persist : persist,
start: absoluteStartTime + delay,
runner
};
this._lastRunnerId = runner.id;
this._runners.push(runnerInfo);
this._runners.sort((a, b) => a.start - b.start);
this._runnerIds = this._runners.map(info => info.runner.id);
this.updateTime()._continue();
return this;
}
seek(dt) {
return this.time(this._time + dt);
}
source(fn) {
if (fn == null) return this._timeSource;
this._timeSource = fn;
return this;
}
speed(speed) {
if (speed == null) return this._speed;
this._speed = speed;
return this;
}
stop() {
// Go to start and pause
this.time(0);
return this.pause();
}
time(time) {
if (time == null) return this._time;
this._time = time;
return this._continue(true);
} // Remove the runner from this timeline
unschedule(runner) {
const index = this._runnerIds.indexOf(runner.id);
if (index < 0) return this;
this._runners.splice(index, 1);
this._runnerIds.splice(index, 1);
runner.timeline(null);
return this;
} // Makes sure, that after pausing the time doesn't jump
updateTime() {
if (!this.active()) {
this._lastSourceTime = this._timeSource();
}
return this;
} // Checks if we are running and continues the animation
_continue(immediateStep = false) {
Animator.cancelFrame(this._nextFrame);
this._nextFrame = null;
if (immediateStep) return this._stepImmediate();
if (this._paused) return this;
this._nextFrame = Animator.frame(this._step);
return this;
}
_stepFn(immediateStep = false) {
// Get the time delta from the last time and update the time
const time = this._timeSource();
let dtSource = time - this._lastSourceTime;
if (immediateStep) dtSource = 0;
const dtTime = this._speed * dtSource + (this._time - this._lastStepTime);
this._lastSourceTime = time; // Only update the time if we use the timeSource.
// Otherwise use the current time
if (!immediateStep) {
// Update the time
this._time += dtTime;
this._time = this._time < 0 ? 0 : this._time;
}
this._lastStepTime = this._time;
this.fire('time', this._time); // This is for the case that the timeline was seeked so that the time
// is now before the startTime of the runner. That is why we need to set
// the runner to position 0
// FIXME:
// However, resetting in insertion order leads to bugs. Considering the case,
// where 2 runners change the same attribute but in different times,
// resetting both of them will lead to the case where the later defined
// runner always wins the reset even if the other runner started earlier
// and therefore should win the attribute battle
// this can be solved by resetting them backwards
for (let k = this._runners.length; k--;) {
// Get and run the current runner and ignore it if its inactive
const runnerInfo = this._runners[k];
const runner = runnerInfo.runner; // Make sure that we give the actual difference
// between runner start time and now
const dtToStart = this._time - runnerInfo.start; // Dont run runner if not started yet
// and try to reset it
if (dtToStart <= 0) {
runner.reset();
}
} // Run all of the runners directly
let runnersLeft = false;
for (let i = 0, len = this._runners.length; i < len; i++) {
// Get and run the current runner and ignore it if its inactive
const runnerInfo = this._runners[i];
const runner = runnerInfo.runner;
let dt = dtTime; // Make sure that we give the actual difference
// between runner start time and now
const dtToStart = this._time - runnerInfo.start; // Dont run runner if not started yet
if (dtToStart <= 0) {
runnersLeft = true;
continue;
} else if (dtToStart < dt) {
// Adjust dt to make sure that animation is on point
dt = dtToStart;
}
if (!runner.active()) continue; // If this runner is still going, signal that we need another animation
// frame, otherwise, remove the completed runner
const finished = runner.step(dt).done;
if (!finished) {
runnersLeft = true; // continue
} else if (runnerInfo.persist !== true) {
// runner is finished. And runner might get removed
const endTime = runner.duration() - runner.time() + this._time;
if (endTime + runnerInfo.persist < this._time) {
// Delete runner and correct index
runner.unschedule();
--i;
--len;
}
}
} // Basically: we continue when there are runners right from us in time
// when -->, and when runners are left from us when <--
if (runnersLeft && !(this._speed < 0 && this._time === 0) || this._runnerIds.length && this._speed < 0 && this._time > 0) {
this._continue();
} else {
this.pause();
this.fire('finished');
}
return this;
}
}
registerMethods({
Element: {
timeline: function (timeline) {
if (timeline == null) {
this._timeline = this._timeline || new Timeline();
return this._timeline;
} else {
this._timeline = timeline;
return this;
}
}
}
});
class Runner extends EventTarget {
constructor(options) {
super(); // Store a unique id on the runner, so that we can identify it later
this.id = Runner.id++; // Ensure a default value
options = options == null ? timeline.duration : options; // Ensure that we get a controller
options = typeof options === 'function' ? new Controller(options) : options; // Declare all of the variables
this._element = null;
this._timeline = null;
this.done = false;
this._queue = []; // Work out the stepper and the duration
this._duration = typeof options === 'number' && options;
this._isDeclarative = options instanceof Controller;
this._stepper = this._isDeclarative ? options : new Ease(); // We copy the current values from the timeline because they can change
this._history = {}; // Store the state of the runner
this.enabled = true;
this._time = 0;
this._lastTime = 0; // At creation, the runner is in reset state
this._reseted = true; // Save transforms applied to this runner
this.transforms = new Matrix();
this.transformId = 1; // Looping variables
this._haveReversed = false;
this._reverse = false;
this._loopsDone = 0;
this._swing = false;
this._wait = 0;
this._times = 1;
this._frameId = null; // Stores how long a runner is stored after being done
this._persist = this._isDeclarative ? true : null;
}
static sanitise(duration, delay, when) {
// Initialise the default parameters
let times = 1;
let swing = false;
let wait = 0;
duration = duration || timeline.duration;
delay = delay || timeline.delay;
when = when || 'last'; // If we have an object, unpack the values
if (typeof duration === 'object' && !(duration instanceof Stepper)) {
delay = duration.delay || delay;
when = duration.when || when;
swing = duration.swing || swing;
times = duration.times || times;
wait = duration.wait || wait;
duration = duration.duration || timeline.duration;
}
return {
duration: duration,
delay: delay,
swing: swing,
times: times,
wait: wait,
when: when
};
}
active(enabled) {
if (enabled == null) return this.enabled;
this.enabled = enabled;
return this;
}
/*
Private Methods
===============
Methods that shouldn't be used externally
*/
addTransform(transform, index) {
this.transforms.lmultiplyO(transform);
return this;
}
after(fn) {
return this.on('finished', fn);
}
animate(duration, delay, when) {
const o = Runner.sanitise(duration, delay, when);
const runner = new Runner(o.duration);
if (this._timeline) runner.timeline(this._timeline);
if (this._element) runner.element(this._element);
return runner.loop(o).schedule(o.delay, o.when);
}
clearTransform() {
this.transforms = new Matrix();
return this;
} // TODO: Keep track of all transformations so that deletion is faster
clearTransformsFromQueue() {
if (!this.done || !this._timeline || !this._timeline._runnerIds.includes(this.id)) {
this._queue = this._queue.filter(item => {
return !item.isTransform;
});
}
}
delay(delay) {
return this.animate(0, delay);
}
duration() {
return this._times * (this._wait + this._duration) - this._wait;
}
during(fn) {
return this.queue(null, fn);
}
ease(fn) {
this._stepper = new Ease(fn);
return this;
}
/*
Runner Definitions
==================
These methods help us define the runtime behaviour of the Runner or they
help us make new runners from the current runner
*/
element(element) {
if (element == null) return this._element;
this._element = element;
element._prepareRunner();
return this;
}
finish() {
return this.step(Infinity);
}
loop(times, swing, wait) {
// Deal with the user passing in an object
if (typeof times === 'object') {
swing = times.swing;
wait = times.wait;
times = times.times;
} // Sanitise the values and store them
this._times = times || Infinity;
this._swing = swing || false;
this._wait = wait || 0; // Allow true to be passed
if (this._times === true) {
this._times = Infinity;
}
return this;
}
loops(p) {
const loopDuration = this._duration + this._wait;
if (p == null) {
const loopsDone = Math.floor(this._time / loopDuration);
const relativeTime = this._time - loopsDone * loopDuration;
const position = relativeTime / this._duration;
return Math.min(loopsDone + position, this._times);
}
const whole = Math.floor(p);
const partial = p % 1;
const time = loopDuration * whole + this._duration * partial;
return this.time(time);
}
persist(dtOrForever) {
if (dtOrForever == null) return this._persist;
this._persist = dtOrForever;
return this;
}
position(p) {
// Get all of the variables we need
const x = this._time;
const d = this._duration;
const w = this._wait;
const t = this._times;
const s = this._swing;
const r = this._reverse;
let position;
if (p == null) {
/*
This function converts a time to a position in the range [0, 1]
The full explanation can be found in this desmos demonstration
https://www.desmos.com/calculator/u4fbavgche
The logic is slightly simplified here because we can use booleans
*/
// Figure out the value without thinking about the start or end time
const f = function (x) {
const swinging = s * Math.floor(x % (2 * (w + d)) / (w + d));
const backwards = swinging && !r || !swinging && r;
const uncliped = Math.pow(-1, backwards) * (x % (w + d)) / d + backwards;
const clipped = Math.max(Math.min(uncliped, 1), 0);
return clipped;
}; // Figure out the value by incorporating the start time
const endTime = t * (w + d) - w;
position = x <= 0 ? Math.round(f(1e-5)) : x < endTime ? f(x) : Math.round(f(endTime - 1e-5));
return position;
} // Work out the loops done and add the position to the loops done
const loopsDone = Math.floor(this.loops());
const swingForward = s && loopsDone % 2 === 0;
const forwards = swingForward && !r || r && swingForward;
position = loopsDone + (forwards ? p : 1 - p);
return this.loops(position);
}
progress(p) {
if (p == null) {
return Math.min(1, this._time / this.duration());
}
return this.time(p * this.duration());
}
/*
Basic Functionality
===================
These methods allow us to attach basic functions to the runner directly
*/
queue(initFn, runFn, retargetFn, isTransform) {
this._queue.push({
initialiser: initFn || noop,
runner: runFn || noop,
retarget: retargetFn,
isTransform: isTransform,
initialised: false,
finished: false
});
const timeline = this.timeline();
timeline && this.timeline()._continue();
return this;
}
reset() {
if (this._reseted) return this;
this.time(0);
this._reseted = true;
return this;
}
reverse(reverse) {
this._reverse = reverse == null ? !this._reverse : reverse;
return this;
}
schedule(timeline, delay, when) {
// The user doesn't need to pass a timeline if we already have one
if (!(timeline instanceof Timeline)) {
when = delay;
delay = timeline;
timeline = this.timeline();
} // If there is no timeline, yell at the user...
if (!timeline) {
throw Error('Runner cannot be scheduled without timeline');
} // Schedule the runner on the timeline provided
timeline.schedule(this, delay, when);
return this;
}
step(dt) {
// If we are inactive, this stepper just gets skipped
if (!this.enabled) return this; // Update the time and get the new position
dt = dt == null ? 16 : dt;
this._time += dt;
const position = this.position(); // Figure out if we need to run the stepper in this frame
const running = this._lastPosition !== position && this._time >= 0;
this._lastPosition = position; // Figure out if we just started
const duration = this.duration();
const justStarted = this._lastTime <= 0 && this._time > 0;
const justFinished = this._lastTime < duration && this._time >= duration;
this._lastTime = this._time;
if (justStarted) {
this.fire('start', this);
} // Work out if the runner is finished set the done flag here so animations
// know, that they are running in the last step (this is good for
// transformations which can be merged)
const declarative = this._isDeclarative;
this.done = !declarative && !justFinished && this._time >= duration; // Runner is running. So its not in reset state anymore
this._reseted = false;
let converged = false; // Call initialise and the run function
if (running || declarative) {
this._initialise(running); // clear the transforms on this runner so they dont get added again and again
this.transforms = new Matrix();
converged = this._run(declarative ? dt : position);
this.fire('step', this);
} // correct the done flag here
// declarative animations itself know when they converged
this.done = this.done || converged && declarative;
if (justFinished) {
this.fire('finished', this);
}
return this;
}
/*
Runner animation methods
========================
Control how the animation plays
*/
time(time) {
if (time == null) {
return this._time;
}
const dt = time - this._time;
this.step(dt);
return this;
}
timeline(timeline) {
// check explicitly for undefined so we can set the timeline to null
if (typeof timeline === 'undefined') return this._timeline;
this._timeline = timeline;
return this;
}
unschedule() {
const timeline = this.timeline();
timeline && timeline.unschedule(this);
return this;
} // Run each initialise function in the runner if required
_initialise(running) {
// If we aren't running, we shouldn't initialise when not declarative
if (!running && !this._isDeclarative) return; // Loop through all of the initialisers
for (let i = 0, len = this._queue.length; i < len; ++i) {
// Get the current initialiser
const current = this._queue[i]; // Determine whether we need to initialise
const needsIt = this._isDeclarative || !current.initialised && running;
running = !current.finished; // Call the initialiser if we need to
if (needsIt && running) {
current.initialiser.call(this);
current.initialised = true;
}
}
} // Save a morpher to the morpher list so that we can retarget it later
_rememberMorpher(method, morpher) {
this._history[method] = {
morpher: morpher,
caller: this._queue[this._queue.length - 1]
}; // We have to resume the timeline in case a controller
// is already done without being ever run
// This can happen when e.g. this is done:
// anim = el.animate(new SVG.Spring)
// and later
// anim.move(...)
if (this._isDeclarative) {
const timeline = this.timeline();
timeline && timeline.play();
}
} // Try to set the target for a morpher if the morpher exists, otherwise
// Run each run function for the position or dt given
_run(positionOrDt) {
// Run all of the _queue directly
let allfinished = true;
for (let i = 0, len = this._queue.length; i < len; ++i) {
// Get the current function to run
const current = this._queue[i]; // Run the function if its not finished, we keep track of the finished
// flag for the sake of declarative _queue
const converged = current.runner.call(this, positionOrDt);
current.finished = current.finished || converged === true;
allfinished = allfinished && current.finished;
} // We report when all of the constructors are finished
return allfinished;
} // do nothing and return false
_tryRetarget(method, target, extra) {
if (this._history[method]) {
// if the last method wasn't even initialised, throw it away
if (!this._history[method].caller.initialised) {
const index = this._queue.indexOf(this._history[method].caller);
this._queue.splice(index, 1);
return false;
} // for the case of transformations, we use the special retarget function
// which has access to the outer scope
if (this._history[method].caller.retarget) {
this._history[method].caller.retarget.call(this, target, extra); // for everything else a simple morpher change is sufficient
} else {
this._history[method].morpher.to(target);
}
this._history[method].caller.finished = false;
const timeline = this.timeline();
timeline && timeline.play();
return true;
}
return false;
}
}
Runner.id = 0;
class FakeRunner {
constructor(transforms = new Matrix(), id = -1, done = true) {
this.transforms = transforms;
this.id = id;
this.done = done;
}
clearTransformsFromQueue() {}
}
extend([Runner, FakeRunner], {
mergeWith(runner) {
return new FakeRunner(runner.transforms.lmultiply(this.transforms), runner.id);
}
}); // FakeRunner.emptyRunner = new FakeRunner()
const lmultiply = (last, curr) => last.lmultiplyO(curr);
const getRunnerTransform = runner => runner.transforms;
function mergeTransforms() {
// Find the matrix to apply to the element and apply it
const runners = this._transformationRunners.runners;
const netTransform = runners.map(getRunnerTransform).reduce(lmultiply, new Matrix());
this.transform(netTransform);
this._transformationRunners.merge();
if (this._transformationRunners.length() === 1) {
this._frameId = null;
}
}
class RunnerArray {
constructor() {
this.runners = [];
this.ids = [];
}
add(runner) {
if (this.runners.includes(runner)) return;
const id = runner.id + 1;
this.runners.push(runner);
this.ids.push(id);
return this;
}
clearBefore(id) {
const deleteCnt = this.ids.indexOf(id + 1) || 1;
this.ids.splice(0, deleteCnt, 0);
this.runners.splice(0, deleteCnt, new FakeRunner()).forEach(r => r.clearTransformsFromQueue());
return this;
}
edit(id, newRunner) {
const index = this.ids.indexOf(id + 1);
this.ids.splice(index, 1, id + 1);
this.runners.splice(index, 1, newRunner);
return this;
}
getByID(id) {
return this.runners[this.ids.indexOf(id + 1)];
}
length() {
return this.ids.length;
}
merge() {
let lastRunner = null;
for (let i = 0; i < this.runners.length; ++i) {
const runner = this.runners[i];
const condition = lastRunner && runner.done && lastRunner.done // don't merge runner when persisted on timeline
&& (!runner._timeline || !runner._timeline._runnerIds.includes(runner.id)) && (!lastRunner._timeline || !lastRunner._timeline._runnerIds.includes(lastRunner.id));
if (condition) {
// the +1 happens in the function
this.remove(runner.id);
const newRunner = runner.mergeWith(lastRunner);
this.edit(lastRunner.id, newRunner);
lastRunner = newRunner;
--i;
} else {
lastRunner = runner;
}
}
return this;
}
remove(id) {
const index = this.ids.indexOf(id + 1);
this.ids.splice(index, 1);
this.runners.splice(index, 1);
return this;
}
}
registerMethods({
Element: {
animate(duration, delay, when) {
const o = Runner.sanitise(duration, delay, when);
const timeline = this.timeline();
return new Runner(o.duration).loop(o).element(this).timeline(timeline.play()).schedule(o.delay, o.when);
},
delay(by, when) {
return this.animate(0, by, when);
},
// this function searches for all runners on the element and deletes the ones
// which run before the current one. This is because absolute transformations
// overwrite anything anyway so there is no need to waste time computing
// other runners
_clearTransformRunnersBefore(currentRunner) {
this._transformationRunners.clearBefore(currentRunner.id);
},
_currentTransform(current) {
return this._transformationRunners.runners // we need the equal sign here to make sure, that also transformations
// on the same runner which execute before the current transformation are
// taken into account
.filter(runner => runner.id <= current.id).map(getRunnerTransform).reduce(lmultiply, new Matrix());
},
_addRunner(runner) {
this._transformationRunners.add(runner); // Make sure that the runner merge is executed at the very end of
// all Animator functions. That is why we use immediate here to execute
// the merge right after all frames are run
Animator.cancelImmediate(this._frameId);
this._frameId = Animator.immediate(mergeTransforms.bind(this));
},
_prepareRunner() {
if (this._frameId == null) {
this._transformationRunners = new RunnerArray().add(new FakeRunner(new Matrix(this)));
}
}
}
}); // Will output the elements from array A that are not in the array B
const difference = (a, b) => a.filter(x => !b.includes(x));
extend(Runner, {
attr(a, v) {
return this.styleAttr('attr', a, v);
},
// Add animatable styles
css(s, v) {
return this.styleAttr('css', s, v);
},
styleAttr(type, nameOrAttrs, val) {
if (typeof nameOrAttrs === 'string') {
return this.styleAttr(type, {
[nameOrAttrs]: val
});
}
let attrs = nameOrAttrs;
if (this._tryRetarget(type, attrs)) return this;
let morpher = new Morphable(this._stepper).to(attrs);
let keys = Object.keys(attrs);
this.queue(function () {
morpher = morpher.from(this.element()[type](keys));
}, function (pos) {
this.element()[type](morpher.at(pos).valueOf());
return morpher.done();
}, function (newToAttrs) {
// Check if any new keys were added
const newKeys = Object.keys(newToAttrs);
const differences = difference(newKeys, keys); // If their are new keys, initialize them and add them to morpher
if (differences.length) {
// Get the values
const addedFromAttrs = this.element()[type](differences); // Get the already initialized values
const oldFromAttrs = new ObjectBag(morpher.from()).valueOf(); // Merge old and new
Object.assign(oldFromAttrs, addedFromAttrs);
morpher.from(oldFromAttrs);
} // Get the object from the morpher
const oldToAttrs = new ObjectBag(morpher.to()).valueOf(); // Merge in new attributes
Object.assign(oldToAttrs, newToAttrs); // Change morpher target
morpher.to(oldToAttrs); // Make sure that we save the work we did so we don't need it to do again
keys = newKeys;
attrs = newToAttrs;
});
this._rememberMorpher(type, morpher);
return this;
},
zoom(level, point) {
if (this._tryRetarget('zoom', level, point)) return this;
let morpher = new Morphable(this._stepper).to(new SVGNumber(level));
this.queue(function () {
morpher = morpher.from(this.element().zoom());
}, function (pos) {
this.element().zoom(morpher.at(pos), point);
return morpher.done();
}, function (newLevel, newPoint) {
point = newPoint;
morpher.to(newLevel);
});
this._rememberMorpher('zoom', morpher);
return this;
},
/**
** absolute transformations
**/
//
// M v -----|-----(D M v = F v)------|-----> T v
//
// 1. define the final state (T) and decompose it (once)
// t = [tx, ty, the, lam, sy, sx]
// 2. on every frame: pull the current state of all previous transforms
// (M - m can change)
// and then write this as m = [tx0, ty0, the0, lam0, sy0, sx0]
// 3. Find the interpolated matrix F(pos) = m + pos * (t - m)
// - Note F(0) = M
// - Note F(1) = T
// 4. Now you get the delta matrix as a result: D = F * inv(M)
transform(transforms, relative, affine) {
// If we have a declarative function, we should retarget it if possible
relative = transforms.relative || relative;
if (this._isDeclarative && !relative && this._tryRetarget('transform', transforms)) {
return this;
} // Parse the parameters
const isMatrix = Matrix.isMatrixLike(transforms);
affine = transforms.affine != null ? transforms.affine : affine != null ? affine : !isMatrix; // Create a morpher and set its type
const morpher = new Morphable(this._stepper).type(affine ? TransformBag : Matrix);
let origin;
let element;
let current;
let currentAngle;
let startTransform;
function setup() {
// make sure element and origin is defined
element = element || this.element();
origin = origin || getOrigin(transforms, element);
startTransform = new Matrix(relative ? undefined : element); // add the runner to the element so it can merge transformations
element._addRunner(this); // Deactivate all transforms that have run so far if we are absolute
if (!relative) {
element._clearTransformRunnersBefore(this);
}
}
function run(pos) {
// clear all other transforms before this in case something is saved
// on this runner. We are absolute. We dont need these!
if (!relative) this.clearTransform();
const {
x,
y
} = new Point(origin).transform(element._currentTransform(this));
let target = new Matrix({ ...transforms,
origin: [x, y]
});
let start = this._isDeclarative && current ? current : startTransform;
if (affine) {
target = target.decompose(x, y);
start = start.decompose(x, y); // Get the current and target angle as it was set
const rTarget = target.rotate;
const rCurrent = start.rotate; // Figure out the shortest path to rotate directly
const possibilities = [rTarget - 360, rTarget, rTarget + 360];
const distances = possibilities.map(a => Math.abs(a - rCurrent));
const shortest = Math.min(...distances);
const index = distances.indexOf(shortest);
target.rotate = possibilities[index];
}
if (relative) {
// we have to be careful here not to overwrite the rotation
// with the rotate method of Matrix
if (!isMatrix) {
target.rotate = transforms.rotate || 0;
}
if (this._isDeclarative && currentAngle) {
start.rotate = currentAngle;
}
}
morpher.from(start);
morpher.to(target);
const affineParameters = morpher.at(pos);
currentAngle = affineParameters.rotate;
current = new Matrix(affineParameters);
this.addTransform(current);
element._addRunner(this);
return morpher.done();
}
function retarget(newTransforms) {
// only get a new origin if it changed since the last call
if ((newTransforms.origin || 'center').toString() !== (transforms.origin || 'center').toString()) {
origin = getOrigin(newTransforms, element);
} // overwrite the old transformations with the new ones
transforms = { ...newTransforms,
origin
};
}
this.queue(setup, run, retarget, true);
this._isDeclarative && this._rememberMorpher('transform', morpher);
return this;
},
// Animatable x-axis
x(x, relative) {
return this._queueNumber('x', x);
},
// Animatable y-axis
y(y) {
return this._queueNumber('y', y);
},
dx(x = 0) {
return this._queueNumberDelta('x', x);
},
dy(y = 0) {
return this._queueNumberDelta('y', y);
},
dmove(x, y) {
return this.dx(x).dy(y);
},
_queueNumberDelta(method, to) {
to = new SVGNumber(to); // Try to change the target if we have this method already registered
if (this._tryRetarget(method, to)) return this; // Make a morpher and queue the animation
const morpher = new Morphable(this._stepper).to(to);
let from = null;
this.queue(function () {
from = this.element()[method]();
morpher.from(from);
morpher.to(from + to);
}, function (pos) {
this.element()[method](morpher.at(pos));
return morpher.done();
}, function (newTo) {
morpher.to(from + new SVGNumber(newTo));
}); // Register the morpher so that if it is changed again, we can retarget it
this._rememberMorpher(method, morpher);
return this;
},
_queueObject(method, to) {
// Try to change the target if we have this method already registered
if (this._tryRetarget(method, to)) return this; // Make a morpher and queue the animation
const morpher = new Morphable(this._stepper).to(to);
this.queue(function () {
morpher.from(this.element()[method]());
}, function (pos) {
this.element()[method](morpher.at(pos));
return morpher.done();
}); // Register the morpher so that if it is changed again, we can retarget it
this._rememberMorpher(method, morpher);
return this;
},
_queueNumber(method, value) {
return this._queueObject(method, new SVGNumber(value));
},
// Animatable center x-axis
cx(x) {
return this._queueNumber('cx', x);
},
// Animatable center y-axis
cy(y) {
return this._queueNumber('cy', y);
},
// Add animatable move
move(x, y) {
return this.x(x).y(y);
},
// Add animatable center
center(x, y) {
return this.cx(x).cy(y);
},
// Add animatable size
size(width, height) {
// animate bbox based size for all other elements
let box;
if (!width || !height) {
box = this._element.bbox();
}
if (!width) {
width = box.width / box.height * height;
}
if (!height) {
height = box.height / box.width * width;
}
return this.width(width).height(height);
},
// Add animatable width
width(width) {
return this._queueNumber('width', width);
},
// Add animatable height
height(height) {
return this._queueNumber('height', height);
},
// Add animatable plot
plot(a, b, c, d) {
// Lines can be plotted with 4 arguments
if (arguments.length === 4) {
return this.plot([a, b, c, d]);
}
if (this._tryRetarget('plot', a)) return this;
const morpher = new Morphable(this._stepper).type(this._element.MorphArray).to(a);
this.queue(function () {
morpher.from(this._element.array());
}, function (pos) {
this._element.plot(morpher.at(pos));
return morpher.done();
});
this._rememberMorpher('plot', morpher);
return this;
},
// Add leading method
leading(value) {
return this._queueNumber('leading', value);
},
// Add animatable viewbox
viewbox(x, y, width, height) {
return this._queueObject('viewbox', new Box(x, y, width, height));
},
update(o) {
if (typeof o !== 'object') {
return this.update({
offset: arguments[0],
color: arguments[1],
opacity: arguments[2]
});
}
if (o.opacity != null) this.attr('stop-opacity', o.opacity);
if (o.color != null) this.attr('stop-color', o.color);
if (o.offset != null) this.attr('offset', o.offset);
return this;
}
});
extend(Runner, {
rx,
ry,
from,
to
});
register(Runner, 'Runner');
class Svg extends Container {
constructor(node, attrs = node) {
super(nodeOrNew('svg', node), attrs);
this.namespace();
} // Creates and returns defs element
defs() {
if (!this.isRoot()) return this.root().defs();
return adopt(this.node.querySelector('defs')) || this.put(new Defs());
}
isRoot() {
return !this.node.parentNode || !(this.node.parentNode instanceof globals.window.SVGElement) && this.node.parentNode.nodeName !== '#document-fragment';
} // Add namespaces
namespace() {
if (!this.isRoot()) return this.root().namespace();
return this.attr({
xmlns: svg,
version: '1.1'
}).attr('xmlns:xlink', xlink, xmlns).attr('xmlns:svgjs', svgjs, xmlns);
}
removeNamespace() {
return this.attr({
xmlns: null,
version: null
}).attr('xmlns:xlink', null, xmlns).attr('xmlns:svgjs', null, xmlns);
} // Check if this is a root svg
// If not, call root() from this element
root() {
if (this.isRoot()) return this;
return super.root();
}
}
registerMethods({
Container: {
// Create nested svg document
nested: wrapWithAttrCheck(function () {
return this.put(new Svg());
})
}
});
register(Svg, 'Svg', true);
class Symbol extends Container {
// Initialize node
constructor(node, attrs = node) {
super(nodeOrNew('symbol', node), attrs);
}
}
registerMethods({
Container: {
symbol: wrapWithAttrCheck(function () {
return this.put(new Symbol());
})
}
});
register(Symbol, 'Symbol');
function plain(text) {
// clear if build mode is disabled
if (this._build === false) {
this.clear();
} // create text node
this.node.appendChild(globals.document.createTextNode(text));
return this;
} // Get length of text element
function length() {
return this.node.getComputedTextLength();
} // Move over x-axis
// Text is moved by its bounding box
// text-anchor does NOT matter
function x$1(x, box = this.bbox()) {
if (x == null) {
return box.x;
}
return this.attr('x', this.attr('x') + x - box.x);
} // Move over y-axis
function y$1(y, box = this.bbox()) {
if (y == null) {
return box.y;
}
return this.attr('y', this.attr('y') + y - box.y);
}
function move$1(x, y, box = this.bbox()) {
return this.x(x, box).y(y, box);
} // Move center over x-axis
function cx(x, box = this.bbox()) {
if (x == null) {
return box.cx;
}
return this.attr('x', this.attr('x') + x - box.cx);
} // Move center over y-axis
function cy(y, box = this.bbox()) {
if (y == null) {
return box.cy;
}
return this.attr('y', this.attr('y') + y - box.cy);
}
function center(x, y, box = this.bbox()) {
return this.cx(x, box).cy(y, box);
}
function ax(x) {
return this.attr('x', x);
}
function ay(y) {
return this.attr('y', y);
}
function amove(x, y) {
return this.ax(x).ay(y);
} // Enable / disable build mode
function build(build) {
this._build = !!build;
return this;
}
var textable = {
__proto__: null,
plain: plain,
length: length,
x: x$1,
y: y$1,
move: move$1,
cx: cx,
cy: cy,
center: center,
ax: ax,
ay: ay,
amove: amove,
build: build
};
class Text extends Shape {
// Initialize node
constructor(node, attrs = node) {
super(nodeOrNew('text', node), attrs);
this.dom.leading = new SVGNumber(1.3); // store leading value for rebuilding
this._rebuild = true; // enable automatic updating of dy values
this._build = false; // disable build mode for adding multiple lines
} // Set / get leading
leading(value) {
// act as getter
if (value == null) {
return this.dom.leading;
} // act as setter
this.dom.leading = new SVGNumber(value);
return this.rebuild();
} // Rebuild appearance type
rebuild(rebuild) {
// store new rebuild flag if given
if (typeof rebuild === 'boolean') {
this._rebuild = rebuild;
} // define position of all lines
if (this._rebuild) {
const self = this;
let blankLineOffset = 0;
const leading = this.dom.leading;
this.each(function (i) {
const fontSize = globals.window.getComputedStyle(this.node).getPropertyValue('font-size');
const dy = leading * new SVGNumber(fontSize);
if (this.dom.newLined) {
this.attr('x', self.attr('x'));
if (this.text() === '\n') {
blankLineOffset += dy;
} else {
this.attr('dy', i ? dy + blankLineOffset : 0);
blankLineOffset = 0;
}
}
});
this.fire('rebuild');
}
return this;
} // overwrite method from parent to set data properly
setData(o) {
this.dom = o;
this.dom.leading = new SVGNumber(o.leading || 1.3);
return this;
} // Set the text content
text(text) {
// act as getter
if (text === undefined) {
const children = this.node.childNodes;
let firstLine = 0;
text = '';
for (let i = 0, len = children.length; i < len; ++i) {
// skip textPaths - they are no lines
if (children[i].nodeName === 'textPath') {
if (i === 0) firstLine = 1;
continue;
} // add newline if its not the first child and newLined is set to true
if (i !== firstLine && children[i].nodeType !== 3 && adopt(children[i]).dom.newLined === true) {
text += '\n';
} // add content of this node
text += children[i].textContent;
}
return text;
} // remove existing content
this.clear().build(true);
if (typeof text === 'function') {
// call block
text.call(this, this);
} else {
// store text and make sure text is not blank
text = (text + '').split('\n'); // build new lines
for (let j = 0, jl = text.length; j < jl; j++) {
this.newLine(text[j]);
}
} // disable build mode and rebuild lines
return this.build(false).rebuild();
}
}
extend(Text, textable);
registerMethods({
Container: {
// Create text element
text: wrapWithAttrCheck(function (text = '') {
return this.put(new Text()).text(text);
}),
// Create plain text element
plain: wrapWithAttrCheck(function (text = '') {
return this.put(new Text()).plain(text);
})
}
});
register(Text, 'Text');
class Tspan extends Shape {
// Initialize node
constructor(node, attrs = node) {
super(nodeOrNew('tspan', node), attrs);
this._build = false; // disable build mode for adding multiple lines
} // Shortcut dx
dx(dx) {
return this.attr('dx', dx);
} // Shortcut dy
dy(dy) {
return this.attr('dy', dy);
} // Create new line
newLine() {
// mark new line
this.dom.newLined = true; // fetch parent
const text = this.parent(); // early return in case we are not in a text element
if (!(text instanceof Text)) {
return this;
}
const i = text.index(this);
const fontSize = globals.window.getComputedStyle(this.node).getPropertyValue('font-size');
const dy = text.dom.leading * new SVGNumber(fontSize); // apply new position
return this.dy(i ? dy : 0).attr('x', text.x());
} // Set text content
text(text) {
if (text == null) return this.node.textContent + (this.dom.newLined ? '\n' : '');
if (typeof text === 'function') {
this.clear().build(true);
text.call(this, this);
this.build(false);
} else {
this.plain(text);
}
return this;
}
}
extend(Tspan, textable);
registerMethods({
Tspan: {
tspan: wrapWithAttrCheck(function (text = '') {
const tspan = new Tspan(); // clear if build mode is disabled
if (!this._build) {
this.clear();
} // add new tspan
return this.put(tspan).text(text);
})
},
Text: {
newLine: function (text = '') {
return this.tspan(text).newLine();
}
}
});
register(Tspan, 'Tspan');
class Circle extends Shape {
constructor(node, attrs = node) {
super(nodeOrNew('circle', node), attrs);
}
radius(r) {
return this.attr('r', r);
} // Radius x value
rx(rx) {
return this.attr('r', rx);
} // Alias radius x value
ry(ry) {
return this.rx(ry);
}
size(size) {
return this.radius(new SVGNumber(size).divide(2));
}
}
extend(Circle, {
x: x$3,
y: y$3,
cx: cx$1,
cy: cy$1,
width: width$2,
height: height$2
});
registerMethods({
Container: {
// Create circle element
circle: wrapWithAttrCheck(function (size = 0) {
return this.put(new Circle()).size(size).move(0, 0);
})
}
});
register(Circle, 'Circle');
class ClipPath extends Container {
constructor(node, attrs = node) {
super(nodeOrNew('clipPath', node), attrs);
} // Unclip all clipped elements and remove itself
remove() {
// unclip all targets
this.targets().forEach(function (el) {
el.unclip();
}); // remove clipPath from parent
return super.remove();
}
targets() {
return baseFind('svg [clip-path*=' + this.id() + ']');
}
}
registerMethods({
Container: {
// Create clipping element
clip: wrapWithAttrCheck(function () {
return this.defs().put(new ClipPath());
})
},
Element: {
// Distribute clipPath to svg element
clipper() {
return this.reference('clip-path');
},
clipWith(element) {
// use given clip or create a new one
const clipper = element instanceof ClipPath ? element : this.parent().clip().add(element); // apply mask
return this.attr('clip-path', 'url(#' + clipper.id() + ')');
},
// Unclip element
unclip() {
return this.attr('clip-path', null);
}
}
});
register(ClipPath, 'ClipPath');
class ForeignObject extends Element {
constructor(node, attrs = node) {
super(nodeOrNew('foreignObject', node), attrs);
}
}
registerMethods({
Container: {
foreignObject: wrapWithAttrCheck(function (width, height) {
return this.put(new ForeignObject()).size(width, height);
})
}
});
register(ForeignObject, 'ForeignObject');
function dmove(dx, dy) {
this.children().forEach((child, i) => {
let bbox; // We have to wrap this for elements that dont have a bbox
// e.g. title and other descriptive elements
try {
// Get the childs bbox
bbox = child.bbox();
} catch (e) {
return;
} // Get childs matrix
const m = new Matrix(child); // Translate childs matrix by amount and
// transform it back into parents space
const matrix = m.translate(dx, dy).transform(m.inverse()); // Calculate new x and y from old box
const p = new Point(bbox.x, bbox.y).transform(matrix); // Move element
child.move(p.x, p.y);
});
return this;
}
function dx(dx) {
return this.dmove(dx, 0);
}
function dy(dy) {
return this.dmove(0, dy);
}
function height(height, box = this.bbox()) {
if (height == null) return box.height;
return this.size(box.width, height, box);
}
function move(x = 0, y = 0, box = this.bbox()) {
const dx = x - box.x;
const dy = y - box.y;
return this.dmove(dx, dy);
}
function size(width, height, box = this.bbox()) {
const p = proportionalSize(this, width, height, box);
const scaleX = p.width / box.width;
const scaleY = p.height / box.height;
this.children().forEach((child, i) => {
const o = new Point(box).transform(new Matrix(child).inverse());
child.scale(scaleX, scaleY, o.x, o.y);
});
return this;
}
function width(width, box = this.bbox()) {
if (width == null) return box.width;
return this.size(width, box.height, box);
}
function x(x, box = this.bbox()) {
if (x == null) return box.x;
return this.move(x, box.y, box);
}
function y(y, box = this.bbox()) {
if (y == null) return box.y;
return this.move(box.x, y, box);
}
var containerGeometry = {
__proto__: null,
dmove: dmove,
dx: dx,
dy: dy,
height: height,
move: move,
size: size,
width: width,
x: x,
y: y
};
class G extends Container {
constructor(node, attrs = node) {
super(nodeOrNew('g', node), attrs);
}
}
extend(G, containerGeometry);
registerMethods({
Container: {
// Create a group element
group: wrapWithAttrCheck(function () {
return this.put(new G());
})
}
});
register(G, 'G');
class A extends Container {
constructor(node, attrs = node) {
super(nodeOrNew('a', node), attrs);
} // Link target attribute
target(target) {
return this.attr('target', target);
} // Link url
to(url) {
return this.attr('href', url, xlink);
}
}
extend(A, containerGeometry);
registerMethods({
Container: {
// Create a hyperlink element
link: wrapWithAttrCheck(function (url) {
return this.put(new A()).to(url);
})
},
Element: {
unlink() {
const link = this.linker();
if (!link) return this;
const parent = link.parent();
if (!parent) {
return this.remove();
}
const index = parent.index(link);
parent.add(this, index);
link.remove();
return this;
},
linkTo(url) {
// reuse old link if possible
let link = this.linker();
if (!link) {
link = new A();
this.wrap(link);
}
if (typeof url === 'function') {
url.call(link, link);
} else {
link.to(url);
}
return this;
},
linker() {
const link = this.parent();
if (link && link.node.nodeName.toLowerCase() === 'a') {
return link;
}
return null;
}
}
});
register(A, 'A');
class Mask extends Container {
// Initialize node
constructor(node, attrs = node) {
super(nodeOrNew('mask', node), attrs);
} // Unmask all masked elements and remove itself
remove() {
// unmask all targets
this.targets().forEach(function (el) {
el.unmask();
}); // remove mask from parent
return super.remove();
}
targets() {
return baseFind('svg [mask*=' + this.id() + ']');
}
}
registerMethods({
Container: {
mask: wrapWithAttrCheck(function () {
return this.defs().put(new Mask());
})
},
Element: {
// Distribute mask to svg element
masker() {
return this.reference('mask');
},
maskWith(element) {
// use given mask or create a new one
const masker = element instanceof Mask ? element : this.parent().mask().add(element); // apply mask
return this.attr('mask', 'url(#' + masker.id() + ')');
},
// Unmask element
unmask() {
return this.attr('mask', null);
}
}
});
register(Mask, 'Mask');
class Stop extends Element {
constructor(node, attrs = node) {
super(nodeOrNew('stop', node), attrs);
} // add color stops
update(o) {
if (typeof o === 'number' || o instanceof SVGNumber) {
o = {
offset: arguments[0],
color: arguments[1],
opacity: arguments[2]
};
} // set attributes
if (o.opacity != null) this.attr('stop-opacity', o.opacity);
if (o.color != null) this.attr('stop-color', o.color);
if (o.offset != null) this.attr('offset', new SVGNumber(o.offset));
return this;
}
}
registerMethods({
Gradient: {
// Add a color stop
stop: function (offset, color, opacity) {
return this.put(new Stop()).update(offset, color, opacity);
}
}
});
register(Stop, 'Stop');
function cssRule(selector, rule) {
if (!selector) return '';
if (!rule) return selector;
let ret = selector + '{';
for (const i in rule) {
ret += unCamelCase(i) + ':' + rule[i] + ';';
}
ret += '}';
return ret;
}
class Style extends Element {
constructor(node, attrs = node) {
super(nodeOrNew('style', node), attrs);
}
addText(w = '') {
this.node.textContent += w;
return this;
}
font(name, src, params = {}) {
return this.rule('@font-face', {
fontFamily: name,
src: src,
...params
});
}
rule(selector, obj) {
return this.addText(cssRule(selector, obj));
}
}
registerMethods('Dom', {
style(selector, obj) {
return this.put(new Style()).rule(selector, obj);
},
fontface(name, src, params) {
return this.put(new Style()).font(name, src, params);
}
});
register(Style, 'Style');
class TextPath extends Text {
// Initialize node
constructor(node, attrs = node) {
super(nodeOrNew('textPath', node), attrs);
} // return the array of the path track element
array() {
const track = this.track();
return track ? track.array() : null;
} // Plot path if any
plot(d) {
const track = this.track();
let pathArray = null;
if (track) {
pathArray = track.plot(d);
}
return d == null ? pathArray : this;
} // Get the path element
track() {
return this.reference('href');
}
}
registerMethods({
Container: {
textPath: wrapWithAttrCheck(function (text, path) {
// Convert text to instance if needed
if (!(text instanceof Text)) {
text = this.text(text);
}
return text.path(path);
})
},
Text: {
// Create path for text to run on
path: wrapWithAttrCheck(function (track, importNodes = true) {
const textPath = new TextPath(); // if track is a path, reuse it
if (!(track instanceof Path)) {
// create path element
track = this.defs().path(track);
} // link textPath to path and add content
textPath.attr('href', '#' + track, xlink); // Transplant all nodes from text to textPath
let node;
if (importNodes) {
while (node = this.node.firstChild) {
textPath.node.appendChild(node);
}
} // add textPath element as child node and return textPath
return this.put(textPath);
}),
// Get the textPath children
textPath() {
return this.findOne('textPath');
}
},
Path: {
// creates a textPath from this path
text: wrapWithAttrCheck(function (text) {
// Convert text to instance if needed
if (!(text instanceof Text)) {
text = new Text().addTo(this.parent()).text(text);
} // Create textPath from text and path and return
return text.path(this);
}),
targets() {
return baseFind('svg textPath').filter(node => {
return (node.attr('href') || '').includes(this.id());
}); // Does not work in IE11. Use when IE support is dropped
// return baseFind('svg textPath[*|href*=' + this.id() + ']')
}
}
});
TextPath.prototype.MorphArray = PathArray;
register(TextPath, 'TextPath');
class Use extends Shape {
constructor(node, attrs = node) {
super(nodeOrNew('use', node), attrs);
} // Use element as a reference
use(element, file) {
// Set lined element
return this.attr('href', (file || '') + '#' + element, xlink);
}
}
registerMethods({
Container: {
// Create a use element
use: wrapWithAttrCheck(function (element, file) {
return this.put(new Use()).use(element, file);
})
}
});
register(Use, 'Use');
/* Optional Modules */
const SVG = makeInstance;
extend([Svg, Symbol, Image, Pattern, Marker], getMethodsFor('viewbox'));
extend([Line, Polyline, Polygon, Path], getMethodsFor('marker'));
extend(Text, getMethodsFor('Text'));
extend(Path, getMethodsFor('Path'));
extend(Defs, getMethodsFor('Defs'));
extend([Text, Tspan], getMethodsFor('Tspan'));
extend([Rect, Ellipse, Gradient, Runner], getMethodsFor('radius'));
extend(EventTarget, getMethodsFor('EventTarget'));
extend(Dom, getMethodsFor('Dom'));
extend(Element, getMethodsFor('Element'));
extend(Shape, getMethodsFor('Shape'));
extend([Container, Fragment], getMethodsFor('Container'));
extend(Gradient, getMethodsFor('Gradient'));
extend(Runner, getMethodsFor('Runner'));
List.extend(getMethodNames());
registerMorphableType([SVGNumber, Color, Box, Matrix, SVGArray, PointArray, PathArray, Point]);
makeMorphable();
export { A, Animator, SVGArray as Array, Box, Circle, ClipPath, Color, Container, Controller, Defs, Dom, Ease, Element, Ellipse, EventTarget, ForeignObject, Fragment, G, Gradient, Image, Line, List, Marker, Mask, Matrix, Morphable, NonMorphable, SVGNumber as Number, ObjectBag, PID, Path, PathArray, Pattern, Point, PointArray, Polygon, Polyline, Queue, Rect, Runner, SVG, Shape, Spring, Stop, Style, Svg, Symbol, Text, TextPath, Timeline, TransformBag, Tspan, Use, adopt, assignNewId, clearEvents, create, defaults, dispatch, easing, eid, extend, baseFind as find, getClass, getEventTarget, getEvents, getWindow, makeInstance, makeMorphable, mockAdopt, namespaces, nodeOrNew, off, on, parser, regex, register, registerMorphableType, registerWindow, restoreWindow, root, saveWindow, utils, windowEvents, withWindow, wrapWithAttrCheck };
//# sourceMappingURL=svg.esm.js.map