├── 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 |
--------------------------------------------------------------------------------