Skip to content

Commit

Permalink
A few improvements to produce cleaner and smaller SVG code
Browse files Browse the repository at this point in the history
  • Loading branch information
Amphiluke committed Apr 4, 2020
1 parent 2b6304d commit 55befee
Show file tree
Hide file tree
Showing 10 changed files with 283 additions and 76 deletions.
31 changes: 15 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +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)
<a id="lindsvg-demo-svg"></a>
![Generated SVG tree](https://amphiluke.github.io/l-systems/img/autumn-tree.svg)

## Installation

Expand Down Expand Up @@ -94,36 +95,34 @@ An object returned by `getSVGData` contains [path data](https://www.w3.org/TR/SV

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:
For example, to generate the tree [demonstrated above](#lindsvg-demo-svg) (all but foliage) the following options were used:

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

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

// Output SVG parameters
let svgParams = {
width: 420,
height: 325,
width: 565,
height: 445,
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)", ""]
stroke: "#514d3a",
"stroke-width": ["16", "11", "9", "7", "6", "5", "3", "2", "1"],
"stroke-linecap": ["square", "round"] // the rest items are equal to the last one
}
};

Expand All @@ -134,7 +133,7 @@ let svgCode = getMultiPathSVGCode(lsParams, svgParams);
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.
If an attribute array contains less elements than the maximum branching depth (e.g. see `stroke-linecap` 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).

Expand Down
64 changes: 46 additions & 18 deletions dist/lindsvg.esm.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*!
lindsvg v1.3.0
lindsvg v1.3.1
https://amphiluke.github.io/l-systems/
(c) 2020 Amphiluke
*/
Expand Down Expand Up @@ -127,12 +127,28 @@ let defaults = {
iterations: 3
};

/**
* Remove all the stuff which doesn’t affect the drawing process from the raw generated codeword
* @param {String} codeword - Raw L-system code
* @return {String} - Clean L-system code
*/
function cleanCodeword(codeword) {
// Remove auxiliary drawing-indifferent letters
let cleanCodeword = codeword.replace(/[^FB[\]+-]/g, "");
do {
codeword = cleanCodeword;
// Remove useless brackets that don’t contain F commands or other brackets (preserving bracket balance!)
cleanCodeword = cleanCodeword.replace(/\[[^F[\]]*]/g, "");
} while (cleanCodeword !== codeword);
return cleanCodeword;
}

/**
* Generate L-system code
* @param {LSParams} lsParams - L-system parameters
* @return {String}
* @return {String} - Clean L-system code
*/
function generate(lsParams) {
function generateCodeword(lsParams) {
let validity = validate(lsParams);
if (validity !== true) {
throw new LSError(validity);
Expand All @@ -142,7 +158,16 @@ function generate(lsParams) {
for (; iterations > 0; iterations--) {
code = [...code].reduce((accumulator, letter) => accumulator + (rules[letter] || ""), "");
}
return code;
return cleanCodeword(code);
}

/**
* Split a codeword into “tokens” (group equal adjacent commands)
* @param {String} codeword - L-system code
* @return {String[]}
*/
function tokenizeCodeword(codeword) {
return codeword.match(/([FB[\]+-])\1*/g); // tokenize
}

let proto = {
Expand Down Expand Up @@ -187,21 +212,20 @@ function createTurtle({x, y, step, alpha, theta}) {
return turtle;
}

/**
* 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[]}
*/
function tokenizeCodeword(codeword) {
return codeword.replace(/[^FB[\]+-]/g, "").match(/([FB[\]+-])\1*/g);
}

function formatCoordinates(x, y) {
// Unary plus is used to remove insignificant trailing zeros
return `${+x.toFixed(4)} ${+y.toFixed(4)}`;
}

/**
* Delete useless M commands which are followed by other M commands, and those in the end of path data
* @param {String} pathData - SVG path data
* @return {String}
*/
function dropUselessMoves(pathData) {
return pathData.replace(/(?:M-?\d+(?:\.\d+)? -?\d+(?:\.\d+)?)+(?=M|$)/g, "");
}

/**
* Get the value of the d attribute
* @param {String[]} tokens - Tokenized codeword
Expand All @@ -210,7 +234,7 @@ function formatCoordinates(x, y) {
*/
function getPathData(tokens, turtle) {
let prevCommand; // used to avoid unnecessary repeating of the commands L and M
return tokens.reduce((accumulator, token) => {
let pathData = tokens.reduce((accumulator, token) => {
let tokenLength = token.length;
switch (token[0]) {
case "F":
Expand Down Expand Up @@ -246,6 +270,7 @@ function getPathData(tokens, turtle) {
}
return accumulator;
}, "M" + formatCoordinates(turtle.x, turtle.y));
return dropUselessMoves(pathData);
}

/**
Expand Down Expand Up @@ -302,7 +327,10 @@ function getMultiPathData(tokens, turtle) {
// 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"));
return multiPathData
.filter(pathData => pathData.includes("L"))
// also delete useless M commands (including those in the end of path data)
.map(dropUselessMoves);
}

/**
Expand All @@ -311,7 +339,7 @@ function getMultiPathData(tokens, turtle) {
* @return {{pathData: String, minX: Number, minY: Number, width: Number, height: Number}}
*/
function getSVGData(lsParams) {
let codeword = generate(lsParams);
let codeword = generateCodeword(lsParams);
let turtle = createTurtle({x: 0, y: 0, ...lsParams});
let pathData = getPathData(tokenizeCodeword(codeword), turtle);
return {
Expand All @@ -326,7 +354,7 @@ function getSVGData(lsParams) {
* @return {{multiPathData: String[], minX: Number, minY: Number, width: Number, height: Number}}
*/
function getMultiPathSVGData(lsParams) {
let codeword = generate(lsParams);
let codeword = generateCodeword(lsParams);
let turtle = createTurtle({x: 0, y: 0, ...lsParams});
let multiPathData = getMultiPathData(tokenizeCodeword(codeword), turtle);
return {
Expand Down
4 changes: 2 additions & 2 deletions dist/lindsvg.esm.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 55befee

Please sign in to comment.