فهرست منبع

Initial commit.

simondevyoutube 5 سال پیش
والد
کامیت
e37ad6bdbd
8فایلهای تغییر یافته به همراه1305 افزوده شده و 0 حذف شده
  1. 77 0
      base.css
  2. 88 0
      complex.html
  3. 96 0
      experimental.html
  4. 65 0
      simple.html
  5. 318 0
      src/complex.js
  6. 453 0
      src/main.js
  7. 60 0
      src/random.js
  8. 148 0
      src/simple.js

+ 77 - 0
base.css

@@ -0,0 +1,77 @@
+
+.column {
+  display: flex;
+  flex-direction: column;
+}
+
+.row {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  padding: 0.25em;
+}
+
+.center {
+  justify-content: center;
+}
+
+.bordered {
+  background-color: white;
+  border-radius: 0% 0% 0% 0% / 0% 0% 0% 0% ;
+  box-shadow: 5px 5px rgba(0, 0, 0, 0.1);
+  padding: 10px;
+}
+
+.col1 {
+  width: 125px;
+  max-width: 125px;
+  text-align: right;
+}
+
+.color {
+  margin: 0 20px;
+}
+
+p {
+  margin: 0;
+  font-family: 'Indie Flower', cursive;
+  font-family: 'Chau Philomene One', sans-serif;
+  font-size: 1em;
+}
+
+h1 {
+  margin: 0;
+  font-family: 'Indie Flower', cursive;
+  font-family: 'Chau Philomene One', sans-serif;
+  font-size: 2em;
+}
+
+body {
+  margin: 0;
+  padding: 0;
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+}
+
+body > div {
+  margin: 0;
+  padding: 0;
+  width: 100%;
+  height: 100%;
+}
+
+.sentence {
+  position: absolute;
+  top: 80px;
+  width: 100%;
+  height: 100%;
+}
+
+#sentenceText {
+  font-family: 'Indie Flower', cursive;
+  font-family: 'Chau Philomene One', sans-serif;
+  font-size: 4em;
+  text-shadow: 5px 5px rgba(0, 0, 0, 0.1);
+  line-height: normal;
+}

+ 88 - 0
complex.html

@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>L-Systems</title>
+  <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
+  <link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.indigo-pink.min.css">
+  <link rel="stylesheet" href="./base.css">
+  <link href="https://fonts.googleapis.com/css2?family=Indie+Flower&display=swap" rel="stylesheet">
+  <link href="https://fonts.googleapis.com/css2?family=Chau+Philomene+One&display=swap" rel="stylesheet">
+</head>
+<body>
+  <div class="row center">
+    <div class="column center bordered">
+      <div class="row">
+        <h1 class="col1">Background</h1>
+        <input id="background.color" class="color" type="color" name="body" value="#d8edf9"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Presets</p>
+        <input id="presets" class="mdl-slider mdl-js-slider" type="range" min="0" max="1" value="2"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Iterations</p>
+        <input id="iterations" class="mdl-slider mdl-js-slider" type="range" min="0" max="7" value="3"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Seed</p>
+        <input id="seed" class="mdl-slider mdl-js-slider" type="range" min="0" max="100" value="0"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Variability</p>
+        <input id="variability" class="mdl-slider mdl-js-slider" type="range" min="0.0" max="1.0" step="0.01" value="0.1"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Animate</p>
+        <input id="animate" class="color" type="checkbox" value="false"></input>
+      </div>
+      <div class="row">
+        <h1 class="col1">Leaves</h1>
+        <input id="leaf.color" class="color" type="color" name="body" value="#1c852b"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Type</p>
+        <input id="leaf.type" class="mdl-slider mdl-js-slider" type="range" min="0" max="4" value="0"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Repeat</p>
+        <input id="leaf.repeat" class="mdl-slider mdl-js-slider" type="range" min="1" max="10" value="1"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Length</p>
+        <input id="leaf.length" class="mdl-slider mdl-js-slider" type="range" min="0.1" max="10.0" value="5.0"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Width</p>
+        <input id="leaf.width" class="mdl-slider mdl-js-slider" type="range" min="0.5" max="40.0" value="2.0"></input>
+      </div>
+      <div class="row">
+        <h1 class="col1">Branches</h1>
+        <input id="branch.color" class="color" type="color" name="body" value="#85521c"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Alpha</p>
+        <input id="leaf.alpha" class="mdl-slider mdl-js-slider" type="range" min="0.01" max="1.0" value="0.75" step="0.01"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Length</p>
+        <input id="branch.length" class="mdl-slider mdl-js-slider" type="range" min="0.5" max="100.0" value="30.0"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Width</p>
+        <input id="branch.width" class="mdl-slider mdl-js-slider" type="range" min="0.1" max="40.0" value="10.0" step="0.1"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Angle</p>
+        <input id="branch.angle" class="mdl-slider mdl-js-slider" type="range" min="0.1" max="90.0" value="22.5" step="0.1"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Width Falloff</p>
+        <input id="branch.widthFalloff" class="mdl-slider mdl-js-slider" type="range" min="0.1" max="1.0" value="0.5" step="0.01"></input>
+      </div>
+    </div>
+    <canvas id="canvas" width="900px" height="1000px"></canvas>
+  </div>
+  <script src="./src/complex.js" type="module"></script>
+  <script defer src="https://code.getmdl.io/1.3.0/material.min.js"></script>
+</body>
+</html>

+ 96 - 0
experimental.html

