Skip to content

Commit

Permalink
Merge branch 'lindsvg-1.3.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
Amphiluke committed Mar 14, 2020
2 parents 79e61a1 + edabdf7 commit 7f4bbab
Show file tree
Hide file tree
Showing 15 changed files with 605 additions and 151 deletions.
78 changes: 69 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

Simple dependency-free module used to generate SVG images of deterministic L-systems.

![Generated SVG tree](https://amphiluke.github.io/l-systems/img/tree.svg)

## Installation

### In an npm project
Expand Down Expand Up @@ -35,12 +37,20 @@ If you rather prefer using ES modules in a browser, just choose the “esm” bu

## API & examples

The module exports two methods:
The module exports two pairs of methods.

1. The methods returning ready-to-render L-system’s SVG code as a string:
* `getSVGCode(lsParams[, svgParams])`;
* `getMultiPathSVGCode(lsParams[, svgParams])`;
2. The methods returning raw data that you may use to construct the SVG code yourself:
* `getSVGData(lsParams)`;
* `getMultiPathSVGData(lsParams)`.

The “multi-path” methods (`getMultiPathSVGCode` and `getMultiPathSVGData`) differ from the “normal” methods (`getSVGCode` and `getSVGData`) in that they provide the ability for advanced stylisation of _branched_ L-systems. SVG images created using these “multi-path” methods contain several `<path>` elements, each one for a specific branching level, so they can be stylised differently (color, line width, etc.)

* `getSVGCode(lsParams[, svgParams])`: returns ready-to-render L-system’s SVG code as a string;
* `getSVGData(lsParams)`: returns raw data that you may use to construct the SVG code yourself.
All methods expect L-system parameters object as their first argument. These parameters are explained through the comments in the snippet below. Additionally, the methods `getSVGCode` and `getMultiPathSVGCode` may be passed an _optional_ parameter to alter the output SVG settings (refer the comments in the snippet below).

Both methods expect L-system parameters object as their first argument. These parameters are explained through the comments in the snippet below. Additionally, the method `getSVGCode` may be passed an _optional_ parameter to alter the output SVG settings (refer the comments in the snippet below).
### Using “single-path” methods

```javascript
let {getSVGCode, getSVGData} = require("lindsvg");
Expand All @@ -65,21 +75,71 @@ let svgParams = {
width: 600, // Desired SVG element width
height: 600, // Desired SVG element height
padding: 5, // Additional space to extend the viewBox
pathAttributes: { // Name to value map for the path element attributes
pathAttributes: { // Name to value map for the <path> element attributes
stroke: "green",
"stroke-width": "2px"
"stroke-width": "2"
}
};

// Get ready-to-render L-system’s SVG code as a string
// Get ready-to-render L-system’s SVG code as a string...
let svgCode = getSVGCode(lsParams, svgParams);

// Get raw data required for SVG rendering
// ...or get raw data required for manual SVG assemblage
let {pathData, minX, minY, width, height} = getSVGData(lsParams);
```

An object returned by `getSVGData` contains [path data](https://www.w3.org/TR/SVG11/paths.html#PathData) needed to draw the L-system, and also the drawing boundaries that are essential for the `viewBox` attribute.

### Using “multi-path” methods

Using “multi-path” methods (`getMultiPathSVGCode` and `getMultiPathSVGData`) allows you to specify different path attributes for every `<path>` element separately, which may make branched L-systems (like plants) look “more naturally”.

For example, the image of a tree [demonstrated above](#lindsvg) was generated using the following options:

```javascript
let {getMultiPathSVGCode, getMultiPathSVGData} = require("lindsvg");

// L-system parameters
let lsParams = {
axiom: "FFF+FFFF-FF+FF-[-Y][+Y][Z][+Z]",
rules: {
F: "F",
Y: "FF+F-F-F[FFFZ][+Z]-F-FZ",
Z: "FF-F+F+F[FY][-Y]+F+F++Y"
},
alpha: 90 * Math.PI / 180,
theta: 10 * Math.PI / 180,
iterations: 7,
step: 5
};

// Output SVG parameters
let svgParams = {
width: 420,
height: 325,
padding: 10,
pathAttributes: {
stroke: ["#514d3a", "#514d3a", "#514d2a", "#55771c", "#55771c", "#44621c",
"rgba(131, 163, 90, 0.5)", "rgba(164, 184, 102, 0.5)", "rgba(192, 200, 97, 0.5)"],
"stroke-width": ["11", "5", "3", "1"], // the rest items are equal to the last one
"stroke-linecap": ["square", "square", "round"],
transform: ["skewY(-35)", ""]
}
};

// Get ready-to-render L-system’s SVG code as a string...
let svgCode = getMultiPathSVGCode(lsParams, svgParams);

// ...or get raw data required for manual SVG assemblage
let {multiPathData, minX, minY, width, height} = getMultiPathSVGData(lsParams);
```

If an attribute array contains less elements than the maximum branching depth (e.g. see `stroke-width` in the example above), the missing items are considered equal to the last one. So you don’t need to repeat the same value in the end of the list.

The property `multiPathData` in the object returned by `getMultiPathSVGData` is a _list_ of path data for every `<path>` element. The list is sorted in the order of increasing branch level (the deeper the branch the higher the index in the array).

### Error handling

In case of invalid input L-system parameters, the methods throw a custom exception. You may use it to get a detailed explanation of which parameter(s) failed to pass validation, and format the message as you wish.

```javascript
Expand All @@ -99,6 +159,6 @@ try {
}
```

### Compatibility note
## Compatibility note

lindsvg utilizes the ECMAScript 2018 syntax. If you want to use the module in environments that do not support ES 2018, please transpile the sources with babel or whatever for your needs.
149 changes: 131 additions & 18 deletions dist/lindsvg.esm.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*!
lindsvg v1.2.1
lindsvg v1.3.0
https://amphiluke.github.io/l-systems/
(c) 2020 Amphiluke
*/
Expand Down Expand Up @@ -191,10 +191,10 @@ 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<String>}
* @return {String[]}
*/
function tokenizeCodeword(codeword) {
return codeword.match(/([FB[\]+-])\1*/g);
return codeword.replace(/[^FB[\]+-]/g, "").match(/([FB[\]+-])\1*/g);
}

function formatCoordinates(x, y) {
Expand All @@ -204,7 +204,7 @@ function formatCoordinates(x, y) {

/**
* Get the value of the d attribute
* @param {Array<String>} tokens - Tokenized codeword
* @param {String[]} tokens - Tokenized codeword
* @param {Object} turtle - Turtle object to work with
* @return {String}
*/
Expand Down Expand Up @@ -248,6 +248,63 @@ 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;
let multiPathData = 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)]);
// Some L-systems can produce branching levels which contain no real draw commands (only moves and rotations).
// Such L-systems usually don’t have F commands in their axiom nor they have a production for F (example is
// the Penrose tiling). Having <path> elements with only M commands is meaningless, so filtering them out
return multiPathData.filter(pathData => pathData.includes("L"));
}

/**
* Get raw data required for SVG rendering
* @param {LSParams} lsParams - L-system parameters
Expand All @@ -264,16 +321,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
Expand All @@ -282,14 +347,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 `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox.join(" ")}" height="${height}" width="${width}">${content}</svg>`;
}

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, "&quot;");
return `${accumulator} ${name}="${value}"`;
}, "");
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${minX - padding} ${minY - padding} ${width + 2 * padding} ${height + 2 * padding}" height="${svgConfig.height}" width="${svgConfig.width}">
<path d="${pathData}"${pathAttrStr}></path>
</svg>`;
}

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: `<path d="${pathData}"${pathAttrStr}></path>`
});
}

/**
* 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}<path d="${pathData}"${pathAttrStr}></path>`;
}, "");
return makeSVGCode({
viewBox: [minX - padding, minY - padding, naturalWidth + 2 * padding, naturalHeight + 2 * padding],
width,
height,
content
});
}

export { getMultiPathSVGCode, getMultiPathSVGData, getSVGCode, getSVGData };
Loading

0 comments on commit 7f4bbab

Please sign in to comment.