function transformSVGPath(pathStr) { const DIGIT_0 = 48, DIGIT_9 = 57, COMMA = 44, SPACE = 32, PERIOD = 46, MINUS = 45; const DEGS_TO_RADS = Math.PI/180.0; var path = new THREE.Shape(); var idx = 1, len = pathStr.length, activeCmd, x = 0, y = 0, nx = 0, ny = 0, firstX = null, firstY = null, x1 = 0, x2 = 0, y1 = 0, y2 = 0, rx = 0, ry = 0, xar = 0, laf = 0, sf = 0, cx, cy; function eatNum() { var sidx, c, isFloat = false, s; // eat delims while (idx < len) { c = pathStr.charCodeAt(idx); if (c !== COMMA && c !== SPACE) break; idx++; } if (c === MINUS) sidx = idx++; else sidx = idx; // eat number while (idx < len) { c = pathStr.charCodeAt(idx); if (DIGIT_0 <= c && c <= DIGIT_9) { idx++; continue; } else if (c === PERIOD) { idx++; isFloat = true; continue; } s = pathStr.substring(sidx, idx); return isFloat ? parseFloat(s) : parseInt(s); } s = pathStr.substring(sidx); return isFloat ? parseFloat(s) : parseInt(s); } function nextIsNum() { var c; // do permanently eat any delims... while (idx < len) { c = pathStr.charCodeAt(idx); if (c !== COMMA && c !== SPACE) break; idx++; } c = pathStr.charCodeAt(idx); return (c === MINUS || (DIGIT_0 <= c && c <= DIGIT_9)); } function eatAbsoluteArc() { rx = eatNum(); ry = eatNum(); xar = eatNum() * DEGS_TO_RADS; laf = eatNum(); sf = eatNum(); nx = eatNum(); ny = eatNum(); if( activeCmd == 'a' ) { // relative nx += x; ny += y; } //console.debug( "[SVGPath2ThreeShape.eatAbsoluteArc] Read arc params: rx=" + rx + ", ry=" + ry + ", xar=" + xar + ", laf=" + laf + ", sf=" + sf + ", nx=" + nx + ", ny=" + ny ); if (rx !== ry) { console.warn("Forcing elliptical arc to be a circular one :(", rx, ry); } // SVG implementation notes does all the math for us! woo! // http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes // step1, using x1 as x1' x1 = Math.cos(xar) * (x - nx) / 2 + Math.sin(xar) * (y - ny) / 2; y1 = -Math.sin(xar) * (x - nx) / 2 + Math.cos(xar) * (y - ny) / 2; // step 2, using x2 as cx' //console.debug( "[SVGPath2ThreeShape.eatAbsoluteArc] TMP x1=" + x1 + ", y1=" + y1 + ", (rx*rx * y1*y1 + ry*ry * x1*x1)=" + (rx*rx * y1*y1 + ry*ry * x1*x1) + ", (rx*rx * ry*ry - rx*rx * y1*y1 - ry*ry * x1*x1)=" + (rx*rx * ry*ry - rx*rx * y1*y1 - ry*ry * x1*x1) ); var norm = Math.sqrt( Math.abs( (rx*rx * ry*ry - rx*rx * y1*y1 - ry*ry * x1*x1) / (rx*rx * y1*y1 + ry*ry * x1*x1) ) ); if (laf === sf) norm = -norm; x2 = norm * rx * y1 / ry; y2 = norm * -ry * x1 / rx; //console.debug( "[SVGPath2ThreeShape.eatAbsoluteArc] TMP norm=" + norm + ", x2=" + x2 + ", y2=" + y2 ); // step 3 cx = Math.cos(xar) * x2 - Math.sin(xar) * y2 + (x + nx) / 2; cy = Math.sin(xar) * x2 + Math.cos(xar) * y2 + (y + ny) / 2; //console.debug( "[SVGPath2ThreeShape.eatAbsoluteArc] TMP cx=" + cx + ", cy=" + cy ); var u = new THREE.Vector2(1, 0), v = new THREE.Vector2((x1 - x2) / rx, (y1 - y2) / ry); var startAng = Math.acos(u.dot(v) / u.length() / v.length()); if (u.x * v.y - u.y * v.x < 0) startAng = -startAng; // we can reuse 'v' from start angle as our 'u' for delta angle u.x = (-x1 - x2) / rx; u.y = (-y1 - y2) / ry; var deltaAng = Math.acos(v.dot(u) / v.length() / u.length()); // This normalization ends up making our curves fail to triangulate... if (v.x * u.y - v.y * u.x < 0) deltaAng = -deltaAng; if (!sf && deltaAng > 0) deltaAng -= Math.PI * 2; if (sf && deltaAng < 0) deltaAng += Math.PI * 2; //console.debug( "[SVGPath2ThreeShape.eatAbsoluteArc] Building arc from values: cx=" + cx + ", cy=" + cy + ", startAng=" + startAng + ", deltaAng=" + deltaAng + ", endAng=" + (startAng+deltaAng) + ", sweepFlag=" + sf ); path.absarc(cx, cy, rx, startAng, startAng + deltaAng, sf); x = nx; y = ny; } var canRepeat; activeCmd = pathStr[0]; while (idx <= len) { canRepeat = true; switch (activeCmd) { // moveto commands, become lineto's if repeated case 'M': x = eatNum(); y = eatNum(); path.moveTo(x, y); activeCmd = 'L'; break; case 'm': x += eatNum(); y += eatNum(); path.moveTo(x, y); activeCmd = 'l'; break; case 'Z': case 'z': canRepeat = false; if (x !== firstX || y !== firstY) path.lineTo(firstX, firstY); break; // - lines! case 'L': case 'H': case 'V': nx = (activeCmd === 'V') ? x : eatNum(); ny = (activeCmd === 'H') ? y : eatNum(); path.lineTo(nx, ny); x = nx; y = ny; break; case 'l': case 'h': case 'v': nx = (activeCmd === 'v') ? x : (x + eatNum()); ny = (activeCmd === 'h') ? y : (y + eatNum()); path.lineTo(nx, ny); x = nx; y = ny; break; // - cubic bezier case 'C': x1 = eatNum(); y1 = eatNum(); case 'S': if (activeCmd === 'S') { x1 = 2 * x - x2; y1 = 2 * y - y2; } x2 = eatNum(); y2 = eatNum(); nx = eatNum(); ny = eatNum(); path.bezierCurveTo(x1, y1, x2, y2, nx, ny); x = nx; y = ny; break; case 'c': x1 = x + eatNum(); y1 = y + eatNum(); case 's': if (activeCmd === 's') { x1 = 2 * x - x2; y1 = 2 * y - y2; } x2 = x + eatNum(); y2 = y + eatNum(); nx = x + eatNum(); ny = y + eatNum(); path.bezierCurveTo(x1, y1, x2, y2, nx, ny); x = nx; y = ny; break; // - quadratic bezier case 'Q': x1 = eatNum(); y1 = eatNum(); case 'T': if (activeCmd === 'T') { x1 = 2 * x - x1; y1 = 2 * y - y1; } nx = eatNum(); ny = eatNum(); path.quadraticCurveTo(x1, y1, nx, ny); x = nx; y = ny; break; case 'q': x1 = x + eatNum(); y1 = y + eatNum(); case 't': if (activeCmd === 't') { x1 = 2 * x - x1; y1 = 2 * y - y1; } nx = x + eatNum(); ny = y + eatNum(); path.quadraticCurveTo(x1, y1, nx, ny); x = nx; y = ny; break; // - elliptical arc case 'A': // eatAbsoluteArc(); case 'a': eatAbsoluteArc(); break; default: throw new Error("weird path command: " + activeCmd); } if (firstX === null) { firstX = x; firstY = y; } // just reissue the command if (canRepeat && nextIsNum()) continue; activeCmd = pathStr[idx++]; } return path; }