├── .gitignore ├── README.md ├── source ├── line.js ├── index.html ├── vertex.js ├── index.js └── polygon.js ├── webpack.config.js ├── package.json └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log* 2 | node_modules 3 | *.DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Crystallization 2 | 3 | A JavaScript Canvas implementation of an old Flash experiment, exploring the recursive subdivision of polygons to form crystallisation like patterns. 4 | -------------------------------------------------------------------------------- /source/line.js: -------------------------------------------------------------------------------- 1 | // —————————————————————————————————————————————————— 2 | // Line 3 | // —————————————————————————————————————————————————— 4 | 5 | class Line { 6 | constructor(start, end) { 7 | this.start = start; 8 | this.end = end; 9 | } 10 | } 11 | 12 | // —————————————————————————————————————————————————— 13 | // Exports 14 | // —————————————————————————————————————————————————— 15 | 16 | export default Line; 17 | -------------------------------------------------------------------------------- /source/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 28 | 29 | 30 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 2 | var webpack = require('webpack'); 3 | var isDev = process.argv.indexOf('-p') === -1; 4 | var config = { 5 | context: __dirname + '/source', 6 | entry: { 7 | main: './index.js' 8 | }, 9 | output: { 10 | path: __dirname + '/deploy', 11 | filename: '[name]-[hash:6].js' 12 | }, 13 | module: { 14 | loaders: [{ 15 | test: /\.js$/, 16 | loader: 'babel', 17 | exclude: /(node_modules)/, 18 | query: { 19 | presets: ['es2015', 'stage-0'] 20 | } 21 | }] 22 | }, 23 | resolve: { 24 | extensions: ['', '.js'] 25 | }, 26 | plugins: [ 27 | new HtmlWebpackPlugin({ 28 | template: 'index.html' 29 | }) 30 | ] 31 | }; 32 | if (isDev) { 33 | config.devtool = 'eval-source-map'; 34 | } else { 35 | config.plugins.push( 36 | new webpack.optimize.AggressiveMergingPlugin(), 37 | new webpack.optimize.UglifyJsPlugin({ 38 | compress: { warnings: false } 39 | }), 40 | new webpack.optimize.DedupePlugin() 41 | ); 42 | } 43 | module.exports = config; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crystallization", 3 | "version": "1.0.0", 4 | "description": "Crystallization", 5 | "main": "index.js", 6 | "scripts": { 7 | "clean": "rm -rf deploy", 8 | "build": "npm run clean && webpack -p --env production", 9 | "start": "webpack-dev-server --hot --inline --port 3000", 10 | "commit_deploy": "git add deploy -A && git commit -m \"deploy files at `date`\" -n > /dev/null 2>&1; exit 0", 11 | "ghpages": "git subtree split --prefix deploy/ -b gh-pages && git push -f origin gh-pages:gh-pages && git branch -D gh-pages", 12 | "deploy": "npm run build && npm run commit_deploy && npm run ghpages" 13 | }, 14 | "author": "Justin Windle ", 15 | "license": "ISC", 16 | "devDependencies": { 17 | "babel-core": "^6.18.2", 18 | "babel-loader": "^6.2.8", 19 | "babel-preset-es2015": "^6.18.0", 20 | "babel-preset-stage-0": "^6.16.0", 21 | "html-webpack-plugin": "^2.24.1", 22 | "webpack": "^1.13.3", 23 | "webpack-dev-server": "^1.16.2" 24 | }, 25 | "dependencies": { 26 | "sketch-js": "^1.1.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /source/vertex.js: -------------------------------------------------------------------------------- 1 | // —————————————————————————————————————————————————— 2 | // Vertex 3 | // —————————————————————————————————————————————————— 4 | 5 | class Vertex { 6 | constructor(x = 0, y = 0) { 7 | this.x = x; 8 | this.y = y; 9 | } 10 | distanceSq(other) { 11 | const dx = other.x - this.x; 12 | const dy = other.y - this.y; 13 | return dx * dx + dy * dy; 14 | } 15 | distance(other) { 16 | const dx = other.x - this.x; 17 | const dy = other.y - this.y; 18 | return Math.sqrt(dx * dx + dy * dy); 19 | } 20 | angle(other) { 21 | const dx = other.x - this.x; 22 | const dy = other.y - this.y; 23 | return Math.atan2(dy, dx); 24 | } 25 | lerp(other, amount) { 26 | const dx = other.x - this.x; 27 | const dy = other.y - this.y; 28 | return new Vertex(this.x + dx * amount, this.y + dy * amount); 29 | } 30 | clone() { 31 | return new Vertex(this.x, this.y); 32 | } 33 | } 34 | 35 | // —————————————————————————————————————————————————— 36 | // Exports 37 | // —————————————————————————————————————————————————— 38 | 39 | export default Vertex; 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 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 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 | -------------------------------------------------------------------------------- /source/index.js: -------------------------------------------------------------------------------- 1 | // —————————————————————————————————————————————————— 2 | // Dependencies 3 | // —————————————————————————————————————————————————— 4 | 5 | import Polygon from './polygon'; 6 | import Vertex from './vertex'; 7 | import Sketch from 'sketch-js'; 8 | 9 | // —————————————————————————————————————————————————— 10 | // Main 11 | // —————————————————————————————————————————————————— 12 | 13 | const Crystallisation = Sketch.create({ 14 | // Sketch settings 15 | container: document.getElementById('container'), 16 | autoclear: false, 17 | retina: 'auto', 18 | // Configurable 19 | settings: { 20 | iterations: 50, 21 | randomness: 0.25, 22 | opposite: 0.1, 23 | minAngle: 0.4, 24 | minSide: 2 25 | }, 26 | // Custom properties 27 | polygons: [], 28 | lines: [], 29 | // Setup 30 | setup() { 31 | this.reset(); 32 | }, 33 | // Subdivide next polygon 34 | step() { 35 | // Choose a polygon to subdivide 36 | const index = ~~random(this.polygons.length - 1); 37 | // Subdivide the polygon 38 | const slices = this.polygons[index].subdivide( 39 | this.settings.randomness, 40 | this.settings.opposite 41 | ); 42 | // Check whether all slices are usable 43 | let drop = false; 44 | let i, slice, n = slices.length; 45 | for (i = 0; i < n; i++) { 46 | slice = slices[i]; 47 | if (slice.minAngle() < this.settings.minAngle) { 48 | drop = true; 49 | break; 50 | } 51 | } 52 | for (i = 0; i < n; i++) { 53 | slice = slices[i]; 54 | if (slice.minSide() < this.settings.minSide) { 55 | drop = true; 56 | break; 57 | } 58 | } 59 | // If all slices are usable 60 | if (!drop) { 61 | // Remember unique lines from the chosen polygons 62 | this.polygons[index].unique.forEach(line => this.lines.push(line)); 63 | // Draw and store the slices 64 | // TODO move to top level 65 | 66 | slices.forEach(slice => { 67 | this.polygons.push(slice); 68 | slice.draw(this); 69 | }); 70 | this.fill(); 71 | this.stroke(); 72 | // Remove the original polygon 73 | this.polygons.splice(index, 1); 74 | } 75 | }, 76 | // Clears the canvas 77 | clear() { 78 | this.clearRect(0, 0, this.canvas.width, this.canvas.height); 79 | }, 80 | // Clears polygons to a single rectangle 81 | reset() { 82 | // Initial bounding box points 83 | const a = new Vertex(0, 0); 84 | const b = new Vertex(this.width, 0); 85 | const c = new Vertex(this.width, this.height); 86 | const d = new Vertex(0, this.height); 87 | // Initial bounding polygon 88 | this.polygons = [new Polygon(a, b, c, d)]; 89 | // Clear stored lines 90 | this.lines = []; 91 | this.clear(); 92 | }, 93 | resize() { 94 | this.strokeStyle = '#333'; 95 | this.fillStyle = '#fcfcfc'; 96 | this.lineWidth = 0.25; 97 | }, 98 | // Toggle update loop 99 | toggle() { 100 | if (this.running) this.stop(); 101 | else this.start(); 102 | }, 103 | // Sketch.js update loop 104 | draw() { 105 | for (let i = 0; i < this.settings.iterations; i++) { 106 | this.step(); 107 | } 108 | }, 109 | // Save output as an image 110 | export() { 111 | window.open(this.canvas.toDataURL(), 'Crystallisation'); 112 | } 113 | }); 114 | 115 | const gui = new dat.GUI(); 116 | gui.add(Crystallisation.settings, 'minSide').min(0).max(100).name('Min Side Length'); 117 | gui.add(Crystallisation.settings, 'minAngle').min(0.0).max(1.2).step(0.01).name('Min Angle (rad)'); 118 | gui.add(Crystallisation.settings, 'iterations').min(1).max(100).name('Iterations'); 119 | gui.add(Crystallisation.settings, 'randomness').min(0.0).max(1.0).step(0.01).name('Randomness'); 120 | gui.add(Crystallisation.settings, 'opposite').min(0.0).max(1.0).step(0.01).name('Opposite Sides'); 121 | gui.add(Crystallisation, 'toggle').name('Start / Stop'); 122 | gui.add(Crystallisation, 'reset').name('Reset Polygons'); 123 | gui.add(Crystallisation, 'clear').name('Clear Canvas'); 124 | gui.add(Crystallisation, 'export').name('Save'); 125 | gui.close(); 126 | -------------------------------------------------------------------------------- /source/polygon.js: -------------------------------------------------------------------------------- 1 | // —————————————————————————————————————————————————— 2 | // Dependencies 3 | // —————————————————————————————————————————————————— 4 | 5 | import Vertex from './vertex'; 6 | import Line from './line'; 7 | 8 | // —————————————————————————————————————————————————— 9 | // Helpers 10 | // —————————————————————————————————————————————————— 11 | 12 | // Cosine rule (SSS) a^2 = b^2 + c^2 - 2bc cos A 13 | const SSS = (a, b, c) => Math.acos(a * a + b * b - c * c) / (2 * a * b); 14 | 15 | // —————————————————————————————————————————————————— 16 | // Polygon 17 | // —————————————————————————————————————————————————— 18 | 19 | class Polygon { 20 | constructor(...vertices) { 21 | this.vertices = vertices; 22 | // Assign principal generation 23 | this.generation = 0; 24 | // unique lines 25 | this.unique = []; 26 | } 27 | // Subdivides this polygon into 2 and returns both 28 | subdivide(randomness = 0.0, opposite = 0.5) { 29 | let i1, i2, j1, j2, l1, l2, nv, p1, p2, v1, v2; 30 | // Current number of sides 31 | nv = this.vertices.length; 32 | // Choose two unique indices 33 | i1 = ~~random(nv); 34 | i2 = random() < opposite ? (~~(i1 + nv / 2) % nv) : ~~random(nv); 35 | while (i2 === i1) { i2 = ~~random(nv); } 36 | // Choose lerp points 37 | l1 = 0.5 + random(randomness * -0.5, randomness * 0.5); 38 | l2 = 0.5 + random(randomness * -0.5, randomness * 0.5); 39 | // Create new vertices as linear interpolations between adjacent 40 | v1 = this.vertices[i1].lerp(this.vertices[(i1 + 1) % nv], l1); 41 | v2 = this.vertices[i2].lerp(this.vertices[(i2 + 1) % nv], l2); 42 | // Winding iterators 43 | [j1, j2] = [i1, i2]; 44 | // First polygon winds clockwise from v1 to v2 45 | p1 = new Polygon(v1); 46 | while (j1 !== i2) { 47 | p1.vertices.push(this.vertices[j1 = (j1 + 1) % nv]); 48 | } 49 | p1.vertices.push(v2); 50 | // Second polygon winds clockwise from v2 to v1 51 | p2 = new Polygon(v2); 52 | while (j2 !== i1) { 53 | p2.vertices.push(this.vertices[j2 = (j2 + 1) % nv]); 54 | } 55 | p2.vertices.push(v1); 56 | // Increment generations 57 | p1.generation = this.generation + 1; 58 | p2.generation = this.generation + 1; 59 | // Store this unique line only 60 | p1.unique.push(new Line(v1, v2)); 61 | // Return new polygons 62 | return [p1, p2]; 63 | } 64 | // Computes the centroid point 65 | centroid() { 66 | // Centroid points 67 | let cx = 0.0; 68 | let cy = 0.0; 69 | // Sum all vertices 70 | this.vertices.forEach(vertex => { 71 | cx += vertex.x; 72 | cy += vertex.y; 73 | }); 74 | // Take an average as the centroid 75 | return new Vertex( 76 | cx / this.vertices.length, 77 | cy / this.vertices.length 78 | ); 79 | } 80 | // Computes the minimum angle between vertices 81 | minAngle() { 82 | let val = Number.MAX_VALUE; 83 | let len = this.vertices.length; 84 | // Build triangles from vertices 85 | let prev, next, a, b, c, A, B, C; 86 | this.vertices.forEach((vertex, index) => { 87 | // Next / previous vertices in loop 88 | prev = this.vertices[(index - 1 + len) % len]; 89 | next = this.vertices[(index + 1 + len) % len]; 90 | // Compute triangle sides 91 | a = prev.distance(vertex); 92 | b = next.distance(vertex); 93 | c = next.distance(prev); 94 | // Compute angles 95 | A = SSS(b, c, a); 96 | B = SSS(c, a, b); 97 | C = Math.PI - A - B; 98 | // store the lowest 99 | val = min(val, C); 100 | }); 101 | return val; 102 | } 103 | // Computes the minimum side length 104 | minSide() { 105 | let side = Number.MAX_VALUE; 106 | let prev = this.vertices[this.vertices.length - 1]; 107 | this.vertices.forEach(vertex => { 108 | side = Math.min(side, vertex.distanceSq(prev)); 109 | prev = vertex; 110 | }); 111 | return Math.sqrt(side); 112 | } 113 | // Computes the perimeter of this polygon 114 | perimeter() { 115 | let first = this.vertices[0]; 116 | let last = this.vertices[this.vertices.length - 1]; 117 | let result = last.distance(first); 118 | this.vertices.reduce((a, b) => result += a.distance(b), b); 119 | return result; 120 | } 121 | // Draws this polygon to a given context 122 | draw(context) { 123 | context.beginPath(); 124 | this.vertices.forEach(({ x, y }, index) => { 125 | if (index === 0) { 126 | context.moveTo(x, y); 127 | } else { 128 | context.lineTo(x, y); 129 | } 130 | }); 131 | context.closePath(); 132 | } 133 | } 134 | 135 | // —————————————————————————————————————————————————— 136 | // Exports 137 | // —————————————————————————————————————————————————— 138 | 139 | export default Polygon; 140 | --------------------------------------------------------------------------------