@@ -0,0 +1,96 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>L-Systems</title>
+  <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
+  <link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.indigo-pink.min.css">
+  <link rel="stylesheet" href="./base.css">
+  <link href="https://fonts.googleapis.com/css2?family=Indie+Flower&display=swap" rel="stylesheet">
+  <link href="https://fonts.googleapis.com/css2?family=Chau+Philomene+One&display=swap" rel="stylesheet">
+</head>
+<body>
+  <div class="row center">
+    <div class="column center bordered">
+      <div class="row">
+        <h1 class="col1">Background</h1>
+        <input id="background.color" class="color" type="color" name="body" value="#d8edf9"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Presets</p>
+        <input id="presets" class="mdl-slider mdl-js-slider" type="range" min="0" max="5" value="0"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Iterations</p>
+        <input id="iterations" class="mdl-slider mdl-js-slider" type="range" min="1" max="7" value="3"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Seed</p>
+        <input id="seed" class="mdl-slider mdl-js-slider" type="range" min="0" max="100" value="0"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Variability</p>
+        <input id="variability" class="mdl-slider mdl-js-slider" type="range" min="0.0" max="1.0" step="0.01" value="0.1"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Animation Speed</p>
+        <input id="animation.speed" class="mdl-slider mdl-js-slider" type="range" min="0.01" max="1.0" step="0.01" value="1.0"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Animation Age</p>
+        <input id="animation.age" class="mdl-slider mdl-js-slider" type="range" min="0.01" max="1.0" step="0.01" value="1.0"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Animate</p>
+        <input id="animate" class="color" type="checkbox" value="false"></input>
+      </div>
+      <div class="row">
+        <h1 class="col1">Leaves</h1>
+        <input id="leaf.color" class="color" type="color" name="body" value="#1c852b"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Type</p>
+        <input id="leaf.type" class="mdl-slider mdl-js-slider" type="range" min="0" max="4" value="0"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Repeat</p>
+        <input id="leaf.repeat" class="mdl-slider mdl-js-slider" type="range" min="1" max="10" value="1"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Length</p>
+        <input id="leaf.length" class="mdl-slider mdl-js-slider" type="range" min="0.1" max="10.0" value="5.0"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Width</p>
+        <input id="leaf.width" class="mdl-slider mdl-js-slider" type="range" min="0.5" max="40.0" value="2.0"></input>
+      </div>
+      <div class="row">
+        <h1 class="col1">Branches</h1>
+        <input id="branch.color" class="color" type="color" name="body" value="#85521c"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Alpha</p>
+        <input id="leaf.alpha" class="mdl-slider mdl-js-slider" type="range" min="0.01" max="1.0" value="0.75" step="0.01"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Length</p>
+        <input id="branch.length" class="mdl-slider mdl-js-slider" type="range" min="0.5" max="100.0" value="30.0"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Width</p>
+        <input id="branch.width" class="mdl-slider mdl-js-slider" type="range" min="0.1" max="40.0" value="10.0" step="0.1"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Angle</p>
+        <input id="branch.angle" class="mdl-slider mdl-js-slider" type="range" min="0.1" max="90.0" value="22.5" step="0.1"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Width Falloff</p>
+        <input id="branch.widthFalloff" class="mdl-slider mdl-js-slider" type="range" min="0.1" max="1.0" value="0.5" step="0.01"></input>
+      </div>
+    </div>
+    <canvas id="canvas" width="1200px" height="1000px"></canvas>
+  </div>
+  <script src="./src/main.js" type="module"></script>
+  <script defer src="https://code.getmdl.io/1.3.0/material.min.js"></script>
+</body>
+</html>

+ 65 - 0
simple.html

@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>L-Systems</title>
+  <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
+  <link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.indigo-pink.min.css">
+  <link rel="stylesheet" href="./base.css">
+  <link href="https://fonts.googleapis.com/css2?family=Indie+Flower&display=swap" rel="stylesheet">
+  <link href="https://fonts.googleapis.com/css2?family=Chau+Philomene+One&display=swap" rel="stylesheet">
+</head>
+<body>
+  <div class="row center">
+    <div class="column center bordered">
+      <div class="row">
+        <p class="col1">Presets</p>
+        <input id="presets" class="mdl-slider mdl-js-slider" type="range" min="0" max="3" value="2" onchange="APP_OnChange()"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Iterations</p>
+        <input id="iterations" class="mdl-slider mdl-js-slider" type="range" min="1" max="5" value="3" onchange="APP_OnChange()"></input>
+      </div>
+      <div class="row">
+        <h1 class="col1">Leaves</h1>
+        <input id="leaf.color" class="color" type="color" name="body" value="#1c852b" onchange="APP_OnChange()"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Length</p>
+        <input id="leaf.length" class="mdl-slider mdl-js-slider" type="range" min="0.5" max="20.0" value="5.0" onchange="APP_OnChange()"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Width</p>
+        <input id="leaf.width" class="mdl-slider mdl-js-slider" type="range" min="0.5" max="20.0" value="2.0" onchange="APP_OnChange()"></input>
+      </div>
+      <div class="row">
+        <h1 class="col1">Branches</h1>
+        <input id="branch.color" class="color" type="color" name="body" value="#85521c" onchange="APP_OnChange()"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Alpha</p>
+        <input id="leaf.alpha" class="mdl-slider mdl-js-slider" type="range" min="0.01" max="1.0" value="0.75" step="0.01" onchange="APP_OnChange()"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Length</p>
+        <input id="branch.length" class="mdl-slider mdl-js-slider" type="range" min="0.5" max="100.0" value="30.0" onchange="APP_OnChange()"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Width</p>
+        <input id="branch.width" class="mdl-slider mdl-js-slider" type="range" min="0.1" max="10.0" value="1.0" step="0.1" onchange="APP_OnChange()"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Angle</p>
+        <input id="branch.angle" class="mdl-slider mdl-js-slider" type="range" min="0.1" max="90.0" value="22.5" step="0.1" onchange="APP_OnChange()"></input>
+      </div>
+      <div class="row">
+        <p class="col1">Falloff</p>
+        <input id="branch.lengthFalloff" class="mdl-slider mdl-js-slider" type="range" min="0.1" max="1.0" value="0.75" step="0.01" onchange="APP_OnChange()"></input>
+      </div>
+    </div>
+    <canvas id="canvas" width="1000px" height="1000px"></canvas>
+  </div>
+  <script src="./src/simple.js">
+  </script>
+  <script defer src="https://code.getmdl.io/1.3.0/material.min.js"></script>
+</body>
+</html>

