├── .gitignore
├── LICENSE
├── README.md
├── css
└── main.css
├── js
├── generator.js
├── main.js
└── view.js
└── map.html
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | *.iml
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Rob Dawson
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 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,
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 THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Generative Pattern Maker
2 |
3 | Here's a web-based [generative pattern maker](https://codebox.net/pages/generative-patterns) that produces images like these:
4 |
5 |
6 |
7 |
8 |
9 | You can [use the online version of the tool to make your own patterns](https://codebox.net/html_raw/generative-patterns/index.html). It also works offline, so you can just download the code and open the HTML file in a web browser.
10 |
11 | Click the 'START' button to begin a new pattern. You can pause at any point, or just let the pattern run to completion. Running the 'PENCIL' tool produces a nice shading effect (shown in the images above). If you produce something beautiful you can save a copy using the 'DOWNLOAD' button.
12 |
13 | If you select the 'Continuous' option then each time a pattern is completed a new one is started automatically.
14 |
15 | Each pattern is based on a 'seed' number that can be used to reproduce the same pattern again. The three most recent seeds are shown at the bottom of the page. Clicking a seed number will generate the corresponding pattern again.
16 |
17 |
--------------------------------------------------------------------------------
/css/main.css:
--------------------------------------------------------------------------------
1 | html,body {
2 | width: 100%;
3 | height: 100%;
4 | margin: 0;
5 | }
6 | #container {
7 | display: flex;
8 | flex-direction: column;
9 | width: 100%;
10 | height: 100%
11 | }
12 | #canvas {
13 | flex: 1 1 0;
14 | border: 1px solid black;
15 | min-height: 0;
16 | }
17 | #canvas, #controls {
18 | margin: 5px;
19 | border: 1px solid black;
20 | }
21 | #controls {
22 | margin-top: -6px;
23 | padding: 10px;
24 | display: flex;
25 | flex-direction: row;
26 | align-items: center;
27 | }
28 | #controls button {
29 | border: 1px solid black;
30 | padding: 5px 10px;
31 | margin: 0 5px;
32 | cursor: pointer;
33 | border-radius: 3px;
34 | text-transform: uppercase;
35 | width: 100px;
36 | background-color: transparent;
37 | }
38 | #controls button:disabled {
39 | border: 1px solid grey;
40 | cursor: default;
41 | }
42 | .checkboxContainer, #seedList {
43 | display: flex;
44 | flex-direction: row;
45 | margin: 0 0 0 10px;
46 | }
47 | #seedList {
48 | margin: 0 0 0 20px;
49 | }
50 | #recentSeeds {
51 | margin: 0 10px;
52 | padding: 0;
53 | }
54 | #recentSeeds li {
55 | display: inline-block;
56 | margin-right: 5px;
57 | cursor: pointer;
58 | }
59 | #recentSeeds li:hover {
60 | text-decoration: underline;
61 | }
62 | #recentSeeds li:first-child {
63 | font-weight: bold;
64 | }
65 | #checkboxes {
66 | display: flex;
67 | flex-direction: row;
68 | }
69 | @media screen and (max-width:480px) {
70 | #controls {
71 | flex-direction: column;
72 | padding: 0px;
73 | }
74 | #controls button {
75 | width: 100%;
76 | border-radius: 0;
77 | padding:10px;
78 | }
79 | .checkboxContainer, #seedList {
80 | margin: 5px;
81 | }
82 | #checkboxes {
83 | margin: 5px;
84 | }
85 | #seedList {
86 | justify-content: center;
87 | width: 100%;
88 | align-items: center;
89 | margin: -5px 0 10px 0;
90 | }
91 | }
--------------------------------------------------------------------------------
/js/generator.js:
--------------------------------------------------------------------------------
1 | const generator = (() => {
2 | "use strict";
3 |
4 | function buildModel(config, rnd, collisionDetector) {
5 | let activeLineCount = 0,
6 | seeds;
7 |
8 | function buildLine(p0, angle, parent) {
9 | const growthRate = 1;
10 | const line = {
11 | p0,
12 | p1: {...p0},
13 | angle,
14 | generation: parent ? parent.generation + 1 : 0,
15 | parent,
16 | active: true,
17 | split: false,
18 | expired: false,
19 | steps: 0,
20 | rnd: rnd(),
21 | grow() {
22 | this.p1.x += Math.sin(angle) * growthRate;
23 | this.p1.y += Math.cos(angle) * growthRate;
24 | this.steps++;
25 | this.split = rnd() < config.pBifurcation;
26 | if (rnd() < this.generation * config.expiryThreshold) {
27 | this.expired = true;
28 | }
29 | },
30 | clip() {
31 | this.p1.x -= Math.sin(angle) * growthRate;
32 | this.p1.y -= Math.cos(angle) * growthRate;
33 | }
34 | };
35 | activeLineCount++;
36 | line.grow();
37 | return line;
38 | }
39 |
40 | function buildSeed() {
41 | const angle = rnd(0, Math.PI * 2);
42 | return {
43 | angle,
44 | lines: [buildLine({
45 | x: rnd(0, canvas.width),
46 | y: rnd(0, canvas.height),
47 | }, angle)],
48 | grow() {
49 | this.lines.filter(l=>l.active).forEach(line => {
50 | line.grow();
51 | if (line.expired) {
52 | line.active = false;
53 | activeLineCount--;
54 | return;
55 | }
56 | if (collisionDetector.checkForCollisions(line, model.forEachLineUntilTrue)) {
57 | line.clip();
58 | line.active = false;
59 | activeLineCount--;
60 | return;
61 | }
62 | if (line.split) {
63 | line.split = false;
64 | const newAngle = line.angle + Math.PI/2 * (rnd() < 0.5 ? 1 : -1);
65 | this.lines.push(buildLine({
66 | x: line.p1.x,
67 | y: line.p1.y,
68 | }, newAngle, line))
69 | }
70 | });
71 | }
72 | };
73 | }
74 |
75 | const model = {
76 | generate() {
77 | seeds = Array(config.seedCount).fill().map(buildSeed);
78 | },
79 | grow() {
80 | seeds.forEach(s => s.grow());
81 | },
82 | forEachLineUntilTrue(fn) {
83 | (seeds || []).some(seed => {
84 | return seed.lines.some(line => {
85 | return fn(line, config);
86 | })
87 | })
88 | },
89 | isActive() {
90 | return activeLineCount > 0;
91 | }
92 | };
93 |
94 | return model;
95 | }
96 |
97 | function randomFromSeed(seed) {
98 | // https://stackoverflow.com/a/47593316/138256
99 | function mulberry32() {
100 | var t = seed += 0x6D2B79F5;
101 | t = Math.imul(t ^ t >>> 15, t | 1);
102 | t ^= t + Math.imul(t ^ t >>> 7, t | 61);
103 | return ((t ^ t >>> 14) >>> 0) / 4294967296;
104 | }
105 |
106 | return function(a=1, b=0) {
107 | const min = b && a,
108 | max = b || a;
109 | return mulberry32() * (max - min) + min;
110 | }
111 | }
112 |
113 | function buildCollisionDetector(canvas) {
114 | const CLOCKWISE = 1, ANTICLOCKWISE = 2, COLINEAR = 0;
115 |
116 | function lineOffscreen(line) {
117 | return !canvas.isVisible(line.p1.x, line.p1.y);
118 | }
119 |
120 | function orientation(p, q, r) {
121 | const val = ((q.y - p.y) * (r.x - q.x)) - ((q.x - p.x) * (r.y - q.y));
122 | if (val > 0) {
123 | return CLOCKWISE;
124 | } else if (val < 0) {
125 | return ANTICLOCKWISE;
126 | }
127 | return COLINEAR;
128 | }
129 |
130 | function onSegment(p, q, r){
131 | return (q.x <= Math.max(p.x, r.x)) && (q.x >= Math.min(p.x, r.x)) && (q.y <= Math.max(p.y, r.y)) && (q.y >= Math.min(p.y, r.y));
132 | }
133 |
134 | function linesIntersect(l1, l2) {
135 | const o1 = orientation(l1.p0, l1.p1, l2.p0),
136 | o2 = orientation(l1.p0, l1.p1, l2.p1),
137 | o3 = orientation(l2.p0, l2.p1, l1.p0),
138 | o4 = orientation(l2.p0, l2.p1, l1.p1);
139 |
140 | if ((o1 != o2) && (o3 != o4)) {
141 | return true;
142 | }
143 |
144 | if ((o1 === COLINEAR) && onSegment(l1.p0, l2.p0, l1.p1)) {
145 | return true;
146 | }
147 |
148 | if ((o2 === COLINEAR) && onSegment(l1.p0, l2.p1, l1.p1)) {
149 | return true;
150 | }
151 |
152 | if ((o3 === COLINEAR) && onSegment(l2.p0, l1.p0, l2.p1)) {
153 | return true;
154 | }
155 |
156 | if ((o4 === COLINEAR) && onSegment(l2.p0, l1.p1, l2.p1)) {
157 | return true;
158 | }
159 |
160 | return false;
161 | }
162 |
163 | return {
164 | checkForCollisions(line1, forEachLineUntilTrue) {
165 | if (lineOffscreen(line1)) {
166 | return true;
167 | }
168 |
169 | let foundCollision = false;
170 | forEachLineUntilTrue(line2 => {
171 | if (line1 === line2 || line1.parent === line2 || line2.parent === line1) {
172 | return;
173 | }
174 | return foundCollision = linesIntersect(line1, line2);
175 | });
176 | return foundCollision;
177 | }
178 | };
179 | }
180 |
181 | function buildRandomConfig(rnd) {
182 | const useGradients = rnd() > 0.3;
183 |
184 | return {
185 | seedCount: Math.round(rnd(1, 10)),
186 | pBifurcation: rnd(0.02, 0.05),
187 | maxRectWidth: rnd(0,100),
188 | rectBaseHue: rnd(360),
189 | rectSaturation: rnd(20,100),
190 | rectHueVariation: rnd(100),
191 | rectAlpha: useGradients ? rnd(0.4,0.8) : rnd(0.1,0.4),
192 | rectLightness: rnd(20,70),
193 | expiryThreshold: rnd(0.001),
194 | lineDarkness: rnd(),
195 | pencilHorizontal: rnd() > 0.5,
196 | useGradients
197 | };
198 | }
199 |
200 | function createNewRender(seed, onFinished) {
201 | function update() {
202 | model.grow();
203 |
204 | if (model.isActive()) {
205 | view.canvas.clear();
206 | model.forEachLineUntilTrue((line, config) => {
207 | view.canvas.drawRect(line, Math.min(config.maxRectWidth, line.steps), `hsla(${(config.rectBaseHue + (line.rnd - 0.5) * config.rectHueVariation) % 360},${config.rectSaturation}%,${config.rectLightness}%,${config.rectAlpha})`, config.useGradients);
208 | });
209 | model.forEachLineUntilTrue((line, config) => {
210 | const lineColourValue = Math.round(config.lineDarkness * 100),
211 | lineColour = `rgb(${lineColourValue},${lineColourValue},${lineColourValue})`;
212 | view.canvas.drawLine(line, lineColour)
213 | });
214 | return false;
215 | }
216 | return true;
217 | }
218 |
219 | let model, rnd, config, stopRequested, collisionDetector;
220 |
221 | const render = {
222 | init() {
223 | stopRequested = false;
224 | rnd = randomFromSeed(seed);
225 | config = buildRandomConfig(rnd);
226 | collisionDetector = buildCollisionDetector(view.canvas);
227 | model = buildModel(config, rnd, collisionDetector);
228 | view.canvas.clear();
229 | model.generate();
230 | },
231 | start() {
232 | function doUpdate() {
233 | const isComplete = update();
234 |
235 | if (stopRequested) {
236 | stopRequested = false;
237 |
238 | } else if (isComplete) {
239 | onFinished();
240 |
241 | } else {
242 | requestAnimationFrame(doUpdate);
243 | }
244 | }
245 | doUpdate();
246 | },
247 | stop() {
248 | stopRequested = true;
249 | },
250 | applyPencil() {
251 | view.canvas.clear();
252 | model.forEachLineUntilTrue((line, config) => {
253 | if (!config.pencilHorizontal || Math.sin(line.angle)**2 < rnd()) {
254 | view.canvas.drawWithPencil(line, rnd(10, 100), {
255 | h: (config.rectBaseHue + (line.rnd - 0.5) * config.rectHueVariation) % 360,
256 | s: config.rectSaturation,
257 | l: config.rectLightness
258 | }, rnd);
259 | }
260 | });
261 | model.forEachLineUntilTrue((line, config) => {
262 | const lineColourValue = Math.round(config.lineDarkness * 100),
263 | lineColour = `rgb(${lineColourValue},${lineColourValue},${lineColourValue})`;
264 | view.canvas.drawLine(line, lineColour)
265 | });
266 | }
267 | };
268 | return render;
269 | }
270 |
271 | let render, onFinishedCurrentHandler = () => {};
272 |
273 | return {
274 | onFinishedCurrent(handler) {
275 | onFinishedCurrentHandler = handler;
276 | },
277 | startNew(seed=Date.now() & 0xfffff) {
278 | if (render) {
279 | render.stop();
280 | }
281 | render = createNewRender(seed, onFinishedCurrentHandler);
282 | render.init();
283 | render.start();
284 | return seed;
285 | },
286 | resume() {
287 | render.start();
288 | },
289 | pause() {
290 | render.stop();
291 | },
292 | applyPencil() {
293 | render.applyPencil();
294 | }
295 | };
296 |
297 | })();
298 |
--------------------------------------------------------------------------------
/js/main.js:
--------------------------------------------------------------------------------
1 | function init() {
2 | "use strict";
3 | view.init();
4 |
5 | function startNew(seed) {
6 | const newSeed = generator.startNew(seed);
7 | view.addSeed(newSeed);
8 | }
9 |
10 | view.onStart(startNew);
11 |
12 | view.onResume(() => {
13 | generator.resume();
14 | });
15 |
16 | view.onPause(() => {
17 | generator.pause();
18 | });
19 |
20 | view.onPencil(() => {
21 | generator.applyPencil();
22 | });
23 |
24 | view.onSeedClick(seed => {
25 | startNew(seed);
26 | });
27 |
28 | generator.onFinishedCurrent(() => {
29 | if (view.isContinuous()) {
30 | startNew();
31 | } else {
32 | view.setStopped();
33 | }
34 | });
35 | }
36 | init();
--------------------------------------------------------------------------------
/js/view.js:
--------------------------------------------------------------------------------
1 | const view = (() => {
2 | "use strict";
3 | const elPlayPause = document.getElementById('playPause'),
4 | elDownload = document.getElementById('download'),
5 | elContinuous = document.getElementById('continuous'),
6 | elPencil = document.getElementById('pencil'),
7 | elSeeds = document.getElementById('recentSeeds'),
8 | elCanvas = document.getElementById('canvas'),
9 |
10 | NO_OP = () => {},
11 | MAX_SEEDS = 3,
12 |
13 | STATE_INIT = 1,
14 | STATE_RUNNING = 2,
15 | STATE_PAUSED = 3,
16 | STATE_STOPPED = 4,
17 |
18 | viewModel = {};
19 |
20 | let onStartHandler, onResumeHandler, onPauseHandler, onSeedClickHandler, onPencilClickHandler;
21 |
22 | elPlayPause.onclick = () => {
23 | let handler, newState;
24 | if (viewModel.state === STATE_INIT || viewModel.state === STATE_STOPPED) {
25 | handler = onStartHandler || NO_OP;
26 | newState = STATE_RUNNING;
27 |
28 | } else if (viewModel.state === STATE_RUNNING) {
29 | handler = onPauseHandler || NO_OP;
30 | newState = STATE_PAUSED;
31 |
32 | } else if (viewModel.state === STATE_PAUSED) {
33 | handler = onResumeHandler || NO_OP;
34 | newState = STATE_RUNNING;
35 |
36 | } else {
37 | console.assert(false, 'Unexpected state: ' + viewModel.state);
38 | }
39 | viewModel.state = newState;
40 | updateFromModel();
41 | handler();
42 | };
43 |
44 | elContinuous.onclick = () => {
45 | viewModel.isContinuous = elContinuous.checked;
46 | };
47 |
48 | elPencil.onclick = () => {
49 | (onPencilClickHandler || NO_OP)();
50 | };
51 |
52 | elSeeds.onclick = e => {
53 | viewModel.state = STATE_RUNNING;
54 | (onSeedClickHandler || NO_OP)(Number(e.target.innerText));
55 | };
56 |
57 | elDownload.onclick = () => {
58 | const link = document.createElement('a');
59 | link.download = `${viewModel.seeds[0]}.png`;
60 | link.href = elCanvas.toDataURL();
61 | link.click();
62 | };
63 |
64 | function updateFromModel() {
65 | if (viewModel.state === STATE_RUNNING) {
66 | elPlayPause.innerText ='Pause';
67 | } else if (viewModel.state === STATE_PAUSED) {
68 | elPlayPause.innerText = 'Resume';
69 | } else {
70 | elPlayPause.innerText = 'Start';
71 | }
72 |
73 | elContinuous.checked = viewModel.isContinuous;
74 | elSeeds.innerHTML = viewModel.seeds.map(seed => `