diff --git a/dist/lindsvg.esm.js b/dist/lindsvg.esm.js index 1227431..f543efa 100644 --- a/dist/lindsvg.esm.js +++ b/dist/lindsvg.esm.js @@ -1,5 +1,5 @@ /*! -lindsvg v1.2.0 +lindsvg v1.2.1 https://amphiluke.github.io/l-systems/ (c) 2020 Amphiluke */ @@ -146,9 +146,9 @@ function generate(lsParams) { } let proto = { - translate() { - this.x += this.step * Math.cos(this.alpha); - this.y += this.step * Math.sin(this.alpha); + translate(stepCount = 1) { + this.x += stepCount * this.step * Math.cos(this.alpha); + this.y += stepCount * this.step * Math.sin(this.alpha); this.minX = Math.min(this.minX, this.x); this.maxX = Math.max(this.maxX, this.x); this.minY = Math.min(this.minY, this.y); @@ -157,11 +157,15 @@ let proto = { rotate(factor) { this.alpha += factor * this.theta; }, - pushStack() { - this.stack.push({x: this.x, y: this.y, alpha: this.alpha}); + pushStack(repeatCount = 1) { + for (; repeatCount > 0; repeatCount--) { + this.stack.push({x: this.x, y: this.y, alpha: this.alpha}); + } }, - popStack() { - ({x: this.x, y: this.y, alpha: this.alpha} = this.stack.pop()); + popStack(repeatCount) { + for (; repeatCount > 0; repeatCount--) { + ({x: this.x, y: this.y, alpha: this.alpha} = this.stack.pop()); + } }, getDrawingRect() { let minX = Math.floor(this.minX); @@ -185,11 +189,12 @@ function createTurtle({x, y, step, alpha, theta}) { /** * Remove all letters which don’t affect the drawing process from the codeword + * and split it into “tokens” for the further processing * @param {String} codeword - L-system code - * @return {String} + * @return {Array} */ -function cleanCodeword(codeword) { - return codeword.replace(/[^FB[\]+-]/g, ""); +function tokenizeCodeword(codeword) { + return codeword.match(/([FB[\]+-])\1*/g); } function formatCoordinates(x, y) { @@ -199,21 +204,22 @@ function formatCoordinates(x, y) { /** * Get the value of the d attribute - * @param {String} codeword - Clean code + * @param {Array} tokens - Tokenized codeword * @param {Object} turtle - Turtle object to work with * @return {String} */ -function getPathData(codeword, turtle) { +function getPathData(tokens, turtle) { let prevCommand; // used to avoid unnecessary repeating of the commands L and M - return [...codeword].reduce((accumulator, symbol) => { - switch (symbol) { + return tokens.reduce((accumulator, token) => { + let tokenLength = token.length; + switch (token[0]) { case "F": - turtle.translate(); + turtle.translate(tokenLength); accumulator += (prevCommand === "L" ? " " : "L") + formatCoordinates(turtle.x, turtle.y); prevCommand = "L"; break; case "B": - turtle.translate(); + turtle.translate(tokenLength); if (prevCommand === "M") { // As the spec states, “If a moveto is followed by multiple pairs of coordinates, // the subsequent pairs are treated as implicit lineto commands”. @@ -224,16 +230,16 @@ function getPathData(codeword, turtle) { prevCommand = "M"; break; case "+": - turtle.rotate(1); + turtle.rotate(tokenLength); break; case "-": - turtle.rotate(-1); + turtle.rotate(-tokenLength); break; case "[": - turtle.pushStack(); + turtle.pushStack(tokenLength); break; case "]": - turtle.popStack(); + turtle.popStack(tokenLength); accumulator += `M${formatCoordinates(turtle.x, turtle.y)}`; prevCommand = "M"; break; @@ -250,7 +256,7 @@ function getPathData(codeword, turtle) { function getSVGData(lsParams) { let codeword = generate(lsParams); let turtle = createTurtle({x: 0, y: 0, ...lsParams}); - let pathData = getPathData(cleanCodeword(codeword), turtle); + let pathData = getPathData(tokenizeCodeword(codeword), turtle); return { pathData, ...turtle.getDrawingRect() diff --git a/dist/lindsvg.esm.min.js b/dist/lindsvg.esm.min.js index 9906a7c..76f94ec 100644 --- a/dist/lindsvg.esm.min.js +++ b/dist/lindsvg.esm.min.js @@ -1,6 +1,6 @@ /*! -lindsvg v1.2.0 +lindsvg v1.2.1 https://amphiluke.github.io/l-systems/ (c) 2020 Amphiluke */ -let t={AXIOM:"Axiom may only contain the following characters: A..Z,+,-,[,]",RULE:"Production rules may only contain the following characters: A..Z,+,-,[,]",LETTER:"Allowed alphabet letters are: A..Z",ALPHA:"The “alpha” parameter must be a finite number",THETA:"The “theta” parameter must be a finite number",STEP:"The “step” parameter must be a positive finite number",COUNT:"The number of iterations must be integer and finite",NUMBER:"A valid finite number expected"},e=/^[A-Z]$/;let a=/^[A-Z+\-[\]]*$/;function i(e,i=t.RULE){return a.test(e)||i}function r(a,r,n){let s=Object.create(null);return Object.entries(a).forEach(([a,h])=>{let l=function(a,i=t.LETTER){return e.test(a)||i}(a,r);!0===l&&(l=i(h,n)),!0!==l&&(s[a]=l)}),!Object.keys(s).length||s}function n(e){let a=Object.create(null);return Object.entries(e).forEach(([e,n])=>{let s=!0;switch(e){case"axiom":s=i(n,t.AXIOM);break;case"rules":s=r(n);break;case"alpha":case"theta":s=function(e,a=t.NUMBER){return Number.isFinite(e)||a}(n,t[e.toUpperCase()]);break;case"step":s=function(e,a=t.STEP){return Number.isFinite(e)&&e>0||a}(n);break;case"iterations":s=function(e,a=t.COUNT){return Number.isInteger(e)&&e>0||a}(n)}!0!==s&&(a[e]=s)}),!Object.keys(a).length||a}class s extends Error{constructor(t){let e=JSON.stringify(t,null,2);super(e),Object.defineProperty(this,"lsErrors",{value:JSON.parse(e)})}toJSON(){return JSON.parse(JSON.stringify(this.lsErrors))}}Object.defineProperty(s.prototype,"name",{configurable:!0,enumerable:!1,writable:!0,value:"LSError"});let h={F:"",B:"","+":"+","-":"-","[":"[","]":"]"},l={alpha:0,theta:0,step:10,iterations:3};let c={translate(){this.x+=this.step*Math.cos(this.alpha),this.y+=this.step*Math.sin(this.alpha),this.minX=Math.min(this.minX,this.x),this.maxX=Math.max(this.maxX,this.x),this.minY=Math.min(this.minY,this.y),this.maxY=Math.max(this.maxY,this.y)},rotate(t){this.alpha+=t*this.theta},pushStack(){this.stack.push({x:this.x,y:this.y,alpha:this.alpha})},popStack(){({x:this.x,y:this.y,alpha:this.alpha}=this.stack.pop())},getDrawingRect(){let t=Math.floor(this.minX),e=Math.floor(this.minY),a=Math.ceil(this.maxX),i=Math.ceil(this.maxY);return{minX:t,minY:e,maxX:a,maxY:i,width:a-t,height:i-e}}};function o(t,e){return`${+t.toFixed(4)} ${+e.toFixed(4)}`}function u(t){let e=function(t){let e=n(t);if(!0!==e)throw new s(e);let{axiom:a,iterations:i}={...l,...t},r={...h,...t.rules};for(;i>0;i--)a=[...a].reduce((t,e)=>t+(r[e]||""),"");return a}(t),a=function({x:t,y:e,step:a,alpha:i,theta:r}){let n=Object.create(c);return n.stack=[],n.x=n.minX=n.maxX=t,n.y=n.minY=n.maxY=e,n.step=a,n.alpha=-i,n.theta=r,n}({x:0,y:0,...t});return{pathData:function(t,e){let a;return[...t].reduce((t,i)=>{switch(i){case"F":e.translate(),t+=("L"===a?" ":"L")+o(e.x,e.y),a="L";break;case"B":e.translate(),"M"===a&&(t=t.slice(0,t.lastIndexOf("M"))),t+="M"+o(e.x,e.y),a="M";break;case"+":e.rotate(1);break;case"-":e.rotate(-1);break;case"[":e.pushStack();break;case"]":e.popStack(),t+=`M${o(e.x,e.y)}`,a="M"}return t},"M"+o(e.x,e.y))}(function(t){return t.replace(/[^FB[\]+-]/g,"")}(e),a),...a.getDrawingRect()}}function p(t,e){let{pathData:a,minX:i,minY:r,width:n,height:s}=u(t),h={width:e.width||n,height:e.height||s,padding:e.padding||0,pathAttributes:{fill:e.fill||"none",stroke:e.stroke||"#000",...e.pathAttributes}},{padding:l}=h,c=Object.entries(h.pathAttributes).reduce((t,[e,a])=>`${t} ${e}="${a=a.replace(/"/g,""")}"`,"");return`\n \n`}export{p as getSVGCode,u as getSVGData}; +let t={AXIOM:"Axiom may only contain the following characters: A..Z,+,-,[,]",RULE:"Production rules may only contain the following characters: A..Z,+,-,[,]",LETTER:"Allowed alphabet letters are: A..Z",ALPHA:"The “alpha” parameter must be a finite number",THETA:"The “theta” parameter must be a finite number",STEP:"The “step” parameter must be a positive finite number",COUNT:"The number of iterations must be integer and finite",NUMBER:"A valid finite number expected"},e=/^[A-Z]$/;let a=/^[A-Z+\-[\]]*$/;function i(e,i=t.RULE){return a.test(e)||i}function r(a,r,n){let s=Object.create(null);return Object.entries(a).forEach(([a,h])=>{let l=function(a,i=t.LETTER){return e.test(a)||i}(a,r);!0===l&&(l=i(h,n)),!0!==l&&(s[a]=l)}),!Object.keys(s).length||s}function n(e){let a=Object.create(null);return Object.entries(e).forEach(([e,n])=>{let s=!0;switch(e){case"axiom":s=i(n,t.AXIOM);break;case"rules":s=r(n);break;case"alpha":case"theta":s=function(e,a=t.NUMBER){return Number.isFinite(e)||a}(n,t[e.toUpperCase()]);break;case"step":s=function(e,a=t.STEP){return Number.isFinite(e)&&e>0||a}(n);break;case"iterations":s=function(e,a=t.COUNT){return Number.isInteger(e)&&e>0||a}(n)}!0!==s&&(a[e]=s)}),!Object.keys(a).length||a}class s extends Error{constructor(t){let e=JSON.stringify(t,null,2);super(e),Object.defineProperty(this,"lsErrors",{value:JSON.parse(e)})}toJSON(){return JSON.parse(JSON.stringify(this.lsErrors))}}Object.defineProperty(s.prototype,"name",{configurable:!0,enumerable:!1,writable:!0,value:"LSError"});let h={F:"",B:"","+":"+","-":"-","[":"[","]":"]"},l={alpha:0,theta:0,step:10,iterations:3};let c={translate(t=1){this.x+=t*this.step*Math.cos(this.alpha),this.y+=t*this.step*Math.sin(this.alpha),this.minX=Math.min(this.minX,this.x),this.maxX=Math.max(this.maxX,this.x),this.minY=Math.min(this.minY,this.y),this.maxY=Math.max(this.maxY,this.y)},rotate(t){this.alpha+=t*this.theta},pushStack(t=1){for(;t>0;t--)this.stack.push({x:this.x,y:this.y,alpha:this.alpha})},popStack(t){for(;t>0;t--)({x:this.x,y:this.y,alpha:this.alpha}=this.stack.pop())},getDrawingRect(){let t=Math.floor(this.minX),e=Math.floor(this.minY),a=Math.ceil(this.maxX),i=Math.ceil(this.maxY);return{minX:t,minY:e,maxX:a,maxY:i,width:a-t,height:i-e}}};function o(t,e){return`${+t.toFixed(4)} ${+e.toFixed(4)}`}function u(t){let e=function(t){let e=n(t);if(!0!==e)throw new s(e);let{axiom:a,iterations:i}={...l,...t},r={...h,...t.rules};for(;i>0;i--)a=[...a].reduce((t,e)=>t+(r[e]||""),"");return a}(t),a=function({x:t,y:e,step:a,alpha:i,theta:r}){let n=Object.create(c);return n.stack=[],n.x=n.minX=n.maxX=t,n.y=n.minY=n.maxY=e,n.step=a,n.alpha=-i,n.theta=r,n}({x:0,y:0,...t});return{pathData:function(t,e){let a;return t.reduce((t,i)=>{let r=i.length;switch(i[0]){case"F":e.translate(r),t+=("L"===a?" ":"L")+o(e.x,e.y),a="L";break;case"B":e.translate(r),"M"===a&&(t=t.slice(0,t.lastIndexOf("M"))),t+="M"+o(e.x,e.y),a="M";break;case"+":e.rotate(r);break;case"-":e.rotate(-r);break;case"[":e.pushStack(r);break;case"]":e.popStack(r),t+=`M${o(e.x,e.y)}`,a="M"}return t},"M"+o(e.x,e.y))}(function(t){return t.match(/([FB[\]+-])\1*/g)}(e),a),...a.getDrawingRect()}}function p(t,e){let{pathData:a,minX:i,minY:r,width:n,height:s}=u(t),h={width:e.width||n,height:e.height||s,padding:e.padding||0,pathAttributes:{fill:e.fill||"none",stroke:e.stroke||"#000",...e.pathAttributes}},{padding:l}=h,c=Object.entries(h.pathAttributes).reduce((t,[e,a])=>`${t} ${e}="${a=a.replace(/"/g,""")}"`,"");return`\n \n`}export{p as getSVGCode,u as getSVGData}; diff --git a/dist/lindsvg.js b/dist/lindsvg.js index 95c4aa8..141be53 100644 --- a/dist/lindsvg.js +++ b/dist/lindsvg.js @@ -1,5 +1,5 @@ /*! -lindsvg v1.2.0 +lindsvg v1.2.1 https://amphiluke.github.io/l-systems/ (c) 2020 Amphiluke */ @@ -152,9 +152,9 @@ https://amphiluke.github.io/l-systems/ } let proto = { - translate() { - this.x += this.step * Math.cos(this.alpha); - this.y += this.step * Math.sin(this.alpha); + translate(stepCount = 1) { + this.x += stepCount * this.step * Math.cos(this.alpha); + this.y += stepCount * this.step * Math.sin(this.alpha); this.minX = Math.min(this.minX, this.x); this.maxX = Math.max(this.maxX, this.x); this.minY = Math.min(this.minY, this.y); @@ -163,11 +163,15 @@ https://amphiluke.github.io/l-systems/ rotate(factor) { this.alpha += factor * this.theta; }, - pushStack() { - this.stack.push({x: this.x, y: this.y, alpha: this.alpha}); + pushStack(repeatCount = 1) { + for (; repeatCount > 0; repeatCount--) { + this.stack.push({x: this.x, y: this.y, alpha: this.alpha}); + } }, - popStack() { - ({x: this.x, y: this.y, alpha: this.alpha} = this.stack.pop()); + popStack(repeatCount) { + for (; repeatCount > 0; repeatCount--) { + ({x: this.x, y: this.y, alpha: this.alpha} = this.stack.pop()); + } }, getDrawingRect() { let minX = Math.floor(this.minX); @@ -191,11 +195,12 @@ https://amphiluke.github.io/l-systems/ /** * Remove all letters which don’t affect the drawing process from the codeword + * and split it into “tokens” for the further processing * @param {String} codeword - L-system code - * @return {String} + * @return {Array} */ - function cleanCodeword(codeword) { - return codeword.replace(/[^FB[\]+-]/g, ""); + function tokenizeCodeword(codeword) { + return codeword.match(/([FB[\]+-])\1*/g); } function formatCoordinates(x, y) { @@ -205,21 +210,22 @@ https://amphiluke.github.io/l-systems/ /** * Get the value of the d attribute - * @param {String} codeword - Clean code + * @param {Array} tokens - Tokenized codeword * @param {Object} turtle - Turtle object to work with * @return {String} */ - function getPathData(codeword, turtle) { + function getPathData(tokens, turtle) { let prevCommand; // used to avoid unnecessary repeating of the commands L and M - return [...codeword].reduce((accumulator, symbol) => { - switch (symbol) { + return tokens.reduce((accumulator, token) => { + let tokenLength = token.length; + switch (token[0]) { case "F": - turtle.translate(); + turtle.translate(tokenLength); accumulator += (prevCommand === "L" ? " " : "L") + formatCoordinates(turtle.x, turtle.y); prevCommand = "L"; break; case "B": - turtle.translate(); + turtle.translate(tokenLength); if (prevCommand === "M") { // As the spec states, “If a moveto is followed by multiple pairs of coordinates, // the subsequent pairs are treated as implicit lineto commands”. @@ -230,16 +236,16 @@ https://amphiluke.github.io/l-systems/ prevCommand = "M"; break; case "+": - turtle.rotate(1); + turtle.rotate(tokenLength); break; case "-": - turtle.rotate(-1); + turtle.rotate(-tokenLength); break; case "[": - turtle.pushStack(); + turtle.pushStack(tokenLength); break; case "]": - turtle.popStack(); + turtle.popStack(tokenLength); accumulator += `M${formatCoordinates(turtle.x, turtle.y)}`; prevCommand = "M"; break; @@ -256,7 +262,7 @@ https://amphiluke.github.io/l-systems/ function getSVGData(lsParams) { let codeword = generate(lsParams); let turtle = createTurtle({x: 0, y: 0, ...lsParams}); - let pathData = getPathData(cleanCodeword(codeword), turtle); + let pathData = getPathData(tokenizeCodeword(codeword), turtle); return { pathData, ...turtle.getDrawingRect() diff --git a/dist/lindsvg.min.js b/dist/lindsvg.min.js index 9fe8e8c..86ba2e2 100644 --- a/dist/lindsvg.min.js +++ b/dist/lindsvg.min.js @@ -1,6 +1,6 @@ /*! -lindsvg v1.2.0 +lindsvg v1.2.1 https://amphiluke.github.io/l-systems/ (c) 2020 Amphiluke */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t=t||self).lindsvg={})}(this,(function(t){"use strict";let e={AXIOM:"Axiom may only contain the following characters: A..Z,+,-,[,]",RULE:"Production rules may only contain the following characters: A..Z,+,-,[,]",LETTER:"Allowed alphabet letters are: A..Z",ALPHA:"The “alpha” parameter must be a finite number",THETA:"The “theta” parameter must be a finite number",STEP:"The “step” parameter must be a positive finite number",COUNT:"The number of iterations must be integer and finite",NUMBER:"A valid finite number expected"},a=/^[A-Z]$/;let i=/^[A-Z+\-[\]]*$/;function r(t,a=e.RULE){return i.test(t)||a}function n(t,i,n){let s=Object.create(null);return Object.entries(t).forEach(([t,h])=>{let o=function(t,i=e.LETTER){return a.test(t)||i}(t,i);!0===o&&(o=r(h,n)),!0!==o&&(s[t]=o)}),!Object.keys(s).length||s}function s(t){let a=Object.create(null);return Object.entries(t).forEach(([t,i])=>{let s=!0;switch(t){case"axiom":s=r(i,e.AXIOM);break;case"rules":s=n(i);break;case"alpha":case"theta":s=function(t,a=e.NUMBER){return Number.isFinite(t)||a}(i,e[t.toUpperCase()]);break;case"step":s=function(t,a=e.STEP){return Number.isFinite(t)&&t>0||a}(i);break;case"iterations":s=function(t,a=e.COUNT){return Number.isInteger(t)&&t>0||a}(i)}!0!==s&&(a[t]=s)}),!Object.keys(a).length||a}class h extends Error{constructor(t){let e=JSON.stringify(t,null,2);super(e),Object.defineProperty(this,"lsErrors",{value:JSON.parse(e)})}toJSON(){return JSON.parse(JSON.stringify(this.lsErrors))}}Object.defineProperty(h.prototype,"name",{configurable:!0,enumerable:!1,writable:!0,value:"LSError"});let o={F:"",B:"","+":"+","-":"-","[":"[","]":"]"},c={alpha:0,theta:0,step:10,iterations:3};let l={translate(){this.x+=this.step*Math.cos(this.alpha),this.y+=this.step*Math.sin(this.alpha),this.minX=Math.min(this.minX,this.x),this.maxX=Math.max(this.maxX,this.x),this.minY=Math.min(this.minY,this.y),this.maxY=Math.max(this.maxY,this.y)},rotate(t){this.alpha+=t*this.theta},pushStack(){this.stack.push({x:this.x,y:this.y,alpha:this.alpha})},popStack(){({x:this.x,y:this.y,alpha:this.alpha}=this.stack.pop())},getDrawingRect(){let t=Math.floor(this.minX),e=Math.floor(this.minY),a=Math.ceil(this.maxX),i=Math.ceil(this.maxY);return{minX:t,minY:e,maxX:a,maxY:i,width:a-t,height:i-e}}};function u(t,e){return`${+t.toFixed(4)} ${+e.toFixed(4)}`}function p(t){let e=function(t){let e=s(t);if(!0!==e)throw new h(e);let{axiom:a,iterations:i}={...c,...t},r={...o,...t.rules};for(;i>0;i--)a=[...a].reduce((t,e)=>t+(r[e]||""),"");return a}(t),a=function({x:t,y:e,step:a,alpha:i,theta:r}){let n=Object.create(l);return n.stack=[],n.x=n.minX=n.maxX=t,n.y=n.minY=n.maxY=e,n.step=a,n.alpha=-i,n.theta=r,n}({x:0,y:0,...t});return{pathData:function(t,e){let a;return[...t].reduce((t,i)=>{switch(i){case"F":e.translate(),t+=("L"===a?" ":"L")+u(e.x,e.y),a="L";break;case"B":e.translate(),"M"===a&&(t=t.slice(0,t.lastIndexOf("M"))),t+="M"+u(e.x,e.y),a="M";break;case"+":e.rotate(1);break;case"-":e.rotate(-1);break;case"[":e.pushStack();break;case"]":e.popStack(),t+=`M${u(e.x,e.y)}`,a="M"}return t},"M"+u(e.x,e.y))}(function(t){return t.replace(/[^FB[\]+-]/g,"")}(e),a),...a.getDrawingRect()}}t.getSVGCode=function(t,e){let{pathData:a,minX:i,minY:r,width:n,height:s}=p(t),h={width:e.width||n,height:e.height||s,padding:e.padding||0,pathAttributes:{fill:e.fill||"none",stroke:e.stroke||"#000",...e.pathAttributes}},{padding:o}=h,c=Object.entries(h.pathAttributes).reduce((t,[e,a])=>`${t} ${e}="${a=a.replace(/"/g,""")}"`,"");return`\n \n`},t.getSVGData=p,Object.defineProperty(t,"__esModule",{value:!0})})); +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t=t||self).lindsvg={})}(this,(function(t){"use strict";let e={AXIOM:"Axiom may only contain the following characters: A..Z,+,-,[,]",RULE:"Production rules may only contain the following characters: A..Z,+,-,[,]",LETTER:"Allowed alphabet letters are: A..Z",ALPHA:"The “alpha” parameter must be a finite number",THETA:"The “theta” parameter must be a finite number",STEP:"The “step” parameter must be a positive finite number",COUNT:"The number of iterations must be integer and finite",NUMBER:"A valid finite number expected"},a=/^[A-Z]$/;let i=/^[A-Z+\-[\]]*$/;function r(t,a=e.RULE){return i.test(t)||a}function n(t,i,n){let s=Object.create(null);return Object.entries(t).forEach(([t,h])=>{let o=function(t,i=e.LETTER){return a.test(t)||i}(t,i);!0===o&&(o=r(h,n)),!0!==o&&(s[t]=o)}),!Object.keys(s).length||s}function s(t){let a=Object.create(null);return Object.entries(t).forEach(([t,i])=>{let s=!0;switch(t){case"axiom":s=r(i,e.AXIOM);break;case"rules":s=n(i);break;case"alpha":case"theta":s=function(t,a=e.NUMBER){return Number.isFinite(t)||a}(i,e[t.toUpperCase()]);break;case"step":s=function(t,a=e.STEP){return Number.isFinite(t)&&t>0||a}(i);break;case"iterations":s=function(t,a=e.COUNT){return Number.isInteger(t)&&t>0||a}(i)}!0!==s&&(a[t]=s)}),!Object.keys(a).length||a}class h extends Error{constructor(t){let e=JSON.stringify(t,null,2);super(e),Object.defineProperty(this,"lsErrors",{value:JSON.parse(e)})}toJSON(){return JSON.parse(JSON.stringify(this.lsErrors))}}Object.defineProperty(h.prototype,"name",{configurable:!0,enumerable:!1,writable:!0,value:"LSError"});let o={F:"",B:"","+":"+","-":"-","[":"[","]":"]"},l={alpha:0,theta:0,step:10,iterations:3};let c={translate(t=1){this.x+=t*this.step*Math.cos(this.alpha),this.y+=t*this.step*Math.sin(this.alpha),this.minX=Math.min(this.minX,this.x),this.maxX=Math.max(this.maxX,this.x),this.minY=Math.min(this.minY,this.y),this.maxY=Math.max(this.maxY,this.y)},rotate(t){this.alpha+=t*this.theta},pushStack(t=1){for(;t>0;t--)this.stack.push({x:this.x,y:this.y,alpha:this.alpha})},popStack(t){for(;t>0;t--)({x:this.x,y:this.y,alpha:this.alpha}=this.stack.pop())},getDrawingRect(){let t=Math.floor(this.minX),e=Math.floor(this.minY),a=Math.ceil(this.maxX),i=Math.ceil(this.maxY);return{minX:t,minY:e,maxX:a,maxY:i,width:a-t,height:i-e}}};function u(t,e){return`${+t.toFixed(4)} ${+e.toFixed(4)}`}function p(t){let e=function(t){let e=s(t);if(!0!==e)throw new h(e);let{axiom:a,iterations:i}={...l,...t},r={...o,...t.rules};for(;i>0;i--)a=[...a].reduce((t,e)=>t+(r[e]||""),"");return a}(t),a=function({x:t,y:e,step:a,alpha:i,theta:r}){let n=Object.create(c);return n.stack=[],n.x=n.minX=n.maxX=t,n.y=n.minY=n.maxY=e,n.step=a,n.alpha=-i,n.theta=r,n}({x:0,y:0,...t});return{pathData:function(t,e){let a;return t.reduce((t,i)=>{let r=i.length;switch(i[0]){case"F":e.translate(r),t+=("L"===a?" ":"L")+u(e.x,e.y),a="L";break;case"B":e.translate(r),"M"===a&&(t=t.slice(0,t.lastIndexOf("M"))),t+="M"+u(e.x,e.y),a="M";break;case"+":e.rotate(r);break;case"-":e.rotate(-r);break;case"[":e.pushStack(r);break;case"]":e.popStack(r),t+=`M${u(e.x,e.y)}`,a="M"}return t},"M"+u(e.x,e.y))}(function(t){return t.match(/([FB[\]+-])\1*/g)}(e),a),...a.getDrawingRect()}}t.getSVGCode=function(t,e){let{pathData:a,minX:i,minY:r,width:n,height:s}=p(t),h={width:e.width||n,height:e.height||s,padding:e.padding||0,pathAttributes:{fill:e.fill||"none",stroke:e.stroke||"#000",...e.pathAttributes}},{padding:o}=h,l=Object.entries(h.pathAttributes).reduce((t,[e,a])=>`${t} ${e}="${a=a.replace(/"/g,""")}"`,"");return`\n \n`},t.getSVGData=p,Object.defineProperty(t,"__esModule",{value:!0})})); diff --git a/package-lock.json b/package-lock.json index 165866c..fdde8ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "lindsvg", - "version": "1.2.0", + "version": "1.2.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -31,9 +31,9 @@ "dev": true }, "@types/node": { - "version": "13.1.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.1.6.tgz", - "integrity": "sha512-Jg1F+bmxcpENHP23sVKkNuU3uaxPnsBMW0cLjleiikFKomJQbsn0Cqk2yDvQArqzZN6ABfBkZ0To7pQ8sLdWDg==", + "version": "13.7.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.7.7.tgz", + "integrity": "sha512-Uo4chgKbnPNlxQwoFmYIwctkQVkMMmsAoGGU4JKwLuvBefF0pCq4FybNSnfkfRCpC7ZW7kttcC/TrRtAJsvGtg==", "dev": true }, "acorn": { @@ -815,9 +815,9 @@ } }, "rollup": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-1.29.0.tgz", - "integrity": "sha512-V63Iz0dSdI5qPPN5HmCN6OBRzBFhMqNWcvwgq863JtSCTU6Vdvqq6S2fYle/dSCyoPrBkIP3EIr1RVs3HTRqqg==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-1.32.0.tgz", + "integrity": "sha512-ab2tF5pdDqm2zuI8j02ceyrJSScl9V2C24FgWQ1v1kTFTu1UrG5H0hpP++mDZlEFyZX4k0chtGEHU2i+pAzBgA==", "dev": true, "requires": { "@types/estree": "*", diff --git a/package.json b/package.json index 5f6d5bb..8cfe39d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lindsvg", - "version": "1.2.0", + "version": "1.2.1", "description": "Lindenmayer System [Scalable] Vector Graphics", "main": "dist/lindsvg.js", "module": "dist/lindsvg.esm.js", @@ -29,7 +29,7 @@ "homepage": "https://amphiluke.github.io/l-systems/", "devDependencies": { "eslint": "^6.8.0", - "rollup": "^1.29.0", + "rollup": "^1.32.0", "rollup-plugin-terser": "^5.2.0" }, "engines": { diff --git a/src/svg.mjs b/src/svg.mjs index c067ab3..0042296 100644 --- a/src/svg.mjs +++ b/src/svg.mjs @@ -3,11 +3,12 @@ import {createTurtle} from "./turtle.mjs"; /** * Remove all letters which don’t affect the drawing process from the codeword + * and split it into “tokens” for the further processing * @param {String} codeword - L-system code - * @return {String} + * @return {Array} */ -function cleanCodeword(codeword) { - return codeword.replace(/[^FB[\]+-]/g, ""); +function tokenizeCodeword(codeword) { + return codeword.match(/([FB[\]+-])\1*/g); } function formatCoordinates(x, y) { @@ -17,21 +18,22 @@ function formatCoordinates(x, y) { /** * Get the value of the d attribute - * @param {String} codeword - Clean code + * @param {Array} tokens - Tokenized codeword * @param {Object} turtle - Turtle object to work with * @return {String} */ -function getPathData(codeword, turtle) { +function getPathData(tokens, turtle) { let prevCommand; // used to avoid unnecessary repeating of the commands L and M - return [...codeword].reduce((accumulator, symbol) => { - switch (symbol) { + return tokens.reduce((accumulator, token) => { + let tokenLength = token.length; + switch (token[0]) { case "F": - turtle.translate(); + turtle.translate(tokenLength); accumulator += (prevCommand === "L" ? " " : "L") + formatCoordinates(turtle.x, turtle.y); prevCommand = "L"; break; case "B": - turtle.translate(); + turtle.translate(tokenLength); if (prevCommand === "M") { // As the spec states, “If a moveto is followed by multiple pairs of coordinates, // the subsequent pairs are treated as implicit lineto commands”. @@ -42,16 +44,16 @@ function getPathData(codeword, turtle) { prevCommand = "M"; break; case "+": - turtle.rotate(1); + turtle.rotate(tokenLength); break; case "-": - turtle.rotate(-1); + turtle.rotate(-tokenLength); break; case "[": - turtle.pushStack(); + turtle.pushStack(tokenLength); break; case "]": - turtle.popStack(); + turtle.popStack(tokenLength); accumulator += `M${formatCoordinates(turtle.x, turtle.y)}`; prevCommand = "M"; break; @@ -68,7 +70,7 @@ function getPathData(codeword, turtle) { export function getSVGData(lsParams) { let codeword = generate(lsParams); let turtle = createTurtle({x: 0, y: 0, ...lsParams}); - let pathData = getPathData(cleanCodeword(codeword), turtle); + let pathData = getPathData(tokenizeCodeword(codeword), turtle); return { pathData, ...turtle.getDrawingRect() diff --git a/src/turtle.mjs b/src/turtle.mjs index 918d938..698cd08 100644 --- a/src/turtle.mjs +++ b/src/turtle.mjs @@ -1,7 +1,7 @@ let proto = { - translate() { - this.x += this.step * Math.cos(this.alpha); - this.y += this.step * Math.sin(this.alpha); + translate(stepCount = 1) { + this.x += stepCount * this.step * Math.cos(this.alpha); + this.y += stepCount * this.step * Math.sin(this.alpha); this.minX = Math.min(this.minX, this.x); this.maxX = Math.max(this.maxX, this.x); this.minY = Math.min(this.minY, this.y); @@ -10,11 +10,15 @@ let proto = { rotate(factor) { this.alpha += factor * this.theta; }, - pushStack() { - this.stack.push({x: this.x, y: this.y, alpha: this.alpha}); + pushStack(repeatCount = 1) { + for (; repeatCount > 0; repeatCount--) { + this.stack.push({x: this.x, y: this.y, alpha: this.alpha}); + } }, - popStack() { - ({x: this.x, y: this.y, alpha: this.alpha} = this.stack.pop()); + popStack(repeatCount) { + for (; repeatCount > 0; repeatCount--) { + ({x: this.x, y: this.y, alpha: this.alpha} = this.stack.pop()); + } }, getDrawingRect() { let minX = Math.floor(this.minX);