+ 318 - 0
src/complex.js

@@ -0,0 +1,318 @@
+import {random} from './random.js';
+
+console.log('L-Systems Demo');
+
+let _APP = null;
+
+window.addEventListener('DOMContentLoaded', () => {
+  _APP = new LSystemDemo();
+
+  const inputs = document.querySelectorAll('input');
+  inputs.forEach(i => {
+    i.onchange = () => {
+      _APP.OnChange();
+    };
+  });
+});
+
+const _PRESETS = [
+  {
+    axiom: 'F',
+    rules: [
+      {symbol: 'F', odds: 0.33, newSymbols: 'F[+F]F[-F][F]'},
+      {symbol: 'F', odds: 0.33, newSymbols: 'F[+F][F]'},
+      {symbol: 'F', odds: 0.34, newSymbols: 'F[-F][F]'},
+    ]
+  },
+  {
+    axiom: 'X',
+    rules: [
+      {symbol: 'F', odds: 1.0, newSymbols: 'FF'},
+      {symbol: 'X', odds: 1.0, newSymbols: 'F+[-F-XF-X][+FF][--XF[+X]][++F-X]'},
+    ]
+  },
+  {
+    axiom: 'F',
+    rules: [
+      {symbol: 'F', odds: 1.0, newSymbols: 'FF+[+F-F-F]-[-F+F+F]'},
+    ]
+  },
+  {
+    axiom: 'X',
+    rules: [
+      {symbol: 'F', odds: 1.0, newSymbols: 'FX[FX[+XF]]'},
+      {symbol: 'X', odds: 1.0, newSymbols: 'FF[+XZ++X-F[+ZX]][-X++F-X]'},
+      {symbol: 'Z', odds: 1.0, newSymbols: '[+F-X-F][++ZX]'},
+    ]
+  },
+  {
+    axiom: 'F',
+    rules: [
+      {symbol: 'F', odds: 1.0, newSymbols: 'F[+F]F[-F]F'},
+    ]
+  },
+  {
+    axiom: 'X',
+    rules: [
+      {symbol: 'X', odds: 0.33, newSymbols: 'F[+X]F[-X]+X'},
+      {symbol: 'X', odds: 0.33, newSymbols: 'F[-X]F[-X]+X'},
+      {symbol: 'X', odds: 0.34, newSymbols: 'F[-X]F+X'},
+      {symbol: 'F', odds: 1.0, newSymbols: 'FF'},
+    ]
+  },
+  {
+    axiom: 'X',
+    rules: [
+      {symbol: 'X', odds: 1.0, newSymbols: 'F[-[[X]+X]]+F[+FX]-X'},
+      {symbol: 'F', odds: 1.0, newSymbols: 'FF'},
+    ]
+  },
+];
+
+
+function _RouletteSelection(rules) {
+  const roll = random.Random();
+  let sum = 0;
+  for (let r of rules) {
+    sum += r.odds;
+    if (roll < sum) {
+      return r;
+    }
+  }
+  return rules[sortedParents.length - 1];
+}
+
+
+class LSystemDemo {
+  constructor() {
+    document.getElementById('presets').max = _PRESETS.length - 1;
+
+    this._id = 0;
+
+    this.OnChange();
+  }
+
+  OnChange() {
+    this._UpdateFromUI();
+    this._ApplyRules();
+
+    if (this._animate) {
+      this._iterator = this._RenderAsync();
+      this._id++;
+  
+      // When we see that this changed, stop rendering.
+      const iteratorID = this._id;
+  
+      const _PumpIterator = () => {
+        if (this._id != iteratorID) {
+          return;
+        }
+  
+        const r = this._iterator.next();
+        if (!r.done) {
+          window.setTimeout(_PumpIterator, 0);
+        }
+      };
+      _PumpIterator();
+    } else {
+      this._iterator = this._RenderAsync();
+
+      while (!this._iterator.next().done);
+  }
+  }
+
+  _UpdateFromUI() {
+    const preset = document.getElementById('presets').valueAsNumber;
+    this._axiom = _PRESETS[preset].axiom;
+    this._rules = _PRESETS[preset].rules;
+    
+    this._backgroundColor = document.getElementById('background.color').value;
+    document.body.bgColor = this._backgroundColor;
+
+    this._iterations = document.getElementById('iterations').valueAsNumber;
+    this._animate = document.getElementById('animate').checked;
+    this._seed = document.getElementById('seed').value;
+    this._variability = document.getElementById('variability').valueAsNumber;
+    this._leafType = document.getElementById('leaf.type').valueAsNumber;
+    this._leafLength = document.getElementById('leaf.length').valueAsNumber;
+    this._leafWidth = document.getElementById('leaf.width').valueAsNumber;
+    this._leafColor = document.getElementById('leaf.color').value;
+    this._leafAlpha = document.getElementById('leaf.alpha').value;
+    this._leafRepeat = document.getElementById('leaf.repeat').value;
+    this._branchLength = document.getElementById('branch.length').valueAsNumber;
+    this._branchWidth = document.getElementById('branch.width').valueAsNumber;
+    this._branchAngle = document.getElementById('branch.angle').valueAsNumber;
+    this._branchColor = document.getElementById('branch.color').value;
+    this._branchWidthFalloff = document.getElementById('branch.widthFalloff').valueAsNumber;
+
+    random.Seed(this._seed);
+  }
+
+  _ApplyRulesToSentence(sentence) {
+    const newSentence = [];
+    for (let i = 0; i < sentence.length; i++) {
+      const [c, params] = sentence[i];
+
+      const matchingRules = [];
+      for (let rule of this._rules) {
+        if (c == rule.symbol) {
+          matchingRules.push(rule);
+        }
+      }
+      if (matchingRules.length > 0) {
+        const rule = _RouletteSelection(matchingRules);
+        newSentence.push(...rule.newSymbols.split('').map(
+            c => [c, this._CreateParameterizedSymbol(c, params)]));
+      } else {
+        newSentence.push([c, params]);
+      }
+    }
+    return newSentence;
+  }
+
+  _ApplyRules() {
+    let cur = [...this._axiom.split('').map(c => [c, this._CreateParameterizedSymbol(c, {})])];
+
+    for (let i = 0; i < this._iterations; i++) {
+      cur = this._ApplyRulesToSentence(cur);
+    }
+    this._sentence = cur;
+  }
+
+  _CreateParameterizedSymbol(c, params) {
+    if (c == 'F') {
+      const branchLengthMult = 1.0;
+      const randomLength = random.RandomRange(
+          this._branchLength * (1 - this._variability),
+          this._branchLength * (1 + this._variability));
+      const branchLength = branchLengthMult * randomLength;
+      return {
+        branchLength: branchLength,
+      };
+    } else if (c == '+' || c == '-') {
+      const baseAngle = this._branchAngle;
+      const randomAngleMult = random.RandomRange(
+          (1 - this._variability), (1 + this._variability))
+      const finalAngle = baseAngle * randomAngleMult;
+      return {
+        angle: finalAngle,
+      };
+    }
+
+    return {};
+  }
+
+  *_RenderAsync() {
+    const canvas = document.getElementById('canvas');
+    const ctx = canvas.getContext('2d');
+    ctx.resetTransform();
+    ctx.clearRect(0, 0, canvas.width, canvas.height);
+    ctx.transform(1, 0, 0, 1, canvas.width / 2, canvas.height);
+
+    const stateStack = [];
+    let currentState = {
+      width: this._branchWidth,
+    };
+
+    for (let i = 0; i < this._sentence.length; i++) {
+      yield;
+      const [c, params] = this._sentence[i];
+
+      if (c == 'F') {
+        ctx.fillStyle = this._branchColor;
+        const w1 = currentState.width;
+        currentState.width *= (1 - (1 - this._branchWidthFalloff) ** 3);
+        currentState.width = Math.max(this._branchWidth * 0.25, currentState.width);
+        const w2 = currentState.width;
+        const l = params.branchLength;
+
+        ctx.beginPath();
+        ctx.moveTo(-w2 / 2, -l - 1);
+        ctx.lineTo(-w1 / 2, 1);
+        ctx.lineTo(w1 / 2, 1);
+        ctx.lineTo(w2 / 2, -l - 1);
+        ctx.lineTo(-w2 / 2, -l - 1);
+        ctx.closePath();
+        ctx.fill();
+
+        ctx.transform(1, 0, 0, 1, 0, -l);
+      } else if (c == '+') {
+        const a = params.angle;
+        ctx.rotate(a * Math.PI / 180);
+      } else if (c == '-') {
+        const a = params.angle;
+        ctx.rotate(-a * Math.PI / 180);
+      } else if (c == '[') {
+        ctx.save();
+        stateStack.push({...currentState});
+      } else if (c == ']') {
+        ctx.fillStyle = this._leafColor;
+        ctx.strokeStyle = this._leafColor;
+        ctx.globalAlpha = this._leafAlpha;
+
+        const _DrawLeaf = () => {
+          ctx.save();
+
+          const leafWidth = random.RandomRange(
+              this._leafWidth * (1 - this._variability),
+              this._leafWidth * (1 + this._variability));
+          const leafLength = random.RandomRange(
+              this._leafLength * (1 - this._variability),
+              this._leafLength * (1 + this._variability));
+          ctx.scale(leafWidth, leafLength);
+          if (this._leafType == 0) {
+            ctx.beginPath();
+            ctx.moveTo(0, 0);
+            ctx.lineTo(1, -1);
+            ctx.lineTo(0, -4);
+            ctx.lineTo(-1, -1);
+            ctx.lineTo(0, 0);
+            ctx.closePath();
+            ctx.fill();
+            ctx.stroke();
+          } else if (this._leafType == 1) {
+            ctx.beginPath();
+            ctx.arc(0, -2, 2, 0, 2 * Math.PI);
+            ctx.closePath();
+            ctx.fill();
+            ctx.stroke();
+          } else if (this._leafType == 2) {
+            ctx.beginPath();
+            ctx.moveTo(0, 0);
+            ctx.lineTo(1, -1);
+            ctx.lineTo(1, -4);
+            ctx.lineTo(0, -5);
+            ctx.lineTo(-1, -4);
+            ctx.lineTo(-1, -1);
+            ctx.lineTo(0, 0);
+            ctx.closePath();
+            ctx.fill();
+            ctx.stroke();
+  
+            ctx.fillRect(0, 0, 0.25, -5);
+          }
+          ctx.restore();
+        }
+
+        _DrawLeaf();
+        if (this._leafRepeat > 1) {
+          ctx.save();
+          for (let r = 0; r < this._leafRepeat; r++) {
+            ctx.rotate((r + 1) * 5 * Math.PI / 180);
+            _DrawLeaf();
+          }
+          ctx.restore();
+          ctx.save();
+          for (let r = 0; r < this._leafRepeat; r++) {
+            ctx.rotate(-(r + 1) * 5 * Math.PI / 180);
+            _DrawLeaf();
+          }
+          ctx.restore();
+        }
+
+        ctx.restore();
+        currentState = stateStack.pop();
+      }
+    }
+  }
+};

