Initial commit: igny8 project

This commit is contained in:
igny8
2025-11-09 10:27:02 +00:00
commit 60b8188111
27265 changed files with 4360521 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
import { attrs as defaults } from './defaults.js'
import { isNumber } from './regex.js'
import Color from '../../types/Color.js'
import SVGArray from '../../types/SVGArray.js'
import SVGNumber from '../../types/SVGNumber.js'
const colorAttributes = new Set([
'fill',
'stroke',
'color',
'bgcolor',
'stop-color',
'flood-color',
'lighting-color'
])
const hooks = []
export function registerAttrHook(fn) {
hooks.push(fn)
}
// 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 (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
? defaults[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 (colorAttributes.has(attr) && 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
}

View File

@@ -0,0 +1,43 @@
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 this.attr('cx', x)
}
// Move by center over y-axis
export function cy(y) {
return 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))
}

View File

@@ -0,0 +1,88 @@
import Matrix from '../../types/Matrix.js'
import Point from '../../types/Point.js'
import Box from '../../types/Box.js'
import { proportionalSize } from '../../utils/utils.js'
import { getWindow } from '../../utils/window.js'
export function dmove(dx, dy) {
this.children().forEach((child) => {
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
// Bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1905039
// Because bbox for nested svgs returns the contents bbox in the coordinate space of the svg itself (weird!), we cant use bbox for svgs
// Therefore we have to use getBoundingClientRect. But THAT is broken (as explained in the bug).
// Funnily enough the broken behavior would work for us but that breaks it in chrome
// So we have to replicate the broken behavior of FF by just reading the attributes of the svg itself
bbox =
child.node instanceof getWindow().SVGSVGElement
? new Box(child.attr(['x', 'y', 'width', 'height']))
: 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
}
export function dx(dx) {
return this.dmove(dx, 0)
}
export function dy(dy) {
return this.dmove(0, dy)
}
export function height(height, box = this.bbox()) {
if (height == null) return box.height
return this.size(box.width, height, box)
}
export function move(x = 0, y = 0, box = this.bbox()) {
const dx = x - box.x
const dy = y - box.y
return this.dmove(dx, dy)
}
export 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) => {
const o = new Point(box).transform(new Matrix(child).inverse())
child.scale(scaleX, scaleY, o.x, o.y)
})
return this
}
export function width(width, box = this.bbox()) {
if (width == null) return box.width
return this.size(width, box.height, box)
}
export function x(x, box = this.bbox()) {
if (x == null) return box.x
return this.move(x, box.y, box)
}
export function y(y, box = this.bbox()) {
if (y == null) return box.y
return this.move(box.x, y, box)
}

View File

@@ -0,0 +1,44 @@
export function noop() {}
// Default animation values
export const timeline = {
duration: 400,
ease: '>',
delay: 0
}
// Default attribute values
export 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'
}

View File

@@ -0,0 +1,143 @@
import { delimiter } from './regex.js'
import { makeInstance } from '../../utils/adopter.js'
import { globals } from '../../utils/window.js'
let listenerId = 0
export const windowEvents = {}
export 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
}
export function getEventTarget(instance) {
return instance.getEventTarget()
}
export 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
export 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
export 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)
}
})
}
export 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
}

View File

@@ -0,0 +1,13 @@
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) })
}

View File

@@ -0,0 +1,5 @@
// Default namespaces
export const svg = 'http://www.w3.org/2000/svg'
export const html = 'http://www.w3.org/1999/xhtml'
export const xmlns = 'http://www.w3.org/2000/xmlns/'
export const xlink = 'http://www.w3.org/1999/xlink'

View File

@@ -0,0 +1,30 @@
import { globals } from '../../utils/window.js'
import { makeInstance } from '../../utils/adopter.js'
export default 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
}

View File

@@ -0,0 +1,25 @@
import PointArray from '../../types/PointArray.js'
export const 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) {
const b = this.bbox()
return width == null ? b.width : this.size(width, b.height)
}
// Set height of element
export function height(height) {
const b = this.bbox()
return height == null ? b.height : this.size(b.width, height)
}

View File

@@ -0,0 +1,34 @@
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')))
}
// 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))
}
// 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))
)
}
// Set element size to given width and height
export function size(width, height) {
const p = proportionalSize(this, width, height)
return this.attr('points', this.array().size(p.width, p.height))
}

View File

