diff options
Diffstat (limited to 'src/modules')
-rw-r--r-- | src/modules/core/attr.js | 80 | ||||
-rw-r--r-- | src/modules/core/circled.js | 64 | ||||
-rw-r--r-- | src/modules/core/defaults.js | 48 | ||||
-rw-r--r-- | src/modules/core/event.js | 119 | ||||
-rw-r--r-- | src/modules/core/gradiented.js | 14 | ||||
-rw-r--r-- | src/modules/core/namespaces.js | 5 | ||||
-rw-r--r-- | src/modules/core/parser.js | 26 | ||||
-rw-r--r-- | src/modules/core/pointed.js | 25 | ||||
-rw-r--r-- | src/modules/core/poly.js | 31 | ||||
-rw-r--r-- | src/modules/core/regex.js | 58 | ||||
-rw-r--r-- | src/modules/core/selector.js | 16 | ||||
-rw-r--r-- | src/modules/core/textable.js | 18 | ||||
-rw-r--r-- | src/modules/optional/arrange.js | 98 | ||||
-rw-r--r-- | src/modules/optional/class.js | 44 | ||||
-rw-r--r-- | src/modules/optional/css.js | 71 | ||||
-rw-r--r-- | src/modules/optional/data.js | 26 | ||||
-rw-r--r-- | src/modules/optional/memory.js | 39 | ||||
-rw-r--r-- | src/modules/optional/sugar.js | 159 | ||||
-rw-r--r-- | src/modules/optional/transform.js | 72 |
19 files changed, 1013 insertions, 0 deletions
diff --git a/src/modules/core/attr.js b/src/modules/core/attr.js new file mode 100644 index 0000000..ed34dc9 --- /dev/null +++ b/src/modules/core/attr.js @@ -0,0 +1,80 @@ +import { isImage, isNumber } from './regex.js' +import { attrs as defaults } from './defaults.js' +import Color from '../../types/Color.js' +import SVGArray from '../../types/SVGArray.js' +import SVGNumber from '../../types/SVGNumber.js' + +// Set svg element attribute +export default function attr (attr, val, ns) { + // act as full getter + if (attr == null) { + // get an object of attributes + attr = {} + val = this.node.attributes + + for (let node of val) { + attr[node.nodeName] = isNumber.test(node.nodeValue) + ? parseFloat(node.nodeValue) + : node.nodeValue + } + + return attr + } else if (Array.isArray(attr)) { + // FIXME: implement + } else if (typeof attr === '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 ? defaults[attr] // FIXME: do we need to return defaults? + : isNumber.test(val) ? parseFloat(val) + : val + } else { + // convert image fill and stroke to patterns + if (attr === 'fill' || attr === 'stroke') { + if (isImage.test(val)) { + val = this.doc().defs().image(val) + } + } + + // FIXME: This is fine, but what about the lines above? + // How does attr know about image()? + while (typeof val.attrHook === 'function') { + val = val.attrHook(this, attr) + } + + // 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 +} diff --git a/src/modules/core/circled.js b/src/modules/core/circled.js new file mode 100644 index 0000000..9a3b1ad --- /dev/null +++ b/src/modules/core/circled.js @@ -0,0 +1,64 @@ +// FIXME: import this to runner +import { proportionalSize } from '../../utils/utils.js' +import SVGNumber from '../../types/SVGNumber.js' + +// Radius x value +export function rx (rx) { + return this.attr('rx', rx) +} + +// Radius y value +export function ry (ry) { + return this.attr('ry', ry) +} + +// Move over x-axis +export function x (x) { + return x == null + ? this.cx() - this.rx() + : this.cx(x + this.rx()) +} + +// Move over y-axis +export function y (y) { + return y == null + ? this.cy() - this.ry() + : this.cy(y + this.ry()) +} + +// Move by center over x-axis +export function cx (x) { + return x == null + ? this.attr('cx') + : this.attr('cx', x) +} + +// Move by center over y-axis +export function cy (y) { + return y == null + ? this.attr('cy') + : this.attr('cy', y) +} + +// Set width of element +export function width (width) { + return width == null + ? this.rx() * 2 + : this.rx(new SVGNumber(width).divide(2)) +} + +// Set height of element +export function height (height) { + return height == null + ? this.ry() * 2 + : this.ry(new SVGNumber(height).divide(2)) +} + +// Custom size function +export function size (width, height) { + var p = proportionalSize(this, width, height) + + return this + .rx(new SVGNumber(p.width).divide(2)) + .ry(new SVGNumber(p.height).divide(2)) +} diff --git a/src/modules/core/defaults.js b/src/modules/core/defaults.js new file mode 100644 index 0000000..0d496bc --- /dev/null +++ b/src/modules/core/defaults.js @@ -0,0 +1,48 @@ + +export function noop () {} + +// Default animation values +export let timeline = { + duration: 400, + ease: '>', + delay: 0 +} + +// Default attribute values +export let 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 + 'font-size': 16, + 'font-family': 'Helvetica, Arial, sans-serif', + 'text-anchor': 'start' +} diff --git a/src/modules/core/event.js b/src/modules/core/event.js new file mode 100644 index 0000000..2fcaf58 --- /dev/null +++ b/src/modules/core/event.js @@ -0,0 +1,119 @@ +import { delimiter } from './regex.js' +import { makeInstance } from '../../utils/adopter.js' + +let listenerId = 0 + +function getEvents (node) { + const n = makeInstance(node).getEventHolder() + if (!n.events) n.events = {} + return n.events +} + +function getEventTarget (node) { + return makeInstance(node).getEventTarget() +} + +function clearEvents (node) { + const n = makeInstance(node).getEventHolder() + if (n.events) n.events = {} +} + +// Add event binder in the SVG namespace +export function on (node, events, listener, binding, options) { + var l = listener.bind(binding || node) + var bag = getEvents(node) + var n = getEventTarget(node) + + // 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) { + var ev = event.split('.')[0] + var 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 +export function off (node, events, listener, options) { + var bag = getEvents(node) + var n = getEventTarget(node) + + // 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) { + var ev = event && event.split('.')[0] + var ns = event && event.split('.')[1] + var 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(node) + } + }) +} + +export function dispatch (node, event, data) { + var n = getEventTarget(node) + + // Dispatch event + if (event instanceof window.Event) { + n.dispatchEvent(event) + } else { + event = new window.CustomEvent(event, { detail: data, cancelable: true }) + n.dispatchEvent(event) + } + return event +} diff --git a/src/modules/core/gradiented.js b/src/modules/core/gradiented.js new file mode 100644 index 0000000..d34a9fe --- /dev/null +++ b/src/modules/core/gradiented.js @@ -0,0 +1,14 @@ +// FIXME: add to runner +import SVGNumber from '../../types/SVGNumber.js' + +export 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) }) +} + +export 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) }) +} diff --git a/src/modules/core/namespaces.js b/src/modules/core/namespaces.js new file mode 100644 index 0000000..3791298 --- /dev/null +++ b/src/modules/core/namespaces.js @@ -0,0 +1,5 @@ +// Default namespaces +export let ns = 'http://www.w3.org/2000/svg' +export let xmlns = 'http://www.w3.org/2000/xmlns/' +export let xlink = 'http://www.w3.org/1999/xlink' +export let svgjs = 'http://svgjs.com/svgjs' diff --git a/src/modules/core/parser.js b/src/modules/core/parser.js new file mode 100644 index 0000000..7a656ef --- /dev/null +++ b/src/modules/core/parser.js @@ -0,0 +1,26 @@ +import Doc from '../../elements/Doc.js' + +export default function parser () { + // Reuse cached element if possible + if (!parser.nodes) { + let svg = new Doc().size(2, 0) + svg.node.cssText = [ + 'opacity: 0', + 'position: absolute', + 'left: -100%', + 'top: -100%', + 'overflow: hidden' + ].join(';') + + let path = svg.path().node + + parser.nodes = { svg, path } + } + + if (!parser.nodes.svg.node.parentNode) { + let b = document.body || document.documentElement + parser.nodes.svg.addTo(b) + } + + return parser.nodes +} diff --git a/src/modules/core/pointed.js b/src/modules/core/pointed.js new file mode 100644 index 0000000..95e6819 --- /dev/null +++ b/src/modules/core/pointed.js @@ -0,0 +1,25 @@ +import PointArray from '../../types/PointArray.js' + +export let MorphArray = PointArray + +// Move by left top corner over x-axis +export function x (x) { + return x == null ? this.bbox().x : this.move(x, this.bbox().y) +} + +// Move by left top corner over y-axis +export function y (y) { + return y == null ? this.bbox().y : this.move(this.bbox().x, y) +} + +// Set width of element +export function width (width) { + let b = this.bbox() + return width == null ? b.width : this.size(width, b.height) +} + +// Set height of element +export function height (height) { + let b = this.bbox() + return height == null ? b.height : this.size(b.width, height) +} diff --git a/src/modules/core/poly.js b/src/modules/core/poly.js new file mode 100644 index 0000000..ad12020 --- /dev/null +++ b/src/modules/core/poly.js @@ -0,0 +1,31 @@ +import { proportionalSize } from '../../utils/utils.js' +import PointArray from '../../types/PointArray.js' + +// Get array +export function array () { + return this._array || (this._array = new PointArray(this.attr('points'))) +} + +// Plot new path +export function plot (p) { + return (p == null) ? this.array() + : this.clear().attr('points', typeof p === 'string' ? p + : (this._array = new PointArray(p))) +} + +// Clear array cache +export function clear () { + delete this._array + return this +} + +// Move by left top corner +export function move (x, y) { + return this.attr('points', this.array().move(x, y)) +} + +// Set element size to given width and height +export function size (width, height) { + let p = proportionalSize(this, width, height) + return this.attr('points', this.array().size(p.width, p.height)) +} diff --git a/src/modules/core/regex.js b/src/modules/core/regex.js new file mode 100644 index 0000000..1056554 --- /dev/null +++ b/src/modules/core/regex.js @@ -0,0 +1,58 @@ +// Parse unit value +export let numberAndUnit = /^([+-]?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?)([a-z%]*)$/i + +// Parse hex value +export let hex = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i + +// Parse rgb value +export let rgb = /rgb\((\d+),(\d+),(\d+)\)/ + +// Parse reference id +export let reference = /(#[a-z0-9\-_]+)/i + +// splits a transformation chain +export let transforms = /\)\s*,?\s*/ + +// Whitespace +export let whitespace = /\s/g + +// Test hex value +export let isHex = /^#[a-f0-9]{3,6}$/i + +// Test rgb value +export let isRgb = /^rgb\(/ + +// Test css declaration +export let isCss = /[^:]+:[^;]+;?/ + +// Test for blank string +export let isBlank = /^(\s+)?$/ + +// Test for numeric string +export let isNumber = /^[+-]?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i + +// Test for percent value +export let isPercent = /^-?[\d.]+%$/ + +// Test for image url +export let isImage = /\.(jpg|jpeg|png|gif|svg)(\?[^=]+.*)?/i + +// split at whitespace and comma +export let delimiter = /[\s,]+/ + +// The following regex are used to parse the d attribute of a path + +// Matches all hyphens which are not after an exponent +export let hyphen = /([^e])-/gi + +// Replaces and tests for all path letters +export let pathLetters = /[MLHVCSQTAZ]/gi + +// yes we need this one, too +export let isPathLetter = /[MLHVCSQTAZ]/i + +// matches 0.154.23.45 +export let numbersWithDots = /((\d?\.\d+(?:e[+-]?\d+)?)((?:\.\d+(?:e[+-]?\d+)?)+))+/gi + +// matches . +export let dots = /\./g diff --git a/src/modules/core/selector.js b/src/modules/core/selector.js new file mode 100644 index 0000000..1e0b55e --- /dev/null +++ b/src/modules/core/selector.js @@ -0,0 +1,16 @@ +import { adopt } from '../../utils/adopter.js' +import { map } from '../../utils/utils.js' +import { registerMethods } from '../../utils/methods.js' + +export default function baseFind (query, parent) { + return map((parent || document).querySelectorAll(query), function (node) { + return adopt(node) + }) +} + +// Scoped find method +export function find (query) { + return baseFind(query, this.node) +} + +registerMethods('Dom', { find }) diff --git a/src/modules/core/textable.js b/src/modules/core/textable.js new file mode 100644 index 0000000..c9a90db --- /dev/null +++ b/src/modules/core/textable.js @@ -0,0 +1,18 @@ +// Create plain text node +export function plain (text) { + // clear if build mode is disabled + if (this._build === false) { + this.clear() + } + + // create text node + this.node.appendChild(document.createTextNode(text)) + + return this +} + +// FIXME: Does this also work for textpath? +// Get length of text element +export function length () { + return this.node.getComputedTextLength() +} diff --git a/src/modules/optional/arrange.js b/src/modules/optional/arrange.js new file mode 100644 index 0000000..ca0e074 --- /dev/null +++ b/src/modules/optional/arrange.js @@ -0,0 +1,98 @@ +import { registerMethods } from '../../utils/methods.js' + +// Get all siblings, including myself +export function siblings () { + return this.parent().children() +} + +// Get the curent position siblings +export function position () { + return this.parent().index(this) +} + +// Get the next element (will return null if there is none) +export function next () { + return this.siblings()[this.position() + 1] +} + +// Get the next element (will return null if there is none) +export function prev () { + return this.siblings()[this.position() - 1] +} + +// Send given element one step forward +export function forward () { + var i = this.position() + 1 + var p = this.parent() + + // move node one step forward + p.removeElement(this).add(this, i) + + // make sure defs node is always at the top + if (typeof p.isRoot === 'function' && p.isRoot()) { + p.node.appendChild(p.defs().node) + } + + return this +} + +// Send given element one step backward +export function backward () { + var i = this.position() + + if (i > 0) { + this.parent().removeElement(this).add(this, i - 1) + } + + return this +} + +// Send given element all the way to the front +export function front () { + var p = this.parent() + + // Move node forward + p.node.appendChild(this.node) + + // Make sure defs node is always at the top + if (typeof p.isRoot === 'function' && p.isRoot()) { + p.node.appendChild(p.defs().node) + } + + return this +} + +// Send given element all the way to the back +export function back () { + if (this.position() > 0) { + this.parent().removeElement(this).add(this, 0) + } + + return this +} + +// Inserts a given element before the targeted element +export function before (element) { + element.remove() + + var i = this.position() + + this.parent().add(element, i) + + return this +} + +// Inserts a given element after the targeted element +export function after (element) { + element.remove() + + var i = this.position() + + this.parent().add(element, i + 1) + + return this +} + +registerMethods('Dom', { + siblings, position, next, prev, forward, backward, front, back, before, after +}) diff --git a/src/modules/optional/class.js b/src/modules/optional/class.js new file mode 100644 index 0000000..1d28fd5 --- /dev/null +++ b/src/modules/optional/class.js @@ -0,0 +1,44 @@ +import { delimiter } from '../core/regex.js' +import { registerMethods } from '../../utils/methods.js' + +// Return array of classes on the node +function classes () { + var 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)) { + var 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 +}) diff --git a/src/modules/optional/css.js b/src/modules/optional/css.js new file mode 100644 index 0000000..924b13d --- /dev/null +++ b/src/modules/optional/css.js @@ -0,0 +1,71 @@ +// FIXME: We dont need exports +import { camelCase } from '../../utils/utils.js' +import { isBlank } from '../core/regex.js' +import { registerMethods } from '../../utils/methods.js' + +// Dynamic style generator +export function css (style, val) { + let 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) { + let t = el.split(/\s*:\s*/) + ret[t[0]] = t[1] + }) + return ret + } + + if (arguments.length < 2) { + // get style properties in the array + if (Array.isArray(style)) { + for (let name of style) { + let cased = camelCase(name) + ret[cased] = 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 (let 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 +export function show () { + return this.css('display', '') +} + +// Hide element +export function hide () { + return this.css('display', 'none') +} + +// Is element visible? +export function visible () { + return this.css('display') !== 'none' +} + +registerMethods('Dom', { + css, show, hide, visible +}) diff --git a/src/modules/optional/data.js b/src/modules/optional/data.js new file mode 100644 index 0000000..341d129 --- /dev/null +++ b/src/modules/optional/data.js @@ -0,0 +1,26 @@ +import { registerMethods } from '../../utils/methods.js' + +// Store data values on svg nodes +export function data (a, v, r) { + 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 }) diff --git a/src/modules/optional/memory.js b/src/modules/optional/memory.js new file mode 100644 index 0000000..d1bf7cf --- /dev/null +++ b/src/modules/optional/memory.js @@ -0,0 +1,39 @@ +import { registerMethods } from '../../utils/methods.js' +// FIXME: We need a constructor to set this up + +// Remember arbitrary data +export function remember (k, v) { + // remember every item in an object individually + if (typeof arguments[0] === 'object') { + for (var 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 +export function forget () { + if (arguments.length === 0) { + this._memory = {} + } else { + for (var i = arguments.length - 1; i >= 0; i--) { + delete this.memory()[arguments[i]] + } + } + return this +} + +// return local memory object +export function memory () { + return (this._memory = this._memory || {}) +} + +registerMethods('Dom', { remember, forget, memory }) diff --git a/src/modules/optional/sugar.js b/src/modules/optional/sugar.js new file mode 100644 index 0000000..904e353 --- /dev/null +++ b/src/modules/optional/sugar.js @@ -0,0 +1,159 @@ +import { registerMethods } from '../../utils/methods.js' +import Color from '../../types/Color.js' +import Element from '../../elements/Element.js' +import Matrix from '../../types/Matrix.js' +import Point from '../../types/Point.js' +import Runner from '../../animation/Runner.js' +import SVGNumber from '../../types/SVGNumber.js' + +// Define list of available attributes for stroke and fill +var 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) { + var extension = {} + var i + + extension[m] = function (o) { + if (typeof o === 'undefined') { + return this + } + if (typeof o === 'string' || 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(['Shape', '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, around) { + var directionString = typeof direction === 'string' ? direction + : isFinite(direction) ? 'both' + : 'both' + var origin = (direction === 'both' && isFinite(around)) ? [around, around] + : (direction === 'x') ? [around, 0] + : (direction === 'y') ? [0, around] + : isFinite(direction) ? [direction, direction] + : [0, 0] + this.transform({ flip: directionString, origin: origin }, true) + }, + + // Opacity + opacity: function (value) { + return this.attr('opacity', value) + }, + + // Relative move over x axis + dx: function (x) { + return this.x(new SVGNumber(x).plus(this instanceof Runner ? 0 : this.x()), true) + }, + + // Relative move over y axis + dy: function (y) { + return this.y(new SVGNumber(y).plus(this instanceof Runner ? 0 : this.y()), true) + }, + + // Relative move over x and y axes + dmove: function (x, y) { + return this.dx(x).dy(y) + } +}) + +registerMethods('radius', { + // Add x and y radius + radius: function (x, y) { + var type = (this._element || this).type + return type === 'radialGradient' || type === 'radialGradient' + ? this.attr('r', new SVGNumber(x)) + : this.rx(x).ry(y == null ? x : 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 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) + } +}) diff --git a/src/modules/optional/transform.js b/src/modules/optional/transform.js new file mode 100644 index 0000000..7535fdc --- /dev/null +++ b/src/modules/optional/transform.js @@ -0,0 +1,72 @@ +import { getOrigin } from '../../utils/utils.js' +import { delimiter, transforms } from '../core/regex.js' +import { registerMethods } from '../../utils/methods.js' +import Matrix from '../../types/Matrix.js' + +// Reset all transformations +export function untransform () { + return this.attr('transform', null) +} + +// merge the whole transformation chain into one matrix and returns it +export function matrixify () { + var matrix = (this.attr('transform') || '') + // split transformations + .split(transforms).slice(0, -1).map(function (str) { + // generate key => value pairs + var 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 +export function toParent (parent) { + if (this === parent) return this + var ctm = this.screenCTM() + var pCtm = parent.screenCTM().inverse() + + this.addTo(parent).untransform().transform(pCtm.multiply(ctm)) + + return this +} + +// same as above with parent equals root-svg +export function toDoc () { + return this.toParent(this.doc()) +} + +// Add transformations +export function transform (o, relative) { + // Act as a getter if no object was passed + if (o == null || typeof o === 'string') { + var decomposed = new Matrix(this).decompose() + return decomposed[o] || decomposed + } + + 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 + var cleanRelative = relative === true ? this : (relative || false) + var result = new Matrix(cleanRelative).transform(o) + return this.attr('transform', result) +} + +registerMethods('Element', { + untransform, matrixify, toParent, toDoc, transform +}) |