+ 453 - 0
src/main.js

@@ -0,0 +1,453 @@
+import {random} from './random.js';
+
+console.log('L-Systems Demo');
+
+let _APP = null;
+
+window.addEventListener('DOMContentLoaded', () => {
+  _APP = new LSystemDemo();
+
+  const inputs = document.querySelectorAll('input');
+  inputs.forEach(i => {
+    i.onchange = () => {
+      _APP.OnChange();
+    };
+  });
+});
+
+
+const _PRESETS = [
+  {
+    axiom: 'F',
+    rules: [
+      {
+        symbol: 'F', odds: 1.0,
+        Iterate: (prev) => {
+          const newSymbolChars = 'F[+X]F[-X]X';
+          const symbols = newSymbolChars.split('').map(
+            c => ({symbol: c, params: {age: prev.params.age - 1}}));
+
+          symbols[0].params.age = prev.params.age;
+          symbols[10].params.age = prev.params.age;
+          return symbols;
+        },
+      },
+      {
+        symbol: 'X', odds: 1.0,
+        Iterate: (prev) => {
+          const newSymbolChars = 'F[+L]F[-L]L';
+          const symbols = newSymbolChars.split('').map(
+            c => ({symbol: c, params: {age: prev.params.age - 1}}));
+
+          symbols[0].params.age = prev.params.age;
+          symbols[10].params.age = prev.params.age;
+          return symbols;
+        },
+      },
+    ]
+  },
+  {
+    axiom: 'L',
+    rules: [
+      {
+        symbol: 'L', odds: 1.0,
+        Iterate: (prev) => {
+          const newSymbolChars = 'F[+L]F[-L]L';
+          const symbols = newSymbolChars.split('').map(
+            c => ({symbol: c, params: {age: prev.params.age - 1}}));
+
+          symbols[0].params.age = prev.params.age;
+          symbols[10].params.age = prev.params.age;
+          return symbols;
+        },
+      },
+    ]
+  },
+  {
+    axiom: 'L',
+    rules: [
+      {
+        symbol: 'L', odds: 0.33,
+        Iterate: (prev) => {
+          const newSymbolChars = 'F[+L]F[-L]+L';
+          const symbols = newSymbolChars.split('').map(
+            c => ({symbol: c, params: {age: prev.params.age - 1}}));
+
+            symbols[3].params.age = prev.params.age - 2;
+            symbols[8].params.age = prev.params.age - 2;
+
+            symbols[0].params.age = prev.params.age;
+            symbols[symbols.length - 1].params.age = prev.params.age;
+            symbols[symbols.length - 4].params.age = prev.params.age;
+
+          return symbols;
+        },
+      },
+      {
+        symbol: 'L', odds: 0.33,
+        Iterate: (prev) => {
+          const newSymbolChars = 'F[-L]F[-L]+L';
+          const symbols = newSymbolChars.split('').map(
+            c => ({symbol: c, params: {age: prev.params.age - 1}}));
+
+          symbols[3].params.age = prev.params.age - 2;
+          symbols[8].params.age = prev.params.age - 2;
+          
+          symbols[0].params.age = prev.params.age;
+          symbols[symbols.length - 1].params.age = prev.params.age;
+          symbols[symbols.length - 4].params.age = prev.params.age;
+
+          return symbols;
+        },
+      },
+      {
+        symbol: 'L', odds: 0.34,
+        Iterate: (prev) => {
+          const newSymbolChars = 'F[-L]F+L';
+          const symbols = newSymbolChars.split('').map(
+            c => ({symbol: c, params: {age: prev.params.age - 1}}));
+
+            symbols[3].params.age = prev.params.age - 2;
+
+            symbols[0].params.age = prev.params.age;
+            symbols[symbols.length - 1].params.age = prev.params.age;
+
+          return symbols;
+        },
+      },
+      {
+        symbol: 'F', odds: 1.0,
+        Iterate: (prev) => {
+          const newSymbolChars = 'FF';
+          const symbols = newSymbolChars.split('').map(
+            c => ({symbol: c, params: {age: prev.params.age - 1}}));
+
+          symbols[0].params.age = prev.params.age;
+
+          return symbols;
+        },
+      },
+    ]
+  },
+];
+
+
+function _RouletteSelection(rules) {
+  const roll = random.Random();
+  let sum = 0;
+  for (let r of rules) {
+    sum += r.odds;
+    if (roll < sum) {
+      return r;
+    }
+  }
+  return rules[sortedParents.length - 1];
+}
+
+
+class LSystemDemo {
+  constructor() {
+    document.getElementById('presets').max = _PRESETS.length - 1;
+
+    this._id = 0;
+
+    this.OnChange();
+  }
+
+  OnChange() {
+    this._UpdateFromUI();
+    this._ApplyRules();
+
+    // When we see that this changed, stop rendering.
+    this._id++;
+
+    const iteratorID = this._id;
+    this._animationTimeElapsed = 0.0;
+    this._totalAnimationTime = this._iterations * 20.0 / this._animationSpeed;
+    this._previousRAF = null;
+
+    if (this._animate) {
+      const _RAF = (t) => {
+        if (this._id != iteratorID) {
+          return;
+        }
+        if (this._previousRAF === null) {
+          this._previousRAF = t;
+        }
+
+        const timeInSeconds = (t - this._previousRAF) / 1000.0;
+        this._animationTimeElapsed += timeInSeconds;
+        this._Animate(timeInSeconds * this._animationSpeed);
+        this._previousRAF = t;
+
+        requestAnimationFrame((t) => {
+          _RAF(t);
+        });
+      };
+      requestAnimationFrame((t) => {
+        _RAF(t);
+      });
+    } else {
+      this._animationTimeElapsed = this._totalAnimationTime;
+      this._Animate(this._totalAnimationTime);
+    }
+  }
+
+  _UpdateFromUI() {
+    const preset = document.getElementById('presets').valueAsNumber;
+    this._axiom = _PRESETS[preset].axiom;
+    this._rules = _PRESETS[preset].rules;
+    
+    this._backgroundColor = document.getElementById('background.color').value;
+    document.body.bgColor = this._backgroundColor;
+
+    this._animate = document.getElementById('animate').checked;
+    this._animationSpeed = document.getElementById('animation.speed').valueAsNumber;
+    this._animationAgeSpeed = document.getElementById('animation.age').valueAsNumber;
+    this._iterations = document.getElementById('iterations').valueAsNumber;
+    this._seed = document.getElementById('seed').value;
+    this._variability = document.getElementById('variability').valueAsNumber;
+    this._leafType = document.getElementById('leaf.type').valueAsNumber;
+    this._leafLength = document.getElementById('leaf.length').valueAsNumber;
+    this._leafWidth = document.getElementById('leaf.width').valueAsNumber;
+    this._leafColor = document.getElementById('leaf.color').value;
+    this._leafAlpha = document.getElementById('leaf.alpha').value;
+    this._leafRepeat = document.getElementById('leaf.repeat').value;
+    this._branchLength = document.getElementById('branch.length').valueAsNumber;
+    this._branchWidth = document.getElementById('branch.width').valueAsNumber;
+    this._branchAngle = document.getElementById('branch.angle').valueAsNumber;
+    this._branchColor = document.getElementById('branch.color').value;
+    this._branchWidthFalloff = document.getElementById('branch.widthFalloff').valueAsNumber;
+
+    random.Seed(this._seed);
+  }
+
+  _ApplyRulesToSentence(sentence) {
+    const newSentence = [];
+    for (let i = 0; i < sentence.length; i++) {
+      const s = sentence[i];
+
+      const matchingRules = [];
+      for (let rule of this._rules) {
+        if (s.symbol == rule.symbol) {
+          matchingRules.push(rule);
+        }
+      }
+      if (matchingRules.length > 0) {
+        const rule = _RouletteSelection(matchingRules);
+        const newSymbols = rule.Iterate(s);
+        newSentence.push(...newSymbols.map(cur => this._CreateParameterizedSymbol(cur, s.params)))
+      } else {
+        newSentence.push(s);
+      }
+    }
+    return newSentence;
+  }
+
+  _ApplyRules() {
+    let cur = [...this._axiom.split('').map(c => this._CreateParameterizedSymbol({symbol: c}))];
+
+    for (let i = 0; i < this._iterations; i++) {
+      cur = this._ApplyRulesToSentence(cur);
+    }
+    this._sentence = cur;
+  }
+
+  _CreateParameterizedSymbol(c, params) {
+    let symbol = c;
+    if (!c.params) {
+      c.params = {age: 0.0};
+    }
+
+    if (c.symbol == 'F') {
+      const branchLengthMult = 1.0;
+      const randomLength = random.RandomRange(
+          this._branchLength * (1 - this._variability),
+          this._branchLength * (1 + this._variability));
+      const branchLength = branchLengthMult * randomLength;
+
+      symbol.params = {...symbol.params, ...{branchLength: branchLength}};
+    } else if (c.symbol == '+' || c.symbol == '-') {
+      const baseAngle = this._branchAngle;
+      const randomAngleMult = random.RandomRange(
+          (1 - this._variability), (1 + this._variability))
+      const finalAngle = baseAngle * randomAngleMult;
+
+      symbol.params = {...symbol.params, ...{angle: finalAngle}};
+    } else if (c.symbol == 'L') {
+      const leafWidth = random.RandomRange(
+        this._leafWidth * (1 - this._variability),
+        this._leafWidth * (1 + this._variability));
+      const leafLength = random.RandomRange(
+        this._leafLength * (1 - this._variability),
+        this._leafLength * (1 + this._variability));
+      symbol.params = {...symbol.params, ...{width: leafWidth, length: leafLength}};
+    }
+
+    return symbol;
+  }
+
+  _Animate(timeElapsed) {
+    const canvas = document.getElementById('canvas');
+    const ctx = canvas.getContext('2d');
+    ctx.resetTransform();
+    ctx.clearRect(0, 0, canvas.width, canvas.height);
+    ctx.transform(1, 0, 0, 1, canvas.width / 2, canvas.height);
+
+    for (let i = 0; i < this._sentence.length; i++) {
+      this._sentence[i].params.age += timeElapsed * this._animationAgeSpeed;
+    }
+
+    ctx.shadowBlur = 5;
+    ctx.shadowOffsetX = 2;
+    ctx.shadowOffsetY = 2;
+    ctx.shadowColor = '#000000';
+
+    this._RenderToContext(ctx);
+
+    ctx.shadowBlur = 0;
+    ctx.shadowOffsetX = 0;
+    ctx.shadowOffsetY = 0;
+    ctx.resetTransform();
+    ctx.transform(1, 0, 0, 1, canvas.width / 2, canvas.height);
+
+    this._RenderToContext(ctx);
+  }
+
+  _RenderToContext(ctx, timeElapsed) {
+    const stateStack = [];
+
+    const widthFactor = Math.max(0.0, (1.0 / (1.0 + Math.exp(-this._animationTimeElapsed / 10.0))) * 2 - 1);
+    const widthByAge = this._branchWidth * Math.max(0.25, widthFactor);
+
+    let currentState = {
+      width: widthByAge,
+    };
+
+    const leafFactor = 1.0;
+    const totalAgeFactor = Math.min(1.0, this._animationTimeElapsed / this._totalAnimationTime) ** 0.5;
+
+    for (let i = 0; i < this._sentence.length; i++) {
+      const s = this._sentence[i];
+      const c = s.symbol;
+      const params = s.params;
+
+      const ageFactor = Math.max(0.0, (1.0 / (1.0 + Math.exp(-params.age))) * 2 - 1);
+
+      if (c == 'F') {
+        ctx.fillStyle = this._branchColor;
+        ctx.strokeStyle = this._branchColor;
+        const w1 = currentState.width;
+        currentState.width *= (1 - (1 - this._branchWidthFalloff) ** 3);
+        currentState.width = Math.max(widthByAge * 0.25, currentState.width);
+        const w2 = currentState.width;
+        const l = params.branchLength * ageFactor;
+
+        if (ageFactor > 0) {
+          ctx.beginPath();
+          ctx.moveTo(-w2 / 2, -l);
+          ctx.lineTo(-w1 / 2, 1);
+          ctx.lineTo(w1 / 2, 1);
+          ctx.lineTo(w2 / 2, -l);
+          ctx.lineTo(-w2 / 2, -l);
+          ctx.closePath();
+          ctx.fill();
+  
+          ctx.globalAlpha = 0.2;
+          ctx.beginPath();
+          ctx.moveTo(-w2 / 2, -l);
+          ctx.lineTo(-w1 / 2, 0);
+          ctx.closePath();
+          ctx.stroke();
+  
+          ctx.beginPath();
+          ctx.moveTo(w1 / 2, 0);
+          ctx.lineTo(w2 / 2, -l);
+          ctx.closePath();
+          ctx.stroke();
+  
+          ctx.transform(1, 0, 0, 1, 0, -l);
+          ctx.globalAlpha = 1.0;
+        }
+      } else if (c == 'L') {
+        if (ageFactor > 0) {
+          ctx.fillStyle = this._leafColor;
+          ctx.strokeStyle = this._leafColor;
+          ctx.globalAlpha = this._leafAlpha;
+  
+          const _DrawLeaf = () => {
+            ctx.save();
+            ctx.scale(params.width * ageFactor * leafFactor, params.length * ageFactor * leafFactor);
+            if (this._leafType == 0) {
+              ctx.beginPath();
+              ctx.moveTo(0, 0);
+              ctx.lineTo(1, -1);
+              ctx.lineTo(0, -4);
+              ctx.lineTo(-1, -1);
+              ctx.lineTo(0, 0);
+              ctx.closePath();
+              ctx.fill();
+              ctx.stroke();
+            } else if (this._leafType == 1) {
+              ctx.beginPath();
+              ctx.arc(0, -2, 2, 0, 2 * Math.PI);
+              ctx.closePath();
+              ctx.fill();
+              ctx.stroke();
+            } else if (this._leafType == 2) {
+              ctx.beginPath();
+              ctx.moveTo(0, 0);
+              ctx.lineTo(1, -1);
+              ctx.lineTo(1, -4);
+              ctx.lineTo(0, -5);
+              ctx.lineTo(-1, -4);
+              ctx.lineTo(-1, -1);
+              ctx.lineTo(0, 0);
+              ctx.closePath();
+              ctx.fill();
+              ctx.stroke();
+    
+              ctx.fillRect(0, 0, 0.25, -5);
+            } else if (this._leafType == 3) {
+              ctx.beginPath();
+              ctx.arc(0, -2, 2, 0, 2 * Math.PI);
+              ctx.closePath();
+              ctx.fill();
+              ctx.stroke();
+            }
+            ctx.restore();
+          }
+  
+          _DrawLeaf();
+          if (this._leafRepeat > 1) {
+            ctx.save();
+            for (let r = 0; r < this._leafRepeat; r++) {
+              ctx.rotate((r + 1) * 5 * Math.PI / 180);
+              _DrawLeaf();
+            }
+            ctx.restore();
+            ctx.save();
+            for (let r = 0; r < this._leafRepeat; r++) {
+              ctx.rotate(-(r + 1) * 5 * Math.PI / 180);
+              _DrawLeaf();
+            }
+            ctx.restore();
+          }
+          ctx.globalAlpha = 1.0;
+        }
+      } else if (c == '+') {
+        const a = params.angle;
+        ctx.rotate(a * Math.PI / 180);
+      } else if (c == '-') {
+        const a = params.angle;
+        ctx.rotate(-a * Math.PI / 180);
+      } else if (c == '[') {
+        ctx.save();
+        stateStack.push({...currentState});
+      } else if (c == ']') {
+        ctx.restore();
+        currentState = stateStack.pop();
+      }
+    }
+  }
+};

