├── 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 |
22 |
23 |
Iterations
24 |
25 |
26 |
30 |
31 |
Variability
32 |
33 |
34 |
38 |
39 |
Leaves
40 |
41 |
42 |
46 |
50 |
54 |
58 |
59 |
Branches
60 |
61 |
62 |
66 |
70 |
74 |
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 |
22 |
23 |
Iterations
24 |
25 |
26 |
30 |
31 |
Variability
32 |
33 |
34 |
35 |
Animation Speed
36 |
37 |
38 |
39 |
Animation Age
40 |
41 |
42 |
46 |
47 |
Leaves
48 |
49 |
50 |
54 |
58 |
62 |
66 |
67 |
Branches
68 |
69 |
70 |
74 |
78 |
82 |
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 |
18 |
19 |
Iterations
20 |
21 |
22 |
23 |
Leaves
24 |
25 |
26 |
30 |
34 |
35 |
Branches
36 |
37 |
38 |
42 |
46 |
50 |
54 |
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 | }
--------------------------------------------------------------------------------