├── README.markdown ├── css └── style.css ├── index.html └── js ├── lib ├── DAT.GUI.min.js └── RAF.js └── recursion.js /README.markdown: -------------------------------------------------------------------------------- 1 | # Recursion Toy 2 | Revisiting one of my favorite topics - **recursion**. This tool allows you to explore branching algorithms and rendering techniques, as well as save a snapshot of your creations. 3 | 4 | You can play with it live here: http://soulwire.co.uk/recursion-toy/ 5 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-family: "Helvetica Neue", Arial, sans-serif; 3 | background-color: #f2f2f2; 4 | overflow: hidden; 5 | padding: 0; 6 | margin: 0; 7 | height: 100%; 8 | } 9 | 10 | #output { 11 | white-space: nowrap; 12 | text-transform: uppercase; 13 | font-size: 10px; 14 | position: absolute; 15 | padding: 0; 16 | margin: 0; 17 | color: #FFF; 18 | } 19 | 20 | #output li { 21 | background: rgba(20,20,20,0.9); 22 | list-style: none; 23 | margin-right: 1px; 24 | line-height: 22px; 25 | padding: 0px 10px; 26 | display: block; 27 | float: left; 28 | } 29 | 30 | #output li a { 31 | border-bottom: 1px dotted #999; 32 | text-decoration: none; 33 | color: #FFF; 34 | } 35 | 36 | .guidat { 37 | margin-right: 0px !important; 38 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Recursion Toy 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /js/lib/DAT.GUI.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * dat.gui Javascript Controller Library 3 | * http://dataarts.github.com/dat.gui 4 | * 5 | * Copyright 2011 Data Arts Team, Google Creative Lab 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | */ 13 | var DAT=DAT||{}; 14 | DAT.GUI=function(a){a==void 0&&(a={});var b=!1;a.height==void 0?a.height=300:b=!0;var d=[],c=[],i=!0,f,h,j=this,g=!0,e=280;if(a.width!=void 0)e=a.width;var q=!1,k,p,n=0,r;this.domElement=document.createElement("div");this.domElement.setAttribute("class","guidat");this.domElement.style.width=e+"px";var l=a.height,m=document.createElement("div");m.setAttribute("class","guidat-controllers");m.style.height=l+"px";m.addEventListener("DOMMouseScroll",function(a){var b=this.scrollTop;a.wheelDelta?b+=a.wheelDelta: 15 | a.detail&&(b+=a.detail);a.preventDefault&&a.preventDefault();a.returnValue=!1;m.scrollTop=b},!1);var o=document.createElement("a");o.setAttribute("class","guidat-toggle");o.setAttribute("href","#");o.innerHTML=g?"Close Controls":"Open Controls";var t=!1,C=0,x=0,u=!1,v,y,w,z,D=function(a){y=v;z=w;v=a.pageY;w=a.pageX;a=v-y;if(!g)if(a>0)g=!0,l=k=1,o.innerHTML=p||"Close Controls";else return;var b=z-w;if(a>0&&l>h){var d=DAT.GUI.map(l,h,h+100,1,0);a*=d}t=!0;C+=a;k+=a;l+=a;m.style.height=k+"px";x+=b;e+= 16 | b;e=DAT.GUI.constrain(e,240,500);j.domElement.style.width=e+"px";A()};o.addEventListener("mousedown",function(a){y=v=a.pageY;z=w=a.pageX;u=!0;a.preventDefault();C=x=0;document.addEventListener("mousemove",D,!1);return!1},!1);o.addEventListener("click",function(a){a.preventDefault();return!1},!1);document.addEventListener("mouseup",function(a){u&&!t&&j.toggle();if(u&&t)if(x==0&&B(),k>h)clearTimeout(r),k=n=h,s();else if(m.children.length>=1){var b=m.children[0].offsetHeight;clearTimeout(r);n=Math.round(l/ 17 | b)*b-1;n<=0?(j.close(),k=b*2):(k=n,s())}document.removeEventListener("mousemove",D,!1);a.preventDefault();return u=t=!1},!1);this.domElement.appendChild(m);this.domElement.appendChild(o);if(a.domElement)a.domElement.appendChild(this.domElement);else if(DAT.GUI.autoPlace){if(DAT.GUI.autoPlaceContainer==null)DAT.GUI.autoPlaceContainer=document.createElement("div"),DAT.GUI.autoPlaceContainer.setAttribute("id","guidat"),document.body.appendChild(DAT.GUI.autoPlaceContainer);DAT.GUI.autoPlaceContainer.appendChild(this.domElement)}this.autoListenIntervalTime= 18 | 1E3/60;var E=function(){f=setInterval(function(){j.listen()},this.autoListenIntervalTime)};this.__defineSetter__("autoListen",function(a){(i=a)?c.length>0&&E():clearInterval(f)});this.__defineGetter__("autoListen",function(){return i});this.listenTo=function(a){c.length==0&&E();c.push(a)};this.unlistenTo=function(a){for(var b=0;bk?"auto":"hidden"},G={number:DAT.GUI.ControllerNumber,string:DAT.GUI.ControllerString,"boolean":DAT.GUI.ControllerBoolean,"function":DAT.GUI.ControllerFunction};this.reset=function(){for(var a=0,b=DAT.GUI.allControllers.length;a-1)document.body.scrollTop=DAT.GUI.scrollTop;n=k;this.open()}DAT.GUI.guiIndex++}DAT.GUI.allGuis.push(this);if(DAT.GUI.allGuis.length==1&&(window.addEventListener("keyup",function(a){!DAT.GUI.supressHotKeys&&a.keyCode==72&&DAT.GUI.toggleHide()},!1), 24 | DAT.GUI.inlineCSS))a=document.createElement("style"),a.setAttribute("type","text/css"),a.innerHTML=DAT.GUI.inlineCSS,document.head.insertBefore(a,document.head.firstChild)};DAT.GUI.hidden=!1;DAT.GUI.autoPlace=!0;DAT.GUI.autoPlaceContainer=null;DAT.GUI.allControllers=[];DAT.GUI.allGuis=[];DAT.GUI.supressHotKeys=!1;DAT.GUI.toggleHide=function(){DAT.GUI.hidden?DAT.GUI.open():DAT.GUI.close()}; 25 | DAT.GUI.open=function(){DAT.GUI.hidden=!1;for(var a in DAT.GUI.allGuis)DAT.GUI.allGuis[a].domElement.style.display="block"};DAT.GUI.close=function(){DAT.GUI.hidden=!0;for(var a in DAT.GUI.allGuis)DAT.GUI.allGuis[a].domElement.style.display="none"};DAT.GUI.saveURL=function(){var a=DAT.GUI.replaceGetVar("saveString",DAT.GUI.getSaveString());window.location=a};DAT.GUI.scrollTop=-1; 26 | DAT.GUI.load=function(a){var a=a.split(","),b=parseInt(a[0]);DAT.GUI.scrollTop=parseInt(a[1]);for(var d=0;dd&&(a=d);return a};DAT.GUI.error=function(a){typeof console.error=="function"&&console.error("[DAT.GUI ERROR] "+a)};DAT.GUI.roundToDecimal=function(a,b){var d=Math.pow(10,b);return Math.round(a*d)/d};DAT.GUI.extendController=function(a){a.prototype=new DAT.GUI.Controller;a.prototype.constructor=a};DAT.GUI.addClass=function(a,b){DAT.GUI.hasClass(a,b)||(a.className+=" "+b)}; 32 | DAT.GUI.hasClass=function(a,b){return a.className.indexOf(b)!=-1};DAT.GUI.removeClass=function(a,b){a.className=a.className.replace(RegExp(" "+b,"g"),"")};DAT.GUI.getVarFromURL("saveString")!=null&&DAT.GUI.load(DAT.GUI.getVarFromURL("saveString")); 33 | DAT.GUI.Controller=function(){this.parent=arguments[0];this.object=arguments[1];this.propertyName=arguments[2];if(arguments.length>0)this.initialValue=this.object[this.propertyName];this.domElement=document.createElement("div");this.domElement.setAttribute("class","guidat-controller "+this.type);this.propertyNameElement=document.createElement("span");this.propertyNameElement.setAttribute("class","guidat-propertyname");this.name(this.propertyName);this.domElement.appendChild(this.propertyNameElement); 34 | DAT.GUI.makeUnselectable(this.domElement)};DAT.GUI.Controller.prototype.changeFunction=null;DAT.GUI.Controller.prototype.finishChangeFunction=null;DAT.GUI.Controller.prototype.name=function(a){this.propertyNameElement.innerHTML=a;return this};DAT.GUI.Controller.prototype.reset=function(){this.setValue(this.initialValue);return this};DAT.GUI.Controller.prototype.listen=function(){this.parent.listenTo(this);return this};DAT.GUI.Controller.prototype.unlisten=function(){this.parent.unlistenTo(this);return this}; 35 | DAT.GUI.Controller.prototype.setValue=function(a){if(this.object[this.propertyName]!=void 0)this.object[this.propertyName]=a;else{var b={};b[this.propertyName]=a;this.object.set(b)}this.changeFunction!=null&&this.changeFunction.call(this,a);this.updateDisplay();return this};DAT.GUI.Controller.prototype.getValue=function(){var a=this.object[this.propertyName];a==void 0&&(a=this.object.get(this.propertyName));return a};DAT.GUI.Controller.prototype.updateDisplay=function(){}; 36 | DAT.GUI.Controller.prototype.onChange=function(a){this.changeFunction=a;return this};DAT.GUI.Controller.prototype.onFinishChange=function(a){this.finishChangeFunction=a;return this}; 37 | DAT.GUI.Controller.prototype.options=function(){var a=this,b=document.createElement("select");if(arguments.length==1){var d=arguments[0],c;for(c in d){var i=document.createElement("option");i.innerHTML=c;i.setAttribute("value",d[c]);if(arguments[c]==this.getValue())i.selected=!0;b.appendChild(i)}}else for(c=0;c=h&&(a=h);return DAT.GUI.Controller.prototype.setValue.call(this, 47 | a)};this.updateDisplay=function(){g.value=DAT.GUI.roundToDecimal(a.getValue(),4);if(e)e.value=a.getValue()}};DAT.GUI.extendController(DAT.GUI.ControllerNumber); 48 | DAT.GUI.ControllerNumberSlider=function(a,b,d,c,i){var f=!1,h=this;this.domElement=document.createElement("div");this.domElement.setAttribute("class","guidat-slider-bg");this.fg=document.createElement("div");this.fg.setAttribute("class","guidat-slider-fg");this.domElement.appendChild(this.fg);var j=function(b){if(f){var c;c=h.domElement;var d=0,g=0;if(c.offsetParent){do d+=c.offsetLeft,g+=c.offsetTop;while(c=c.offsetParent);c=[d,g]}else c=void 0;b=DAT.GUI.map(b.pageX,c[0],c[0]+h.domElement.offsetWidth, 49 | a.getMin(),a.getMax());b=Math.round(b/a.getStep())*a.getStep();a.setValue(b)}};this.domElement.addEventListener("mousedown",function(b){f=!0;DAT.GUI.addClass(a.domElement,"active");j(b);document.addEventListener("mouseup",g,!1)},!1);var g=function(){DAT.GUI.removeClass(a.domElement,"active");f=!1;a.finishChangeFunction!=null&&a.finishChangeFunction.call(this,a.getValue());document.removeEventListener("mouseup",g,!1)};this.__defineSetter__("value",function(b){this.fg.style.width=DAT.GUI.map(b,a.getMin(), 50 | a.getMax(),0,100)+"%"});document.addEventListener("mousemove",j,!1);this.value=i}; 51 | DAT.GUI.ControllerString=function(){this.type="string";var a=this;DAT.GUI.Controller.apply(this,arguments);var b=document.createElement("input"),d=this.getValue();b.setAttribute("value",d);b.setAttribute("spellcheck","false");this.domElement.addEventListener("mouseup",function(){b.focus();b.select()},!1);b.addEventListener("keyup",function(c){c.keyCode==13&&a.finishChangeFunction!=null&&(a.finishChangeFunction.call(this,a.getValue()),b.blur());a.setValue(b.value)},!1);b.addEventListener("mousedown", 52 | function(){DAT.GUI.makeSelectable(b)},!1);b.addEventListener("blur",function(){DAT.GUI.supressHotKeys=!1;a.finishChangeFunction!=null&&a.finishChangeFunction.call(this,a.getValue())},!1);b.addEventListener("focus",function(){DAT.GUI.supressHotKeys=!0},!1);this.updateDisplay=function(){b.value=a.getValue()};this.options=function(){a.domElement.removeChild(b);return DAT.GUI.Controller.prototype.options.apply(this,arguments)};this.domElement.appendChild(b)};DAT.GUI.extendController(DAT.GUI.ControllerString); 53 | DAT.GUI.inlineCSS="#guidat { position: fixed; top: 0; right: 0; width: auto; z-index: 1001; text-align: right; } .guidat { color: #fff; opacity: 0.97; text-align: left; float: right; margin-right: 20px; margin-bottom: 20px; background-color: #fff; } .guidat, .guidat input { font: 9.5px Lucida Grande, sans-serif; } .guidat-controllers { height: 300px; overflow-y: auto; overflow-x: hidden; background-color: rgba(0, 0, 0, 0.1); } a.guidat-toggle:link, a.guidat-toggle:visited, a.guidat-toggle:active { text-decoration: none; cursor: pointer; color: #fff; background-color: #222; text-align: center; display: block; padding: 5px; } a.guidat-toggle:hover { background-color: #000; } .guidat-controller { padding: 3px; height: 25px; clear: left; border-bottom: 1px solid #222; background-color: #111; } .guidat-controller, .guidat-controller input, .guidat-slider-bg, .guidat-slider-fg { -moz-transition: background-color 0.15s linear; -webkit-transition: background-color 0.15s linear; transition: background-color 0.15s linear; } .guidat-controller.boolean:hover, .guidat-controller.function:hover { background-color: #000; } .guidat-controller input { float: right; outline: none; border: 0; padding: 4px; margin-top: 2px; background-color: #222; } .guidat-controller select { margin-top: 4px; float: right; } .guidat-controller input:hover { background-color: #444; } .guidat-controller input:focus, .guidat-controller.active input { background-color: #555; color: #fff; } .guidat-controller.number { border-left: 5px solid #00aeff; } .guidat-controller.string { border-left: 5px solid #1ed36f; } .guidat-controller.string input { border: 0; color: #1ed36f; margin-right: 2px; width: 148px; } .guidat-controller.boolean { border-left: 5px solid #54396e; } .guidat-controller.function { border-left: 5px solid #e61d5f; } .guidat-controller.number input[type=text] { width: 35px; margin-left: 5px; margin-right: 2px; color: #00aeff; } .guidat .guidat-controller.boolean input { margin-top: 6px; margin-right: 2px; font-size: 20px; } .guidat-controller:last-child { border-bottom: none; -webkit-box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.5); -moz-box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.5); box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.5); } .guidat-propertyname { padding: 5px; padding-top: 7px; cursor: default; display: inline-block; } .guidat-controller .guidat-slider-bg:hover, .guidat-controller.active .guidat-slider-bg { background-color: #444; } .guidat-controller .guidat-slider-bg .guidat-slider-fg:hover, .guidat-controller.active .guidat-slider-bg .guidat-slider-fg { background-color: #52c8ff; } .guidat-slider-bg { background-color: #222; cursor: ew-resize; width: 40%; margin-top: 2px; float: right; height: 21px; } .guidat-slider-fg { cursor: ew-resize; background-color: #00aeff; height: 21px; } "; 54 | -------------------------------------------------------------------------------- /js/lib/RAF.js: -------------------------------------------------------------------------------- 1 | // Shim courtesy of Paul Irish & Jerome Etienne 2 | window.cancelRequestAnimFrame = ( function() { 3 | return window.cancelAnimationFrame || 4 | window.webkitCancelRequestAnimationFrame || 5 | window.mozCancelRequestAnimationFrame || 6 | window.oCancelRequestAnimationFrame || 7 | window.msCancelRequestAnimationFrame || 8 | clearTimeout 9 | } )(); 10 | 11 | window.requestAnimFrame = (function(){ 12 | return window.requestAnimationFrame || 13 | window.webkitRequestAnimationFrame || 14 | window.mozRequestAnimationFrame || 15 | window.oRequestAnimationFrame || 16 | window.msRequestAnimationFrame || 17 | function(/* function */ callback, /* DOMElement */ element){ 18 | return window.setTimeout(callback, 1000 / 60); 19 | }; 20 | })(); -------------------------------------------------------------------------------- /js/recursion.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Copyright (C) 2011 by Justin Windle 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 13 | * all 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 21 | * THE SOFTWARE. 22 | */ 23 | 24 | /** 25 | * -------------------- 26 | * SETTINGS 27 | * -------------------- 28 | */ 29 | 30 | var CONFIG = {}; 31 | var PRESETS = {}; 32 | var RENDER_MODES = { 33 | Darkness: 'darkness', 34 | Segmented: 'segmented', 35 | Sketched: 'sketched' 36 | }; 37 | 38 | PRESETS['Vines'] = {RENDER_MODE:RENDER_MODES.Darkness,BRANCH_PROBABILITY:0.2572,MAX_CONCURRENT:388,NUM_BRANCHES:4,MIN_RADIUS:0.1,MAX_RADIUS:69,MIN_WANDER_STEP:1.0184,MAX_WANDER_STEP:0.1702,MIN_GROWTH_RATE:10.6214,MAX_GROWTH_RATE:11.8251,MIN_SHRINK_RATE:0.99656,MAX_SHRINK_RATE:0.91265,MIN_DIVERGENCE:0.5101,MAX_DIVERGENCE:0.37466}; 39 | PRESETS['Fibrous'] = {RENDER_MODE:RENDER_MODES.Segmented,BRANCH_PROBABILITY:0.05,MAX_CONCURRENT:800,NUM_BRANCHES:3,MIN_RADIUS:0.1,MAX_RADIUS:50,MIN_WANDER_STEP:0.28,MAX_WANDER_STEP:0.7,MIN_GROWTH_RATE:5,MAX_GROWTH_RATE:9,MIN_SHRINK_RATE:0.98,MAX_SHRINK_RATE:0.99,MIN_DIVERGENCE:0.01,MAX_DIVERGENCE:0.05}; 40 | PRESETS['Graffiti'] = {RENDER_MODE:RENDER_MODES.Sketched,BRANCH_PROBABILITY:0.05,MAX_CONCURRENT:500,NUM_BRANCHES:6,MIN_RADIUS:0.15,MAX_RADIUS:70,MIN_WANDER_STEP:0.1197,MAX_WANDER_STEP:1.8269,MIN_GROWTH_RATE:13.66,MAX_GROWTH_RATE:17.35,MIN_SHRINK_RATE:0.95,MAX_SHRINK_RATE:0.98,MIN_DIVERGENCE:1.3268,MAX_DIVERGENCE:1.3885}; 41 | PRESETS['Knarled'] = {RENDER_MODE:RENDER_MODES.Darkness,BRANCH_PROBABILITY:0.09,MAX_CONCURRENT:500,NUM_BRANCHES:5,MIN_RADIUS:0.1,MAX_RADIUS:100,MIN_WANDER_STEP:0.1,MAX_WANDER_STEP:0.2,MIN_GROWTH_RATE:3.7,MAX_GROWTH_RATE:10,MIN_SHRINK_RATE:0.97,MAX_SHRINK_RATE:0.99,MIN_DIVERGENCE:0.01,MAX_DIVERGENCE:0.05}; 42 | PRESETS['Beech Tree'] = {RENDER_MODE:RENDER_MODES.Darkness,BRANCH_PROBABILITY:0.085,MAX_CONCURRENT:500,NUM_BRANCHES:1,MIN_RADIUS:0.1,MAX_RADIUS:40,MIN_WANDER_STEP:0.1599,MAX_WANDER_STEP:0.4,MIN_GROWTH_RATE:8,MAX_GROWTH_RATE:15,MIN_SHRINK_RATE:0.98,MAX_SHRINK_RATE:0.982,MIN_DIVERGENCE:0.31,MAX_DIVERGENCE:0.87}; 43 | PRESETS['Frost'] = {RENDER_MODE:RENDER_MODES.Sketched,BRANCH_PROBABILITY:0.09,MAX_CONCURRENT:1000,NUM_BRANCHES:6,MIN_RADIUS:0.1,MAX_RADIUS:40,MIN_WANDER_STEP:0,MAX_WANDER_STEP:0,MIN_GROWTH_RATE:9.2,MAX_GROWTH_RATE:9.8,MIN_SHRINK_RATE:0.97,MAX_SHRINK_RATE:0.97,MIN_DIVERGENCE:0.4,MAX_DIVERGENCE:0.8}; 44 | PRESETS['Wooly'] = {RENDER_MODE:RENDER_MODES.Segmented,BRANCH_PROBABILITY:0.07,MAX_CONCURRENT:348,NUM_BRANCHES:9,MIN_RADIUS:1.5,MAX_RADIUS:99,MIN_WANDER_STEP:0.5093,MAX_WANDER_STEP:2.654,MIN_GROWTH_RATE:7.8279,MAX_GROWTH_RATE:18.2956,MIN_SHRINK_RATE:0.94489,MAX_SHRINK_RATE:0.98716,MIN_DIVERGENCE:1.4656,MAX_DIVERGENCE:2.6998}; 45 | PRESETS['Vegetable Root'] = {RENDER_MODE:RENDER_MODES.Darkness,BRANCH_PROBABILITY:0.06,MAX_CONCURRENT:437,NUM_BRANCHES:1,MIN_RADIUS:0.1,MAX_RADIUS:100,MIN_WANDER_STEP:0.05,MAX_WANDER_STEP:0.25,MIN_GROWTH_RATE:5,MAX_GROWTH_RATE:9,MIN_SHRINK_RATE:0.98,MAX_SHRINK_RATE:0.99,MIN_DIVERGENCE:0,MAX_DIVERGENCE:0.1}; 46 | PRESETS['Hairball'] = {RENDER_MODE:RENDER_MODES.Sketched,BRANCH_PROBABILITY:0.6,MAX_CONCURRENT:800,MAX_DIVERGENCE:2.1,MAX_GROWTH_RATE:4.5,MAX_RADIUS:30,MAX_SHRINK_RATE:0.992,MAX_WANDER_STEP:0.2,MIN_DIVERGENCE:2,MIN_GROWTH_RATE:3.5,MIN_RADIUS:0.5,MIN_SHRINK_RATE:0.992,MIN_WANDER_STEP:0.1,NUM_BRANCHES:7}; 47 | PRESETS['Intenstines'] = {RENDER_MODE:RENDER_MODES.Darkness,BRANCH_PROBABILITY:1,MAX_CONCURRENT:350,NUM_BRANCHES:3,MIN_RADIUS:0.1,MAX_RADIUS:100,MIN_WANDER_STEP:0.1,MAX_WANDER_STEP:0.72,MIN_GROWTH_RATE:0.9,MAX_GROWTH_RATE:6.15,MIN_SHRINK_RATE:0.935,MAX_SHRINK_RATE:0.999,MIN_DIVERGENCE:0.01,MAX_DIVERGENCE:0.05}; 48 | 49 | function configure(settings) { 50 | for(var prop in settings) { 51 | CONFIG[prop] = settings[prop]; 52 | } 53 | } 54 | 55 | configure(PRESETS['Vines']); 56 | 57 | /** 58 | * -------------------- 59 | * UTILS 60 | * -------------------- 61 | */ 62 | 63 | var PI = Math.PI; 64 | var TWO_PI = Math.PI * 2; 65 | var HALF_PI = Math.PI / 2; 66 | var BRANCHES = []; 67 | 68 | function random(min, max) { 69 | return min + Math.random() * (max - min); 70 | } 71 | 72 | /** 73 | * -------------------- 74 | * BRANCH 75 | * -------------------- 76 | */ 77 | 78 | var Branch = function(x, y, theta, radius, scale, generation) { 79 | 80 | this.x = x; 81 | this.y = y; 82 | this.ox = x; 83 | this.oy = y; 84 | this.x1 = NaN; 85 | this.x2 = NaN; 86 | this.y1 = NaN; 87 | this.y2 = NaN; 88 | this.scale = scale || 1.0; 89 | this.theta = theta; 90 | this.oTheta = theta; 91 | this.radius = radius; 92 | this.generation = generation || 1; 93 | this.growing = true; 94 | this.age = 0; 95 | 96 | this.wanderStep = random(CONFIG.MIN_WANDER_STEP, CONFIG.MAX_WANDER_STEP); 97 | this.growthRate = random(CONFIG.MIN_GROWTH_RATE, CONFIG.MAX_GROWTH_RATE); 98 | this.shrinkRate = random(CONFIG.MIN_SHRINK_RATE, CONFIG.MAX_SHRINK_RATE); 99 | } 100 | 101 | Branch.prototype = { 102 | 103 | update: function() { 104 | 105 | if(this.growing) { 106 | 107 | this.ox = this.x; 108 | this.oy = this.y; 109 | this.oTheta = this.theta; 110 | 111 | this.theta += random(-this.wanderStep, this.wanderStep); 112 | 113 | this.x += Math.cos(this.theta) * this.growthRate * this.scale; 114 | this.y += Math.sin(this.theta) * this.growthRate * this.scale; 115 | 116 | this.scale *= this.shrinkRate; 117 | 118 | // Branch 119 | if(BRANCHES.length < CONFIG.MAX_CONCURRENT && Math.random() < CONFIG.BRANCH_PROBABILITY) { 120 | 121 | var offset = random(CONFIG.MIN_DIVERGENCE, CONFIG.MAX_DIVERGENCE); 122 | var theta = this.theta + offset * (Math.random() < 0.5 ? 1 : -1); 123 | var scale = this.scale * 0.95; 124 | var radius = this.radius * scale; 125 | var branch = new Branch(this.x, this.y, theta, radius, scale); 126 | 127 | branch.generation = this.generation + 1; 128 | 129 | BRANCHES.push(branch); 130 | } 131 | 132 | // Limit 133 | if(this.radius * this.scale <= CONFIG.MIN_RADIUS) { 134 | this.growing = false; 135 | } 136 | 137 | this.age++; 138 | } 139 | }, 140 | 141 | render: function(context) { 142 | 143 | if(this.growing) { 144 | 145 | var x1, x2, y1, y2; 146 | var scale = this.scale; 147 | var radius = this.radius * scale; 148 | 149 | context.save(); 150 | 151 | switch(CONFIG.RENDER_MODE) { 152 | 153 | case RENDER_MODES.Segmented : 154 | 155 | // Draw outline 156 | context.beginPath(); 157 | context.moveTo(this.ox, this.oy); 158 | context.lineTo(this.x, this.y); 159 | 160 | if(radius > 5.0) { 161 | context.shadowOffsetX = 1; 162 | context.shadowOffsetY = 1; 163 | context.shadowBlur = scale; 164 | context.shadowColor = 'rgba(0,0,0,0.05)'; 165 | } 166 | 167 | context.lineWidth = radius + scale; 168 | context.strokeStyle = '#000'; 169 | context.lineCap = 'round'; 170 | context.stroke(); 171 | context.closePath(); 172 | 173 | // Draw fill 174 | context.beginPath(); 175 | context.moveTo(this.ox, this.oy); 176 | context.lineTo(this.x, this.y); 177 | 178 | context.lineWidth = radius; 179 | context.strokeStyle = '#FFF'; 180 | context.lineCap = 'round'; 181 | context.stroke(); 182 | 183 | context.closePath(); 184 | 185 | break; 186 | 187 | case RENDER_MODES.Sketched : 188 | 189 | radius *= 0.5; 190 | radius += 0.5; 191 | 192 | x1 = this.x + Math.cos(this.theta - HALF_PI) * radius; 193 | x2 = this.x + Math.cos(this.theta + HALF_PI) * radius; 194 | 195 | y1 = this.y + Math.sin(this.theta - HALF_PI) * radius; 196 | y2 = this.y + Math.sin(this.theta + HALF_PI) * radius; 197 | 198 | context.lineWidth = 0.5 + scale; 199 | context.strokeStyle = '#000'; 200 | context.fillStyle = '#FFF'; 201 | context.lineCap = 'round'; 202 | 203 | // Starting point 204 | if(this.generation === 1 && this.age === 1) { 205 | context.beginPath(); 206 | context.arc(this.x, this.y, radius, 0, TWO_PI); 207 | context.stroke(); 208 | context.fill(); 209 | } 210 | 211 | // Draw sides 212 | if(this.age > 1) { 213 | context.beginPath(); 214 | context.moveTo(this.x1, this.y1); 215 | context.lineTo(x1, y1); 216 | context.moveTo(this.x2, this.y2); 217 | context.lineTo(x2, y2); 218 | context.stroke(); 219 | } 220 | 221 | // Draw ribbon 222 | context.beginPath(); 223 | context.moveTo(this.x1, this.y1); 224 | context.lineTo(x1, y1); 225 | context.lineTo(x2, y2); 226 | context.lineTo(this.x2, this.y2); 227 | context.closePath(); 228 | context.fill(); 229 | 230 | this.x1 = x1; 231 | this.x2 = x2; 232 | 233 | this.y1 = y1; 234 | this.y2 = y2; 235 | 236 | break; 237 | 238 | case RENDER_MODES.Darkness : 239 | 240 | radius *= 0.5; 241 | 242 | x1 = this.x + Math.cos(this.theta - HALF_PI) * radius; 243 | x2 = this.x + Math.cos(this.theta + HALF_PI) * radius; 244 | 245 | y1 = this.y + Math.sin(this.theta - HALF_PI) * radius; 246 | y2 = this.y + Math.sin(this.theta + HALF_PI) * radius; 247 | 248 | context.lineWidth = scale; 249 | context.strokeStyle = 'rgba(255,255,255,0.9)'; 250 | context.lineCap = 'round'; 251 | context.fillStyle = '#111'; 252 | 253 | // Starting point 254 | if(this.generation === 1 && this.age === 1) { 255 | context.beginPath(); 256 | context.arc(this.x, this.y, radius, 0, TWO_PI); 257 | context.closePath(); 258 | context.fill(); 259 | context.stroke(); 260 | } 261 | 262 | // Shadow 263 | if(scale > 0.05) { 264 | context.shadowOffsetX = scale; 265 | context.shadowOffsetY = scale; 266 | context.shadowBlur = scale; 267 | context.shadowColor = '#111'; 268 | } 269 | 270 | // Draw ribbon 271 | context.beginPath(); 272 | context.moveTo(this.x1, this.y1); 273 | context.lineTo(x1, y1); 274 | context.lineTo(x2, y2); 275 | context.lineTo(this.x2, this.y2); 276 | context.closePath(); 277 | context.fill(); 278 | 279 | // Draw sides 280 | if(this.age > 1 && scale > 0.1) { 281 | context.beginPath(); 282 | context.moveTo(this.x1, this.y1); 283 | context.lineTo(x1, y1); 284 | context.moveTo(this.x2, this.y2); 285 | context.lineTo(x2, y2); 286 | context.stroke(); 287 | } 288 | 289 | this.x1 = x1; 290 | this.x2 = x2; 291 | 292 | this.y1 = y1; 293 | this.y2 = y2; 294 | 295 | break; 296 | } 297 | 298 | context.restore(); 299 | } 300 | }, 301 | 302 | destroy: function() { 303 | 304 | } 305 | }; 306 | 307 | /** 308 | * -------------------- 309 | * SKETCH 310 | * -------------------- 311 | */ 312 | 313 | var Recursion = new function() { 314 | 315 | var started = false; 316 | var $canvas = $('#canvas'); 317 | var $branchCount = $('#output .branchCount'); 318 | var canvas = $canvas[0]; 319 | var context = canvas.getContext('2d'); 320 | 321 | function spawn(x, y) { 322 | 323 | var theta, radius; 324 | 325 | for(var i = 0; i < CONFIG.NUM_BRANCHES; i++) { 326 | theta = (i / CONFIG.NUM_BRANCHES) * TWO_PI; 327 | radius = CONFIG.MAX_RADIUS; 328 | BRANCHES.push(new Branch(x, y, theta - HALF_PI, radius)); 329 | } 330 | } 331 | 332 | function update() { 333 | 334 | //cancelRequestAnimFrame(update); 335 | requestAnimFrame(update); 336 | 337 | var i, n, branch; 338 | 339 | for(i = 0, n = BRANCHES.length; i < n; i++) { 340 | branch = BRANCHES[i]; 341 | branch.update(); 342 | branch.render(context); 343 | } 344 | 345 | // Strip dead branches 346 | for(i = BRANCHES.length - 1; i >= 0; i--) { 347 | if(!BRANCHES[i].growing) { 348 | BRANCHES.splice(i,1); 349 | } 350 | } 351 | 352 | var count = BRANCHES.length.toString(); 353 | while(count.length < 3) { count = '0' + count; } 354 | $branchCount.text('Branch count: ' + count); 355 | } 356 | 357 | function onClick(e) { 358 | 359 | Recursion.reset(); 360 | spawn(e.offsetX, e.offsetY); 361 | } 362 | 363 | function onResize(e) { 364 | 365 | var width = window.innerWidth; 366 | var height = window.innerHeight; 367 | var scale = window.devicePixelRatio || 1; 368 | canvas.width = width * scale; 369 | canvas.height = height * scale; 370 | canvas.style.width = width + 'px'; 371 | canvas.style.height = height + 'px'; 372 | context.scale(scale, scale); 373 | 374 | Recursion.reset(); 375 | spawn(window.innerWidth / 2, window.innerHeight / 2); 376 | } 377 | 378 | return { 379 | 380 | init: function() { 381 | 382 | onResize(); 383 | 384 | if(!started) { 385 | started = true; 386 | $(window).resize(onResize); 387 | $canvas.click(onClick); 388 | update(); 389 | } 390 | }, 391 | 392 | reset: function() { 393 | 394 | for(var i = 0, n = BRANCHES.length; i < n; i++) { 395 | BRANCHES[i].destroy(); 396 | } 397 | 398 | BRANCHES = []; 399 | }, 400 | 401 | save: function() { 402 | 403 | var image = canvas.toDataURL('image/png'); 404 | 405 | var win = window.open('about:blank', '_blank', 'width=1000,height=700'); 406 | 407 | var html = $(''); 408 | var head = $(''); 409 | var body = $(''); 410 | 411 | body.css({ 412 | background: '#f2f2f2', 413 | padding: 0, 414 | margin: 0 415 | }); 416 | 417 | head.append($('Recursion » Right Click & Save the Image Below')); 418 | body.append($('')); 419 | 420 | html.append(head); 421 | html.append(body); 422 | 423 | win.document.write('' + html.html()); 424 | win.document.close(); 425 | }, 426 | 427 | clear: function() { 428 | context.clearRect(0, 0, canvas.width, canvas.height); 429 | } 430 | }; 431 | } 432 | 433 | /** 434 | * -------------------- 435 | * GUI 436 | * -------------------- 437 | */ 438 | 439 | function saveConfig() { 440 | var config = []; 441 | for(var i in CONFIG) { config.push(i + ':' + CONFIG[i]); } 442 | console.log("PRESETS['__name__'] = {" + config.join(',') + "};"); 443 | } 444 | 445 | // Build preset map for GUI 446 | var preset = {key:''}, keys = {}; 447 | for(var i in PRESETS) { keys[i] = i; } 448 | 449 | function randomise() { 450 | CONFIG.BRANCH_PROBABILITY = random(0.01,1.0); 451 | CONFIG.MAX_CONCURRENT = random(10,1000); 452 | CONFIG.NUM_BRANCHES = random(1,20); 453 | CONFIG.MIN_RADIUS = random(0.1,2.0); 454 | CONFIG.MAX_RADIUS = random(CONFIG.MIN_RADIUS,100); 455 | CONFIG.MIN_WANDER_STEP = random(0.1,PI); 456 | CONFIG.MAX_WANDER_STEP = random(CONFIG.MIN_WANDER_STEP,PI); 457 | CONFIG.MIN_GROWTH_RATE = random(0.1,20); 458 | CONFIG.MAX_GROWTH_RATE = random(CONFIG.MIN_GROWTH_RATE,20); 459 | CONFIG.MIN_SHRINK_RATE = random(0.9,0.999); 460 | CONFIG.MAX_SHRINK_RATE = random(CONFIG.MIN_SHRINK_RATE,0.999); 461 | CONFIG.MIN_DIVERGENCE = random(0.0,PI); 462 | CONFIG.MAX_DIVERGENCE = random(CONFIG.MIN_DIVERGENCE,PI); 463 | Recursion.init(); 464 | GUI.listenAll(); 465 | } 466 | 467 | var GUI = new DAT.GUI({width: 340}); 468 | GUI.name('Recursion Settings'); 469 | 470 | // Config 471 | GUI.add(CONFIG, 'NUM_BRANCHES').name('Trunk Count').min(1).max(20).step(1); 472 | GUI.add(CONFIG, 'MAX_CONCURRENT').name('Max Concurrent').min(10).max(1000).step(1); 473 | GUI.add(CONFIG, 'BRANCH_PROBABILITY').name('Branch Probability').min(0.01).max(1.0).step(0.01); 474 | GUI.add(CONFIG, 'MIN_RADIUS').name('Radius (Min)').min(0.1).max(100); 475 | GUI.add(CONFIG, 'MAX_RADIUS').name('Radius (Max)').min(0.1).max(100); 476 | GUI.add(CONFIG, 'MIN_WANDER_STEP').name('Wander (Min)').min(0.0).max(PI).step(0.01); 477 | GUI.add(CONFIG, 'MAX_WANDER_STEP').name('Wander (Max)').min(0.0).max(PI).step(0.01); 478 | GUI.add(CONFIG, 'MIN_GROWTH_RATE').name('Growth (Min)').min(0.1).max(20).step(0.1); 479 | GUI.add(CONFIG, 'MAX_GROWTH_RATE').name('Growth (Max)').min(0.1).max(20).step(0.1); 480 | GUI.add(CONFIG, 'MIN_SHRINK_RATE').name('Shrink (Min)').min(0.9).max(0.999).step(0.005); 481 | GUI.add(CONFIG, 'MAX_SHRINK_RATE').name('Shrink (Max)').min(0.9).max(0.999).step(0.005); 482 | GUI.add(CONFIG, 'MIN_DIVERGENCE').name('Divergeence (Min)').min(0.0).max(PI).step(0.01); 483 | GUI.add(CONFIG, 'MAX_DIVERGENCE').name('Divergeence (Max)').min(0.0).max(PI).step(0.01); 484 | 485 | GUI.add(preset, 'key').name('Preset Behaviors').options(keys).onChange(function(){ 486 | configure(PRESETS[preset.key]); 487 | Recursion.init(); 488 | GUI.listenAll(); 489 | }); 490 | GUI.add(CONFIG, 'RENDER_MODE').name('Render Style').options(RENDER_MODES).onChange(Recursion.init); 491 | GUI.add(Recursion, 'save').name('Save as PNG'); 492 | GUI.add(Recursion, 'clear').name('Clear').onFire(Recursion.reset); 493 | GUI.add(Recursion, 'init').name('Clear & Regenerate'); 494 | GUI.close(); 495 | 496 | //GUI.add(this, 'randomise').name('Randomise'); 497 | //GUI.add(this, 'saveConfig').name('Save Config'); 498 | 499 | /** 500 | * -------------------- 501 | * INIT 502 | * -------------------- 503 | */ 504 | 505 | Recursion.init(); 506 | --------------------------------------------------------------------------------