+ 60 - 0
src/random.js

@@ -0,0 +1,60 @@
+// The reason we use our own random instead of Math.random() is because
+// we can seed this, and thus get the same L-System each time we view.
+// Otherwise, when you view the same L-System with the same parameters,
+// using Math.random(), it'll change each time.
+//
+// Code from https://stackoverflow.com/questions/521295/
+
+export const random = (function() {
+
+  function xmur3(str) {
+    for(var i = 0, h = 1779033703 ^ str.length; i < str.length; i++)
+        h = Math.imul(h ^ str.charCodeAt(i), 3432918353),
+        h = h << 13 | h >>> 19;
+    return function() {
+        h = Math.imul(h ^ h >>> 16, 2246822507);
+        h = Math.imul(h ^ h >>> 13, 3266489909);
+        return (h ^= h >>> 16) >>> 0;
+    }
+  }
+
+  function sfc32(a, b, c, d) {
+    return function() {
+      a >>>= 0; b >>>= 0; c >>>= 0; d >>>= 0; 
+      var t = (a + b) | 0;
+      a = b ^ b >>> 9;
+      b = c + (c << 3) | 0;
+      c = (c << 21 | c >>> 11);
+      d = d + 1 | 0;
+      t = t + d | 0;
+      c = c + t | 0;
+      return (t >>> 0) / 4294967296;
+    }
+  }
+
+  let _SeededRandom = null;
+
+  function _Random() {
+    if (!_SeededRandom) {
+      _Seed('abc');
+    }
+
+    return _SeededRandom();
+  }
+
+  function _RandomRange(a, b) {
+    return _Random() * (b - a) + a;
+  }
+
+  function _Seed(s) {
+    const seed = xmur3(s + '');
+    _SeededRandom = sfc32(seed(), seed(), seed(), seed());
+  }
+
+  return {
+    Seed: _Seed,
+    Random: _Random,
+    RandomRange: _RandomRange,
+  }
+})();
+

