├── sprawl.html └── sprawl.js /sprawl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Sprawl 8 | 9 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /sprawl.js: -------------------------------------------------------------------------------- 1 | const FRAME_RATE = 60; 2 | const G = 100; 3 | 4 | let activeStreets = []; 5 | let inactiveStreets = []; 6 | 7 | function setup() { 8 | createCanvas(windowWidth, windowHeight); 9 | frameRate(FRAME_RATE); 10 | 11 | activeStreets = [ 12 | new Street({ x: 0, y: 0, vx: 50, vy: 50, spawnEnergy: 50, angleRandomness: 0 }), 13 | new Street({ x: windowWidth * 0.4, y: windowHeight * 0.4, vx: -10, vy: -10, spawnEnergy: 20, angleRandomness: 1 }), 14 | new Street({ x: windowWidth * 0.4, y: windowHeight * 0.5, vx: -10, vy: 10, spawnEnergy: 20, angleRandomness: 1 }), 15 | new Street({ x: windowWidth * 0.5, y: windowHeight * 0.4, vx: 10, vy: -10, spawnEnergy: 20, angleRandomness: 1 }), 16 | new Street({ x: windowWidth * 0.5, y: windowHeight * 0.5, vx: 10, vy: 10, spawnEnergy: 20, angleRandomness: 1 }), 17 | new Street({ x: windowWidth * 0.9, y: windowHeight * 0.9, vx: -50, vy: 0, angleRandomness: 2 }), 18 | ]; 19 | } 20 | 21 | class Street { 22 | constructor({ parent, x, y, vx, vy, spawnEnergy = 10, angleRandomness = 0 }) { 23 | this.parent = parent; 24 | 25 | this.xStart = x; 26 | this.yStart = y; 27 | this.xEnd = x; 28 | this.yEnd = y; 29 | 30 | this.vx = vx; 31 | this.vy = vy; 32 | 33 | this.energy = 0; 34 | this.spawnEnergy = spawnEnergy; 35 | this.angleRandomness = angleRandomness; 36 | } 37 | 38 | step(dt) { 39 | const xStart = this.xEnd; 40 | const yStart = this.yEnd; 41 | const xEnd = (this.xEnd += this.vx * dt); 42 | const yEnd = (this.yEnd += this.vy * dt); 43 | return { xStart, yStart, xEnd, yEnd }; 44 | } 45 | 46 | crosses(other) { 47 | var aSide = (other.xStart - other.xEnd) * (this.yEnd - other.yEnd) - (other.yStart - other.yEnd) * (this.xEnd - other.xEnd) > 0; 48 | var bSide = (other.xStart - other.xEnd) * (this.yStart - other.yEnd) - (other.yStart - other.yEnd) * (this.xStart - other.xEnd) > 0; 49 | var cSide = (this.xStart - this.xEnd) * (other.yEnd - this.yEnd) - (this.yStart - this.yEnd) * (other.xEnd - this.xEnd) > 0; 50 | var dSide = (this.xStart - this.xEnd) * (other.yStart - this.yEnd) - (this.yStart - this.yEnd) * (other.xStart - this.xEnd) > 0; 51 | return aSide !== bSide && cSide !== dSide; 52 | } 53 | 54 | outOfBounds(x, y) { 55 | return this.xEnd < 0 || this.xEnd > x || this.yEnd < 0 || this.yEnd > y; 56 | } 57 | 58 | feed(dE) { 59 | this.energy += dE; 60 | 61 | let actions = []; 62 | 63 | if (this.energy > this.spawnEnergy) { 64 | this.energy -= this.spawnEnergy; 65 | 66 | const angle = HALF_PI + (Math.random() < 0.5 ? PI : 0) + (Math.random() * 0.1 - 0.05) * PI * this.angleRandomness; 67 | const vx = this.vx * Math.cos(angle) - this.vy * Math.sin(angle); 68 | const vy = this.vx * Math.sin(angle) - this.vy * Math.cos(angle); 69 | 70 | actions.push({ 71 | action: "spawn", 72 | street: new Street({ 73 | x: this.xEnd, 74 | y: this.yEnd, 75 | vx, 76 | vy, 77 | parent: this, 78 | spawnEnergy: this.spawnEnergy, 79 | angleRandomness: this.angleRandomness, 80 | }), 81 | }); 82 | } 83 | 84 | return actions; 85 | } 86 | } 87 | 88 | function draw() { 89 | let stillActive = []; 90 | let deactivated = []; 91 | let spawned = []; 92 | 93 | if (activeStreets.length === 0) { 94 | noLoop(); 95 | } 96 | 97 | for (const i in activeStreets) { 98 | const s = activeStreets[i]; 99 | 100 | const ds = s.step(1 / FRAME_RATE); 101 | line(ds.xStart, ds.yStart, ds.xEnd, ds.yEnd); 102 | 103 | actions = s.feed(Math.random()); 104 | for (a of actions) { 105 | switch(a.action) { 106 | case "spawn": 107 | spawned.push(a.street); 108 | break; 109 | } 110 | } 111 | 112 | let deactivate = false; 113 | // Out of bounds check 114 | if (s.outOfBounds(windowWidth, windowHeight)) { 115 | deactivate = true; 116 | } 117 | // Collisions 118 | for (s2 of activeStreets) { 119 | // Don't collide with itself 120 | if (s === s2) { 121 | continue; 122 | } 123 | // Only one line should stop in collision 124 | if (s2.collidedWith === s) { 125 | continue; 126 | } 127 | // Don't collide with the parent or child 128 | if (s.parent === s2 || s2.parent === s) { 129 | continue; 130 | } 131 | // Check the collision 132 | if (s.crosses(s2)) { 133 | s.collidedWith = s2; 134 | deactivate = true; 135 | } 136 | } 137 | for (s2 of inactiveStreets) { 138 | // Don't collide with itself 139 | if (s === s2) { 140 | continue; 141 | } 142 | // Only one line should stop in collision 143 | if (s2.collidedWith === s) { 144 | continue; 145 | } 146 | // Don't collide with the parent or child 147 | if (s.parent === s2 || s2.parent === s) { 148 | continue; 149 | } 150 | // Check the collision 151 | if (s.crosses(s2)) { 152 | s.collidedWith = s2; 153 | deactivate = true; 154 | } 155 | } 156 | 157 | if (deactivate) { 158 | deactivated.push(s); 159 | } else { 160 | stillActive.push(s); 161 | } 162 | } 163 | 164 | activeStreets = stillActive.concat(spawned); 165 | inactiveStreets = inactiveStreets.concat(deactivated); 166 | } 167 | --------------------------------------------------------------------------------