├── LICENSE ├── base.css ├── complex.html ├── experimental.html ├── simple.html └── src ├── complex.js ├── main.js ├── random.js └── simple.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 simondevyoutube 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /base.css: -------------------------------------------------------------------------------- 1 | 2 | .column { 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | .row { 8 | display: flex; 9 | flex-direction: row; 10 | align-items: center; 11 | padding: 0.25em; 12 | } 13 | 14 | .center { 15 | justify-content: center; 16 | } 17 | 18 | .bordered { 19 | background-color: white; 20 | border-radius: 0% 0% 0% 0% / 0% 0% 0% 0% ; 21 | box-shadow: 5px 5px rgba(0, 0, 0, 0.1); 22 | padding: 10px; 23 | } 24 | 25 | .col1 { 26 | width: 125px; 27 | max-width: 125px; 28 | text-align: right; 29 | } 30 | 31 | .color { 32 | margin: 0 20px; 33 | } 34 | 35 | p { 36 | margin: 0; 37 | font-family: 'Indie Flower', cursive; 38 | font-family: 'Chau Philomene One', sans-serif; 39 | font-size: 1em; 40 | } 41 | 42 | h1 { 43 | margin: 0; 44 | font-family: 'Indie Flower', cursive; 45 | font-family: 'Chau Philomene One', sans-serif; 46 | font-size: 2em; 47 | } 48 | 49 | body { 50 | margin: 0; 51 | padding: 0; 52 | width: 100%; 53 | height: 100%; 54 | overflow: hidden; 55 | } 56 | 57 | body > div { 58 | margin: 0; 59 | padding: 0; 60 | width: 100%; 61 | height: 100%; 62 | } 63 | 64 | .sentence { 65 | position: absolute; 66 | top: 80px; 67 | width: 100%; 68 | height: 100%; 69 | } 70 | 71 | #sentenceText { 72 | font-family: 'Indie Flower', cursive; 73 | font-family: 'Chau Philomene One', sans-serif; 74 | font-size: 4em; 75 | text-shadow: 5px 5px rgba(0, 0, 0, 0.1); 76 | line-height: normal; 77 | } -------------------------------------------------------------------------------- /complex.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | L-Systems 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |

Background

16 | 17 |
18 |
19 |

Presets

20 | 21 |
22 |
23 |

Iterations

24 | 25 |
26 |
27 |

Seed

28 | 29 |
30 |
31 |

Variability

32 | 33 |
34 |
35 |

Animate

36 | 37 |
38 |
39 |

Leaves

40 | 41 |
42 |
43 |

Type

44 | 45 |
46 |
47 |

Repeat

48 | 49 |
50 |
51 |

Length

52 | 53 |
54 |
55 |

Width

56 | 57 |
58 |
59 |

Branches

60 | 61 |
62 |
63 |

Alpha

64 | 65 |
66 |
67 |

Length

68 | 69 |
70 |
71 |

Width

72 | 73 |
74 |
75 |

Angle

76 | 77 |
78 |
79 |

Width Falloff

80 | 81 |
82 |
83 | 84 |
85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /experimental.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | L-Systems 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |

Background

16 | 17 |
18 |
19 |

Presets

20 | 21 |
22 |
23 |

Iterations

24 | 25 |
26 |
27 |

Seed

28 | 29 |
30 |
31 |

Variability

32 | 33 |
34 |
35 |

Animation Speed

36 | 37 |
38 |
39 |

Animation Age

40 | 41 |
42 |
43 |

Animate

44 | 45 |
46 |
47 |

Leaves

48 | 49 |
50 |
51 |

Type

52 | 53 |
54 |
55 |

Repeat

56 | 57 |
58 |
59 |

Length

60 | 61 |
62 |
63 |

Width

64 | 65 |
66 |
67 |

Branches

68 | 69 |
70 |
71 |

Alpha

72 | 73 |
74 |
75 |

Length

76 | 77 |
78 |
79 |

Width

80 | 81 |
82 |
83 |

Angle

84 | 85 |
86 |
87 |

Width Falloff

88 | 89 |
90 |
91 | 92 |
93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | L-Systems 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |

Presets

16 | 17 |
18 |
19 |

Iterations

20 | 21 |
22 |
23 |

Leaves

24 | 25 |
26 |
27 |

Length

28 | 29 |
30 |
31 |

Width

32 | 33 |
34 |
35 |

Branches

36 | 37 |
38 |
39 |

Alpha

40 | 41 |
42 |
43 |

Length

44 | 45 |
46 |
47 |

Width

48 | 49 |
50 |
51 |

Angle

52 | 53 |
54 |
55 |

Falloff

56 | 57 |
58 |
59 | 60 |
61 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/complex.js: -------------------------------------------------------------------------------- 1 | import {random} from './random.js'; 2 | 3 | console.log('L-Systems Demo'); 4 | 5 | let _APP = null; 6 | 7 | window.addEventListener('DOMContentLoaded', () => { 8 | _APP = new LSystemDemo(); 9 | 10 | const inputs = document.querySelectorAll('input'); 11 | inputs.forEach(i => { 12 | i.onchange = () => { 13 | _APP.OnChange(); 14 | }; 15 | }); 16 | }); 17 | 18 | const _PRESETS = [ 19 | { 20 | axiom: 'F', 21 | rules: [ 22 | {symbol: 'F', odds: 0.33, newSymbols: 'F[+F]F[-F][F]'}, 23 | {symbol: 'F', odds: 0.33, newSymbols: 'F[+F][F]'}, 24 | {symbol: 'F', odds: 0.34, newSymbols: 'F[-F][F]'}, 25 | ] 26 | }, 27 | { 28 | axiom: 'X', 29 | rules: [ 30 | {symbol: 'F', odds: 1.0, newSymbols: 'FF'}, 31 | {symbol: 'X', odds: 1.0, newSymbols: 'F+[-F-XF-X][+FF][--XF[+X]][++F-X]'}, 32 | ] 33 | }, 34 | { 35 | axiom: 'F', 36 | rules: [ 37 | {symbol: 'F', odds: 1.0, newSymbols: 'FF+[+F-F-F]-[-F+F+F]'}, 38 | ] 39 | }, 40 | { 41 | axiom: 'X', 42 | rules: [ 43 | {symbol: 'F', odds: 1.0, newSymbols: 'FX[FX[+XF]]'}, 44 | {symbol: 'X', odds: 1.0, newSymbols: 'FF[+XZ++X-F[+ZX]][-X++F-X]'}, 45 | {symbol: 'Z', odds: 1.0, newSymbols: '[+F-X-F][++ZX]'}, 46 | ] 47 | }, 48 | { 49 | axiom: 'F', 50 | rules: [ 51 | {symbol: 'F', odds: 1.0, newSymbols: 'F[+F]F[-F]F'}, 52 | ] 53 | }, 54 | { 55 | axiom: 'X', 56 | rules: [ 57 | {symbol: 'X', odds: 0.33, newSymbols: 'F[+X]F[-X]+X'}, 58 | {symbol: 'X', odds: 0.33, newSymbols: 'F[-X]F[-X]+X'}, 59 | {symbol: 'X', odds: 0.34, newSymbols: 'F[-X]F+X'}, 60 | {symbol: 'F', odds: 1.0, newSymbols: 'FF'}, 61 | ] 62 | }, 63 | { 64 | axiom: 'X', 65 | rules: [ 66 | {symbol: 'X', odds: 1.0, newSymbols: 'F[-[[X]+X]]+F[+FX]-X'}, 67 | {symbol: 'F', odds: 1.0, newSymbols: 'FF'}, 68 | ] 69 | }, 70 | ]; 71 | 72 | 73 | function _RouletteSelection(rules) { 74 | const roll = random.Random(); 75 | let sum = 0; 76 | for (let r of rules) { 77 | sum += r.odds; 78 | if (roll < sum) { 79 | return r; 80 | } 81 | } 82 | return rules[sortedParents.length - 1]; 83 | } 84 | 85 | 86 | class LSystemDemo { 87 | constructor() { 88 | document.getElementById('presets').max = _PRESETS.length - 1; 89 | 90 | this._id = 0; 91 | 92 | this.OnChange(); 93 | } 94 | 95 | OnChange() { 96 | this._UpdateFromUI(); 97 | this._ApplyRules(); 98 | 99 | if (this._animate) { 100 | this._iterator = this._RenderAsync(); 101 | this._id++; 102 | 103 | // When we see that this changed, stop rendering. 104 | const iteratorID = this._id; 105 | 106 | const _PumpIterator = () => { 107 | if (this._id != iteratorID) { 108 | return; 109 | } 110 | 111 | const r = this._iterator.next(); 112 | if (!r.done) { 113 | window.setTimeout(_PumpIterator, 0); 114 | } 115 | }; 116 | _PumpIterator(); 117 | } else { 118 | this._iterator = this._RenderAsync(); 119 | 120 | while (!this._iterator.next().done); 121 | } 122 | } 123 | 124 | _UpdateFromUI() { 125 | const preset = document.getElementById('presets').valueAsNumber; 126 | this._axiom = _PRESETS[preset].axiom; 127 | this._rules = _PRESETS[preset].rules; 128 | 129 | this._backgroundColor = document.getElementById('background.color').value; 130 | document.body.bgColor = this._backgroundColor; 131 | 132 | this._iterations = document.getElementById('iterations').valueAsNumber; 133 | this._animate = document.getElementById('animate').checked; 134 | this._seed = document.getElementById('seed').value; 135 | this._variability = document.getElementById('variability').valueAsNumber; 136 | this._leafType = document.getElementById('leaf.type').valueAsNumber; 137 | this._leafLength = document.getElementById('leaf.length').valueAsNumber; 138 | this._leafWidth = document.getElementById('leaf.width').valueAsNumber; 139 | this._leafColor = document.getElementById('leaf.color').value; 140 | this._leafAlpha = document.getElementById('leaf.alpha').value; 141 | this._leafRepeat = document.getElementById('leaf.repeat').value; 142 | this._branchLength = document.getElementById('branch.length').valueAsNumber; 143 | this._branchWidth = document.getElementById('branch.width').valueAsNumber; 144 | this._branchAngle = document.getElementById('branch.angle').valueAsNumber; 145 | this._branchColor = document.getElementById('branch.color').value; 146 | this._branchWidthFalloff = document.getElementById('branch.widthFalloff').valueAsNumber; 147 | 148 | random.Seed(this._seed); 149 | } 150 | 151 | _ApplyRulesToSentence(sentence) { 152 | const newSentence = []; 153 | for (let i = 0; i < sentence.length; i++) { 154 | const [c, params] = sentence[i]; 155 | 156 | const matchingRules = []; 157 | for (let rule of this._rules) { 158 | if (c == rule.symbol) { 159 | matchingRules.push(rule); 160 | } 161 | } 162 | if (matchingRules.length > 0) { 163 | const rule = _RouletteSelection(matchingRules); 164 | newSentence.push(...rule.newSymbols.split('').map( 165 | c => [c, this._CreateParameterizedSymbol(c, params)])); 166 | } else { 167 | newSentence.push([c, params]); 168 | } 169 | } 170 | return newSentence; 171 | } 172 | 173 | _ApplyRules() { 174 | let cur = [...this._axiom.split('').map(c => [c, this._CreateParameterizedSymbol(c, {})])]; 175 | 176 | for (let i = 0; i < this._iterations; i++) { 177 | cur = this._ApplyRulesToSentence(cur); 178 | } 179 | this._sentence = cur; 180 | } 181 | 182 | _CreateParameterizedSymbol(c, params) { 183 | if (c == 'F') { 184 | const branchLengthMult = 1.0; 185 | const randomLength = random.RandomRange( 186 | this._branchLength * (1 - this._variability), 187 | this._branchLength * (1 + this._variability)); 188 | const branchLength = branchLengthMult * randomLength; 189 | return { 190 | branchLength: branchLength, 191 | }; 192 | } else if (c == '+' || c == '-') { 193 | const baseAngle = this._branchAngle; 194 | const randomAngleMult = random.RandomRange( 195 | (1 - this._variability), (1 + this._variability)) 196 | const finalAngle = baseAngle * randomAngleMult; 197 | return { 198 | angle: finalAngle, 199 | }; 200 | } 201 | 202 | return {}; 203 | } 204 | 205 | *_RenderAsync() { 206 | const canvas = document.getElementById('canvas'); 207 | const ctx = canvas.getContext('2d'); 208 | ctx.resetTransform(); 209 | ctx.clearRect(0, 0, canvas.width, canvas.height); 210 | ctx.transform(1, 0, 0, 1, canvas.width / 2, canvas.height); 211 | 212 | const stateStack = []; 213 | let currentState = { 214 | width: this._branchWidth, 215 | }; 216 | 217 | for (let i = 0; i < this._sentence.length; i++) { 218 | yield; 219 | const [c, params] = this._sentence[i]; 220 | 221 | if (c == 'F') { 222 | ctx.fillStyle = this._branchColor; 223 | const w1 = currentState.width; 224 | currentState.width *= (1 - (1 - this._branchWidthFalloff) ** 3); 225 | currentState.width = Math.max(this._branchWidth * 0.25, currentState.width); 226 | const w2 = currentState.width; 227 | const l = params.branchLength; 228 | 229 | ctx.beginPath(); 230 | ctx.moveTo(-w2 / 2, -l - 1); 231 | ctx.lineTo(-w1 / 2, 1); 232 | ctx.lineTo(w1 / 2, 1); 233 | ctx.lineTo(w2 / 2, -l - 1); 234 | ctx.lineTo(-w2 / 2, -l - 1); 235 | ctx.closePath(); 236 | ctx.fill(); 237 | 238 | ctx.transform(1, 0, 0, 1, 0, -l); 239 | } else if (c == '+') { 240 | const a = params.angle; 241 | ctx.rotate(a * Math.PI / 180); 242 | } else if (c == '-') { 243 | const a = params.angle; 244 | ctx.rotate(-a * Math.PI / 180); 245 | } else if (c == '[') { 246 | ctx.save(); 247 | stateStack.push({...currentState}); 248 | } else if (c == ']') { 249 | ctx.fillStyle = this._leafColor; 250 | ctx.strokeStyle = this._leafColor; 251 | ctx.globalAlpha = this._leafAlpha; 252 | 253 | const _DrawLeaf = () => { 254 | ctx.save(); 255 | 256 | const leafWidth = random.RandomRange( 257 | this._leafWidth * (1 - this._variability), 258 | this._leafWidth * (1 + this._variability)); 259 | const leafLength = random.RandomRange( 260 | this._leafLength * (1 - this._variability), 261 | this._leafLength * (1 + this._variability)); 262 | ctx.scale(leafWidth, leafLength); 263 | if (this._leafType == 0) { 264 | ctx.beginPath(); 265 | ctx.moveTo(0, 0); 266 | ctx.lineTo(1, -1); 267 | ctx.lineTo(0, -4); 268 | ctx.lineTo(-1, -1); 269 | ctx.lineTo(0, 0); 270 | ctx.closePath(); 271 | ctx.fill(); 272 | ctx.stroke(); 273 | } else if (this._leafType == 1) { 274 | ctx.beginPath(); 275 | ctx.arc(0, -2, 2, 0, 2 * Math.PI); 276 | ctx.closePath(); 277 | ctx.fill(); 278 | ctx.stroke(); 279 | } else if (this._leafType == 2) { 280 | ctx.beginPath(); 281 | ctx.moveTo(0, 0); 282 | ctx.lineTo(1, -1); 283 | ctx.lineTo(1, -4); 284 | ctx.lineTo(0, -5); 285 | ctx.lineTo(-1, -4); 286 | ctx.lineTo(-1, -1); 287 | ctx.lineTo(0, 0); 288 | ctx.closePath(); 289 | ctx.fill(); 290 | ctx.stroke(); 291 | 292 | ctx.fillRect(0, 0, 0.25, -5); 293 | } 294 | ctx.restore(); 295 | } 296 | 297 | _DrawLeaf(); 298 | if (this._leafRepeat > 1) { 299 | ctx.save(); 300 | for (let r = 0; r < this._leafRepeat; r++) { 301 | ctx.rotate((r + 1) * 5 * Math.PI / 180); 302 | _DrawLeaf(); 303 | } 304 | ctx.restore(); 305 | ctx.save(); 306 | for (let r = 0; r < this._leafRepeat; r++) { 307 | ctx.rotate(-(r + 1) * 5 * Math.PI / 180); 308 | _DrawLeaf(); 309 | } 310 | ctx.restore(); 311 | } 312 | 313 | ctx.restore(); 314 | currentState = stateStack.pop(); 315 | } 316 | } 317 | } 318 | }; 319 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import {random} from './random.js'; 2 | 3 | console.log('L-Systems Demo'); 4 | 5 | let _APP = null; 6 | 7 | window.addEventListener('DOMContentLoaded', () => { 8 | _APP = new LSystemDemo(); 9 | 10 | const inputs = document.querySelectorAll('input'); 11 | inputs.forEach(i => { 12 | i.onchange = () => { 13 | _APP.OnChange(); 14 | }; 15 | }); 16 | }); 17 | 18 | 19 | const _PRESETS = [ 20 | { 21 | axiom: 'F', 22 | rules: [ 23 | { 24 | symbol: 'F', odds: 1.0, 25 | Iterate: (prev) => { 26 | const newSymbolChars = 'F[+X]F[-X]X'; 27 | const symbols = newSymbolChars.split('').map( 28 | c => ({symbol: c, params: {age: prev.params.age - 1}})); 29 | 30 | symbols[0].params.age = prev.params.age; 31 | symbols[10].params.age = prev.params.age; 32 | return symbols; 33 | }, 34 | }, 35 | { 36 | symbol: 'X', odds: 1.0, 37 | Iterate: (prev) => { 38 | const newSymbolChars = 'F[+L]F[-L]L'; 39 | const symbols = newSymbolChars.split('').map( 40 | c => ({symbol: c, params: {age: prev.params.age - 1}})); 41 | 42 | symbols[0].params.age = prev.params.age; 43 | symbols[10].params.age = prev.params.age; 44 | return symbols; 45 | }, 46 | }, 47 | ] 48 | }, 49 | { 50 | axiom: 'L', 51 | rules: [ 52 | { 53 | symbol: 'L', odds: 1.0, 54 | Iterate: (prev) => { 55 | const newSymbolChars = 'F[+L]F[-L]L'; 56 | const symbols = newSymbolChars.split('').map( 57 | c => ({symbol: c, params: {age: prev.params.age - 1}})); 58 | 59 | symbols[0].params.age = prev.params.age; 60 | symbols[10].params.age = prev.params.age; 61 | return symbols; 62 | }, 63 | }, 64 | ] 65 | }, 66 | { 67 | axiom: 'L', 68 | rules: [ 69 | { 70 | symbol: 'L', odds: 0.33, 71 | Iterate: (prev) => { 72 | const newSymbolChars = 'F[+L]F[-L]+L'; 73 | const symbols = newSymbolChars.split('').map( 74 | c => ({symbol: c, params: {age: prev.params.age - 1}})); 75 | 76 | symbols[3].params.age = prev.params.age - 2; 77 | symbols[8].params.age = prev.params.age - 2; 78 | 79 | symbols[0].params.age = prev.params.age; 80 | symbols[symbols.length - 1].params.age = prev.params.age; 81 | symbols[symbols.length - 4].params.age = prev.params.age; 82 | 83 | return symbols; 84 | }, 85 | }, 86 | { 87 | symbol: 'L', odds: 0.33, 88 | Iterate: (prev) => { 89 | const newSymbolChars = 'F[-L]F[-L]+L'; 90 | const symbols = newSymbolChars.split('').map( 91 | c => ({symbol: c, params: {age: prev.params.age - 1}})); 92 | 93 | symbols[3].params.age = prev.params.age - 2; 94 | symbols[8].params.age = prev.params.age - 2; 95 | 96 | symbols[0].params.age = prev.params.age; 97 | symbols[symbols.length - 1].params.age = prev.params.age; 98 | symbols[symbols.length - 4].params.age = prev.params.age; 99 | 100 | return symbols; 101 | }, 102 | }, 103 | { 104 | symbol: 'L', odds: 0.34, 105 | Iterate: (prev) => { 106 | const newSymbolChars = 'F[-L]F+L'; 107 | const symbols = newSymbolChars.split('').map( 108 | c => ({symbol: c, params: {age: prev.params.age - 1}})); 109 | 110 | symbols[3].params.age = prev.params.age - 2; 111 | 112 | symbols[0].params.age = prev.params.age; 113 | symbols[symbols.length - 1].params.age = prev.params.age; 114 | 115 | return symbols; 116 | }, 117 | }, 118 | { 119 | symbol: 'F', odds: 1.0, 120 | Iterate: (prev) => { 121 | const newSymbolChars = 'FF'; 122 | const symbols = newSymbolChars.split('').map( 123 | c => ({symbol: c, params: {age: prev.params.age - 1}})); 124 | 125 | symbols[0].params.age = prev.params.age; 126 | 127 | return symbols; 128 | }, 129 | }, 130 | ] 131 | }, 132 | ]; 133 | 134 | 135 | function _RouletteSelection(rules) { 136 | const roll = random.Random(); 137 | let sum = 0; 138 | for (let r of rules) { 139 | sum += r.odds; 140 | if (roll < sum) { 141 | return r; 142 | } 143 | } 144 | return rules[sortedParents.length - 1]; 145 | } 146 | 147 | 148 | class LSystemDemo { 149 | constructor() { 150 | document.getElementById('presets').max = _PRESETS.length - 1; 151 | 152 | this._id = 0; 153 | 154 | this.OnChange(); 155 | } 156 | 157 | OnChange() { 158 | this._UpdateFromUI(); 159 | this._ApplyRules(); 160 | 161 | // When we see that this changed, stop rendering. 162 | this._id++; 163 | 164 | const iteratorID = this._id; 165 | this._animationTimeElapsed = 0.0; 166 | this._totalAnimationTime = this._iterations * 20.0 / this._animationSpeed; 167 | this._previousRAF = null; 168 | 169 | if (this._animate) { 170 | const _RAF = (t) => { 171 | if (this._id != iteratorID) { 172 | return; 173 | } 174 | if (this._previousRAF === null) { 175 | this._previousRAF = t; 176 | } 177 | 178 | const timeInSeconds = (t - this._previousRAF) / 1000.0; 179 | this._animationTimeElapsed += timeInSeconds; 180 | this._Animate(timeInSeconds * this._animationSpeed); 181 | this._previousRAF = t; 182 | 183 | requestAnimationFrame((t) => { 184 | _RAF(t); 185 | }); 186 | }; 187 | requestAnimationFrame((t) => { 188 | _RAF(t); 189 | }); 190 | } else { 191 | this._animationTimeElapsed = this._totalAnimationTime; 192 | this._Animate(this._totalAnimationTime); 193 | } 194 | } 195 | 196 | _UpdateFromUI() { 197 | const preset = document.getElementById('presets').valueAsNumber; 198 | this._axiom = _PRESETS[preset].axiom; 199 | this._rules = _PRESETS[preset].rules; 200 | 201 | this._backgroundColor = document.getElementById('background.color').value; 202 | document.body.bgColor = this._backgroundColor; 203 | 204 | this._animate = document.getElementById('animate').checked; 205 | this._animationSpeed = document.getElementById('animation.speed').valueAsNumber; 206 | this._animationAgeSpeed = document.getElementById('animation.age').valueAsNumber; 207 | this._iterations = document.getElementById('iterations').valueAsNumber; 208 | this._seed = document.getElementById('seed').value; 209 | this._variability = document.getElementById('variability').valueAsNumber; 210 | this._leafType = document.getElementById('leaf.type').valueAsNumber; 211 | this._leafLength = document.getElementById('leaf.length').valueAsNumber; 212 | this._leafWidth = document.getElementById('leaf.width').valueAsNumber; 213 | this._leafColor = document.getElementById('leaf.color').value; 214 | this._leafAlpha = document.getElementById('leaf.alpha').value; 215 | this._leafRepeat = document.getElementById('leaf.repeat').value; 216 | this._branchLength = document.getElementById('branch.length').valueAsNumber; 217 | this._branchWidth = document.getElementById('branch.width').valueAsNumber; 218 | this._branchAngle = document.getElementById('branch.angle').valueAsNumber; 219 | this._branchColor = document.getElementById('branch.color').value; 220 | this._branchWidthFalloff = document.getElementById('branch.widthFalloff').valueAsNumber; 221 | 222 | random.Seed(this._seed); 223 | } 224 | 225 | _ApplyRulesToSentence(sentence) { 226 | const newSentence = []; 227 | for (let i = 0; i < sentence.length; i++) { 228 | const s = sentence[i]; 229 | 230 | const matchingRules = []; 231 | for (let rule of this._rules) { 232 | if (s.symbol == rule.symbol) { 233 | matchingRules.push(rule); 234 | } 235 | } 236 | if (matchingRules.length > 0) { 237 | const rule = _RouletteSelection(matchingRules); 238 | const newSymbols = rule.Iterate(s); 239 | newSentence.push(...newSymbols.map(cur => this._CreateParameterizedSymbol(cur, s.params))) 240 | } else { 241 | newSentence.push(s); 242 | } 243 | } 244 | return newSentence; 245 | } 246 | 247 | _ApplyRules() { 248 | let cur = [...this._axiom.split('').map(c => this._CreateParameterizedSymbol({symbol: c}))]; 249 | 250 | for (let i = 0; i < this._iterations; i++) { 251 | cur = this._ApplyRulesToSentence(cur); 252 | } 253 | this._sentence = cur; 254 | } 255 | 256 | _CreateParameterizedSymbol(c, params) { 257 | let symbol = c; 258 | if (!c.params) { 259 | c.params = {age: 0.0}; 260 | } 261 | 262 | if (c.symbol == 'F') { 263 | const branchLengthMult = 1.0; 264 | const randomLength = random.RandomRange( 265 | this._branchLength * (1 - this._variability), 266 | this._branchLength * (1 + this._variability)); 267 | const branchLength = branchLengthMult * randomLength; 268 | 269 | symbol.params = {...symbol.params, ...{branchLength: branchLength}}; 270 | } else if (c.symbol == '+' || c.symbol == '-') { 271 | const baseAngle = this._branchAngle; 272 | const randomAngleMult = random.RandomRange( 273 | (1 - this._variability), (1 + this._variability)) 274 | const finalAngle = baseAngle * randomAngleMult; 275 | 276 | symbol.params = {...symbol.params, ...{angle: finalAngle}}; 277 | } else if (c.symbol == 'L') { 278 | const leafWidth = random.RandomRange( 279 | this._leafWidth * (1 - this._variability), 280 | this._leafWidth * (1 + this._variability)); 281 | const leafLength = random.RandomRange( 282 | this._leafLength * (1 - this._variability), 283 | this._leafLength * (1 + this._variability)); 284 | symbol.params = {...symbol.params, ...{width: leafWidth, length: leafLength}}; 285 | } 286 | 287 | return symbol; 288 | } 289 | 290 | _Animate(timeElapsed) { 291 | const canvas = document.getElementById('canvas'); 292 | const ctx = canvas.getContext('2d'); 293 | ctx.resetTransform(); 294 | ctx.clearRect(0, 0, canvas.width, canvas.height); 295 | ctx.transform(1, 0, 0, 1, canvas.width / 2, canvas.height); 296 | 297 | for (let i = 0; i < this._sentence.length; i++) { 298 | this._sentence[i].params.age += timeElapsed * this._animationAgeSpeed; 299 | } 300 | 301 | ctx.shadowBlur = 5; 302 | ctx.shadowOffsetX = 2; 303 | ctx.shadowOffsetY = 2; 304 | ctx.shadowColor = '#000000'; 305 | 306 | this._RenderToContext(ctx); 307 | 308 | ctx.shadowBlur = 0; 309 | ctx.shadowOffsetX = 0; 310 | ctx.shadowOffsetY = 0; 311 | ctx.resetTransform(); 312 | ctx.transform(1, 0, 0, 1, canvas.width / 2, canvas.height); 313 | 314 | this._RenderToContext(ctx); 315 | } 316 | 317 | _RenderToContext(ctx, timeElapsed) { 318 | const stateStack = []; 319 | 320 | const widthFactor = Math.max(0.0, (1.0 / (1.0 + Math.exp(-this._animationTimeElapsed / 10.0))) * 2 - 1); 321 | const widthByAge = this._branchWidth * Math.max(0.25, widthFactor); 322 | 323 | let currentState = { 324 | width: widthByAge, 325 | }; 326 | 327 | const leafFactor = 1.0; 328 | const totalAgeFactor = Math.min(1.0, this._animationTimeElapsed / this._totalAnimationTime) ** 0.5; 329 | 330 | for (let i = 0; i < this._sentence.length; i++) { 331 | const s = this._sentence[i]; 332 | const c = s.symbol; 333 | const params = s.params; 334 | 335 | const ageFactor = Math.max(0.0, (1.0 / (1.0 + Math.exp(-params.age))) * 2 - 1); 336 | 337 | if (c == 'F') { 338 | ctx.fillStyle = this._branchColor; 339 | ctx.strokeStyle = this._branchColor; 340 | const w1 = currentState.width; 341 | currentState.width *= (1 - (1 - this._branchWidthFalloff) ** 3); 342 | currentState.width = Math.max(widthByAge * 0.25, currentState.width); 343 | const w2 = currentState.width; 344 | const l = params.branchLength * ageFactor; 345 | 346 | if (ageFactor > 0) { 347 | ctx.beginPath(); 348 | ctx.moveTo(-w2 / 2, -l); 349 | ctx.lineTo(-w1 / 2, 1); 350 | ctx.lineTo(w1 / 2, 1); 351 | ctx.lineTo(w2 / 2, -l); 352 | ctx.lineTo(-w2 / 2, -l); 353 | ctx.closePath(); 354 | ctx.fill(); 355 | 356 | ctx.globalAlpha = 0.2; 357 | ctx.beginPath(); 358 | ctx.moveTo(-w2 / 2, -l); 359 | ctx.lineTo(-w1 / 2, 0); 360 | ctx.closePath(); 361 | ctx.stroke(); 362 | 363 | ctx.beginPath(); 364 | ctx.moveTo(w1 / 2, 0); 365 | ctx.lineTo(w2 / 2, -l); 366 | ctx.closePath(); 367 | ctx.stroke(); 368 | 369 | ctx.transform(1, 0, 0, 1, 0, -l); 370 | ctx.globalAlpha = 1.0; 371 | } 372 | } else if (c == 'L') { 373 | if (ageFactor > 0) { 374 | ctx.fillStyle = this._leafColor; 375 | ctx.strokeStyle = this._leafColor; 376 | ctx.globalAlpha = this._leafAlpha; 377 | 378 | const _DrawLeaf = () => { 379 | ctx.save(); 380 | ctx.scale(params.width * ageFactor * leafFactor, params.length * ageFactor * leafFactor); 381 | if (this._leafType == 0) { 382 | ctx.beginPath(); 383 | ctx.moveTo(0, 0); 384 | ctx.lineTo(1, -1); 385 | ctx.lineTo(0, -4); 386 | ctx.lineTo(-1, -1); 387 | ctx.lineTo(0, 0); 388 | ctx.closePath(); 389 | ctx.fill(); 390 | ctx.stroke(); 391 | } else if (this._leafType == 1) { 392 | ctx.beginPath(); 393 | ctx.arc(0, -2, 2, 0, 2 * Math.PI); 394 | ctx.closePath(); 395 | ctx.fill(); 396 | ctx.stroke(); 397 | } else if (this._leafType == 2) { 398 | ctx.beginPath(); 399 | ctx.moveTo(0, 0); 400 | ctx.lineTo(1, -1); 401 | ctx.lineTo(1, -4); 402 | ctx.lineTo(0, -5); 403 | ctx.lineTo(-1, -4); 404 | ctx.lineTo(-1, -1); 405 | ctx.lineTo(0, 0); 406 | ctx.closePath(); 407 | ctx.fill(); 408 | ctx.stroke(); 409 | 410 | ctx.fillRect(0, 0, 0.25, -5); 411 | } else if (this._leafType == 3) { 412 | ctx.beginPath(); 413 | ctx.arc(0, -2, 2, 0, 2 * Math.PI); 414 | ctx.closePath(); 415 | ctx.fill(); 416 | ctx.stroke(); 417 | } 418 | ctx.restore(); 419 | } 420 | 421 | _DrawLeaf(); 422 | if (this._leafRepeat > 1) { 423 | ctx.save(); 424 | for (let r = 0; r < this._leafRepeat; r++) { 425 | ctx.rotate((r + 1) * 5 * Math.PI / 180); 426 | _DrawLeaf(); 427 | } 428 | ctx.restore(); 429 | ctx.save(); 430 | for (let r = 0; r < this._leafRepeat; r++) { 431 | ctx.rotate(-(r + 1) * 5 * Math.PI / 180); 432 | _DrawLeaf(); 433 | } 434 | ctx.restore(); 435 | } 436 | ctx.globalAlpha = 1.0; 437 | } 438 | } else if (c == '+') { 439 | const a = params.angle; 440 | ctx.rotate(a * Math.PI / 180); 441 | } else if (c == '-') { 442 | const a = params.angle; 443 | ctx.rotate(-a * Math.PI / 180); 444 | } else if (c == '[') { 445 | ctx.save(); 446 | stateStack.push({...currentState}); 447 | } else if (c == ']') { 448 | ctx.restore(); 449 | currentState = stateStack.pop(); 450 | } 451 | } 452 | } 453 | }; 454 | -------------------------------------------------------------------------------- /src/random.js: -------------------------------------------------------------------------------- 1 | // The reason we use our own random instead of Math.random() is because 2 | // we can seed this, and thus get the same L-System each time we view. 3 | // Otherwise, when you view the same L-System with the same parameters, 4 | // using Math.random(), it'll change each time. 5 | // 6 | // Code from https://stackoverflow.com/questions/521295/ 7 | 8 | export const random = (function() { 9 | 10 | function xmur3(str) { 11 | for(var i = 0, h = 1779033703 ^ str.length; i < str.length; i++) 12 | h = Math.imul(h ^ str.charCodeAt(i), 3432918353), 13 | h = h << 13 | h >>> 19; 14 | return function() { 15 | h = Math.imul(h ^ h >>> 16, 2246822507); 16 | h = Math.imul(h ^ h >>> 13, 3266489909); 17 | return (h ^= h >>> 16) >>> 0; 18 | } 19 | } 20 | 21 | function sfc32(a, b, c, d) { 22 | return function() { 23 | a >>>= 0; b >>>= 0; c >>>= 0; d >>>= 0; 24 | var t = (a + b) | 0; 25 | a = b ^ b >>> 9; 26 | b = c + (c << 3) | 0; 27 | c = (c << 21 | c >>> 11); 28 | d = d + 1 | 0; 29 | t = t + d | 0; 30 | c = c + t | 0; 31 | return (t >>> 0) / 4294967296; 32 | } 33 | } 34 | 35 | let _SeededRandom = null; 36 | 37 | function _Random() { 38 | if (!_SeededRandom) { 39 | _Seed('abc'); 40 | } 41 | 42 | return _SeededRandom(); 43 | } 44 | 45 | function _RandomRange(a, b) { 46 | return _Random() * (b - a) + a; 47 | } 48 | 49 | function _Seed(s) { 50 | const seed = xmur3(s + ''); 51 | _SeededRandom = sfc32(seed(), seed(), seed(), seed()); 52 | } 53 | 54 | return { 55 | Seed: _Seed, 56 | Random: _Random, 57 | RandomRange: _RandomRange, 58 | } 59 | })(); 60 | 61 | -------------------------------------------------------------------------------- /src/simple.js: -------------------------------------------------------------------------------- 1 | console.log('L-Systems Demo'); 2 | 3 | let _APP = null; 4 | 5 | window.addEventListener('DOMContentLoaded', () => { 6 | _APP = new LSystemDemo(); 7 | }); 8 | 9 | 10 | const _PRESETS = [ 11 | { 12 | axiom: 'X', 13 | rules: [ 14 | ['F', 'FF'], 15 | ['X', 'F+[-F-XF-X][+FF][--XF[+X]][++F-X]'], 16 | ] 17 | }, 18 | { 19 | axiom: 'FX', 20 | rules: [ 21 | ['F', 'FF+[+F-F-F]-[-F+F+F]'], 22 | ] 23 | }, 24 | { 25 | axiom: 'X', 26 | rules: [ 27 | ['F', 'FX[FX[+XF]]'], 28 | ['X', 'FF[+XZ++X-F[+ZX]][-X++F-X]'], 29 | ['Z', '[+F-X-F][++ZX]'], 30 | ] 31 | }, 32 | { 33 | axiom: 'F', 34 | rules: [ 35 | ['F', 'F > F[+F]F[-F]F'], 36 | ] 37 | }, 38 | ]; 39 | 40 | class LSystemDemo { 41 | constructor() { 42 | this._sentence = this._axiom; 43 | this._id = 0; 44 | 45 | this.OnChange(); 46 | } 47 | 48 | OnChange() { 49 | this._UpdateFromUI(); 50 | this._ApplyRules(); 51 | this._Render(); 52 | } 53 | 54 | _UpdateFromUI() { 55 | const preset = document.getElementById('presets').valueAsNumber; 56 | this._axiom = _PRESETS[preset].axiom; 57 | this._rules = _PRESETS[preset].rules; 58 | 59 | this._iterations = document.getElementById('iterations').valueAsNumber; 60 | this._leafLength = document.getElementById('leaf.length').valueAsNumber; 61 | this._leafWidth = document.getElementById('leaf.width').valueAsNumber; 62 | this._leafColor = document.getElementById('leaf.color').value; 63 | this._leafAlpha = document.getElementById('leaf.alpha').value; 64 | this._branchLength = document.getElementById('branch.length').valueAsNumber; 65 | this._branchWidth = document.getElementById('branch.width').valueAsNumber; 66 | this._branchAngle = document.getElementById('branch.angle').valueAsNumber; 67 | this._branchColor = document.getElementById('branch.color').value; 68 | this._branchLengthFalloff = document.getElementById('branch.lengthFalloff').value; 69 | } 70 | 71 | _FindMatchingRule(c) { 72 | for (let rule of this._rules) { 73 | if (c == rule[0]) { 74 | return rule; 75 | } 76 | } 77 | return null; 78 | } 79 | 80 | _ApplyRulesToSentence(sentence) { 81 | let newSentence = ''; 82 | for (let i = 0; i < sentence.length; i++) { 83 | const c = sentence[i]; 84 | 85 | const rule = this._FindMatchingRule(c); 86 | if (rule) { 87 | newSentence += rule[1]; 88 | } else { 89 | newSentence += c; 90 | } 91 | } 92 | return newSentence; 93 | } 94 | 95 | _ApplyRules() { 96 | let cur = this._axiom; 97 | for (let i = 0; i < this._iterations; i++) { 98 | cur = this._ApplyRulesToSentence(cur); 99 | 100 | this._branchLength *= this._branchLengthFalloff; 101 | } 102 | this._sentence = cur; 103 | } 104 | 105 | _Render() { 106 | const canvas = document.getElementById('canvas'); 107 | const ctx = canvas.getContext('2d'); 108 | ctx.resetTransform(); 109 | ctx.clearRect(0, 0, canvas.width, canvas.height); 110 | ctx.transform(1, 0, 0, 1, canvas.width / 2, canvas.height); 111 | 112 | for (let i = 0; i < this._sentence.length; i++) { 113 | const c = this._sentence[i]; 114 | 115 | if (c == 'F') { 116 | ctx.fillStyle = this._branchColor; 117 | ctx.fillRect(0, 0, this._branchWidth, -this._branchLength); 118 | ctx.transform(1, 0, 0, 1, 0, -this._branchLength); 119 | } else if (c == '+') { 120 | ctx.rotate(this._branchAngle * Math.PI / 180); 121 | } else if (c == '-') { 122 | ctx.rotate(-this._branchAngle * Math.PI / 180); 123 | } else if (c == '[') { 124 | ctx.save(); 125 | } else if (c == ']') { 126 | ctx.fillStyle = this._leafColor; 127 | ctx.globalAlpha = this._leafAlpha; 128 | 129 | ctx.scale(this._leafWidth, this._leafLength); 130 | 131 | ctx.beginPath(); 132 | ctx.moveTo(0, 0); 133 | ctx.lineTo(1, -1); 134 | ctx.lineTo(0, -4); 135 | ctx.lineTo(-1, -1); 136 | ctx.lineTo(0, 0); 137 | ctx.closePath(); 138 | ctx.fill(); 139 | 140 | ctx.restore(); 141 | } 142 | } 143 | } 144 | }; 145 | 146 | function APP_OnChange() { 147 | _APP.OnChange(); 148 | } --------------------------------------------------------------------------------