+ 148 - 0
src/simple.js

@@ -0,0 +1,148 @@
+console.log('L-Systems Demo');
+
+let _APP = null;
+
+window.addEventListener('DOMContentLoaded', () => {
+  _APP = new LSystemDemo();
+});
+
+
+const _PRESETS = [
+  {
+    axiom: 'X',
+    rules: [
+      ['F', 'FF'],
+      ['X', 'F+[-F-XF-X][+FF][--XF[+X]][++F-X]'],
+    ]
+  },
+  {
+    axiom: 'FX',
+    rules: [
+      ['F', 'FF+[+F-F-F]-[-F+F+F]'],
+    ]
+  },
+  {
+    axiom: 'X',
+    rules: [
+      ['F', 'FX[FX[+XF]]'],
+      ['X', 'FF[+XZ++X-F[+ZX]][-X++F-X]'],
+      ['Z', '[+F-X-F][++ZX]'],
+    ]
+  },
+  {
+    axiom: 'F',
+    rules: [
+      ['F', 'F > F[+F]F[-F]F'],
+    ]
+  },
+];
+
+class LSystemDemo {
+  constructor() {
+    this._sentence = this._axiom;
+    this._id = 0;
+
+    this.OnChange();
+  }
+
+  OnChange() {
+    this._UpdateFromUI();
+    this._ApplyRules();
+    this._Render();
+  }
+
+  _UpdateFromUI() {
+    const preset = document.getElementById('presets').valueAsNumber;
+    this._axiom = _PRESETS[preset].axiom;
+    this._rules = _PRESETS[preset].rules;
+    
+    this._iterations = document.getElementById('iterations').valueAsNumber;
+    this._leafLength = document.getElementById('leaf.length').valueAsNumber;
+    this._leafWidth = document.getElementById('leaf.width').valueAsNumber;
+    this._leafColor = document.getElementById('leaf.color').value;
+    this._leafAlpha = document.getElementById('leaf.alpha').value;
+    this._branchLength = document.getElementById('branch.length').valueAsNumber;
+    this._branchWidth = document.getElementById('branch.width').valueAsNumber;
+    this._branchAngle = document.getElementById('branch.angle').valueAsNumber;
+    this._branchColor = document.getElementById('branch.color').value;
+    this._branchLengthFalloff = document.getElementById('branch.lengthFalloff').value;
+  }
+
+  _FindMatchingRule(c) {
+    for (let rule of this._rules) {
+      if (c == rule[0]) {
+        return rule;
+      }
+    }
+    return null;
+  }
+
+  _ApplyRulesToSentence(sentence) {
+    let newSentence = '';
+    for (let i = 0; i < sentence.length; i++) {
+      const c = sentence[i];
+
+      const rule = this._FindMatchingRule(c);
+      if (rule) {
+        newSentence += rule[1];
+      } else {
+        newSentence += c;
+      }
+    }
+    return newSentence;
+  }
+
+  _ApplyRules() {
+    let cur = this._axiom;
+    for (let i = 0; i < this._iterations; i++) {
+      cur = this._ApplyRulesToSentence(cur);
+
+      this._branchLength *= this._branchLengthFalloff;
+    }
+    this._sentence = cur;
+  }
+
+  _Render() {
+    const canvas = document.getElementById('canvas');
+    const ctx = canvas.getContext('2d');
+    ctx.resetTransform();
+    ctx.clearRect(0, 0, canvas.width, canvas.height);
+    ctx.transform(1, 0, 0, 1, canvas.width / 2, canvas.height);
+
+    for (let i = 0; i < this._sentence.length; i++) {
+      const c = this._sentence[i];
+
+      if (c == 'F') {
+        ctx.fillStyle = this._branchColor;
+        ctx.fillRect(0, 0, this._branchWidth, -this._branchLength);
+        ctx.transform(1, 0, 0, 1, 0, -this._branchLength);
+      } else if (c == '+') {
+        ctx.rotate(this._branchAngle * Math.PI / 180);
+      } else if (c == '-') {
+        ctx.rotate(-this._branchAngle * Math.PI / 180);
+      } else if (c == '[') {
+        ctx.save();
+      } else if (c == ']') {
+        ctx.fillStyle = this._leafColor;
+        ctx.globalAlpha = this._leafAlpha;
+
+        ctx.scale(this._leafWidth, this._leafLength);
+
+        ctx.beginPath();
+        ctx.moveTo(0, 0);
+        ctx.lineTo(1, -1);
+        ctx.lineTo(0, -4);
+        ctx.lineTo(-1, -1);
+        ctx.lineTo(0, 0);
+        ctx.closePath();
+        ctx.fill();
+
+        ctx.restore();
+      }
+    }
+  }
+};
+
+function APP_OnChange() {
+  _APP.OnChange();
+}