From ffe06d58f478e9674d839d3c038a59d9bedebcd9 Mon Sep 17 00:00:00 2001 From: Amphiluke Date: Sun, 8 Mar 2020 17:47:45 +0700 Subject: [PATCH 1/5] Implement multi-path output mode --- dist/lindsvg.esm.js | 143 ++++++++++++++++++++++++++++++++----- dist/lindsvg.esm.min.js | 4 +- dist/lindsvg.js | 143 ++++++++++++++++++++++++++++++++----- dist/lindsvg.min.js | 4 +- package-lock.json | 2 +- package.json | 2 +- src/lindsvg.mjs | 4 +- src/svg.mjs | 139 +++++++++++++++++++++++++++++++---- test/browser-esm-test.html | 16 +++-- test/browser-umd-test.html | 13 ++-- test/node-cjs-test.js | 33 +++++++-- test/node-esm-test.mjs | 55 +++++++++----- test/params.mjs | 46 ++++++------ test/svg/.gitignore | 1 + 14 files changed, 494 insertions(+), 111 deletions(-) create mode 100644 test/svg/.gitignore diff --git a/dist/lindsvg.esm.js b/dist/lindsvg.esm.js index f543efa..30d7837 100644 --- a/dist/lindsvg.esm.js +++ b/dist/lindsvg.esm.js @@ -1,5 +1,5 @@ /*! -lindsvg v1.2.1 +lindsvg v1.3.0 https://amphiluke.github.io/l-systems/ (c) 2020 Amphiluke */ @@ -191,7 +191,7 @@ 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 {Array} + * @return {String[]} */ function tokenizeCodeword(codeword) { return codeword.match(/([FB[\]+-])\1*/g); @@ -204,7 +204,7 @@ function formatCoordinates(x, y) { /** * Get the value of the d attribute - * @param {Array} tokens - Tokenized codeword + * @param {String[]} tokens - Tokenized codeword * @param {Object} turtle - Turtle object to work with * @return {String} */ @@ -248,6 +248,59 @@ function getPathData(tokens, turtle) { }, "M" + formatCoordinates(turtle.x, turtle.y)); } +/** + * Get the values of the d attribute for each path element + * @param {String[]} tokens - Tokenized codeword + * @param {Object} turtle - Turtle object to work with + * @return {String[]} + */ +function getMultiPathData(tokens, turtle) { + let prevCommand; // used to avoid unnecessary repeating of the commands L and M + let branchLevel = 0; + return tokens.reduce((accumulator, token) => { + let pathData = accumulator[branchLevel] || ""; + let tokenLength = token.length; + switch (token[0]) { + case "F": + turtle.translate(tokenLength); + pathData += (prevCommand === "L" ? " " : "L") + formatCoordinates(turtle.x, turtle.y); + prevCommand = "L"; + break; + case "B": + 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”. + // This is not what we want, so delete the preceding moveto command + pathData = pathData.slice(0, pathData.lastIndexOf("M")); + } + pathData += "M" + formatCoordinates(turtle.x, turtle.y); + prevCommand = "M"; + break; + case "+": + turtle.rotate(tokenLength); + break; + case "-": + turtle.rotate(-tokenLength); + break; + case "[": + branchLevel += tokenLength; + turtle.pushStack(tokenLength); + pathData = `${accumulator[branchLevel] || ""}M${formatCoordinates(turtle.x, turtle.y)}`; + prevCommand = "M"; + break; + case "]": + branchLevel -= tokenLength; + turtle.popStack(tokenLength); + pathData = `${accumulator[branchLevel]}M${formatCoordinates(turtle.x, turtle.y)}`; + prevCommand = "M"; + break; + } + accumulator[branchLevel] = pathData; + return accumulator; + }, ["M" + formatCoordinates(turtle.x, turtle.y)]); +} + /** * Get raw data required for SVG rendering * @param {LSParams} lsParams - L-system parameters @@ -264,16 +317,24 @@ function getSVGData(lsParams) { } /** - * Get ready-to-render L-system’s SVG code + * Get raw data required for rendering of a multi-path SVG * @param {LSParams} lsParams - L-system parameters - * @param {SVGParams} svgParams - Output SVG parameters - * @return {String} + * @return {{multiPathData: String[], minX: Number, minY: Number, width: Number, height: Number}} */ -function getSVGCode(lsParams, svgParams) { - let {pathData, minX, minY, width, height} = getSVGData(lsParams); - let svgConfig = { - width: svgParams.width || width, - height: svgParams.height || height, +function getMultiPathSVGData(lsParams) { + let codeword = generate(lsParams); + let turtle = createTurtle({x: 0, y: 0, ...lsParams}); + let multiPathData = getMultiPathData(tokenizeCodeword(codeword), turtle); + return { + multiPathData, + ...turtle.getDrawingRect() + }; +} + +function makeSVGConfig(svgParams, naturalWidth, naturalHeight) { + return { + width: svgParams.width || naturalWidth, + height: svgParams.height || naturalHeight, padding: svgParams.padding || 0, pathAttributes: { // for backward compatibility with v1.1.0, also check fill and stroke as direct props of svgParams @@ -282,14 +343,62 @@ function getSVGCode(lsParams, svgParams) { ...svgParams.pathAttributes } }; - let {padding} = svgConfig; - let pathAttrStr = Object.entries(svgConfig.pathAttributes).reduce((accumulator, [name, value]) => { +} + +function makeSVGCode({viewBox, width, height, content}) { + return `${content}`; +} + +function makeAttrString(attrs, index) { + return Object.entries(attrs).reduce((accumulator, [name, value]) => { + if (Array.isArray(value)) { + value = value[Math.min(index, value.length - 1)]; + } + if (value === undefined) { + return accumulator; + } value = value.replace(/"/g, """); return `${accumulator} ${name}="${value}"`; }, ""); - return ` - -`; } -export { getSVGCode, getSVGData }; +/** + * Get ready-to-render L-system’s SVG code + * @param {LSParams} lsParams - L-system parameters + * @param {SVGParams} svgParams - Output SVG parameters + * @return {String} + */ +function getSVGCode(lsParams, svgParams) { + let {pathData, minX, minY, width: naturalWidth, height: naturalHeight} = getSVGData(lsParams); + let {padding, width, height, pathAttributes} = makeSVGConfig(svgParams, naturalWidth, naturalHeight); + let pathAttrStr = makeAttrString(pathAttributes, 0); + return makeSVGCode({ + viewBox: [minX - padding, minY - padding, naturalWidth + 2 * padding, naturalHeight + 2 * padding], + width, + height, + content: `` + }); +} + +/** + * Get ready-to-render multi-path SVG code for an L-system + * @param {LSParams} lsParams - L-system parameters + * @param {SVGParams} svgParams - Output SVG parameters + * @return {String} + */ +function getMultiPathSVGCode(lsParams, svgParams) { + let {multiPathData, minX, minY, width: naturalWidth, height: naturalHeight} = getMultiPathSVGData(lsParams); + let {padding, width, height, pathAttributes} = makeSVGConfig(svgParams, naturalWidth, naturalHeight); + let content = multiPathData.reduce((accumulator, pathData, index) => { + let pathAttrStr = makeAttrString(pathAttributes, index); + return `${accumulator}`; + }, ""); + return makeSVGCode({ + viewBox: [minX - padding, minY - padding, naturalWidth + 2 * padding, naturalHeight + 2 * padding], + width, + height, + content + }); +} + +export { getMultiPathSVGCode, getMultiPathSVGData, getSVGCode, getSVGData }; diff --git a/dist/lindsvg.esm.min.js b/dist/lindsvg.esm.min.js index 76f94ec..387dd13 100644 --- a/dist/lindsvg.esm.min.js +++ b/dist/lindsvg.esm.min.js @@ -1,6 +1,6 @@ /*! -lindsvg v1.2.1 +lindsvg v1.3.0 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(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}; +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 h=Object.create(null);return Object.entries(a).forEach(([a,s])=>{let c=function(a,i=t.LETTER){return e.test(a)||i}(a,r);!0===c&&(c=i(s,n)),!0!==c&&(h[a]=c)}),!Object.keys(h).length||h}function n(e){let a=Object.create(null);return Object.entries(e).forEach(([e,n])=>{let h=!0;switch(e){case"axiom":h=i(n,t.AXIOM);break;case"rules":h=r(n);break;case"alpha":case"theta":h=function(e,a=t.NUMBER){return Number.isFinite(e)||a}(n,t[e.toUpperCase()]);break;case"step":h=function(e,a=t.STEP){return Number.isFinite(e)&&e>0||a}(n);break;case"iterations":h=function(e,a=t.COUNT){return Number.isInteger(e)&&e>0||a}(n)}!0!==h&&(a[e]=h)}),!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 s={F:"",B:"","+":"+","-":"-","[":"[","]":"]"},c={alpha:0,theta:0,step:10,iterations:3};function o(t){let e=n(t);if(!0!==e)throw new h(e);let{axiom:a,iterations:i}={...c,...t},r={...s,...t.rules};for(;i>0;i--)a=[...a].reduce((t,e)=>t+(r[e]||""),"");return a}let l={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({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}function p(t){return t.match(/([FB[\]+-])\1*/g)}function m(t,e){return`${+t.toFixed(4)} ${+e.toFixed(4)}`}function x(t){let e=o(t),a=u({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")+m(e.x,e.y),a="L";break;case"B":e.translate(r),"M"===a&&(t=t.slice(0,t.lastIndexOf("M"))),t+="M"+m(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${m(e.x,e.y)}`,a="M"}return t},"M"+m(e.x,e.y))}(p(e),a),...a.getDrawingRect()}}function f(t){let e=o(t),a=u({x:0,y:0,...t});return{multiPathData:function(t,e){let a,i=0;return t.reduce((t,r)=>{let n=t[i]||"",h=r.length;switch(r[0]){case"F":e.translate(h),n+=("L"===a?" ":"L")+m(e.x,e.y),a="L";break;case"B":e.translate(h),"M"===a&&(n=n.slice(0,n.lastIndexOf("M"))),n+="M"+m(e.x,e.y),a="M";break;case"+":e.rotate(h);break;case"-":e.rotate(-h);break;case"[":i+=h,e.pushStack(h),n=`${t[i]||""}M${m(e.x,e.y)}`,a="M";break;case"]":i-=h,e.popStack(h),n=`${t[i]}M${m(e.x,e.y)}`,a="M"}return t[i]=n,t},["M"+m(e.x,e.y)])}(p(e),a),...a.getDrawingRect()}}function b(t,e,a){return{width:t.width||e,height:t.height||a,padding:t.padding||0,pathAttributes:{fill:t.fill||"none",stroke:t.stroke||"#000",...t.pathAttributes}}}function d({viewBox:t,width:e,height:a,content:i}){return`${i}`}function g(t,e){return Object.entries(t).reduce((t,[a,i])=>(Array.isArray(i)&&(i=i[Math.min(e,i.length-1)]),void 0===i?t:`${t} ${a}="${i=i.replace(/"/g,""")}"`),"")}function y(t,e){let{pathData:a,minX:i,minY:r,width:n,height:h}=x(t),{padding:s,width:c,height:o,pathAttributes:l}=b(e,n,h);return d({viewBox:[i-s,r-s,n+2*s,h+2*s],width:c,height:o,content:``})}function w(t,e){let{multiPathData:a,minX:i,minY:r,width:n,height:h}=f(t),{padding:s,width:c,height:o,pathAttributes:l}=b(e,n,h);return d({viewBox:[i-s,r-s,n+2*s,h+2*s],width:c,height:o,content:a.reduce((t,e,a)=>`${t}`,"")})}export{w as getMultiPathSVGCode,f as getMultiPathSVGData,y as getSVGCode,x as getSVGData}; diff --git a/dist/lindsvg.js b/dist/lindsvg.js index 141be53..1a1c6c0 100644 --- a/dist/lindsvg.js +++ b/dist/lindsvg.js @@ -1,5 +1,5 @@ /*! -lindsvg v1.2.1 +lindsvg v1.3.0 https://amphiluke.github.io/l-systems/ (c) 2020 Amphiluke */ @@ -197,7 +197,7 @@ 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 {Array} + * @return {String[]} */ function tokenizeCodeword(codeword) { return codeword.match(/([FB[\]+-])\1*/g); @@ -210,7 +210,7 @@ https://amphiluke.github.io/l-systems/ /** * Get the value of the d attribute - * @param {Array} tokens - Tokenized codeword + * @param {String[]} tokens - Tokenized codeword * @param {Object} turtle - Turtle object to work with * @return {String} */ @@ -254,6 +254,59 @@ https://amphiluke.github.io/l-systems/ }, "M" + formatCoordinates(turtle.x, turtle.y)); } + /** + * Get the values of the d attribute for each path element + * @param {String[]} tokens - Tokenized codeword + * @param {Object} turtle - Turtle object to work with + * @return {String[]} + */ + function getMultiPathData(tokens, turtle) { + let prevCommand; // used to avoid unnecessary repeating of the commands L and M + let branchLevel = 0; + return tokens.reduce((accumulator, token) => { + let pathData = accumulator[branchLevel] || ""; + let tokenLength = token.length; + switch (token[0]) { + case "F": + turtle.translate(tokenLength); + pathData += (prevCommand === "L" ? " " : "L") + formatCoordinates(turtle.x, turtle.y); + prevCommand = "L"; + break; + case "B": + 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”. + // This is not what we want, so delete the preceding moveto command + pathData = pathData.slice(0, pathData.lastIndexOf("M")); + } + pathData += "M" + formatCoordinates(turtle.x, turtle.y); + prevCommand = "M"; + break; + case "+": + turtle.rotate(tokenLength); + break; + case "-": + turtle.rotate(-tokenLength); + break; + case "[": + branchLevel += tokenLength; + turtle.pushStack(tokenLength); + pathData = `${accumulator[branchLevel] || ""}M${formatCoordinates(turtle.x, turtle.y)}`; + prevCommand = "M"; + break; + case "]": + branchLevel -= tokenLength; + turtle.popStack(tokenLength); + pathData = `${accumulator[branchLevel]}M${formatCoordinates(turtle.x, turtle.y)}`; + prevCommand = "M"; + break; + } + accumulator[branchLevel] = pathData; + return accumulator; + }, ["M" + formatCoordinates(turtle.x, turtle.y)]); + } + /** * Get raw data required for SVG rendering * @param {LSParams} lsParams - L-system parameters @@ -270,16 +323,24 @@ https://amphiluke.github.io/l-systems/ } /** - * Get ready-to-render L-system’s SVG code + * Get raw data required for rendering of a multi-path SVG * @param {LSParams} lsParams - L-system parameters - * @param {SVGParams} svgParams - Output SVG parameters - * @return {String} + * @return {{multiPathData: String[], minX: Number, minY: Number, width: Number, height: Number}} */ - function getSVGCode(lsParams, svgParams) { - let {pathData, minX, minY, width, height} = getSVGData(lsParams); - let svgConfig = { - width: svgParams.width || width, - height: svgParams.height || height, + function getMultiPathSVGData(lsParams) { + let codeword = generate(lsParams); + let turtle = createTurtle({x: 0, y: 0, ...lsParams}); + let multiPathData = getMultiPathData(tokenizeCodeword(codeword), turtle); + return { + multiPathData, + ...turtle.getDrawingRect() + }; + } + + function makeSVGConfig(svgParams, naturalWidth, naturalHeight) { + return { + width: svgParams.width || naturalWidth, + height: svgParams.height || naturalHeight, padding: svgParams.padding || 0, pathAttributes: { // for backward compatibility with v1.1.0, also check fill and stroke as direct props of svgParams @@ -288,16 +349,66 @@ https://amphiluke.github.io/l-systems/ ...svgParams.pathAttributes } }; - let {padding} = svgConfig; - let pathAttrStr = Object.entries(svgConfig.pathAttributes).reduce((accumulator, [name, value]) => { + } + + function makeSVGCode({viewBox, width, height, content}) { + return `${content}`; + } + + function makeAttrString(attrs, index) { + return Object.entries(attrs).reduce((accumulator, [name, value]) => { + if (Array.isArray(value)) { + value = value[Math.min(index, value.length - 1)]; + } + if (value === undefined) { + return accumulator; + } value = value.replace(/"/g, """); return `${accumulator} ${name}="${value}"`; }, ""); - return ` - -`; } + /** + * Get ready-to-render L-system’s SVG code + * @param {LSParams} lsParams - L-system parameters + * @param {SVGParams} svgParams - Output SVG parameters + * @return {String} + */ + function getSVGCode(lsParams, svgParams) { + let {pathData, minX, minY, width: naturalWidth, height: naturalHeight} = getSVGData(lsParams); + let {padding, width, height, pathAttributes} = makeSVGConfig(svgParams, naturalWidth, naturalHeight); + let pathAttrStr = makeAttrString(pathAttributes, 0); + return makeSVGCode({ + viewBox: [minX - padding, minY - padding, naturalWidth + 2 * padding, naturalHeight + 2 * padding], + width, + height, + content: `` + }); + } + + /** + * Get ready-to-render multi-path SVG code for an L-system + * @param {LSParams} lsParams - L-system parameters + * @param {SVGParams} svgParams - Output SVG parameters + * @return {String} + */ + function getMultiPathSVGCode(lsParams, svgParams) { + let {multiPathData, minX, minY, width: naturalWidth, height: naturalHeight} = getMultiPathSVGData(lsParams); + let {padding, width, height, pathAttributes} = makeSVGConfig(svgParams, naturalWidth, naturalHeight); + let content = multiPathData.reduce((accumulator, pathData, index) => { + let pathAttrStr = makeAttrString(pathAttributes, index); + return `${accumulator}`; + }, ""); + return makeSVGCode({ + viewBox: [minX - padding, minY - padding, naturalWidth + 2 * padding, naturalHeight + 2 * padding], + width, + height, + content + }); + } + + exports.getMultiPathSVGCode = getMultiPathSVGCode; + exports.getMultiPathSVGData = getMultiPathSVGData; exports.getSVGCode = getSVGCode; exports.getSVGData = getSVGData; diff --git a/dist/lindsvg.min.js b/dist/lindsvg.min.js index 86ba2e2..c807cc3 100644 --- a/dist/lindsvg.min.js +++ b/dist/lindsvg.min.js @@ -1,6 +1,6 @@ /*! -lindsvg v1.2.1 +lindsvg v1.3.0 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:"","+":"+","-":"-","[":"[","]":"]"},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})})); +!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};function u(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}let l={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 p({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}function f(t){return t.match(/([FB[\]+-])\1*/g)}function m(t,e){return`${+t.toFixed(4)} ${+e.toFixed(4)}`}function d(t){let e=u(t),a=p({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")+m(e.x,e.y),a="L";break;case"B":e.translate(r),"M"===a&&(t=t.slice(0,t.lastIndexOf("M"))),t+="M"+m(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${m(e.x,e.y)}`,a="M"}return t},"M"+m(e.x,e.y))}(f(e),a),...a.getDrawingRect()}}function x(t){let e=u(t),a=p({x:0,y:0,...t});return{multiPathData:function(t,e){let a,i=0;return t.reduce((t,r)=>{let n=t[i]||"",s=r.length;switch(r[0]){case"F":e.translate(s),n+=("L"===a?" ":"L")+m(e.x,e.y),a="L";break;case"B":e.translate(s),"M"===a&&(n=n.slice(0,n.lastIndexOf("M"))),n+="M"+m(e.x,e.y),a="M";break;case"+":e.rotate(s);break;case"-":e.rotate(-s);break;case"[":i+=s,e.pushStack(s),n=`${t[i]||""}M${m(e.x,e.y)}`,a="M";break;case"]":i-=s,e.popStack(s),n=`${t[i]}M${m(e.x,e.y)}`,a="M"}return t[i]=n,t},["M"+m(e.x,e.y)])}(f(e),a),...a.getDrawingRect()}}function b(t,e,a){return{width:t.width||e,height:t.height||a,padding:t.padding||0,pathAttributes:{fill:t.fill||"none",stroke:t.stroke||"#000",...t.pathAttributes}}}function g({viewBox:t,width:e,height:a,content:i}){return`${i}`}function y(t,e){return Object.entries(t).reduce((t,[a,i])=>(Array.isArray(i)&&(i=i[Math.min(e,i.length-1)]),void 0===i?t:`${t} ${a}="${i=i.replace(/"/g,""")}"`),"")}t.getMultiPathSVGCode=function(t,e){let{multiPathData:a,minX:i,minY:r,width:n,height:s}=x(t),{padding:h,width:o,height:c,pathAttributes:u}=b(e,n,s);return g({viewBox:[i-h,r-h,n+2*h,s+2*h],width:o,height:c,content:a.reduce((t,e,a)=>`${t}`,"")})},t.getMultiPathSVGData=x,t.getSVGCode=function(t,e){let{pathData:a,minX:i,minY:r,width:n,height:s}=d(t),{padding:h,width:o,height:c,pathAttributes:u}=b(e,n,s);return g({viewBox:[i-h,r-h,n+2*h,s+2*h],width:o,height:c,content:``})},t.getSVGData=d,Object.defineProperty(t,"__esModule",{value:!0})})); diff --git a/package-lock.json b/package-lock.json index fdde8ba..7e32e2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "lindsvg", - "version": "1.2.1", + "version": "1.3.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 8cfe39d..bac9c5f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lindsvg", - "version": "1.2.1", + "version": "1.3.0", "description": "Lindenmayer System [Scalable] Vector Graphics", "main": "dist/lindsvg.js", "module": "dist/lindsvg.esm.js", diff --git a/src/lindsvg.mjs b/src/lindsvg.mjs index b3e5d79..8624896 100644 --- a/src/lindsvg.mjs +++ b/src/lindsvg.mjs @@ -20,8 +20,8 @@ * @property {Number} [width] - Desired SVG width * @property {Number} [height] - Desired SVG height * @property {Number} [padding=0] - Additional space to extend the viewBox - * @property {Object} [pathAttributes={fill:"none",stroke:"#000"}] - Name to value map for the “path” element attributes + * @property {Object.} [pathAttributes={fill:"none",stroke:"#000"}] - Name to value map for the element attributes */ -export {getSVGData, getSVGCode} from "./svg.mjs"; +export * from "./svg.mjs"; diff --git a/src/svg.mjs b/src/svg.mjs index 0042296..9468203 100644 --- a/src/svg.mjs +++ b/src/svg.mjs @@ -5,7 +5,7 @@ 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 {Array} + * @return {String[]} */ function tokenizeCodeword(codeword) { return codeword.match(/([FB[\]+-])\1*/g); @@ -18,7 +18,7 @@ function formatCoordinates(x, y) { /** * Get the value of the d attribute - * @param {Array} tokens - Tokenized codeword + * @param {String[]} tokens - Tokenized codeword * @param {Object} turtle - Turtle object to work with * @return {String} */ @@ -62,6 +62,59 @@ function getPathData(tokens, turtle) { }, "M" + formatCoordinates(turtle.x, turtle.y)); } +/** + * Get the values of the d attribute for each path element + * @param {String[]} tokens - Tokenized codeword + * @param {Object} turtle - Turtle object to work with + * @return {String[]} + */ +function getMultiPathData(tokens, turtle) { + let prevCommand; // used to avoid unnecessary repeating of the commands L and M + let branchLevel = 0; + return tokens.reduce((accumulator, token) => { + let pathData = accumulator[branchLevel] || ""; + let tokenLength = token.length; + switch (token[0]) { + case "F": + turtle.translate(tokenLength); + pathData += (prevCommand === "L" ? " " : "L") + formatCoordinates(turtle.x, turtle.y); + prevCommand = "L"; + break; + case "B": + 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”. + // This is not what we want, so delete the preceding moveto command + pathData = pathData.slice(0, pathData.lastIndexOf("M")); + } + pathData += "M" + formatCoordinates(turtle.x, turtle.y); + prevCommand = "M"; + break; + case "+": + turtle.rotate(tokenLength); + break; + case "-": + turtle.rotate(-tokenLength); + break; + case "[": + branchLevel += tokenLength; + turtle.pushStack(tokenLength); + pathData = `${accumulator[branchLevel] || ""}M${formatCoordinates(turtle.x, turtle.y)}`; + prevCommand = "M"; + break; + case "]": + branchLevel -= tokenLength; + turtle.popStack(tokenLength); + pathData = `${accumulator[branchLevel]}M${formatCoordinates(turtle.x, turtle.y)}`; + prevCommand = "M"; + break; + } + accumulator[branchLevel] = pathData; + return accumulator; + }, ["M" + formatCoordinates(turtle.x, turtle.y)]); +} + /** * Get raw data required for SVG rendering * @param {LSParams} lsParams - L-system parameters @@ -78,16 +131,24 @@ export function getSVGData(lsParams) { } /** - * Get ready-to-render L-system’s SVG code + * Get raw data required for rendering of a multi-path SVG * @param {LSParams} lsParams - L-system parameters - * @param {SVGParams} svgParams - Output SVG parameters - * @return {String} + * @return {{multiPathData: String[], minX: Number, minY: Number, width: Number, height: Number}} */ -export function getSVGCode(lsParams, svgParams) { - let {pathData, minX, minY, width, height} = getSVGData(lsParams); - let svgConfig = { - width: svgParams.width || width, - height: svgParams.height || height, +export function getMultiPathSVGData(lsParams) { + let codeword = generate(lsParams); + let turtle = createTurtle({x: 0, y: 0, ...lsParams}); + let multiPathData = getMultiPathData(tokenizeCodeword(codeword), turtle); + return { + multiPathData, + ...turtle.getDrawingRect() + }; +} + +function makeSVGConfig(svgParams, naturalWidth, naturalHeight) { + return { + width: svgParams.width || naturalWidth, + height: svgParams.height || naturalHeight, padding: svgParams.padding || 0, pathAttributes: { // for backward compatibility with v1.1.0, also check fill and stroke as direct props of svgParams @@ -96,12 +157,60 @@ export function getSVGCode(lsParams, svgParams) { ...svgParams.pathAttributes } }; - let {padding} = svgConfig; - let pathAttrStr = Object.entries(svgConfig.pathAttributes).reduce((accumulator, [name, value]) => { +} + +function makeSVGCode({viewBox, width, height, content}) { + return `${content}`; +} + +function makeAttrString(attrs, index) { + return Object.entries(attrs).reduce((accumulator, [name, value]) => { + if (Array.isArray(value)) { + value = value[Math.min(index, value.length - 1)]; + } + if (value === undefined) { + return accumulator; + } value = value.replace(/"/g, """); return `${accumulator} ${name}="${value}"`; }, ""); - return ` - -`; +} + +/** + * Get ready-to-render L-system’s SVG code + * @param {LSParams} lsParams - L-system parameters + * @param {SVGParams} svgParams - Output SVG parameters + * @return {String} + */ +export function getSVGCode(lsParams, svgParams) { + let {pathData, minX, minY, width: naturalWidth, height: naturalHeight} = getSVGData(lsParams); + let {padding, width, height, pathAttributes} = makeSVGConfig(svgParams, naturalWidth, naturalHeight); + let pathAttrStr = makeAttrString(pathAttributes, 0); + return makeSVGCode({ + viewBox: [minX - padding, minY - padding, naturalWidth + 2 * padding, naturalHeight + 2 * padding], + width, + height, + content: `` + }); +} + +/** + * Get ready-to-render multi-path SVG code for an L-system + * @param {LSParams} lsParams - L-system parameters + * @param {SVGParams} svgParams - Output SVG parameters + * @return {String} + */ +export function getMultiPathSVGCode(lsParams, svgParams) { + let {multiPathData, minX, minY, width: naturalWidth, height: naturalHeight} = getMultiPathSVGData(lsParams); + let {padding, width, height, pathAttributes} = makeSVGConfig(svgParams, naturalWidth, naturalHeight); + let content = multiPathData.reduce((accumulator, pathData, index) => { + let pathAttrStr = makeAttrString(pathAttributes, index); + return `${accumulator}`; + }, ""); + return makeSVGCode({ + viewBox: [minX - padding, minY - padding, naturalWidth + 2 * padding, naturalHeight + 2 * padding], + width, + height, + content + }); } diff --git a/test/browser-esm-test.html b/test/browser-esm-test.html index 20d3908..2896bed 100644 --- a/test/browser-esm-test.html +++ b/test/browser-esm-test.html @@ -9,22 +9,26 @@ position: fixed; left: 0; top: 0; - width: 100vw; + width: 50vw; height: 100vh; } + svg + svg { + left: 50vw; + }