summaryrefslogtreecommitdiffstats
path: root/src/modules
diff options
context:
space:
mode:
Diffstat (limited to 'src/modules')
-rw-r--r--src/modules/core/attr.js80
-rw-r--r--src/modules/core/circled.js64
-rw-r--r--src/modules/core/defaults.js48
-rw-r--r--src/modules/core/event.js119
-rw-r--r--src/modules/core/gradiented.js14
-rw-r--r--src/modules/core/namespaces.js5
-rw-r--r--src/modules/core/parser.js26
-rw-r--r--src/modules/core/pointed.js25
-rw-r--r--src/modules/core/poly.js31
-rw-r--r--src/modules/core/regex.js58
-rw-r--r--src/modules/core/selector.js16
-rw-r--r--src/modules/core/textable.js18
-rw-r--r--src/modules/optional/arrange.js98
-rw-r--r--src/modules/optional/class.js44
-rw-r--r--src/modules/optional/css.js71
-rw-r--r--src/modules/optional/data.js26
-rw-r--r--src/modules/optional/memory.js39
-rw-r--r--src/modules/optional/sugar.js159
-rw-r--r--src/modules/optional/transform.js72
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
+})