@@ -0,0 +1,39 @@
// Parse unit value
export const numberAndUnit =
/^([+-]?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?)([a-z%]*)$/i
// Parse hex value
export const hex = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i
// Parse rgb value
export const rgb = /rgb\((\d+),(\d+),(\d+)\)/
// Parse reference id
export const reference = /(#[a-z_][a-z0-9\-_]*)/i
// splits a transformation chain
export const transforms = /\)\s*,?\s*/
// Whitespace
export const whitespace = /\s/g
// Test hex value
export const isHex = /^#[a-f0-9]{3}$|^#[a-f0-9]{6}$/i
// Test rgb value
export const isRgb = /^rgb\(/
// Test for blank string
export const isBlank = /^(\s+)?$/
// Test for numeric string
export const isNumber = /^[+-]?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i
// Test for image url
export const isImage = /\.(jpg|jpeg|png|gif|svg)(\?[^=]+.*)?/i
// split at whitespace and comma
export const delimiter = /[\s,]+/
// Test for path letter
export const isPathLetter = /[MLHVCSQTAZ]/i

View File

@@ -0,0 +1,21 @@
import { adopt } from '../../utils/adopter.js'
import { globals } from '../../utils/window.js'
import { map } from '../../utils/utils.js'
import List from '../../types/List.js'
export default function baseFind(query, parent) {
return new List(
map((parent || globals.document).querySelectorAll(query), function (node) {
return adopt(node)
})
)
}
// Scoped find method
export function find(query) {
return baseFind(query, this.node)
}
export function findOne(query) {
return adopt(this.node.querySelector(query))
}

View File

@@ -0,0 +1,83 @@
import { globals } from '../../utils/window.js'
// 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(globals.document.createTextNode(text))
return this
}
// Get length of text element
export function length() {
return this.node.getComputedTextLength()
}
// Move over x-axis
// Text is moved by its bounding box
// text-anchor does NOT matter
export function x(x, box = this.bbox()) {
if (x == null) {
return box.x
}
return this.attr('x', this.attr('x') + x - box.x)
}
// Move over y-axis
export function y(y, box = this.bbox()) {
if (y == null) {
return box.y
}
return this.attr('y', this.attr('y') + y - box.y)
}
export function move(x, y, box = this.bbox()) {
return this.x(x, box).y(y, box)
}
// Move center over x-axis
export 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
export function cy(y, box = this.bbox()) {
if (y == null) {
return box.cy
}
return this.attr('y', this.attr('y') + y - box.cy)
}
export function center(x, y, box = this.bbox()) {
return this.cx(x, box).cy(y, box)
}
export function ax(x) {
return this.attr('x', x)
}
export function ay(y) {
return this.attr('y', y)
}
export function amove(x, y) {
return this.ax(x).ay(y)
}
// Enable / disable build mode
export function build(build) {
this._build = !!build
return this
}

View File

@@ -0,0 +1,114 @@
import { makeInstance } from '../../utils/adopter.js'
import { registerMethods } from '../../utils/methods.js'
// Get all siblings, including myself
export function siblings() {
return this.parent().children()
}
// Get the current 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() {
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
export 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
export function front() {
const p = this.parent()
// Move node forward
p.add(this.remove())
return this
}
// Send given element all the way to the back
export function back() {
const p = this.parent()
// Move node back
p.add(this.remove(), 0)
return this
}
// Inserts a given element before the targeted element
export 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
export function after(element) {
element = makeInstance(element)
element.remove()
const i = this.position()
this.parent().add(element, i + 1)
return this
}
export function insertBefore(element) {
element = makeInstance(element)
element.before(this)
return this
}
export 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
})

View File

@@ -0,0 +1,53 @@
import { delimiter } from '../core/regex.js'
import { registerMethods } from '../../utils/methods.js'
// Return array of classes on the node
export function classes() {
const attr = this.attr('class')
return attr == null ? [] : attr.trim().split(delimiter)
}
// Return true if class exists on the node, false otherwise
export function hasClass(name) {
return this.classes().indexOf(name) !== -1
}
// Add class to the node
export 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
export 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
export function toggleClass(name) {
return this.hasClass(name) ? this.removeClass(name) : this.addClass(name)
}
registerMethods('Dom', {
classes,
hasClass,
addClass,
removeClass,
toggleClass
})

View File

@@ -0,0 +1,79 @@
import { isBlank } from '../core/regex.js'
import { registerMethods } from '../../utils/methods.js'
// Dynamic style generator
export 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 = name
ret[name] = this.node.style.getPropertyValue(cased)
}
return ret
}
// get style for property
if (typeof style === 'string') {
return this.node.style.getPropertyValue(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.setProperty(
name,
style[name] == null || isBlank.test(style[name]) ? '' : style[name]
)
}
}
}
// set style for property
if (arguments.length === 2) {
this.node.style.setProperty(
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
})

View File

@@ -0,0 +1,47 @@
import { registerMethods } from '../../utils/methods.js'
import { filter, map } from '../../utils/utils.js'
// Store data values on svg nodes
export 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 })

View File

@@ -0,0 +1,40 @@
import { registerMethods } from '../../utils/methods.js'
// Remember arbitrary data
export 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
export 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
export function memory() {
return (this._memory = this._memory || {})
}
registerMethods('Dom', { remember, forget, memory })

View File

@@ -0,0 +1,200 @@
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 SVGNumber from '../../types/SVGNumber.js'
// Define list of available attributes for stroke and fill
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',
'contextmenu',
'wheel',
'pointerdown',
'pointermove',
'pointerup',
'pointerleave',
'pointercancel'
].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)

View File

@@ -0,0 +1,83 @@
import { getOrigin, isDescriptive } 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() {
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
export function toParent(parent, i) {
if (this === parent) return this
if (isDescriptive(this.node)) return this.addTo(parent, i)
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
export function toRoot(i) {
return this.toParent(this.root(), i)
}
// Add transformations
export 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
})