├── .browserslistrc ├── .eslintrc.js ├── .gitignore ├── Examples.md ├── LICENSE ├── README.md ├── babel.config.js ├── deploy.sh ├── package-lock.json ├── package.json ├── public ├── android-icon-144x144.png ├── android-icon-192x192.png ├── android-icon-36x36.png ├── android-icon-48x48.png ├── android-icon-72x72.png ├── android-icon-96x96.png ├── apple-icon-114x114.png ├── apple-icon-120x120.png ├── apple-icon-144x144.png ├── apple-icon-152x152.png ├── apple-icon-180x180.png ├── apple-icon-57x57.png ├── apple-icon-60x60.png ├── apple-icon-72x72.png ├── apple-icon-76x76.png ├── apple-icon-precomposed.png ├── apple-icon.png ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon-96x96.png ├── favicon.ico ├── index.html ├── manifest.json ├── ms-icon-144x144.png ├── ms-icon-150x150.png ├── ms-icon-310x310.png └── ms-icon-70x70.png ├── src ├── App.vue ├── CodeEditor.vue ├── LSystem.js ├── LineCollection.js ├── NoWebGL.vue ├── Turtle.js ├── assets │ └── logo.png ├── bus.js ├── components │ └── HelloWorld.vue ├── createScene.js ├── editor.styl ├── getCodeModel.js ├── grammar │ └── parser.js ├── lmode.js ├── main.js └── shared.styl └── vue.config.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential', 8 | 'eslint:recommended' 9 | ], 10 | parserOptions: { 11 | parser: 'babel-eslint' 12 | }, 13 | ignorePatterns: [ 14 | "w-gl/*.js" 15 | ], 16 | rules: { 17 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 18 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 19 | 'no-unused-vars': 1, 20 | 'no-mixed-spaces-and-tabs': 0 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /Examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | These systems are generated by [Thomas Munoz](https://github.com/Epholys) and come from 4 | https://epholys.itch.io/lsys 5 | 6 | [Cog](https://anvaka.github.io/lsystem/?code=%2F%2F%20by%20Thomas%20Munoz%0A%2F%2F%20from%20https%3A%2F%2Fgithub.com%2FEpholys%2Fprocgen%0Aaxiom%3A%20F-F-F-F-F-F-F-F%0Arules%3A%20%0A%20F%20%3D%3E%20-F%2B%2BF-%0A%0Adepth%3A%205%0AstepsPerFrame%3A%2010%0Aangle%3A%2045%0A%0Aactions%3A%0A%20%20F%20%3D%3E%20draw%28%29) 7 | 8 | 9 | [Algae](https://anvaka.github.io/lsystem/?code=%2F%2F%20by%20Thomas%20Munoz%0A%2F%2F%20from%20https%3A%2F%2Fgithub.com%2FEpholys%2Fprocgen%0Aaxiom%3A%20F%0Arules%3A%20%0A%20F%20%3D%3E%20FF-%5B-F%2BF%2BF%5D%2BF%5B%2B%2BF-F-F%5D%0A%0Adepth%3A%205%0AstepsPerFrame%3A%201000%0Adirection%3A%20%5B0%2C%201%2C%200%5D%0Aangle%3A%2022.5%0A%0Aactions%3A%0A%20%20F%20%3D%3E%20draw%28%29) 10 | 11 | [Twilight](https://anvaka.github.io/lsystem/?code=%2F%2F%20by%20Thomas%20Munoz%0A%2F%2F%20from%20https%3A%2F%2Fgithub.com%2FEpholys%2Fprocgen%0Aaxiom%3A%20X%0Arules%3A%20%0A%20Y%20%3D%3E%20%5B%2BX%5D%0A%20F%20%3D%3E%20FF%0A%20X%20%3D%3E%20F%5B-FX%5D%5B%2BY%5D%5BFFY%5B--FX%5D%5BFX%5D%5B%2B%2BFX%5D%5DFYF%5B%2BFX%5D%0A%0Adepth%3A%205%0A%0AstepsPerFrame%3A%201000%0Adirection%3A%20%5B0%2C%201%2C%200%5D%0Aangle%3A%2016.5%0A%0Aactions%3A%0A%20%20F%20%3D%3E%20draw%28%29) 12 | 13 | [Mandala](https://anvaka.github.io/lsystem/?code=%2F%2F%20by%20Thomas%20Munoz%0A%2F%2F%20from%20https%3A%2F%2Fgithub.com%2FEpholys%2Fprocgen%0Aaxiom%3A%20X---------X-------X%0Arules%3A%20%0A%20%20X%20%3D%3E%20%5BG%5D%5B%2BG%5D%5B-G%5D%5B%2B%2BG%5D%5B--G%5D%5B%2B%2B%2BG%5D%5B---G%5D%5B%2B%2B%2B%2BG%5D%5B----G%5D%0A%20%20G%20%3D%3E%20GG%5B--G%5D%5B%2B%2BG%5D%0A%0Adepth%3A%206%0A%0AstepsPerFrame%3A%201000%0Adirection%3A%20%5B0%2C%201%2C%200%5D%0Aangle%3A%2015%0A%0Aactions%3A%0A%20%20G%20%3D%3E%20draw%28%29) 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2025 Andrei Kashcha 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # L-System 2 | 3 | This website allows you to edit [L-System](https://en.wikipedia.org/wiki/L-system) and immediately see results online: 4 | 5 | ![demo](https://i.imgur.com/z8YW0YK.png) 6 | 7 | https://anvaka.github.io/lsystem 8 | 9 | ## High level details 10 | 11 | The rendering is done with WebGL use can explore generated system similar to a map (with pan-zoom interaction). 12 | 13 | To change camera angle hold `Option` (or `Alt`) key and drag with mouse, or slide two fingers up/down. 14 | 15 | You can click `Randomize` button to generate a well-known L-System and tweak it. While `randomize` 16 | functionality provides basic L-Systems, I'd love to find a better way to introduce variety here. Please let 17 | me know if you have suggestions how to improve random L-System generation logic, so that it produces appealing 18 | results. 19 | 20 | ## Syntax/L-Systems details 21 | 22 | L-Systems are described very well on [Paul Bourke's](http://paulbourke.net/fractals/lsys/) website. 23 | 24 | ### Sections in the editor 25 | 26 | Each section can be entered in the text editor, followed by a semicolon. For example: 27 | 28 | ``` 29 | axiom: X 30 | rules: 31 | X => F+F 32 | ``` 33 | 34 | Here we entered two sections: `axiom` and `rules`. The list of available sections, along with their 35 | meanings is below: 36 | 37 | * `axiom` - initial state of the system 38 | * `rules` - list of rewrite rules that are applied on each iteration 39 | * `depth` - how deep we are allowed to go recursively 40 | * `angle` - if specified, this argument governs rotation angle. Can be overridden with actions 41 | * `actions` - list of graphic commands that are triggered by a matching character in the evolved system. 42 | * `width` - width in pixels of the drawn line 43 | * `color` - color of the line. Accepts names and hex. E.g. `blue`, works the same as `#0000ff` 44 | * `stepsPerFrame` - how many steps we are allowed to render per single frame. If set to -1 the scene is rendered immediately. This could be dangerous on deep systems, as the entire system traversal may exhaust the browser's resources. 45 | * `direction` - three numbers separated by coma `x, y, z` that set initial direction 46 | * `position` three numbers separated by coma `x, y, z` that set initial position 47 | 48 | ### Actions 49 | 50 | Actions are associated with each symbol in the `rules` section. When processor finds matching action 51 | it executes it. 52 | 53 | For example, let's say we have the following system: 54 | 55 | ``` 56 | axiom: X 57 | rules: 58 | X => F+FX 59 | 60 | depth: 4 61 | actions: 62 | F => draw(10) 63 | + => rotate(90) 64 | ``` 65 | 66 | Once the system is unwrapped, we get the following string: 67 | 68 | ``` 69 | F+FF+FF+FF+F 70 | ``` 71 | 72 | Each `F` has an associated action `draw(10)` which means "draw 10 units in current direction". 73 | Each `+` has an associated action `rotate(90)` which means "rotate 90 degrees". 74 | 75 | Can you guess what `F+FF+FF+FF+F` will render? To see the final result, [click here](https://anvaka.github.io/lsystem/?code=axiom%3A%20X%0Arules%3A%0A%20%20X%20%3D%3E%20F%2BFX%0A%0Adepth%3A%204%0Aactions%3A%0A%20%20F%20%3D%3E%20draw%2810%29%0A%20%20%2B%20%3D%3E%20rotate%2890%29) 76 | 77 | Here is the list of all available actions: 78 | 79 | `draw(x)` draw `x` units in current direction 80 | `move(x)` move `x` units in current direction without drawing 81 | `rotate(deg)` rotate current direction `deg` degrees around `Z` axis 82 | `rotateX(deg)` rotate current direction `deg` degrees around `X` axis 83 | `rotateY(deg)` rotate current direction `deg` degrees around `Y` axis 84 | `push()` saves current render state onto stack 85 | `pop()` restores previously saved render state 86 | 87 | By default the following actions are added automatically: 88 | actions: 89 | 90 | ``` 91 | F => draw(10) 92 | f => move(10) 93 | + => rotate(60) 94 | - => rotate(-60) 95 | [ => push() 96 | ] => pop() 97 | ``` 98 | 99 | 100 | ## Local development 101 | 102 | ``` 103 | npm install 104 | ``` 105 | 106 | ### Compiles and hot-reloads for development 107 | ``` 108 | npm start 109 | ``` 110 | 111 | ### Compiles and minifies for production 112 | ``` 113 | npm run build 114 | ``` 115 | 116 | # License 117 | 118 | [MIT](https://github.com/anvaka/lsystem/blob/master/LICENSE.md) -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | rm -rf ./dist 3 | npm run build 4 | cd ./dist 5 | git init 6 | git add . 7 | git commit -m 'push to gh-pages' 8 | git push --force git@github.com:anvaka/lsystem.git main:gh-pages 9 | cd ../ 10 | git tag `date "+release-%Y%m%d%H%M%S"` 11 | git push --tags 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "l-system", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "start": "vue-cli-service serve", 8 | "build": "vue-cli-service build", 9 | "lint": "vue-cli-service lint" 10 | }, 11 | "dependencies": { 12 | "codemirror": "^5.54.0", 13 | "core-js": "^3.6.4", 14 | "ngraph.events": "^1.2.1", 15 | "query-state": "^4.3.0", 16 | "stylus": "^0.54.7", 17 | "stylus-loader": "^3.0.2", 18 | "tinycolor2": "^1.4.1", 19 | "vue": "^2.6.11", 20 | "vue-codemirror-lite": "^1.0.4", 21 | "w-gl": "^0.21.0" 22 | }, 23 | "devDependencies": { 24 | "@vue/cli-plugin-babel": "~4.3.0", 25 | "@vue/cli-plugin-eslint": "~4.3.0", 26 | "@vue/cli-service": "~4.3.0", 27 | "babel-eslint": "^10.1.0", 28 | "eslint": "^6.7.2", 29 | "eslint-plugin-vue": "^6.2.2", 30 | "vue-template-compiler": "^2.6.11" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /public/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvaka/lsystem/7f0431d64d0b0cfc9ff2fb8416f0e797c36a4593/public/android-icon-144x144.png -------------------------------------------------------------------------------- /public/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvaka/lsystem/7f0431d64d0b0cfc9ff2fb8416f0e797c36a4593/public/android-icon-192x192.png -------------------------------------------------------------------------------- /public/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvaka/lsystem/7f0431d64d0b0cfc9ff2fb8416f0e797c36a4593/public/android-icon-36x36.png -------------------------------------------------------------------------------- /public/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvaka/lsystem/7f0431d64d0b0cfc9ff2fb8416f0e797c36a4593/public/android-icon-48x48.png -------------------------------------------------------------------------------- /public/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvaka/lsystem/7f0431d64d0b0cfc9ff2fb8416f0e797c36a4593/public/android-icon-72x72.png -------------------------------------------------------------------------------- /public/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvaka/lsystem/7f0431d64d0b0cfc9ff2fb8416f0e797c36a4593/public/android-icon-96x96.png -------------------------------------------------------------------------------- /public/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvaka/lsystem/7f0431d64d0b0cfc9ff2fb8416f0e797c36a4593/public/apple-icon-114x114.png -------------------------------------------------------------------------------- /public/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvaka/lsystem/7f0431d64d0b0cfc9ff2fb8416f0e797c36a4593/public/apple-icon-120x120.png -------------------------------------------------------------------------------- /public/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvaka/lsystem/7f0431d64d0b0cfc9ff2fb8416f0e797c36a4593/public/apple-icon-144x144.png -------------------------------------------------------------------------------- /public/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvaka/lsystem/7f0431d64d0b0cfc9ff2fb8416f0e797c36a4593/public/apple-icon-152x152.png -------------------------------------------------------------------------------- /public/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvaka/lsystem/7f0431d64d0b0cfc9ff2fb8416f0e797c36a4593/public/apple-icon-180x180.png -------------------------------------------------------------------------------- /public/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvaka/lsystem/7f0431d64d0b0cfc9ff2fb8416f0e797c36a4593/public/apple-icon-57x57.png -------------------------------------------------------------------------------- /public/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvaka/lsystem/7f0431d64d0b0cfc9ff2fb8416f0e797c36a4593/public/apple-icon-60x60.png -------------------------------------------------------------------------------- /public/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvaka/lsystem/7f0431d64d0b0cfc9ff2fb8416f0e797c36a4593/public/apple-icon-72x72.png -------------------------------------------------------------------------------- /public/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvaka/lsystem/7f0431d64d0b0cfc9ff2fb8416f0e797c36a4593/public/apple-icon-76x76.png -------------------------------------------------------------------------------- /public/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvaka/lsystem/7f0431d64d0b0cfc9ff2fb8416f0e797c36a4593/public/apple-icon-precomposed.png -------------------------------------------------------------------------------- /public/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvaka/lsystem/7f0431d64d0b0cfc9ff2fb8416f0e797c36a4593/public/apple-icon.png -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvaka/lsystem/7f0431d64d0b0cfc9ff2fb8416f0e797c36a4593/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvaka/lsystem/7f0431d64d0b0cfc9ff2fb8416f0e797c36a4593/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvaka/lsystem/7f0431d64d0b0cfc9ff2fb8416f0e797c36a4593/public/favicon-96x96.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvaka/lsystem/7f0431d64d0b0cfc9ff2fb8416f0e797c36a4593/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | L-System Explorer 29 | 47 | 48 | 49 | 52 |
53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "L System", 3 | "icons": [ 4 | { 5 | "src": "android-icon-36x36.png", 6 | "sizes": "36x36", 7 | "type": "image\/png", 8 | "density": "0.75" 9 | }, 10 | { 11 | "src": "android-icon-48x48.png", 12 | "sizes": "48x48", 13 | "type": "image\/png", 14 | "density": "1.0" 15 | }, 16 | { 17 | "src": "android-icon-72x72.png", 18 | "sizes": "72x72", 19 | "type": "image\/png", 20 | "density": "1.5" 21 | }, 22 | { 23 | "src": "android-icon-96x96.png", 24 | "sizes": "96x96", 25 | "type": "image\/png", 26 | "density": "2.0" 27 | }, 28 | { 29 | "src": "android-icon-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image\/png", 32 | "density": "3.0" 33 | }, 34 | { 35 | "src": "android-icon-192x192.png", 36 | "sizes": "192x192", 37 | "type": "image\/png", 38 | "density": "4.0" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /public/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvaka/lsystem/7f0431d64d0b0cfc9ff2fb8416f0e797c36a4593/public/ms-icon-144x144.png -------------------------------------------------------------------------------- /public/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvaka/lsystem/7f0431d64d0b0cfc9ff2fb8416f0e797c36a4593/public/ms-icon-150x150.png -------------------------------------------------------------------------------- /public/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvaka/lsystem/7f0431d64d0b0cfc9ff2fb8416f0e797c36a4593/public/ms-icon-310x310.png -------------------------------------------------------------------------------- /public/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvaka/lsystem/7f0431d64d0b0cfc9ff2fb8416f0e797c36a4593/public/ms-icon-70x70.png -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 110 | 111 | 251 | -------------------------------------------------------------------------------- /src/CodeEditor.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | -------------------------------------------------------------------------------- /src/LSystem.js: -------------------------------------------------------------------------------- 1 | import Turtle from './Turtle'; 2 | import tinycolor from 'tinycolor2'; 3 | 4 | // We don't want to crash the browser. If the expanded system is larger than this 5 | // we will stop iteration 6 | const LIMIT = 1000000; 7 | 8 | export default class LSystem { 9 | constructor(scene, systemSettings) { 10 | coerceTypes(systemSettings); 11 | this.rules = systemSettings.rules; 12 | this.actions = compileActions(systemSettings, this); 13 | this.turtle = new Turtle(scene, systemSettings); 14 | this.stepsPerFrame = systemSettings.stepsPerFrame || 42; 15 | this.drawQueue = []; 16 | this.lastDrawnIndex = -1; 17 | 18 | const depth = Number.isFinite(systemSettings.depth) ? systemSettings.depth : 5; 19 | let start = systemSettings.start; 20 | if (start === undefined) start = Object.keys(this.rules)[0]; 21 | if (start === undefined) throw new Error('System does not have neither rewrite rules nor start state'); 22 | this.start = start; 23 | this.depth = depth; 24 | this.simulate(); // TODO: Might exhaust system resources 25 | this.iterator = this.renderIterator(); 26 | } 27 | 28 | dispose() { 29 | this.turtle.dispose(); 30 | } 31 | 32 | frame() { 33 | let next, steps = 0; 34 | do { 35 | next = this.iterator.next(); 36 | } while (!next.done && steps++ < this.stepsPerFrame ); 37 | return !next.done; 38 | } 39 | 40 | simulate() { 41 | let production = this.start; 42 | let index = 0; 43 | while (index < this.depth && production.length < LIMIT) { 44 | production = this.iterateRules(production); 45 | index += 1; 46 | } 47 | 48 | this.production = production; 49 | this.complete = production.length < LIMIT; 50 | } 51 | 52 | iterateRules(rule) { 53 | let result = []; 54 | for (let op of rule) { 55 | let rewriteRule = this.rules[op]; 56 | if (rewriteRule !== undefined) result.push(rewriteRule); 57 | else result.push(op); 58 | } 59 | return result.join(''); 60 | } 61 | 62 | *renderIterator() { 63 | for (let op of this.production) { 64 | if (this.actions[op]) { 65 | if (this.stepsPerFrame < 0) { 66 | yield* this.actions[op](); 67 | } else { 68 | yield this.actions[op](); 69 | } 70 | } 71 | } 72 | } 73 | } 74 | 75 | function compileActions(systemSettings, lSystem) { 76 | let actions = {}; 77 | 78 | let defaultRotationAngle = Number.isFinite(systemSettings.angle) ? systemSettings.angle : 60; 79 | 80 | let mergedActions = { 81 | 'F': {name: 'draw', args: []}, 82 | 'f': {name: 'move', args: []}, 83 | '+': {name: 'rotate', args: [defaultRotationAngle]}, 84 | '-': {name: 'rotate', args: [-defaultRotationAngle]}, 85 | '&': {name: 'swapAngle', args: [-defaultRotationAngle]}, 86 | '[': {name: 'push', args: []}, 87 | ']': {name: 'pop', args: []}, 88 | ...systemSettings.actions 89 | } 90 | 91 | Object.keys(mergedActions).forEach(key => { 92 | let value = mergedActions[key]; 93 | let turtleAction = turtleCanDo(value, lSystem); 94 | if (turtleAction) { 95 | actions[key] = turtleAction; 96 | } else { 97 | throw new Error("Turtle does not know how to do '" + value + "'"); 98 | } 99 | }); 100 | 101 | return actions; 102 | } 103 | 104 | function turtleCanDo(command, lSystem) { 105 | if (command.name.indexOf('rotate') === 0) { 106 | let angle = command.args[0] 107 | if (!Number.isFinite(angle)) throw new Error('`rotate() needs one float argument'); 108 | 109 | switch (command.name) { 110 | case 'rotateX': return function() { lSystem.turtle.rotateX(angle) }; 111 | case 'rotateY': return function() { lSystem.turtle.rotateY(angle) }; 112 | } 113 | 114 | return function() { lSystem.turtle.rotateZ(angle) }; 115 | } 116 | 117 | if (command.name === 'draw') { 118 | let length = getLength(command.args[0], 'draw'); 119 | return function() { 120 | lSystem.turtle.draw(length) 121 | } 122 | } 123 | 124 | if (command.name.match(/chcolor|setColor/i)) { 125 | let color = command.args[0]; 126 | return function() { 127 | lSystem.turtle.setColor(color) 128 | } 129 | } 130 | 131 | if (command.name === 'push') return function() {lSystem.turtle.push()} 132 | if (command.name === 'pop') return function() {lSystem.turtle.pop()} 133 | if (command.name === 'move') { 134 | let length = getLength(command.args[0], 'move'); 135 | return function() {lSystem.turtle.move(length)} 136 | } 137 | if (command.name.match(/swapAngle/i)) { 138 | return function() {lSystem.turtle.swapAngle()} 139 | } 140 | } 141 | 142 | 143 | function getLength(value, name) { 144 | let length = 10; 145 | if (value !== undefined) { 146 | length = value; 147 | if (!Number.isFinite(length)) throw new Error(name +'(l) expects `l` to be a float number. Got: ' + value); 148 | } 149 | return length; 150 | } 151 | 152 | 153 | function coerceTypes(system) { 154 | if (system.angle !== undefined) system.angle = Number.parseFloat(system.angle); 155 | if (system.width !== undefined) system.width = Number.parseFloat(system.width); 156 | if (system.depth !== undefined) system.depth = Number.parseFloat(system.depth); 157 | if (system.color !== undefined) { 158 | let rgba = tinycolor(system.color).toRgb(); 159 | system.color = (rgba.r << 24) | (rgba.g << 16) | (rgba.b << 8) | (rgba.a * 255 | 0) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/LineCollection.js: -------------------------------------------------------------------------------- 1 | import {GLCollection, defineProgram, InstancedAttribute, ColorAttribute} from 'w-gl'; 2 | 3 | export default class LineCollection extends GLCollection { 4 | constructor(gl, options = {}) { 5 | let program = defineProgram({ 6 | gl, 7 | vertex: ` 8 | uniform mat4 modelViewProjection; 9 | uniform float width; 10 | uniform vec2 resolution; 11 | 12 | attribute vec4 color; 13 | attribute vec3 from, to; 14 | attribute vec2 point; 15 | 16 | varying vec4 vColor; 17 | varying vec2 vPoint; 18 | 19 | void main() { 20 | vec4 clip0 = modelViewProjection * vec4(from, 1.0); 21 | vec4 clip1 = modelViewProjection * vec4(to, 1.0); 22 | 23 | vec2 screen0 = resolution * (0.5 * clip0.xy/clip0.w + 0.5); 24 | vec2 screen1 = resolution * (0.5 * clip1.xy/clip1.w + 0.5); 25 | 26 | vec2 xBasis = normalize(screen1 - screen0); 27 | vec2 yBasis = vec2(-xBasis.y, xBasis.x); 28 | 29 | // Offset the original points: 30 | vec2 pt0 = screen0 + width * point.x * yBasis; 31 | vec2 pt1 = screen1 + width * point.x * yBasis; 32 | 33 | vec2 pt = mix(pt0, pt1, point.y); 34 | vec4 clip = mix(clip0, clip1, point.y); 35 | 36 | gl_Position = vec4(clip.w * (2.0 * pt/resolution - 1.0), clip.z, clip.w); 37 | vColor = color.abgr; // mix(.abgr, aToColor.abgr, aPosition.y); 38 | }`, 39 | 40 | fragment: ` 41 | precision highp float; 42 | varying vec4 vColor; 43 | 44 | void main() { 45 | gl_FragColor = vColor; 46 | }`, 47 | attributes: { 48 | color: new ColorAttribute() 49 | }, 50 | instanced: { 51 | point: new InstancedAttribute([ 52 | -0.5, 0, -0.5, 1, 0.5, 1, // First 2D triangle of the quad 53 | -0.5, 0, 0.5, 1, 0.5, 0 // Second 2D triangle of the quad 54 | ]) 55 | } 56 | }); 57 | super(program); 58 | this.width = options.width || 2; 59 | this.multiColorSegment = true; 60 | } 61 | 62 | draw(_, drawContext) { 63 | if (!this.uniforms) { 64 | this.uniforms = { 65 | modelViewProjection: this.modelViewProjection, 66 | width: this.width, 67 | resolution: [drawContext.width, drawContext.height] 68 | } 69 | } 70 | this.uniforms.resolution[0] = drawContext.width; 71 | this.uniforms.resolution[1] = drawContext.height; 72 | this.program.draw(this.uniforms); 73 | } 74 | 75 | // implement lineRenderTrait to allow SVG export via w-gl 76 | forEachLine(cb) { 77 | let count = this.program.getCount() 78 | for (let i = 0; i < count; ++i) { 79 | let vertex = this.program.get(i); 80 | let from = { x: vertex.from[0], y: vertex.from[1], z: vertex.from[2], color: vertex.color } 81 | let to = { x: vertex.to[0], y: vertex.to[1], z: vertex.to[2], color: vertex.color } 82 | cb(from, to); 83 | } 84 | } 85 | 86 | getLineColor(from) { 87 | let count = this.program.getCount() 88 | let c = from ? 89 | from.color : 90 | count > 0 ? this.program.get(0).color : 0xFFFFFFFF; 91 | 92 | return [ 93 | ((c >> 24) & 0xFF) / 255, 94 | ((c >> 16) & 0xFF) / 255, 95 | ((c >> 8) & 0xFF) / 255, 96 | ((c >> 0) & 0xFF) / 255, 97 | ] 98 | } 99 | } -------------------------------------------------------------------------------- /src/NoWebGL.vue: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /src/Turtle.js: -------------------------------------------------------------------------------- 1 | import LineCollection from './LineCollection'; 2 | import tinycolor from 'tinycolor2'; 3 | 4 | export default class Turtle { 5 | constructor(scene, options = {}) { 6 | this.position = options.position || [0, 0, 0]; 7 | checkArray('position', this.position); 8 | this.direction = options.direction || [1, 0, 0]; 9 | normalize(this.direction); 10 | checkArray('direction', this.direction); 11 | 12 | this.lines = new LineCollection(scene.getGL(), { capacity: 1024, width: options.width || 2 }); 13 | this.scene = scene; 14 | this.color = options.color || 0xFFFFFFFF; 15 | 16 | this.scene.appendChild(this.lines); 17 | this.stack = []; 18 | this.invertZYAngle = false; 19 | } 20 | 21 | push() { 22 | this.stack.push({ 23 | color: this.color, 24 | direction: [this.direction[0], this.direction[1], this.direction[2]], 25 | position: [this.position[0], this.position[1], this.position[2]], 26 | }) 27 | } 28 | 29 | pop() { 30 | let state = this.stack.pop(); 31 | this.color = state.color; 32 | this.direction = state.direction; 33 | this.position = state.position; 34 | } 35 | 36 | dispose() { 37 | this.scene.removeChild(this.lines); 38 | } 39 | 40 | move(distance) { 41 | let p = this.position; 42 | let n = this.direction; 43 | let x = p[0] + distance * n[0]; 44 | let y = p[1] + distance * n[1]; 45 | let z = p[2] + distance * n[2]; 46 | p[0] = x; p[1] = y; p[2] = z; 47 | } 48 | 49 | draw(distance) { 50 | let p = this.position; 51 | let n = this.direction; 52 | let x = p[0] + distance * n[0]; 53 | let y = p[1] + distance * n[1]; 54 | let z = p[2] + distance * n[2]; 55 | this.lines.add({ 56 | from: p, 57 | to: [x, y, z], 58 | color: this.color 59 | }); 60 | 61 | p[0] = x; p[1] = y; p[2] = z; 62 | this.scene.renderFrame(); 63 | } 64 | 65 | setColor(newColorValue) { 66 | if (newColorValue === undefined) { 67 | throw new Error('setColor() expects color value, got undefined'); 68 | } 69 | const rgba = tinycolor(typeof newColorValue === 'number' ? 70 | Object.values(tinycolor.names)[newColorValue] : newColorValue 71 | ).toRgb(); 72 | this.color = (rgba.r << 24) | (rgba.g << 16) | (rgba.b << 8) | (rgba.a * 255 | 0) 73 | } 74 | 75 | rotateZ(angleInDegrees) { 76 | if (this.invertZYAngle) angleInDegrees *= -1; 77 | let rad = Math.PI * angleInDegrees / 180; 78 | let n = this.direction; 79 | 80 | let x = Math.cos(rad) * n[0] - Math.sin(rad) * n[1]; 81 | let y = Math.sin(rad) * n[0] + Math.cos(rad) * n[1]; 82 | 83 | n[0] = x; 84 | n[1] = y; 85 | } 86 | 87 | rotateY(angleInDegrees) { 88 | if (this.invertZYAngle) angleInDegrees *= -1; 89 | let rad = Math.PI * angleInDegrees / 180; 90 | let n = this.direction; 91 | 92 | let x = Math.cos(rad) * n[0] - Math.sin(rad) * n[2]; 93 | let z = Math.sin(rad) * n[0] + Math.cos(rad) * n[2]; 94 | 95 | n[0] = x; 96 | n[2] = z; 97 | } 98 | 99 | rotateX(angleInDegrees) { 100 | let rad = Math.PI * angleInDegrees / 180; 101 | let n = this.direction; 102 | 103 | let y = Math.cos(rad) * n[1] - Math.sin(rad) * n[2]; 104 | let z = Math.sin(rad) * n[1] + Math.cos(rad) * n[2]; 105 | 106 | n[1] = y; 107 | n[2]= z; 108 | } 109 | 110 | swapAngle() { 111 | this.invertZYAngle = !this.invertZYAngle; 112 | } 113 | } 114 | 115 | function checkArray(arrayName, array, ) { 116 | if (!Array.isArray(array)) { 117 | throw new Error('Array is expected for `' + arrayName + '`'); 118 | } 119 | if (array.length === 2) { 120 | array.push(0); 121 | } 122 | if (array.length !== 3) { 123 | throw new Error('Array `' + arrayName + '` should have 2 or 3 coordinates, got ' + array.length); 124 | } 125 | } 126 | function normalize(arr) { 127 | let l = Math.hypot(arr[0], arr[1], arr[2]); 128 | arr[0] /= l; 129 | arr[1] /= l; 130 | arr[2] /= l; 131 | } 132 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvaka/lsystem/7f0431d64d0b0cfc9ff2fb8416f0e797c36a4593/src/assets/logo.png -------------------------------------------------------------------------------- /src/bus.js: -------------------------------------------------------------------------------- 1 | import eventify from 'ngraph.events'; 2 | const bus = eventify({}); 3 | export default bus; -------------------------------------------------------------------------------- /src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 41 | 42 | 43 | 59 | -------------------------------------------------------------------------------- /src/createScene.js: -------------------------------------------------------------------------------- 1 | import {createScene, createGuide, toSVG} from 'w-gl'; 2 | import LSystem from './LSystem'; 3 | 4 | export default function createLScene(canvas) { 5 | let scene = createScene(canvas); 6 | let guide = createGuide(scene, { 7 | lineColor: 0x0d3f71ff 8 | }); 9 | 10 | scene.setClearColor(12/255, 41/255, 82/255, 1) 11 | let initialSceneSize = 40; 12 | scene.setViewBox({ 13 | left: -initialSceneSize, 14 | top: -initialSceneSize, 15 | right: initialSceneSize, 16 | bottom: initialSceneSize, 17 | }); 18 | 19 | let canDrawMore = false; 20 | let lSystem = []; 21 | let disposeLater; 22 | let raf = requestAnimationFrame(frame); 23 | 24 | return { 25 | dispose, 26 | setSystem, 27 | saveToSVG, 28 | isComplete, 29 | } 30 | 31 | function saveToSVG(fileName) { 32 | let svg = toSVG(scene, { 33 | open() { 34 | return ``; 35 | } 36 | }); 37 | let blob = new Blob([svg], {type: "image/svg+xml"}); 38 | let url = window.URL.createObjectURL(blob); 39 | // For some reason, safari doesn't like when download happens on the same 40 | // event loop cycle. Pushing it to the next one. 41 | setTimeout(() => { 42 | let a = document.createElement("a"); 43 | a.href = url; 44 | a.download = fileName; 45 | a.click(); 46 | revokeLater(url); 47 | }, 30) 48 | } 49 | 50 | function revokeLater(url) { 51 | // In iOS immediately revoked URLs cause "WebKitBlobResource error 1." error 52 | // Setting a timeout to revoke URL in the future fixes the error: 53 | setTimeout(() => { 54 | window.URL.revokeObjectURL(url); 55 | }, 45000); 56 | } 57 | 58 | function setSystem(newSystem) { 59 | cancelAnimationFrame(raf); 60 | if (disposeLater) { 61 | lSystem.forEach(l => disposeLater.push(l)); 62 | } else { 63 | disposeLater = lSystem; 64 | } 65 | 66 | if (!Array.isArray(newSystem)) { 67 | newSystem = [newSystem] 68 | } 69 | let wantGuideToBeHidden = false; 70 | lSystem = []; 71 | newSystem.forEach(systemSettings => { 72 | if (systemSettings.hideGrid) { 73 | wantGuideToBeHidden = true; 74 | } 75 | lSystem.push(new LSystem(scene, systemSettings)); 76 | }); 77 | 78 | if (wantGuideToBeHidden && guide) { 79 | guide.dispose(); 80 | guide = null; 81 | } else if (!wantGuideToBeHidden && !guide) { 82 | guide = createGuide(scene); 83 | } 84 | 85 | raf = requestAnimationFrame(frame); 86 | } 87 | 88 | function isComplete() { 89 | return lSystem.every(x => x.complete); 90 | } 91 | 92 | function frame() { 93 | canDrawMore = false; 94 | lSystem.forEach(drawSystem); 95 | if (canDrawMore) { 96 | raf = requestAnimationFrame(frame); 97 | } 98 | if (disposeLater) { 99 | disposeLater.forEach(l => l.dispose()) 100 | disposeLater = null; 101 | } 102 | } 103 | 104 | function drawSystem(system) { 105 | canDrawMore |= system.frame(); 106 | } 107 | 108 | function dispose() { 109 | cancelAnimationFrame(raf); 110 | scene.dispose(); 111 | } 112 | } -------------------------------------------------------------------------------- /src/editor.styl: -------------------------------------------------------------------------------- 1 | // (C) Copyright (c) 2014 fergaldoyle 2 | // https://github.com/fergaldoyle/brackets-visual-studio-dark/blob/master/LICENSE 3 | // 4 | // Adjusted to vector fields need by https://github.com/anvaka 5 | 6 | @import "./shared.styl"; 7 | 8 | standardText = #DDD; 9 | maxHeight = 320px; 10 | 11 | .CodeMirror { 12 | height: auto; 13 | max-height: maxHeight; 14 | font-size: 14px; 15 | z-index: 0; 16 | background-color: window-background; 17 | border: 1px solid window-background; 18 | 19 | .CodeMirror-gutter-elt{ 20 | background: transparent; 21 | } 22 | 23 | .CodeMirror-scroll { 24 | height: auto; 25 | max-height: maxHeight; 26 | } 27 | .CodeMirror-linenumber, 28 | .CodeMirror-scroll, 29 | .CodeMirror-gutters { 30 | color: standardText; 31 | font-family: Consolas,'SourceCodePro-Medium',monaco,monospace; 32 | } 33 | 34 | .CodeMirror-linenumber { 35 | color: #2B91AF; 36 | } 37 | 38 | .CodeMirror-selected { 39 | background-color: #264F78; 40 | } 41 | 42 | .cm-matchhighlight { 43 | //background-color: #123E70; 44 | } 45 | 46 | .cm-matchingtag, 47 | .CodeMirror-matchingtag { 48 | background-color: #113D6F; 49 | //box-shadow: 0px 0px 2px #ADC0D3; 50 | } 51 | 52 | .cm-matchingbracket, 53 | .CodeMirror-matchingbracket { 54 | background-color: #113D6F; 55 | box-shadow: 0px 0px 3px #99B0C7; 56 | color: standardText !important; 57 | } 58 | 59 | 60 | .CodeMirror-cursor { 61 | border-left: 1px solid standardText; 62 | z-index: 3; 63 | } 64 | 65 | .CodeMirror-activeline-background { 66 | outline: 2px solid #dddddd; 67 | } 68 | // Languages 69 | // common 70 | .cm-m-lsystem, 71 | .cm-variable, 72 | .cm-variable-2, 73 | .cm-variable-3 { 74 | color: standardText; 75 | } 76 | 77 | .cm-pragma { 78 | font-weight: bold; 79 | color: #0afff8; 80 | } 81 | 82 | .cm-operator { 83 | color: #B4B4B4; 84 | } 85 | 86 | .cm-comment { 87 | color: #57A64A; 88 | font-style: italic; 89 | } 90 | 91 | .cm-string { 92 | color: #D69D85; 93 | } 94 | 95 | .cm-attribute { 96 | color: #9CDCFE; 97 | } 98 | 99 | .cm-number { 100 | color: #B5CEA8; 101 | } 102 | 103 | .cm-def { 104 | color: #4EC9B0; 105 | } 106 | 107 | .cm-meta { 108 | color: #9B9B9B; 109 | } 110 | 111 | .cm-keyword, 112 | .cm-tag, .cm-atom { 113 | color: #569CD6; 114 | } 115 | 116 | .cm-bracket { 117 | color: #808080; 118 | } 119 | 120 | .cm-m-clike.cm-builtin { 121 | color: #569CD6; 122 | } 123 | 124 | .cm-m-clike.cm-variable-2 { 125 | //color: #d26bca; 126 | } 127 | // search highlight 128 | .CodeMirror-searching { 129 | color: #ddd !important; 130 | } 131 | 132 | .CodeMirror-searching.searching-last, 133 | .CodeMirror-searching.searching-first { 134 | border-radius: 1px; 135 | background-color: #653306; 136 | box-shadow: 0px 0px 3px rgba(0, 0, 0, 0.8); 137 | padding: 1px 0; 138 | } 139 | 140 | .CodeMirror-searching.searching-current-match { 141 | background-color: #515C6A; 142 | } 143 | } 144 | 145 | .CodeMirror-focused { 146 | border: 1px dashed white; 147 | background: #13294f; 148 | } -------------------------------------------------------------------------------- /src/getCodeModel.js: -------------------------------------------------------------------------------- 1 | import parseExpression from './grammar/parser'; 2 | const queryState = require('query-state'); 3 | 4 | var qs = queryState({ 5 | code: getInitialCode() 6 | }, { 7 | useSearch: true 8 | }); 9 | 10 | // Create a Parser object from our grammar. 11 | let standardCollection = [ 12 | `// dragon curve 13 | axiom: X 14 | rules: 15 | X => X+YF+ 16 | Y => -FX-Y 17 | 18 | depth: 10 19 | angle: 90`, 20 | 21 | `// William McWorters Terdragon 22 | axiom: F 23 | rules: 24 | F => F+F-F 25 | 26 | depth: 8 27 | angle: 120`, 28 | 29 | `// William McWorters Pentl 30 | axiom: F-F-F-F-F 31 | rules: 32 | F => cF-F-F++dF+F-F 33 | 34 | depth: 4 35 | angle: 72 36 | actions: 37 | c => setColor("mediumpurple") 38 | d => setColor("violet") `, 39 | 40 | `// William McWorters Pentant 41 | axiom: X-X-X-X-X 42 | rules: 43 | F => 44 | X => dFX-FX-FX+FY+FY+FX-FX 45 | Y => cFY+FY-FX-FX-FY+FY+FY 46 | 47 | depth: 3 48 | angle: 72 49 | actions: 50 | c => setColor("goldenrod") 51 | d => setColor("gold") `, 52 | 53 | `// William McWorter Sierπnski Carpet 54 | axiom: F 55 | rules: 56 | F => cF+F-F-F-f+dF+F+F-F 57 | f => fff 58 | 59 | angle: 90 60 | depth: 4 61 | actions: 62 | c => setColor("goldenrod") 63 | d => setColor("gold") `, 64 | 65 | `// Hexagonal Gosper 66 | axiom: X 67 | rules: 68 | X => X+YF++YF-FX--FXFX-YF+ 69 | Y => -FX+YFYF++YF+FX--FX-Y 70 | 71 | angle: 60`, 72 | 73 | `// Peano curve 74 | axiom: X 75 | rules: 76 | X => XFYFX-F-YFXFY+F+XFYFX 77 | Y => YFXFY+F+XFYFX-F-YFXFY 78 | 79 | depth:4 80 | angle: 90`, 81 | 82 | `// Gary Teachout Pean-c 83 | axiom: FX 84 | rules: 85 | F => 86 | X => FX-FY-FX+FY+FX+FY+FX+FY+FX-FY-FX-FY-FX-FY-FX+FY+FX 87 | Y => FY 88 | 89 | depth: 3 90 | angle: 45`, 91 | 92 | `// Square Sierpinski 93 | axiom: F+XF+F+XF 94 | rules: 95 | X => XF-F+F-XF+F+XF-F+F-X 96 | 97 | depth: 4 98 | angle: 90 `, 99 | 100 | `// Tree 101 | axiom: X 102 | rules: 103 | F => FF 104 | X => F-[[X]+X]+F[+FX]-X 105 | 106 | direction: [0, 1, 0] 107 | angle: 22.5`, 108 | `// Tree with color 109 | axiom: X 110 | rules: 111 | F => FF 112 | X => F-[[X]+X]+cF[+dFX]-X 113 | 114 | color:brown 115 | direction: [0, 1, 0] 116 | angle: 22.5a 117 | actions: 118 | c => setColor('green') 119 | d => setColor('lightgreen')`, 120 | 121 | `// Bush, after P. Bourke 122 | axiom: Y 123 | rules: 124 | X => X[-FFF]c[+FFF]FX 125 | Y => dYFXe[+Y][-Y] 126 | 127 | color: brown 128 | direction: [0, 1, 0] 129 | angle: 22.5 130 | actions: 131 | c => setColor('green') 132 | d => setColor('lime') 133 | e => setColor('brown')`, 134 | 135 | ` // Grains, after P. Bourke 136 | axiom: Y 137 | rules: 138 | X => dX[-F+FF]cg[+F-FF]dFX 139 | Y => eYFX[+Y][-Y] 140 | 141 | depth:5 142 | direction: [0, 1, 0] 143 | angle: 27 144 | actions: 145 | c => setColor('green') 146 | d => setColor('lime') 147 | e => setColor('lightgreen') 148 | g => rotate(0.3)`, 149 | 150 | ` // Grains 2, after P. Bourke 151 | axiom: Y 152 | rules: 153 | X => cX[-FF-F]g[+FF+F]dFX 154 | Y => eYFX[+Y][-Y]XFY 155 | 156 | depth:4 157 | direction: [0, 1, 0] 158 | angle: 27 159 | actions: 160 | c => setColor('green') 161 | d => setColor('lime') 162 | e => setColor('gold') 163 | g => rotate(0.2)`, 164 | 165 | `// Bush, P. Bourke 166 | axiom: F 167 | rules: 168 | F => FF+[c+F-F-F]-[-F+F+dF] 169 | 170 | color: green 171 | direction: [0, 1, 0] 172 | angle: 21 173 | depth:4 174 | actions: 175 | c => setColor('green') 176 | d => setColor('lime')`, 177 | 178 | `// P. Bourkes Pentaplexy 179 | axiom: F++F++F++F++F 180 | rules: 181 | F => cF++F++F+++++dF-F++F 182 | 183 | depth:3 184 | angle: 36 185 | actions: 186 | c => setColor('mediumpurple') 187 | d => setColor('violet')`, 188 | 189 | `// poetasters shrub 190 | axiom: F 191 | rules: 192 | F => Fe[+cFF]Fd[-FF]cF 193 | 194 | color:brown 195 | direction: [0, 1, 0] 196 | angle: 322 197 | depth: 4 198 | actions: 199 | c => setColor('green') 200 | d => setColor('lightgreen') 201 | e => setColor('brown')`, 202 | 203 | `// poetasters weed, after, P. Bourke 204 | axiom: F 205 | rules: 206 | F -> F-[XY]+[XY]F+[XY]-[XY] 207 | X -> +dFY 208 | Y -> -cFX 209 | 210 | color: brown 211 | direction: [0, 1, 0.5] 212 | angle: 22.5 213 | depth:5 214 | actions: 215 | c => setColor('green') 216 | d => setColor('lime')`, 217 | 218 | `// Unlikely bush (after Bourke) 219 | axiom: F 220 | rules: 221 | F => eF[+cFF][-FF]cF[-F]d[+F]F 222 | 223 | color: brown 224 | direction: [0, 1, 0] 225 | angle: 330 226 | depth:3 227 | actions: 228 | c => setColor('green') 229 | d => setColor('lime') 230 | e => setColor('brown')`, 231 | 232 | `// Weed, P. Bourke 233 | axiom: F 234 | rules: 235 | F -> FF-[XY]+[XY] 236 | X -> +cFY 237 | Y -> -dFX 238 | 239 | color: brown 240 | direction: [0, 1, 1] 241 | angle: 22.5 242 | actions: 243 | c => setColor('green') 244 | d => setColor('lime')`, 245 | 246 | `// P. Bourke after Saupe 247 | axiom: VZFFF 248 | rules: 249 | V -> [+++W][---W]YV 250 | W -> +X[-W]Z 251 | X -> -W[+X]Z 252 | Y -> YZ 253 | Z -> [-FcFF][+FdFF]F 254 | 255 | color: green 256 | depth:8 257 | direction: [0, 1, 0] 258 | angle: 20 259 | actions: 260 | c => setColor('lightgreen') 261 | d => setColor('lime')`, 262 | 263 | `// poetaster's curly, inspired by McWorter 264 | axiom: F+F-F+F 265 | rules: 266 | F => eF-F-F++[cF+F-dF[GGG]][GGG] 267 | G => c--g--g--g--g--g--g--g 268 | 269 | depth: 3 270 | angle: 17 271 | width:2 272 | direction: [-0.5,0.7,-0.5] 273 | actions: 274 | c => setColor("palegreen") 275 | d => setColor("violet") 276 | e => setColor("green") 277 | g => draw(2)`, 278 | 279 | `// aquatic plant 280 | axiom: F 281 | rules: 282 | F -> FFc[-F++F]d[+F--F]e++F--F 283 | 284 | color: brown 285 | direction: [0, 1, 0.5] 286 | angle: 27 287 | depth:4 288 | actions: 289 | c => setColor('green') 290 | d => setColor('lime') 291 | e => setColor('goldenrod')`, 292 | 293 | `// another aquatic 294 | axiom: F 295 | rules: 296 | F => FMNOMBxPNMyO 297 | M => e[-F++F++] 298 | N => d[+F--F--] 299 | O => c++F--F 300 | P => d--F++F 301 | 302 | color: brown 303 | direction: [0, 1, -1] 304 | angle: 27 305 | depth:5 306 | actions: 307 | c => setColor('green') 308 | d => setColor('lime') 309 | e => setColor('goldenrod') 310 | x => rotateX(2) 311 | y => rotateY(-3)`, 312 | 313 | `// poetasters third aquatic 314 | axiom: F 315 | rules: 316 | F -> FMNxQRyQR[O-O-O-O-0] 317 | M => d[++FF+FF+] 318 | N => d[--FF-FF-] 319 | O => e[F-F-F++dF+F-F] 320 | Q => c++F--F 321 | R => c--F++F 322 | 323 | color: brown 324 | direction: [0, 1, -1] 325 | angle: 17 326 | depth:4 327 | actions: 328 | c => setColor('green') 329 | d => setColor('lime') 330 | e => setColor('goldenrod') 331 | x => rotateX(2) 332 | y => rotateY(-1.5)`, 333 | 334 | `// poetasters sallow thorn 335 | axiom: F 336 | rules: 337 | F -> FMNxQRyQROP 338 | M => d[++FF+FF+] 339 | N => d[--FF-FF-] 340 | O => e[-F++F++] 341 | P => e[+F--F--] 342 | Q => c++F--F 343 | R => c--F++F 344 | 345 | color: brown 346 | direction: [0.5, 1, -1] 347 | angle: 27 348 | depth:5 349 | actions: 350 | c => setColor('green') 351 | d => setColor('lime') 352 | e => setColor('goldenrod') 353 | x => rotateX(2) 354 | y => rotateY(-1.5)`, 355 | 356 | `// Pean-c flower after Gary Teachout 357 | axiom: FXhFXiFX 358 | rules: 359 | F => 360 | X => [FX-FY][-cFX-FY-FX][ZZ]-dFY-FX+FY+FX 361 | Y => FY 362 | Z => -cFX-FY-FX 363 | 364 | color: green 365 | depth: 3 366 | angle: 340 367 | width: 2 368 | direction: [1,1,1] 369 | actions: 370 | c => setColor("violet") 371 | d => setColor("lime") 372 | h => rotate(5) 373 | i => rotate(-3)`, 374 | 375 | `// poetasters succulent 1 376 | axiom: A 377 | rules: 378 | A =>[FL]gAhg[FLA] 379 | F => cSF 380 | S => dFL 381 | L => c[F+F+F]fe[F-F-F] 382 | 383 | color:green 384 | direction: [0, 1, 0.5] 385 | width: 4 386 | angle: 17 387 | depth: 7 388 | actions: 389 | c => setColor('green') 390 | d => setColor('lime') 391 | e => setColor('lightgreen') 392 | g => rotate(4.5) 393 | h => rotate(-3)`, 394 | 395 | `// Pyramids, Anthony Hanmer ADH258a 396 | axiom: F++F++F+++F--F--F 397 | rules: 398 | F => cFF++F++F++dFFF 399 | 400 | color: gold 401 | angle: 60 402 | depth:3 403 | actions: 404 | c => setColor('gold') 405 | d => setColor('goldenrod')`, 406 | 407 | `// Hilbert curve 408 | axiom: X 409 | rules: 410 | X => -YF+XFX+FY- 411 | Y => +XF-YFY-FX+ 412 | 413 | angle: 90`, 414 | 415 | `// Levey Curve 416 | axiom: F++F++F++F 417 | rules: 418 | F => -dF++cF- 419 | angle: 45 420 | depth: 12 421 | actions: 422 | c => setColor("goldenrod") 423 | d => setColor("gold")`, 424 | 425 | `// blocks 426 | axiom: F+F+F+F 427 | rules: 428 | F => F-f+FF-FF-FF-FFf-FFFF 429 | f => ffffff 430 | 431 | angle: 90 432 | depth: 3 `, 433 | 434 | `// aztec blocks 435 | axiom: F-F-F-F 436 | rules: 437 | F => F-cf+FF-F-FF-Ff-FF+df-FF+F+FF+Ff+FFF 438 | f => ffffff 439 | 440 | angle: 90 441 | depth: 2 442 | actions: 443 | c => setColor("goldenrod") 444 | d => setColor("gold")`, 445 | 446 | `// Color Mosaic 447 | axiom: F+F+F+F 448 | rules: 449 | F => dFF+F+cF+F+FF 450 | 451 | color: green 452 | depth: 3 453 | angle: 90 454 | actions: 455 | c => setColor('lime') 456 | d => setColor('green')`, 457 | 458 | `// 3 Blocks 459 | axiom: F^^F^^F 460 | rules: 461 | F => F-fff^F^^F^^F&&fff-FFF 462 | f => fff 463 | 464 | depth: 3 465 | actions: 466 | - => rotate(-90) 467 | ^ => rotate(60) 468 | & => rotate(-60) `, 469 | `// Leaf 470 | axiom: Y---Y 471 | rules: 472 | X => F-FF-F--[--X]F-FF-F--F-FF-F-- 473 | Y => f-F+X+F-fY 474 | 475 | depth: 8 476 | angle: 60`, `// esum 477 | axiom: X+X+X+X+X+X+X+X 478 | rules: 479 | X => [F[-X++Y]] 480 | Y => [F[-Y--X]] 481 | F => F 482 | 483 | depth: 6 484 | angle: -45`, 485 | `// Penrose tiling 486 | axiom: [N]++[N]++[N]++[N]++[N] 487 | rules: 488 | M => OF++PF----NF[-OF----MF]++ 489 | N => +OF--PF[---MF--NF]+ 490 | O => -MF++NF[+++OF++PF]- 491 | P => --OF++++MF[+PF++++NF]--NF 492 | F => 493 | 494 | depth: 4 495 | angle: 36 496 | ` 497 | ] 498 | 499 | export default function getCodeModel(scene) { 500 | let model = { 501 | setCode, 502 | error: null, 503 | randomize, 504 | code: qs.get('code') 505 | } 506 | let lastPickedIndex = -1; 507 | 508 | setCode(model.code); 509 | 510 | return model; 511 | 512 | function randomize() { 513 | let index; 514 | do { index = pickRandomIndex(standardCollection) } while (index === lastPickedIndex); 515 | lastPickedIndex = index; 516 | let code = standardCollection[lastPickedIndex]; 517 | setCode(code); 518 | model.ignoreNextUpdate = true; 519 | model.code = code; 520 | } 521 | 522 | function setCode(newCode) { 523 | newCode = newCode.trim(); 524 | if (!newCode) { 525 | model.error = 'Enter a system description above' 526 | return; 527 | } 528 | try { 529 | let system = parseExpression(newCode); 530 | if (system) { 531 | // TODO: I messed up with grammar, and seems like string value takes precedence over 532 | // axiom clause. A little hack here to put them back on the same page until better fix: 533 | if (system.axiom) system.start = system.axiom; 534 | scene.setSystem(system); 535 | if (scene.isComplete()) { 536 | model.error = null; 537 | } else { 538 | model.error = 'The system limit reached.\nRendering first 1,000,000 characters' 539 | } 540 | qs.set('code', newCode); 541 | } else { 542 | model.error = 'Could not parse the input string'; 543 | } 544 | } catch (e) { 545 | model.error = e.message; 546 | } 547 | } 548 | } 549 | 550 | function getInitialCode() { 551 | // todo: query state? 552 | return `axiom: X 553 | rules: 554 | X => -YF+XFX+FY- 555 | Y => +XF-YFY-FX+ 556 | 557 | depth: 5 558 | stepsPerFrame: 10 559 | width: 2 560 | color: #FFFFFF 561 | 562 | actions: 563 | - => rotate(-90) 564 | + => rotate(90) 565 | F => draw() 566 | ` 567 | } 568 | 569 | 570 | function getRandomSystem(length) { 571 | let states = 'FXY'; 572 | let res = []; 573 | let lastCh = ''; 574 | while (res.length < length) { 575 | let ch = pickChar(); 576 | if (ch === '+' && lastCh === '-') ch = '-' 577 | else if (ch === '-' && lastCh === '+') ch = '+'; 578 | else if (ch === 'F' && lastCh === 'F') ch = Math.random() < 0.5 ? 'X': 'Y'; 579 | res.push(ch); 580 | lastCh = ch; 581 | } 582 | 583 | for (let ch of 'FXY') { 584 | if (res.indexOf(ch) < 0) { 585 | res[Math.floor(Math.random() * res.length)] = ch; 586 | } 587 | } 588 | 589 | let X = res.join(''); 590 | let Y = res.reverse().map( x => { 591 | if (x === '+') return '-'; 592 | if (x === '-') return '+'; 593 | if (x === 'Y') return 'X'; 594 | if (x === 'X') return 'Y'; 595 | return x; 596 | } ).join('') 597 | 598 | let angle = Math.random() < 0.5 ? 60 : 90; 599 | if (angle === 90 && ( X.match(/(\+\+|\+-|--)/) || Y.match(/(\+\+|\+-|--)/))) angle = 45; 600 | return {X, Y, angle} 601 | 602 | function pickChar() { 603 | let r = Math.random(); 604 | if (r < 0.78) { 605 | r = Math.random(); 606 | return states[Math.floor(states.length * r)] 607 | } else { 608 | return r < 0.89 ? '+' : '-' 609 | } 610 | } 611 | } 612 | 613 | function pickRandomIndex(arr) { 614 | return Math.floor(Math.random() * arr.length); 615 | } 616 | -------------------------------------------------------------------------------- /src/grammar/parser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a very simple parser to parse our "L-system" language 3 | * 4 | * It works as a simple state machine, which has two rules: 5 | * 6 | * "ExpectKeyValue" and "ExpectRewriteValue". In each state it consumes 7 | * a line from the input and either transitions to the next state 8 | * or adds new key/values to the current state. 9 | */ 10 | const separator = /:|->|=>|=|→/ 11 | const rules = /rules|production rules/i; 12 | const actions = /actions/i; 13 | const ws = /[ \t]/g; 14 | const numericKey = /angle|depth|width|stepsPerFrame/i; 15 | const vectorKey = /direction|position/i; 16 | const parens = /[[\]()]/; 17 | 18 | /** 19 | * Read rewrite rule. 20 | */ 21 | class ExpectRewriteValue { 22 | constructor(addTo, parent, valueParser) { 23 | this.parent = parent; 24 | this.addTo = addTo; 25 | this.valueParser = valueParser || (result => result); 26 | } 27 | 28 | process(line) { 29 | let result = line.split(separator) 30 | if (result.length === 1) throw new Error('Expected `_ => value`, found ' + line); 31 | 32 | let key = result[0].trim(); 33 | if (key.length !== 1) { 34 | // we are done with this rule 35 | return this.parent.process(line); 36 | } 37 | let v = result.slice(1).join('').replace(ws, ''); 38 | this.addTo[key] = this.valueParser(v); 39 | return this; 40 | } 41 | } 42 | 43 | function extractAction(stringValue) { 44 | let m = stringValue.match(/(\w+?)\s*\((.+?)?\)/); 45 | if (!m) { 46 | throw new Error('Expected `action()` call, got: ' + stringValue); 47 | } 48 | let name = m[1]; 49 | let args; 50 | if (!m[2]) args = []; 51 | else { 52 | args = m[2].split(',').map(v => { 53 | let stringValue = extractStringValue(v); 54 | if (stringValue !== undefined) return stringValue; 55 | return Number.parseFloat(v.trim()); 56 | }); 57 | } 58 | return {name, args}; 59 | } 60 | 61 | function extractStringValue(value) { 62 | let matchedString = (value && value.match(/\s*(?:'(.*?)'|"(.*?)")\s*/)); 63 | if (matchedString) { 64 | return matchedString[1] || matchedString[2]; 65 | } 66 | } 67 | 68 | /** 69 | * Expect arbitrary key/value line. 70 | */ 71 | class ExpectKeyValue { 72 | constructor(addTo) { 73 | this.addTo = addTo; 74 | } 75 | 76 | process(line) { 77 | let result = line.split(separator) 78 | if (result.length === 1) throw new Error('Expected `key: value`, found ' + line); 79 | let key = result[0].trim(); 80 | let value = result.slice(1).join('').trim(); 81 | if (key.match(rules)) { 82 | this.addTo.rules = {}; 83 | return new ExpectRewriteValue(this.addTo.rules, this); 84 | } else if (key.match(actions)) { 85 | this.addTo.actions = {}; 86 | return new ExpectRewriteValue(this.addTo.actions, this, extractAction); 87 | } else if (key.match(numericKey)) { 88 | let v = Number.parseFloat(value); 89 | if (!Number.isFinite(v)) { 90 | throw new Error('Expected a number value for `' + key + '`'); 91 | } 92 | this.addTo[key] = v; 93 | return this; 94 | } else if (key.match(vectorKey)) { 95 | this.addTo[key] = parseVector(value, key) 96 | return this; 97 | } 98 | 99 | this.addTo[key] = value; 100 | return this; 101 | } 102 | } 103 | 104 | function parseVector(v, vectorName) { 105 | return v.replace(parens, '').replace(ws, '').split(',').map((component, position) => { 106 | if (!component) { 107 | throw new Error('Missing vector component at position ' + position +' for `' + vectorName + '`'); 108 | } 109 | let x = Number.parseFloat(component); 110 | if (!Number.isFinite(x)) { 111 | throw new Error('Vectors are expected to be numbers. Found `' + component + '` in `' + v + '`'); 112 | } 113 | return x; 114 | }) 115 | } 116 | 117 | export default function parse(str) { 118 | let lines = str.split('\n'); 119 | let currentState = new ExpectKeyValue({}); 120 | 121 | lines.forEach((line, lineNumber) => { 122 | line = line.trim(); 123 | // ignore comments and empty lines 124 | if (line === '' || line.startsWith('//')) return; 125 | try { 126 | currentState = currentState.process(line); 127 | } catch (e) { 128 | throw new Error(e.message + '\n\n at line ' + lineNumber); 129 | } 130 | }); 131 | 132 | while (currentState.parent) { 133 | currentState = currentState.parent; 134 | } 135 | return currentState.addTo; 136 | } 137 | -------------------------------------------------------------------------------- /src/lmode.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 4 | // Distributed under an MIT license: https://codemirror.net/LICENSE 5 | 6 | // Hacked together to barely support l-system definition 7 | // by Andrei Kashcha (anvaka@) 8 | 9 | module.exports = function(CodeMirror) { 10 | "use strict"; 11 | 12 | CodeMirror.defineMode("javascript", function(config, parserConfig) { 13 | var indentUnit = config.indentUnit; 14 | var statementIndent = parserConfig.statementIndent; 15 | var jsonMode = parserConfig.json; 16 | var lSystemMode = parserConfig.lsystem; 17 | var isTS = parserConfig.typescript; 18 | var wordRE = parserConfig.wordCharacters || /[\w$\xa1-\uffff]/; 19 | var ruleCharRE = /[a-zA-Z+_^?~<>|&*%\][-]/; 20 | 21 | // Tokenizer 22 | 23 | var keywords = function(){ 24 | function kw(type) {return {type: type, style: "keyword"};} 25 | var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c"), D = kw("keyword d"); 26 | var operator = kw("operator"), atom = {type: "atom", style: "atom"}; 27 | 28 | return { 29 | axiom: kw('axiom'), 30 | angle: kw('angle'), 31 | start: kw('start'), 32 | direction: kw('direction'), 33 | position: kw('position'), 34 | hideGrid: kw('hideGrid'), 35 | rules: kw('rules'), 36 | depth: kw('depth'), 37 | stepsPerFrame: kw('stepsPerFrame'), 38 | actions: kw('actions'), 39 | color: kw('color'), 40 | width: kw('width'), 41 | }; 42 | }(); 43 | let nope = {test: () => false} 44 | 45 | var isOperatorChar = nope; 46 | 47 | function readRegexp(stream) { 48 | var escaped = false, next, inSet = false; 49 | while ((next = stream.next()) != null) { 50 | if (!escaped) { 51 | if (next == "/" && !inSet) return; 52 | if (next == "[") inSet = true; 53 | else if (inSet && next == "]") inSet = false; 54 | } 55 | escaped = !escaped && next == "\\"; 56 | } 57 | } 58 | 59 | // Used as scratch variables to communicate multiple values without 60 | // consing up tons of objects. 61 | var type, content; 62 | function ret(tp, style, cont) { 63 | type = tp; content = cont; 64 | return style; 65 | } 66 | function tokenBase(stream, state) { 67 | var ch = stream.next(); 68 | if (ch == '"' || ch == "'") { 69 | state.tokenize = tokenString(ch); 70 | return state.tokenize(stream, state); 71 | } else if (ch == "." && stream.match(/^\d[\d_]*(?:[eE][+\-]?[\d_]+)?/)) { 72 | return ret("number", "number"); 73 | } else if (ch == "." && stream.match("..")) { 74 | return ret("spread", "meta"); 75 | } else if (/[{}\(\),;\:\.]/.test(ch)) { 76 | return ret(ch); 77 | } else if (ch == "=" && stream.eat(">")) { 78 | return ret("=>", "operator"); 79 | } else if (ch == "0" && stream.match(/^(?:x[\dA-Fa-f_]+|o[0-7_]+|b[01_]+)n?/)) { 80 | return ret("number", "number"); 81 | } else if (/\d/.test(ch)) { 82 | stream.match(/^[\d_]*(?:n|(?:\.[\d_]*)?(?:[eE][+\-]?[\d_]+)?)?/); 83 | return ret("number", "number"); 84 | } else if (ch == "/") { 85 | if (stream.eat("*")) { 86 | state.tokenize = tokenComment; 87 | return tokenComment(stream, state); 88 | } else if (stream.eat("/")) { 89 | stream.skipToEnd(); 90 | return ret("comment", "comment"); 91 | } else if (expressionAllowed(stream, state, 1)) { 92 | readRegexp(stream); 93 | stream.match(/^\b(([gimyus])(?![gimyus]*\2))+\b/); 94 | return ret("regexp", "string-2"); 95 | } else { 96 | stream.eat("="); 97 | return ret("operator", "operator", stream.current()); 98 | } 99 | } else if (ch == "`") { 100 | state.tokenize = tokenQuasi; 101 | return tokenQuasi(stream, state); 102 | } else if (ch == "#" && stream.peek() == "!") { 103 | stream.skipToEnd(); 104 | return ret("meta", "meta"); 105 | } else if (ch == "#" && stream.eatWhile(wordRE)) { 106 | return ret("variable", "property") 107 | } else if (ch == "<" && stream.match("!--") || ch == "-" && stream.match("->")) { 108 | stream.skipToEnd() 109 | return ret("comment", "comment") 110 | } else if (isOperatorChar.test(ch)) { 111 | if (ch != ">" || !state.lexical || state.lexical.type != ">") { 112 | if (stream.eat("=")) { 113 | if (ch == "!" || ch == "=") stream.eat("=") 114 | } else if (/[<>*+\-]/.test(ch)) { 115 | stream.eat(ch) 116 | if (ch == ">") stream.eat(ch) 117 | } 118 | } 119 | if (ch == "?" && stream.eat(".")) return ret(".") 120 | return ret("operator", "operator", stream.current()); 121 | } else if (wordRE.test(ch)) { 122 | stream.eatWhile(wordRE); 123 | var word = stream.current() 124 | if (state.lastType != ".") { 125 | if (keywords.propertyIsEnumerable(word)) { 126 | var kw = keywords[word] 127 | return ret(kw.type, kw.style, word) 128 | } 129 | if (word == "async" && stream.match(/^(\s|\/\*.*?\*\/)*[\[\(\w]/, false)) 130 | return ret("async", "keyword", word) 131 | } 132 | return ret("variable", "variable", word) 133 | } else if (ruleCharRE.test(ch)) { 134 | var arrow = stream.string.indexOf("=>", stream.start); 135 | if (arrow > -1) { 136 | var word = stream.current() 137 | return ret("def", "def", word) 138 | } 139 | } 140 | } 141 | 142 | function tokenString(quote) { 143 | return function(stream, state) { 144 | var escaped = false, next; 145 | while ((next = stream.next()) != null) { 146 | if (next == quote && !escaped) break; 147 | escaped = !escaped && next == "\\"; 148 | } 149 | if (!escaped) state.tokenize = tokenBase; 150 | return ret("string", "string"); 151 | }; 152 | } 153 | 154 | function tokenComment(stream, state) { 155 | var maybeEnd = false, ch; 156 | while (ch = stream.next()) { 157 | if (ch == "/" && maybeEnd) { 158 | state.tokenize = tokenBase; 159 | break; 160 | } 161 | maybeEnd = (ch == "*"); 162 | } 163 | return ret("comment", "comment"); 164 | } 165 | 166 | function tokenQuasi(stream, state) { 167 | var escaped = false, next; 168 | while ((next = stream.next()) != null) { 169 | if (!escaped && (next == "`" || next == "$" && stream.eat("{"))) { 170 | state.tokenize = tokenBase; 171 | break; 172 | } 173 | escaped = !escaped && next == "\\"; 174 | } 175 | return ret("quasi", "string-2", stream.current()); 176 | } 177 | 178 | var brackets = "([{}])"; 179 | // This is a crude lookahead trick to try and notice that we're 180 | // parsing the argument patterns for a fat-arrow function before we 181 | // actually hit the arrow token. It only works if the arrow is on 182 | // the same line as the arguments and there's no strange noise 183 | // (comments) in between. Fallback is to only notice when we hit the 184 | // arrow, and not declare the arguments as locals for the arrow 185 | // body. 186 | function findFatArrow(stream, state) { 187 | if (state.fatArrowAt) state.fatArrowAt = null; 188 | var arrow = stream.string.indexOf("=>", stream.start); 189 | if (arrow < 0) return; 190 | 191 | if (isTS) { // Try to skip TypeScript return type declarations after the arguments 192 | var m = /:\s*(?:\w+(?:<[^>]*>|\[\])?|\{[^}]*\})\s*$/.exec(stream.string.slice(stream.start, arrow)) 193 | if (m) arrow = m.index 194 | } 195 | 196 | var depth = 0, sawSomething = false; 197 | for (var pos = arrow - 1; pos >= 0; --pos) { 198 | var ch = stream.string.charAt(pos); 199 | var bracket = brackets.indexOf(ch); 200 | if (bracket >= 0 && bracket < 3) { 201 | if (!depth) { ++pos; break; } 202 | if (--depth == 0) { if (ch == "(") sawSomething = true; break; } 203 | } else if (bracket >= 3 && bracket < 6) { 204 | ++depth; 205 | } else if (wordRE.test(ch)) { 206 | sawSomething = true; 207 | } else if (/["'\/`]/.test(ch)) { 208 | for (;; --pos) { 209 | if (pos == 0) return 210 | var next = stream.string.charAt(pos - 1) 211 | if (next == ch && stream.string.charAt(pos - 2) != "\\") { pos--; break } 212 | } 213 | } else if (sawSomething && !depth) { 214 | ++pos; 215 | break; 216 | } 217 | } 218 | if (sawSomething && !depth) state.fatArrowAt = pos; 219 | } 220 | 221 | // Parser 222 | 223 | var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true, "regexp": true, "this": true, "jsonld-keyword": true}; 224 | 225 | function JSLexical(indented, column, type, align, prev, info) { 226 | this.indented = indented; 227 | this.column = column; 228 | this.type = type; 229 | this.prev = prev; 230 | this.info = info; 231 | if (align != null) this.align = align; 232 | } 233 | 234 | function inScope(state, varname) { 235 | for (var v = state.localVars; v; v = v.next) 236 | if (v.name == varname) return true; 237 | for (var cx = state.context; cx; cx = cx.prev) { 238 | for (var v = cx.vars; v; v = v.next) 239 | if (v.name == varname) return true; 240 | } 241 | } 242 | 243 | function parseJS(state, style, type, content, stream) { 244 | var cc = state.cc; 245 | // Communicate our context to the combinators. 246 | // (Less wasteful than consing up a hundred closures on every call.) 247 | cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc; cx.style = style; 248 | 249 | if (!state.lexical.hasOwnProperty("align")) 250 | state.lexical.align = true; 251 | 252 | while(true) { 253 | var combinator = cc.length ? cc.pop() : jsonMode ? expression : statement; 254 | if (combinator(type, content)) { 255 | while(cc.length && cc[cc.length - 1].lex) 256 | cc.pop()(); 257 | if (cx.marked) return cx.marked; 258 | if (type == "variable" && inScope(state, content)) return "variable-2"; 259 | return style; 260 | } 261 | } 262 | } 263 | 264 | // Combinator utils 265 | 266 | var cx = {state: null, column: null, marked: null, cc: null}; 267 | function pass() { 268 | for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]); 269 | } 270 | function cont() { 271 | pass.apply(null, arguments); 272 | return true; 273 | } 274 | function inList(name, list) { 275 | for (var v = list; v; v = v.next) if (v.name == name) return true 276 | return false; 277 | } 278 | function register(varname) { 279 | var state = cx.state; 280 | cx.marked = "def"; 281 | if (state.context) { 282 | if (state.lexical.info == "var" && state.context && state.context.block) { 283 | // FIXME function decls are also not block scoped 284 | var newContext = registerVarScoped(varname, state.context) 285 | if (newContext != null) { 286 | state.context = newContext 287 | return 288 | } 289 | } else if (!inList(varname, state.localVars)) { 290 | state.localVars = new Var(varname, state.localVars) 291 | return 292 | } 293 | } 294 | // Fall through means this is global 295 | if (parserConfig.globalVars && !inList(varname, state.globalVars)) 296 | state.globalVars = new Var(varname, state.globalVars) 297 | } 298 | function registerVarScoped(varname, context) { 299 | if (!context) { 300 | return null 301 | } else if (context.block) { 302 | var inner = registerVarScoped(varname, context.prev) 303 | if (!inner) return null 304 | if (inner == context.prev) return context 305 | return new Context(inner, context.vars, true) 306 | } else if (inList(varname, context.vars)) { 307 | return context 308 | } else { 309 | return new Context(context.prev, new Var(varname, context.vars), false) 310 | } 311 | } 312 | 313 | function isModifier(name) { 314 | return name == "public" || name == "private" || name == "protected" || name == "abstract" || name == "readonly" 315 | } 316 | 317 | // Combinators 318 | 319 | function Context(prev, vars, block) { this.prev = prev; this.vars = vars; this.block = block } 320 | function Var(name, next) { this.name = name; this.next = next } 321 | 322 | var defaultVars = new Var("this", new Var("arguments", null)) 323 | function pushcontext() { 324 | cx.state.context = new Context(cx.state.context, cx.state.localVars, false) 325 | cx.state.localVars = defaultVars 326 | } 327 | function pushblockcontext() { 328 | cx.state.context = new Context(cx.state.context, cx.state.localVars, true) 329 | cx.state.localVars = null 330 | } 331 | function popcontext() { 332 | cx.state.localVars = cx.state.context.vars 333 | cx.state.context = cx.state.context.prev 334 | } 335 | popcontext.lex = true 336 | function pushlex(type, info) { 337 | var result = function() { 338 | var state = cx.state, indent = state.indented; 339 | if (state.lexical.type == "stat") indent = state.lexical.indented; 340 | else for (var outer = state.lexical; outer && outer.type == ")" && outer.align; outer = outer.prev) 341 | indent = outer.indented; 342 | state.lexical = new JSLexical(indent, cx.stream.column(), type, null, state.lexical, info); 343 | }; 344 | result.lex = true; 345 | return result; 346 | } 347 | function poplex() { 348 | var state = cx.state; 349 | if (state.lexical.prev) { 350 | if (state.lexical.type == ")") 351 | state.indented = state.lexical.indented; 352 | state.lexical = state.lexical.prev; 353 | } 354 | } 355 | poplex.lex = true; 356 | 357 | function expect(wanted) { 358 | function exp(type) { 359 | if (type == wanted) return cont(); 360 | else if (wanted == ";" || type == "}" || type == ")" || type == "]") return pass(); 361 | else return cont(exp); 362 | }; 363 | return exp; 364 | } 365 | 366 | function statement(type, value) { 367 | if (type == "var") return cont(pushlex("vardef", value), vardef, expect(";"), poplex); 368 | if (type == "keyword a") return cont(pushlex("form"), parenExpr, statement, poplex); 369 | if (type == "keyword b") return cont(pushlex("form"), statement, poplex); 370 | if (type == "keyword d") return cx.stream.match(/^\s*$/, false) ? cont() : cont(pushlex("stat"), maybeexpression, expect(";"), poplex); 371 | if (type == "debugger") return cont(expect(";")); 372 | if (type == "{") return cont(pushlex("}"), pushblockcontext, block, poplex, popcontext); 373 | if (type == ";") return cont(); 374 | if (type == "if") { 375 | if (cx.state.lexical.info == "else" && cx.state.cc[cx.state.cc.length - 1] == poplex) 376 | cx.state.cc.pop()(); 377 | return cont(pushlex("form"), parenExpr, statement, poplex, maybeelse); 378 | } 379 | if (type == "function") return cont(functiondef); 380 | if (type == "for") return cont(pushlex("form"), forspec, statement, poplex); 381 | if (type == "class" || (isTS && value == "interface")) { 382 | cx.marked = "keyword" 383 | return cont(pushlex("form", type == "class" ? type : value), className, poplex) 384 | } 385 | if (type == "variable") { 386 | if (isTS && value == "declare") { 387 | cx.marked = "keyword" 388 | return cont(statement) 389 | } else if (isTS && (value == "module" || value == "enum" || value == "type") && cx.stream.match(/^\s*\w/, false)) { 390 | cx.marked = "keyword" 391 | if (value == "enum") return cont(enumdef); 392 | else if (value == "type") return cont(typename, expect("operator"), typeexpr, expect(";")); 393 | else return cont(pushlex("form"), pattern, expect("{"), pushlex("}"), block, poplex, poplex) 394 | } else if (isTS && value == "namespace") { 395 | cx.marked = "keyword" 396 | return cont(pushlex("form"), expression, statement, poplex) 397 | } else if (isTS && value == "abstract") { 398 | cx.marked = "keyword" 399 | return cont(statement) 400 | } else { 401 | return cont(pushlex("stat"), maybelabel); 402 | } 403 | } 404 | if (type == "switch") return cont(pushlex("form"), parenExpr, expect("{"), pushlex("}", "switch"), pushblockcontext, 405 | block, poplex, poplex, popcontext); 406 | if (type == "case") return cont(expression, expect(":")); 407 | if (type == "default") return cont(expect(":")); 408 | if (type == "catch") return cont(pushlex("form"), pushcontext, maybeCatchBinding, statement, poplex, popcontext); 409 | if (type == "export") return cont(pushlex("stat"), afterExport, poplex); 410 | if (type == "import") return cont(pushlex("stat"), afterImport, poplex); 411 | if (type == "async") return cont(statement) 412 | if (value == "@") return cont(expression, statement) 413 | return pass(pushlex("stat"), expression, expect(";"), poplex); 414 | } 415 | function maybeCatchBinding(type) { 416 | if (type == "(") return cont(funarg, expect(")")) 417 | } 418 | function expression(type, value) { 419 | return expressionInner(type, value, false); 420 | } 421 | function expressionNoComma(type, value) { 422 | return expressionInner(type, value, true); 423 | } 424 | function parenExpr(type) { 425 | if (type != "(") return pass() 426 | return cont(pushlex(")"), maybeexpression, expect(")"), poplex) 427 | } 428 | function expressionInner(type, value, noComma) { 429 | if (cx.state.fatArrowAt == cx.stream.start) { 430 | var body = noComma ? arrowBodyNoComma : arrowBody; 431 | if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, expect("=>"), body, popcontext); 432 | else if (type == "variable") return pass(pushcontext, pattern, expect("=>"), body, popcontext); 433 | } 434 | 435 | var maybeop = noComma ? maybeoperatorNoComma : maybeoperatorComma; 436 | if (atomicTypes.hasOwnProperty(type)) return cont(maybeop); 437 | if (type == "function") return cont(functiondef, maybeop); 438 | if (type == "class" || (isTS && value == "interface")) { cx.marked = "keyword"; return cont(pushlex("form"), classExpression, poplex); } 439 | if (type == "keyword c" || type == "async") return cont(noComma ? expressionNoComma : expression); 440 | if (type == "(") return cont(pushlex(")"), maybeexpression, expect(")"), poplex, maybeop); 441 | if (type == "operator" || type == "spread") return cont(noComma ? expressionNoComma : expression); 442 | if (type == "[") return cont(pushlex("]"), arrayLiteral, poplex, maybeop); 443 | if (type == "{") return contCommasep(objprop, "}", null, maybeop); 444 | if (type == "quasi") return pass(quasi, maybeop); 445 | if (type == "new") return cont(maybeTarget(noComma)); 446 | if (type == "import") return cont(expression); 447 | return cont(); 448 | } 449 | function maybeexpression(type) { 450 | if (type.match(/[;\}\)\],]/)) return pass(); 451 | return pass(expression); 452 | } 453 | 454 | function maybeoperatorComma(type, value) { 455 | if (type == ",") return cont(maybeexpression); 456 | return maybeoperatorNoComma(type, value, false); 457 | } 458 | function maybeoperatorNoComma(type, value, noComma) { 459 | var me = noComma == false ? maybeoperatorComma : maybeoperatorNoComma; 460 | var expr = noComma == false ? expression : expressionNoComma; 461 | if (type == "=>") return cont(pushcontext, noComma ? arrowBodyNoComma : arrowBody, popcontext); 462 | if (type == "operator") { 463 | if (/\+\+|--/.test(value) || isTS && value == "!") return cont(me); 464 | if (isTS && value == "<" && cx.stream.match(/^([^<>]|<[^<>]*>)*>\s*\(/, false)) 465 | return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, me); 466 | if (value == "?") return cont(expression, expect(":"), expr); 467 | return cont(expr); 468 | } 469 | if (type == "quasi") { return pass(quasi, me); } 470 | if (type == ";") return; 471 | if (type == "(") return contCommasep(expressionNoComma, ")", "call", me); 472 | if (type == ".") return cont(property, me); 473 | if (type == "[") return cont(pushlex("]"), maybeexpression, expect("]"), poplex, me); 474 | if (isTS && value == "as") { cx.marked = "keyword"; return cont(typeexpr, me) } 475 | if (type == "regexp") { 476 | cx.state.lastType = cx.marked = "operator" 477 | cx.stream.backUp(cx.stream.pos - cx.stream.start - 1) 478 | return cont(expr) 479 | } 480 | } 481 | function quasi(type, value) { 482 | if (type != "quasi") return pass(); 483 | if (value.slice(value.length - 2) != "${") return cont(quasi); 484 | return cont(expression, continueQuasi); 485 | } 486 | function continueQuasi(type) { 487 | if (type == "}") { 488 | cx.marked = "string-2"; 489 | cx.state.tokenize = tokenQuasi; 490 | return cont(quasi); 491 | } 492 | } 493 | function arrowBody(type) { 494 | findFatArrow(cx.stream, cx.state); 495 | return pass(type == "{" ? statement : expression); 496 | } 497 | function arrowBodyNoComma(type) { 498 | findFatArrow(cx.stream, cx.state); 499 | return pass(type == "{" ? statement : expressionNoComma); 500 | } 501 | function maybeTarget(noComma) { 502 | return function(type) { 503 | if (type == ".") return cont(noComma ? targetNoComma : target); 504 | else if (type == "variable" && isTS) return cont(maybeTypeArgs, noComma ? maybeoperatorNoComma : maybeoperatorComma) 505 | else return pass(noComma ? expressionNoComma : expression); 506 | }; 507 | } 508 | function target(_, value) { 509 | if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorComma); } 510 | } 511 | function targetNoComma(_, value) { 512 | if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorNoComma); } 513 | } 514 | function maybelabel(type) { 515 | if (type == ":") return cont(poplex, statement); 516 | return pass(maybeoperatorComma, expect(";"), poplex); 517 | } 518 | function property(type) { 519 | if (type == "variable") {cx.marked = "property"; return cont();} 520 | } 521 | function objprop(type, value) { 522 | if (type == "async") { 523 | cx.marked = "property"; 524 | return cont(objprop); 525 | } else if (type == "variable" || cx.style == "keyword") { 526 | cx.marked = "property"; 527 | if (value == "get" || value == "set") return cont(getterSetter); 528 | var m // Work around fat-arrow-detection complication for detecting typescript typed arrow params 529 | if (isTS && cx.state.fatArrowAt == cx.stream.start && (m = cx.stream.match(/^\s*:\s*/, false))) 530 | cx.state.fatArrowAt = cx.stream.pos + m[0].length 531 | return cont(afterprop); 532 | } else if (type == "number" || type == "string") { 533 | cx.marked = (cx.style + " property"); 534 | return cont(afterprop); 535 | } else if (type == "jsonld-keyword") { 536 | return cont(afterprop); 537 | } else if (isTS && isModifier(value)) { 538 | cx.marked = "keyword" 539 | return cont(objprop) 540 | } else if (type == "[") { 541 | return cont(expression, maybetype, expect("]"), afterprop); 542 | } else if (type == "spread") { 543 | return cont(expressionNoComma, afterprop); 544 | } else if (value == "*") { 545 | cx.marked = "keyword"; 546 | return cont(objprop); 547 | } else if (type == ":") { 548 | return pass(afterprop) 549 | } 550 | } 551 | function getterSetter(type) { 552 | if (type != "variable") return pass(afterprop); 553 | cx.marked = "property"; 554 | return cont(functiondef); 555 | } 556 | function afterprop(type) { 557 | if (type == ":") return cont(expressionNoComma); 558 | if (type == "(") return pass(functiondef); 559 | } 560 | function commasep(what, end, sep) { 561 | function proceed(type, value) { 562 | if (sep ? sep.indexOf(type) > -1 : type == ",") { 563 | var lex = cx.state.lexical; 564 | if (lex.info == "call") lex.pos = (lex.pos || 0) + 1; 565 | return cont(function(type, value) { 566 | if (type == end || value == end) return pass() 567 | return pass(what) 568 | }, proceed); 569 | } 570 | if (type == end || value == end) return cont(); 571 | if (sep && sep.indexOf(";") > -1) return pass(what) 572 | return cont(expect(end)); 573 | } 574 | return function(type, value) { 575 | if (type == end || value == end) return cont(); 576 | return pass(what, proceed); 577 | }; 578 | } 579 | function contCommasep(what, end, info) { 580 | for (var i = 3; i < arguments.length; i++) 581 | cx.cc.push(arguments[i]); 582 | return cont(pushlex(end, info), commasep(what, end), poplex); 583 | } 584 | function block(type) { 585 | if (type == "}") return cont(); 586 | return pass(statement, block); 587 | } 588 | function maybetype(type, value) { 589 | if (isTS) { 590 | if (type == ":") return cont(typeexpr); 591 | if (value == "?") return cont(maybetype); 592 | } 593 | } 594 | function maybetypeOrIn(type, value) { 595 | if (isTS && (type == ":" || value == "in")) return cont(typeexpr) 596 | } 597 | function mayberettype(type) { 598 | if (isTS && type == ":") { 599 | if (cx.stream.match(/^\s*\w+\s+is\b/, false)) return cont(expression, isKW, typeexpr) 600 | else return cont(typeexpr) 601 | } 602 | } 603 | function isKW(_, value) { 604 | if (value == "is") { 605 | cx.marked = "keyword" 606 | return cont() 607 | } 608 | } 609 | function typeexpr(type, value) { 610 | if (value == "keyof" || value == "typeof" || value == "infer") { 611 | cx.marked = "keyword" 612 | return cont(value == "typeof" ? expressionNoComma : typeexpr) 613 | } 614 | if (type == "variable" || value == "void") { 615 | cx.marked = "type" 616 | return cont(afterType) 617 | } 618 | if (value == "|" || value == "&") return cont(typeexpr) 619 | if (type == "string" || type == "number" || type == "atom") return cont(afterType); 620 | if (type == "[") return cont(pushlex("]"), commasep(typeexpr, "]", ","), poplex, afterType) 621 | if (type == "{") return cont(pushlex("}"), commasep(typeprop, "}", ",;"), poplex, afterType) 622 | if (type == "(") return cont(commasep(typearg, ")"), maybeReturnType, afterType) 623 | if (type == "<") return cont(commasep(typeexpr, ">"), typeexpr) 624 | } 625 | function maybeReturnType(type) { 626 | if (type == "=>") return cont(typeexpr) 627 | } 628 | function typeprop(type, value) { 629 | if (type == "variable" || cx.style == "keyword") { 630 | cx.marked = "property" 631 | return cont(typeprop) 632 | } else if (value == "?" || type == "number" || type == "string") { 633 | return cont(typeprop) 634 | } else if (type == ":") { 635 | return cont(typeexpr) 636 | } else if (type == "[") { 637 | return cont(expect("variable"), maybetypeOrIn, expect("]"), typeprop) 638 | } else if (type == "(") { 639 | return pass(functiondecl, typeprop) 640 | } 641 | } 642 | function typearg(type, value) { 643 | if (type == "variable" && cx.stream.match(/^\s*[?:]/, false) || value == "?") return cont(typearg) 644 | if (type == ":") return cont(typeexpr) 645 | if (type == "spread") return cont(typearg) 646 | return pass(typeexpr) 647 | } 648 | function afterType(type, value) { 649 | if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType) 650 | if (value == "|" || type == "." || value == "&") return cont(typeexpr) 651 | if (type == "[") return cont(typeexpr, expect("]"), afterType) 652 | if (value == "extends" || value == "implements") { cx.marked = "keyword"; return cont(typeexpr) } 653 | if (value == "?") return cont(typeexpr, expect(":"), typeexpr) 654 | } 655 | function maybeTypeArgs(_, value) { 656 | if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType) 657 | } 658 | function typeparam() { 659 | return pass(typeexpr, maybeTypeDefault) 660 | } 661 | function maybeTypeDefault(_, value) { 662 | if (value == "=") return cont(typeexpr) 663 | } 664 | function vardef(_, value) { 665 | if (value == "enum") {cx.marked = "keyword"; return cont(enumdef)} 666 | return pass(pattern, maybetype, maybeAssign, vardefCont); 667 | } 668 | function pattern(type, value) { 669 | if (isTS && isModifier(value)) { cx.marked = "keyword"; return cont(pattern) } 670 | if (type == "variable") { register(value); return cont(); } 671 | if (type == "spread") return cont(pattern); 672 | if (type == "[") return contCommasep(eltpattern, "]"); 673 | if (type == "{") return contCommasep(proppattern, "}"); 674 | } 675 | function proppattern(type, value) { 676 | if (type == "variable" && !cx.stream.match(/^\s*:/, false)) { 677 | register(value); 678 | return cont(maybeAssign); 679 | } 680 | if (type == "variable") cx.marked = "property"; 681 | if (type == "spread") return cont(pattern); 682 | if (type == "}") return pass(); 683 | if (type == "[") return cont(expression, expect(']'), expect(':'), proppattern); 684 | return cont(expect(":"), pattern, maybeAssign); 685 | } 686 | function eltpattern() { 687 | return pass(pattern, maybeAssign) 688 | } 689 | function maybeAssign(_type, value) { 690 | if (value == "=") return cont(expressionNoComma); 691 | } 692 | function vardefCont(type) { 693 | if (type == ",") return cont(vardef); 694 | } 695 | function maybeelse(type, value) { 696 | if (type == "keyword b" && value == "else") return cont(pushlex("form", "else"), statement, poplex); 697 | } 698 | function forspec(type, value) { 699 | if (value == "await") return cont(forspec); 700 | if (type == "(") return cont(pushlex(")"), forspec1, poplex); 701 | } 702 | function forspec1(type) { 703 | if (type == "var") return cont(vardef, forspec2); 704 | if (type == "variable") return cont(forspec2); 705 | return pass(forspec2) 706 | } 707 | function forspec2(type, value) { 708 | if (type == ")") return cont() 709 | if (type == ";") return cont(forspec2) 710 | if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression, forspec2) } 711 | return pass(expression, forspec2) 712 | } 713 | function functiondef(type, value) { 714 | if (value == "*") {cx.marked = "keyword"; return cont(functiondef);} 715 | if (type == "variable") {register(value); return cont(functiondef);} 716 | if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, mayberettype, statement, popcontext); 717 | if (isTS && value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, functiondef) 718 | } 719 | function functiondecl(type, value) { 720 | if (value == "*") {cx.marked = "keyword"; return cont(functiondecl);} 721 | if (type == "variable") {register(value); return cont(functiondecl);} 722 | if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, mayberettype, popcontext); 723 | if (isTS && value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, functiondecl) 724 | } 725 | function typename(type, value) { 726 | if (type == "keyword" || type == "variable") { 727 | cx.marked = "type" 728 | return cont(typename) 729 | } else if (value == "<") { 730 | return cont(pushlex(">"), commasep(typeparam, ">"), poplex) 731 | } 732 | } 733 | function funarg(type, value) { 734 | if (value == "@") cont(expression, funarg) 735 | if (type == "spread") return cont(funarg); 736 | if (isTS && isModifier(value)) { cx.marked = "keyword"; return cont(funarg); } 737 | if (isTS && type == "this") return cont(maybetype, maybeAssign) 738 | return pass(pattern, maybetype, maybeAssign); 739 | } 740 | function classExpression(type, value) { 741 | // Class expressions may have an optional name. 742 | if (type == "variable") return className(type, value); 743 | return classNameAfter(type, value); 744 | } 745 | function className(type, value) { 746 | if (type == "variable") {register(value); return cont(classNameAfter);} 747 | } 748 | function classNameAfter(type, value) { 749 | if (value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, classNameAfter) 750 | if (value == "extends" || value == "implements" || (isTS && type == ",")) { 751 | if (value == "implements") cx.marked = "keyword"; 752 | return cont(isTS ? typeexpr : expression, classNameAfter); 753 | } 754 | if (type == "{") return cont(pushlex("}"), classBody, poplex); 755 | } 756 | function classBody(type, value) { 757 | if (type == "async" || 758 | (type == "variable" && 759 | (value == "static" || value == "get" || value == "set" || (isTS && isModifier(value))) && 760 | cx.stream.match(/^\s+[\w$\xa1-\uffff]/, false))) { 761 | cx.marked = "keyword"; 762 | return cont(classBody); 763 | } 764 | if (type == "variable" || cx.style == "keyword") { 765 | cx.marked = "property"; 766 | return cont(classfield, classBody); 767 | } 768 | if (type == "number" || type == "string") return cont(classfield, classBody); 769 | if (type == "[") 770 | return cont(expression, maybetype, expect("]"), classfield, classBody) 771 | if (value == "*") { 772 | cx.marked = "keyword"; 773 | return cont(classBody); 774 | } 775 | if (isTS && type == "(") return pass(functiondecl, classBody) 776 | if (type == ";" || type == ",") return cont(classBody); 777 | if (type == "}") return cont(); 778 | if (value == "@") return cont(expression, classBody) 779 | } 780 | function classfield(type, value) { 781 | if (value == "?") return cont(classfield) 782 | if (type == ":") return cont(typeexpr, maybeAssign) 783 | if (value == "=") return cont(expressionNoComma) 784 | var context = cx.state.lexical.prev, isInterface = context && context.info == "interface" 785 | return pass(isInterface ? functiondecl : functiondef) 786 | } 787 | function afterExport(type, value) { 788 | if (value == "*") { cx.marked = "keyword"; return cont(maybeFrom, expect(";")); } 789 | if (value == "default") { cx.marked = "keyword"; return cont(expression, expect(";")); } 790 | if (type == "{") return cont(commasep(exportField, "}"), maybeFrom, expect(";")); 791 | return pass(statement); 792 | } 793 | function exportField(type, value) { 794 | if (value == "as") { cx.marked = "keyword"; return cont(expect("variable")); } 795 | if (type == "variable") return pass(expressionNoComma, exportField); 796 | } 797 | function afterImport(type) { 798 | if (type == "string") return cont(); 799 | if (type == "(") return pass(expression); 800 | return pass(importSpec, maybeMoreImports, maybeFrom); 801 | } 802 | function importSpec(type, value) { 803 | if (type == "{") return contCommasep(importSpec, "}"); 804 | if (type == "variable") register(value); 805 | if (value == "*") cx.marked = "keyword"; 806 | return cont(maybeAs); 807 | } 808 | function maybeMoreImports(type) { 809 | if (type == ",") return cont(importSpec, maybeMoreImports) 810 | } 811 | function maybeAs(_type, value) { 812 | if (value == "as") { cx.marked = "keyword"; return cont(importSpec); } 813 | } 814 | function maybeFrom(_type, value) { 815 | if (value == "from") { cx.marked = "keyword"; return cont(expression); } 816 | } 817 | function arrayLiteral(type) { 818 | if (type == "]") return cont(); 819 | return pass(commasep(expressionNoComma, "]")); 820 | } 821 | function enumdef() { 822 | return pass(pushlex("form"), pattern, expect("{"), pushlex("}"), commasep(enummember, "}"), poplex, poplex) 823 | } 824 | function enummember() { 825 | return pass(pattern, maybeAssign); 826 | } 827 | 828 | function isContinuedStatement(state, textAfter) { 829 | return state.lastType == "operator" || state.lastType == "," || 830 | isOperatorChar.test(textAfter.charAt(0)) || 831 | /[,.]/.test(textAfter.charAt(0)); 832 | } 833 | 834 | function expressionAllowed(stream, state, backUp) { 835 | return state.tokenize == tokenBase && 836 | /^(?:operator|sof|keyword [bcd]|case|new|export|default|spread|[\[{}\(,;:]|=>)$/.test(state.lastType) || 837 | (state.lastType == "quasi" && /\{\s*$/.test(stream.string.slice(0, stream.pos - (backUp || 0)))) 838 | } 839 | 840 | // Interface 841 | 842 | return { 843 | startState: function(basecolumn) { 844 | var state = { 845 | tokenize: tokenBase, 846 | lastType: "sof", 847 | cc: [], 848 | lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false), 849 | localVars: parserConfig.localVars, 850 | context: parserConfig.localVars && new Context(null, null, false), 851 | indented: basecolumn || 0 852 | }; 853 | if (parserConfig.globalVars && typeof parserConfig.globalVars == "object") 854 | state.globalVars = parserConfig.globalVars; 855 | return state; 856 | }, 857 | 858 | token: function(stream, state) { 859 | if (stream.sol()) { 860 | if (!state.lexical.hasOwnProperty("align")) 861 | state.lexical.align = false; 862 | state.indented = stream.indentation(); 863 | findFatArrow(stream, state); 864 | } 865 | if (state.tokenize != tokenComment && stream.eatSpace()) return null; 866 | var style = state.tokenize(stream, state); 867 | if (type == "comment") return style; 868 | state.lastType = type == "operator" && (content == "++" || content == "--") ? "incdec" : type; 869 | return parseJS(state, style, type, content, stream); 870 | }, 871 | 872 | indent: function(state, textAfter) { 873 | if (state.tokenize == tokenComment) return CodeMirror.Pass; 874 | if (state.tokenize != tokenBase) return 0; 875 | var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical, top 876 | // Kludge to prevent 'maybelse' from blocking lexical scope pops 877 | if (!/^\s*else\b/.test(textAfter)) for (var i = state.cc.length - 1; i >= 0; --i) { 878 | var c = state.cc[i]; 879 | if (c == poplex) lexical = lexical.prev; 880 | else if (c != maybeelse) break; 881 | } 882 | while ((lexical.type == "stat" || lexical.type == "form") && 883 | (firstChar == "}" || ((top = state.cc[state.cc.length - 1]) && 884 | (top == maybeoperatorComma || top == maybeoperatorNoComma) && 885 | !/^[,\.=+\-*:?[\(]/.test(textAfter)))) 886 | lexical = lexical.prev; 887 | if (statementIndent && lexical.type == ")" && lexical.prev.type == "stat") 888 | lexical = lexical.prev; 889 | var type = lexical.type, closing = firstChar == type; 890 | 891 | if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info.length + 1 : 0); 892 | else if (type == "form" && firstChar == "{") return lexical.indented; 893 | else if (type == "form") return lexical.indented + indentUnit; 894 | else if (type == "stat") 895 | return lexical.indented + (isContinuedStatement(state, textAfter) ? statementIndent || indentUnit : 0); 896 | else if (lexical.info == "switch" && !closing && parserConfig.doubleIndentSwitch != false) 897 | return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit); 898 | else if (lexical.align) return lexical.column + (closing ? 0 : 1); 899 | else return lexical.indented + (closing ? 0 : indentUnit); 900 | }, 901 | 902 | electricInput: /^\s*(?:case .*?:|default:|\{|\})$/, 903 | blockCommentStart: jsonMode ? null : "/*", 904 | blockCommentEnd: jsonMode ? null : "*/", 905 | blockCommentContinue: jsonMode ? null : " * ", 906 | lineComment: jsonMode ? null : "//", 907 | fold: "brace", 908 | closeBrackets: "()[]{}''\"\"``", 909 | 910 | helperType: jsonMode ? "json" : "javascript", 911 | jsonldMode: false, 912 | jsonMode: jsonMode, 913 | 914 | expressionAllowed: expressionAllowed, 915 | 916 | skipExpression: function(state) { 917 | var top = state.cc[state.cc.length - 1] 918 | if (top == expression || top == expressionNoComma) state.cc.pop() 919 | } 920 | }; 921 | }); 922 | 923 | CodeMirror.registerHelper("wordChars", "javascript", /[\w$]/); 924 | 925 | CodeMirror.defineMIME("text/javascript", "javascript"); 926 | CodeMirror.defineMIME("text/ecmascript", "javascript"); 927 | CodeMirror.defineMIME("application/javascript", "javascript"); 928 | CodeMirror.defineMIME("application/x-javascript", "javascript"); 929 | CodeMirror.defineMIME("application/ecmascript", "javascript"); 930 | CodeMirror.defineMIME("application/json", {name: "javascript", json: true}); 931 | CodeMirror.defineMIME("application/lsystem", {name: "javascript", json: true, lsystem: true}); 932 | CodeMirror.defineMIME("application/x-json", {name: "javascript", json: true}); 933 | CodeMirror.defineMIME("application/ld+json", {name: "javascript", jsonld: true}); 934 | CodeMirror.defineMIME("text/typescript", { name: "javascript", typescript: true }); 935 | CodeMirror.defineMIME("application/typescript", { name: "javascript", typescript: true }); 936 | } -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import NoWebGL from './NoWebGL'; 4 | import {isWebGLEnabled} from 'w-gl'; 5 | 6 | Vue.config.productionTip = false 7 | 8 | let canRender = isWebGLEnabled(document.querySelector('#canvas')); 9 | 10 | new Vue({ 11 | render: h => h(canRender ? App : NoWebGL), 12 | }).$mount('#app') 13 | -------------------------------------------------------------------------------- /src/shared.styl: -------------------------------------------------------------------------------- 1 | 2 | window-background = rgba(6, 24, 56, 1.0); -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | publicPath: '', 3 | }; --------------------------------------------------------------------------------