├── .gitignore ├── README.md ├── bot.js ├── generator.js ├── lsystem.js ├── mentionHandler.js ├── package.json ├── progress.json └── twitterer.js /.gitignore: -------------------------------------------------------------------------------- 1 | creds.json 2 | node_modules 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | lsystembot 2 | ========= 3 | 4 | generates random lindenmeyer systems 5 | 6 | config 7 | ===== 8 | 9 | install dependencies for node-canvas: https://github.com/Automattic/node-canvas 10 | 11 | npm install 12 | 13 | In the Wild 14 | ==== 15 | 16 | https://twitter.com/LSystemBot 17 | 18 | 19 | Samples 20 | ==== 21 | ![sample 1](https://pbs.twimg.com/media/CF2jRjPUMAEFJYD.png) 22 | ![sample 2](https://pbs.twimg.com/media/B_nbDIXWAAAKaOl.png) 23 | ![sample 3](https://pbs.twimg.com/media/CGR3ltJUIAAj9wO.png) 24 | ![sample 4](https://pbs.twimg.com/media/CF3Mf85UgAEzzwt.png) 25 | 26 | License 27 | ==== 28 | MIT 29 | -------------------------------------------------------------------------------- /bot.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | var generator = require('./generator.js'); 4 | var lsystem = require('./lsystem.js'); 5 | var twitterer = require('./twitterer.js'); 6 | var mentionHandler = require('./mentionHandler.js'); 7 | twitterer.useCreds(JSON.parse(fs.readFileSync('./creds.json'))); 8 | 9 | var action, retry; 10 | 11 | action = function() { 12 | var system = generator.generate(); 13 | var canvasBuf = lsystem.expand(system, 10); 14 | 15 | if(canvasBuf === null) { 16 | console.log('path not good enough, retrying...'); 17 | retry(); 18 | return; 19 | } 20 | 21 | //fs.writeFileSync(__dirname + '/text.png', canvasBuf); 22 | 23 | console.log('tweeting:', JSON.stringify(system)); 24 | 25 | twitterer.tweet(JSON.stringify(system), canvasBuf, undefined, function(error, res) { 26 | if(error || (res||{}).statusCode !== 200) { 27 | console.log('error tweeting:', error, (res||{}).body); 28 | retry(); 29 | } else { 30 | console.log('tweet success'); 31 | } 32 | }); 33 | 34 | mentionHandler.handleMentions(twitterer); 35 | } 36 | 37 | retry = function() { 38 | setTimeout(action, 1); 39 | } 40 | 41 | action(); 42 | -------------------------------------------------------------------------------- /generator.js: -------------------------------------------------------------------------------- 1 | 2 | var MAX_TWEET_LENGTH = 140; 3 | var IMAGE_COST = 23; 4 | 5 | /* 6 | {"start":"F","rules":{"F":"FFB","B":"+F"}[,"a":60][,"iter":4][,"wiggly":true]} 7 | */ 8 | 9 | // TODO: special templates: symmetry, heavy branching, plants 10 | 11 | function chooseRandom(arr) { 12 | return arr[Math.floor(Math.random() * arr.length)]; 13 | } 14 | 15 | function genRandomString(min, max, charSet) { 16 | var length = Math.floor(Math.random() * (max - min)) + min; 17 | var str = ''; 18 | 19 | for(var i = 0; i < length; i++) { 20 | var char = chooseRandom(charSet); 21 | if(char === ']') { 22 | var insertIndex = Math.floor(Math.random()*(str.length)); 23 | str = str.substring(0, insertIndex) + '[' + str.substring(insertIndex); 24 | } 25 | str += char; 26 | } 27 | return str; 28 | } 29 | 30 | exports.generate = function() { 31 | var system = {}; 32 | var killOrder = ['a', 'iter']; 33 | var charSet = ['F']; 34 | var alphabet = 'ABCDEGHIJKLMNOPQRSTUVWXYZ'.split(''); 35 | var controlCharSet = ['F', '+', '-', ']']; 36 | var angles = [36, 45, 60, 90, Math.floor(Math.random()*360), Math.floor(Math.random()*360)]; 37 | var iters = [4, 5, 6, 7, 16]; 38 | var i, index; 39 | 40 | var extraSymbols = Math.floor(Math.random()*5) + 1; 41 | for(i = 0; i < extraSymbols; i++) { 42 | index = Math.floor(Math.random()*alphabet.length); 43 | charSet.push(alphabet.splice(index, 1)[0]); 44 | } 45 | 46 | system.start = genRandomString(1, 5, charSet); 47 | 48 | system.rules = {}; 49 | charSet.forEach(function(char) { 50 | var ruleStr = genRandomString(0, 10, charSet.concat(controlCharSet)); 51 | if(ruleStr.length > 0) { 52 | system.rules[char] = ruleStr; 53 | } 54 | }); 55 | 56 | 57 | // choose optional parameters (if not specified, random values are chosen later) 58 | system.a = chooseRandom(angles); 59 | system.iter = chooseRandom(iters); 60 | 61 | // shorten tweet length 62 | while((JSON.stringify(system).length > MAX_TWEET_LENGTH - IMAGE_COST) && (killOrder.length > 0)) { 63 | var remove = killOrder.pop(); 64 | delete system[remove]; 65 | } 66 | 67 | return system; 68 | } 69 | -------------------------------------------------------------------------------- /lsystem.js: -------------------------------------------------------------------------------- 1 | var Canvas = require('canvas'); 2 | var fabric = require('fabric').fabric; 3 | 4 | var width = 1024; 5 | var height = 512; 6 | 7 | function chooseRandom(arr) { 8 | return arr[Math.floor(Math.random() * arr.length)]; 9 | } 10 | 11 | exports.expand = function(system, minLength) { 12 | //var canvas = new Canvas(width, height, 'svg'); 13 | var canvas = fabric.createCanvasForNode(width, height); 14 | //var ctx = canvas.getContext('2d'); 15 | 16 | var start = system.start; 17 | var rules = system.rules; 18 | var angle = system['a'] || chooseRandom([36, 45, 60, 90, Math.random()*360, Math.random()*360]); 19 | angle = angle * Math.PI / 180; 20 | var iterations = system.iter || chooseRandom([3, 4, 5, 6, 7, 8, 9]); 21 | var hue = Math.random()*360; 22 | var saturation = Math.random()*0.8 + 0.1; 23 | var lightness = Math.random()*0.8 + 0.1; 24 | lightness -= Math.abs(0.5-lightness) < 0.2 ? 0.3 : 0; 25 | var fgColor = `hsl(${hue}, ${saturation*100}%, ${lightness*100}%)`; 26 | var bgColor = `hsl(${hue}, ${saturation*100}%, ${100-lightness*100}%)`; 27 | 28 | var iterate = function(str, rules) { 29 | var out = '', result; 30 | str.split('').forEach(function(character) { 31 | result = rules[character]; 32 | if(result) { 33 | out += result; 34 | } else { 35 | out += character; 36 | } 37 | }); 38 | return out; 39 | }; 40 | 41 | var string = start; 42 | for(var i=0; i { point.x += Math.cos(a)*dist; point.y += Math.sin(a)*dist; pathStr += ` L ${point.x} ${point.y}`; }, 61 | '+': () => { a -= angle; }, 62 | '-': () => { a += angle; }, 63 | '[': () => { stack.push({point: {x: point.x, y: point.y}, a: a}); }, 64 | ']': () => { ({point: point, a: a} = stack.pop()); pathStr += ` M ${point.x} ${point.y}`; } 65 | } 66 | 67 | var commands = string.replace(/[^F+-\[\]]/g, '').split(''); 68 | commands.forEach((cmd) => { 69 | var move = translations[cmd]; 70 | if(move) { 71 | move(); 72 | } 73 | }); 74 | 75 | let path = new fabric.Path(pathStr); 76 | path.stroke = fgColor; 77 | path.strokeLineCap = 'round'; 78 | path.strokeLineJoin = 'round'; 79 | path.fill = 'none'; 80 | canvas.backgroundColor = bgColor; 81 | canvas.add(path); 82 | let bounds = path.getBoundingRect(); 83 | if(bounds.width === 1 || bounds.height === 1) return null; 84 | path.scaleToHeight(canvas.height*0.98); 85 | path.center(); 86 | bounds = path.getBoundingRect(); 87 | if(isNaN(bounds.width) || isNaN(bounds.height)) return null; 88 | canvas.renderAll(); 89 | 90 | return canvas.nodeCanvas.toBuffer(); 91 | } 92 | -------------------------------------------------------------------------------- /mentionHandler.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var lsystem = require('./lsystem.js'); 3 | 4 | var fileName = './lion.png'; 5 | var credsFile = './creds.json'; 6 | 7 | exports.handleMentions = function(twitterer) { 8 | var creds = JSON.parse(fs.readFileSync(credsFile)); 9 | var last = creds.lastMention; 10 | 11 | console.log('checking mentions since', last); 12 | twitterer.getMentions(last, function(err, res) { 13 | if(err) { 14 | console.log('error getting mentions'); 15 | return; 16 | } else { 17 | var mentions = JSON.parse(res.body); 18 | if(mentions.length === 0) return; 19 | 20 | mentions.forEach(function(mention) { 21 | try { 22 | var text = mention.text; 23 | var screenName = mention.user.screen_name; 24 | var color = mention.user.profile_link_color; 25 | var id = mention.id_str; 26 | 27 | if(parseInt(id) > parseInt(last)) { 28 | creds.lastMention = id; 29 | fs.writeFileSync(progressFile, JSON.stringify(creds)); 30 | } 31 | 32 | var system = JSON.parse(/{.*}/.exec(text)); 33 | console.log('found mention:', system); 34 | if(system === null) return; 35 | 36 | var path = lsystem.expand(system); 37 | console.log('tweeting reply:', JSON.stringify(system)); 38 | 39 | twitterer.tweet('@' + screenName, path, id, function(error, res) { 40 | if(error || (res||{}).statusCode !== 200) { 41 | console.log('error tweeting:', error, (res||{}).body); 42 | } else { 43 | console.log('tweet success'); 44 | } 45 | }); 46 | } catch(e) { 47 | console.log('mention not renderable or something', e); 48 | } 49 | }); 50 | } 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "newlsystembot", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "bot.js", 6 | "dependencies": { 7 | "canvas": "^1.4.0", 8 | "fabric": "^1.6.4", 9 | "request": "^2.56.0" 10 | }, 11 | "devDependencies": {}, 12 | "scripts": { 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "author": "", 16 | "license": "BSD-2-Clause" 17 | } 18 | -------------------------------------------------------------------------------- /progress.json: -------------------------------------------------------------------------------- 1 | {"lastMention":"772720897463496704"} -------------------------------------------------------------------------------- /twitterer.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var request = require('request'); 4 | 5 | var authSettings; 6 | 7 | var useCreds = function(creds) { 8 | authSettings = creds; 9 | }; 10 | 11 | 12 | // tweet with pictures 13 | var updateWithMedia = function(status, canvasBuf, reply, callback) { 14 | var form, r, url = 'https://api.twitter.com/1.1/statuses/update_with_media.json'; 15 | 16 | r = request.post(url, { 17 | oauth: authSettings 18 | }, callback); 19 | 20 | form = r.form(); 21 | form.append('status', status); 22 | if(reply !== undefined) { 23 | form.append('in_reply_to_status_id', reply); 24 | } 25 | form.append('media[]', canvasBuf); 26 | 27 | return form; 28 | }; 29 | 30 | 31 | // get all mentions since last checked 32 | var getMentions = function(sinceId, callback) { 33 | var form, r, url = 'https://api.twitter.com/1.1/statuses/mentions_timeline.json'; 34 | var qs = { 'latest_results': true }; 35 | if(sinceId !== undefined) { 36 | qs['since_id'] = sinceId; 37 | } 38 | 39 | console.log(qs); 40 | 41 | r = request.get(url, { 42 | oauth: authSettings, 43 | qs: qs 44 | }, callback); 45 | 46 | return form; 47 | }; 48 | 49 | module.exports = { 50 | useCreds : useCreds, 51 | tweet : updateWithMedia, 52 | getMentions : getMentions 53 | } 54 | --------------------------------------------------------------------------------