├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── doc ├── .gitkeep ├── example1.png └── example2.png ├── package-lock.json ├── package.json └── src ├── main ├── cli-mandelbrot.js └── lib │ ├── calculateScreen.js │ ├── colorByIterations.js │ ├── def.json │ ├── iterationsToEscape.js │ └── keyPress.js └── test ├── dataRenderer.js ├── lib ├── calculateScreen.spec.js ├── iterationsToEscape.spec.js └── keyPress.spec.js └── performanceTest.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "parser": "babel-eslint", 4 | "rules": { 5 | "arrow-body-style": [2, "as-needed"], 6 | "arrow-parens": [2, "as-needed"], 7 | "arrow-spacing": [2, { "before": true, "after": true }], 8 | "constructor-super": 2, 9 | "generator-star-spacing": [2, { "before": true, "after": true }], 10 | "no-class-assign": 2, 11 | "no-confusing-arrow": 0, 12 | "no-const-assign": 2, 13 | "no-dupe-class-members": 2, 14 | "no-new-symbol": 2, 15 | "no-this-before-super": 2, 16 | "no-useless-constructor": 2, 17 | "no-var": 2, 18 | "object-shorthand": [2, "always"], 19 | "prefer-arrow-callback": 2, 20 | "prefer-const": 2, 21 | "prefer-reflect": [0, { "exceptions": [] }], 22 | "prefer-rest-params": 0, 23 | "prefer-spread": 0, 24 | "prefer-template": 2, 25 | "require-yield": 2, 26 | "template-curly-spacing": [2, "never"], 27 | "yield-star-spacing": [2, { "before": true, "after": true }], 28 | "space-before-function-paren": [2, "never"] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - "6" 5 | - "7" 6 | - "8" 7 | install: npm install 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Dany Shaanan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cli-mandelbrot 2 | 3 | [![Build Status](https://travis-ci.org/danyshaanan/cli-mandelbrot.png)](https://travis-ci.org/danyshaanan/cli-mandelbrot) 4 | [![NPM Version](https://img.shields.io/npm/v/cli-mandelbrot.svg?style=flat)](https://npmjs.org/package/cli-mandelbrot) 5 | [![License](http://img.shields.io/npm/l/cli-mandelbrot.svg?style=flat)](LICENSE) 6 | [![Dependency Status](https://david-dm.org/danyshaanan/cli-mandelbrot.svg)](https://david-dm.org/danyshaanan/cli-mandelbrot) 7 | [![devDependency Status](https://david-dm.org/danyshaanan/cli-mandelbrot/dev-status.svg)](https://david-dm.org/danyshaanan/cli-mandelbrot#info=devDependencies) 8 | 9 | #### View the [Mandelbrot set](http://en.wikipedia.org/wiki/Mandelbrot_set) from your terminal 10 | 11 | * * * 12 | 13 | ### Installation 14 | 15 | ```bash 16 | npm install -g cli-mandelbrot 17 | ``` 18 | 19 | * * * 20 | 21 | ### Usage 22 | 23 | ```bash 24 | cli-mandelbrot 25 | ``` 26 | 27 | ![Example screen shot](https://raw.github.com/danyshaanan/cli-mandelbrot/master/doc/example1.png?raw=true) 28 | 29 | Use the 'wasd' keys to move around, 'r' and 'f' to zoom in and out, 't' and 'f' to calculate more or less iterations, 'q' to toggle the help text, and 'o' to quit. 30 | 31 | ![Example screen shot](https://raw.github.com/danyshaanan/cli-mandelbrot/master/doc/example2.png?raw=true) 32 | 33 | * * * 34 | 35 | ### Development 36 | 37 | ```bash 38 | nvm use 6 # or nvm install 6 39 | git clone git@github.com:danyshaanan/cli-mandelbrot.git 40 | cd cli-mandelbrot 41 | npm i 42 | npm test 43 | ``` 44 | 45 | * * * 46 | 47 | ### Feedback 48 | 49 | * If you enjoyed this package, please star it [on Github](https://github.com/danyshaanan/cli-mandelbrot). 50 | * You are invited to [Open an issue on Github](https://github.com/danyshaanan/cli-mandelbrot/issues). 51 | * For other matters, my email address can be found on my [NpmJS page](https://www.npmjs.org/~danyshaanan), my [Github page](https://github.com/danyshaanan), or my [website](http://danyshaanan.com/). 52 | -------------------------------------------------------------------------------- /doc/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danyshaanan/cli-mandelbrot/ada1e2c77cfe599efe367d11f7ad7f4b7868ae9c/doc/.gitkeep -------------------------------------------------------------------------------- /doc/example1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danyshaanan/cli-mandelbrot/ada1e2c77cfe599efe367d11f7ad7f4b7868ae9c/doc/example1.png -------------------------------------------------------------------------------- /doc/example2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danyshaanan/cli-mandelbrot/ada1e2c77cfe599efe367d11f7ad7f4b7868ae9c/doc/example2.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cli-mandelbrot", 3 | "version": "1.0.4", 4 | "description": "A command line viewer of the Mandelbrot set", 5 | "keywords": [ 6 | "mandelbrot", 7 | "fractal", 8 | "ascii", 9 | "math", 10 | "cli" 11 | ], 12 | "homepage": "https://github.com/danyshaanan/cli-mandelbrot", 13 | "author": { 14 | "name": "Dany Shaanan", 15 | "email": "danyshaanan@gmail.com", 16 | "url": "http://danyshaanan.com/" 17 | }, 18 | "contributors": [], 19 | "license": "MIT", 20 | "licenses": [ 21 | { 22 | "type": "MIT", 23 | "url": "https://github.com/danyshaanan/cli-mandelbrot/blob/master/LICENSE" 24 | } 25 | ], 26 | "repository": { 27 | "type": "git", 28 | "url": "git://github.com/danyshaanan/cli-mandelbrot.git" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/danyshaanan/cli-mandelbrot", 32 | "email": "danyshaanan@gmail.com" 33 | }, 34 | "preferGlobal": true, 35 | "engines": { 36 | "node": ">=6.0.0" 37 | }, 38 | "engine-strict": true, 39 | "scripts": { 40 | "perf": "node src/test/performanceTest.js", 41 | "lint": "eslint src/", 42 | "ava": "ava src/test/**/*.spec.js", 43 | "watch": "npm run ava -- --watch ", 44 | "test": "npm run lint && npm run ava", 45 | "start": "node ./src/main/cli-mandelbrot.js" 46 | }, 47 | "bin": { 48 | "cli-mandelbrot": "./src/main/cli-mandelbrot.js" 49 | }, 50 | "dependencies": { 51 | "cli-color": "0.3.2", 52 | "lodash": "4.17.11" 53 | }, 54 | "devDependencies": { 55 | "ava": "0.15.2", 56 | "babel-eslint": "6.0.2", 57 | "eslint": "2.8.0", 58 | "eslint-config-standard": "5.2.0", 59 | "eslint-plugin-promise": "1.1.0", 60 | "eslint-plugin-standard": "1.3.2" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/cli-mandelbrot.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict' 4 | 5 | const _ = require('lodash') 6 | const clc = require('cli-color') 7 | const keyPress = require('./lib/keyPress.js') 8 | const def = require('./lib/def.json') 9 | 10 | const calculateScreenOrig = require('./lib/calculateScreen.js') 11 | const calculateScreenHash = (def, w, h) => JSON.stringify({ def, w, h }) 12 | const calculateScreen = _.memoize(calculateScreenOrig, calculateScreenHash) 13 | 14 | let helpTextOn = true 15 | 16 | const start = clc.moveTo(0, 0) 17 | const toEnd = () => clc.moveTo(clc.width, clc.height) 18 | const keys = { 19 | q: 'toggle help text', 20 | w: 'up', 21 | s: 'down', 22 | a: 'left', 23 | d: 'right', 24 | r: 'zoom in', 25 | f: 'zoom out', 26 | t: 'more iterations', 27 | g: 'less iterations', 28 | o: 'quit' 29 | } 30 | 31 | const stringify = o => _.map(o, (v, i) => ` ${i}: ${v}`).join('\n') 32 | const calculateHelp = def => clc.xterm(8)(`\nDef: \n${stringify(def)}\n\nKeys:\n${stringify(keys)}`) 33 | 34 | const print = () => { 35 | process.stdout.write(start + calculateScreen(def, clc.width, clc.height)) 36 | if (helpTextOn) process.stdout.write(start + calculateHelp(def) + toEnd()) 37 | } 38 | 39 | process.stdin.setRawMode(true) 40 | process.stdin.resume() 41 | process.stdin.on('data', data => { 42 | const key = data.toString()[0] 43 | if (key === 'o') { 44 | console.log(clc.reset) 45 | process.exit() 46 | } else if (key === 'q') { 47 | helpTextOn = !helpTextOn 48 | } 49 | keyPress(def, key) 50 | print() 51 | }) 52 | 53 | print() 54 | -------------------------------------------------------------------------------- /src/main/lib/calculateScreen.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const iterationsToEscape = require('./iterationsToEscape.js') 4 | const colorByIterations = require('./colorByIterations.js') 5 | 6 | const getColor = (x, y, iterations) => colorByIterations(iterationsToEscape(x, y, iterations)) 7 | 8 | module.exports = (def, w, h) => { 9 | const xs = Array.from(Array(w), (_, i) => def.x + (i - w / 2) / def.pixelsPerUnit) 10 | const ys = Array.from(Array(h), (_, i) => def.y + (i - h / 2) / def.pixelsPerUnit * def.pixelAspectRatio) 11 | return ys.map(y => xs.map(x => getColor(x, y, def.iterations)).join('')).join('\n') 12 | } 13 | -------------------------------------------------------------------------------- /src/main/lib/colorByIterations.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const clc = require('cli-color') 4 | 5 | const colors = [196, 202, 208, 214, 220, 226, 190, 154, 118, 82, 46, 47, 48, 49, 50, 51, 45, 39, 33, 27, 21, 57, 93, 129, 165, 201] 6 | 7 | module.exports = i => clc.bgXterm(i === undefined ? 0 : colors[i % colors.length])(' ') 8 | -------------------------------------------------------------------------------- /src/main/lib/def.json: -------------------------------------------------------------------------------- 1 | { 2 | "pixelsPerUnit": 32, 3 | "pixelAspectRatio": 2.4, 4 | "x": -1.149719506110225, 5 | "y": -0.312197910519423, 6 | "iterations": 100 7 | } 8 | -------------------------------------------------------------------------------- /src/main/lib/iterationsToEscape.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | let t 4 | 5 | module.exports = (x0, y0, iterations) => { 6 | let [x, y, i] = [x0, y0, 0] 7 | while (true) { 8 | if (x * x + y * y >= 4) return i 9 | t = x * x - y * y + x0 10 | y = 2 * x * y + y0 11 | x = t 12 | i++ 13 | if (i > iterations) return undefined 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/lib/keyPress.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const keyFuncs = { 4 | w: def => { def.y -= 10 / def.pixelsPerUnit }, 5 | a: def => { def.x -= 10 / def.pixelsPerUnit }, 6 | s: def => { def.y += 10 / def.pixelsPerUnit }, 7 | d: def => { def.x += 10 / def.pixelsPerUnit }, 8 | r: def => { def.pixelsPerUnit *= 2 }, 9 | f: def => { def.pixelsPerUnit /= 2 }, 10 | t: def => { def.iterations += 10 }, 11 | g: def => { def.iterations -= 10 } 12 | } 13 | 14 | module.exports = (def, key) => (keyFuncs[key] && keyFuncs[key](def), def) 15 | -------------------------------------------------------------------------------- /src/test/dataRenderer.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _ = require('lodash') 4 | const iterationsToEscape = require('../main/lib/iterationsToEscape.js') 5 | 6 | const trunc = a => Math.floor(a * 100) / 100 7 | const shuffleAndCutTwo = a => _(a).shuffle().take(2).value() 8 | const jump = 0.04 9 | 10 | let pointsByIndex = [] 11 | let undefinedPoints = [] 12 | 13 | for (let i = -2; i <= 2; i += jump) { 14 | for (let j = -2; j <= 2; j += jump) { 15 | const p = { x: trunc(i), y: trunc(j) } 16 | const iterations = iterationsToEscape(i, j, 10) 17 | if (iterations === undefined) { 18 | undefinedPoints.push(p) 19 | } else { 20 | pointsByIndex[iterations] = pointsByIndex[iterations] || [] 21 | pointsByIndex[iterations].push(p) 22 | } 23 | } 24 | } 25 | 26 | undefinedPoints = shuffleAndCutTwo(undefinedPoints) 27 | pointsByIndex = _.map(pointsByIndex, shuffleAndCutTwo) 28 | 29 | console.log(undefinedPoints) 30 | console.log(pointsByIndex) 31 | -------------------------------------------------------------------------------- /src/test/lib/calculateScreen.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import test from 'ava' 4 | import calculateScreen from '../../main/lib/calculateScreen.js' 5 | 6 | const def = { 7 | pixelsPerUnit: 32, 8 | pixelAspectRatio: 2.4, 9 | x: -1.149719506110225, 10 | y: -0.312197910519423, 11 | iterations: 100 12 | } 13 | 14 | const result = calculateScreen(def, 2, 2) 15 | const expected = '\u001b[48;5;190m \u001b[49m\u001b[48;5;190m \u001b[49m\n\u001b[48;5;39m \u001b[49m\u001b[48;5;0m \u001b[49m' 16 | 17 | test('keyPress should do nothing for undefined key', t => { 18 | t.deepEqual(result, expected) 19 | }) 20 | -------------------------------------------------------------------------------- /src/test/lib/iterationsToEscape.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import test from 'ava' 4 | import iterationsToEscape from '../../main/lib/iterationsToEscape.js' 5 | 6 | const undefinedPoints = [ { x: 0, y: 0 }, { x: -1.32, y: 0.04 }, { x: -0.12, y: 0.12 } ] 7 | const pointsByIndex = [ 8 | [ { x: 2, y: 2 }, { x: 1.84, y: -1 }, { x: 1.6, y: 1.96 } ], 9 | [ { x: 1, y: 1 }, { x: 0.56, y: 1.12 }, { x: 0, y: -1.52 } ], 10 | [ { x: 0.7, y: 0.7 }, { x: 0.6, y: 0.76 }, { x: -0.16, y: 1.2 } ], 11 | [ { x: 0.55, y: 0.55 }, { x: 0.32, y: -0.92 }, { x: -1, y: -0.72 } ], 12 | [ { x: 0.5, y: 0.5 }, { x: -1.44, y: 0.32 }, { x: -0.92, y: -0.56 } ], 13 | [ { x: 0.45, y: 0.45 }, { x: -1.64, y: 0.12 }, { x: -0.72, y: -0.64 } ], 14 | [ { x: 0.43, y: 0.43 }, { x: -0.2, y: -1.04 }, { x: 0.2, y: -0.68 } ], 15 | [ { x: 0.42, y: 0.42 }, { x: -0.36, y: -0.72 }, { x: 0.36, y: 0.04 } ], 16 | [ { x: 0.40, y: 0.40 }, { x: 0.32, y: 0.64 }, { x: -0.76, y: 0.36 } ], 17 | [ { x: 0.399, y: 0.399 }, { x: 0.44, y: 0.2 }, { x: 0.44, y: -0.2 } ], 18 | [ { x: 0.393, y: 0.393 }, { x: -0.28, y: -0.76 }, { x: -0.28, y: 0.76 } ] 19 | ] 20 | 21 | undefinedPoints.forEach(point => { 22 | test(`iterationsToEscape should return undefined for [${point.x},${point.y}]`, t => { 23 | t.deepEqual(iterationsToEscape(point.x, point.y, 10), undefined) 24 | }) 25 | }) 26 | 27 | pointsByIndex.forEach((points, value) => { 28 | points.forEach(point => { 29 | test(`iterationsToEscape should return ${value} for [${point.x},${point.y}]`, t => { 30 | t.deepEqual(iterationsToEscape(point.x, point.y, 10), value) 31 | }) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /src/test/lib/keyPress.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import _ from 'lodash' 4 | import test from 'ava' 5 | import keyPress from '../../main/lib/keyPress.js' 6 | 7 | const def = { 8 | pixelsPerUnit: 10, 9 | pixelAspectRatio: 2.4, 10 | x: 0, 11 | y: 0, 12 | iterations: 10 13 | } 14 | let defClone 15 | 16 | test.beforeEach(t => { 17 | defClone = _.clone(def) 18 | }) 19 | 20 | 'adfgrstw'.split('').forEach(letter => { 21 | test(`The "${letter}" key should change def`, t => { 22 | keyPress(defClone, letter) 23 | t.notDeepEqual(def, defClone) 24 | }) 25 | }) 26 | 27 | 'bcehijklmnopquvxyz'.split('').forEach(letter => { 28 | test(`The "${letter}" key should not change def`, t => { 29 | keyPress(defClone, letter) 30 | t.deepEqual(def, defClone) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /src/test/performanceTest.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict' 4 | 5 | const clc = require('cli-color') 6 | const calculateScreen = require('../main/lib/calculateScreen.js') 7 | 8 | const average = (times, func) => { 9 | const start = Date.now() 10 | for (let i = times; i; i--) func() 11 | const end = Date.now() 12 | return (end - start) / times 13 | } 14 | 15 | const def = { 16 | pixelsPerUnit: 36028797018963970, 17 | pixelAspectRatio: 2.4, 18 | x: -1.1497195061102734, 19 | y: -0.31219791051937973, 20 | iterations: 230 21 | } 22 | 23 | console.log(average(10, () => calculateScreen(def, clc.width, clc.height))) 24 | --------------------------------------------------------------------------------