7164 lines
175 KiB
JavaScript
7164 lines
175 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)
|
|
*/;
|
|
'use strict';
|
|
|
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
|
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();
|
|
|
|
exports.A = A;
|
|
exports.Animator = Animator;
|
|
exports.Array = SVGArray;
|
|
exports.Box = Box;
|
|
exports.Circle = Circle;
|
|
exports.ClipPath = ClipPath;
|
|
exports.Color = Color;
|
|
exports.Container = Container;
|
|
exports.Controller = Controller;
|
|
exports.Defs = Defs;
|
|
exports.Dom = Dom;
|
|
exports.Ease = Ease;
|
|
exports.Element = Element;
|
|
exports.Ellipse = Ellipse;
|
|
exports.EventTarget = EventTarget;
|
|
exports.ForeignObject = ForeignObject;
|
|
exports.Fragment = Fragment;
|
|
exports.G = G;
|
|
exports.Gradient = Gradient;
|
|
exports.Image = Image;
|
|
exports.Line = Line;
|
|
exports.List = List;
|
|
exports.Marker = Marker;
|
|
exports.Mask = Mask;
|
|
exports.Matrix = Matrix;
|
|
exports.Morphable = Morphable;
|
|
exports.NonMorphable = NonMorphable;
|
|
exports.Number = SVGNumber;
|
|
exports.ObjectBag = ObjectBag;
|
|
exports.PID = PID;
|
|
exports.Path = Path;
|
|
exports.PathArray = PathArray;
|
|
exports.Pattern = Pattern;
|
|
exports.Point = Point;
|
|
exports.PointArray = PointArray;
|
|
exports.Polygon = Polygon;
|
|
exports.Polyline = Polyline;
|
|
exports.Queue = Queue;
|
|
exports.Rect = Rect;
|
|
exports.Runner = Runner;
|
|
exports.SVG = SVG;
|
|
exports.Shape = Shape;
|
|
exports.Spring = Spring;
|
|
exports.Stop = Stop;
|
|
exports.Style = Style;
|
|
exports.Svg = Svg;
|
|
exports.Symbol = Symbol;
|
|
exports.Text = Text;
|
|
exports.TextPath = TextPath;
|
|
exports.Timeline = Timeline;
|
|
exports.TransformBag = TransformBag;
|
|
exports.Tspan = Tspan;
|
|
exports.Use = Use;
|
|
exports.adopt = adopt;
|
|
exports.assignNewId = assignNewId;
|
|
exports.clearEvents = clearEvents;
|
|
exports.create = create;
|
|
exports.defaults = defaults;
|
|
exports.dispatch = dispatch;
|
|
exports.easing = easing;
|
|
exports.eid = eid;
|
|
exports.extend = extend;
|
|
exports.find = baseFind;
|
|
exports.getClass = getClass;
|
|
exports.getEventTarget = getEventTarget;
|
|
exports.getEvents = getEvents;
|
|
exports.getWindow = getWindow;
|
|
exports.makeInstance = makeInstance;
|
|
exports.makeMorphable = makeMorphable;
|
|
exports.mockAdopt = mockAdopt;
|
|
exports.namespaces = namespaces;
|
|
exports.nodeOrNew = nodeOrNew;
|
|
exports.off = off;
|
|
exports.on = on;
|
|
exports.parser = parser;
|
|
exports.regex = regex;
|
|
exports.register = register;
|
|
exports.registerMorphableType = registerMorphableType;
|
|
exports.registerWindow = registerWindow;
|
|
exports.restoreWindow = restoreWindow;
|
|
exports.root = root;
|
|
exports.saveWindow = saveWindow;
|
|
exports.utils = utils;
|
|
exports.windowEvents = windowEvents;
|
|
exports.withWindow = withWindow;
|
|
exports.wrapWithAttrCheck = wrapWithAttrCheck;
|
|
//# sourceMappingURL=svg.node.js.map
|