Skip to content

Commit 2dc1bcd

Browse files
authored
Merge pull request #326 from jeffeb3/feature/lsystem-patterns
New L-system patterns
2 parents dd68284 + 1a38b93 commit 2dc1bcd

4 files changed

Lines changed: 170 additions & 22 deletions

File tree

src/common/lindenmayer.js

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,20 @@ const shortestPath = (nodes) => {
1111
for (let i = 0; i < nodes.length - 1; i++) {
1212
const node1 = nodes[i]
1313
const node2 = nodes[i + 1]
14-
let node1Key = node1.toString()
15-
let edge12Key = edgeKey(node1, node2)
14+
const node1Key = node1.toString()
15+
const edge12Key = edgeKey(node1, node2)
1616

1717
if (visited[edge12Key]) {
18-
const unvisitedNode = nearestUnvisitedNode(i + 1, nodes, visited, graph)
18+
const result = nearestUnvisitedNode(i + 1, nodes, visited, graph)
1919

20-
if (unvisitedNode != null) {
20+
if (result) {
2121
const shortestSubPath = graph.dijkstraShortestPath(
2222
node1Key,
23-
unvisitedNode.toString(),
23+
result.node.toString(),
2424
)
2525

2626
path.push(...cloneVertices(shortestSubPath.slice(1)))
27-
i = nodes.indexOf(unvisitedNode) - 1
27+
i = result.index - 1
2828
}
2929
} else {
3030
path.push(node2)
@@ -41,7 +41,7 @@ const nearestUnvisitedNode = (nodeIndex, nodes, visited, graph) => {
4141
const node2 = nodes[i + 1]
4242

4343
if (!visited[edgeKey(node1, node2)]) {
44-
return node2
44+
return { node: node2, index: i + 1 }
4545
}
4646
}
4747

@@ -98,6 +98,7 @@ export const lsystem = (config) => {
9898
}
9999
input = output
100100
}
101+
101102
return output
102103
}
103104

@@ -111,10 +112,26 @@ const lsystemDraw = (vertex, angle, config) => {
111112
)
112113
}
113114

115+
// Skip to matching closing bracket, accounting for nesting
116+
const skipBranch = (instructions, startIndex) => {
117+
let depth = 1
118+
let i = startIndex + 1
119+
120+
while (i < instructions.length && depth > 0) {
121+
if (instructions[i] === "[") depth++
122+
else if (instructions[i] === "]") depth--
123+
i++
124+
}
125+
126+
return i - 1 // return index of matching ]
127+
}
128+
114129
export const lsystemPath = (instructions, config) => {
115130
let vertex = new Victor(0, 0)
116131
let currVertices = [vertex]
117132
let angle = -Math.PI / 2
133+
const rng = config.rng || Math.random
134+
const branchProb = config.branchProbability ?? 1
118135

119136
if (config.startingAngle) {
120137
angle =
@@ -125,6 +142,7 @@ export const lsystemPath = (instructions, config) => {
125142

126143
// This will store the previous return paths we are not working on.
127144
let returnPaths = []
145+
128146
for (let i = 0; i < instructions.length; i++) {
129147
let char = instructions[i]
130148

@@ -145,11 +163,19 @@ export const lsystemPath = (instructions, config) => {
145163
returnPaths.slice(-1)[0].push("B")
146164
}
147165
} else if (char === "[") {
148-
// open a branch
149-
returnPaths.push([])
166+
// Randomly skip branch based on probability
167+
if (branchProb < 1 && rng() > branchProb) {
168+
i = skipBranch(instructions, i)
169+
} else {
170+
// open a branch
171+
returnPaths.push([])
172+
}
150173
} else if (char === "]") {
151174
// Return to the beginning of the branch
152-
let returnPath = returnPaths.pop().reverse()
175+
let returnPath = returnPaths.pop()
176+
177+
if (!returnPath) continue
178+
returnPath.reverse()
153179

154180
for (let j = 0; j < returnPath.length; j++) {
155181
let revChar = returnPath[j]

src/components/SliderOption.js

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,33 @@ const SliderOption = ({
4545
}
4646

4747
const handleInputChange = (e) => {
48+
const newValue = parseFloat(e.target.value)
49+
4850
handleChange(e.target.value)
51+
if (!isNaN(newValue) && newValue >= minimum && newValue <= maximum) {
52+
handleChangeComplete(newValue)
53+
}
54+
}
55+
56+
const handleInputBlur = (e) => {
57+
let newValue = parseFloat(e.target.value)
58+
59+
if (isNaN(newValue)) {
60+
newValue = 0
61+
} else if (newValue < minimum) {
62+
newValue = minimum
63+
} else if (newValue > maximum) {
64+
newValue = maximum
65+
}
66+
67+
handleChange(newValue)
68+
handleChangeComplete(newValue)
69+
}
70+
71+
const handleInputKeyDown = (e) => {
72+
if (e.key === "Enter") {
73+
e.target.blur()
74+
}
4975
}
5076

5177
const handleChangeComplete = (newValue) => {
@@ -59,6 +85,12 @@ const SliderOption = ({
5985
}
6086

6187
let marks
88+
const parsedValue = parseFloat(value)
89+
const clampedValue = Math.min(
90+
Math.max(isNaN(parsedValue) ? minimum : parsedValue, minimum),
91+
maximum,
92+
)
93+
6294
if (isNaN(minimum) || isNaN(maximum)) {
6395
marks = {}
6496
} else if (option.range) {
@@ -71,7 +103,7 @@ const SliderOption = ({
71103
} else {
72104
marks = {
73105
[minimum]: `${minimum}`,
74-
[value]: `${value}`,
106+
[clampedValue]: `${clampedValue}`,
75107
[maximum]: `${maximum}`,
76108
}
77109
}
@@ -86,7 +118,7 @@ const SliderOption = ({
86118
marks={marks}
87119
range={option.range}
88120
allowCross={false}
89-
value={value}
121+
value={clampedValue}
90122
onChangeComplete={handleChangeComplete}
91123
onChange={handleChange}
92124
/>
@@ -105,6 +137,8 @@ const SliderOption = ({
105137
value={value}
106138
autoComplete="off"
107139
onChange={handleInputChange}
140+
onBlur={handleInputBlur}
141+
onKeyDown={handleInputKeyDown}
108142
/>
109143
)
110144

src/features/shapes/lsystem/LSystem.js

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import seedrandom from "seedrandom"
12
import Shape from "../Shape"
23
import {
34
lsystem,
@@ -28,6 +29,47 @@ const options = {
2829
return onMaxIterations(subtypes[state.subtype], state)
2930
},
3031
},
32+
angleOffset: {
33+
title: "Angle offset",
34+
type: "slider",
35+
min: -15,
36+
max: 15,
37+
step: 0.5,
38+
default: 0,
39+
},
40+
lsystemBranchProbability: {
41+
title: "Branch probability",
42+
type: "slider",
43+
min: 0,
44+
max: 100,
45+
step: 5,
46+
default: 100,
47+
isVisible: (model, state) => {
48+
const subtype = subtypes[state.subtype]
49+
50+
if (!subtype) return false
51+
52+
const allRules = subtype.axiom + Object.values(subtype.rules).join("")
53+
54+
return allRules.includes("[")
55+
},
56+
},
57+
seed: {
58+
title: "Seed",
59+
min: 1,
60+
max: 999,
61+
step: 1,
62+
isVisible: (model, state) => {
63+
const subtype = subtypes[state.subtype]
64+
65+
if (!subtype) return false
66+
67+
const allRules = subtype.axiom + Object.values(subtype.rules).join("")
68+
const hasBranches = allRules.includes("[")
69+
70+
return hasBranches && (state.lsystemBranchProbability ?? 100) < 100
71+
},
72+
},
3173
}
3274

3375
export default class LSystem extends Shape {
@@ -46,24 +88,27 @@ export default class LSystem extends Shape {
4688
...{
4789
iterations: 3,
4890
subtype: "McWorter's Pentadendrite",
91+
angleOffset: 0,
92+
seed: 1,
93+
lsystemBranchProbability: 100,
4994
},
5095
}
5196
}
5297

5398
getVertices(state) {
5499
const shape = state.shape
55100
const iterations = shape.iterations || 1
56-
57-
// generate our vertices using a set of l-system rules
58-
let config = subtypes[shape.subtype]
59-
60-
config.iterations = iterations
61-
config.side = 5
62-
63-
if (config.angle === undefined) {
64-
config.angle = Math.PI / 2
101+
const subtype = subtypes[shape.subtype]
102+
const baseAngle = subtype.angle !== undefined ? subtype.angle : Math.PI / 2
103+
const offsetRadians = ((shape.angleOffset || 0) * Math.PI) / 180
104+
const config = {
105+
...subtype,
106+
iterations,
107+
side: 5,
108+
angle: baseAngle + offsetRadians,
109+
rng: seedrandom(shape.seed),
110+
branchProbability: (shape.lsystemBranchProbability ?? 100) / 100,
65111
}
66-
67112
const path = lsystemOptimize(lsystemPath(lsystem(config), config), config)
68113
const scale = 18.0 // to normalize starting size
69114

src/features/shapes/lsystem/subtypes.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,17 @@ export const subtypes = {
1010
startingAngle: Math.PI,
1111
maxIterations: 2,
1212
},
13+
// https://en.wikipedia.org/wiki/L-system#Example_7:_Fractal_plant
14+
"Anklet of Krishna": {
15+
axiom: "-X--X",
16+
draw: ["F"],
17+
rules: {
18+
X: "XFX--XFX",
19+
},
20+
angle: Math.PI / 4,
21+
minIterations: 2,
22+
maxIterations: 6,
23+
},
1324
// http://www.kevs3d.co.uk/dev/lsystems/
1425
"Cog Triangle": {
1526
axiom: "W----W----W",
@@ -78,6 +89,16 @@ export const subtypes = {
7889
angle: Math.PI / 9,
7990
maxIterations: 8,
8091
},
92+
"Fractal Tree 6": {
93+
axiom: "F",
94+
draw: ["F"],
95+
rules: {
96+
F: "FF+[+F-F-F]-[-F+F+F]",
97+
},
98+
angle: Math.PI / 8,
99+
minIterations: 2,
100+
maxIterations: 5,
101+
},
81102
// http://mathforum.org/advanced/robertd/lsys2d.html
82103
"Gosper (flowsnake)": {
83104
axiom: "A",
@@ -160,6 +181,17 @@ export const subtypes = {
160181
startingAngle: -Math.PI / 3,
161182
maxIterations: 5,
162183
},
184+
// https://en.wikipedia.org/wiki/L%C3%A9vy_C_curve
185+
"Lévy C Curve": {
186+
axiom: "F",
187+
draw: ["F"],
188+
rules: {
189+
F: "+F--F+",
190+
},
191+
angle: Math.PI / 4,
192+
minIterations: 6,
193+
maxIterations: 14,
194+
},
163195
// http://mathforum.org/advanced/robertd/lsys2d.html
164196
"McWorter's Pentadendrite": {
165197
axiom: "F-F-F-F-F",
@@ -223,4 +255,15 @@ export const subtypes = {
223255
},
224256
maxIterations: 8,
225257
},
258+
// https://en.wikipedia.org/wiki/Terdragon
259+
Terdragon: {
260+
axiom: "F",
261+
draw: ["F"],
262+
rules: {
263+
F: "F+F-F",
264+
},
265+
angle: (2 * Math.PI) / 3,
266+
minIterations: 3,
267+
maxIterations: 10,
268+
},
226269
}

0 commit comments

Comments
 (0)