UNPKG

svgedit

Version:

Powerful SVG-Editor for your browser

875 lines (793 loc) 44.1 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>JSDoc: Source: svgcanvas/recalculate.js</title> <script src="scripts/prettify/prettify.js"> </script> <script src="scripts/prettify/lang-css.js"> </script> <!--[if lt IE 9]> <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script> <![endif]--> <link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css"> <link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css"> </head> <body> <div id="main"> <h1 class="page-title">Source: svgcanvas/recalculate.js</h1> <section> <article> <pre class="prettyprint source linenums"><code>/** * Recalculate. * @module recalculate * @license MIT */ import { NS } from './namespaces.js'; import { convertToNum } from '../common/units.js'; import { isWebkit } from '../common/browser.js'; import { getRotationAngle, getHref, getBBox, getRefElem, isNullish } from './utilities.js'; import { BatchCommand, ChangeElementCommand } from './history.js'; import { remapElement } from './coords.js'; import { isIdentity, matrixMultiply, transformPoint, transformListToTransform, hasMatrixTransform } from './math.js'; import { mergeDeep } from '../editor/components/jgraduate/Util.js'; let context_; /** * @interface module:recalculate.EditorContext */ /** * @function module:recalculate.EditorContext#getSVGRoot * @returns {SVGSVGElement} The root DOM element */ /** * @function module:recalculate.EditorContext#getStartTransform * @returns {string} */ /** * @function module:recalculate.EditorContext#setStartTransform * @param {string} transform * @returns {void} */ /** * @function module:recalculate.init * @param {module:recalculate.EditorContext} editorContext * @returns {void} */ export const init = function (editorContext) { context_ = editorContext; }; /** * Updates a `&lt;clipPath>`s values based on the given translation of an element. * @function module:recalculate.updateClipPath * @param {string} attr - The clip-path attribute value with the clipPath's ID * @param {Float} tx - The translation's x value * @param {Float} ty - The translation's y value * @returns {void} */ export const updateClipPath = function (attr, tx, ty) { const path = getRefElem(attr).firstChild; const cpXform = path.transform.baseVal; const newxlate = context_.getSVGRoot().createSVGTransform(); newxlate.setTranslate(tx, ty); cpXform.appendItem(newxlate); // Update clipPath's dimensions recalculateDimensions(path); }; /** * Decides the course of action based on the element's transform list. * @function module:recalculate.recalculateDimensions * @param {Element} selected - The DOM element to recalculate * @returns {Command} Undo command object with the resulting change */ export const recalculateDimensions = function (selected) { if (!selected) return null; const svgroot = context_.getSVGRoot(); const dataStorage = context_.getDataStorage(); const tlist = selected.transform?.baseVal; // remove any unnecessary transforms if (tlist &amp;&amp; tlist.numberOfItems > 0) { let k = tlist.numberOfItems; const noi = k; while (k--) { const xform = tlist.getItem(k); if (xform.type === 0) { tlist.removeItem(k); // remove identity matrices } else if (xform.type === 1) { if (isIdentity(xform.matrix)) { if (noi === 1) { // Overcome Chrome bug (though only when noi is 1) with // `removeItem` preventing `removeAttribute` from // subsequently working // See https://bugs.chromium.org/p/chromium/issues/detail?id=843901 selected.removeAttribute('transform'); return null; } tlist.removeItem(k); } // remove zero-degree rotations } else if (xform.type === 4 &amp;&amp; xform.angle === 0) { tlist.removeItem(k); } } // End here if all it has is a rotation if (tlist.numberOfItems === 1 &amp;&amp; getRotationAngle(selected)) { return null; } } // if this element had no transforms, we are done if (!tlist || tlist.numberOfItems === 0) { // Chrome apparently had a bug that requires clearing the attribute first. selected.setAttribute('transform', ''); // However, this still next line currently doesn't work at all in Chrome selected.removeAttribute('transform'); // selected.transform.baseVal.clear(); // Didn't help for Chrome bug return null; } // TODO: Make this work for more than 2 if (tlist) { let mxs = []; let k = tlist.numberOfItems; while (k--) { const xform = tlist.getItem(k); if (xform.type === 1) { mxs.push([ xform.matrix, k ]); } else if (mxs.length) { mxs = []; } } if (mxs.length === 2) { const mNew = svgroot.createSVGTransformFromMatrix(matrixMultiply(mxs[1][0], mxs[0][0])); tlist.removeItem(mxs[0][1]); tlist.removeItem(mxs[1][1]); tlist.insertItemBefore(mNew, mxs[1][1]); } // combine matrix + translate k = tlist.numberOfItems; if (k >= 2 &amp;&amp; tlist.getItem(k - 2).type === 1 &amp;&amp; tlist.getItem(k - 1).type === 2) { const mt = svgroot.createSVGTransform(); const m = matrixMultiply( tlist.getItem(k - 2).matrix, tlist.getItem(k - 1).matrix ); mt.setMatrix(m); tlist.removeItem(k - 2); tlist.removeItem(k - 2); tlist.appendItem(mt); } } // If it still has a single [M] or [R][M], return null too (prevents BatchCommand from being returned). switch (selected.tagName) { // Ignore these elements, as they can absorb the [M] case 'line': case 'polyline': case 'polygon': case 'path': break; default: if ((tlist.numberOfItems === 1 &amp;&amp; tlist.getItem(0).type === 1) || (tlist.numberOfItems === 2 &amp;&amp; tlist.getItem(0).type === 1 &amp;&amp; tlist.getItem(0).type === 4)) { return null; } } // Grouped SVG element const gsvg = (dataStorage.has(selected, 'gsvg')) ? dataStorage.get(selected, 'gsvg') : undefined; // we know we have some transforms, so set up return variable const batchCmd = new BatchCommand('Transform'); // store initial values that will be affected by reducing the transform list let changes = {}; let initial = null; let attrs = []; switch (selected.tagName) { case 'line': attrs = [ 'x1', 'y1', 'x2', 'y2' ]; break; case 'circle': attrs = [ 'cx', 'cy', 'r' ]; break; case 'ellipse': attrs = [ 'cx', 'cy', 'rx', 'ry' ]; break; case 'foreignObject': case 'rect': case 'image': attrs = [ 'width', 'height', 'x', 'y' ]; break; case 'use': case 'text': case 'tspan': attrs = [ 'x', 'y' ]; break; case 'polygon': case 'polyline': { initial = {}; initial.points = selected.getAttribute('points'); const list = selected.points; const len = list.numberOfItems; changes.points = new Array(len); for (let i = 0; i &lt; len; ++i) { const pt = list.getItem(i); changes.points[i] = { x: pt.x, y: pt.y }; } break; } case 'path': initial = {}; initial.d = selected.getAttribute('d'); changes.d = selected.getAttribute('d'); break; } // switch on element type to get initial values if (attrs.length) { Array.prototype.forEach.call(attrs, function (attr) { changes[attr] = selected.getAttribute(attr); }); for (const [ attr, val ] of Object.entries(changes)) { changes[attr] = convertToNum(attr, val); } } else if (gsvg) { // GSVG exception changes = { x: Number(gsvg.getAttribute('x')) || 0, y: Number(gsvg.getAttribute('y')) || 0 }; } // if we haven't created an initial array in polygon/polyline/path, then // make a copy of initial values and include the transform if (isNullish(initial)) { initial = mergeDeep({}, changes); for (const [ attr, val ] of Object.entries(initial)) { initial[attr] = convertToNum(attr, val); } } // save the start transform value too initial.transform = context_.getStartTransform() || ''; let oldcenter; let newcenter; // if it's a regular group, we have special processing to flatten transforms if ((selected.tagName === 'g' &amp;&amp; !gsvg) || selected.tagName === 'a') { const box = getBBox(selected); oldcenter = { x: box.x + box.width / 2, y: box.y + box.height / 2 }; newcenter = transformPoint( box.x + box.width / 2, box.y + box.height / 2, transformListToTransform(tlist).matrix ); // let m = svgroot.createSVGMatrix(); // temporarily strip off the rotate and save the old center const gangle = getRotationAngle(selected); if (gangle) { const a = gangle * Math.PI / 180; const s = Math.abs(a) > (1.0e-10) ? Math.sin(a) / (1 - Math.cos(a)) : 2 / a; for (let i = 0; i &lt; tlist.numberOfItems; ++i) { const xform = tlist.getItem(i); if (xform.type === 4) { // extract old center through mystical arts const rm = xform.matrix; oldcenter.y = (s * rm.e + rm.f) / 2; oldcenter.x = (rm.e - s * rm.f) / 2; tlist.removeItem(i); break; } } } const N = tlist.numberOfItems; let tx = 0; let ty = 0; let operation = 0; let firstM; if (N) { firstM = tlist.getItem(0).matrix; } let oldStartTransform; // first, if it was a scale then the second-last transform will be it if (N >= 3 &amp;&amp; tlist.getItem(N - 2).type === 3 &amp;&amp; tlist.getItem(N - 3).type === 2 &amp;&amp; tlist.getItem(N - 1).type === 2) { operation = 3; // scale // if the children are unrotated, pass the scale down directly // otherwise pass the equivalent matrix() down directly const tm = tlist.getItem(N - 3).matrix; const sm = tlist.getItem(N - 2).matrix; const tmn = tlist.getItem(N - 1).matrix; const children = selected.childNodes; let c = children.length; while (c--) { const child = children.item(c); tx = 0; ty = 0; if (child.nodeType === 1) { const childTlist = child.transform.baseVal; // some children might not have a transform (&lt;metadata>, &lt;defs>, etc) if (!childTlist) { continue; } const m = transformListToTransform(childTlist).matrix; // Convert a matrix to a scale if applicable // if (hasMatrixTransform(childTlist) &amp;&amp; childTlist.numberOfItems == 1) { // if (m.b==0 &amp;&amp; m.c==0 &amp;&amp; m.e==0 &amp;&amp; m.f==0) { // childTlist.removeItem(0); // const translateOrigin = svgroot.createSVGTransform(), // scale = svgroot.createSVGTransform(), // translateBack = svgroot.createSVGTransform(); // translateOrigin.setTranslate(0, 0); // scale.setScale(m.a, m.d); // translateBack.setTranslate(0, 0); // childTlist.appendItem(translateBack); // childTlist.appendItem(scale); // childTlist.appendItem(translateOrigin); // } // } const angle = getRotationAngle(child); oldStartTransform = context_.getStartTransform(); // const childxforms = []; context_.setStartTransform(child.getAttribute('transform')); if (angle || hasMatrixTransform(childTlist)) { const e2t = svgroot.createSVGTransform(); e2t.setMatrix(matrixMultiply(tm, sm, tmn, m)); childTlist.clear(); childTlist.appendItem(e2t); // childxforms.push(e2t); // if not rotated or skewed, push the [T][S][-T] down to the child } else { // update the transform list with translate,scale,translate // slide the [T][S][-T] from the front to the back // [T][S][-T][M] = [M][T2][S2][-T2] // (only bringing [-T] to the right of [M]) // [T][S][-T][M] = [T][S][M][-T2] // [-T2] = [M_inv][-T][M] const t2n = matrixMultiply(m.inverse(), tmn, m); // [T2] is always negative translation of [-T2] const t2 = svgroot.createSVGMatrix(); t2.e = -t2n.e; t2.f = -t2n.f; // [T][S][-T][M] = [M][T2][S2][-T2] // [S2] = [T2_inv][M_inv][T][S][-T][M][-T2_inv] const s2 = matrixMultiply(t2.inverse(), m.inverse(), tm, sm, tmn, m, t2n.inverse()); const translateOrigin = svgroot.createSVGTransform(); const scale = svgroot.createSVGTransform(); const translateBack = svgroot.createSVGTransform(); translateOrigin.setTranslate(t2n.e, t2n.f); scale.setScale(s2.a, s2.d); translateBack.setTranslate(t2.e, t2.f); childTlist.appendItem(translateBack); childTlist.appendItem(scale); childTlist.appendItem(translateOrigin); // childxforms.push(translateBack); // childxforms.push(scale); // childxforms.push(translateOrigin); // logMatrix(translateBack.matrix); // logMatrix(scale.matrix); } // not rotated batchCmd.addSubCommand(recalculateDimensions(child)); // TODO: If any &lt;use> have this group as a parent and are // referencing this child, then we need to impose a reverse // scale on it so that when it won't get double-translated // const uses = selected.getElementsByTagNameNS(NS.SVG, 'use'); // const href = '#' + child.id; // let u = uses.length; // while (u--) { // const useElem = uses.item(u); // if (href == getHref(useElem)) { // const usexlate = svgroot.createSVGTransform(); // usexlate.setTranslate(-tx,-ty); // useElem.transform.baseVal.insertItemBefore(usexlate,0); // batchCmd.addSubCommand( recalculateDimensions(useElem) ); // } // } context_.setStartTransform(oldStartTransform); } // element } // for each child // Remove these transforms from group tlist.removeItem(N - 1); tlist.removeItem(N - 2); tlist.removeItem(N - 3); } else if (N >= 3 &amp;&amp; tlist.getItem(N - 1).type === 1) { operation = 3; // scale const m = transformListToTransform(tlist).matrix; const e2t = svgroot.createSVGTransform(); e2t.setMatrix(m); tlist.clear(); tlist.appendItem(e2t); // next, check if the first transform was a translate // if we had [ T1 ] [ M ] we want to transform this into [ M ] [ T2 ] // therefore [ T2 ] = [ M_inv ] [ T1 ] [ M ] } else if ((N === 1 || (N > 1 &amp;&amp; tlist.getItem(1).type !== 3)) &amp;&amp; tlist.getItem(0).type === 2) { operation = 2; // translate const T_M = transformListToTransform(tlist).matrix; tlist.removeItem(0); const mInv = transformListToTransform(tlist).matrix.inverse(); const M2 = matrixMultiply(mInv, T_M); tx = M2.e; ty = M2.f; if (tx !== 0 || ty !== 0) { // we pass the translates down to the individual children const children = selected.childNodes; let c = children.length; const clipPathsDone = []; while (c--) { const child = children.item(c); if (child.nodeType === 1) { // Check if child has clip-path if (child.getAttribute('clip-path')) { // tx, ty const attr = child.getAttribute('clip-path'); if (!clipPathsDone.includes(attr)) { updateClipPath(attr, tx, ty); clipPathsDone.push(attr); } } oldStartTransform = context_.getStartTransform(); context_.setStartTransform(child.getAttribute('transform')); const childTlist = child.transform?.baseVal; // some children might not have a transform (&lt;metadata>, &lt;defs>, etc) if (childTlist) { const newxlate = svgroot.createSVGTransform(); newxlate.setTranslate(tx, ty); if (childTlist.numberOfItems) { childTlist.insertItemBefore(newxlate, 0); } else { childTlist.appendItem(newxlate); } batchCmd.addSubCommand(recalculateDimensions(child)); // If any &lt;use> have this group as a parent and are // referencing this child, then impose a reverse translate on it // so that when it won't get double-translated const uses = selected.getElementsByTagNameNS(NS.SVG, 'use'); const href = '#' + child.id; let u = uses.length; while (u--) { const useElem = uses.item(u); if (href === getHref(useElem)) { const usexlate = svgroot.createSVGTransform(); usexlate.setTranslate(-tx, -ty); useElem.transform.baseVal.insertItemBefore(usexlate, 0); batchCmd.addSubCommand(recalculateDimensions(useElem)); } } context_.setStartTransform(oldStartTransform); } } } context_.setStartTransform(oldStartTransform); } // else, a matrix imposition from a parent group // keep pushing it down to the children } else if (N === 1 &amp;&amp; tlist.getItem(0).type === 1 &amp;&amp; !gangle) { operation = 1; const m = tlist.getItem(0).matrix; const children = selected.childNodes; let c = children.length; while (c--) { const child = children.item(c); if (child.nodeType === 1) { oldStartTransform = context_.getStartTransform(); context_.setStartTransform(child.getAttribute('transform')); const childTlist = child.transform?.baseVal; if (!childTlist) { continue; } const em = matrixMultiply(m, transformListToTransform(childTlist).matrix); const e2m = svgroot.createSVGTransform(); e2m.setMatrix(em); childTlist.clear(); childTlist.appendItem(e2m, 0); batchCmd.addSubCommand(recalculateDimensions(child)); context_.setStartTransform(oldStartTransform); // Convert stroke // TODO: Find out if this should actually happen somewhere else const sw = child.getAttribute('stroke-width'); if (child.getAttribute('stroke') !== 'none' &amp;&amp; !isNaN(sw)) { const avg = (Math.abs(em.a) + Math.abs(em.d)) / 2; child.setAttribute('stroke-width', sw * avg); } } } tlist.clear(); // else it was just a rotate } else { if (gangle) { const newRot = svgroot.createSVGTransform(); newRot.setRotate(gangle, newcenter.x, newcenter.y); if (tlist.numberOfItems) { tlist.insertItemBefore(newRot, 0); } else { tlist.appendItem(newRot); } } if (tlist.numberOfItems === 0) { selected.removeAttribute('transform'); } return null; } // if it was a translate, put back the rotate at the new center if (operation === 2) { if (gangle) { newcenter = { x: oldcenter.x + firstM.e, y: oldcenter.y + firstM.f }; const newRot = svgroot.createSVGTransform(); newRot.setRotate(gangle, newcenter.x, newcenter.y); if (tlist.numberOfItems) { tlist.insertItemBefore(newRot, 0); } else { tlist.appendItem(newRot); } } // if it was a resize } else if (operation === 3) { const m = transformListToTransform(tlist).matrix; const roldt = svgroot.createSVGTransform(); roldt.setRotate(gangle, oldcenter.x, oldcenter.y); const rold = roldt.matrix; const rnew = svgroot.createSVGTransform(); rnew.setRotate(gangle, newcenter.x, newcenter.y); const rnewInv = rnew.matrix.inverse(); const mInv = m.inverse(); const extrat = matrixMultiply(mInv, rnewInv, rold, m); tx = extrat.e; ty = extrat.f; if (tx !== 0 || ty !== 0) { // now push this transform down to the children // we pass the translates down to the individual children const children = selected.childNodes; let c = children.length; while (c--) { const child = children.item(c); if (child.nodeType === 1) { oldStartTransform = context_.getStartTransform(); context_.setStartTransform(child.getAttribute('transform')); const childTlist = child.transform?.baseVal; const newxlate = svgroot.createSVGTransform(); newxlate.setTranslate(tx, ty); if (childTlist.numberOfItems) { childTlist.insertItemBefore(newxlate, 0); } else { childTlist.appendItem(newxlate); } batchCmd.addSubCommand(recalculateDimensions(child)); context_.setStartTransform(oldStartTransform); } } } if (gangle) { if (tlist.numberOfItems) { tlist.insertItemBefore(rnew, 0); } else { tlist.appendItem(rnew); } } } // else, it's a non-group } else { // TODO: box might be null for some elements (&lt;metadata> etc), need to handle this const box = getBBox(selected); // Paths (and possbly other shapes) will have no BBox while still in &lt;defs>, // but we still may need to recalculate them (see issue 595). // TODO: Figure out how to get BBox from these elements in case they // have a rotation transform if (!box &amp;&amp; selected.tagName !== 'path') return null; let m; // = svgroot.createSVGMatrix(); // temporarily strip off the rotate and save the old center const angle = getRotationAngle(selected); if (angle) { oldcenter = { x: box.x + box.width / 2, y: box.y + box.height / 2 }; newcenter = transformPoint( box.x + box.width / 2, box.y + box.height / 2, transformListToTransform(tlist).matrix ); const a = angle * Math.PI / 180; const s = (Math.abs(a) > (1.0e-10)) ? Math.sin(a) / (1 - Math.cos(a)) // TODO: This blows up if the angle is exactly 0! : 2 / a; for (let i = 0; i &lt; tlist.numberOfItems; ++i) { const xform = tlist.getItem(i); if (xform.type === 4) { // extract old center through mystical arts const rm = xform.matrix; oldcenter.y = (s * rm.e + rm.f) / 2; oldcenter.x = (rm.e - s * rm.f) / 2; tlist.removeItem(i); break; } } } // 2 = translate, 3 = scale, 4 = rotate, 1 = matrix imposition let operation = 0; const N = tlist.numberOfItems; // Check if it has a gradient with userSpaceOnUse, in which case // adjust it by recalculating the matrix transform. // TODO: Make this work in Webkit using transformlist.SVGTransformList if (!isWebkit()) { const fill = selected.getAttribute('fill'); if (fill &amp;&amp; fill.startsWith('url(')) { const paint = getRefElem(fill); if (paint) { let type = 'pattern'; if (paint?.tagName !== type) type = 'gradient'; const attrVal = paint.getAttribute(type + 'Units'); if (attrVal === 'userSpaceOnUse') { // Update the userSpaceOnUse element m = transformListToTransform(tlist).matrix; const gtlist = paint.transform.baseVal; const gmatrix = transformListToTransform(gtlist).matrix; m = matrixMultiply(m, gmatrix); const mStr = 'matrix(' + [ m.a, m.b, m.c, m.d, m.e, m.f ].join(',') + ')'; paint.setAttribute(type + 'Transform', mStr); } } } } // first, if it was a scale of a non-skewed element, then the second-last // transform will be the [S] // if we had [M][T][S][T] we want to extract the matrix equivalent of // [T][S][T] and push it down to the element if (N >= 3 &amp;&amp; tlist.getItem(N - 2).type === 3 &amp;&amp; tlist.getItem(N - 3).type === 2 &amp;&amp; tlist.getItem(N - 1).type === 2) { // Removed this so a &lt;use> with a given [T][S][T] would convert to a matrix. // Is that bad? // &amp;&amp; selected.nodeName != 'use' operation = 3; // scale m = transformListToTransform(tlist, N - 3, N - 1).matrix; tlist.removeItem(N - 1); tlist.removeItem(N - 2); tlist.removeItem(N - 3); // if we had [T][S][-T][M], then this was a skewed element being resized // Thus, we simply combine it all into one matrix } else if (N === 4 &amp;&amp; tlist.getItem(N - 1).type === 1) { operation = 3; // scale m = transformListToTransform(tlist).matrix; const e2t = svgroot.createSVGTransform(); e2t.setMatrix(m); tlist.clear(); tlist.appendItem(e2t); // reset the matrix so that the element is not re-mapped m = svgroot.createSVGMatrix(); // if we had [R][T][S][-T][M], then this was a rotated matrix-element // if we had [T1][M] we want to transform this into [M][T2] // therefore [ T2 ] = [ M_inv ] [ T1 ] [ M ] and we can push [T2] // down to the element } else if ((N === 1 || (N > 1 &amp;&amp; tlist.getItem(1).type !== 3)) &amp;&amp; tlist.getItem(0).type === 2) { operation = 2; // translate const oldxlate = tlist.getItem(0).matrix; const meq = transformListToTransform(tlist, 1).matrix; const meqInv = meq.inverse(); m = matrixMultiply(meqInv, oldxlate, meq); tlist.removeItem(0); // else if this child now has a matrix imposition (from a parent group) // we might be able to simplify } else if (N === 1 &amp;&amp; tlist.getItem(0).type === 1 &amp;&amp; !angle) { // Remap all point-based elements m = transformListToTransform(tlist).matrix; switch (selected.tagName) { case 'line': changes = { x1: selected.getAttribute('x1'), y1: selected.getAttribute('y1'), x2: selected.getAttribute('x2'), y2: selected.getAttribute('y2') }; // Fallthrough case 'polyline': case 'polygon': changes.points = selected.getAttribute('points'); if (changes.points) { const list = selected.points; const len = list.numberOfItems; changes.points = new Array(len); for (let i = 0; i &lt; len; ++i) { const pt = list.getItem(i); changes.points[i] = { x: pt.x, y: pt.y }; } } // Fallthrough case 'path': changes.d = selected.getAttribute('d'); operation = 1; tlist.clear(); break; default: break; } // if it was a rotation, put the rotate back and return without a command // (this function has zero work to do for a rotate()) } else { // operation = 4; // rotation if (angle) { const newRot = svgroot.createSVGTransform(); newRot.setRotate(angle, newcenter.x, newcenter.y); if (tlist.numberOfItems) { tlist.insertItemBefore(newRot, 0); } else { tlist.appendItem(newRot); } } if (tlist.numberOfItems === 0) { selected.removeAttribute('transform'); } return null; } // if it was a translate or resize, we need to remap the element and absorb the xform if (operation === 1 || operation === 2 || operation === 3) { remapElement(selected, changes, m); } // if we are remapping // if it was a translate, put back the rotate at the new center if (operation === 2) { if (angle) { if (!hasMatrixTransform(tlist)) { newcenter = { x: oldcenter.x + m.e, y: oldcenter.y + m.f }; } const newRot = svgroot.createSVGTransform(); newRot.setRotate(angle, newcenter.x, newcenter.y); if (tlist.numberOfItems) { tlist.insertItemBefore(newRot, 0); } else { tlist.appendItem(newRot); } } // We have special processing for tspans: Tspans are not transformable // but they can have x,y coordinates (sigh). Thus, if this was a translate, // on a text element, also translate any tspan children. if (selected.tagName === 'text') { const children = selected.childNodes; let c = children.length; while (c--) { const child = children.item(c); if (child.tagName === 'tspan') { const tspanChanges = { x: Number(child.getAttribute('x')) || 0, y: Number(child.getAttribute('y')) || 0 }; remapElement(child, tspanChanges, m); } } } // [Rold][M][T][S][-T] became [Rold][M] // we want it to be [Rnew][M][Tr] where Tr is the // translation required to re-center it // Therefore, [Tr] = [M_inv][Rnew_inv][Rold][M] } else if (operation === 3 &amp;&amp; angle) { const { matrix } = transformListToTransform(tlist); const roldt = svgroot.createSVGTransform(); roldt.setRotate(angle, oldcenter.x, oldcenter.y); const rold = roldt.matrix; const rnew = svgroot.createSVGTransform(); rnew.setRotate(angle, newcenter.x, newcenter.y); const rnewInv = rnew.matrix.inverse(); const mInv = matrix.inverse(); const extrat = matrixMultiply(mInv, rnewInv, rold, matrix); remapElement(selected, changes, extrat); if (angle) { if (tlist.numberOfItems) { tlist.insertItemBefore(rnew, 0); } else { tlist.appendItem(rnew); } } } } // a non-group // if the transform list has been emptied, remove it if (tlist.numberOfItems === 0) { selected.removeAttribute('transform'); } batchCmd.addSubCommand(new ChangeElementCommand(selected, initial)); return batchCmd; }; </code></pre> </article> </section> </div> <nav> <h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-blur.html">blur</a></li><li><a href="module-browser.html">browser</a></li><li><a href="module-clear.html">clear</a></li><li><a href="module-contextmenu.html">contextmenu</a></li><li><a href="module-coords.html">coords</a></li><li><a href="module-draw.html">draw</a></li><li><a href="module-elem-get-set%2520get%2520and%2520set%2520methods..html">elem-get-set get and set methods.</a></li><li><a href="module-event.html">event</a></li><li><a href="module-history.html">history</a></li><li><a href="module-jGraduate.html">jGraduate</a></li><li><a href="module-jPicker.html">jPicker</a></li><li><a href="module-jQueryAttr.html">jQueryAttr</a></li><li><a href="module-layer.html">layer</a></li><li><a href="module-locale.html">locale</a></li><li><a href="module-math.html">math</a></li><li><a href="module-namespaces.html">namespaces</a></li><li><a href="module-path.html">path</a></li><li><a href="module-recalculate.html">recalculate</a></li><li><a href="module-sanitize.html">sanitize</a></li><li><a href="module-select.html">select</a></li><li><a href="module-selected-elem.html">selected-elem</a></li><li><a href="module-selection.html">selection</a></li><li><a href="module-svg.html">svg</a></li><li><a href="module-svgcanvas.html">svgcanvas</a></li><li><a href="module-SVGEditor.html">SVGEditor</a></li><li><a href="module-text-actions%2520Tools%2520for%2520Text%2520edit%2520functions.html">text-actions Tools for Text edit functions</a></li><li><a href="module-undo.html">undo</a></li><li><a href="module-units.html">units</a></li><li><a href="module-utilities.html">utilities</a></li></ul><h3>Externals</h3><ul><li><a href="external-JamilihArray.html">JamilihArray</a></li><li><a href="external-jQuery.html">jQuery</a></li><li><a href="external-Math.html">Math</a></li><li><a href="external-MouseEvent.html">MouseEvent</a></li><li><a href="external-Window.html">Window</a></li></ul><h3>Namespaces</h3><ul><li><a href="external-jQuery.fn.html">fn</a></li><li><a href="external-jQuery.fn.$.fn.jPicker.defaults.html">defaults</a></li><li><a href="external-jQuery.fn.exports.jPickerMethod.html">exports.jPickerMethod</a></li><li><a href="external-jQuery.fn.jGraduateDefaults.html">jGraduateDefaults</a></li><li><a href="external-jQuery.fn.jGraduateDefaults.images.html">images</a></li><li><a href="external-jQuery.fn.jGraduateDefaults.window.html">window</a></li><li><a href="external-jQuery.jGraduate.html">jGraduate</a></li><li><a href="external-jQuery.jPicker.html">jPicker</a></li><li><a href="external-jQuery.jPicker.ColorMethods.html">ColorMethods</a></li><li><a href="module-path.html#.pathActions">pathActions</a></li><li><a href="module-svgcanvas.SvgCanvas_pathActions.html">pathActions</a></li><li><a href="module-svgcanvas.SvgCanvas_textActions.html">textActions</a></li></ul><h3>Classes</h3><ul><li><a href="BottomPanel.html">BottomPanel</a></li><li><a href="configObj.html">configObj</a></li><li><a href="Dropdown.html">Dropdown</a></li><li><a href="EditorStartup.html">EditorStartup</a></li><li><a href="ElixMenuButton.html">ElixMenuButton</a></li><li><a href="ElixNumberSpinBox.html">ElixNumberSpinBox</a></li><li><a href="ExplorerButton.html">ExplorerButton</a></li><li><a href="external-jQuery.jGraduate.Paint.html">Paint</a></li><li><a href="external-jQuery.jPicker.Color.html">Color</a></li><li><a href="FlyingButton.html">FlyingButton</a></li><li><a href="LayersPanel.html">LayersPanel</a></li><li><a href="LeftPanel.html">LeftPanel</a></li><li><a href="MainMenu.html">MainMenu</a></li><li><a href="module.exports.html">exports</a></li><li><a href="module.exports_module.exports.html">exports</a></li><li><a href="module-draw.Drawing.html">Drawing</a></li><li><a href="module-draw.Layer.html">Layer</a></li><li><a href="module-history.BatchCommand.html">BatchCommand</a></li><li><a href="module-history.ChangeElementCommand.html">ChangeElementCommand</a></li><li><a href="module-history.Command.html">Command</a></li><li><a href="module-history.HistoryRecordingService.html">HistoryRecordingService</a></li><li><a href="module-history.InsertElementCommand.html">InsertElementCommand</a></li><li><a href="module-history.MoveElementCommand.html">MoveElementCommand</a></li><li><a href="module-history.RemoveElementCommand.html">RemoveElementCommand</a></li><li><a href="module-history.UndoManager.html">UndoManager</a></li><li><a href="module-jPicker.module.exports.html">module.exports</a></li><li><a href="module-layer.Layer.html">Layer</a></li><li><a href="module-path.Path.html">Path</a></li><li><a href="module-path.Segment.html">Segment</a></li><li><a href="module-select.Selector.html">Selector</a></li><li><a href="module-select.SelectorManager.html">SelectorManager</a></li><li><a href="module-svgcanvas.SvgCanvas.html">SvgCanvas</a></li><li><a href="module-SVGEditor-Editor.html">Editor</a></li><li><a href="NumberSpinBox.html">NumberSpinBox</a></li><li><a href="PaintBox.html">PaintBox</a></li><li><a href="PlainNumberSpinBox.html">PlainNumberSpinBox</a></li><li><a href="Rulers.html">Rulers</a></li><li><a href="SeCMenuDialog.html">SeCMenuDialog</a></li><li><a href="SeCMenuLayerDialog.html">SeCMenuLayerDialog</a></li><li><a href="SeColorPicker.html">SeColorPicker</a></li><li><a href="SeEditPrefsDialog.html">SeEditPrefsDialog</a></li><li><a href="SeExportDialog.html">SeExportDialog</a></li><li><a href="SeImgPropDialog.html">SeImgPropDialog</a></li><li><a href="SEInput.html">SEInput</a></li><li><a href="SeList.html">SeList</a></li><li><a href="SeMenu.html">SeMenu</a></li><li><a href="SeMenuItem.html">SeMenuItem</a></li><li><a href="SEPalette.html">SEPalette</a></li><li><a href="SePlainAlertDialog.html">SePlainAlertDialog</a></li><li><a href="SePlainBorderButton.html">SePlainBorderButton</a></li><li><a href="SePromptDialog.html">SePromptDialog</a></li><li><a href="SESpinInput.html">SESpinInput</a></li><li><a href="SeStorageDialog.html">SeStorageDialog</a></li><li><a href="SeSvgSourceEditorDialog.html">SeSvgSourceEditorDialog</a></li><li><a href="SeText.html">SeText</a></li><li><a href="ToolButton.html">ToolButton</a></li><li><a href="TopPanel.html">TopPanel</a></li></ul><h3>Interfaces</h3><ul><li><a href="module-coords.EditorContext.html">EditorContext</a></li><li><a href="module-draw.DrawCanvasInit.html">DrawCanvasInit</a></li><li><a href="module-history.HistoryCommand.html">HistoryCommand</a></li><li><a href="module-history.HistoryEventHandler.html">HistoryEventHandler</a></li><li><a href="module-locale.LocaleEditorInit.html">LocaleEditorInit</a></li><li><a href="module-path.EditorContext.html">EditorContext</a></li><li><a href="module-recalculate.EditorContext.html">EditorContext</a></li><li><a href="module-select.SVGFactory.html">SVGFactory</a></li><li><a href="module-svgcanvas.PrivateMethods.html">PrivateMethods</a></li><li><a href="module-SVGEditor.Config.html">Config</a></li><li><a href="module-SVGEditor.Prefs.html">Prefs</a></li><li><a href="module-SVGthis.CustomHandler.html">CustomHandler</a></li><li><a href="module-units.ElementContainer.html">ElementContainer</a></li><li><a href="module-utilities.EditorContext.html">EditorContext</a></li></ul><h3>Events</h3><ul><li><a href="module-history-Command.html#event:event:history">history</a></li><li><a href="module-svgcanvas.SvgCanvas.html#event:event:changed">changed</a></li><li><a href="module-svgcanvas.SvgCanvas.html#event:event:cleared">cleared</a></li><li><a href="module-svgcanvas.SvgCanvas.html#event:event:contextset">contextset</a></li><li><a href="module-svgcanvas.SvgCanvas.html#event:event:exported">exported</a></li><li><a href="module-svgcanvas.SvgCanvas.html#event:event:exportedPDF">exportedPDF</a></li><li><a href="module-svgcanvas.SvgCanvas.html#event:event:ext_addLangData">ext_addLangData</a></li><li><a href="module-svgcanvas.SvgCanvas.html#event:event:ext_callback">ext_callback</a></li><li><a href="module-svgcanvas.SvgCanvas.html#event:event:ext_canvasUpdated">ext_canvasUpdated</a></li><li><a href="module-svgcanvas.SvgCanvas.html#event:event:ext_elementChanged">ext_elementChanged</a></li><li><a href="module-svgcanvas.SvgCanvas.html#event:event:ext_elementTransition">ext_elementTransition</a></li><li><a href="module-svgcanvas.SvgCanvas.html#event:event:ext_IDsUpdated">ext_IDsUpdated</a></li><li><a href="module-svgcanvas.SvgCanvas.html#event:event:ext_langChanged">ext_langChanged</a></li><li><a href="module-svgcanvas.SvgCanvas.html#event:event:ext_langReady">ext_langReady</a></li><li><a href="module-svgcanvas.SvgCanvas.html#event:event:ext_mouseDown">ext_mouseDown</a></li><li><a href="module-svgcanvas.SvgCanvas.html#event:event:ext_mouseMove">ext_mouseMove</a></li><li><a href="module-svgcanvas.SvgCanvas.html#event:event:ext_mouseUp">ext_mouseUp</a></li><li><a href="module-svgcanvas.SvgCanvas.html#event:event:ext_onNewDocument">ext_onNewDocument</a></li><li><a href="module-svgcanvas.SvgCanvas.html#event:event:ext_selectedChanged">ext_selectedChanged</a></li><li><a href="module-svgcanvas.SvgCanvas.html#event:event:ext_toolButtonStateUpdate">ext_toolButtonStateUpdate</a></li><li><a href="module-svgcanvas.SvgCanvas.html#event:event:ext_workareaResized">ext_workareaResized</a></li><li><a href="module-svgcanvas.SvgCanvas.html#event:event:ext_zoomChanged">ext_zoomChanged</a></li><li><a href="module-svgcanvas.SvgCanvas.html#event:event:extension_added">extension_added</a></li><li><a href="module-svgcanvas.SvgCanvas.html#event:event:extensions_added">extensions_added</a></li><li><a href="module-svgcanvas.SvgCanvas.html#event:event:GenericCanvasEvent">GenericCanvasEvent</a></li><li><a href="module-svgcanvas.SvgCanvas.html#event:event:message">message</a></li><li><a href="module-svgcanvas.SvgCanvas.html#event:event:pointsAdded">pointsAdded</a></li><li><a href="module-svgcanvas.SvgCanvas.html#event:event:saved">saved</a></li><li><a href="module-svgcanvas.SvgCanvas.html#event:event:selected">selected</a></li><li><a href="module-svgcanvas.SvgCanvas.html#event:event:setnonce">setnonce</a></li><li><a href="module-svgcanvas.SvgCanvas.html#event:event:transition">transition</a></li><li><a href="module-svgcanvas.SvgCanvas.html#event:event:unsetnonce">unsetnonce</a></li><li><a href="module-svgcanvas.SvgCanvas.html#event:event:updateCanvas">updateCanvas</a></li><li><a href="module-svgcanvas.SvgCanvas.html#event:event:zoomDone">zoomDone</a></li><li><a href="module-svgcanvas.SvgCanvas.html#event:event:zoomed">zoomed</a></li><li><a href="module-SVGEditor.html#event:event:svgEditorReadyEvent">svgEditorReadyEvent</a></li></ul><h3>Tutorials</h3><ul><li><a href="tutorial-CanvasAPI.html">CanvasAPI</a></li><li><a href="tutorial-Editor.html">Editor</a></li><li><a href="tutorial-EditorAPI.html">EditorAPI</a></li><li><a href="tutorial-Events.html">Events</a></li><li><a href="tutorial-FrequentlyAskedQuestions.html">Frequently Asked Questions (FAQ)</a></li></ul><h3>Global</h3><ul><li><a href="global.html#attributeChangedCallback">attributeChangedCallback</a></li><li><a href="global.html#connectedCallback">connectedCallback</a></li><li><a href="global.html#constructor">constructor</a></li><li><a href="global.html#expireCookie">expireCookie</a></li><li><a href="global.html#findPos">findPos</a></li><li><a href="global.html#formatValueFormatthenumericvalueasastring.Thisisusedafterincrementing/decrementingthevaluetoreformatthevalueasastring.">formatValue Format the numeric value as a string. This is used after incrementing/decrementing the value to reformat the value as a string.</a></li><li><a href="global.html#get">get</a></li><li><a href="global.html#getClosest">getClosest</a></li><li><a href="global.html#getParents">getParents</a></li><li><a href="global.html#init">init</a></li><li><a href="global.html#inputsize">inputsize</a></li><li><a href="global.html#isNullish">isNullish</a></li><li><a href="global.html#loadloadConfig">load load Config</a></li><li><a href="global.html#loadFromURLLoadconfig/datafromURLifgiven">loadFromURL Load config/data from URL if given</a></li><li><a href="global.html#name">name</a></li><li><a href="global.html#observedAttributes">observedAttributes</a></li><li><a href="global.html#parseValue">parseValue</a></li><li><a href="global.html#pref">pref</a></li><li><a href="global.html#processResults">processResults</a></li><li><a href="global.html#readySignal">readySignal</a></li><li><a href="global.html#regexEscape">regexEscape</a></li><li><a href="global.html#removeStoragePrefCookie">removeStoragePrefCookie</a></li><li><a href="global.html#replaceStoragePrompt">replaceStoragePrompt</a></li><li><a href="global.html#set">set</a></li><li><a href="global.html#setupCurConfig">setupCurConfig</a></li><li><a href="global.html#setupCurPrefs">setupCurPrefs</a></li><li><a href="global.html#src">src</a></li><li><a href="global.html#stateEffects">stateEffects</a></li><li><a href="global.html#stepDown">stepDown</a></li><li><a href="global.html#stepUp">stepUp</a></li><li><a href="global.html#touchHandler">touchHandler</a></li><li><a href="global.html#updateLib">updateLib</a></li><li><a href="global.html#value">value</a></li></ul> </nav> <br class="clear"> <footer> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.7</a> on Mon Nov 08 2021 09:47:00 GMT+0100 (Central European Standard Time) </footer> <script> prettyPrint(); </script> <script src="scripts/linenumber.js"> </script> </body> </html>