UNPKG

three-stdlib

Version:

stand-alone library of threejs examples

1 lines 126 kB
{"version":3,"file":"SVGLoader.cjs","sources":["../../src/loaders/SVGLoader.js"],"sourcesContent":["import {\n Box2,\n BufferGeometry,\n FileLoader,\n Float32BufferAttribute,\n Loader,\n Matrix3,\n Path,\n Shape,\n ShapePath,\n ShapeUtils,\n Vector2,\n Vector3,\n} from 'three'\n\nconst COLOR_SPACE_SVG = 'srgb'\n\nconst SVGLoader = /* @__PURE__ */ (() => {\n class SVGLoader extends Loader {\n constructor(manager) {\n super(manager)\n\n // Default dots per inch\n this.defaultDPI = 90\n\n // Accepted units: 'mm', 'cm', 'in', 'pt', 'pc', 'px'\n this.defaultUnit = 'px'\n }\n\n load(url, onLoad, onProgress, onError) {\n const scope = this\n\n const loader = new FileLoader(scope.manager)\n loader.setPath(scope.path)\n loader.setRequestHeader(scope.requestHeader)\n loader.setWithCredentials(scope.withCredentials)\n loader.load(\n url,\n function (text) {\n try {\n onLoad(scope.parse(text))\n } catch (e) {\n if (onError) {\n onError(e)\n } else {\n console.error(e)\n }\n\n scope.manager.itemError(url)\n }\n },\n onProgress,\n onError,\n )\n }\n\n parse(text) {\n const scope = this\n\n function parseNode(node, style) {\n if (node.nodeType !== 1) return\n\n const transform = getNodeTransform(node)\n\n let isDefsNode = false\n\n let path = null\n\n switch (node.nodeName) {\n case 'svg':\n style = parseStyle(node, style)\n break\n\n case 'style':\n parseCSSStylesheet(node)\n break\n\n case 'g':\n style = parseStyle(node, style)\n break\n\n case 'path':\n style = parseStyle(node, style)\n if (node.hasAttribute('d')) path = parsePathNode(node)\n break\n\n case 'rect':\n style = parseStyle(node, style)\n path = parseRectNode(node)\n break\n\n case 'polygon':\n style = parseStyle(node, style)\n path = parsePolygonNode(node)\n break\n\n case 'polyline':\n style = parseStyle(node, style)\n path = parsePolylineNode(node)\n break\n\n case 'circle':\n style = parseStyle(node, style)\n path = parseCircleNode(node)\n break\n\n case 'ellipse':\n style = parseStyle(node, style)\n path = parseEllipseNode(node)\n break\n\n case 'line':\n style = parseStyle(node, style)\n path = parseLineNode(node)\n break\n\n case 'defs':\n isDefsNode = true\n break\n\n case 'use':\n style = parseStyle(node, style)\n\n const href = node.getAttributeNS('http://www.w3.org/1999/xlink', 'href') || ''\n const usedNodeId = href.substring(1)\n const usedNode = node.viewportElement.getElementById(usedNodeId)\n if (usedNode) {\n parseNode(usedNode, style)\n } else {\n console.warn(\"SVGLoader: 'use node' references non-existent node id: \" + usedNodeId)\n }\n\n break\n\n default:\n // console.log( node );\n }\n\n if (path) {\n if (style.fill !== undefined && style.fill !== 'none') {\n path.color.setStyle(style.fill, COLOR_SPACE_SVG)\n }\n\n transformPath(path, currentTransform)\n\n paths.push(path)\n\n path.userData = { node: node, style: style }\n }\n\n const childNodes = node.childNodes\n\n for (let i = 0; i < childNodes.length; i++) {\n const node = childNodes[i]\n\n if (isDefsNode && node.nodeName !== 'style' && node.nodeName !== 'defs') {\n // Ignore everything in defs except CSS style definitions\n // and nested defs, because it is OK by the standard to have\n // <style/> there.\n continue\n }\n\n parseNode(node, style)\n }\n\n if (transform) {\n transformStack.pop()\n\n if (transformStack.length > 0) {\n currentTransform.copy(transformStack[transformStack.length - 1])\n } else {\n currentTransform.identity()\n }\n }\n }\n\n function parsePathNode(node) {\n const path = new ShapePath()\n\n const point = new Vector2()\n const control = new Vector2()\n\n const firstPoint = new Vector2()\n let isFirstPoint = true\n let doSetFirstPoint = false\n\n const d = node.getAttribute('d')\n\n if (d === '' || d === 'none') return null\n\n // console.log( d );\n\n const commands = d.match(/[a-df-z][^a-df-z]*/gi)\n\n for (let i = 0, l = commands.length; i < l; i++) {\n const command = commands[i]\n\n const type = command.charAt(0)\n const data = command.slice(1).trim()\n\n if (isFirstPoint === true) {\n doSetFirstPoint = true\n isFirstPoint = false\n }\n\n let numbers\n\n switch (type) {\n case 'M':\n numbers = parseFloats(data)\n for (let j = 0, jl = numbers.length; j < jl; j += 2) {\n point.x = numbers[j + 0]\n point.y = numbers[j + 1]\n control.x = point.x\n control.y = point.y\n\n if (j === 0) {\n path.moveTo(point.x, point.y)\n } else {\n path.lineTo(point.x, point.y)\n }\n\n if (j === 0) firstPoint.copy(point)\n }\n\n break\n\n case 'H':\n numbers = parseFloats(data)\n\n for (let j = 0, jl = numbers.length; j < jl; j++) {\n point.x = numbers[j]\n control.x = point.x\n control.y = point.y\n path.lineTo(point.x, point.y)\n\n if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point)\n }\n\n break\n\n case 'V':\n numbers = parseFloats(data)\n\n for (let j = 0, jl = numbers.length; j < jl; j++) {\n point.y = numbers[j]\n control.x = point.x\n control.y = point.y\n path.lineTo(point.x, point.y)\n\n if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point)\n }\n\n break\n\n case 'L':\n numbers = parseFloats(data)\n\n for (let j = 0, jl = numbers.length; j < jl; j += 2) {\n point.x = numbers[j + 0]\n point.y = numbers[j + 1]\n control.x = point.x\n control.y = point.y\n path.lineTo(point.x, point.y)\n\n if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point)\n }\n\n break\n\n case 'C':\n numbers = parseFloats(data)\n\n for (let j = 0, jl = numbers.length; j < jl; j += 6) {\n path.bezierCurveTo(\n numbers[j + 0],\n numbers[j + 1],\n numbers[j + 2],\n numbers[j + 3],\n numbers[j + 4],\n numbers[j + 5],\n )\n control.x = numbers[j + 2]\n control.y = numbers[j + 3]\n point.x = numbers[j + 4]\n point.y = numbers[j + 5]\n\n if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point)\n }\n\n break\n\n case 'S':\n numbers = parseFloats(data)\n\n for (let j = 0, jl = numbers.length; j < jl; j += 4) {\n path.bezierCurveTo(\n getReflection(point.x, control.x),\n getReflection(point.y, control.y),\n numbers[j + 0],\n numbers[j + 1],\n numbers[j + 2],\n numbers[j + 3],\n )\n control.x = numbers[j + 0]\n control.y = numbers[j + 1]\n point.x = numbers[j + 2]\n point.y = numbers[j + 3]\n\n if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point)\n }\n\n break\n\n case 'Q':\n numbers = parseFloats(data)\n\n for (let j = 0, jl = numbers.length; j < jl; j += 4) {\n path.quadraticCurveTo(numbers[j + 0], numbers[j + 1], numbers[j + 2], numbers[j + 3])\n control.x = numbers[j + 0]\n control.y = numbers[j + 1]\n point.x = numbers[j + 2]\n point.y = numbers[j + 3]\n\n if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point)\n }\n\n break\n\n case 'T':\n numbers = parseFloats(data)\n\n for (let j = 0, jl = numbers.length; j < jl; j += 2) {\n const rx = getReflection(point.x, control.x)\n const ry = getReflection(point.y, control.y)\n path.quadraticCurveTo(rx, ry, numbers[j + 0], numbers[j + 1])\n control.x = rx\n control.y = ry\n point.x = numbers[j + 0]\n point.y = numbers[j + 1]\n\n if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point)\n }\n\n break\n\n case 'A':\n numbers = parseFloats(data, [3, 4], 7)\n\n for (let j = 0, jl = numbers.length; j < jl; j += 7) {\n // skip command if start point == end point\n if (numbers[j + 5] == point.x && numbers[j + 6] == point.y) continue\n\n const start = point.clone()\n point.x = numbers[j + 5]\n point.y = numbers[j + 6]\n control.x = point.x\n control.y = point.y\n parseArcCommand(\n path,\n numbers[j],\n numbers[j + 1],\n numbers[j + 2],\n numbers[j + 3],\n numbers[j + 4],\n start,\n point,\n )\n\n if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point)\n }\n\n break\n\n case 'm':\n numbers = parseFloats(data)\n\n for (let j = 0, jl = numbers.length; j < jl; j += 2) {\n point.x += numbers[j + 0]\n point.y += numbers[j + 1]\n control.x = point.x\n control.y = point.y\n\n if (j === 0) {\n path.moveTo(point.x, point.y)\n } else {\n path.lineTo(point.x, point.y)\n }\n\n if (j === 0) firstPoint.copy(point)\n }\n\n break\n\n case 'h':\n numbers = parseFloats(data)\n\n for (let j = 0, jl = numbers.length; j < jl; j++) {\n point.x += numbers[j]\n control.x = point.x\n control.y = point.y\n path.lineTo(point.x, point.y)\n\n if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point)\n }\n\n break\n\n case 'v':\n numbers = parseFloats(data)\n\n for (let j = 0, jl = numbers.length; j < jl; j++) {\n point.y += numbers[j]\n control.x = point.x\n control.y = point.y\n path.lineTo(point.x, point.y)\n\n if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point)\n }\n\n break\n\n case 'l':\n numbers = parseFloats(data)\n\n for (let j = 0, jl = numbers.length; j < jl; j += 2) {\n point.x += numbers[j + 0]\n point.y += numbers[j + 1]\n control.x = point.x\n control.y = point.y\n path.lineTo(point.x, point.y)\n\n if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point)\n }\n\n break\n\n case 'c':\n numbers = parseFloats(data)\n\n for (let j = 0, jl = numbers.length; j < jl; j += 6) {\n path.bezierCurveTo(\n point.x + numbers[j + 0],\n point.y + numbers[j + 1],\n point.x + numbers[j + 2],\n point.y + numbers[j + 3],\n point.x + numbers[j + 4],\n point.y + numbers[j + 5],\n )\n control.x = point.x + numbers[j + 2]\n control.y = point.y + numbers[j + 3]\n point.x += numbers[j + 4]\n point.y += numbers[j + 5]\n\n if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point)\n }\n\n break\n\n case 's':\n numbers = parseFloats(data)\n\n for (let j = 0, jl = numbers.length; j < jl; j += 4) {\n path.bezierCurveTo(\n getReflection(point.x, control.x),\n getReflection(point.y, control.y),\n point.x + numbers[j + 0],\n point.y + numbers[j + 1],\n point.x + numbers[j + 2],\n point.y + numbers[j + 3],\n )\n control.x = point.x + numbers[j + 0]\n control.y = point.y + numbers[j + 1]\n point.x += numbers[j + 2]\n point.y += numbers[j + 3]\n\n if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point)\n }\n\n break\n\n case 'q':\n numbers = parseFloats(data)\n\n for (let j = 0, jl = numbers.length; j < jl; j += 4) {\n path.quadraticCurveTo(\n point.x + numbers[j + 0],\n point.y + numbers[j + 1],\n point.x + numbers[j + 2],\n point.y + numbers[j + 3],\n )\n control.x = point.x + numbers[j + 0]\n control.y = point.y + numbers[j + 1]\n point.x += numbers[j + 2]\n point.y += numbers[j + 3]\n\n if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point)\n }\n\n break\n\n case 't':\n numbers = parseFloats(data)\n\n for (let j = 0, jl = numbers.length; j < jl; j += 2) {\n const rx = getReflection(point.x, control.x)\n const ry = getReflection(point.y, control.y)\n path.quadraticCurveTo(rx, ry, point.x + numbers[j + 0], point.y + numbers[j + 1])\n control.x = rx\n control.y = ry\n point.x = point.x + numbers[j + 0]\n point.y = point.y + numbers[j + 1]\n\n if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point)\n }\n\n break\n\n case 'a':\n numbers = parseFloats(data, [3, 4], 7)\n\n for (let j = 0, jl = numbers.length; j < jl; j += 7) {\n // skip command if no displacement\n if (numbers[j + 5] == 0 && numbers[j + 6] == 0) continue\n\n const start = point.clone()\n point.x += numbers[j + 5]\n point.y += numbers[j + 6]\n control.x = point.x\n control.y = point.y\n parseArcCommand(\n path,\n numbers[j],\n numbers[j + 1],\n numbers[j + 2],\n numbers[j + 3],\n numbers[j + 4],\n start,\n point,\n )\n\n if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point)\n }\n\n break\n\n case 'Z':\n case 'z':\n path.currentPath.autoClose = true\n\n if (path.currentPath.curves.length > 0) {\n // Reset point to beginning of Path\n point.copy(firstPoint)\n path.currentPath.currentPoint.copy(point)\n isFirstPoint = true\n }\n\n break\n\n default:\n console.warn(command)\n }\n\n // console.log( type, parseFloats( data ), parseFloats( data ).length )\n\n doSetFirstPoint = false\n }\n\n return path\n }\n\n function parseCSSStylesheet(node) {\n if (!node.sheet || !node.sheet.cssRules || !node.sheet.cssRules.length) return\n\n for (let i = 0; i < node.sheet.cssRules.length; i++) {\n const stylesheet = node.sheet.cssRules[i]\n\n if (stylesheet.type !== 1) continue\n\n const selectorList = stylesheet.selectorText\n .split(/,/gm)\n .filter(Boolean)\n .map((i) => i.trim())\n\n for (let j = 0; j < selectorList.length; j++) {\n // Remove empty rules\n const definitions = Object.fromEntries(Object.entries(stylesheet.style).filter(([, v]) => v !== ''))\n\n stylesheets[selectorList[j]] = Object.assign(stylesheets[selectorList[j]] || {}, definitions)\n }\n }\n }\n\n /**\n * https://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes\n * https://mortoray.com/2017/02/16/rendering-an-svg-elliptical-arc-as-bezier-curves/ Appendix: Endpoint to center arc conversion\n * From\n * rx ry x-axis-rotation large-arc-flag sweep-flag x y\n * To\n * aX, aY, xRadius, yRadius, aStartAngle, aEndAngle, aClockwise, aRotation\n */\n\n function parseArcCommand(path, rx, ry, x_axis_rotation, large_arc_flag, sweep_flag, start, end) {\n if (rx == 0 || ry == 0) {\n // draw a line if either of the radii == 0\n path.lineTo(end.x, end.y)\n return\n }\n\n x_axis_rotation = (x_axis_rotation * Math.PI) / 180\n\n // Ensure radii are positive\n rx = Math.abs(rx)\n ry = Math.abs(ry)\n\n // Compute (x1', y1')\n const dx2 = (start.x - end.x) / 2.0\n const dy2 = (start.y - end.y) / 2.0\n const x1p = Math.cos(x_axis_rotation) * dx2 + Math.sin(x_axis_rotation) * dy2\n const y1p = -Math.sin(x_axis_rotation) * dx2 + Math.cos(x_axis_rotation) * dy2\n\n // Compute (cx', cy')\n let rxs = rx * rx\n let rys = ry * ry\n const x1ps = x1p * x1p\n const y1ps = y1p * y1p\n\n // Ensure radii are large enough\n const cr = x1ps / rxs + y1ps / rys\n\n if (cr > 1) {\n // scale up rx,ry equally so cr == 1\n const s = Math.sqrt(cr)\n rx = s * rx\n ry = s * ry\n rxs = rx * rx\n rys = ry * ry\n }\n\n const dq = rxs * y1ps + rys * x1ps\n const pq = (rxs * rys - dq) / dq\n let q = Math.sqrt(Math.max(0, pq))\n if (large_arc_flag === sweep_flag) q = -q\n const cxp = (q * rx * y1p) / ry\n const cyp = (-q * ry * x1p) / rx\n\n // Step 3: Compute (cx, cy) from (cx', cy')\n const cx = Math.cos(x_axis_rotation) * cxp - Math.sin(x_axis_rotation) * cyp + (start.x + end.x) / 2\n const cy = Math.sin(x_axis_rotation) * cxp + Math.cos(x_axis_rotation) * cyp + (start.y + end.y) / 2\n\n // Step 4: Compute θ1 and Δθ\n const theta = svgAngle(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry)\n const delta = svgAngle((x1p - cxp) / rx, (y1p - cyp) / ry, (-x1p - cxp) / rx, (-y1p - cyp) / ry) % (Math.PI * 2)\n\n path.currentPath.absellipse(cx, cy, rx, ry, theta, theta + delta, sweep_flag === 0, x_axis_rotation)\n }\n\n function svgAngle(ux, uy, vx, vy) {\n const dot = ux * vx + uy * vy\n const len = Math.sqrt(ux * ux + uy * uy) * Math.sqrt(vx * vx + vy * vy)\n let ang = Math.acos(Math.max(-1, Math.min(1, dot / len))) // floating point precision, slightly over values appear\n if (ux * vy - uy * vx < 0) ang = -ang\n return ang\n }\n\n /*\n * According to https://www.w3.org/TR/SVG/shapes.html#RectElementRXAttribute\n * rounded corner should be rendered to elliptical arc, but bezier curve does the job well enough\n */\n function parseRectNode(node) {\n const x = parseFloatWithUnits(node.getAttribute('x') || 0)\n const y = parseFloatWithUnits(node.getAttribute('y') || 0)\n const rx = parseFloatWithUnits(node.getAttribute('rx') || node.getAttribute('ry') || 0)\n const ry = parseFloatWithUnits(node.getAttribute('ry') || node.getAttribute('rx') || 0)\n const w = parseFloatWithUnits(node.getAttribute('width'))\n const h = parseFloatWithUnits(node.getAttribute('height'))\n\n // Ellipse arc to Bezier approximation Coefficient (Inversed). See:\n // https://spencermortensen.com/articles/bezier-circle/\n const bci = 1 - 0.551915024494\n\n const path = new ShapePath()\n\n // top left\n path.moveTo(x + rx, y)\n\n // top right\n path.lineTo(x + w - rx, y)\n if (rx !== 0 || ry !== 0) {\n path.bezierCurveTo(x + w - rx * bci, y, x + w, y + ry * bci, x + w, y + ry)\n }\n\n // bottom right\n path.lineTo(x + w, y + h - ry)\n if (rx !== 0 || ry !== 0) {\n path.bezierCurveTo(x + w, y + h - ry * bci, x + w - rx * bci, y + h, x + w - rx, y + h)\n }\n\n // bottom left\n path.lineTo(x + rx, y + h)\n if (rx !== 0 || ry !== 0) {\n path.bezierCurveTo(x + rx * bci, y + h, x, y + h - ry * bci, x, y + h - ry)\n }\n\n // back to top left\n path.lineTo(x, y + ry)\n if (rx !== 0 || ry !== 0) {\n path.bezierCurveTo(x, y + ry * bci, x + rx * bci, y, x + rx, y)\n }\n\n return path\n }\n\n function parsePolygonNode(node) {\n function iterator(match, a, b) {\n const x = parseFloatWithUnits(a)\n const y = parseFloatWithUnits(b)\n\n if (index === 0) {\n path.moveTo(x, y)\n } else {\n path.lineTo(x, y)\n }\n\n index++\n }\n\n const regex = /([+-]?\\d*\\.?\\d+(?:e[+-]?\\d+)?)(?:,|\\s)([+-]?\\d*\\.?\\d+(?:e[+-]?\\d+)?)/g\n\n const path = new ShapePath()\n\n let index = 0\n\n node.getAttribute('points').replace(regex, iterator)\n\n path.currentPath.autoClose = true\n\n return path\n }\n\n function parsePolylineNode(node) {\n function iterator(match, a, b) {\n const x = parseFloatWithUnits(a)\n const y = parseFloatWithUnits(b)\n\n if (index === 0) {\n path.moveTo(x, y)\n } else {\n path.lineTo(x, y)\n }\n\n index++\n }\n\n const regex = /([+-]?\\d*\\.?\\d+(?:e[+-]?\\d+)?)(?:,|\\s)([+-]?\\d*\\.?\\d+(?:e[+-]?\\d+)?)/g\n\n const path = new ShapePath()\n\n let index = 0\n\n node.getAttribute('points').replace(regex, iterator)\n\n path.currentPath.autoClose = false\n\n return path\n }\n\n function parseCircleNode(node) {\n const x = parseFloatWithUnits(node.getAttribute('cx') || 0)\n const y = parseFloatWithUnits(node.getAttribute('cy') || 0)\n const r = parseFloatWithUnits(node.getAttribute('r') || 0)\n\n const subpath = new Path()\n subpath.absarc(x, y, r, 0, Math.PI * 2)\n\n const path = new ShapePath()\n path.subPaths.push(subpath)\n\n return path\n }\n\n function parseEllipseNode(node) {\n const x = parseFloatWithUnits(node.getAttribute('cx') || 0)\n const y = parseFloatWithUnits(node.getAttribute('cy') || 0)\n const rx = parseFloatWithUnits(node.getAttribute('rx') || 0)\n const ry = parseFloatWithUnits(node.getAttribute('ry') || 0)\n\n const subpath = new Path()\n subpath.absellipse(x, y, rx, ry, 0, Math.PI * 2)\n\n const path = new ShapePath()\n path.subPaths.push(subpath)\n\n return path\n }\n\n function parseLineNode(node) {\n const x1 = parseFloatWithUnits(node.getAttribute('x1') || 0)\n const y1 = parseFloatWithUnits(node.getAttribute('y1') || 0)\n const x2 = parseFloatWithUnits(node.getAttribute('x2') || 0)\n const y2 = parseFloatWithUnits(node.getAttribute('y2') || 0)\n\n const path = new ShapePath()\n path.moveTo(x1, y1)\n path.lineTo(x2, y2)\n path.currentPath.autoClose = false\n\n return path\n }\n\n //\n\n function parseStyle(node, style) {\n style = Object.assign({}, style) // clone style\n\n let stylesheetStyles = {}\n\n if (node.hasAttribute('class')) {\n const classSelectors = node\n .getAttribute('class')\n .split(/\\s/)\n .filter(Boolean)\n .map((i) => i.trim())\n\n for (let i = 0; i < classSelectors.length; i++) {\n stylesheetStyles = Object.assign(stylesheetStyles, stylesheets['.' + classSelectors[i]])\n }\n }\n\n if (node.hasAttribute('id')) {\n stylesheetStyles = Object.assign(stylesheetStyles, stylesheets['#' + node.getAttribute('id')])\n }\n\n function addStyle(svgName, jsName, adjustFunction) {\n if (adjustFunction === undefined)\n adjustFunction = function copy(v) {\n if (v.startsWith('url')) console.warn('SVGLoader: url access in attributes is not implemented.')\n\n return v\n }\n\n if (node.hasAttribute(svgName)) style[jsName] = adjustFunction(node.getAttribute(svgName))\n if (stylesheetStyles[svgName]) style[jsName] = adjustFunction(stylesheetStyles[svgName])\n if (node.style && node.style[svgName] !== '') style[jsName] = adjustFunction(node.style[svgName])\n }\n\n function clamp(v) {\n return Math.max(0, Math.min(1, parseFloatWithUnits(v)))\n }\n\n function positive(v) {\n return Math.max(0, parseFloatWithUnits(v))\n }\n\n addStyle('fill', 'fill')\n addStyle('fill-opacity', 'fillOpacity', clamp)\n addStyle('fill-rule', 'fillRule')\n addStyle('opacity', 'opacity', clamp)\n addStyle('stroke', 'stroke')\n addStyle('stroke-opacity', 'strokeOpacity', clamp)\n addStyle('stroke-width', 'strokeWidth', positive)\n addStyle('stroke-linejoin', 'strokeLineJoin')\n addStyle('stroke-linecap', 'strokeLineCap')\n addStyle('stroke-miterlimit', 'strokeMiterLimit', positive)\n addStyle('visibility', 'visibility')\n\n return style\n }\n\n // http://www.w3.org/TR/SVG11/implnote.html#PathElementImplementationNotes\n\n function getReflection(a, b) {\n return a - (b - a)\n }\n\n // from https://github.com/ppvg/svg-numbers (MIT License)\n\n function parseFloats(input, flags, stride) {\n if (typeof input !== 'string') {\n throw new TypeError('Invalid input: ' + typeof input)\n }\n\n // Character groups\n const RE = {\n SEPARATOR: /[ \\t\\r\\n\\,.\\-+]/,\n WHITESPACE: /[ \\t\\r\\n]/,\n DIGIT: /[\\d]/,\n SIGN: /[-+]/,\n POINT: /\\./,\n COMMA: /,/,\n EXP: /e/i,\n FLAGS: /[01]/,\n }\n\n // States\n const SEP = 0\n const INT = 1\n const FLOAT = 2\n const EXP = 3\n\n let state = SEP\n let seenComma = true\n let number = '',\n exponent = ''\n const result = []\n\n function throwSyntaxError(current, i, partial) {\n const error = new SyntaxError('Unexpected character \"' + current + '\" at index ' + i + '.')\n error.partial = partial\n throw error\n }\n\n function newNumber() {\n if (number !== '') {\n if (exponent === '') result.push(Number(number))\n else result.push(Number(number) * Math.pow(10, Number(exponent)))\n }\n\n number = ''\n exponent = ''\n }\n\n let current\n const length = input.length\n\n for (let i = 0; i < length; i++) {\n current = input[i]\n\n // check for flags\n if (Array.isArray(flags) && flags.includes(result.length % stride) && RE.FLAGS.test(current)) {\n state = INT\n number = current\n newNumber()\n continue\n }\n\n // parse until next number\n if (state === SEP) {\n // eat whitespace\n if (RE.WHITESPACE.test(current)) {\n continue\n }\n\n // start new number\n if (RE.DIGIT.test(current) || RE.SIGN.test(current)) {\n state = INT\n number = current\n continue\n }\n\n if (RE.POINT.test(current)) {\n state = FLOAT\n number = current\n continue\n }\n\n // throw on double commas (e.g. \"1, , 2\")\n if (RE.COMMA.test(current)) {\n if (seenComma) {\n throwSyntaxError(current, i, result)\n }\n\n seenComma = true\n }\n }\n\n // parse integer part\n if (state === INT) {\n if (RE.DIGIT.test(current)) {\n number += current\n continue\n }\n\n if (RE.POINT.test(current)) {\n number += current\n state = FLOAT\n continue\n }\n\n if (RE.EXP.test(current)) {\n state = EXP\n continue\n }\n\n // throw on double signs (\"-+1\"), but not on sign as separator (\"-1-2\")\n if (RE.SIGN.test(current) && number.length === 1 && RE.SIGN.test(number[0])) {\n throwSyntaxError(current, i, result)\n }\n }\n\n // parse decimal part\n if (state === FLOAT) {\n if (RE.DIGIT.test(current)) {\n number += current\n continue\n }\n\n if (RE.EXP.test(current)) {\n state = EXP\n continue\n }\n\n // throw on double decimal points (e.g. \"1..2\")\n if (RE.POINT.test(current) && number[number.length - 1] === '.') {\n throwSyntaxError(current, i, result)\n }\n }\n\n // parse exponent part\n if (state === EXP) {\n if (RE.DIGIT.test(current)) {\n exponent += current\n continue\n }\n\n if (RE.SIGN.test(current)) {\n if (exponent === '') {\n exponent += current\n continue\n }\n\n if (exponent.length === 1 && RE.SIGN.test(exponent)) {\n throwSyntaxError(current, i, result)\n }\n }\n }\n\n // end of number\n if (RE.WHITESPACE.test(current)) {\n newNumber()\n state = SEP\n seenComma = false\n } else if (RE.COMMA.test(current)) {\n newNumber()\n state = SEP\n seenComma = true\n } else if (RE.SIGN.test(current)) {\n newNumber()\n state = INT\n number = current\n } else if (RE.POINT.test(current)) {\n newNumber()\n state = FLOAT\n number = current\n } else {\n throwSyntaxError(current, i, result)\n }\n }\n\n // add the last number found (if any)\n newNumber()\n\n return result\n }\n\n // Units\n\n const units = ['mm', 'cm', 'in', 'pt', 'pc', 'px']\n\n // Conversion: [ fromUnit ][ toUnit ] (-1 means dpi dependent)\n const unitConversion = {\n mm: {\n mm: 1,\n cm: 0.1,\n in: 1 / 25.4,\n pt: 72 / 25.4,\n pc: 6 / 25.4,\n px: -1,\n },\n cm: {\n mm: 10,\n cm: 1,\n in: 1 / 2.54,\n pt: 72 / 2.54,\n pc: 6 / 2.54,\n px: -1,\n },\n in: {\n mm: 25.4,\n cm: 2.54,\n in: 1,\n pt: 72,\n pc: 6,\n px: -1,\n },\n pt: {\n mm: 25.4 / 72,\n cm: 2.54 / 72,\n in: 1 / 72,\n pt: 1,\n pc: 6 / 72,\n px: -1,\n },\n pc: {\n mm: 25.4 / 6,\n cm: 2.54 / 6,\n in: 1 / 6,\n pt: 72 / 6,\n pc: 1,\n px: -1,\n },\n px: {\n px: 1,\n },\n }\n\n function parseFloatWithUnits(string) {\n let theUnit = 'px'\n\n if (typeof string === 'string' || string instanceof String) {\n for (let i = 0, n = units.length; i < n; i++) {\n const u = units[i]\n\n if (string.endsWith(u)) {\n theUnit = u\n string = string.substring(0, string.length - u.length)\n break\n }\n }\n }\n\n let scale = undefined\n\n if (theUnit === 'px' && scope.defaultUnit !== 'px') {\n // Conversion scale from pixels to inches, then to default units\n\n scale = unitConversion['in'][scope.defaultUnit] / scope.defaultDPI\n } else {\n scale = unitConversion[theUnit][scope.defaultUnit]\n\n if (scale < 0) {\n // Conversion scale to pixels\n\n scale = unitConversion[theUnit]['in'] * scope.defaultDPI\n }\n }\n\n return scale * parseFloat(string)\n }\n\n // Transforms\n\n function getNodeTransform(node) {\n if (\n !(\n node.hasAttribute('transform') ||\n (node.nodeName === 'use' && (node.hasAttribute('x') || node.hasAttribute('y')))\n )\n ) {\n return null\n }\n\n const transform = parseNodeTransform(node)\n\n if (transformStack.length > 0) {\n transform.premultiply(transformStack[transformStack.length - 1])\n }\n\n currentTransform.copy(transform)\n transformStack.push(transform)\n\n return transform\n }\n\n function parseNodeTransform(node) {\n const transform = new Matrix3()\n const currentTransform = tempTransform0\n\n if (node.nodeName === 'use' && (node.hasAttribute('x') || node.hasAttribute('y'))) {\n const tx = parseFloatWithUnits(node.getAttribute('x'))\n const ty = parseFloatWithUnits(node.getAttribute('y'))\n\n transform.translate(tx, ty)\n }\n\n if (node.hasAttribute('transform')) {\n const transformsTexts = node.getAttribute('transform').split(')')\n\n for (let tIndex = transformsTexts.length - 1; tIndex >= 0; tIndex--) {\n const transformText = transformsTexts[tIndex].trim()\n\n if (transformText === '') continue\n\n const openParPos = transformText.indexOf('(')\n const closeParPos = transformText.length\n\n if (openParPos > 0 && openParPos < closeParPos) {\n const transformType = transformText.slice(0, openParPos)\n\n const array = parseFloats(transformText.slice(openParPos + 1))\n\n currentTransform.identity()\n\n switch (transformType) {\n case 'translate':\n if (array.length >= 1) {\n const tx = array[0]\n let ty = 0\n\n if (array.length >= 2) {\n ty = array[1]\n }\n\n currentTransform.translate(tx, ty)\n }\n\n break\n\n case 'rotate':\n if (array.length >= 1) {\n let angle = 0\n let cx = 0\n let cy = 0\n\n // Angle\n angle = (array[0] * Math.PI) / 180\n\n if (array.length >= 3) {\n // Center x, y\n cx = array[1]\n cy = array[2]\n }\n\n // Rotate around center (cx, cy)\n tempTransform1.makeTranslation(-cx, -cy)\n tempTransform2.makeRotation(angle)\n tempTransform3.multiplyMatrices(tempTransform2, tempTransform1)\n tempTransform1.makeTranslation(cx, cy)\n currentTransform.multiplyMatrices(tempTransform1, tempTransform3)\n }\n\n break\n\n case 'scale':\n if (array.length >= 1) {\n const scaleX = array[0]\n let scaleY = scaleX\n\n if (array.length >= 2) {\n scaleY = array[1]\n }\n\n currentTransform.scale(scaleX, scaleY)\n }\n\n break\n\n case 'skewX':\n if (array.length === 1) {\n currentTransform.set(1, Math.tan((array[0] * Math.PI) / 180), 0, 0, 1, 0, 0, 0, 1)\n }\n\n break\n\n case 'skewY':\n if (array.length === 1) {\n currentTransform.set(1, 0, 0, Math.tan((array[0] * Math.PI) / 180), 1, 0, 0, 0, 1)\n }\n\n break\n\n case 'matrix':\n if (array.length === 6) {\n currentTransform.set(array[0], array[2], array[4], array[1], array[3], array[5], 0, 0, 1)\n }\n\n break\n }\n }\n\n transform.premultiply(currentTransform)\n }\n }\n\n return transform\n }\n\n function transformPath(path, m) {\n function transfVec2(v2) {\n tempV3.set(v2.x, v2.y, 1).applyMatrix3(m)\n\n v2.set(tempV3.x, tempV3.y)\n }\n\n function transfEllipseGeneric(curve) {\n // For math description see:\n // https://math.stackexchange.com/questions/4544164\n\n const a = curve.xRadius\n const b = curve.yRadius\n\n const cosTheta = Math.cos(curve.aRotation)\n const sinTheta = Math.sin(curve.aRotation)\n\n const v1 = new Vector3(a * cosTheta, a * sinTheta, 0)\n const v2 = new Vector3(-b * sinTheta, b * cosTheta, 0)\n\n const f1 = v1.applyMatrix3(m)\n const f2 = v2.applyMatrix3(m)\n\n const mF = tempTransform0.set(f1.x, f2.x, 0, f1.y, f2.y, 0, 0, 0, 1)\n\n const mFInv = tempTransform1.copy(mF).invert()\n const mFInvT = tempTransform2.copy(mFInv).transpose()\n const mQ = mFInvT.multiply(mFInv)\n const mQe = mQ.elements\n\n const ed = eigenDecomposition(mQe[0], mQe[1], mQe[4])\n const rt1sqrt = Math.sqrt(ed.rt1)\n const rt2sqrt = Math.sqrt(ed.rt2)\n\n curve.xRadius = 1 / rt1sqrt\n curve.yRadius = 1 / rt2sqrt\n curve.aRotation = Math.atan2(ed.sn, ed.cs)\n\n const isFullEllipse = (curve.aEndAngle - curve.aStartAngle) % (2 * Math.PI) < Number.EPSILON\n\n // Do not touch angles of a full ellipse because after transformation they\n // would converge to a sinle value effectively removing the whole curve\n\n if (!isFullEllipse) {\n const mDsqrt = tempTransform1.set(rt1sqrt, 0, 0, 0, rt2sqrt, 0, 0, 0, 1)\n\n const mRT = tempTransform2.set(ed.cs, ed.sn, 0, -ed.sn, ed.cs, 0, 0, 0, 1)\n\n const mDRF = mDsqrt.multiply(mRT).multiply(mF)\n\n const transformAngle = (phi) => {\n const { x: cosR, y: sinR } = new Vector3(Math.cos(phi), Math.sin(phi), 0).applyMatrix3(mDRF)\n\n return Math.atan2(sinR, cosR)\n }\n\n curve.aStartAngle = transformAngle(curve.aStartAngle)\n curve.aEndAngle = transformAngle(curve.aEndAngle)\n\n if (isTransformFlipped(m)) {\n curve.aClockwise = !curve.aClockwise\n }\n }\n }\n\n function transfEllipseNoSkew(curve) {\n // Faster shortcut if no skew is applied\n // (e.g, a euclidean transform of a group containing the ellipse)\n\n const sx = getTransformScaleX(m)\n const sy = getTransformScaleY(m)\n\n curve.xRadius *= sx\n curve.yRadius *= sy\n\n // Extract rotation angle from the matrix of form:\n //\n // | cosθ sx -sinθ sy |\n // | sinθ sx cosθ sy |\n //\n // Remembering that tanθ = sinθ / cosθ; and that\n // `sx`, `sy`, or both might be zero.\n const theta =\n sx > Number.EPSILON ? Math.atan2(m.elements[1], m.elements[0]) : Math.atan2(-m.elements[3], m.elements[4])\n\n curve.aRotation += theta\n\n if (isTransformFlipped(m)) {\n curve.aStartAngle *= -1\n curve.aEndAngle *= -1\n curve.aClockwise = !curve.aClockwise\n }\n }\n\n const subPaths = path.subPaths\n\n for (let i = 0, n = subPaths.length; i < n; i++) {\n const subPath = subPaths[i]\n const curves = subPath.curves\n\n for (let j = 0; j < curves.length; j++) {\n const curve = curves[j]\n\n if (curve.isLineCurve) {\n transfVec2(curve.v1)\n transfVec2(curve.v2)\n } else if (curve.isCubicBezierCurve) {\n transfVec2(curve.v0)\n transfVec2(curve.v1)\n transfVec2(curve.v2)\n transfVec2(curve.v3)\n } else if (curve.isQuadraticBezierCurve) {\n transfVec2(curve.v0)\n transfVec2(curve.v1)\n transfVec2(curve.v2)\n } else if (curve.isEllipseCurve) {\n // Transform ellipse center point\n\n tempV2.set(curve.aX, curve.aY)\n transfVec2(tempV2)\n curve.aX = tempV2.x\n curve.aY = tempV2.y\n\n // Transform ellipse shape parameters\n\n if (isTransformSkewed(m)) {\n transfEllipseGeneric(curve)\n } else {\n transfEllipseNoSkew(curve)\n }\n }\n }\n }\n }\n\n function isTransformFlipped(m) {\n const te = m.elements\n return te[0] * te[4] - te[1] * te[3] < 0\n }\n\n function isTransformSkewed(m) {\n const te = m.elements\n const basisDot = te[0] * te[3] + te[1] * te[4]\n\n // Shortcut for trivial rotations and transformations\n if (basisDot === 0) return false\n\n const sx = getTransformScaleX(m)\n const sy = getTransformScaleY(m)\n\n return Math.abs(basisDot / (sx * sy)) > Number.EPSILON\n }\n\n function getTransformScaleX(m) {\n const te = m.elements\n return Math.sqrt(te[0] * te[0] + te[1] * te[1])\n }\n\n function getTransformScaleY(m) {\n const te = m.elements\n return Math.sqrt(te[3] * te[3] + te[4] * te[4])\n }\n\n // Calculates the eigensystem of a real symmetric 2x2 matrix\n // [ A B ]\n // [ B C ]\n // in the form\n // [ A B ] = [ cs -sn ] [ rt1 0 ] [ cs sn ]\n // [ B C ] [ sn cs ] [ 0 rt2 ] [ -sn cs ]\n // where rt1 >= rt2.\n //\n // Adapted from: https://www.mpi-hd.mpg.de/personalhomes/globes/3x3/index.html\n // -> Algorithms for real symmetric matrices -> Analytical (2x2 symmetric)\n function eigenDecomposition(A, B, C) {\n let rt1, rt2, cs, sn, t\n const sm = A + C\n const df = A - C\n const rt = Math.sqrt(df * df + 4 * B * B)\n\n if (sm > 0) {\n rt1 = 0.5 * (sm + rt)\n t = 1 / rt1\n rt2 = A * t * C - B * t * B\n } else if (sm < 0) {\n rt2 = 0.5 * (sm - rt)\n } else {\n // This case needs to be treated separately to avoid div by 0\n\n rt1 = 0.5 * rt\n rt2 = -0.5 * rt\n }\n\n // Calculate eigenvectors\n\n if (df > 0) {\n cs = df + rt\n } else {\n cs = df - rt\n }\n\n if (Math.abs(cs) > 2 * Math.abs(B)) {\n t = (-2 * B) / cs\n sn = 1 / Math.sqrt(1 + t * t)\n cs = t * sn\n } else if (Math.abs(B) === 0) {\n cs = 1\n sn = 0\n } else {\n t = (-0.5 * cs) / B\n cs = 1 / Math.sqrt(1 + t * t)\n sn = t * cs\n }\n\n if (df > 0) {\n t = cs\n cs = -sn\n sn = t\n }\n\n return { rt1, rt2, cs, sn }\n }\n\n //\n\n const paths = []\n const stylesheets = {}\n\n const transformStack = []\n\n const tempTransform0 = new Matrix3()\n const tempTransform1 = new Matrix3()\n const tempTransform2 = new Matrix3()\n const tempTransform3 = new Matrix3()\n const tempV2 = new Vector2()\n const tempV3 = new Vector3()\n\n const currentTransform = new Matrix3()\n\n const xml = new DOMParser().parseFromString(text, 'image/svg+xml') // application/xml\n\n parseNode(xml.documentElement, {\n fill: '#000',\n fillOpacity: 1,\n strokeOpacity: 1,\n strokeWidth: 1,\n strokeLineJoin: 'miter',\n strokeLineCap: 'butt',\n strokeMiterLimit: 4,\n })\n\n const data = { paths: paths, xml: xml.documentElement }\n\n // console.log( paths );\n return data\n }\n\n static createShapes(shapePath) {\n // Param shapePath: a shapepath as returned by the parse function of this class\n // Returns Shape object\n\n const BIGNUMBER = 999999999\n\n const IntersectionLocationType = {\n ORIGIN: 0,\n DESTINATION: 1,\n BETWEEN: 2,\n LEFT: 3,\n RIGHT: 4,\n BEHIND: 5,\n BEYOND: 6,\n }\n\n const classifyResult = {\n loc: IntersectionLocationType.ORIGIN,\n t: 0,\n }\n\n function findEdgeIntersection(a0, a1, b0, b1) {\n const x1 = a0.x\n const x2 = a1.x\n const x3 = b0.x\n const x4 = b1.x\n const y1 = a0.y\n const y2 = a1.y\n const y3 = b0.y\n const y4 = b1.y\n const nom1 = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)\n const nom2 = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)\n const denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)\n const t1 = nom1 / denom\n const t2 = nom2 / denom\n\n if ((denom === 0 && nom1 !== 0) || t1 <= 0 || t1 >= 1 || t2 < 0 || t2 > 1) {\n //1. lines are parallel or edges don't intersect\n\n return null\n } else if (nom1 === 0 && denom === 0) {\n //2. lines are colinear\n\n //check if endpoints of edge2 (b0-b1) lies on edge1 (a0-a1)\n for (let i = 0; i < 2; i++) {\n classifyPoint(i === 0 ? b0 : b1, a0, a1)\n //find position of this endpoints relatively to edge1\n if (classifyResult.loc == IntersectionLocationType.ORIGIN) {\n const point = i === 0 ? b0 : b1\n return { x: point.x, y: point.y, t: classifyResult.t }\n } else if (classifyResult.loc == IntersectionLocationType.BETWEEN) {\n const x = +(x1 + classifyResult.t * (x2 - x1)).toPrecision(10)\n const y = +(y1 + classifyResult.t * (y2 - y1)).toPrecision(10)\n return { x: x, y: y, t: classifyResult.t }\n }\n }\n\n return null\n } else {\n //3. edges intersect\n\n for (let i = 0; i < 2; i++) {\n classifyPoint(i === 0 ? b0 : b1, a0, a1)\n\n if (classifyResult.loc == IntersectionLocationType.ORIGIN) {\n const point = i === 0 ? b0 : b1\n return { x: point.x, y: point.y, t: classifyResult.t }\n }\n }\n\n const x = +(x1 + t1 * (x2 - x1)).toPrecision(10)\n const y = +(y1 + t1 * (y2 - y1)).toPrecision(10)\n return { x: x, y: y, t: t1 }\n }\n }\n\n function classifyPoint(p, edgeStart, edgeEnd) {\n const ax = edgeEnd.x - edgeStart.x\n const ay = edgeEnd.y - edgeStart.y\n const bx = p.x - edgeStart.x\n const by = p.y - edgeStart.y\n const sa = ax * by - bx * ay\n\n if (p.x === edgeStart.x && p.y === edgeStart.y) {\n classifyResult.loc = IntersectionLocationType.ORIGIN\n classifyResult.t = 0\n return\n }\n\n if (p.x === edgeEnd.x && p.y === edgeEnd.y) {\n classifyResult.loc = IntersectionLocationType.DESTINATION\n classifyResult.t = 1\n return\n }\n\n